浏览代码

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

Refactor app and reporting
3.x
Andrew Wallo 1年前
父节点
当前提交
339300e640
没有帐户链接到提交者的电子邮件
共有 80 个文件被更改,包括 3067 次插入1727 次删除
  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 查看文件

@@ -2,9 +2,6 @@
2 2
 
3 3
 namespace App\Contracts;
4 4
 
5
-use App\DTO\AccountBalanceDTO;
6
-use App\DTO\AccountBalanceReportDTO;
7
-use App\Enums\Accounting\AccountCategory;
8 5
 use App\Models\Accounting\Account;
9 6
 use App\ValueObjects\Money;
10 7
 
@@ -20,14 +17,8 @@ interface AccountHandler
20 17
 
21 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 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 22
     public function getTotalBalanceForAllBankAccounts(string $startDate, string $endDate): Money;
32 23
 
33 24
     public function getAccountCategoryOrder(): array;

+ 21
- 0
app/Contracts/ExportableReport.php 查看文件

@@ -0,0 +1,21 @@
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 查看文件

@@ -12,8 +12,7 @@ class AccountBalanceDTO implements Wireable
12 12
         public string $creditBalance,
13 13
         public ?string $netMovement,
14 14
         public ?string $endingBalance,
15
-    ) {
16
-    }
15
+    ) {}
17 16
 
18 17
     public function toLivewire(): array
19 18
     {

+ 1
- 2
app/DTO/AccountCategoryDTO.php 查看文件

@@ -12,8 +12,7 @@ class AccountCategoryDTO implements Wireable
12 12
     public function __construct(
13 13
         public array $accounts,
14 14
         public AccountBalanceDTO $summary,
15
-    ) {
16
-    }
15
+    ) {}
17 16
 
18 17
     public function toLivewire(): array
19 18
     {

+ 1
- 2
app/DTO/AccountDTO.php 查看文件

@@ -10,8 +10,7 @@ class AccountDTO implements Wireable
10 10
         public string $accountName,
11 11
         public string $accountCode,
12 12
         public AccountBalanceDTO $balance,
13
-    ) {
14
-    }
13
+    ) {}
15 14
 
16 15
     public function toLivewire(): array
