Browse Source

Merge pull request #53 from andrewdwallo/development-3.x

Refactor app and reporting
3.x
Andrew Wallo 1 year ago
parent
commit
339300e640
No account linked to committer's email address
80 changed files with 3067 additions and 1727 deletions
  1. 0
    9
      app/Contracts/AccountHandler.php
  2. 21
    0
      app/Contracts/ExportableReport.php
  3. 1
    2
      app/DTO/AccountBalanceDTO.php
  4. 1
    2
      app/DTO/AccountCategoryDTO.php
  5. 1
    2
      app/DTO/AccountDTO.php
  6. 17
    0
      app/DTO/ReportCategoryDTO.php
  7. 5
    3
      app/DTO/ReportDTO.php
  8. 3
    9
      app/Events/CompanyConfigured.php
  9. 3
    6
      app/Events/CompanyDefaultEvent.php
  10. 4
    9
      app/Events/CompanyDefaultUpdated.php
  11. 7
    18
      app/Events/CompanyGenerated.php
  12. 5
    15
      app/Events/CurrencyRateChanged.php
  13. 3
    6
      app/Events/DefaultCurrencyChanged.php
  14. 5
    12
      app/Events/PlaidSuccess.php
  15. 6
    15
      app/Events/StartTransactionImport.php
  16. 0
    6
      app/Facades/Accounting.php
  17. 2
    2
      app/Filament/Company/Clusters/Settings/Pages/Invoice.php
  18. 2
    2
      app/Filament/Company/Clusters/Settings/Pages/Localization.php
  19. 1
    1
      app/Filament/Company/Clusters/Settings/Resources/CurrencyResource/Pages/CreateCurrency.php
  20. 2
    2
      app/Filament/Company/Clusters/Settings/Resources/CurrencyResource/Pages/EditCurrency.php
  21. 1
    1
      app/Filament/Company/Clusters/Settings/Resources/DiscountResource/Pages/CreateDiscount.php
  22. 1
    1
      app/Filament/Company/Clusters/Settings/Resources/DiscountResource/Pages/EditDiscount.php
  23. 1
    1
      app/Filament/Company/Clusters/Settings/Resources/TaxResource/Pages/CreateTax.php
  24. 1
    1
      app/Filament/Company/Clusters/Settings/Resources/TaxResource/Pages/EditTax.php
  25. 12
    0
      app/Filament/Company/Pages/Accounting/AccountChart.php
  26. 38
    14
      app/Filament/Company/Pages/Accounting/Transactions.php
  27. 6
    6
      app/Filament/Company/Pages/CreateCompany.php
  28. 2
    1
      app/Filament/Company/Pages/Reports.php
  29. 70
    113
      app/Filament/Company/Pages/Reports/AccountBalances.php
  30. 249
    0
      app/Filament/Company/Pages/Reports/BaseReportPage.php
  31. 92
    0
      app/Filament/Company/Pages/Reports/TrialBalance.php
  32. 9
    4
      app/Filament/Company/Resources/Banking/AccountResource.php
  33. 1
    1
      app/Filament/Company/Resources/Banking/AccountResource/Pages/CreateAccount.php
  34. 2
    2
      app/Filament/Company/Resources/Banking/AccountResource/Pages/EditAccount.php
  35. 1
    1
      app/Filament/Company/Resources/Core/DepartmentResource/Pages/CreateDepartment.php
  36. 2
    2
      app/Filament/Company/Resources/Core/DepartmentResource/Pages/EditDepartment.php
  37. 4
    4
      app/Http/Controllers/PlaidWebhookController.php
  38. 7
    21
      app/Jobs/ProcessTransactionImport.php
  39. 5
    13
      app/Jobs/ProcessTransactionUpdate.php
  40. 1
    2
      app/Listeners/ConfigureChartOfAccounts.php
  41. 5
    8
      app/Listeners/CreateConnectedAccount.php
  42. 3
    6
      app/Listeners/HandleTransactionImport.php
  43. 3
    4
      app/Listeners/UpdateCurrencyRates.php
  44. 6
    7
      app/Models/Accounting/Account.php
  45. 0
    6
      app/Models/Accounting/JournalEntry.php
  46. 2
    3
      app/Models/Banking/BankAccount.php
  47. 3
    28
      app/Models/Banking/Institution.php
  48. 11
    4
      app/Observers/AccountObserver.php
  49. 1
    3
      app/Providers/CurrencyServiceProvider.php
  50. 0
    88
      app/Providers/EventServiceProvider.php
  51. 1
    4
      app/Providers/SquireServiceProvider.php
  52. 0
    1
      app/Repositories/Banking/ConnectedBankAccountRepository.php
  53. 0
    76
      app/Services/AccountBalancesExportService.php
  54. 15
    104
      app/Services/AccountService.php
  55. 4
    9
      app/Services/ConnectedBankAccountService.php
  56. 8
    15
      app/Services/CurrencyService.php
  57. 64
    0
      app/Services/ExportService.php
  58. 47
    37
      app/Services/PlaidService.php
  59. 170
    0
      app/Services/ReportService.php
  60. 43
    0
      app/Support/Column.php
  61. 98
    0
      app/Transformers/AccountBalanceReportTransformer.php
  62. 52
    0
      app/Transformers/BaseReportTransformer.php
  63. 92
    0
      app/Transformers/TrialBalanceReportTransformer.php
  64. 4
    9
      app/ValueObjects/Money.php
  65. 4
    9
      app/View/Models/InvoiceViewModel.php
  66. 0
    1
      bootstrap/providers.php
  67. 2
    1
      composer.json
  68. 1290
    492
      composer.lock
  69. 14
    15
      database/migrations/2023_09_03_100000_create_accounting_tables.php
  70. 273
    263
      package-lock.json
  71. 7
    7
      package.json
  72. 3
    1
      resources/data/lang/en.json
  73. 0
    127
      resources/views/components/company/reports/account-balances.blade.php
  74. 148
    0
      resources/views/components/company/reports/report-pdf.blade.php
  75. 62
    0
      resources/views/components/company/tables/reports/detailed-report.blade.php
  76. 11
    3
      resources/views/filament/company/pages/accounting/chart.blade.php
  77. 0
    10
      resources/views/filament/company/pages/create-company.blade.php
  78. 0
    75
      resources/views/filament/company/pages/reports/account-balances.blade.php
  79. 25
    0
      resources/views/filament/company/pages/reports/detailed-report.blade.php
  80. 2
    2
      resources/views/livewire/company/service/connected-account/list-institutions.blade.php

+ 0
- 9
app/Contracts/AccountHandler.php View File

2
 
2
 
3
 namespace App\Contracts;
3
 namespace App\Contracts;
4
 
4
 
5
-use App\DTO\AccountBalanceDTO;
6
-use App\DTO\AccountBalanceReportDTO;
7
-use App\Enums\Accounting\AccountCategory;
8
 use App\Models\Accounting\Account;
5
 use App\Models\Accounting\Account;
9
 use App\ValueObjects\Money;
6
 use App\ValueObjects\Money;
10
 
7
 
20
 
17
 
21
     public function getEndingBalance(Account $account, string $startDate, string $endDate): ?Money;
18
     public function getEndingBalance(Account $account, string $startDate, string $endDate): ?Money;
22
 
19
 
23
-    public function calculateNetMovementByCategory(AccountCategory $category, int $debitBalance, int $creditBalance): int;
24
-
25
     public function getBalances(Account $account, string $startDate, string $endDate): array;
20
     public function getBalances(Account $account, string $startDate, string $endDate): array;
26
 
21
 
27
-    public function formatBalances(array $balances): AccountBalanceDTO;
28
-
29
-    public function buildAccountBalanceReport(string $startDate, string $endDate): AccountBalanceReportDTO;
30
-
31
     public function getTotalBalanceForAllBankAccounts(string $startDate, string $endDate): Money;
22
     public function getTotalBalanceForAllBankAccounts(string $startDate, string $endDate): Money;
32
 
23
 
33
     public function getAccountCategoryOrder(): array;
24
     public function getAccountCategoryOrder(): array;

+ 21
- 0
app/Contracts/ExportableReport.php View File

1
+<?php
2
+
3
+namespace App\Contracts;
4
+
5
+use App\DTO\ReportCategoryDTO;
6
+
7
+interface ExportableReport
8
+{
9
+    public function getTitle(): string;
10
+
11
+    public function getHeaders(): array;
12
+
13
+    /**
14
+     * @return ReportCategoryDTO[]
15
+     */
16
+    public function getCategories(): array;
17
+
18
+    public function getOverallTotals(): array;
19
+
20
+    public function getColumns(): array;
21
+}

+ 1
- 2
app/DTO/AccountBalanceDTO.php View File

12
         public string $creditBalance,
12
         public string $creditBalance,
13
         public ?string $netMovement,
13
         public ?string $netMovement,
14
         public ?string $endingBalance,
14
         public ?string $endingBalance,
15
-    ) {
16
-    }
15
+    ) {}
17
 
16
 
18
     public function toLivewire(): array
17
     public function toLivewire(): array
19
     {
18
     {

+ 1
- 2
app/DTO/AccountCategoryDTO.php View File

12
     public function __construct(
12
     public function __construct(
13
         public array $accounts,
13
         public array $accounts,
14
         public AccountBalanceDTO $summary,
14
         public AccountBalanceDTO $summary,
15
-    ) {
16
-    }
15
+    ) {}
17
 
16
 
18
     public function toLivewire(): array
17
     public function toLivewire(): array
19
     {
18
     {

+ 1
- 2
app/DTO/AccountDTO.php View File

10
         public string $accountName,
10
         public string $accountName,
11
         public string $accountCode,
11
         public string $accountCode,
12
         public AccountBalanceDTO $balance,
12
         public AccountBalanceDTO $balance,
13
-    ) {
14
-    }
13
+    ) {}
15
 
14
 
16
     public function toLivewire(): array
15
     public function toLivewire(): array
17
     {
16
     {

+ 17
- 0
app/DTO/ReportCategoryDTO.php View File

1
+<?php
2
+
3
+namespace App\DTO;
4
+
5
+class ReportCategoryDTO
6
+{
7
+    /**
8
+     * @param  string[]  $header
9
+     * @param  string[][]  $data
10
+     * @param  string[]  $summary
11
+     */
12
+    public function __construct(
13
+        public array $header,
14
+        public array $data,
15
+        public array $summary,
16
+    ) {}
17
+}

app/DTO/AccountBalanceReportDTO.php → app/DTO/ReportDTO.php View File

4
 
4
 
5
 use Livewire\Wireable;
5
 use Livewire\Wireable;
6
 
6
 
7
-class AccountBalanceReportDTO implements Wireable
7
+class ReportDTO implements Wireable
8
 {
8
 {
9
     public function __construct(
9
     public function __construct(
10
         /**
10
         /**
12
          */
12
          */
13
         public array $categories,
13
         public array $categories,
14
         public AccountBalanceDTO $overallTotal,
14
         public AccountBalanceDTO $overallTotal,
15
-    ) {
16
-    }
15
+        public array $fields,
16
+    ) {}
17
 
17
 
18
     public function toLivewire(): array
18
     public function toLivewire(): array
19
     {
19
     {
20
         return [
20
         return [
21
             'categories' => $this->categories,
21
             'categories' => $this->categories,
22
             'overallTotal' => $this->overallTotal->toLivewire(),
22
             'overallTotal' => $this->overallTotal->toLivewire(),
23
+            'fields' => $this->fields,
23
         ];
24
         ];
24
     }
25
     }
25
 
26
 
28
         return new static(
29
         return new static(
29
             $value['categories'],
30
             $value['categories'],
30
             AccountBalanceDTO::fromLivewire($value['overallTotal']),
31
             AccountBalanceDTO::fromLivewire($value['overallTotal']),
32
+            $value['fields'],
31
         );
33
         );
32
     }
34
     }
33
 }
35
 }

+ 3
- 9
app/Events/CompanyConfigured.php View File

13
     use InteractsWithSockets;
13
     use InteractsWithSockets;
14
     use SerializesModels;
14
     use SerializesModels;
15
 
15
 
16
-    public Company $company;
17
-
18
-    /**
19
-     * Create a new event instance.
20
-     */
21
-    public function __construct(Company $company)
22
-    {
23
-        $this->company = $company;
24
-    }
16
+    public function __construct(
17
+        public Company $company
18
+    ) {}
25
 }
19
 }

+ 3
- 6
app/Events/CompanyDefaultEvent.php View File

13
     use InteractsWithSockets;
13
     use InteractsWithSockets;
14
     use SerializesModels;
14
     use SerializesModels;
15
 
15
 
16
-    public Model $model;
17
-
18
     /**
16
     /**
19
      * Create a new event instance.
17
      * Create a new event instance.
20
      */
18
      */
21
-    public function __construct(Model $model)
22
-    {
23
-        $this->model = $model;
24
-    }
19
+    public function __construct(
20
+        public Model $model
21
+    ) {}
25
 }
22
 }

+ 4
- 9
app/Events/CompanyDefaultUpdated.php View File

13
     use InteractsWithSockets;
13
     use InteractsWithSockets;
14
     use SerializesModels;
14
     use SerializesModels;
15
 
15
 
16
-    public Model $record;
17
-
18
-    public array $data;
19
-
20
     /**
16
     /**
21
      * Create a new event instance.
17
      * Create a new event instance.
22
      */
18
      */
23
-    public function __construct(Model $record, array $data)
24
-    {
25
-        $this->record = $record;
26
-        $this->data = $data;
27
-    }
19
+    public function __construct(
20
+        public Model $record,
21
+        public array $data
22
+    ) {}
28
 }
23
 }

+ 7
- 18
app/Events/CompanyGenerated.php View File

12
     use Dispatchable;
12
     use Dispatchable;
13
     use SerializesModels;
13
     use SerializesModels;
14
 
14
 
15
-    public User $user;
16
-
17
-    public Company $company;
18
-
19
-    public string $country;
20
-
21
-    public string $language;
22
-
23
-    public string $currency;
24
-
25
     /**
15
     /**
26
      * Create a new event instance.
16
      * Create a new event instance.
27
      */
17
      */
28
-    public function __construct(User $user, Company $company, string $country, string $language = 'en', string $currency = 'USD')
29
-    {
30
-        $this->user = $user;
31
-        $this->company = $company;
32
-        $this->country = $country;
33
-        $this->language = $language;
34
-        $this->currency = $currency;
35
-    }
18
+    public function __construct(
19
+        public User $user,
20
+        public Company $company,
21
+        public string $country,
22
+        public string $language = 'en',
23
+        public string $currency = 'USD'
24
+    ) {}
36
 }
25
 }

+ 5
- 15
app/Events/CurrencyRateChanged.php View File

13
     use InteractsWithSockets;
13
     use InteractsWithSockets;
14
     use SerializesModels;
14
     use SerializesModels;
15
 
15
 
16
-    public Currency $currency;
17
-
18
-    public float $oldRate;
19
-
20
-    public float $newRate;
21
-
22
-    /**
23
-     * Create a new event instance.
24
-     */
25
-    public function __construct(Currency $currency, float $oldRate, float $newRate)
26
-    {
27
-        $this->currency = $currency;
28
-        $this->oldRate = $oldRate;
29
-        $this->newRate = $newRate;
30
-    }
16
+    public function __construct(
17
+        public Currency $currency,
18
+        public float $oldRate,
19
+        public float $newRate
20
+    ) {}
31
 }
21
 }

+ 3
- 6
app/Events/DefaultCurrencyChanged.php View File

13
     use InteractsWithSockets;
13
     use InteractsWithSockets;
14
     use SerializesModels;
14
     use SerializesModels;
15
 
15
 
16
-    public Currency $currency;
17
-
18
     /**
16
     /**
19
      * Create a new event instance.
17
      * Create a new event instance.
20
      */
18
      */
21
-    public function __construct(Currency $currency)
22
-    {
23
-        $this->currency = $currency;
24
-    }
19
+    public function __construct(
20
+        public Currency $currency
21
+    ) {}
25
 }
22
 }

+ 5
- 12
app/Events/PlaidSuccess.php View File

11
     use Dispatchable;
11
     use Dispatchable;
12
     use SerializesModels;
12
     use SerializesModels;
13
 
13
 
14
-    public string $publicToken;
15
-
16
-    public string $accessToken;
17
-
18
-    public Company $company;
19
-
20
     /**
14
     /**
21
      * Create a new event instance.
15
      * Create a new event instance.
22
      */
16
      */
23
-    public function __construct(string $publicToken, string $accessToken, Company $company)
24
-    {
25
-        $this->publicToken = $publicToken;
26
-        $this->accessToken = $accessToken;
27
-        $this->company = $company;
28
-    }
17
+    public function __construct(
18
+        public string $publicToken,
19
+        public string $accessToken,
20
+        public Company $company
21
+    ) {}
29
 }
22
 }

+ 6
- 15
app/Events/StartTransactionImport.php View File

12
     use Dispatchable;
12
     use Dispatchable;
13
     use SerializesModels;
13
     use SerializesModels;
14
 
14
 
15
-    public Company $company;
16
-
17
-    public ConnectedBankAccount $connectedBankAccount;
18
-
19
-    public int | string $selectedBankAccountId;
20
-
21
-    public string $startDate;
22
-
23
     /**
15
     /**
24
      * Create a new event instance.
16
      * Create a new event instance.
25
      */
17
      */
26
-    public function __construct(Company $company, ConnectedBankAccount $connectedBankAccount, int | string $selectedBankAccountId, string $startDate)
27
-    {
28
-        $this->company = $company;
29
-        $this->connectedBankAccount = $connectedBankAccount;
30
-        $this->selectedBankAccountId = $selectedBankAccountId;
31
-        $this->startDate = $startDate;
32
-    }
18
+    public function __construct(
19
+        public Company $company,
20
+        public ConnectedBankAccount $connectedBankAccount,
21
+        public int | string $selectedBankAccountId,
22
+        public string $startDate
23
+    ) {}
33
 }
24
 }

+ 0
- 6
app/Facades/Accounting.php View File

3
 namespace App\Facades;
3
 namespace App\Facades;
4
 
4
 
5
 use App\Contracts\AccountHandler;
5
 use App\Contracts\AccountHandler;
6
-use App\DTO\AccountBalanceDTO;
7
-use App\DTO\AccountBalanceReportDTO;
8
-use App\Enums\Accounting\AccountCategory;
9
 use App\Models\Accounting\Account;
6
 use App\Models\Accounting\Account;
10
 use App\ValueObjects\Money;
7
 use App\ValueObjects\Money;
11
 use Illuminate\Support\Facades\Facade;
8
 use Illuminate\Support\Facades\Facade;
16
  * @method static Money getNetMovement(Account $account, string $startDate, string $endDate)
13
  * @method static Money getNetMovement(Account $account, string $startDate, string $endDate)
17
  * @method static Money|null getStartingBalance(Account $account, string $startDate)
14
  * @method static Money|null getStartingBalance(Account $account, string $startDate)
18
  * @method static Money|null getEndingBalance(Account $account, string $startDate, string $endDate)
15
  * @method static Money|null getEndingBalance(Account $account, string $startDate, string $endDate)
19
- * @method static int calculateNetMovementByCategory(AccountCategory $category, int $debitBalance, int $creditBalance)
20
  * @method static array getBalances(Account $account, string $startDate, string $endDate)
16
  * @method static array getBalances(Account $account, string $startDate, string $endDate)
21
- * @method static AccountBalanceDTO formatBalances(array $balances)
22
- * @method static AccountBalanceReportDTO buildAccountBalanceReport(string $startDate, string $endDate)
23
  * @method static Money getTotalBalanceForAllBankAccounts(string $startDate, string $endDate)
17
  * @method static Money getTotalBalanceForAllBankAccounts(string $startDate, string $endDate)
24
  * @method static array getAccountCategoryOrder()
18
  * @method static array getAccountCategoryOrder()
25
  * @method static string getEarliestTransactionDate()
19
  * @method static string getEarliestTransactionDate()

+ 2
- 2
app/Filament/Company/Clusters/Settings/Pages/Invoice.php View File

15
 use Filament\Forms\Components\Component;
15
 use Filament\Forms\Components\Component;
16
 use Filament\Forms\Components\FileUpload;
16
 use Filament\Forms\Components\FileUpload;
17
 use Filament\Forms\Components\Grid;
17
 use Filament\Forms\Components\Grid;
18
-use Filament\Forms\Components\MarkdownEditor;
19
 use Filament\Forms\Components\Section;
18
 use Filament\Forms\Components\Section;
20
 use Filament\Forms\Components\Select;
19
 use Filament\Forms\Components\Select;
21
 use Filament\Forms\Components\Textarea;