17 16
     {

+ 17
- 0
app/DTO/ReportCategoryDTO.php 查看文件

@@ -0,0 +1,17 @@
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 查看文件

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

+ 3
- 9
app/Events/CompanyConfigured.php 查看文件

@@ -13,13 +13,7 @@ class CompanyConfigured
13 13
     use InteractsWithSockets;
14 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 查看文件

@@ -13,13 +13,10 @@ class CompanyDefaultEvent
13 13
     use InteractsWithSockets;
14 14
     use SerializesModels;
15 15
 
16
-    public Model $model;
17
-
18 16
     /**
19 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 查看文件

@@ -13,16 +13,11 @@ class CompanyDefaultUpdated
13 13
     use InteractsWithSockets;
14 14
     use SerializesModels;
15 15
 
16
-    public Model $record;
17
-
18
-    public array $data;
19
-
20 16
     /**
21 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 查看文件

@@ -12,25 +12,14 @@ class CompanyGenerated
12 12
     use Dispatchable;
13 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 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 查看文件

@@ -13,19 +13,9 @@ class CurrencyRateChanged
13 13
     use InteractsWithSockets;
14 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 查看文件

@@ -13,13 +13,10 @@ class DefaultCurrencyChanged
13 13
     use InteractsWithSockets;
14 14
     use SerializesModels;
15 15
 
16
-    public Currency $currency;
17
-
18 16
     /**
19 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 查看文件

@@ -11,19 +11,12 @@ class PlaidSuccess
11 11
     use Dispatchable;
12 12
     use SerializesModels;
13 13
 
14
-    public string $publicToken;
15
-
16
-    public string $accessToken;
17
-
18
-    public Company $company;
19
-
20 14
     /**
21 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 查看文件

@@ -12,22 +12,13 @@ class StartTransactionImport
12 12
     use Dispatchable;
13 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 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 查看文件

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

+ 2
- 2
app/Filament/Company/Clusters/Settings/Pages/Invoice.php 查看文件

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

+ 2
- 2
app/Filament/Company/Clusters/Settings/Pages/Localization.php 查看文件

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

+ 1
- 1
app/Filament/Company/Clusters/Settings/Resources/CurrencyResource/Pages/CreateCurrency.php 查看文件

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

+ 2
- 2
app/Filament/Company/Clusters/Settings/Resources/CurrencyResource/Pages/EditCurrency.php 查看文件

@@ -24,9 +24,9 @@ class EditCurrency extends EditRecord
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 32
     protected function mutateFormDataBeforeSave(array $data): array

+ 1
- 1
app/Filament/Company/Clusters/Settings/Resources/DiscountResource/Pages/CreateDiscount.php 查看文件

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

+ 1
- 1
app/Filament/Company/Clusters/Settings/Resources/DiscountResource/Pages/EditDiscount.php 查看文件

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

+ 1
- 1
app/Filament/Company/Clusters/Settings/Resources/TaxResource/Pages/CreateTax.php 查看文件

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

+ 1
- 1
app/Filament/Company/Clusters/Settings/Resources/TaxResource/Pages/EditTax.php 查看文件

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

+ 12
- 0
app/Filament/Company/Pages/Accounting/AccountChart.php 查看文件

@@ -10,6 +10,7 @@ use App\Utilities\Currency\CurrencyAccessor;
10 10
 use Filament\Actions\Action;
11 11
 use Filament\Actions\CreateAction;
12 12
 use Filament\Actions\EditAction;
13
+use Filament\Forms\Components\Checkbox;
13 14
 use Filament\Forms\Components\Component;
14 15
 use Filament\Forms\Components\Select;
15 16
 use Filament\Forms\Components\Textarea;
@@ -98,6 +99,7 @@ class AccountChart extends Page
98 99
                 $this->getNameFormComponent(),
99 100
                 $this->getCurrencyFormComponent(),
100 101
                 $this->getDescriptionFormComponent(),
102
+                $this->getArchiveFormComponent(),
101 103
             ]);
102 104
     }
103 105
 
@@ -161,6 +163,16 @@ class AccountChart extends Page
161 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 176
     private function getChartSubtypeOptions($useActiveTab = true): array
165 177
     {
166 178
         $subtypes = $useActiveTab ?

+ 38
- 14
app/Filament/Company/Pages/Accounting/Transactions.php 查看文件

@@ -11,6 +11,7 @@ use App\Filament\Company\Pages\Service\ConnectedAccount;
11 11
 use App\Filament\Forms\Components\DateRangeSelect;
12 12
 use App\Filament\Forms\Components\JournalEntryRepeater;
13 13
 use App\Models\Accounting\Account;
14
+use App\Models\Accounting\JournalEntry;
14 15
 use App\Models\Accounting\Transaction;
15 16
 use App\Models\Banking\BankAccount;
16 17
 use App\Models\Company;
@@ -127,15 +128,16 @@ class Transactions extends Page implements HasTable
127 128
         return $form
128 129
             ->schema([
129 130
                 Forms\Components\Select::make('bankAccountIdFiltered')
130
-                    ->label('Account')
131
-                    ->hiddenLabel()
132
-                    ->allowHtml()
133
-                    ->options(fn () => $this->getBankAccountOptions(true, true))
134 131
                     ->live()
132
+                    ->allowHtml()
133
+                    ->hiddenLabel()
134
+                    ->columnSpan(2)
135
+                    ->label('Account')
135 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 143
     public function transactionForm(Form $form): Form
@@ -149,7 +151,7 @@ class Transactions extends Page implements HasTable
149 151
                     ->label('Description'),
150 152
                 Forms\Components\Select::make('bank_account_id')
151 153
                     ->label('Account')
152
-                    ->options(fn () => $this->getBankAccountOptions())
154
+                    ->options(fn (?Transaction $transaction) => $this->getBankAccountOptions(currentBankAccountId: $transaction?->bank_account_id))
153 155
                     ->live()
154 156
                     ->searchable()
155 157
                     ->afterStateUpdated(function (Set $set, $state, $old, Get $get) {
@@ -179,7 +181,7 @@ class Transactions extends Page implements HasTable
179 181
                     ->required(),
180 182
                 Forms\Components\Select::make('account_id')
181 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 185
                     ->searchable()
184 186
                     ->preload()
185 187
                     ->required(),
@@ -224,12 +226,15 @@ class Transactions extends Page implements HasTable
224 226
                     ->sortable()
225 227
                     ->localizeDate(),
226 228
                 Tables\Columns\TextColumn::make('description')
229
+                    ->label('Description')
227 230
                     ->limit(30)
228
-                    ->label('Description'),
231
+                    ->toggleable(),
229 232
                 Tables\Columns\TextColumn::make('bankAccount.account.name')
230
-                    ->label('Account'),
233
+                    ->label('Account')
234
+                    ->toggleable(),
231 235
                 Tables\Columns\TextColumn::make('account.name')
232 236
                     ->label('Category')
237
+                    ->toggleable()
233 238
                     ->state(static fn (Transaction $transaction) => $transaction->account->name ?? 'Journal Entry'),
234 239
                 Tables\Columns\TextColumn::make('amount')
235 240
                     ->label('Amount')
@@ -296,6 +301,7 @@ class Transactions extends Page implements HasTable
296 301
                 $this->buildDateRangeFilter('updated_at', 'Last Modified'),
297 302
             ], layout: Tables\Enums\FiltersLayout::Modal)
298 303
             ->deferFilters()
304
+            ->deferLoading()
299 305
             ->filtersFormColumns(2)
300 306
             ->filtersTriggerAction(
301 307
                 fn (Tables\Actions\Action $action) => $action
@@ -557,7 +563,7 @@ class Transactions extends Page implements HasTable
557 563
                 ->label('Description'),
558 564
             Select::make('account_id')
559 565
                 ->label('Account')
560
-                ->options(fn (): array => $this->getChartAccountOptions())
566
+                ->options(fn (?JournalEntry $journalEntry): array => $this->getChartAccountOptions(currentAccountId: $journalEntry?->account_id))
561 567
                 ->live()
562 568
                 ->softRequired()
563 569
                 ->searchable(),
@@ -707,8 +713,10 @@ class Transactions extends Page implements HasTable
707 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 720
         $excludedCategory = match ($type) {
713 721
             TransactionType::Deposit => AccountCategory::Expense,
714 722
             TransactionType::Withdrawal => AccountCategory::Revenue,
@@ -716,16 +724,21 @@ class Transactions extends Page implements HasTable
716 724
         };
717 725
 
718 726
         return Account::query()
719
-            ->when($nominalAccountsOnly, fn (Builder $query) => $query->whereNull('accountable_type'))
727
+            ->when($nominalAccountsOnly, fn (Builder $query) => $query->doesntHave('bankAccount'))
720 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 733
             ->get()
722 734
             ->groupBy(fn (Account $account) => $account->category->getPluralLabel())
723 735
             ->map(fn (Collection $accounts, string $category) => $accounts->pluck('name', 'id'))
724 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 742
         $onlyWithTransactions ??= false;
730 743
 
731 744
         $options = $isFilter ? [
@@ -733,6 +746,17 @@ class Transactions extends Page implements HasTable
733 746
         ] : [];
734 747
 
735 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 760
             ->when($onlyWithTransactions, fn (Builder $query) => $query->has('transactions'))
737 761
             ->get()
738 762
             ->groupBy('account.subtype.name')

+ 6
- 6
app/Filament/Company/Pages/CreateCompany.php 查看文件

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

+ 2
- 1
app/Filament/Company/Pages/Reports.php 查看文件

@@ -3,6 +3,7 @@
3 3
 namespace App\Filament\Company\Pages;
4 4
 
5 5
 use App\Filament\Company\Pages\Reports\AccountBalances;
6
+use App\Filament\Company\Pages\Reports\TrialBalance;
6 7
 use App\Infolists\Components\ReportEntry;
7 8
 use Filament\Infolists\Components\Section;
8 9
 use Filament\Infolists\Infolist;
@@ -38,7 +39,7 @@ class Reports extends Page
38 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 40
                             ->icon('heroicon-o-scale')
40 41
                             ->iconColor(Color::Sky)
41
-                            ->url('#'),
42
+                            ->url(TrialBalance::getUrl()),
42 43
                         ReportEntry::make('account_transactions')
43 44
                             ->hiddenLabel()
44 45
                             ->heading('Account Transactions')

+ 70
- 113
app/Filament/Company/Pages/Reports/AccountBalances.php 查看文件

@@ -2,149 +2,106 @@
2 2
 
3 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 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 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 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 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 查看文件

@@ -0,0 +1,249 @@
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 查看文件

@@ -0,0 +1,92 @@
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 查看文件

@@ -11,6 +11,7 @@ use App\Models\Banking\BankAccount;
11 11
 use Filament\Forms;
12 12
 use Filament\Forms\Form;
13 13
 use Filament\Resources\Resource;
14
+use Filament\Support\Enums\Alignment;
14 15
 use Filament\Support\Enums\FontWeight;
15 16
 use Filament\Tables;
16 17
 use Filament\Tables\Table;
@@ -111,12 +112,16 @@ class AccountResource extends Resource
111 112
                     ->icon(static fn (BankAccount $record) => $record->isEnabled() ? 'heroicon-o-lock-closed' : null)
112 113
                     ->tooltip(static fn (BankAccount $record) => $record->isEnabled() ? 'Default Account' : null)
113 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 120
                 Tables\Columns\TextColumn::make('account.ending_balance')
117
-                    ->localizeLabel('Current Balance')
121
+                    ->localizeLabel('Ending Balance')
118 122
                     ->state(static fn (BankAccount $record) => $record->account->ending_balance->convert()->formatWithCode())
119
-                    ->sortable(),
123
+                    ->toggleable()
124
+                    ->alignment(Alignment::End),
120 125
             ])
121 126
             ->filters([
122 127
                 //

+ 1
- 1
app/Filament/Company/Resources/Banking/AccountResource/Pages/CreateAccount.php 查看文件

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

+ 2
- 2
app/Filament/Company/Resources/Banking/AccountResource/Pages/EditAccount.php 查看文件

@@ -19,9 +19,9 @@ class EditAccount extends EditRecord
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 27
     protected function mutateFormDataBeforeSave(array $data): array

+ 1
- 1
app/Filament/Company/Resources/Core/DepartmentResource/Pages/CreateDepartment.php 查看文件

@@ -11,6 +11,6 @@ class CreateDepartment extends CreateRecord
11 11
 
12 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 查看文件

@@ -17,8 +17,8 @@ class EditDepartment extends EditRecord
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 查看文件

@@ -29,14 +29,14 @@ class PlaidWebhookController extends Controller
29 29
     private function handleDefaultUpdate(array $payload): void
30 30
     {
31 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 36
         })->first();
37 37
 
38 38
         if ($company && $newTransactions > 0) {
39
-            ProcessTransactionUpdate::dispatch($company, $itemID)
39
+            ProcessTransactionUpdate::dispatch($company, $itemId)
40 40
                 ->onQueue('transactions');
41 41
         }
42 42
     }

+ 7
- 21
app/Jobs/ProcessTransactionImport.php 查看文件

@@ -22,27 +22,13 @@ class ProcessTransactionImport implements ShouldQueue
22 22
     use Queueable;
23 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 34
      * Execute the job.

+ 5
- 13
app/Jobs/ProcessTransactionUpdate.php 查看文件

@@ -20,18 +20,10 @@ class ProcessTransactionUpdate implements ShouldQueue
20 20
     use Queueable;
21 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 29
      * Execute the job.
@@ -39,7 +31,7 @@ class ProcessTransactionUpdate implements ShouldQueue
39 31
     public function handle(PlaidService $plaidService, TransactionService $transactionService): void
40 32
     {
41 33
         $connectedBankAccounts = $this->company->connectedBankAccounts()
42
-            ->where('item_id', $this->item_id)
34
+            ->where('item_id', $this->itemId)
43 35
             ->where('import_transactions', true)
44 36
             ->get();
45 37
 

+ 1
- 2
app/Listeners/ConfigureChartOfAccounts.php 查看文件

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

+ 5
- 8
app/Listeners/CreateConnectedAccount.php 查看文件

@@ -10,15 +10,12 @@ use Illuminate\Support\Facades\DB;
10 10
 
11 11
 class CreateConnectedAccount
12 12
 {
13
-    protected PlaidService $plaid;
14
-
15 13
     /**
16 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 21
      * Handle the event.
@@ -36,9 +33,9 @@ class CreateConnectedAccount
36 33
 
37 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 40
         $this->processInstitution($authResponse, $institutionResponse, $company, $accessToken);
44 41
     }

+ 3
- 6
app/Listeners/HandleTransactionImport.php 查看文件

@@ -9,15 +9,12 @@ use Illuminate\Support\Facades\DB;
9 9
 
10 10
 class HandleTransactionImport
11 11
 {
12
-    protected ConnectedBankAccountService $connectedBankAccountService;
13
-
14 12
     /**
15 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 20
      * Handle the event.

+ 3
- 4
app/Listeners/UpdateCurrencyRates.php 查看文件

@@ -13,10 +13,9 @@ readonly class UpdateCurrencyRates
13 13
     /**
14 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 21
      * Handle the event.

+ 6
- 7
app/Models/Accounting/Account.php 查看文件

@@ -7,6 +7,7 @@ use App\Concerns\CompanyOwned;
7 7
 use App\Enums\Accounting\AccountCategory;
8 8
 use App\Enums\Accounting\AccountType;
9 9
 use App\Facades\Accounting;
10
+use App\Models\Banking\BankAccount;
10 11
 use App\Models\Setting\Currency;
11 12
 use App\Observers\AccountObserver;
12 13
 use Database\Factories\Accounting\AccountFactory;
@@ -17,7 +18,6 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
17 18
 use Illuminate\Database\Eloquent\Model;
18 19
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
19 20
 use Illuminate\Database\Eloquent\Relations\HasMany;
20
-use Illuminate\Database\Eloquent\Relations\MorphTo;
21 21
 use Illuminate\Support\Carbon;
22 22
 
23 23
 #[ObservedBy(AccountObserver::class)]
@@ -39,10 +39,9 @@ class Account extends Model
39 39
         'name',
40 40
         'currency_code',
41 41
         'description',
42
-        'active',
42
+        'archived',
43 43
         'default',
44
-        'accountable_id',
45
-        'accountable_type',
44
+        'bank_account_id',
46 45
         'created_by',
47 46
         'updated_by',
48 47
     ];
@@ -50,7 +49,7 @@ class Account extends Model
50 49
     protected $casts = [
51 50
         'category' => AccountCategory::class,
52 51
         'type' => AccountType::class,
53
-        'active' => 'boolean',
52
+        'archived' => 'boolean',
54 53
         'default' => 'boolean',
55 54
     ];
56 55
 
@@ -75,9 +74,9 @@ class Account extends Model
75 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 82
     public function getLastTransactionDate(): ?string

+ 0
- 6
app/Models/Accounting/JournalEntry.php 查看文件

@@ -7,7 +7,6 @@ use App\Collections\Accounting\JournalEntryCollection;
7 7
 use App\Concerns\Blamable;
8 8
 use App\Concerns\CompanyOwned;
9 9
 use App\Enums\Accounting\JournalEntryType;
10
-use App\Models\Banking\BankAccount;
11 10
 use Database\Factories\Accounting\JournalEntryFactory;
12 11
 use Illuminate\Database\Eloquent\Factories\Factory;
13 12
 use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -46,11 +45,6 @@ class JournalEntry extends Model
46 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 48
     public function isUncategorized(): bool
55 49
     {
56 50
         return $this->account->isUncategorized();

+ 2
- 3
app/Models/Banking/BankAccount.php 查看文件

@@ -19,7 +19,6 @@ use Illuminate\Database\Eloquent\Model;
19 19
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
20 20
 use Illuminate\Database\Eloquent\Relations\HasMany;
21 21
 use Illuminate\Database\Eloquent\Relations\HasOne;
22
-use Illuminate\Database\Eloquent\Relations\MorphOne;
23 22
 
24 23
 #[ObservedBy(BankAccountObserver::class)]
25 24
 class BankAccount extends Model
@@ -56,9 +55,9 @@ class BankAccount extends Model
56 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 63
     public function institution(): BelongsTo

+ 3
- 28
app/Models/Banking/Institution.php 查看文件

@@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Collection;
7 7
 use Illuminate\Database\Eloquent\Factories\HasFactory;
8 8
 use Illuminate\Database\Eloquent\Model;
9 9
 use Illuminate\Database\Eloquent\Relations\HasMany;
10
-use Illuminate\Support\Carbon;
10
+use Illuminate\Database\Eloquent\Relations\HasOne;
11 11
 use Illuminate\Support\Facades\Storage;
12 12
 
13 13
 class Institution extends Model
@@ -46,34 +46,9 @@ class Institution extends Model
46 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 54
     protected function logoUrl(): Attribute

+ 11
- 4
app/Observers/AccountObserver.php 查看文件

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

+ 1
- 3
app/Providers/CurrencyServiceProvider.php 查看文件

@@ -21,7 +21,5 @@ class CurrencyServiceProvider extends ServiceProvider
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 查看文件

@@ -1,88 +0,0 @@
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 查看文件

@@ -10,10 +10,7 @@ use Squire\Repository;
10 10
 
11 11
 class SquireServiceProvider extends ServiceProvider
12 12
 {
13
-    public function register(): void
14
-    {
15
-
16
-    }
13
+    public function register(): void {}
17 14
 
18 15
     public function boot(): void
19 16
     {

+ 0
- 1
app/Repositories/Banking/ConnectedBankAccountRepository.php 查看文件

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

+ 0
- 76
app/Services/AccountBalancesExportService.php 查看文件

@@ -1,76 +0,0 @@
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 查看文件

@@ -3,10 +3,6 @@
3 3
 namespace App\Services;
4 4
 
5 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 6
 use App\Enums\Accounting\AccountCategory;
11 7
 use App\Models\Accounting\Account;
12 8
 use App\Models\Accounting\Transaction;
@@ -14,16 +10,12 @@ use App\Models\Banking\BankAccount;
14 10
 use App\Repositories\Accounting\JournalEntryRepository;
15 11
 use App\Utilities\Currency\CurrencyAccessor;
16 12
 use App\ValueObjects\Money;
17
-use Illuminate\Database\Eloquent\Collection;
18 13
 
19 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 20
     public function getDebitBalance(Account $account, string $startDate, string $endDate): Money
29 21
     {
@@ -63,18 +55,19 @@ class AccountService implements AccountHandler
63 55
 
64 56
     public function getEndingBalance(Account $account, string $startDate, string $endDate): ?Money
65 57
     {
58
+        $netMovement = $this->getNetMovement($account, $startDate, $endDate)->getAmount();
59
+
66 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 64
         $startingBalance = $this->getStartingBalance($account, $startDate)?->getAmount();
71
-        $netMovement = $this->getNetMovement($account, $startDate, $endDate)->getAmount();
72 65
         $endingBalance = $startingBalance + $netMovement;
73 66
 
74 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 72
         return match ($category) {
80 73
             AccountCategory::Asset, AccountCategory::Expense => $debitBalance - $creditBalance,
@@ -102,102 +95,20 @@ class AccountService implements AccountHandler
102 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 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 101
             ->get();
194 102
 
195 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 114
         return new Money($totalBalance, CurrencyAccessor::getDefaultCurrency());

+ 4
- 9
app/Services/ConnectedBankAccountService.php 查看文件

@@ -11,15 +11,10 @@ use App\Repositories\Banking\ConnectedBankAccountRepository;
11 11
 
12 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 19
     public function getOrProcessBankAccountForConnectedBankAccount(Company $company, ConnectedBankAccount $connectedBankAccount, int | string $selectedBankAccountId): BankAccount
25 20
     {

+ 8
- 15
app/Services/CurrencyService.php 查看文件

@@ -10,25 +10,18 @@ use Illuminate\Support\Facades\Log;
10 10
 
11 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 20
      * Determine if the Currency Exchange Rate feature is enabled.
28 21
      */
29 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 27
     public function getSupportedCurrencies(): ?array
@@ -38,7 +31,7 @@ class CurrencyService implements CurrencyHandler
38 31
         }