20
 use Filament\Forms\Components\Textarea;
163
                 TextInput::make('subheader')
162
                 TextInput::make('subheader')
164
                     ->localizeLabel()
163
                     ->localizeLabel()
165
                     ->nullable(),
164
                     ->nullable(),
166
-                MarkdownEditor::make('terms')
165
+                Textarea::make('terms')
166
+                    ->localizeLabel()
167
                     ->nullable(),
167
                     ->nullable(),
168
                 Textarea::make('footer')
168
                 Textarea::make('footer')
169
                     ->localizeLabel('Footer / Notes')
169
                     ->localizeLabel('Footer / Notes')

+ 2
- 2
app/Filament/Company/Clusters/Settings/Pages/Localization.php View File

127
                     ->options(LocalizationModel::getAllLanguages())
127
                     ->options(LocalizationModel::getAllLanguages())
128
                     ->searchable(),
128
                     ->searchable(),
129
                 Select::make('timezone')
129
                 Select::make('timezone')
130
+                    ->softRequired()
130
                     ->localizeLabel()
131
                     ->localizeLabel()
131
                     ->options(Timezone::getTimezoneOptions(CompanyProfileModel::first()->country))
132
                     ->options(Timezone::getTimezoneOptions(CompanyProfileModel::first()->country))
132
-                    ->searchable()
133
-                    ->nullable(),
133
+                    ->searchable(),
134
             ])->columns();
134
             ])->columns();
135
     }
135
     }
136
 
136
 

+ 1
- 1
app/Filament/Company/Clusters/Settings/Resources/CurrencyResource/Pages/CreateCurrency.php View File

18
 
18
 
19
     protected function getRedirectUrl(): string
19
     protected function getRedirectUrl(): string
20
     {
20
     {
21
-        return $this->previousUrl;
21
+        return $this->getResource()::getUrl('index');
22
     }
22
     }
23
 
23
 
24
     protected function mutateFormDataBeforeCreate(array $data): array
24
     protected function mutateFormDataBeforeCreate(array $data): array

+ 2
- 2
app/Filament/Company/Clusters/Settings/Resources/CurrencyResource/Pages/EditCurrency.php View File

24
         ];
24
         ];
25
     }
25
     }
26
 
26
 
27
-    protected function getRedirectUrl(): ?string
27
+    protected function getRedirectUrl(): string
28
     {
28
     {
29
-        return $this->previousUrl;
29
+        return $this->getResource()::getUrl('index');
30
     }
30
     }
31
 
31
 
32
     protected function mutateFormDataBeforeSave(array $data): array
32
     protected function mutateFormDataBeforeSave(array $data): array

+ 1
- 1
app/Filament/Company/Clusters/Settings/Resources/DiscountResource/Pages/CreateDiscount.php View File

18
 
18
 
19
     protected function getRedirectUrl(): string
19
     protected function getRedirectUrl(): string
20
     {
20
     {
21
-        return $this->previousUrl;
21
+        return $this->getResource()::getUrl('index');
22
     }
22
     }
23
 
23
 
24
     protected function mutateFormDataBeforeCreate(array $data): array
24
     protected function mutateFormDataBeforeCreate(array $data): array

+ 1
- 1
app/Filament/Company/Clusters/Settings/Resources/DiscountResource/Pages/EditDiscount.php View File

25
 
25
 
26
     protected function getRedirectUrl(): string
26
     protected function getRedirectUrl(): string
27
     {
27
     {
28
-        return $this->previousUrl;
28
+        return $this->getResource()::getUrl('index');
29
     }
29
     }
30
 
30
 
31
     protected function mutateFormDataBeforeSave(array $data): array
31
     protected function mutateFormDataBeforeSave(array $data): array

+ 1
- 1
app/Filament/Company/Clusters/Settings/Resources/TaxResource/Pages/CreateTax.php View File

18
 
18
 
19
     protected function getRedirectUrl(): string
19
     protected function getRedirectUrl(): string
20
     {
20
     {
21
-        return $this->previousUrl;
21
+        return $this->getResource()::getUrl('index');
22
     }
22
     }
23
 
23
 
24
     protected function mutateFormDataBeforeCreate(array $data): array
24
     protected function mutateFormDataBeforeCreate(array $data): array

+ 1
- 1
app/Filament/Company/Clusters/Settings/Resources/TaxResource/Pages/EditTax.php View File

25
 
25
 
26
     protected function getRedirectUrl(): string
26
     protected function getRedirectUrl(): string
27
     {
27
     {
28
-        return $this->previousUrl;
28
+        return $this->getResource()::getUrl('index');
29
     }
29
     }
30
 
30
 
31
     protected function mutateFormDataBeforeSave(array $data): array
31
     protected function mutateFormDataBeforeSave(array $data): array

+ 12
- 0
app/Filament/Company/Pages/Accounting/AccountChart.php View File

10
 use Filament\Actions\Action;
10
 use Filament\Actions\Action;
11
 use Filament\Actions\CreateAction;
11
 use Filament\Actions\CreateAction;
12
 use Filament\Actions\EditAction;
12
 use Filament\Actions\EditAction;
13
+use Filament\Forms\Components\Checkbox;
13
 use Filament\Forms\Components\Component;
14
 use Filament\Forms\Components\Component;
14
 use Filament\Forms\Components\Select;
15
 use Filament\Forms\Components\Select;
15
 use Filament\Forms\Components\Textarea;
16
 use Filament\Forms\Components\Textarea;
98
                 $this->getNameFormComponent(),
99
                 $this->getNameFormComponent(),
99
                 $this->getCurrencyFormComponent(),
100
                 $this->getCurrencyFormComponent(),
100
                 $this->getDescriptionFormComponent(),
101
                 $this->getDescriptionFormComponent(),
102
+                $this->getArchiveFormComponent(),
101
             ]);
103
             ]);
102
     }
104
     }
103
 
105
 
161
             ->autosize();
163
             ->autosize();
162
     }
164
     }
163
 
165
 
166
+    protected function getArchiveFormComponent(): Component
167
+    {
168
+        return Checkbox::make('archived')
169
+            ->label('Archive Account')
170
+            ->helperText('Archived accounts will not be available for selection in transactions.')
171
+            ->hidden(static function (string $operation): bool {
172
+                return $operation === 'create';
173
+            });
174
+    }
175
+
164
     private function getChartSubtypeOptions($useActiveTab = true): array
176
     private function getChartSubtypeOptions($useActiveTab = true): array
165
     {
177
     {
166
         $subtypes = $useActiveTab ?
178
         $subtypes = $useActiveTab ?

+ 38
- 14
app/Filament/Company/Pages/Accounting/Transactions.php View File

11
 use App\Filament\Forms\Components\DateRangeSelect;
11
 use App\Filament\Forms\Components\DateRangeSelect;
12
 use App\Filament\Forms\Components\JournalEntryRepeater;
12
 use App\Filament\Forms\Components\JournalEntryRepeater;
13
 use App\Models\Accounting\Account;
13
 use App\Models\Accounting\Account;
14
+use App\Models\Accounting\JournalEntry;
14
 use App\Models\Accounting\Transaction;
15
 use App\Models\Accounting\Transaction;
15
 use App\Models\Banking\BankAccount;
16
 use App\Models\Banking\BankAccount;
16
 use App\Models\Company;
17
 use App\Models\Company;
127
         return $form
128
         return $form
128
             ->schema([
129
             ->schema([
129
                 Forms\Components\Select::make('bankAccountIdFiltered')
130
                 Forms\Components\Select::make('bankAccountIdFiltered')
130
-                    ->label('Account')
131
-                    ->hiddenLabel()
132
-                    ->allowHtml()
133
-                    ->options(fn () => $this->getBankAccountOptions(true, true))
134
                     ->live()
131
                     ->live()
132
+                    ->allowHtml()
133
+                    ->hiddenLabel()
134
+                    ->columnSpan(2)
135
+                    ->label('Account')
135
                     ->selectablePlaceholder(false)
136
                     ->selectablePlaceholder(false)
136
-                    ->columnSpan(4),
137
+                    ->extraAttributes(['wire:key' => Str::random()])
138
+                    ->options(fn () => $this->getBankAccountOptions(true, true)),
137
             ])
139
             ])
138
-            ->columns(14);
140
+            ->columns(7);
139
     }
141
     }
140
 
142
 
141
     public function transactionForm(Form $form): Form
143
     public function transactionForm(Form $form): Form
149
                     ->label('Description'),
151
                     ->label('Description'),
150
                 Forms\Components\Select::make('bank_account_id')
152
                 Forms\Components\Select::make('bank_account_id')
151
                     ->label('Account')
153
                     ->label('Account')
152
-                    ->options(fn () => $this->getBankAccountOptions())
154
+                    ->options(fn (?Transaction $transaction) => $this->getBankAccountOptions(currentBankAccountId: $transaction?->bank_account_id))
153
                     ->live()
155
                     ->live()
154
                     ->searchable()
156
                     ->searchable()
155
                     ->afterStateUpdated(function (Set $set, $state, $old, Get $get) {
157
                     ->afterStateUpdated(function (Set $set, $state, $old, Get $get) {
179
                     ->required(),
181
                     ->required(),
180
                 Forms\Components\Select::make('account_id')
182
                 Forms\Components\Select::make('account_id')
181
                     ->label('Category')
183
                     ->label('Category')
182
-                    ->options(fn (Forms\Get $get) => $this->getChartAccountOptions(type: TransactionType::parse($get('type')), nominalAccountsOnly: true))
184
+                    ->options(fn (Forms\Get $get, ?Transaction $transaction) => $this->getChartAccountOptions(type: TransactionType::parse($get('type')), nominalAccountsOnly: true, currentAccountId: $transaction?->account_id))
183
                     ->searchable()
185
                     ->searchable()
184
                     ->preload()
186
                     ->preload()
185
                     ->required(),
187
                     ->required(),
224
                     ->sortable()
226
                     ->sortable()
225
                     ->localizeDate(),
227
                     ->localizeDate(),
226
                 Tables\Columns\TextColumn::make('description')
228
                 Tables\Columns\TextColumn::make('description')
229
+                    ->label('Description')
227
                     ->limit(30)
230
                     ->limit(30)
228
-                    ->label('Description'),
231
+                    ->toggleable(),
229
                 Tables\Columns\TextColumn::make('bankAccount.account.name')
232
                 Tables\Columns\TextColumn::make('bankAccount.account.name')
230
-                    ->label('Account'),
233
+                    ->label('Account')
234
+                    ->toggleable(),
231
                 Tables\Columns\TextColumn::make('account.name')
235
                 Tables\Columns\TextColumn::make('account.name')
232
                     ->label('Category')
236
                     ->label('Category')
237
+                    ->toggleable()
233
                     ->state(static fn (Transaction $transaction) => $transaction->account->name ?? 'Journal Entry'),
238
                     ->state(static fn (Transaction $transaction) => $transaction->account->name ?? 'Journal Entry'),
234
                 Tables\Columns\TextColumn::make('amount')
239
                 Tables\Columns\TextColumn::make('amount')
235
                     ->label('Amount')
240
                     ->label('Amount')
296
                 $this->buildDateRangeFilter('updated_at', 'Last Modified'),
301
                 $this->buildDateRangeFilter('updated_at', 'Last Modified'),
297
             ], layout: Tables\Enums\FiltersLayout::Modal)
302
             ], layout: Tables\Enums\FiltersLayout::Modal)
298
             ->deferFilters()
303
             ->deferFilters()
304
+            ->deferLoading()
299
             ->filtersFormColumns(2)
305
             ->filtersFormColumns(2)
300
             ->filtersTriggerAction(
306
             ->filtersTriggerAction(
301
                 fn (Tables\Actions\Action $action) => $action
307
                 fn (Tables\Actions\Action $action) => $action
557
                 ->label('Description'),
563
                 ->label('Description'),
558
             Select::make('account_id')
564
             Select::make('account_id')
559
                 ->label('Account')
565
                 ->label('Account')
560
-                ->options(fn (): array => $this->getChartAccountOptions())
566
+                ->options(fn (?JournalEntry $journalEntry): array => $this->getChartAccountOptions(currentAccountId: $journalEntry?->account_id))
561
                 ->live()
567
                 ->live()
562
                 ->softRequired()
568
                 ->softRequired()
563
                 ->searchable(),
569
                 ->searchable(),
707
         return 'uncategorized';
713
         return 'uncategorized';
708
     }
714
     }
709
 
715
 
710
-    protected function getChartAccountOptions(?TransactionType $type = null, bool $nominalAccountsOnly = false): array
716
+    protected function getChartAccountOptions(?TransactionType $type = null, ?bool $nominalAccountsOnly = null, ?int $currentAccountId = null): array
711
     {
717
     {
718
+        $nominalAccountsOnly ??= false;
719
+
712
         $excludedCategory = match ($type) {
720
         $excludedCategory = match ($type) {
713
             TransactionType::Deposit => AccountCategory::Expense,
721
             TransactionType::Deposit => AccountCategory::Expense,
714
             TransactionType::Withdrawal => AccountCategory::Revenue,
722
             TransactionType::Withdrawal => AccountCategory::Revenue,
716
         };
724
         };
717
 
725
 
718
         return Account::query()
726
         return Account::query()
719
-            ->when($nominalAccountsOnly, fn (Builder $query) => $query->whereNull('accountable_type'))
727
+            ->when($nominalAccountsOnly, fn (Builder $query) => $query->doesntHave('bankAccount'))
720
             ->when($excludedCategory, fn (Builder $query) => $query->whereNot('category', $excludedCategory))
728
             ->when($excludedCategory, fn (Builder $query) => $query->whereNot('category', $excludedCategory))
729
+            ->where(function (Builder $query) use ($currentAccountId) {
730
+                $query->where('archived', false)
731
+                    ->orWhere('id', $currentAccountId);
732
+            })
721
             ->get()
733
             ->get()
722
             ->groupBy(fn (Account $account) => $account->category->getPluralLabel())
734
             ->groupBy(fn (Account $account) => $account->category->getPluralLabel())
723
             ->map(fn (Collection $accounts, string $category) => $accounts->pluck('name', 'id'))
735
             ->map(fn (Collection $accounts, string $category) => $accounts->pluck('name', 'id'))
724
             ->toArray();
736
             ->toArray();
725
     }
737
     }
726
 
738
 
727
-    protected function getBankAccountOptions(?bool $onlyWithTransactions = null, bool $isFilter = false): array
739
+    protected function getBankAccountOptions(?bool $onlyWithTransactions = null, ?bool $isFilter = null, ?int $currentBankAccountId = null): array
728
     {
740
     {
741
+        $isFilter ??= false;
729
         $onlyWithTransactions ??= false;
742
         $onlyWithTransactions ??= false;
730
 
743
 
731
         $options = $isFilter ? [
744
         $options = $isFilter ? [
733
         ] : [];
746
         ] : [];
734
 
747
 
735
         $bankAccountOptions = BankAccount::with('account.subtype')
748
         $bankAccountOptions = BankAccount::with('account.subtype')
749
+            ->whereHas('account', function (Builder $query) use ($isFilter, $currentBankAccountId) {
750
+                if ($isFilter === false) {
751
+                    $query->where('archived', false);
752
+                }
753
+
754
+                if ($currentBankAccountId) {
755
+                    $query->orWhereHas('bankAccount', function (Builder $query) use ($currentBankAccountId) {
756
+                        $query->where('id', $currentBankAccountId);
757
+                    });
758
+                }
759
+            })
736
             ->when($onlyWithTransactions, fn (Builder $query) => $query->has('transactions'))
760
             ->when($onlyWithTransactions, fn (Builder $query) => $query->has('transactions'))
737
             ->get()
761
             ->get()
738
             ->groupBy('account.subtype.name')
762
             ->groupBy('account.subtype.name')

+ 6
- 6
app/Filament/Company/Pages/CreateCompany.php View File

28
                     ->label(__('filament-companies::default.labels.company_name'))
28
                     ->label(__('filament-companies::default.labels.company_name'))
29
                     ->autofocus()
29
                     ->autofocus()
30
                     ->maxLength(255)
30
                     ->maxLength(255)
31
-                    ->required(),
31
+                    ->softRequired(),
32
                 TextInput::make('profile.email')
32
                 TextInput::make('profile.email')
33
                     ->label('Company Email')
33
                     ->label('Company Email')
34
                     ->email()
34
                     ->email()
35
-                    ->required(),
35
+                    ->softRequired(),
36
                 Select::make('profile.entity_type')
36
                 Select::make('profile.entity_type')
37
                     ->label('Entity Type')
37
                     ->label('Entity Type')
38
                     ->options(EntityType::class)
38
                     ->options(EntityType::class)
39
-                    ->required(),
39
+                    ->softRequired(),
40
                 Select::make('profile.country')
40
                 Select::make('profile.country')
41
                     ->label('Country')
41
                     ->label('Country')
42
                     ->live()
42
                     ->live()
43
                     ->searchable()
43
                     ->searchable()
44
                     ->options(Country::getAvailableCountryOptions())
44
                     ->options(Country::getAvailableCountryOptions())
45
-                    ->required(),
45
+                    ->softRequired(),
46
                 Select::make('locale.language')
46
                 Select::make('locale.language')
47
                     ->label('Language')
47
                     ->label('Language')
48
                     ->searchable()
48
                     ->searchable()
49
                     ->options(Localization::getAllLanguages())
49
                     ->options(Localization::getAllLanguages())
50
-                    ->required(),
50
+                    ->softRequired(),
51
                 Select::make('currencies.code')
51
                 Select::make('currencies.code')
52
                     ->label('Currency')
52
                     ->label('Currency')
53
                     ->searchable()
53
                     ->searchable()
54
                     ->options(CurrencyAccessor::getAllCurrencyOptions())
54
                     ->options(CurrencyAccessor::getAllCurrencyOptions())
55
                     ->optionsLimit(5)
55
                     ->optionsLimit(5)
56
-                    ->required(),
56
+                    ->softRequired(),
57
             ])
57
             ])
58
             ->model(FilamentCompanies::companyModel())
58
             ->model(FilamentCompanies::companyModel())
59
             ->statePath('data');
59
             ->statePath('data');

+ 2
- 1
app/Filament/Company/Pages/Reports.php View File

3
 namespace App\Filament\Company\Pages;
3
 namespace App\Filament\Company\Pages;
4
 
4
 
5
 use App\Filament\Company\Pages\Reports\AccountBalances;
5
 use App\Filament\Company\Pages\Reports\AccountBalances;
6
+use App\Filament\Company\Pages\Reports\TrialBalance;
6
 use App\Infolists\Components\ReportEntry;
7
 use App\Infolists\Components\ReportEntry;
7
 use Filament\Infolists\Components\Section;
8
 use Filament\Infolists\Components\Section;
8
 use Filament\Infolists\Infolist;
9
 use Filament\Infolists\Infolist;
38
                             ->description('The sum of all debit and credit balances for all accounts on a single day. This helps to ensure that the books are in balance.')
39
                             ->description('The sum of all debit and credit balances for all accounts on a single day. This helps to ensure that the books are in balance.')
39
                             ->icon('heroicon-o-scale')
40
                             ->icon('heroicon-o-scale')
40
                             ->iconColor(Color::Sky)
41
                             ->iconColor(Color::Sky)
41
-                            ->url('#'),
42
+                            ->url(TrialBalance::getUrl()),
42
                         ReportEntry::make('account_transactions')
43
                         ReportEntry::make('account_transactions')
43
                             ->hiddenLabel()
44
                             ->hiddenLabel()
44
                             ->heading('Account Transactions')
45
                             ->heading('Account Transactions')

+ 70
- 113
app/Filament/Company/Pages/Reports/AccountBalances.php View File

2
 
2
 
3
 namespace App\Filament\Company\Pages\Reports;
3
 namespace App\Filament\Company\Pages\Reports;
4
 
4
 
5
-use App\DTO\AccountBalanceReportDTO;
6
-use App\Filament\Forms\Components\DateRangeSelect;
7
-use App\Models\Company;
8
-use App\Services\AccountBalancesExportService;
9
-use App\Services\AccountService;
10
-use Barryvdh\DomPDF\Facade\Pdf;
11
-use Filament\Actions\Action;
12
-use Filament\Actions\ActionGroup;
13
-use Filament\Forms\Components\DatePicker;
14
-use Filament\Forms\Components\Split;
5
+use App\Contracts\ExportableReport;
6
+use App\DTO\ReportDTO;
7
+use App\Services\ExportService;
8
+use App\Services\ReportService;
9
+use App\Support\Column;
10
+use App\Transformers\AccountBalanceReportTransformer;
15
 use Filament\Forms\Form;
11
 use Filament\Forms\Form;
16
-use Filament\Forms\Set;
17
-use Filament\Pages\Page;
18
-use Filament\Support\Enums\IconPosition;
19
-use Filament\Support\Enums\IconSize;
20
-use Illuminate\Support\Carbon;
12
+use Filament\Support\Enums\Alignment;
13
+use Guava\FilamentClusters\Forms\Cluster;
21
 use Symfony\Component\HttpFoundation\StreamedResponse;
14
 use Symfony\Component\HttpFoundation\StreamedResponse;
22
 
15
 
23
-class AccountBalances extends Page
16
+class AccountBalances extends BaseReportPage
24
 {
17
 {
25
-    protected static string $view = 'filament.company.pages.reports.account-balances';
18
+    protected static string $view = 'filament.company.pages.reports.detailed-report';
26
 
19
 
27
     protected static ?string $slug = 'reports/account-balances';
20
     protected static ?string $slug = 'reports/account-balances';
28
 
21
 
29
-    public string $startDate = '';
22
+    protected static bool $shouldRegisterNavigation = false;
30
 
23
 
31
-    public string $endDate = '';
24
+    protected ReportService $reportService;
32
 
25
 
33
-    public string $dateRange = '';
26
+    protected ExportService $exportService;
34
 
27
 
35
-    public string $fiscalYearStartDate = '';
36
-
37
-    public string $fiscalYearEndDate = '';
38
-
39
-    public Company $company;
40
-
41
-    public AccountBalanceReportDTO $accountBalanceReport;
42
-
43
-    protected AccountService $accountService;
44
-
45
-    protected AccountBalancesExportService $accountBalancesExportService;
46
-
47
-    public function boot(AccountService $accountService, AccountBalancesExportService $accountBalancesExportService): void
48
-    {
49
-        $this->accountService = $accountService;
50
-        $this->accountBalancesExportService = $accountBalancesExportService;
51
-    }
52
-
53
-    public function mount(): void
54
-    {
55
-        $this->company = auth()->user()->currentCompany;
56
-        $this->fiscalYearStartDate = $this->company->locale->fiscalYearStartDate();
57
-        $this->fiscalYearEndDate = $this->company->locale->fiscalYearEndDate();
58
-        $this->dateRange = $this->getDefaultDateRange();
59
-        $this->setDateRange(Carbon::parse($this->fiscalYearStartDate), Carbon::parse($this->fiscalYearEndDate));
60
-
61
-        $this->loadAccountBalances();
62
-    }
63
-
64
-    public function getDefaultDateRange(): string
65
-    {
66
-        return 'FY-' . now()->year;
67
-    }
68
-
69
-    public function loadAccountBalances(): void
28
+    public function boot(ReportService $reportService, ExportService $exportService): void
70
     {
29
     {
71
-        $this->accountBalanceReport = $this->accountService->buildAccountBalanceReport($this->startDate, $this->endDate);
30
+        $this->reportService = $reportService;
31
+        $this->exportService = $exportService;
72
     }
32
     }
73
 
33
 
74
-    protected function getHeaderActions(): array
34
+    /**
35
+     * @return array<Column>
36
+     */
37
+    public function getTable(): array
75
     {
38
     {
76
         return [
39
         return [
77
-            ActionGroup::make([
78
-                Action::make('exportCSV')
79
-                    ->label('CSV')
80
-                    ->action(fn () => $this->exportCSV()),
81
-                Action::make('exportPDF')
82
-                    ->label('PDF')
83
-                    ->action(fn () => $this->exportPDF()),
84
-            ])
85
-                ->label('Export')
86
-                ->button()
87
-                ->outlined()
88
-                ->dropdownWidth('max-w-[7rem]')
89
-                ->dropdownPlacement('bottom-end')
90
-                ->icon('heroicon-c-chevron-down')
91
-                ->iconSize(IconSize::Small)
92
-                ->iconPosition(IconPosition::After),
40
+            Column::make('account_code')
41
+                ->label('Account Code')
42
+                ->toggleable()
43
+                ->alignment(Alignment::Center),
44
+            Column::make('account_name')
45
+                ->label('Account')
46
+                ->alignment(Alignment::Left),
47
+            Column::make('starting_balance')
48
+                ->label('Starting Balance')
49
+                ->toggleable()
50
+                ->alignment(Alignment::Right),
51
+            Column::make('debit_balance')
52
+                ->label('Debit')
53
+                ->toggleable()
54
+                ->alignment(Alignment::Right),
55
+            Column::make('credit_balance')
56
+                ->label('Credit')
57
+                ->toggleable()
58
+                ->alignment(Alignment::Right),
59
+            Column::make('net_movement')
60
+                ->label('Net Movement')
61
+                ->toggleable()
62
+                ->alignment(Alignment::Right),
63
+            Column::make('ending_balance')
64
+                ->label('Ending Balance')
65
+                ->toggleable()
66
+                ->alignment(Alignment::Right),
93
         ];
67
         ];
94
     }
68
     }
95
 
69
 
96
-    public function exportCSV(): StreamedResponse
70
+    public function form(Form $form): Form
97
     {
71
     {
98
-        return $this->accountBalancesExportService->exportToCsv($this->company, $this->accountBalanceReport, $this->startDate, $this->endDate);
72
+        return $form
73
+            ->inlineLabel()
74
+            ->columns([
75
+                'lg' => 1,
76
+                '2xl' => 2,
77
+            ])
78
+            ->live()
79
+            ->schema([
80
+                $this->getDateRangeFormComponent(),
81
+                Cluster::make([
82
+                    $this->getStartDateFormComponent(),
83
+                    $this->getEndDateFormComponent(),
84
+                ])->hiddenLabel(),
85
+            ]);
99
     }
86
     }
100
 
87
 
101
-    public function exportPDF(): StreamedResponse
88
+    protected function buildReport(array $columns): ReportDTO
102
     {
89
     {
103
-        $pdf = Pdf::loadView('components.company.reports.account-balances', [
104
-            'accountBalanceReport' => $this->accountBalanceReport,
105
-            'startDate' => Carbon::parse($this->startDate)->format('M d, Y'),
106
-            'endDate' => Carbon::parse($this->endDate)->format('M d, Y'),
107
-        ])->setPaper('a4');
108
-
109
-        return response()->streamDownload(function () use ($pdf) {
110
-            echo $pdf->stream();
111
-        }, 'account-balances.pdf');
90
+        return $this->reportService->buildAccountBalanceReport($this->startDate, $this->endDate, $columns);
112
     }
91
     }
113
 
92
 
114
-    public function form(Form $form): Form
93
+    protected function getTransformer(ReportDTO $reportDTO): ExportableReport
115
     {
94
     {
116
-        return $form
117
-            ->schema([
118
-                Split::make([
119
-                    DateRangeSelect::make('dateRange')
120
-                        ->label('Date Range')
121
-                        ->selectablePlaceholder(false)
122
-                        ->startDateField('startDate')
123
-                        ->endDateField('endDate'),
124
-                    DatePicker::make('startDate')
125
-                        ->label('Start Date')
126
-                        ->displayFormat('Y-m-d')
127
-                        ->afterStateUpdated(static function (Set $set) {
128
-                            $set('dateRange', 'Custom');
129
-                        }),
130
-                    DatePicker::make('endDate')
131
-                        ->label('End Date')
132
-                        ->displayFormat('Y-m-d')
133
-                        ->afterStateUpdated(static function (Set $set) {
134
-                            $set('dateRange', 'Custom');
135
-                        }),
136
-                ])->live(),
137
-            ]);
95
+        return new AccountBalanceReportTransformer($reportDTO);
138
     }
96
     }
139
 
97
 
140
-    public function setDateRange(Carbon $start, Carbon $end): void
98
+    public function exportCSV(): StreamedResponse
141
     {
99
     {
142
-        $this->startDate = $start->format('Y-m-d');
143
-        $this->endDate = $end->isFuture() ? now()->format('Y-m-d') : $end->format('Y-m-d');
100
+        return $this->exportService->exportToCsv($this->company, $this->report, $this->startDate, $this->endDate);
144
     }
101
     }
145
 
102
 
146
-    public static function shouldRegisterNavigation(): bool
103
+    public function exportPDF(): StreamedResponse
147
     {
104
     {
148
-        return false;
105
+        return $this->exportService->exportToPdf($this->company, $this->report, $this->startDate, $this->endDate);
149
     }
106
     }
150
 }
107
 }

+ 249
- 0
app/Filament/Company/Pages/Reports/BaseReportPage.php View File

1
+<?php
2
+
3
+namespace App\Filament\Company\Pages\Reports;
4
+
5
+use App\Contracts\ExportableReport;
6
+use App\DTO\ReportDTO;
7
+use App\Filament\Forms\Components\DateRangeSelect;
8
+use App\Models\Company;
9
+use App\Support\Column;
10
+use Filament\Actions\Action;
11
+use Filament\Actions\ActionGroup;
12
+use Filament\Forms\Components\Checkbox;
13
+use Filament\Forms\Components\Component;
14
+use Filament\Forms\Components\DatePicker;
15
+use Filament\Forms\Form;
16
+use Filament\Forms\Set;
17
+use Filament\Pages\Page;
18
+use Filament\Support\Enums\ActionSize;
19
+use Filament\Support\Enums\IconPosition;
20
+use Filament\Support\Enums\IconSize;
21
+use Filament\Support\Facades\FilamentIcon;
22
+use Illuminate\Support\Carbon;
23
+use Livewire\Attributes\Computed;
24
+use Livewire\Attributes\Session;
25
+use Symfony\Component\HttpFoundation\StreamedResponse;
26
+
27
+abstract class BaseReportPage extends Page
28
+{
29
+    #[Session]
30
+    public string $startDate = '';
31
+
32
+    #[Session]
33
+    public string $endDate = '';
34
+
35
+    #[Session]
36
+    public string $dateRange = '';
37
+
38
+    public string $fiscalYearStartDate = '';
39
+
40
+    public string $fiscalYearEndDate = '';
41
+
42
+    public Company $company;
43
+
44
+    #[Session]
45
+    public array $toggledTableColumns = [];
46
+
47
+    abstract protected function buildReport(array $columns): ReportDTO;
48
+
49
+    abstract public function exportCSV(): StreamedResponse;
50
+
51
+    abstract public function exportPDF(): StreamedResponse;
52
+
53
+    abstract protected function getTransformer(ReportDTO $reportDTO): ExportableReport;
54
+
55
+    /**
56
+     * @return array<Column>
57
+     */
58
+    abstract public function getTable(): array;
59
+
60
+    public function mount(): void
61
+    {
62
+        $this->initializeProperties();
63
+
64
+        $this->loadDefaultDateRange();
65
+
66
+        $this->loadReportData();
67
+
68
+        $this->loadDefaultTableColumnToggleState();
69
+    }
70
+
71
+    protected function getForms(): array
72
+    {
73
+        return [
74
+            'toggleTableColumnForm',
75
+            'form',
76
+        ];
77
+    }
78
+
79
+    protected function initializeProperties(): void
80
+    {
81
+        $this->company = auth()->user()->currentCompany;
82
+        $this->fiscalYearStartDate = $this->company->locale->fiscalYearStartDate();
83
+        $this->fiscalYearEndDate = $this->company->locale->fiscalYearEndDate();
84
+    }
85
+
86
+    protected function loadDefaultDateRange(): void
87
+    {
88
+        if (empty($this->dateRange)) {
89
+            $this->dateRange = $this->getDefaultDateRange();
90
+            $this->setDateRange(Carbon::parse($this->fiscalYearStartDate), Carbon::parse($this->fiscalYearEndDate));
91
+        }
92
+    }
93
+
94
+    public function loadReportData(): void
95
+    {
96
+        unset($this->report);
97
+    }
98
+
99
+    protected function loadDefaultTableColumnToggleState(): void
100
+    {
101
+        $tableColumns = $this->getTable();
102
+
103
+        if (empty($this->toggledTableColumns)) {
104
+            foreach ($tableColumns as $column) {
105
+                if ($column->isToggleable()) {
106
+                    if ($column->isToggledHiddenByDefault()) {
107
+                        $this->toggledTableColumns[$column->getName()] = false;
108
+                    } else {
109
+                        $this->toggledTableColumns[$column->getName()] = true;
110
+                    }
111
+                } else {
112
+                    $this->toggledTableColumns[$column->getName()] = true;
113
+                }
114
+            }
115
+        }
116
+
117
+        foreach ($tableColumns as $column) {
118
+            $columnName = $column->getName();
119
+            if (! $column->isToggleable()) {
120
+                $this->toggledTableColumns[$columnName] = true;
121
+            }
122
+
123
+            if ($column->isToggleable() && $column->isToggledHiddenByDefault() && isset($this->toggledTableColumns[$columnName]) && $this->toggledTableColumns[$columnName]) {
124
+                $this->toggledTableColumns[$columnName] = false;
125
+            }
126
+        }
127
+    }
128
+
129
+    public function getDefaultDateRange(): string
130
+    {
131
+        return 'FY-' . now()->year;
132
+    }
133
+
134
+    protected function getToggledColumns(): array
135
+    {
136
+        return array_values(
137
+            array_filter(
138
+                $this->getTable(),
139
+                fn (Column $column) => $this->toggledTableColumns[$column->getName()] ?? false,
140
+            )
141
+        );
142
+    }
143
+
144
+    #[Computed(persist: true)]
145
+    public function report(): ExportableReport
146
+    {
147
+        $columns = $this->getToggledColumns();
148
+        $reportDTO = $this->buildReport($columns);
149
+
150
+        return $this->getTransformer($reportDTO);
151
+    }
152
+
153
+    public function setDateRange(Carbon $start, Carbon $end): void
154
+    {
155
+        $this->startDate = $start->toDateString();
156
+        $this->endDate = $end->isFuture() ? now()->toDateString() : $end->toDateString();
157
+    }
158
+
159
+    public function toggleColumnsAction(): Action
160
+    {
161
+        return Action::make('toggleColumns')
162
+            ->label(__('filament-tables::table.actions.toggle_columns.label'))
163
+            ->iconButton()
164
+            ->size(ActionSize::Large)
165
+            ->icon(FilamentIcon::resolve('tables::actions.toggle-columns') ?? 'heroicon-m-view-columns')
166
+            ->color('gray');
167
+    }
168
+
169
+    public function toggleTableColumnForm(Form $form): Form
170
+    {
171
+        return $form
172
+            ->schema($this->getTableColumnToggleFormSchema())
173
+            ->statePath('toggledTableColumns');
174
+    }
175
+
176
+    /**
177
+     * @return array<Checkbox>
178
+     */
179
+    protected function getTableColumnToggleFormSchema(): array
180
+    {
181
+        $schema = [];
182
+
183
+        foreach ($this->getTable() as $column) {
184
+            if ($column->isToggleable()) {
185
+                $schema[] = Checkbox::make($column->getName())
186
+                    ->label($column->getLabel());
187
+            }
188
+        }
189
+
190
+        return $schema;
191
+    }
192
+
193
+    protected function getHeaderActions(): array
194
+    {
195
+        return [
196
+            ActionGroup::make([
197
+                Action::make('exportCSV')
198
+                    ->label('CSV')
199
+                    ->action(fn () => $this->exportCSV()),
200
+                Action::make('exportPDF')
201
+                    ->label('PDF')
202
+                    ->action(fn () => $this->exportPDF()),
203
+            ])
204
+                ->label('Export')
205
+                ->button()
206
+                ->outlined()
207
+                ->dropdownWidth('max-w-[7rem]')
208
+                ->dropdownPlacement('bottom-end')
209
+                ->icon('heroicon-c-chevron-down')
210
+                ->iconSize(IconSize::Small)
211
+                ->iconPosition(IconPosition::After),
212
+        ];
213
+    }
214
+
215
+    protected function getDateRangeFormComponent(): Component
216
+    {
217
+        return DateRangeSelect::make('dateRange')
218
+            ->label('Date Range')
219
+            ->selectablePlaceholder(false)
220
+            ->startDateField('startDate')
221
+            ->endDateField('endDate');
222
+    }
223
+
224
+    protected function resetDateRange(): void
225
+    {
226
+        $this->dateRange = $this->getDefaultDateRange();
227
+        $this->setDateRange(Carbon::parse($this->fiscalYearStartDate), Carbon::parse($this->fiscalYearEndDate));
228
+    }
229
+
230
+    protected function getStartDateFormComponent(): Component
231
+    {
232
+        return DatePicker::make('startDate')
233
+            ->label('Start Date')
234
+            ->displayFormat('Y-m-d')
235
+            ->afterStateUpdated(static function (Set $set) {
236
+                $set('dateRange', 'Custom');
237
+            });
238
+    }
239
+
240
+    protected function getEndDateFormComponent(): Component
241
+    {
242
+        return DatePicker::make('endDate')
243
+            ->label('End Date')
244
+            ->displayFormat('Y-m-d')
245
+            ->afterStateUpdated(static function (Set $set) {
246
+                $set('dateRange', 'Custom');
247
+            });
248
+    }
249
+}

+ 92
- 0
app/Filament/Company/Pages/Reports/TrialBalance.php View File

1
+<?php
2
+
3
+namespace App\Filament\Company\Pages\Reports;
4
+
5
+use App\Contracts\ExportableReport;
6
+use App\DTO\ReportDTO;
7
+use App\Services\ExportService;
8
+use App\Services\ReportService;
9
+use App\Support\Column;
10
+use App\Transformers\TrialBalanceReportTransformer;
11
+use Filament\Forms\Form;
12
+use Filament\Support\Enums\Alignment;
13
+use Guava\FilamentClusters\Forms\Cluster;
14
+use Symfony\Component\HttpFoundation\StreamedResponse;
15
+
16
+class TrialBalance extends BaseReportPage
17
+{
18
+    protected static string $view = 'filament.company.pages.reports.detailed-report';
19
+
20
+    protected static ?string $slug = 'reports/trial-balance';
21
+
22
+    protected static bool $shouldRegisterNavigation = false;
23
+
24
+    protected ReportService $reportService;
25
+
26
+    protected ExportService $exportService;
27
+
28
+    public function boot(ReportService $reportService, ExportService $exportService): void
29
+    {
30
+        $this->reportService = $reportService;
31
+        $this->exportService = $exportService;
32
+    }
33
+
34
+    public function getTable(): array
35
+    {
36
+        return [
37
+            Column::make('account_code')
38
+                ->label('Account Code')
39
+                ->toggleable()
40
+                ->alignment(Alignment::Center),
41
+            Column::make('account_name')
42
+                ->label('Account')
43
+                ->alignment(Alignment::Left),
44
+            Column::make('debit_balance')
45
+                ->label('Debit')
46
+                ->toggleable()
47
+                ->alignment(Alignment::Right),
48
+            Column::make('credit_balance')
49
+                ->label('Credit')
50
+                ->toggleable()
51
+                ->alignment(Alignment::Right),
52
+        ];
53
+    }
54
+
55
+    public function form(Form $form): Form
56
+    {
57
+        return $form
58
+            ->inlineLabel()
59
+            ->columns([
60
+                'lg' => 1,
61
+                '2xl' => 2,
62
+            ])
63
+            ->live()
64
+            ->schema([
65
+                $this->getDateRangeFormComponent(),
66
+                Cluster::make([
67
+                    $this->getStartDateFormComponent(),
68
+                    $this->getEndDateFormComponent(),
69
+                ])->hiddenLabel(),
70
+            ]);
71
+    }
72
+
73
+    protected function buildReport(array $columns): ReportDTO
74
+    {
75
+        return $this->reportService->buildTrialBalanceReport($this->startDate, $this->endDate, $columns);
76
+    }
77
+
78
+    protected function getTransformer(ReportDTO $reportDTO): ExportableReport
79
+    {
80
+        return new TrialBalanceReportTransformer($reportDTO);
81
+    }
82
+
83
+    public function exportCSV(): StreamedResponse
84
+    {
85
+        return $this->exportService->exportToCsv($this->company, $this->report, $this->startDate, $this->endDate);
86
+    }
87
+
88
+    public function exportPDF(): StreamedResponse
89
+    {
90
+        return $this->exportService->exportToPdf($this->company, $this->report, $this->startDate, $this->endDate);
91
+    }
92
+}

+ 9
- 4
app/Filament/Company/Resources/Banking/AccountResource.php View File

11
 use Filament\Forms;
11
 use Filament\Forms;
12
 use Filament\Forms\Form;
12
 use Filament\Forms\Form;
13
 use Filament\Resources\Resource;
13
 use Filament\Resources\Resource;
14
+use Filament\Support\Enums\Alignment;
14
 use Filament\Support\Enums\FontWeight;