39 32
 
40 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 36
             if ($response->getStatusCode() === 200) {
44 37
                 $responseData = json_decode($response->getBody()->getContents(), true);
@@ -106,7 +99,7 @@ class CurrencyService implements CurrencyHandler
106 99
     public function updateCurrencyRatesCache(string $baseCurrency): ?array
107 100
     {
108 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 104
             if ($response->getStatusCode() === 200) {
112 105
                 $responseData = json_decode($response->getBody()->getContents(), true);

+ 64
- 0
app/Services/ExportService.php 查看文件

@@ -0,0 +1,64 @@
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 查看文件

@@ -15,15 +15,15 @@ class PlaidService
15 15
 {
16 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 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 28
     protected HttpClient $client;
29 29
 
@@ -50,18 +50,18 @@ class PlaidService
50 50
     {
51 51
         $this->client = $client;
52 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 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 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 66
         return $this;
67 67
     }
@@ -77,7 +77,7 @@ class PlaidService
77 77
 
78 78
     public function setBaseUrl(?string $environment): void
79 79
     {
80
-        $this->base_url = match ($environment) {
80
+        $this->baseUrl = match ($environment) {
81 81
             'development' => 'https://development.plaid.com',
82 82
             'production' => 'https://production.plaid.com',
83 83
             default => 'https://sandbox.plaid.com', // Default to sandbox, including if environment is null
@@ -86,7 +86,7 @@ class PlaidService
86 86
 
87 87
     public function getBaseUrl(): string
88 88
     {
89
-        return $this->base_url;
89
+        return $this->baseUrl;
90 90
     }
91 91
 
92 92
     public function getEnvironment(): string
@@ -99,12 +99,12 @@ class PlaidService
99 99
         $request = $this->client->withHeaders([
100 100
             'Plaid-Version' => self::API_VERSION,
101 101
             'Content-Type' => 'application/json',
102
-        ])->baseUrl($this->base_url);
102
+        ])->baseUrl($this->baseUrl);
103 103
 
104 104
         if ($method === 'post') {
105 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,12 +181,12 @@ class PlaidService
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 186
         $data = [
187
-            'client_name' => $client_name,
187
+            'client_name' => $clientName,
188 188
             'language' => $language,
189
-            'country_codes' => $country_codes,
189
+            'country_codes' => $countryCodes,
190 190
             'user' => (object) $user,
191 191
         ];
192 192
 
@@ -194,16 +194,18 @@ class PlaidService
194 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 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 210
         return $this->sendRequest('item/public_token/exchange', $data);
209 211
     }
@@ -218,7 +220,7 @@ class PlaidService
218 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 225
         $options = [
224 226
             'include_optional_metadata' => true,
@@ -226,49 +228,57 @@ class PlaidService
226 228
 
227 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 236
         $data = [
235
-            'institution_id' => $institution_id,
236
-            'country_codes' => $country_codes,
237
+            'institution_id' => $institutionId,
238
+            'country_codes' => $countryCodes,
237 239
             'options' => (object) $options,
238 240
         ];
239 241
 
240 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 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 251
             'options' => (object) $options,
250 252
         ];
251 253
 
252 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 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 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 283
         return $this->sendRequest('item/remove', $data);
274 284
     }

+ 170
- 0
app/Services/ReportService.php 查看文件

@@ -0,0 +1,170 @@
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 查看文件

@@ -0,0 +1,43 @@
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 查看文件

@@ -0,0 +1,98 @@
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 查看文件

@@ -0,0 +1,52 @@
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 查看文件

@@ -0,0 +1,92 @@
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 查看文件

@@ -7,17 +7,12 @@ use App\Utilities\Currency\CurrencyConverter;
7 7
 
8 8
 class Money
9 9
 {
10
-    private int $amount;
11
-
12
-    private string $currencyCode;
13
-
14 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 17
     public function getAmount(): int
23 18
     {

+ 4
- 9
app/View/Models/InvoiceViewModel.php 查看文件

@@ -11,15 +11,10 @@ class InvoiceViewModel
11 11
 {
12 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 19
     public function logo(): ?string
25 20
     {

+ 0
- 1
bootstrap/providers.php 查看文件

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

+ 2
- 1
composer.json 查看文件

@@ -35,7 +35,8 @@
35 35
         "mockery/mockery": "^1.6",
36 36
         "nunomaduro/collision": "^8.0",
37 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 41
     "autoload": {
41 42
         "psr-4": {

+ 1290
- 492
composer.lock
文件差异内容过多而无法显示
查看文件


+ 14
- 15
database/migrations/2023_09_03_100000_create_accounting_tables.php 查看文件

@@ -36,6 +36,18 @@ return new class extends Migration
36 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 51
         Schema::create('accounts', function (Blueprint $table) {
40 52
             $table->id();
41 53
             $table->foreignId('company_id')->constrained()->cascadeOnDelete();
@@ -47,10 +59,9 @@ return new class extends Migration
47 59
             $table->string('name')->nullable()->index();
48 60
             $table->string('currency_code')->nullable();
49 61
             $table->text('description')->nullable();
50
-            $table->boolean('active')->default(true);
62
+            $table->boolean('archived')->default(false);
51 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 65
             $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
55 66
             $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
56 67
             $table->timestamps();
@@ -58,18 +69,6 @@ return new class extends Migration
58 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 72
         Schema::create('connected_bank_accounts', function (Blueprint $table) {
74 73
             $table->id();
75 74
             $table->foreignId('company_id')->constrained()->cascadeOnDelete();

+ 273
- 263
package-lock.json
文件差异内容过多而无法显示
查看文件


+ 7
- 7
package.json 查看文件

@@ -7,13 +7,13 @@
7 7
     },
8 8
     "devDependencies": {
9 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 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 查看文件

@@ -187,5 +187,7 @@
187 187
     "Available": "Available",
188 188
     "Live Rate": "Live Rate",
189 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 查看文件

@@ -1,127 +0,0 @@
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 查看文件

@@ -0,0 +1,148 @@
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 查看文件

@@ -0,0 +1,62 @@
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 查看文件

@@ -21,16 +21,17 @@
21 21
                         <table class="es-table table-fixed w-full divide-y divide-gray-200 text-start text-sm dark:divide-white/5">
22 22
                             <colgroup>
23 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 26
                                 <col span="1" style="width: 15%;">
27
+                                <col span="1" style="width: 10%;">
27 28
                                 <col span="1" style="width: 7.5%;">
28 29
                             </colgroup>
29 30
                             @foreach($subtypes as $subtype)
30 31
                                 <tbody class="es-table__rowgroup divide-y divide-gray-200 whitespace-nowrap dark:divide-white/5">
31 32
                                 <!-- Subtype Name Header Row -->
32 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 35
                                         <div class="es-table__row-content flex items-center space-x-2">
35 36
                                             <span class="es-table__row-title text-gray-800 dark:text-gray-200 font-semibold tracking-wider">
36 37
                                                 {{ $subtype->name }}
@@ -61,6 +62,13 @@
61 62
                                             </small>
62 63
                                         </td>
63 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 72
                                         <td colspan="1" class="es-table__cell px-4 py-4">
65 73
                                             <div>
66 74
                                                 @if($account->default === false)

+ 0
- 10
resources/views/filament/company/pages/create-company.blade.php 查看文件

@@ -1,10 +0,0 @@
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 查看文件

@@ -1,75 +0,0 @@
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 查看文件

@@ -0,0 +1,25 @@
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 查看文件

@@ -23,9 +23,9 @@
23 23
                             {{ $institution->name }}
24 24
                         </h3>
25 25
 
26
-                        @if($institution->getLastImportDate())
26
+                        @if($institution->latestImport)
27 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 29
                             </p>
30 30
                         @endif
31 31
                     </div>

正在加载...
取消
保存