15
 use Filament\Support\Enums\FontWeight;
15
 use Filament\Tables;
16
 use Filament\Tables;
16
 use Filament\Tables\Table;
17
 use Filament\Tables\Table;
111
                     ->icon(static fn (BankAccount $record) => $record->isEnabled() ? 'heroicon-o-lock-closed' : null)
112
                     ->icon(static fn (BankAccount $record) => $record->isEnabled() ? 'heroicon-o-lock-closed' : null)
112
                     ->tooltip(static fn (BankAccount $record) => $record->isEnabled() ? 'Default Account' : null)
113
                     ->tooltip(static fn (BankAccount $record) => $record->isEnabled() ? 'Default Account' : null)
113
                     ->iconPosition('after')
114
                     ->iconPosition('after')
114
-                    ->description(static fn (BankAccount $record) => $record->mask ?? null)
115
-                    ->sortable(),
115
+                    ->description(static fn (BankAccount $record) => $record->mask ?? null),
116
+                Tables\Columns\TextColumn::make('account.subtype.name')
117
+                    ->localizeLabel('Subtype')
118
+                    ->sortable()
119
+                    ->toggleable(),
116
                 Tables\Columns\TextColumn::make('account.ending_balance')
120
                 Tables\Columns\TextColumn::make('account.ending_balance')
117
-                    ->localizeLabel('Current Balance')
121
+                    ->localizeLabel('Ending Balance')
118
                     ->state(static fn (BankAccount $record) => $record->account->ending_balance->convert()->formatWithCode())
122
                     ->state(static fn (BankAccount $record) => $record->account->ending_balance->convert()->formatWithCode())
119
-                    ->sortable(),
123
+                    ->toggleable()
124
+                    ->alignment(Alignment::End),
120
             ])
125
             ])
121
             ->filters([
126
             ->filters([
122
                 //
127
                 //

+ 1
- 1
app/Filament/Company/Resources/Banking/AccountResource/Pages/CreateAccount.php View File

11
 
11
 
12
     protected function getRedirectUrl(): string
12
     protected function getRedirectUrl(): string
13
     {
13
     {
14
-        return $this->previousUrl;
14
+        return $this->getResource()::getUrl('index');
15
     }
15
     }
16
 
16
 
17
     protected function mutateFormDataBeforeCreate(array $data): array
17
     protected function mutateFormDataBeforeCreate(array $data): array

+ 2
- 2
app/Filament/Company/Resources/Banking/AccountResource/Pages/EditAccount.php View File

19
         ];
19
         ];
20
     }
20
     }
21
 
21
 
22
-    protected function getRedirectUrl(): ?string
22
+    protected function getRedirectUrl(): string
23
     {
23
     {
24
-        return $this->previousUrl;
24
+        return $this->getResource()::getUrl('index');
25
     }
25
     }
26
 
26
 
27
     protected function mutateFormDataBeforeSave(array $data): array
27
     protected function mutateFormDataBeforeSave(array $data): array

+ 1
- 1
app/Filament/Company/Resources/Core/DepartmentResource/Pages/CreateDepartment.php View File

11
 
11
 
12
     protected function getRedirectUrl(): string
12
     protected function getRedirectUrl(): string
13
     {
13
     {
14
-        return $this->previousUrl;
14
+        return $this->getResource()::getUrl('index');
15
     }
15
     }
16
 }
16
 }

+ 2
- 2
app/Filament/Company/Resources/Core/DepartmentResource/Pages/EditDepartment.php View File

17
         ];
17
         ];
18
     }
18
     }
19
 
19
 
20
-    protected function getRedirectUrl(): ?string
20
+    protected function getRedirectUrl(): string
21
     {
21
     {
22
-        return $this->previousUrl;
22
+        return $this->getResource()::getUrl('index');
23
     }
23
     }
24
 }
24
 }

+ 4
- 4
app/Http/Controllers/PlaidWebhookController.php View File

29
     private function handleDefaultUpdate(array $payload): void
29
     private function handleDefaultUpdate(array $payload): void
30
     {
30
     {
31
         $newTransactions = $payload['new_transactions'];
31
         $newTransactions = $payload['new_transactions'];
32
-        $itemID = $payload['item_id'];
32
+        $itemId = $payload['item_id'];
33
 
33
 
34
-        $company = Company::whereHas('connectedBankAccounts', static function ($query) use ($itemID) {
35
-            $query->where('item_id', $itemID);
34
+        $company = Company::whereHas('connectedBankAccounts', static function ($query) use ($itemId) {
35
+            $query->where('item_id', $itemId);
36
         })->first();
36
         })->first();
37
 
37
 
38
         if ($company && $newTransactions > 0) {
38
         if ($company && $newTransactions > 0) {
39
-            ProcessTransactionUpdate::dispatch($company, $itemID)
39
+            ProcessTransactionUpdate::dispatch($company, $itemId)
40
                 ->onQueue('transactions');
40
                 ->onQueue('transactions');
41
         }
41
         }
42
     }
42
     }

+ 7
- 21
app/Jobs/ProcessTransactionImport.php View File

22
     use Queueable;
22
     use Queueable;
23
     use SerializesModels;
23
     use SerializesModels;
24
 
24
 
25
-    protected Company $company;
26
-
27
-    protected Account $account;
28
-
29
-    protected BankAccount $bankAccount;
30
-
31
-    protected ConnectedBankAccount $connectedBankAccount;
32
-
33
-    protected string $startDate;
34
-
35
-    /**
36
-     * Create a new job instance.
37
-     */
38
-    public function __construct(Company $company, Account $account, BankAccount $bankAccount, ConnectedBankAccount $connectedBankAccount, string $startDate)
39
-    {
40
-        $this->company = $company;
41
-        $this->account = $account;
42
-        $this->bankAccount = $bankAccount;
43
-        $this->connectedBankAccount = $connectedBankAccount;
44
-        $this->startDate = $startDate;
45
-    }
25
+    public function __construct(
26
+        protected Company $company,
27
+        protected Account $account,
28
+        protected BankAccount $bankAccount,
29
+        protected ConnectedBankAccount $connectedBankAccount,
30
+        protected string $startDate
31
+    ) {}
46
 
32
 
47
     /**
33
     /**
48
      * Execute the job.
34
      * Execute the job.

+ 5
- 13
app/Jobs/ProcessTransactionUpdate.php View File

20
     use Queueable;
20
     use Queueable;
21
     use SerializesModels;
21
     use SerializesModels;
22
 
22
 
23
-    protected Company $company;
24
-
25
-    protected string $item_id;
26
-
27
-    /**
28
-     * Create a new job instance.
29
-     */
30
-    public function __construct(Company $company, string $item_id)
31
-    {
32
-        $this->company = $company;
33
-        $this->item_id = $item_id;
34
-    }
23
+    public function __construct(
24
+        protected Company $company,
25
+        protected string $itemId
26
+    ) {}
35
 
27
 
36
     /**
28
     /**
37
      * Execute the job.
29
      * Execute the job.
39
     public function handle(PlaidService $plaidService, TransactionService $transactionService): void
31
     public function handle(PlaidService $plaidService, TransactionService $transactionService): void
40
     {
32
     {
41
         $connectedBankAccounts = $this->company->connectedBankAccounts()
33
         $connectedBankAccounts = $this->company->connectedBankAccounts()
42
-            ->where('item_id', $this->item_id)
34
+            ->where('item_id', $this->itemId)
43
             ->where('import_transactions', true)
35
             ->where('import_transactions', true)
44
             ->get();
36
             ->get();
45
 
37
 

+ 1
- 2
app/Listeners/ConfigureChartOfAccounts.php View File

72
                     'name' => $accountName,
72
                     'name' => $accountName,
73
                     'currency_code' => CurrencyAccessor::getDefaultCurrency(),
73
                     'currency_code' => CurrencyAccessor::getDefaultCurrency(),
74
                     'description' => $accountDetails['description'] ?? 'No description available.',
74
                     'description' => $accountDetails['description'] ?? 'No description available.',
75
-                    'active' => true,
76
                     'default' => true,
75
                     'default' => true,
77
                     'created_by' => $company->owner->id,
76
                     'created_by' => $company->owner->id,
78
                     'updated_by' => $company->owner->id,
77
                     'updated_by' => $company->owner->id,
79
                 ]);
78
                 ]);
80
 
79
 
81
                 if ($bankAccount) {
80
                 if ($bankAccount) {
82
-                    $account->accountable()->associate($bankAccount);
81
+                    $account->bankAccount()->associate($bankAccount);
83
                 }
82
                 }
84
 
83
 
85
                 $account->save();
84
                 $account->save();

+ 5
- 8
app/Listeners/CreateConnectedAccount.php View File

10
 
10
 
11
 class CreateConnectedAccount
11
 class CreateConnectedAccount
12
 {
12
 {
13
-    protected PlaidService $plaid;
14
-
15
     /**
13
     /**
16
      * Create the event listener.
14
      * Create the event listener.
17
      */
15
      */
18
-    public function __construct(PlaidService $plaid)
19
-    {
20
-        $this->plaid = $plaid;
21
-    }
16
+    public function __construct(
17
+        protected PlaidService $plaidService
18
+    ) {}
22
 
19
 
23
     /**
20
     /**
24
      * Handle the event.
21
      * Handle the event.
36
 
33
 
37
         $company = $event->company;
34
         $company = $event->company;
38
 
35
 
39
-        $authResponse = $this->plaid->getAccounts($accessToken);
36
+        $authResponse = $this->plaidService->getAccounts($accessToken);
40
 
37
 
41
-        $institutionResponse = $this->plaid->getInstitution($authResponse->item->institution_id, $company->profile->country);
38
+        $institutionResponse = $this->plaidService->getInstitution($authResponse->item->institution_id, $company->profile->country);
42
 
39
 
43
         $this->processInstitution($authResponse, $institutionResponse, $company, $accessToken);
40
         $this->processInstitution($authResponse, $institutionResponse, $company, $accessToken);
44
     }
41
     }

+ 3
- 6
app/Listeners/HandleTransactionImport.php View File

9
 
9
 
10
 class HandleTransactionImport
10
 class HandleTransactionImport
11
 {
11
 {
12
-    protected ConnectedBankAccountService $connectedBankAccountService;
13
-
14
     /**
12
     /**
15
      * Create the event listener.
13
      * Create the event listener.
16
      */
14
      */
17
-    public function __construct(ConnectedBankAccountService $connectedBankAccountService)
18
-    {
19
-        $this->connectedBankAccountService = $connectedBankAccountService;
20
-    }
15
+    public function __construct(
16
+        protected ConnectedBankAccountService $connectedBankAccountService
17
+    ) {}
21
 
18
 
22
     /**
19
     /**
23
      * Handle the event.
20
      * Handle the event.

+ 3
- 4
app/Listeners/UpdateCurrencyRates.php View File

13
     /**
13
     /**
14
      * Create the event listener.
14
      * Create the event listener.
15
      */
15
      */
16
-    public function __construct(private CurrencyHandler $currencyService)
17
-    {
18
-        //
19
-    }
16
+    public function __construct(
17
+        private CurrencyHandler $currencyService
18
+    ) {}
20
 
19
 
21
     /**
20
     /**
22
      * Handle the event.
21
      * Handle the event.

+ 6
- 7
app/Models/Accounting/Account.php View File

7
 use App\Enums\Accounting\AccountCategory;
7
 use App\Enums\Accounting\AccountCategory;
8
 use App\Enums\Accounting\AccountType;
8
 use App\Enums\Accounting\AccountType;
9
 use App\Facades\Accounting;
9
 use App\Facades\Accounting;
10
+use App\Models\Banking\BankAccount;
10
 use App\Models\Setting\Currency;
11
 use App\Models\Setting\Currency;
11
 use App\Observers\AccountObserver;
12
 use App\Observers\AccountObserver;
12
 use Database\Factories\Accounting\AccountFactory;
13
 use Database\Factories\Accounting\AccountFactory;
17
 use Illuminate\Database\Eloquent\Model;
18
 use Illuminate\Database\Eloquent\Model;
18
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
19
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
19
 use Illuminate\Database\Eloquent\Relations\HasMany;
20
 use Illuminate\Database\Eloquent\Relations\HasMany;
20
-use Illuminate\Database\Eloquent\Relations\MorphTo;
21
 use Illuminate\Support\Carbon;
21
 use Illuminate\Support\Carbon;
22
 
22
 
23
 #[ObservedBy(AccountObserver::class)]
23
 #[ObservedBy(AccountObserver::class)]
39
         'name',
39
         'name',
40
         'currency_code',
40
         'currency_code',
41
         'description',
41
         'description',
42
-        'active',
42
+        'archived',
43
         'default',
43
         'default',
44
-        'accountable_id',
45
-        'accountable_type',
44
+        'bank_account_id',
46
         'created_by',
45
         'created_by',
47
         'updated_by',
46
         'updated_by',
48
     ];
47
     ];
50
     protected $casts = [
49
     protected $casts = [
51
         'category' => AccountCategory::class,
50
         'category' => AccountCategory::class,
52
         'type' => AccountType::class,
51
         'type' => AccountType::class,
53
-        'active' => 'boolean',
52
+        'archived' => 'boolean',
54
         'default' => 'boolean',
53
         'default' => 'boolean',
55
     ];
54
     ];
56
 
55
 
75
         return $this->belongsTo(Currency::class, 'currency_code', 'code');
74
         return $this->belongsTo(Currency::class, 'currency_code', 'code');
76
     }
75
     }
77
 
76
 
78
-    public function accountable(): MorphTo
77
+    public function bankAccount(): BelongsTo
79
     {
78
     {
80
-        return $this->morphTo();
79
+        return $this->belongsTo(BankAccount::class, 'bank_account_id');
81
     }
80
     }
82
 
81
 
83
     public function getLastTransactionDate(): ?string
82
     public function getLastTransactionDate(): ?string

+ 0
- 6
app/Models/Accounting/JournalEntry.php View File

7
 use App\Concerns\Blamable;
7
 use App\Concerns\Blamable;
8
 use App\Concerns\CompanyOwned;
8
 use App\Concerns\CompanyOwned;
9
 use App\Enums\Accounting\JournalEntryType;
9
 use App\Enums\Accounting\JournalEntryType;
10
-use App\Models\Banking\BankAccount;
11
 use Database\Factories\Accounting\JournalEntryFactory;
10
 use Database\Factories\Accounting\JournalEntryFactory;
12
 use Illuminate\Database\Eloquent\Factories\Factory;
11
 use Illuminate\Database\Eloquent\Factories\Factory;
13
 use Illuminate\Database\Eloquent\Factories\HasFactory;
12
 use Illuminate\Database\Eloquent\Factories\HasFactory;
46
         return $this->belongsTo(Transaction::class, 'transaction_id');
45
         return $this->belongsTo(Transaction::class, 'transaction_id');
47
     }
46
     }
48
 
47
 
49
-    public function bankAccount(): BelongsTo
50
-    {
51
-        return $this->account()->where('accountable_type', BankAccount::class);
52
-    }
53
-
54
     public function isUncategorized(): bool
48
     public function isUncategorized(): bool
55
     {
49
     {
56
         return $this->account->isUncategorized();
50
         return $this->account->isUncategorized();

+ 2
- 3
app/Models/Banking/BankAccount.php View File

19
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
19
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
20
 use Illuminate\Database\Eloquent\Relations\HasMany;
20
 use Illuminate\Database\Eloquent\Relations\HasMany;
21
 use Illuminate\Database\Eloquent\Relations\HasOne;
21
 use Illuminate\Database\Eloquent\Relations\HasOne;
22
-use Illuminate\Database\Eloquent\Relations\MorphOne;
23
 
22
 
24
 #[ObservedBy(BankAccountObserver::class)]
23
 #[ObservedBy(BankAccountObserver::class)]
25
 class BankAccount extends Model
24
 class BankAccount extends Model
56
         return $this->hasOne(ConnectedBankAccount::class, 'bank_account_id');
55
         return $this->hasOne(ConnectedBankAccount::class, 'bank_account_id');
57
     }
56
     }
58
 
57
 
59
-    public function account(): MorphOne
58
+    public function account(): HasOne
60
     {
59
     {
61
-        return $this->morphOne(Account::class, 'accountable');
60
+        return $this->hasOne(Account::class, 'bank_account_id');
62
     }
61
     }
63
 
62
 
64
     public function institution(): BelongsTo
63
     public function institution(): BelongsTo

+ 3
- 28
app/Models/Banking/Institution.php View File

7
 use Illuminate\Database\Eloquent\Factories\HasFactory;
7
 use Illuminate\Database\Eloquent\Factories\HasFactory;
8
 use Illuminate\Database\Eloquent\Model;
8
 use Illuminate\Database\Eloquent\Model;
9
 use Illuminate\Database\Eloquent\Relations\HasMany;
9
 use Illuminate\Database\Eloquent\Relations\HasMany;
10
-use Illuminate\Support\Carbon;
10
+use Illuminate\Database\Eloquent\Relations\HasOne;
11
 use Illuminate\Support\Facades\Storage;
11
 use Illuminate\Support\Facades\Storage;
12
 
12
 
13
 class Institution extends Model
13
 class Institution extends Model
46
         return $this->hasMany(ConnectedBankAccount::class, 'institution_id');
46
         return $this->hasMany(ConnectedBankAccount::class, 'institution_id');
47
     }
47
     }
48
 
48
 
49
-    public function getLastTransactionDate(): ?string
49
+    public function latestImport(): HasOne
50
     {
50
     {
51
-        $latestDate = $this->connectedBankAccounts->map(function ($connectedBankAccount) {
52
-            if ($connectedBankAccount->bankAccount) {
53
-                return $connectedBankAccount->bankAccount->transactions()->max('posted_at');
54
-            }
55
-
56
-            return null;
57
-        })->filter()->max();
58
-
59
-        if ($latestDate) {
60
-            return Carbon::parse($latestDate)->diffForHumans();
61
-        }
62
-
63
-        return null;
64
-    }
65
-
66
-    public function getLastImportDate(): ?string
67
-    {
68
-        $latestDate = $this->connectedBankAccounts->map(function ($connectedBankAccount) {
69
-            return $connectedBankAccount->last_imported_at;
70
-        })->filter()->max();
71
-
72
-        if ($latestDate) {
73
-            return Carbon::parse($latestDate)->diffForHumans();
74
-        }
75
-
76
-        return null;
51
+        return $this->hasOne(ConnectedBankAccount::class, 'institution_id')->latestOfMany('last_imported_at');
77
     }
52
     }
78
 
53
 
79
     protected function logoUrl(): Attribute
54
     protected function logoUrl(): Attribute

+ 11
- 4
app/Observers/AccountObserver.php View File

6
 use App\Enums\Accounting\AccountType;
6
 use App\Enums\Accounting\AccountType;
7
 use App\Models\Accounting\Account;
7
 use App\Models\Accounting\Account;
8
 use App\Models\Accounting\AccountSubtype;
8
 use App\Models\Accounting\AccountSubtype;
9
-use App\Models\Banking\BankAccount;
10
 use App\Utilities\Accounting\AccountCode;
9
 use App\Utilities\Accounting\AccountCode;
10
+use App\Utilities\Currency\CurrencyAccessor;
11
 
11
 
12
 class AccountObserver
12
 class AccountObserver
13
 {
13
 {
14
     public function creating(Account $account): void
14
     public function creating(Account $account): void
15
     {
15
     {
16
         $this->setCategoryAndType($account, true);
16
         $this->setCategoryAndType($account, true);
17
+        $this->setCurrency($account);
17
     }
18
     }
18
 
19
 
19
     public function updating(Account $account): void
20
     public function updating(Account $account): void
36
         }
37
         }
37
     }
38
     }
38
 
39
 
40
+    private function setCurrency(Account $account): void
41
+    {
42
+        if ($account->currency_code === null && $account->subtype->multi_currency === false) {
43
+            $account->currency_code = CurrencyAccessor::getDefaultCurrency();
44
+        }
45
+    }
46
+
39
     private function setFieldsForBankAccount(Account $account): void
47
     private function setFieldsForBankAccount(Account $account): void
40
     {
48
     {
41
         $generatedAccountCode = AccountCode::generate($account->subtype);
49
         $generatedAccountCode = AccountCode::generate($account->subtype);
42
 
50
 
43
         $account->code = $generatedAccountCode;
51
         $account->code = $generatedAccountCode;
44
-
45
-        $account->save();
46
     }
52
     }
47
 
53
 
48
     /**
54
     /**
50
      */
56
      */
51
     public function created(Account $account): void
57
     public function created(Account $account): void
52
     {
58
     {
53
-        if (($account->accountable_type === BankAccount::class) && $account->code === null) {
59
+        if ($account->bankAccount && $account->code === null) {
54
             $this->setFieldsForBankAccount($account);
60
             $this->setFieldsForBankAccount($account);
61
+            $account->save();
55
         }
62
         }
56
     }
63
     }
57
 }
64
 }

+ 1
- 3
app/Providers/CurrencyServiceProvider.php View File

21
         });
21
         });
22
     }
22
     }
23
 
23
 
24
-    public function boot(): void
25
-    {
26
-    }
24
+    public function boot(): void {}
27
 }
25
 }

+ 0
- 88
app/Providers/EventServiceProvider.php View File

1
-<?php
2
-
3
-namespace App\Providers;
4
-
5
-use App\Events\CompanyConfigured;
6
-use App\Events\CompanyDefaultEvent;
7
-use App\Events\CompanyDefaultUpdated;
8
-use App\Events\CompanyGenerated;
9
-use App\Events\CurrencyRateChanged;
10
-use App\Events\DefaultCurrencyChanged;
11
-use App\Events\PlaidSuccess;
12
-use App\Events\StartTransactionImport;
13
-use App\Listeners\ConfigureChartOfAccounts;
14
-use App\Listeners\ConfigureCompanyDefault;
15
-use App\Listeners\CreateCompanyDefaults;
16
-use App\Listeners\CreateConnectedAccount;
17
-use App\Listeners\HandleTransactionImport;
18
-use App\Listeners\SyncAssociatedModels;
19
-use App\Listeners\SyncWithCompanyDefaults;
20
-use App\Listeners\UpdateAccountBalances;
21
-use App\Listeners\UpdateCurrencyRates;
22
-use Illuminate\Auth\Events\Registered;
23
-use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
24
-use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
25
-
26
-class EventServiceProvider extends ServiceProvider
27
-{
28
-    /**
29
-     * The event to listener mappings for the application.
30
-     *
31
-     * @var array<class-string, array<int, class-string>>
32
-     */
33
-    protected $listen = [
34
-        Registered::class => [
35
-            SendEmailVerificationNotification::class,
36
-        ],
37
-        CompanyDefaultEvent::class => [
38
-            SyncWithCompanyDefaults::class,
39
-        ],
40
-        CompanyDefaultUpdated::class => [
41
-            SyncAssociatedModels::class,
42
-        ],
43
-        CompanyConfigured::class => [
44
-            ConfigureCompanyDefault::class,
45
-        ],
46
-        CompanyGenerated::class => [
47
-            CreateCompanyDefaults::class,
48
-            ConfigureChartOfAccounts::class,
49
-        ],
50
-        DefaultCurrencyChanged::class => [
51
-            UpdateCurrencyRates::class,
52
-        ],
53
-        CurrencyRateChanged::class => [
54
-            UpdateAccountBalances::class,
55
-        ],
56
-        PlaidSuccess::class => [
57
-            CreateConnectedAccount::class,
58
-        ],
59
-        StartTransactionImport::class => [
60
-            HandleTransactionImport::class,
61
-        ],
62
-    ];
63
-
64
-    /**
65
-     * The model observers to register.
66
-     *
67
-     * @var array<string, string|object|array<int, string|object>>
68
-     */
69
-    protected $observers = [
70
-        // Currency::class => [CurrencyObserver::class],
71
-    ];
72
-
73
-    /**
74
-     * Register any events for your application.
75
-     */
76
-    public function boot(): void
77
-    {
78
-        //
79
-    }
80
-
81
-    /**
82
-     * Determine if events and listeners should be automatically discovered.
83
-     */
84
-    public function shouldDiscoverEvents(): bool
85
-    {
86
-        return false;
87
-    }
88
-}

+ 1
- 4
app/Providers/SquireServiceProvider.php View File

10
 
10
 
11
 class SquireServiceProvider extends ServiceProvider
11
 class SquireServiceProvider extends ServiceProvider
12
 {
12
 {
13
-    public function register(): void
14
-    {
15
-
16
-    }
13
+    public function register(): void {}
17
 
14
 
18
     public function boot(): void
15
     public function boot(): void
19
     {
16
     {

+ 0
- 1
app/Repositories/Banking/ConnectedBankAccountRepository.php View File

29
             'currency_code' => $connectedBankAccount->currency_code,
29
             'currency_code' => $connectedBankAccount->currency_code,
30
             'description' => $connectedBankAccount->name,
30
             'description' => $connectedBankAccount->name,
31
             'subtype_id' => $accountSubtype->id,
31
             'subtype_id' => $accountSubtype->id,
32
-            'active' => true,
33
         ]);
32
         ]);
34
     }
33
     }
35
 }
34
 }

+ 0
- 76
app/Services/AccountBalancesExportService.php View File

1
-<?php
2
-
3
-namespace App\Services;
4
-
5
-use App\DTO\AccountBalanceReportDTO;
6
-use App\Models\Company;
7
-use Symfony\Component\HttpFoundation\StreamedResponse;
8
-
9
-class AccountBalancesExportService
10
-{
11
-    public function exportToCsv(Company $company, AccountBalanceReportDTO $accountBalanceReport, string $startDate, string $endDate): StreamedResponse
12
-    {
13
-        // Construct the filename
14
-        $filename = $company->name . ' Account Balances ' . $startDate . ' to ' . $endDate . '.csv';
15
-
16
-        $headers = [
17
-            'Content-Type' => 'text/csv',
18
-            'Content-Disposition' => 'attachment; filename="' . $filename . '"',
19
-        ];
20
-
21
-        $callback = static function () use ($company, $startDate, $endDate, $accountBalanceReport) {
22
-            $file = fopen('php://output', 'wb');
23
-
24
-            fputcsv($file, ['Account Balances']);
25
-            fputcsv($file, [$company->name]);
26
-            fputcsv($file, ['Date Range: ' . $startDate . ' to ' . $endDate]);
27
-            fputcsv($file, []);
28
-
29
-            fputcsv($file, ['ACCOUNT CODE', 'ACCOUNT', 'STARTING BALANCE', 'DEBIT', 'CREDIT', 'NET MOVEMENT', 'ENDING BALANCE']);
30
-
31
-            foreach ($accountBalanceReport->categories as $accountCategoryName => $accountCategory) {
32
-                fputcsv($file, ['', $accountCategoryName]);
33
-
34
-                foreach ($accountCategory->accounts as $account) {
35
-                    fputcsv($file, [
36
-                        $account->accountCode,
37
-                        $account->accountName,
38
-                        $account->balance->startingBalance ?? '',
39
-                        $account->balance->debitBalance,
40
-                        $account->balance->creditBalance,
41
-                        $account->balance->netMovement,
42
-                        $account->balance->endingBalance ?? '',
43
-                    ]);
44
-                }
45
-
46
-                // Category Summary row
47
-                fputcsv($file, [
48
-                    '',
49
-                    'Total ' . $accountCategoryName,
50
-                    $accountCategory->summary->startingBalance ?? '',
51
-                    $accountCategory->summary->debitBalance,
52
-                    $accountCategory->summary->creditBalance,
53
-                    $accountCategory->summary->netMovement,
54
-                    $accountCategory->summary->endingBalance ?? '',
55
-                ]);
56
-
57
-                fputcsv($file, []);
58
-            }
59
-
60
-            // Final Row for overall totals
61
-            fputcsv($file, [
62
-                '',
63
-                'Total for all accounts',
64
-                '',
65
-                $accountBalanceReport->overallTotal->debitBalance,
66
-                $accountBalanceReport->overallTotal->creditBalance,
67
-                '',
68
-                '',
69
-            ]);
70
-
71
-            fclose($file);
72
-        };
73
-
74
-        return response()->streamDownload($callback, $filename, $headers);
75
-    }
76
-}

+ 15
- 104
app/Services/AccountService.php View File

3
 namespace App\Services;
3
 namespace App\Services;
4
 
4
 
5
 use App\Contracts\AccountHandler;
5
 use App\Contracts\AccountHandler;
6
-use App\DTO\AccountBalanceDTO;
7
-use App\DTO\AccountBalanceReportDTO;
8
-use App\DTO\AccountCategoryDTO;
9
-use App\DTO\AccountDTO;
10
 use App\Enums\Accounting\AccountCategory;
6
 use App\Enums\Accounting\AccountCategory;
11
 use App\Models\Accounting\Account;
7
 use App\Models\Accounting\Account;
12
 use App\Models\Accounting\Transaction;
8
 use App\Models\Accounting\Transaction;
14
 use App\Repositories\Accounting\JournalEntryRepository;
10
 use App\Repositories\Accounting\JournalEntryRepository;
15
 use App\Utilities\Currency\CurrencyAccessor;
11
 use App\Utilities\Currency\CurrencyAccessor;
16
 use App\ValueObjects\Money;
12
 use App\ValueObjects\Money;
17
-use Illuminate\Database\Eloquent\Collection;
18
 
13
 
19
 class AccountService implements AccountHandler
14
 class AccountService implements AccountHandler
20
 {
15
 {
21
-    protected JournalEntryRepository $journalEntryRepository;
22
-
23
-    public function __construct(JournalEntryRepository $journalEntryRepository)
24
-    {
25
-        $this->journalEntryRepository = $journalEntryRepository;
26
-    }
16
+    public function __construct(
17
+        protected JournalEntryRepository $journalEntryRepository
18
+    ) {}
27
 
19
 
28
     public function getDebitBalance(Account $account, string $startDate, string $endDate): Money
20
     public function getDebitBalance(Account $account, string $startDate, string $endDate): Money
29
     {
21
     {
63
 
55
 
64
     public function getEndingBalance(Account $account, string $startDate, string $endDate): ?Money
56
     public function getEndingBalance(Account $account, string $startDate, string $endDate): ?Money
65
     {
57
     {
58
+        $netMovement = $this->getNetMovement($account, $startDate, $endDate)->getAmount();
59
+
66
         if (in_array($account->category, [AccountCategory::Expense, AccountCategory::Revenue], true)) {
60
         if (in_array($account->category, [AccountCategory::Expense, AccountCategory::Revenue], true)) {
67
-            return null;
61
+            return new Money($netMovement, $account->currency_code);
68
         }
62
         }
69
 
63
 
70
         $startingBalance = $this->getStartingBalance($account, $startDate)?->getAmount();
64
         $startingBalance = $this->getStartingBalance($account, $startDate)?->getAmount();
71
-        $netMovement = $this->getNetMovement($account, $startDate, $endDate)->getAmount();
72
         $endingBalance = $startingBalance + $netMovement;
65
         $endingBalance = $startingBalance + $netMovement;
73
 
66
 
74
         return new Money($endingBalance, $account->currency_code);
67
         return new Money($endingBalance, $account->currency_code);
75
     }
68
     }
76
 
69
 
77
-    public function calculateNetMovementByCategory(AccountCategory $category, int $debitBalance, int $creditBalance): int
70
+    private function calculateNetMovementByCategory(AccountCategory $category, int $debitBalance, int $creditBalance): int
78
     {
71
     {
79
         return match ($category) {
72
         return match ($category) {
80
             AccountCategory::Asset, AccountCategory::Expense => $debitBalance - $creditBalance,
73
             AccountCategory::Asset, AccountCategory::Expense => $debitBalance - $creditBalance,
102
         return $balances;
95
         return $balances;
103
     }
96
     }
104
 
97
 
105
-    public function formatBalances(array $balances): AccountBalanceDTO
106
-    {
107
-        $defaultCurrency = CurrencyAccessor::getDefaultCurrency();
108
-
109
-        foreach ($balances as $key => $balance) {
110
-            $balances[$key] = money($balance, $defaultCurrency)->format();
111
-        }
112
-
113
-        return new AccountBalanceDTO(
114
-            startingBalance: $balances['starting_balance'] ?? null,
115
-            debitBalance: $balances['debit_balance'],
116
-            creditBalance: $balances['credit_balance'],
117
-            netMovement: $balances['net_movement'] ?? null,
118
-            endingBalance: $balances['ending_balance'] ?? null,
119
-        );
120
-    }
121
-
122
-    public function buildAccountBalanceReport(string $startDate, string $endDate): AccountBalanceReportDTO
123
-    {
124
-        $allCategories = $this->getAccountCategoryOrder();
125
-
126
-        $categoryGroupedAccounts = Account::whereHas('journalEntries')
127
-            ->select('id', 'name', 'currency_code', 'category', 'code')
128
-            ->get()
129
-            ->groupBy(fn (Account $account) => $account->category->getPluralLabel())
130
-            ->sortBy(static fn (Collection $groupedAccounts, string $key) => array_search($key, $allCategories, true));
131
-
132
-        $accountCategories = [];
133
-        $reportTotalBalances = [
134
-            'debit_balance' => 0,
135
-            'credit_balance' => 0,
136
-        ];
137
-
138
-        foreach ($allCategories as $categoryName) {
139
-            $accountsInCategory = $categoryGroupedAccounts[$categoryName] ?? collect();
140
-            $categorySummaryBalances = [
141
-                'debit_balance' => 0,
142
-                'credit_balance' => 0,
143
-                'net_movement' => 0,
144
-            ];
145
-
146
-            if (! in_array($categoryName, [AccountCategory::Expense->getPluralLabel(), AccountCategory::Revenue->getPluralLabel()], true)) {
147
-                $categorySummaryBalances['starting_balance'] = 0;
148
-                $categorySummaryBalances['ending_balance'] = 0;
149
-            }
150
-
151
-            $categoryAccounts = [];
152
-
153
-            foreach ($accountsInCategory as $account) {
154
-                /** @var Account $account */
155
-                $accountBalances = $this->getBalances($account, $startDate, $endDate);
156
-
157
-                if (array_sum($accountBalances) === 0) {
158
-                    continue;
159
-                }
160
-
161
-                foreach ($accountBalances as $accountBalanceType => $accountBalance) {
162
-                    $categorySummaryBalances[$accountBalanceType] += $accountBalance;
163
-                }
164
-
165
-                $formattedAccountBalances = $this->formatBalances($accountBalances);
166
-
167
-                $categoryAccounts[] = new AccountDTO(
168
-                    $account->name,
169
-                    $account->code,
170
-                    $formattedAccountBalances,
171
-                );
172
-            }
173
-
174
-            $reportTotalBalances['debit_balance'] += $categorySummaryBalances['debit_balance'];
175
-            $reportTotalBalances['credit_balance'] += $categorySummaryBalances['credit_balance'];
176
-
177
-            $formattedCategorySummaryBalances = $this->formatBalances($categorySummaryBalances);
178
-
179
-            $accountCategories[$categoryName] = new AccountCategoryDTO(
180
-                $categoryAccounts,
181
-                $formattedCategorySummaryBalances,
182
-            );
183
-        }
184
-
185
-        $formattedReportTotalBalances = $this->formatBalances($reportTotalBalances);
186
-
187
-        return new AccountBalanceReportDTO($accountCategories, $formattedReportTotalBalances);
188
-    }
189
-
190
     public function getTotalBalanceForAllBankAccounts(string $startDate, string $endDate): Money
98
     public function getTotalBalanceForAllBankAccounts(string $startDate, string $endDate): Money
191
     {
99
     {
192
-        $bankAccountsAccounts = Account::where('accountable_type', BankAccount::class)
100
+        $bankAccounts = BankAccount::with('account')
193
             ->get();
101
             ->get();
194
 
102
 
195
         $totalBalance = 0;
103
         $totalBalance = 0;
196
 
104
 
197
-        // Get ending balance for each bank account
198
-        foreach ($bankAccountsAccounts as $account) {
199
-            $endingBalance = $this->getEndingBalance($account, $startDate, $endDate)?->getAmount() ?? 0;
200
-            $totalBalance += $endingBalance;
105
+        foreach ($bankAccounts as $bankAccount) {
106
+            $account = $bankAccount->account;
107
+
108
+            if ($account) {
109
+                $endingBalance = $this->getEndingBalance($account, $startDate, $endDate)?->getAmount() ?? 0;
110
+                $totalBalance += $endingBalance;
111
+            }
201
         }
112
         }
202
 
113
 
203
         return new Money($totalBalance, CurrencyAccessor::getDefaultCurrency());
114
         return new Money($totalBalance, CurrencyAccessor::getDefaultCurrency());

+ 4
- 9
app/Services/ConnectedBankAccountService.php View File

11
 
11
 
12
 class ConnectedBankAccountService
12
 class ConnectedBankAccountService
13
 {
13
 {
14
-    protected AccountSubtypeRepository $accountSubtypeRepository;
15
-
16
-    protected ConnectedBankAccountRepository $connectedBankAccountRepository;
17
-
18
-    public function __construct(AccountSubtypeRepository $accountSubtypeRepository, ConnectedBankAccountRepository $connectedBankAccountRepository)
19
-    {
20
-        $this->accountSubtypeRepository = $accountSubtypeRepository;
21
-        $this->connectedBankAccountRepository = $connectedBankAccountRepository;
22
-    }
14
+    public function __construct(
15
+        protected AccountSubtypeRepository $accountSubtypeRepository,
16
+        protected ConnectedBankAccountRepository $connectedBankAccountRepository
17
+    ) {}
23
 
18
 
24
     public function getOrProcessBankAccountForConnectedBankAccount(Company $company, ConnectedBankAccount $connectedBankAccount, int | string $selectedBankAccountId): BankAccount
19
     public function getOrProcessBankAccountForConnectedBankAccount(Company $company, ConnectedBankAccount $connectedBankAccount, int | string $selectedBankAccountId): BankAccount
25
     {
20
     {

+ 8
- 15
app/Services/CurrencyService.php View File

10
 
10
 
11
 class CurrencyService implements CurrencyHandler
11
 class CurrencyService implements CurrencyHandler
12
 {
12
 {
13
-    protected ?string $api_key;
14
-
15
-    protected ?string $base_url;
16
-
17
-    protected Client $client;
18
-
19
-    public function __construct(?string $api_key, ?string $base_url, Client $client)
20
-    {
21
-        $this->api_key = $api_key;
22
-        $this->base_url = $base_url;
23
-        $this->client = $client;
24
-    }
13
+    public function __construct(
14
+        protected ?string $apiKey,
15
+        protected ?string $baseUrl,
16
+        protected Client $client
17
+    ) {}
25
 
18
 
26
     /**
19
     /**
27
      * Determine if the Currency Exchange Rate feature is enabled.
20
      * Determine if the Currency Exchange Rate feature is enabled.
28
      */
21
      */
29
     public function isEnabled(): bool
22
     public function isEnabled(): bool
30
     {
23
     {
31
-        return filled($this->api_key) && filled($this->base_url);
24
+        return filled($this->apiKey) && filled($this->baseUrl);
32
     }
25
     }
33
 
26
 
34
     public function getSupportedCurrencies(): ?array
27
     public function getSupportedCurrencies(): ?array
38
         }
31
         }
39
 
32
 
40
         return Cache::remember('supported_currency_codes', now()->addMonth(), function () {
33
         return Cache::remember('supported_currency_codes', now()->addMonth(), function () {
41
-            $response = $this->client->get("{$this->base_url}/{$this->api_key}/codes");
34
+            $response = $this->client->get("{$this->baseUrl}/{$this->apiKey}/codes");
42
 
35
 
43
             if ($response->getStatusCode() === 200) {
36
             if ($response->getStatusCode() === 200) {
44
                 $responseData = json_decode($response->getBody()->getContents(), true);
37
                 $responseData = json_decode($response->getBody()->getContents(), true);
106
     public function updateCurrencyRatesCache(string $baseCurrency): ?array
99
     public function updateCurrencyRatesCache(string $baseCurrency): ?array
107
     {
100
     {
108
         try {
101
         try {
109
-            $response = $this->client->get("{$this->base_url}/{$this->api_key}/latest/{$baseCurrency}");
102
+            $response = $this->client->get("{$this->baseUrl}/{$this->apiKey}/latest/{$baseCurrency}");
110
 
103
 
111
             if ($response->getStatusCode() === 200) {
104
             if ($response->getStatusCode() === 200) {
112
                 $responseData = json_decode($response->getBody()->getContents(), true);
105
                 $responseData = json_decode($response->getBody()->getContents(), true);

+ 64
- 0
app/Services/ExportService.php View File

1
+<?php
2
+
3
+namespace App\Services;
4
+
5
+use App\Contracts\ExportableReport;
6
+use App\Models\Company;
7
+use Barryvdh\DomPDF\Facade\Pdf;
8
+use Illuminate\Support\Carbon;
9
+use Symfony\Component\HttpFoundation\StreamedResponse;
10
+
11
+class ExportService
12
+{
13
+    public function exportToCsv(Company $company, ExportableReport $report, string $startDate, string $endDate): StreamedResponse
14
+    {
15
+        $filename = $company->name . ' ' . $report->getTitle() . ' ' . $startDate . ' to ' . $endDate . '.csv';
16
+
17
+        $headers = [
18
+            'Content-Type' => 'text/csv',
19
+            'Content-Disposition' => 'attachment; filename="' . $filename . '"',
20
+        ];
21
+
22
+        $callback = function () use ($report, $company, $startDate, $endDate) {
23
+            $file = fopen('php://output', 'wb');
24
+
25
+            fputcsv($file, [$report->getTitle()]);
26
+            fputcsv($file, [$company->name]);
27
+            fputcsv($file, ['Date Range: ' . $startDate . ' to ' . $endDate]);
28
+            fputcsv($file, []);
29
+
30
+            fputcsv($file, $report->getHeaders());
31
+
32
+            foreach ($report->getCategories() as $category) {
33
+                fputcsv($file, $category->header);
34
+
35
+                foreach ($category->data as $accountRow) {
36
+                    fputcsv($file, $accountRow);
37
+                }
38
+
39
+                fputcsv($file, $category->summary);
40
+                fputcsv($file, []); // Empty row for spacing
41
+            }
42
+
43
+            fputcsv($file, $report->getOverallTotals());
44
+
45
+            fclose($file);
46
+        };
47
+
48
+        return response()->streamDownload($callback, $filename, $headers);
49
+    }
50
+
51
+    public function exportToPdf(Company $company, ExportableReport $report, string $startDate, string $endDate): StreamedResponse
52
+    {
53
+        $pdf = Pdf::loadView('components.company.reports.report-pdf', [
54
+            'company' => $company,
55
+            'report' => $report,
56
+            'startDate' => Carbon::parse($startDate)->format('M d, Y'),
57
+            'endDate' => Carbon::parse($endDate)->format('M d, Y'),
58
+        ])->setPaper('a4');
59
+
60
+        return response()->streamDownload(function () use ($pdf) {
61
+            echo $pdf->stream();
62
+        }, strtolower(str_replace(' ', '-', $company->name . '-' . $report->getTitle())) . '.pdf');
63
+    }
64
+}

+ 47
- 37
app/Services/PlaidService.php View File

15
 {
15
 {
16
     public const API_VERSION = '2020-09-14';
16
     public const API_VERSION = '2020-09-14';
17
 
17
 
18
-    protected ?string $client_id;
18
+    protected ?string $clientId;
19
 
19
 
20
-    protected ?string $client_secret;
20
+    protected ?string $clientSecret;
21
 
21
 
22
     protected ?string $environment;
22
     protected ?string $environment;
23
 
23
 
24
-    protected ?string $webhook_url;
24
+    protected ?string $webhookUrl;
25
 
25
 
26
-    protected ?string $base_url;
26
+    protected ?string $baseUrl;
27
 
27
 
28
     protected HttpClient $client;
28
     protected HttpClient $client;
29
 
29
 
50
     {
50
     {
51
         $this->client = $client;
51
         $this->client = $client;
52
         $this->config = $config;
52
         $this->config = $config;
53
-        $this->client_id = $this->config->get('plaid.client_id');
54
-        $this->client_secret = $this->config->get('plaid.client_secret');
53
+        $this->clientId = $this->config->get('plaid.client_id');
54
+        $this->clientSecret = $this->config->get('plaid.client_secret');
55
         $this->environment = $this->config->get('plaid.environment', 'sandbox');
55
         $this->environment = $this->config->get('plaid.environment', 'sandbox');
56
-        $this->webhook_url = $this->config->get('plaid.webhook_url');
56
+        $this->webhookUrl = $this->config->get('plaid.webhook_url');
57
 
57
 
58
         $this->setBaseUrl($this->environment);
58
         $this->setBaseUrl($this->environment);
59
     }
59
     }
60
 
60
 
61
-    public function setClientCredentials(?string $client_id, ?string $client_secret): self
61
+    public function setClientCredentials(?string $clientId, ?string $clientSecret): self
62
     {
62
     {
63
-        $this->client_id = $client_id ?? $this->client_id;
64
-        $this->client_secret = $client_secret ?? $this->client_secret;
63
+        $this->clientId = $clientId ?? $this->clientId;
64
+        $this->clientSecret = $clientSecret ?? $this->clientSecret;
65
 
65
 
66
         return $this;
66
         return $this;
67
     }
67
     }
77
 
77
 
78
     public function setBaseUrl(?string $environment): void
78
     public function setBaseUrl(?string $environment): void
79
     {
79
     {
80
-        $this->base_url = match ($environment) {
80
+        $this->baseUrl = match ($environment) {
81
             'development' => 'https://development.plaid.com',
81
             'development' => 'https://development.plaid.com',
82
             'production' => 'https://production.plaid.com',
82
             'production' => 'https://production.plaid.com',
83
             default => 'https://sandbox.plaid.com', // Default to sandbox, including if environment is null
83
             default => 'https://sandbox.plaid.com', // Default to sandbox, including if environment is null
86
 
86
 
87
     public function getBaseUrl(): string
87
     public function getBaseUrl(): string
88
     {
88
     {
89
-        return $this->base_url;
89
+        return $this->baseUrl;
90
     }
90
     }
91
 
91
 
92
     public function getEnvironment(): string
92
     public function getEnvironment(): string
99
         $request = $this->client->withHeaders([
99
         $request = $this->client->withHeaders([
100
             'Plaid-Version' => self::API_VERSION,
100
             'Plaid-Version' => self::API_VERSION,
101
             'Content-Type' => 'application/json',
101
             'Content-Type' => 'application/json',
102
-        ])->baseUrl($this->base_url);
102
+        ])->baseUrl($this->baseUrl);
103
 
103
 
104
         if ($method === 'post') {
104
         if ($method === 'post') {
105
             $request = $request->withHeaders([
105
             $request = $request->withHeaders([
106
-                'PLAID-CLIENT-ID' => $this->client_id,
107
-                'PLAID-SECRET' => $this->client_secret,
106
+                'PLAID-CLIENT-ID' => $this->clientId,
107
+                'PLAID-SECRET' => $this->clientSecret,
108
             ]);
108
             ]);
109
         }
109
         }
110
 
110
 
181
         );
181
         );
182
     }
182
     }
183
 
183
 
184
-    public function createLinkToken(string $client_name, string $language, array $country_codes, array $user, array $products): object
184
+    public function createLinkToken(string $clientName, string $language, array $countryCodes, array $user, array $products): object
185
     {
185
     {
186
         $data = [
186
         $data = [
187
-            'client_name' => $client_name,
187
+            'client_name' => $clientName,
188
             'language' => $language,
188
             'language' => $language,
189
-            'country_codes' => $country_codes,
189
+            'country_codes' => $countryCodes,
190
             'user' => (object) $user,
190
             'user' => (object) $user,
191
         ];
191
         ];
192
 
192
 
194
             $data['products'] = $products;
194
             $data['products'] = $products;
195
         }
195
         }
196
 
196
 
197
-        if (! empty($this->webhook_url)) {
198
-            $data['webhook'] = $this->webhook_url;
197
+        if (! empty($this->webhookUrl)) {
198
+            $data['webhook'] = $this->webhookUrl;
199
         }
199
         }
200
 
200
 
201
         return $this->sendRequest('link/token/create', $data);
201
         return $this->sendRequest('link/token/create', $data);
202
     }
202
     }
203
 
203
 
204
-    public function exchangePublicToken(string $public_token): object
204
+    public function exchangePublicToken(string $publicToken): object
205
     {
205
     {
206
-        $data = compact('public_token');
206
+        $data = [
207
+            'public_token' => $publicToken,
208
+        ];
207
 
209
 
208
         return $this->sendRequest('item/public_token/exchange', $data);
210
         return $this->sendRequest('item/public_token/exchange', $data);
209
     }
211
     }
218
         return $this->sendRequest('accounts/get', $data);
220
         return $this->sendRequest('accounts/get', $data);
219
     }
221
     }
220
 
222
 
221
-    public function getInstitution(string $institution_id, string $country): object
223
+    public function getInstitution(string $institutionId, string $country): object
222
     {
224
     {
223
         $options = [
225
         $options = [
224
             'include_optional_metadata' => true,
226
             'include_optional_metadata' => true,
226
 
228
 
227
         $plaidCountry = $this->getCountry($country);
229
         $plaidCountry = $this->getCountry($country);
228
 
230
 
229
-        return $this->getInstitutionById($institution_id, [$plaidCountry], $options);
231
+        return $this->getInstitutionById($institutionId, [$plaidCountry], $options);
230
     }
232
     }
231
 
233
 
232
-    public function getInstitutionById(string $institution_id, array $country_codes, array $options = []): object
234
+    public function getInstitutionById(string $institutionId, array $countryCodes, array $options = []): object
233
     {
235
     {
234
         $data = [
236
         $data = [
235
-            'institution_id' => $institution_id,
236
-            'country_codes' => $country_codes,
237
+            'institution_id' => $institutionId,
238
+            'country_codes' => $countryCodes,
237
             'options' => (object) $options,
239
             'options' => (object) $options,
238
         ];
240
         ];
239
 
241
 
240
         return $this->sendRequest('institutions/get_by_id', $data);
242
         return $this->sendRequest('institutions/get_by_id', $data);
241
     }
243
     }
242
 
244
 
243
-    public function getTransactions(string $access_token, string $start_date, string $end_date, array $options = []): object
245
+    public function getTransactions(string $accessToken, string $startDate, string $endDate, array $options = []): object
244
     {
246
     {
245
         $data = [
247
         $data = [
246
-            'access_token' => $access_token,
247
-            'start_date' => $start_date,
248
-            'end_date' => $end_date,
248
+            'access_token' => $accessToken,
249
+            'start_date' => $startDate,
250
+            'end_date' => $endDate,
249
             'options' => (object) $options,
251
             'options' => (object) $options,
250
         ];
252
         ];
251
 
253
 
252
         return $this->sendRequest('transactions/get', $data);
254
         return $this->sendRequest('transactions/get', $data);
253
     }
255
     }
254
 
256
 
255
-    public function fireSandboxWebhook(string $access_token, string $webhook_code, string $webhook_type): object
257
+    public function fireSandboxWebhook(string $accessToken, string $webhookCode, string $webhookType): object
256
     {
258
     {
257
-        $data = compact('access_token', 'webhook_code', 'webhook_type');
259
+        $data = [
260
+            'access_token' => $accessToken,
261
+            'webhook_code' => $webhookCode,
262
+            'webhook_type' => $webhookType,
263
+        ];
258
 
264
 
259
         return $this->sendRequest('sandbox/item/fire_webhook', $data);
265
         return $this->sendRequest('sandbox/item/fire_webhook', $data);
260
     }
266
     }
261
 
267
 
262
-    public function refreshTransactions(string $access_token): object
268
+    public function refreshTransactions(string $accessToken): object
263
     {
269
     {
264
-        $data = compact('access_token');
270
+        $data = [
271
+            'access_token' => $accessToken,
272
+        ];
265
 
273
 
266
         return $this->sendRequest('transactions/refresh', $data);
274
         return $this->sendRequest('transactions/refresh', $data);
267
     }
275
     }
268
 
276
 
269
-    public function removeItem(string $access_token): object
277
+    public function removeItem(string $accessToken): object
270
     {
278
     {
271
-        $data = compact('access_token');
279
+        $data = [
280
+            'access_token' => $accessToken,
281
+        ];
272
 
282
 
273
         return $this->sendRequest('item/remove', $data);
283
         return $this->sendRequest('item/remove', $data);
274
     }
284
     }

+ 170
- 0
app/Services/ReportService.php View File

1
+<?php
2
+
3
+namespace App\Services;
4
+
5
+use App\DTO\AccountBalanceDTO;
6
+use App\DTO\AccountCategoryDTO;
7
+use App\DTO\AccountDTO;
8
+use App\DTO\ReportDTO;
9
+use App\Enums\Accounting\AccountCategory;
10
+use App\Models\Accounting\Account;
11
+use App\Utilities\Currency\CurrencyAccessor;
12
+use Illuminate\Database\Eloquent\Collection;
13
+
14
+class ReportService
15
+{
16
+    public function __construct(
17
+        protected AccountService $accountService,
18
+    ) {}
19
+
20
+    public function formatBalances(array $balances): AccountBalanceDTO
21
+    {
22
+        $defaultCurrency = CurrencyAccessor::getDefaultCurrency();
23
+
24
+        foreach ($balances as $key => $balance) {
25
+            $balances[$key] = money($balance, $defaultCurrency)->format();
26
+        }
27
+
28
+        return new AccountBalanceDTO(
29
+            startingBalance: $balances['starting_balance'] ?? null,
30
+            debitBalance: $balances['debit_balance'],
31
+            creditBalance: $balances['credit_balance'],
32
+            netMovement: $balances['net_movement'] ?? null,
33
+            endingBalance: $balances['ending_balance'] ?? null,
34
+        );
35
+    }
36
+
37
+    private function filterBalances(array $balances, array $fields): array
38
+    {
39
+        return array_filter($balances, static fn ($key) => in_array($key, $fields, true), ARRAY_FILTER_USE_KEY);
40
+    }
41
+
42
+    private function getCategoryGroupedAccounts(array $allCategories): Collection
43
+    {
44
+        return Account::whereHas('journalEntries')
45
+            ->select('id', 'name', 'currency_code', 'category', 'code')
46
+            ->get()
47
+            ->groupBy(fn (Account $account) => $account->category->getPluralLabel())
48
+            ->sortBy(static fn (Collection $groupedAccounts, string $key) => array_search($key, $allCategories, true));
49
+    }
50
+
51
+    public function buildAccountBalanceReport(string $startDate, string $endDate, array $columns = []): ReportDTO
52
+    {
53
+        $allCategories = $this->accountService->getAccountCategoryOrder();
54
+
55
+        $categoryGroupedAccounts = $this->getCategoryGroupedAccounts($allCategories);
56
+
57
+        $balanceFields = ['starting_balance', 'debit_balance', 'credit_balance', 'net_movement', 'ending_balance'];
58
+
59
+        return $this->buildReport(
60
+            $allCategories,
61
+            $categoryGroupedAccounts,
62
+            fn (Account $account) => $this->accountService->getBalances($account, $startDate, $endDate),
63
+            $balanceFields,
64
+            $columns,
65
+            fn (string $categoryName, array &$categorySummaryBalances) => $this->adjustAccountBalanceCategoryFields($categoryName, $categorySummaryBalances),
66
+        );
67
+    }
68
+
69
+    public function buildTrialBalanceReport(string $startDate, string $endDate, array $columns = []): ReportDTO
70
+    {
71
+        $allCategories = $this->accountService->getAccountCategoryOrder();
72
+
73
+        $categoryGroupedAccounts = $this->getCategoryGroupedAccounts($allCategories);
74
+
75
+        $balanceFields = ['debit_balance', 'credit_balance'];
76
+
77
+        return $this->buildReport($allCategories, $categoryGroupedAccounts, function (Account $account) use ($startDate, $endDate) {
78
+            $endingBalance = $this->accountService->getEndingBalance($account, $startDate, $endDate)?->getAmount() ?? 0;
79
+
80
+            if ($endingBalance === 0) {
81
+                return [];
82
+            }
83
+
84
+            return $this->calculateTrialBalance($account->category, $endingBalance);
85
+        }, $balanceFields, $columns);
86
+    }
87
+
88
+    private function buildReport(array $allCategories, Collection $categoryGroupedAccounts, callable $balanceCalculator, array $balanceFields, array $allFields, ?callable $initializeCategoryBalances = null): ReportDTO
89
+    {
90
+        $accountCategories = [];
91
+        $reportTotalBalances = array_fill_keys($balanceFields, 0);
92
+
93
+        foreach ($allCategories as $categoryName) {
94
+            $accountsInCategory = $categoryGroupedAccounts[$categoryName] ?? collect();
95
+            $categorySummaryBalances = array_fill_keys($balanceFields, 0);
96
+
97
+            if ($initializeCategoryBalances) {
98
+                $initializeCategoryBalances($categoryName, $categorySummaryBalances);
99
+            }
100
+
101
+            $categoryAccounts = [];
102
+
103
+            foreach ($accountsInCategory as $account) {
104
+                /** @var Account $account */
105
+                $accountBalances = $balanceCalculator($account);
106
+
107
+                if (array_sum($accountBalances) === 0) {
108
+                    continue;
109
+                }
110
+
111
+                foreach ($accountBalances as $accountBalanceType => $accountBalance) {
112
+                    if (array_key_exists($accountBalanceType, $categorySummaryBalances)) {
113
+                        $categorySummaryBalances[$accountBalanceType] += $accountBalance;
114
+                    }
115
+                }
116
+
117
+                $filteredAccountBalances = $this->filterBalances($accountBalances, $balanceFields);
118
+                $formattedAccountBalances = $this->formatBalances($filteredAccountBalances);
119
+
120
+                $categoryAccounts[] = new AccountDTO(
121
+                    $account->name,
122
+                    $account->code,
123
+                    $formattedAccountBalances,
124
+                );
125
+            }
126
+
127
+            foreach ($balanceFields as $field) {
128
+                if (array_key_exists($field, $categorySummaryBalances)) {
129
+                    $reportTotalBalances[$field] += $categorySummaryBalances[$field];
130
+                }
131
+            }
132
+
133
+            $filteredCategorySummaryBalances = $this->filterBalances($categorySummaryBalances, $balanceFields);
134
+            $formattedCategorySummaryBalances = $this->formatBalances($filteredCategorySummaryBalances);
135
+
136
+            $accountCategories[$categoryName] = new AccountCategoryDTO(
137
+                $categoryAccounts,
138
+                $formattedCategorySummaryBalances,
139
+            );
140
+        }
141
+
142
+        $formattedReportTotalBalances = $this->formatBalances($reportTotalBalances);
143
+
144
+        return new ReportDTO($accountCategories, $formattedReportTotalBalances, $allFields);
145
+    }
146
+
147
+    private function adjustAccountBalanceCategoryFields(string $categoryName, array &$categorySummaryBalances): void
148
+    {
149
+        if (in_array($categoryName, [AccountCategory::Expense->getPluralLabel(), AccountCategory::Revenue->getPluralLabel()], true)) {
150
+            unset($categorySummaryBalances['starting_balance'], $categorySummaryBalances['ending_balance']);
151
+        }
152
+    }
153
+
154
+    private function calculateTrialBalance(AccountCategory $category, int $endingBalance): array
155
+    {
156
+        if (in_array($category, [AccountCategory::Asset, AccountCategory::Expense], true)) {
157
+            if ($endingBalance >= 0) {
158
+                return ['debit_balance' => $endingBalance, 'credit_balance' => 0];
159
+            }
160
+
161
+            return ['debit_balance' => 0, 'credit_balance' => abs($endingBalance)];
162
+        }
163
+
164
+        if ($endingBalance >= 0) {
165
+            return ['debit_balance' => 0, 'credit_balance' => $endingBalance];
166
+        }
167
+
168
+        return ['debit_balance' => abs($endingBalance), 'credit_balance' => 0];
169
+    }
170
+}

+ 43
- 0
app/Support/Column.php View File

1
+<?php
2
+
3
+namespace App\Support;
4
+
5
+use Filament\Support\Components\Component;
6
+use Filament\Support\Concerns\HasAlignment;
7
+use Filament\Support\Enums\Alignment;
8
+use Filament\Tables\Columns\Concerns\CanBeHidden;
9
+use Filament\Tables\Columns\Concerns\CanBeToggled;
10
+use Filament\Tables\Columns\Concerns\HasLabel;
11
+use Filament\Tables\Columns\Concerns\HasName;
12
+
13
+class Column extends Component
14
+{
15
+    use CanBeHidden;
16
+    use CanBeToggled;
17
+    use HasAlignment;
18
+    use HasLabel;
19
+    use HasName;
20
+
21
+    final public function __construct(string $name)
22
+    {
23
+        $this->name($name);
24
+    }
25
+
26
+    public static function make(string $name): static
27
+    {
28
+        $static = app(static::class, ['name' => $name]);
29
+        $static->configure();
30
+
31
+        return $static;
32
+    }
33
+
34
+    public function getAlignmentClass(): string
35
+    {
36
+        return match ($this->getAlignment()) {
37
+            Alignment::Center, Alignment::Justify, Alignment::Between => 'text-center',
38
+            Alignment::Left, Alignment::Start => 'text-left',
39
+            Alignment::Right, Alignment::End => 'text-right',
40
+            default => '',
41
+        };
42
+    }
43
+}

+ 98
- 0
app/Transformers/AccountBalanceReportTransformer.php View File

1
+<?php
2
+
3
+namespace App\Transformers;
4
+
5
+use App\DTO\AccountDTO;
6
+use App\DTO\ReportCategoryDTO;
7
+use App\Support\Column;
8
+
9
+class AccountBalanceReportTransformer extends BaseReportTransformer
10
+{
11
+    public function getTitle(): string
12
+    {
13
+        return 'Account Balances';
14
+    }
15
+
16
+    public function getHeaders(): array
17
+    {
18
+        return array_map(fn (Column $column) => $column->getLabel(), $this->getColumns());
19
+    }
20
+
21
+    /**
22
+     * @return ReportCategoryDTO[]
23
+     */
24
+    public function getCategories(): array
25
+    {
26
+        $categories = [];
27
+
28
+        foreach ($this->report->categories as $accountCategoryName => $accountCategory) {
29
+            // Initialize header with empty strings
30
+            $header = [];
31
+
32
+            foreach ($this->getColumns() as $index => $column) {
33
+                if ($column->getName() === 'account_name') {
34
+                    $header[$index] = $accountCategoryName;
35
+                } else {
36
+                    $header[$index] = '';
37
+                }
38
+            }
39
+
40
+            $data = array_map(function (AccountDTO $account) {
41
+                $row = [];
42
+
43
+                foreach ($this->getColumns() as $column) {
44
+                    $row[] = match ($column->getName()) {
45
+                        'account_code' => $account->accountCode,
46
+                        'account_name' => $account->accountName,
47
+                        'starting_balance' => $account->balance->startingBalance ?? '',
48
+                        'debit_balance' => $account->balance->debitBalance,
49
+                        'credit_balance' => $account->balance->creditBalance,
50
+                        'net_movement' => $account->balance->netMovement ?? '',
51
+                        'ending_balance' => $account->balance->endingBalance ?? '',
52
+                        default => '',
53
+                    };
54
+                }
55
+
56
+                return $row;
57
+            }, $accountCategory->accounts);
58
+
59
+            $summary = [];
60
+
61
+            foreach ($this->getColumns() as $column) {
62
+                $summary[] = match ($column->getName()) {
63
+                    'account_name' => 'Total ' . $accountCategoryName,
64
+                    'starting_balance' => $accountCategory->summary->startingBalance ?? '',
65
+                    'debit_balance' => $accountCategory->summary->debitBalance,
66
+                    'credit_balance' => $accountCategory->summary->creditBalance,
67
+                    'net_movement' => $accountCategory->summary->netMovement ?? '',
68
+                    'ending_balance' => $accountCategory->summary->endingBalance ?? '',
69
+                    default => '',
70
+                };
71
+            }
72
+
73
+            $categories[] = new ReportCategoryDTO(
74
+                header: $header,
75
+                data: $data,
76
+                summary: $summary,
77
+            );
78
+        }
79
+
80
+        return $categories;
81
+    }
82
+
83
+    public function getOverallTotals(): array
84
+    {
85
+        $totals = [];
86
+
87
+        foreach ($this->getColumns() as $column) {
88
+            $totals[] = match ($column->getName()) {
89
+                'account_name' => 'Total for all accounts',
90
+                'debit_balance' => $this->report->overallTotal->debitBalance,
91
+                'credit_balance' => $this->report->overallTotal->creditBalance,
92
+                default => '',
93
+            };
94
+        }
95
+
96
+        return $totals;
97
+    }
98
+}

+ 52
- 0
app/Transformers/BaseReportTransformer.php View File

1
+<?php
2
+
3
+namespace App\Transformers;
4
+
5
+use App\Contracts\ExportableReport;
6
+use App\DTO\ReportDTO;
7
+use Filament\Support\Enums\Alignment;
8
+use Livewire\Wireable;
9
+
10
+abstract class BaseReportTransformer implements ExportableReport, Wireable
11
+{
12
+    protected ReportDTO $report;
13
+
14
+    public function __construct(ReportDTO $report)
15
+    {
16
+        $this->report = $report;
17
+    }
18
+
19
+    public function getColumns(): array
20
+    {
21
+        return $this->report->fields;
22
+    }
23
+
24
+    public function getAlignmentClass(int $index): string
25
+    {
26
+        $column = $this->getColumns()[$index];
27
+
28
+        if ($column->getAlignment() === Alignment::Right) {
29
+            return 'text-right';
30
+        }
31
+
32
+        if ($column->getAlignment() === Alignment::Center) {
33
+            return 'text-center';
34
+        }
35
+
36
+        return 'text-left';
37
+    }
38
+
39
+    public function toLivewire(): array
40
+    {
41
+        return [
42
+            'report' => $this->report->toLivewire(),
43
+        ];
44
+    }
45
+
46
+    public static function fromLivewire($value): static
47
+    {
48
+        return new static(
49
+            ReportDTO::fromLivewire($value['report']),
50
+        );
51
+    }
52
+}

+ 92
- 0
app/Transformers/TrialBalanceReportTransformer.php View File

1
+<?php
2
+
3
+namespace App\Transformers;
4
+
5
+use App\DTO\AccountDTO;
6
+use App\DTO\ReportCategoryDTO;
7
+use App\Support\Column;
8
+
9
+class TrialBalanceReportTransformer extends BaseReportTransformer
10
+{
11
+    public function getTitle(): string
12
+    {
13
+        return 'Trial Balance';
14
+    }
15
+
16
+    public function getHeaders(): array
17
+    {
18
+        return array_map(fn (Column $column) => $column->getLabel(), $this->getColumns());
19
+    }
20
+
21
+    /**
22
+     * @return ReportCategoryDTO[]
23
+     */
24
+    public function getCategories(): array
25
+    {
26
+        $categories = [];
27
+
28
+        foreach ($this->report->categories as $accountCategoryName => $accountCategory) {
29
+            // Initialize header with empty strings
30
+            $header = [];
31
+
32
+            foreach ($this->getColumns() as $index => $column) {
33
+                if ($column->getName() === 'account_name') {
34
+                    $header[$index] = $accountCategoryName;
35
+                } else {
36
+                    $header[$index] = '';
37
+                }
38
+            }
39
+
40
+            $data = array_map(function (AccountDTO $account) {
41
+                $row = [];
42
+
43
+                foreach ($this->getColumns() as $column) {
44
+                    $row[] = match ($column->getName()) {
45
+                        'account_code' => $account->accountCode,
46
+                        'account_name' => $account->accountName,
47
+                        'debit_balance' => $account->balance->debitBalance,
48
+                        'credit_balance' => $account->balance->creditBalance,
49
+                        default => '',
50
+                    };
51
+                }
52
+
53
+                return $row;
54
+            }, $accountCategory->accounts);
55
+
56
+            $summary = [];
57
+
58
+            foreach ($this->getColumns() as $column) {
59
+                $summary[] = match ($column->getName()) {
60
+                    'account_name' => 'Total ' . $accountCategoryName,
61
+                    'debit_balance' => $accountCategory->summary->debitBalance,
62
+                    'credit_balance' => $accountCategory->summary->creditBalance,
63
+                    default => '',
64
+                };
65
+            }
66
+
67
+            $categories[] = new ReportCategoryDTO(
68
+                header: $header,
69
+                data: $data,
70
+                summary: $summary,
71
+            );
72
+        }
73
+
74
+        return $categories;
75
+    }
76
+
77
+    public function getOverallTotals(): array
78
+    {
79
+        $totals = [];
80
+
81
+        foreach ($this->getColumns() as $column) {
82
+            $totals[] = match ($column->getName()) {
83
+                'account_name' => 'Total for all accounts',
84
+                'debit_balance' => $this->report->overallTotal->debitBalance,
85
+                'credit_balance' => $this->report->overallTotal->creditBalance,
86
+                default => '',
87
+            };
88
+        }
89
+
90
+        return $totals;
91
+    }
92
+}

+ 4
- 9
app/ValueObjects/Money.php View File

7
 
7
 
8
 class Money
8
 class Money
9
 {
9
 {
10
-    private int $amount;
11
-
12
-    private string $currencyCode;
13
-
14
     private ?int $convertedAmount = null;
10
     private ?int $convertedAmount = null;
15
 
11
 
16
-    public function __construct(int $amount, string $currencyCode)
17
-    {
18
-        $this->amount = $amount;
19
-        $this->currencyCode = $currencyCode;
20
-    }
12
+    public function __construct(
13
+        private readonly int $amount,
14
+        private readonly string $currencyCode
15
+    ) {}
21
 
16
 
22
     public function getAmount(): int
17
     public function getAmount(): int
23
     {
18
     {

+ 4
- 9
app/View/Models/InvoiceViewModel.php View File

11
 {
11
 {
12
     use HasFont;
12
     use HasFont;
13
 
13
 
14
-    public DocumentDefault $invoice;
15
-
16
-    public ?array $data = [];
17
-
18
-    public function __construct(DocumentDefault $invoice, ?array $data = null)
19
-    {
20
-        $this->invoice = $invoice;
21
-        $this->data = $data;
22
-    }
14
+    public function __construct(
15
+        public DocumentDefault $invoice,
16
+        public ?array $data = null
17
+    ) {}
23
 
18
 
24
     public function logo(): ?string
19
     public function logo(): ?string
25
     {
20
     {

+ 0
- 1
bootstrap/providers.php View File

3
 return [
3
 return [
4
     App\Providers\AppServiceProvider::class,
4
     App\Providers\AppServiceProvider::class,
5
     App\Providers\AuthServiceProvider::class,
5
     App\Providers\AuthServiceProvider::class,
6
-    App\Providers\EventServiceProvider::class,
7
     App\Providers\Filament\AdminPanelProvider::class,
6
     App\Providers\Filament\AdminPanelProvider::class,
8
     App\Providers\FilamentCompaniesServiceProvider::class,
7
     App\Providers\FilamentCompaniesServiceProvider::class,
9
     App\Providers\Filament\UserPanelProvider::class,
8
     App\Providers\Filament\UserPanelProvider::class,

+ 2
- 1
composer.json View File

35
         "mockery/mockery": "^1.6",
35
         "mockery/mockery": "^1.6",
36
         "nunomaduro/collision": "^8.0",
36
         "nunomaduro/collision": "^8.0",
37
         "phpunit/phpunit": "^10.5",
37
         "phpunit/phpunit": "^10.5",
38
-        "spatie/laravel-ignition": "^2.4"
38
+        "spatie/laravel-ignition": "^2.4",
39
+        "spatie/laravel-ray": "^1.36"
39
     },
40
     },
40
     "autoload": {
41
     "autoload": {
41
         "psr-4": {
42
         "psr-4": {

+ 1290
- 492
composer.lock
File diff suppressed because it is too large
View File


+ 14
- 15
database/migrations/2023_09_03_100000_create_accounting_tables.php View File

36
             $table->timestamps();
36
             $table->timestamps();
37
         });
37
         });
38
 
38
 
39
+        Schema::create('bank_accounts', function (Blueprint $table) {
40
+            $table->id();
41
+            $table->foreignId('company_id')->constrained()->cascadeOnDelete();
42
+            $table->foreignId('institution_id')->nullable()->constrained('institutions')->nullOnDelete();
43
+            $table->string('type')->default(BankAccountType::DEFAULT);
44
+            $table->string('number', 20)->nullable();
45
+            $table->boolean('enabled')->default(true);
46
+            $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
47
+            $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
48
+            $table->timestamps();
49
+        });
50
+
39
         Schema::create('accounts', function (Blueprint $table) {
51
         Schema::create('accounts', function (Blueprint $table) {
40
             $table->id();
52
             $table->id();
41
             $table->foreignId('company_id')->constrained()->cascadeOnDelete();
53
             $table->foreignId('company_id')->constrained()->cascadeOnDelete();
47
             $table->string('name')->nullable()->index();
59
             $table->string('name')->nullable()->index();
48
             $table->string('currency_code')->nullable();
60
             $table->string('currency_code')->nullable();
49
             $table->text('description')->nullable();
61
             $table->text('description')->nullable();
50
-            $table->boolean('active')->default(true);
62
+            $table->boolean('archived')->default(false);
51
             $table->boolean('default')->default(false);
63
             $table->boolean('default')->default(false);
52
-            $table->unsignedBigInteger('accountable_id')->nullable();
53
-            $table->string('accountable_type')->nullable();
64
+            $table->foreignId('bank_account_id')->nullable()->constrained('bank_accounts')->cascadeOnDelete();
54
             $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
65
             $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
55
             $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
66
             $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
56
             $table->timestamps();
67
             $table->timestamps();
58
             $table->unique(['company_id', 'code']);
69
             $table->unique(['company_id', 'code']);
59
         });
70
         });
60
 
71
 
61
-        Schema::create('bank_accounts', function (Blueprint $table) {
62
-            $table->id();
63
-            $table->foreignId('company_id')->constrained()->cascadeOnDelete();
64
-            $table->foreignId('institution_id')->nullable()->constrained('institutions')->nullOnDelete();
65
-            $table->string('type')->default(BankAccountType::DEFAULT);
66
-            $table->string('number', 20)->nullable();
67
-            $table->boolean('enabled')->default(true);
68
-            $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
69
-            $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
70
-            $table->timestamps();
71
-        });
72
-
73
         Schema::create('connected_bank_accounts', function (Blueprint $table) {
72
         Schema::create('connected_bank_accounts', function (Blueprint $table) {
74
             $table->id();
73
             $table->id();
75
             $table->foreignId('company_id')->constrained()->cascadeOnDelete();
74
             $table->foreignId('company_id')->constrained()->cascadeOnDelete();

+ 273
- 263
package-lock.json
File diff suppressed because it is too large
View File


+ 7
- 7
package.json View File

7
     },
7
     },
8
     "devDependencies": {
8
     "devDependencies": {
9
         "@tailwindcss/forms": "^0.5.7",
9
         "@tailwindcss/forms": "^0.5.7",
10
-        "@tailwindcss/typography": "^0.5.10",
11
-        "autoprefixer": "^10.4.18",
12
-        "axios": "^1.6.4",
10
+        "@tailwindcss/typography": "^0.5.13",
11
+        "autoprefixer": "^10.4.19",
12
+        "axios": "^1.7.2",
13
         "laravel-vite-plugin": "^1.0",
13
         "laravel-vite-plugin": "^1.0",
14
-        "postcss": "^8.4.35",
15
-        "postcss-nesting": "^12.1.0",
16
-        "tailwindcss": "^3.4.1",
17
-        "vite": "^5.0"
14
+        "postcss": "^8.4.38",
15
+        "postcss-nesting": "^12.1.5",
16
+        "tailwindcss": "^3.4.4",
17
+        "vite": "^5.3"
18
     }
18
     }
19
 }
19
 }

+ 3
- 1
resources/data/lang/en.json View File

187
     "Available": "Available",
187
     "Available": "Available",
188
     "Live Rate": "Live Rate",
188
     "Live Rate": "Live Rate",
189
     "Edit": "Edit",
189
     "Edit": "Edit",
190
-    "Notes": "Notes"
190
+    "Notes": "Notes",
191
+    "Terms": "Terms",
192
+    "Ending Balance": "Ending Balance"
191
 }
193
 }

+ 0
- 127
resources/views/components/company/reports/account-balances.blade.php View File

1
-<!DOCTYPE html>
2
-<html lang="en">
3
-<head>
4
-    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
5
-    <meta name="viewport" content="width=device-width, initial-scale=1">
6
-    <title>Account Balances</title>
7
-    <style>
8
-        @page {
9
-            size: A4;
10
-            margin: 8.5mm 8.5mm 30mm 8.5mm;
11
-        }
12
-
13
-        .header {
14
-            color: #374151;
15
-        }
16
-
17
-        .table-class th,
18
-        .table-class td {
19
-            text-align: right;
20
-            color: #374151;
21
-        }
22
-
23
-        /* Align the first column header and data to the left */
24
-        .table-class th:first-child, .table-class td:first-child {
25
-            text-align: left;
26
-        }
27
-
28
-        .header {
29
-            margin-bottom: 1rem; /* Space between header and table */
30
-        }
31
-
32
-        .header .title,
33
-        .header .company-name,
34
-        .header .date-range {
35
-            margin-bottom: 0.125rem; /* Uniform space between header elements */
36
-        }
37
-
38
-        .title { font-size: 1.5rem; }
39
-        .company-name { font-size: 1.125rem; font-weight: 600; }
40
-        .date-range { font-size: 0.875rem; }
41
-
42
-        .table-class {
43
-            width: 100%;
44
-            border-collapse: collapse;
45
-        }
46
-
47
-        .table-class th,
48
-        .table-class td {
49
-            padding: 0.75rem;
50
-            font-size: 0.75rem;
51
-            line-height: 1rem;
52
-            border-bottom: 1px solid #d1d5db; /* Gray border for all rows */
53
-        }
54
-
55
-        .category-row > td {
56
-            background-color: #f3f4f6; /* Gray background for category names */
57
-            font-weight: 600;
58
-        }
59
-
60
-        .table-body tr { background-color: #ffffff; /* White background for other rows */ }
61
-
62
-        .spacer-row > td { height: 0.75rem; }
63
-
64
-        .summary-row > td,
65
-        .table-footer-row > td {
66
-            font-weight: 600;
67
-            background-color: #ffffff; /* White background for footer */
68
-        }
69
-    </style>
70
-</head>
71
-<body>
72
-    <div class="header">
73
-        <div class="title">Account Balances</div>
74
-        <div class="company-name">{{ auth()->user()->currentCompany->name }}</div>
75
-        <div class="date-range">Date Range: {{ $startDate }} to {{ $endDate }}</div>
76
-    </div>
77
-    <table class="table-class">
78
-        <thead class="table-head" style="display: table-row-group;">
79
-            <tr>
80
-                <th>Account</th>
81
-                <th>Starting Balance</th>
82
-                <th>Debit</th>
83
-                <th>Credit</th>
84
-                <th>Net Movement</th>
85
-                <th>Ending Balance</th>
86
-            </tr>
87
-        </thead>
88
-        @foreach($accountBalanceReport->categories as $accountCategoryName => $accountCategory)
89
-            <tbody>
90
-            <tr class="category-row">
91
-                <td colspan="6">{{ $accountCategoryName }}</td>
92
-            </tr>
93
-            @foreach($accountCategory->accounts as $account)
94
-                <tr>
95
-                    <td>{{ $account->accountName }}</td>
96
-                    <td>{{ $account->balance->startingBalance ?? '' }}</td>
97
-                    <td>{{ $account->balance->debitBalance }}</td>
98
-                    <td>{{ $account->balance->creditBalance }}</td>
99
-                    <td>{{ $account->balance->netMovement }}</td>
100
-                    <td>{{ $account->balance->endingBalance ?? '' }}</td>
101
-                </tr>
102
-            @endforeach
103
-            <tr class="summary-row">
104
-                <td>Total {{ $accountCategoryName }}</td>
105
-                <td>{{ $accountCategory->summary->startingBalance ?? '' }}</td>
106
-                <td>{{ $accountCategory->summary->debitBalance }}</td>
107
-                <td>{{ $accountCategory->summary->creditBalance }}</td>
108
-                <td>{{ $accountCategory->summary->netMovement }}</td>
109
-                <td>{{ $accountCategory->summary->endingBalance ?? '' }}</td>
110
-            </tr>
111
-            <tr class="spacer-row">
112
-                <td colspan="6"></td>
113
-            </tr>
114
-            </tbody>
115
-        @endforeach
116
-        <tfoot>
117
-            <tr class="table-footer-row">
118
-                <td>Total for all accounts</td>
119
-                <td></td>
120
-                <td>{{ $accountBalanceReport->overallTotal->debitBalance }}</td>
121
-                <td>{{ $accountBalanceReport->overallTotal->creditBalance }}</td>
122
-                <td></td>
123
-                <td></td>
124
-            </tr>
125
-        </tfoot>
126
-    </table>
127
-</body>

+ 148
- 0
resources/views/components/company/reports/report-pdf.blade.php View File

1
+<!DOCTYPE html>
2
+<html lang="en">
3
+<head>
4
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
5
+    <meta name="viewport" content="width=device-width, initial-scale=1">
6
+    <title>{{ $report->getTitle() }}</title>
7
+    <style>
8
+        @page {
9
+            size: A4;
10
+            margin: 8.5mm 8.5mm 30mm 8.5mm;
11
+        }
12
+
13
+        .header {
14
+            color: #374151;
15
+            margin-bottom: 1rem;
16
+        }
17
+
18
+        .table-head {
19
+            display: table-row-group;
20
+        }
21
+
22
+        .text-left {
23
+            text-align: left;
24
+        }
25
+
26
+        .text-right {
27
+            text-align: right;
28
+        }
29
+
30
+        .text-center {
31
+            text-align: center;
32
+        }
33
+
34
+        .table-class th,
35
+        .table-class td {
36
+            color: #374151;
37
+        }
38
+
39
+        .header .title,
40
+        .header .company-name,
41
+        .header .date-range {
42
+            margin-bottom: 0.125rem; /* Uniform space between header elements */
43
+        }
44
+
45
+        .title {
46
+            font-size: 1.5rem;
47
+        }
48
+
49
+        .company-name {
50
+            font-size: 1.125rem;
51
+            font-weight: 600;
52
+        }
53
+
54
+        .date-range {
55
+            font-size: 0.875rem;
56
+        }
57
+
58
+        .table-class {
59
+            width: 100%;
60
+            border-collapse: collapse;
61
+        }
62
+
63
+        .table-class th,
64
+        .table-class td {
65
+            padding: 0.75rem;
66
+            font-size: 0.75rem;
67
+            line-height: 1rem;
68
+            border-bottom: 1px solid #d1d5db; /* Gray border for all rows */
69
+        }
70
+
71
+        .category-header-row > td {
72
+            background-color: #f3f4f6; /* Gray background for category names */
73
+            font-weight: 600;
74
+        }
75
+
76
+        .table-body tr {
77
+            background-color: #ffffff; /* White background for other rows */
78
+        }
79
+
80
+        .spacer-row > td {
81
+            height: 0.75rem;
82
+        }
83
+
84
+        .category-summary-row > td,
85
+        .table-footer-row > td {
86
+            font-weight: 600;
87
+            background-color: #ffffff; /* White background for footer */
88
+        }
89
+    </style>
90
+</head>
91
+<body>
92
+<div class="header">
93
+    <div class="title">{{ $report->getTitle() }}</div>
94
+    <div class="company-name">{{ $company->name }}</div>
95
+    <div class="date-range">Date Range: {{ $startDate }} to {{ $endDate }}</div>
96
+</div>
97
+<table class="table-class">
98
+    <thead class="table-head">
99
+    <tr>
100
+        @foreach($report->getHeaders() as $index => $header)
101
+            <th class="{{ $report->getAlignmentClass($index) }}">
102
+                {{ $header }}
103
+            </th>
104
+        @endforeach
105
+    </tr>
106
+    </thead>
107
+    @foreach($report->getCategories() as $category)
108
+        <tbody>
109
+        <tr class="category-header-row">
110
+            @foreach($category->header as $index => $header)
111
+                <td class="{{ $report->getAlignmentClass($index) }}">
112
+                    {{ $header }}
113
+                </td>
114
+            @endforeach
115
+        </tr>
116
+        @foreach($category->data as $account)
117
+            <tr>
118
+                @foreach($account as $index => $cell)
119
+                    <td class="{{ $report->getAlignmentClass($index) }}">
120
+                        {{ $cell }}
121
+                    </td>
122
+                @endforeach
123
+            </tr>
124
+        @endforeach
125
+        <tr class="category-summary-row">
126
+            @foreach($category->summary as $index => $cell)
127
+                <td class="{{ $report->getAlignmentClass($index) }}">
128
+                    {{ $cell }}
129
+                </td>
130
+            @endforeach
131
+        </tr>
132
+        <tr class="spacer-row">
133
+            <td colspan="{{ count($report->getHeaders()) }}"></td>
134
+        </tr>
135
+        </tbody>
136
+    @endforeach
137
+    <tfoot>
138
+    <tr class="table-footer-row">
139
+        @foreach ($report->getOverallTotals() as $index => $total)
140
+            <td class="{{ $report->getAlignmentClass($index) }}">
141
+                {{ $total }}
142
+            </td>
143
+        @endforeach
144
+    </tr>
145
+    </tfoot>
146
+</table>
147
+</body>
148
+</html>

+ 62
- 0
resources/views/components/company/tables/reports/detailed-report.blade.php View File

1
+<table class="w-full table-auto divide-y divide-gray-200 dark:divide-white/5">
2
+    <thead class="divide-y divide-gray-200 dark:divide-white/5">
3
+        <tr class="bg-gray-50 dark:bg-white/5">
4
+            @foreach($report->getHeaders() as $index => $header)
5
+                <th wire:key="header-{{ $index }}" class="px-3 py-3.5 sm:first-of-type:ps-6 sm:last-of-type:pe-6 {{ $report->getAlignmentClass($index) }}">
6
+                    <span class="text-sm font-semibold text-gray-950 dark:text-white">
7
+                        {{ $header }}
8
+                    </span>
9
+                </th>
10
+            @endforeach
11
+        </tr>
12
+    </thead>
13
+    @foreach($report->getCategories() as $categoryIndex => $category)
14
+        <tbody wire:key="category-{{ $categoryIndex }}" class="divide-y divide-gray-200 whitespace-nowrap dark:divide-white/5">
15
+            <tr class="bg-gray-50 dark:bg-white/5">
16
+                @foreach($category->header as $headerIndex => $header)
17
+                    <x-filament-tables::cell wire:key="category-{{ $categoryIndex }}-header-{{ $headerIndex }}" class="{{ $report->getAlignmentClass($headerIndex) }}">
18
+                        <div class="px-3 py-2 text-sm font-semibold text-gray-950 dark:text-white">
19
+                            {{ $header }}
20
+                        </div>
21
+                    </x-filament-tables::cell>
22
+                @endforeach
23
+            </tr>
24
+            @foreach($category->data as $dataIndex => $account)
25
+                <tr wire:key="category-{{ $categoryIndex }}-data-{{ $dataIndex }}">
26
+                    @foreach($account as $cellIndex => $cell)
27
+                        <x-filament-tables::cell wire:key="category-{{ $categoryIndex }}-data-{{ $dataIndex }}-cell-{{ $cellIndex }}" class="{{ $report->getAlignmentClass($cellIndex) }}">
28
+                            <div class="px-3 py-4 text-sm leading-6 text-gray-950 dark:text-white">
29
+                                {{ $cell }}
30
+                            </div>
31
+                        </x-filament-tables::cell>
32
+                    @endforeach
33
+                </tr>
34
+            @endforeach
35
+            <tr wire:key="category-{{ $categoryIndex }}-summary">
36
+                @foreach($category->summary as $summaryIndex => $cell)
37
+                    <x-filament-tables::cell wire:key="category-{{ $categoryIndex }}-summary-{{ $summaryIndex }}" class="{{ $report->getAlignmentClass($summaryIndex) }}">
38
+                        <div class="px-3 py-2 text-sm leading-6 font-semibold text-gray-950 dark:text-white">
39
+                            {{ $cell }}
40
+                        </div>
41
+                    </x-filament-tables::cell>
42
+                @endforeach
43
+            </tr>
44
+            <tr wire:key="category-{{ $categoryIndex }}-spacer">
45
+                <x-filament-tables::cell colspan="{{ count($report->getHeaders()) }}">
46
+                    <div class="px-3 py-2 leading-6 invisible">Hidden Text</div>
47
+                </x-filament-tables::cell>
48
+            </tr>
49
+        </tbody>
50
+    @endforeach
51
+    <tfoot>
52
+        <tr class="bg-gray-50 dark:bg-white/5">
53
+            @foreach($report->getOverallTotals() as $index => $total)
54
+                <x-filament-tables::cell wire:key="footer-total-{{ $index }}" class="{{ $report->getAlignmentClass($index) }}">
55
+                    <div class="px-3 py-2 text-sm leading-6 font-semibold text-gray-950 dark:text-white">
56
+                        {{ $total }}
57
+                    </div>
58
+                </x-filament-tables::cell>
59
+            @endforeach
60
+        </tr>
61
+    </tfoot>
62
+</table>

+ 11
- 3
resources/views/filament/company/pages/accounting/chart.blade.php View File

21
                         <table class="es-table table-fixed w-full divide-y divide-gray-200 text-start text-sm dark:divide-white/5">
21
                         <table class="es-table table-fixed w-full divide-y divide-gray-200 text-start text-sm dark:divide-white/5">
22
                             <colgroup>
22
                             <colgroup>
23
                                 <col span="1" style="width: 12.5%;">
23
                                 <col span="1" style="width: 12.5%;">
24
-                                <col span="1" style="width: 25%;">
25
-                                <col span="1" style="width: 40%;">
24
+                                <col span="1" style="width: 20%;">
25
+                                <col span="1" style="width: 35%;">
26
                                 <col span="1" style="width: 15%;">
26
                                 <col span="1" style="width: 15%;">
27
+                                <col span="1" style="width: 10%;">
27
                                 <col span="1" style="width: 7.5%;">
28
                                 <col span="1" style="width: 7.5%;">
28
                             </colgroup>
29
                             </colgroup>
29
                             @foreach($subtypes as $subtype)
30
                             @foreach($subtypes as $subtype)
30
                                 <tbody class="es-table__rowgroup divide-y divide-gray-200 whitespace-nowrap dark:divide-white/5">
31
                                 <tbody class="es-table__rowgroup divide-y divide-gray-200 whitespace-nowrap dark:divide-white/5">
31
                                 <!-- Subtype Name Header Row -->
32
                                 <!-- Subtype Name Header Row -->
32
                                 <tr class="es-table__row--header bg-gray-50 dark:bg-white/5">
33
                                 <tr class="es-table__row--header bg-gray-50 dark:bg-white/5">
33
-                                    <td colspan="5" class="es-table__cell px-4 py-4">
34
+                                    <td colspan="6" class="es-table__cell px-4 py-4">
34
                                         <div class="es-table__row-content flex items-center space-x-2">
35
                                         <div class="es-table__row-content flex items-center space-x-2">
35
                                             <span class="es-table__row-title text-gray-800 dark:text-gray-200 font-semibold tracking-wider">
36
                                             <span class="es-table__row-title text-gray-800 dark:text-gray-200 font-semibold tracking-wider">
36
                                                 {{ $subtype->name }}
37
                                                 {{ $subtype->name }}
61
                                             </small>
62
                                             </small>
62
                                         </td>
63
                                         </td>
63
                                         <td colspan="2" class="es-table__cell px-4 py-4">{{ $account->description }}</td>
64
                                         <td colspan="2" class="es-table__cell px-4 py-4">{{ $account->description }}</td>
65
+                                        <td colspan="1" class="es-table__cell px-4 py-4">
66
+                                            @if($account->archived)
67
+                                                <x-filament::badge color="gray" size="sm">
68
+                                                    Archived
69
+                                                </x-filament::badge>
70
+                                            @endif
71
+                                        </td>
64
                                         <td colspan="1" class="es-table__cell px-4 py-4">
72
                                         <td colspan="1" class="es-table__cell px-4 py-4">
65
                                             <div>
73
                                             <div>
66
                                                 @if($account->default === false)
74
                                                 @if($account->default === false)

+ 0
- 10
resources/views/filament/company/pages/create-company.blade.php View File

1
-<x-filament-panels::page.simple>
2
-    <x-filament-panels::form wire:submit="register">
3
-        {{ $this->form }}
4
-
5
-        <x-filament-panels::form.actions
6
-            :actions="$this->getCachedFormActions()"
7
-            :full-width="$this->hasFullWidthFormActions()"
8
-        />
9
-    </x-filament-panels::form>
10
-</x-filament-panels::page.simple>

+ 0
- 75
resources/views/filament/company/pages/reports/account-balances.blade.php View File

1
-<x-filament-panels::page>
2
-    <div class="flex flex-col gap-y-6">
3
-        <x-filament-tables::container>
4
-            <div class="p-6 divide-y divide-gray-200 dark:divide-white/5">
5
-                <form wire:submit.prevent="loadAccountBalances" class="w-full">
6
-                    <div class="flex flex-col md:flex-row items-end justify-center gap-4 md:gap-6">
7
-                        <div class="flex-grow">
8
-                            {{ $this->form }}
9
-                        </div>
10
-                        <x-filament::button type="submit" class="mt-4 md:mt-0">
11
-                            Update Report
12
-                        </x-filament::button>
13
-                    </div>
14
-                </form>
15
-            </div>
16
-            <div class="divide-y divide-gray-200 overflow-x-auto dark:divide-white/10 dark:border-t-white/10">
17
-                <table class="w-full table-auto divide-y divide-gray-200 text-start dark:divide-white/5">
18
-                    <thead class="divide-y divide-gray-200 dark:divide-white/5">
19
-                        <tr class="bg-gray-50 dark:bg-white/5">
20
-                            <x-filament-tables::header-cell>Account</x-filament-tables::header-cell>
21
-                            <x-filament-tables::header-cell alignment="end">Starting Balance</x-filament-tables::header-cell>
22
-                            <x-filament-tables::header-cell alignment="end">Debit</x-filament-tables::header-cell>
23
-                            <x-filament-tables::header-cell alignment="end">Credit</x-filament-tables::header-cell>
24
-                            <x-filament-tables::header-cell alignment="end">Net Movement</x-filament-tables::header-cell>
25
-                            <x-filament-tables::header-cell alignment="end">Ending Balance</x-filament-tables::header-cell>
26
-                        </tr>
27
-                    </thead>
28
-                    @foreach($accountBalanceReport->categories as $accountCategoryName => $accountCategory)
29
-                        <tbody class="divide-y divide-gray-200 whitespace-nowrap dark:divide-white/5">
30
-                        <tr class="bg-gray-50 dark:bg-white/5">
31
-                            <x-filament-tables::cell colspan="6">
32
-                                <div class="px-3 py-2 text-sm font-medium text-gray-950 dark:text-white">{{ $accountCategoryName }}</div>
33
-                            </x-filament-tables::cell>
34
-                        </tr>
35
-                        @foreach($accountCategory->accounts as $account)
36
-                            <x-filament-tables::row>
37
-                                <x-filament-tables::cell><div class="px-3 py-4 text-sm leading-6 text-gray-950 dark:text-white">{{ $account->accountName }}</div></x-filament-tables::cell>
38
-                                <x-filament-tables::cell class="text-right"><div class="px-3 py-4 text-sm leading-6 text-gray-950 dark:text-white">{{ $account->balance->startingBalance ?? '' }}</div></x-filament-tables::cell>
39
-                                <x-filament-tables::cell class="text-right"><div class="px-3 py-4 text-sm leading-6 text-gray-950 dark:text-white">{{ $account->balance->debitBalance }}</div></x-filament-tables::cell>
40
-                                <x-filament-tables::cell class="text-right"><div class="px-3 py-4 text-sm leading-6 text-gray-950 dark:text-white">{{ $account->balance->creditBalance }}</div></x-filament-tables::cell>
41
-                                <x-filament-tables::cell class="text-right"><div class="px-3 py-4 text-sm leading-6 text-gray-950 dark:text-white">{{ $account->balance->netMovement }}</div></x-filament-tables::cell>
42
-                                <x-filament-tables::cell class="text-right"><div class="px-3 py-4 text-sm leading-6 text-gray-950 dark:text-white">{{ $account->balance->endingBalance ?? '' }}</div></x-filament-tables::cell>
43
-                            </x-filament-tables::row>
44
-                        @endforeach
45
-                        <x-filament-tables::row>
46
-                            <x-filament-tables::cell><div class="px-3 py-2 text-sm leading-6 font-semibold text-gray-950 dark:text-white">Total {{ $accountCategoryName }}</div></x-filament-tables::cell>
47
-                            <x-filament-tables::cell class="text-right"><div class="px-3 py-2 text-sm leading-6 font-semibold text-gray-950 dark:text-white">{{ $accountCategory->summary->startingBalance ?? '' }}</div></x-filament-tables::cell>
48
-                            <x-filament-tables::cell class="text-right"><div class="px-3 py-2 text-sm leading-6 font-semibold text-gray-950 dark:text-white">{{ $accountCategory->summary->debitBalance }}</div></x-filament-tables::cell>
49
-                            <x-filament-tables::cell class="text-right"><div class="px-3 py-2 text-sm leading-6 font-semibold text-gray-950 dark:text-white">{{ $accountCategory->summary->creditBalance }}</div></x-filament-tables::cell>
50
-                            <x-filament-tables::cell class="text-right"><div class="px-3 py-2 text-sm leading-6 font-semibold text-gray-950 dark:text-white">{{ $accountCategory->summary->netMovement }}</div></x-filament-tables::cell>
51
-                            <x-filament-tables::cell class="text-right"><div class="px-3 py-2 text-sm leading-6 font-semibold text-gray-950 dark:text-white">{{ $accountCategory->summary->endingBalance ?? '' }}</div></x-filament-tables::cell>
52
-                        </x-filament-tables::row>
53
-                        <x-filament-tables::row>
54
-                            <x-filament-tables::cell colspan="6">
55
-                                <div class="px-3 py-2 invisible">Hidden Text</div>
56
-                            </x-filament-tables::cell>
57
-                        </x-filament-tables::row>
58
-                        </tbody>
59
-                    @endforeach
60
-                    <tfoot>
61
-                        <tr class="bg-gray-50 dark:bg-white/5">
62
-                            <x-filament-tables::cell><div class="px-3 py-2 text-sm leading-6 font-semibold text-gray-950 dark:text-white">Total for all accounts</div></x-filament-tables::cell>
63
-                            <x-filament-tables::cell><div class="px-3 py-2 text-sm leading-6 font-semibold text-gray-950 dark:text-white"></div></x-filament-tables::cell>
64
-                            <x-filament-tables::cell class="text-right"><div class="px-3 py-2 text-sm leading-6 font-semibold text-gray-950 dark:text-white">{{ $accountBalanceReport->overallTotal->debitBalance }}</div></x-filament-tables::cell>
65
-                            <x-filament-tables::cell class="text-right"><div class="px-3 py-2 text-sm leading-6 font-semibold text-gray-950 dark:text-white">{{ $accountBalanceReport->overallTotal->creditBalance }}</div></x-filament-tables::cell>
66
-                            <x-filament-tables::cell><div class="px-3 py-2 text-sm leading-6 font-semibold text-gray-950 dark:text-white"></div></x-filament-tables::cell>
67
-                            <x-filament-tables::cell><div class="px-3 py-2 text-sm leading-6 font-semibold text-gray-950 dark:text-white"></div></x-filament-tables::cell>
68
-                        </tr>
69
-                    </tfoot>
70
-                </table>
71
-            </div>
72
-            <div class="es-table__footer-ctn border-t border-gray-200"></div>
73
-        </x-filament-tables::container>
74
-    </div>
75
-</x-filament-panels::page>

+ 25
- 0
resources/views/filament/company/pages/reports/detailed-report.blade.php View File

1
+<x-filament-panels::page>
2
+    <div class="flex flex-col gap-y-6">
3
+        <x-filament-tables::container>
4
+            <div class="p-6 divide-y divide-gray-200 dark:divide-white/5">
5
+                <form wire:submit.prevent="loadReportData">
6
+                    <div class="flex flex-col lg:flex-row items-start lg:items-center justify-center gap-4 lg:gap-12">
7
+                        {{ $this->form }}
8
+                        <x-filament-tables::column-toggle.dropdown
9
+                            class="my-auto"
10
+                            :form="$this->toggleTableColumnForm"
11
+                            :trigger-action="$this->toggleColumnsAction"
12
+                        />
13
+                        <x-filament::button type="submit" class="flex-shrink-0">
14
+                            Update Report
15
+                        </x-filament::button>
16
+                    </div>
17
+                </form>
18
+            </div>
19
+            <div class="relative divide-y divide-gray-200 overflow-x-auto dark:divide-white/10 dark:border-t-white/10">
20
+                <x-company.tables.reports.detailed-report :report="$this->report" />
21
+            </div>
22
+            <div class="es-table__footer-ctn border-t border-gray-200"></div>
23
+        </x-filament-tables::container>
24
+    </div>
25
+</x-filament-panels::page>

+ 2
- 2
resources/views/livewire/company/service/connected-account/list-institutions.blade.php View File

23
                             {{ $institution->name }}
23
                             {{ $institution->name }}
24
                         </h3>
24
                         </h3>
25
 
25
 
26
-                        @if($institution->getLastImportDate())
26
+                        @if($institution->latestImport)
27
                             <p class="connected-account-section-header-description text-sm leading-6 text-gray-500 dark:text-gray-400">
27
                             <p class="connected-account-section-header-description text-sm leading-6 text-gray-500 dark:text-gray-400">
28
-                                {{ __('Last updated') }} {{ $institution->getLastImportDate() }}
28
+                                {{ __('Last updated') }} {{ $institution->latestImport->last_imported_at->diffForHumans() }}
29
                             </p>
29
                             </p>
30
                         @endif
30
                         @endif
31
                     </div>
31
                     </div>

Loading…
Cancel
Save