Browse Source

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

Add Account Transactions Report & Change to SnappyPDF
3.x
Andrew Wallo 1 year ago
parent
commit
f4974ce757
No account linked to committer's email address
36 changed files with 1380 additions and 930 deletions
  1. 19
    0
      README.md
  2. 2
    0
      app/Contracts/ExportableReport.php
  3. 14
    0
      app/DTO/AccountTransactionDTO.php
  4. 1
    6
      app/DTO/ReportCategoryDTO.php
  5. 5
    5
      app/DTO/ReportDTO.php
  6. 41
    1
      app/Filament/Company/Clusters/Settings/Pages/CompanyProfile.php
  7. 2
    1
      app/Filament/Company/Pages/Reports.php
  8. 1
    4
      app/Filament/Company/Pages/Reports/AccountBalances.php
  9. 125
    0
      app/Filament/Company/Pages/Reports/AccountTransactions.php
  10. 5
    0
      app/Filament/Company/Pages/Reports/BaseReportPage.php
  11. 3
    77
      app/Listeners/ConfigureChartOfAccounts.php
  12. 2
    2
      app/Services/AccountService.php
  13. 84
    0
      app/Services/ChartOfAccountsService.php
  14. 19
    8
      app/Services/ExportService.php
  15. 96
    0
      app/Services/ReportService.php
  16. 79
    0
      app/Transformers/AccountTransactionReportTransformer.php
  17. 5
    0
      app/Transformers/BaseReportTransformer.php
  18. 13
    3
      app/Utilities/Currency/CurrencyAccessor.php
  19. 6
    1
      app/Utilities/Localization/Timezone.php
  20. 5
    0
      app/ValueObjects/Money.php
  21. 1
    1
      composer.json
  22. 402
    474
      composer.lock
  23. 0
    267
      config/dompdf.php
  24. 63
    0
      config/snappy.php
  25. 37
    1
      database/factories/Accounting/TransactionFactory.php
  26. 29
    22
      database/factories/UserFactory.php
  27. 23
    23
      package-lock.json
  28. 11
    3
      resources/css/filament/company/theme.css
  29. 146
    0
      resources/views/components/company/reports/account-transactions-report-pdf.blade.php
  30. 0
    5
      resources/views/components/company/reports/report-pdf.blade.php
  31. 68
    0
      resources/views/components/company/tables/reports/account-transactions.blade.php
  32. 21
    0
      resources/views/filament/company/pages/reports/account-transactions.blade.php
  33. 25
    26
      resources/views/filament/company/pages/reports/detailed-report.blade.php
  34. 0
    0
      resources/views/vendor/filament-clusters/.gitkeep
  35. 24
    0
      resources/views/vendor/filament-clusters/cluster.blade.php
  36. 3
    0
      resources/views/vendor/filament-clusters/components/field-wrapper.blade.php

+ 19
- 0
README.md View File

81
 
81
 
82
     php artisan migrate:refresh
82
     php artisan migrate:refresh
83
 
83
 
84
+## Generating PDFs for Reports
85
+
86
+To generate PDFs for reports, the application uses Laravel Snappy. The Laravel Snappy package is already included in the application, but you need to install Wkhtmltopdf separately.
87
+
88
+### Wkhtmltopdf Installation
89
+
90
+1. **Download and install Wkhtmltopdf**
91
+   - [Wkhtmltopdf Downloads](https://wkhtmltopdf.org/downloads.html)
92
+   
93
+   - Alternatively, if you are using Homebrew on macOS, you can install it using the following command:
94
+     ```bash
95
+     brew install wkhtmltopdf
96
+     ```
97
+
98
+2. **Configure the binary paths**
99
+   - If needed, you can change the paths to the Wkhtmltopdf binaries in the Snappy configuration file (`config/snappy.php`).
100
+
101
+For detailed installation instructions, refer to the [Laravel Snappy documentation](https://github.com/barryvdh/laravel-snappy).
102
+
84
 ## Live Currency
103
 ## Live Currency
85
 
104
 
86
 ### Overview
105
 ### Overview

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

18
     public function getOverallTotals(): array;
18
     public function getOverallTotals(): array;
19
 
19
 
20
     public function getColumns(): array;
20
     public function getColumns(): array;
21
+
22
+    public function getPdfView(): string;
21
 }
23
 }

+ 14
- 0
app/DTO/AccountTransactionDTO.php View File

1
+<?php
2
+
3
+namespace App\DTO;
4
+
5
+class AccountTransactionDTO
6
+{
7
+    public function __construct(
8
+        public string $date,
9
+        public string $description,
10
+        public string $debit,
11
+        public string $credit,
12
+        public string $balance,
13
+    ) {}
14
+}

+ 1
- 6
app/DTO/ReportCategoryDTO.php View File

4
 
4
 
5
 class ReportCategoryDTO
5
 class ReportCategoryDTO
6
 {
6
 {
7
-    /**
8
-     * @param  string[]  $header
9
-     * @param  string[][]  $data
10
-     * @param  string[]  $summary
11
-     */
12
     public function __construct(
7
     public function __construct(
13
         public array $header,
8
         public array $header,
14
         public array $data,
9
         public array $data,
15
-        public array $summary,
10
+        public array $summary = [],
16
     ) {}
11
     ) {}
17
 }
12
 }

+ 5
- 5
app/DTO/ReportDTO.php View File

11
          * @var AccountCategoryDTO[]
11
          * @var AccountCategoryDTO[]
12
          */
12
          */
13
         public array $categories,
13
         public array $categories,
14
-        public AccountBalanceDTO $overallTotal,
15
-        public array $fields,
14
+        public ?AccountBalanceDTO $overallTotal = null,
15
+        public array $fields = [],
16
     ) {}
16
     ) {}
17
 
17
 
18
     public function toLivewire(): array
18
     public function toLivewire(): array
19
     {
19
     {
20
         return [
20
         return [
21
             'categories' => $this->categories,
21
             'categories' => $this->categories,
22
-            'overallTotal' => $this->overallTotal->toLivewire(),
22
+            'overallTotal' => $this->overallTotal?->toLivewire(),
23
             'fields' => $this->fields,
23
             'fields' => $this->fields,
24
         ];
24
         ];
25
     }
25
     }
28
     {
28
     {
29
         return new static(
29
         return new static(
30
             $value['categories'],
30
             $value['categories'],
31
-            AccountBalanceDTO::fromLivewire($value['overallTotal']),
32
-            $value['fields'],
31
+            isset($value['overallTotal']) ? AccountBalanceDTO::fromLivewire($value['overallTotal']) : null,
32
+            $value['fields'] ?? [],
33
         );
33
         );
34
     }
34
     }
35
 }
35
 }

+ 41
- 1
app/Filament/Company/Clusters/Settings/Pages/CompanyProfile.php View File

8
 use App\Models\Locale\Country;
8
 use App\Models\Locale\Country;
9
 use App\Models\Locale\State;
9
 use App\Models\Locale\State;
10
 use App\Models\Setting\CompanyProfile as CompanyProfileModel;
10
 use App\Models\Setting\CompanyProfile as CompanyProfileModel;
11
+use App\Utilities\Localization\Timezone;
11
 use Filament\Actions\Action;
12
 use Filament\Actions\Action;
12
 use Filament\Actions\ActionGroup;
13
 use Filament\Actions\ActionGroup;
13
 use Filament\Forms\Components\Component;
14
 use Filament\Forms\Components\Component;
90
             $data = $this->form->getState();
91
             $data = $this->form->getState();
91
 
92
 
92
             $this->handleRecordUpdate($this->record, $data);
93
             $this->handleRecordUpdate($this->record, $data);
93
-
94
         } catch (Halt $exception) {
94
         } catch (Halt $exception) {
95
             return;
95
             return;
96
         }
96
         }
97
 
97
 
98
+        $countryChanged = $this->record->wasChanged('country');
99
+        $stateChanged = $this->record->wasChanged('state_id');
100
+
98
         $this->getSavedNotification()->send();
101
         $this->getSavedNotification()->send();
102
+
103
+        if ($countryChanged || $stateChanged) {
104
+            if ($countryChanged) {
105
+                $this->updateTimezone($this->record->country);
106
+            }
107
+
108
+            $this->getTimezoneChangeNotification()->send();
109
+        }
110
+    }
111
+
112
+    protected function updateTimezone(string $countryCode): void
113
+    {
114
+        $model = \App\Models\Setting\Localization::firstOrFail();
115
+
116
+        $timezones = Timezone::getTimezonesForCountry($countryCode);
117
+
118
+        if (! empty($timezones)) {
119
+            $model->update([
120
+                'timezone' => $timezones[0],
121
+            ]);
122
+        }
123
+    }
124
+
125
+    protected function getTimezoneChangeNotification(): Notification
126
+    {
127
+        return Notification::make()
128
+            ->warning()
129
+            ->title('Timezone Update Required')
130
+            ->body('You have changed your country or state. Please update your timezone to ensure accurate date and time information.')
131
+            ->actions([
132
+                \Filament\Notifications\Actions\Action::make('updateTimezone')
133
+                    ->label('Update Timezone')
134
+                    ->url(Localization::getUrl()),
135
+            ])
136
+            ->persistent()
137
+            ->send();
99
     }
138
     }
100
 
139
 
101
     protected function getSavedNotification(): Notification
140
     protected function getSavedNotification(): Notification
176
                     ->searchable()
215
                     ->searchable()
177
                     ->live()
216
                     ->live()
178
                     ->options(static fn (Get $get) => State::getStateOptions($get('country')))
217
                     ->options(static fn (Get $get) => State::getStateOptions($get('country')))
218
+                    ->afterStateUpdated(static fn (Set $set) => $set('city_id', null))
179
                     ->nullable(),
219
                     ->nullable(),
180
                 TextInput::make('address')
220
                 TextInput::make('address')
181
                     ->localizeLabel('Street Address')
221
                     ->localizeLabel('Street Address')

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

3
 namespace App\Filament\Company\Pages;
3
 namespace App\Filament\Company\Pages;
4
 
4
 
5
 use App\Filament\Company\Pages\Reports\AccountBalances;
5
 use App\Filament\Company\Pages\Reports\AccountBalances;
6
+use App\Filament\Company\Pages\Reports\AccountTransactions;
6
 use App\Filament\Company\Pages\Reports\TrialBalance;
7
 use App\Filament\Company\Pages\Reports\TrialBalance;
7
 use App\Infolists\Components\ReportEntry;
8
 use App\Infolists\Components\ReportEntry;
8
 use Filament\Infolists\Components\Section;
9
 use Filament\Infolists\Components\Section;
46
                             ->description('A record of all transactions for a company. The general ledger is the core of a company\'s financial records.')
47
                             ->description('A record of all transactions for a company. The general ledger is the core of a company\'s financial records.')
47
                             ->icon('heroicon-o-adjustments-horizontal')
48
                             ->icon('heroicon-o-adjustments-horizontal')
48
                             ->iconColor(Color::Amber)
49
                             ->iconColor(Color::Amber)
49
-                            ->url('#'),
50
+                            ->url(AccountTransactions::getUrl()),
50
                     ]),
51
                     ]),
51
             ]);
52
             ]);
52
     }
53
     }

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

71
     {
71
     {
72
         return $form
72
         return $form
73
             ->inlineLabel()
73
             ->inlineLabel()
74
-            ->columns([
75
-                'lg' => 1,
76
-                '2xl' => 2,
77
-            ])
74
+            ->columns()
78
             ->live()
75
             ->live()
79
             ->schema([
76
             ->schema([
80
                 $this->getDateRangeFormComponent(),
77
                 $this->getDateRangeFormComponent(),

+ 125
- 0
app/Filament/Company/Pages/Reports/AccountTransactions.php View File

1
+<?php
2
+
3
+namespace App\Filament\Company\Pages\Reports;
4
+
5
+use App\Contracts\ExportableReport;
6
+use App\DTO\ReportDTO;
7
+use App\Models\Accounting\Account;
8
+use App\Services\ExportService;
9
+use App\Services\ReportService;
10
+use App\Support\Column;
11
+use App\Transformers\AccountTransactionReportTransformer;
12
+use Filament\Forms\Components\Actions;
13
+use Filament\Forms\Components\Select;
14
+use Filament\Forms\Form;
15
+use Filament\Support\Enums\Alignment;
16
+use Guava\FilamentClusters\Forms\Cluster;
17
+use Illuminate\Support\Collection;
18
+use Livewire\Attributes\Session;
19
+use Symfony\Component\HttpFoundation\StreamedResponse;
20
+
21
+class AccountTransactions extends BaseReportPage
22
+{
23
+    protected static string $view = 'filament.company.pages.reports.account-transactions';
24
+
25
+    protected static ?string $slug = 'reports/account-transactions';
26
+
27
+    protected static bool $shouldRegisterNavigation = false;
28
+
29
+    protected ReportService $reportService;
30
+
31
+    protected ExportService $exportService;
32
+
33
+    #[Session]
34
+    public ?string $account_id = 'all';
35
+
36
+    public function boot(ReportService $reportService, ExportService $exportService): void
37
+    {
38
+        $this->reportService = $reportService;
39
+        $this->exportService = $exportService;
40
+    }
41
+
42
+    /**
43
+     * @return array<Column>
44
+     */
45
+    public function getTable(): array
46
+    {
47
+        return [
48
+            Column::make('date')
49
+                ->label('Date')
50
+                ->alignment(Alignment::Left),
51
+            Column::make('description')
52
+                ->label('Description')
53
+                ->alignment(Alignment::Left),
54
+            Column::make('debit')
55
+                ->label('Debit')
56
+                ->alignment(Alignment::Right),
57
+            Column::make('credit')
58
+                ->label('Credit')
59
+                ->alignment(Alignment::Right),
60
+            Column::make('balance')
61
+                ->label('Balance')
62
+                ->alignment(Alignment::Right),
63
+        ];
64
+    }
65
+
66
+    public function form(Form $form): Form
67
+    {
68
+        return $form
69
+            ->columns(4)
70
+            ->live()
71
+            ->schema([
72
+                Select::make('account_id')
73
+                    ->label('Account')
74
+                    ->options($this->getAccountOptions())
75
+                    ->selectablePlaceholder(false)
76
+                    ->searchable(),
77
+                $this->getDateRangeFormComponent(),
78
+                Cluster::make([
79
+                    $this->getStartDateFormComponent(),
80
+                    $this->getEndDateFormComponent(),
81
+                ])->label("\u{200B}"), // its too bad hiddenLabel removes spacing of the label
82
+                Actions::make([
83
+                    Actions\Action::make('loadReportData')
84
+                        ->label('Update Report')
85
+                        ->submit('loadReportData')
86
+                        ->keyBindings(['mod+s']),
87
+                ])->alignEnd()->verticallyAlignEnd(),
88
+            ]);
89
+    }
90
+
91
+    protected function getAccountOptions(): array
92
+    {
93
+        $accounts = Account::query()
94
+            ->get()
95
+            ->groupBy(fn (Account $account) => $account->category->getPluralLabel())
96
+            ->map(fn (Collection $accounts, string $category) => $accounts->pluck('name', 'id'))
97
+            ->toArray();
98
+
99
+        $allAccountsOption = [
100
+            'All Accounts' => ['all' => 'All Accounts'],
101
+        ];
102
+
103
+        return $allAccountsOption + $accounts;
104
+    }
105
+
106
+    protected function buildReport(array $columns): ReportDTO
107
+    {
108
+        return $this->reportService->buildAccountTransactionsReport($this->startDate, $this->endDate, $columns, $this->account_id);
109
+    }
110
+
111
+    protected function getTransformer(ReportDTO $reportDTO): ExportableReport
112
+    {
113
+        return new AccountTransactionReportTransformer($reportDTO);
114
+    }
115
+
116
+    public function exportCSV(): StreamedResponse
117
+    {
118
+        return $this->exportService->exportToCsv($this->company, $this->report, $this->startDate, $this->endDate);
119
+    }
120
+
121
+    public function exportPDF(): StreamedResponse
122
+    {
123
+        return $this->exportService->exportToPdf($this->company, $this->report, $this->startDate, $this->endDate);
124
+    }
125
+}

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

178
             ->statePath('toggledTableColumns');
178
             ->statePath('toggledTableColumns');
179
     }
179
     }
180
 
180
 
181
+    protected function hasToggleableColumns(): bool
182
+    {
183
+        return ! empty($this->getTableColumnToggleFormSchema());
184
+    }
185
+
181
     /**
186
     /**
182
      * @return array<Checkbox>
187
      * @return array<Checkbox>
183
      */
188
      */

+ 3
- 77
app/Listeners/ConfigureChartOfAccounts.php View File

2
 
2
 
3
 namespace App\Listeners;
3
 namespace App\Listeners;
4
 
4
 
5
-use App\Enums\Accounting\AccountType;
6
-use App\Enums\Banking\BankAccountType;
7
 use App\Events\CompanyGenerated;
5
 use App\Events\CompanyGenerated;
8
-use App\Models\Accounting\Account;
9
-use App\Models\Accounting\AccountSubtype;
10
-use App\Models\Banking\BankAccount;
11
-use App\Models\Company;
12
-use App\Utilities\Currency\CurrencyAccessor;
6
+use App\Services\ChartOfAccountsService;
13
 
7
 
14
 class ConfigureChartOfAccounts
8
 class ConfigureChartOfAccounts
15
 {
9
 {
28
     {
22
     {
29
         $company = $event->company;
23
         $company = $event->company;
30
 
24
 
31
-        $this->createChartOfAccounts($company);
32
-    }
33
-
34
-    public function createChartOfAccounts(Company $company): void
35
-    {
36
-        $chartOfAccounts = config('chart-of-accounts.default');
37
-
38
-        foreach ($chartOfAccounts as $type => $subtypes) {
39
-            foreach ($subtypes as $subtypeName => $subtypeConfig) {
40
-                $subtype = AccountSubtype::create([
41
-                    'company_id' => $company->id,
42
-                    'multi_currency' => $subtypeConfig['multi_currency'] ?? false,
43
-                    'category' => AccountType::from($type)->getCategory()->value,
44
-                    'type' => $type,
45
-                    'name' => $subtypeName,
46
-                    'description' => $subtypeConfig['description'] ?? 'No description available.',
47
-                ]);
48
-
49
-                $this->createDefaultAccounts($company, $subtype, $subtypeConfig);
50
-            }
51
-        }
52
-    }
53
-
54
-    private function createDefaultAccounts(Company $company, AccountSubtype $subtype, array $subtypeConfig): void
55
-    {
56
-        if (isset($subtypeConfig['accounts']) && is_array($subtypeConfig['accounts'])) {
57
-            $baseCode = $subtypeConfig['base_code'];
58
-
59
-            foreach ($subtypeConfig['accounts'] as $accountName => $accountDetails) {
60
-                $bankAccount = null;
61
-
62
-                if ($subtypeConfig['multi_currency'] && isset($subtypeConfig['bank_account_type'])) {
63
-                    $bankAccount = $this->createBankAccountForMultiCurrency($company, $subtypeConfig['bank_account_type']);
64
-                }
65
-
66
-                $account = Account::create([
67
-                    'company_id' => $company->id,
68
-                    'subtype_id' => $subtype->id,
69
-                    'category' => $subtype->type->getCategory()->value,
70
-                    'type' => $subtype->type->value,
71
-                    'code' => $baseCode++,
72
-                    'name' => $accountName,
73
-                    'currency_code' => CurrencyAccessor::getDefaultCurrency(),
74
-                    'description' => $accountDetails['description'] ?? 'No description available.',
75
-                    'default' => true,
76
-                    'created_by' => $company->owner->id,
77
-                    'updated_by' => $company->owner->id,
78
-                ]);
79
-
80
-                if ($bankAccount) {
81
-                    $account->bankAccount()->associate($bankAccount);
82
-                }
83
-
84
-                $account->save();
85
-            }
86
-        }
87
-    }
88
-
89
-    private function createBankAccountForMultiCurrency(Company $company, string $bankAccountType): BankAccount
90
-    {
91
-        $bankAccountType = BankAccountType::from($bankAccountType) ?? BankAccountType::Other;
25
+        $chartOfAccountsService = new ChartOfAccountsService();
92
 
26
 
93
-        return BankAccount::create([
94
-            'company_id' => $company->id,
95
-            'institution_id' => null,
96
-            'type' => $bankAccountType,
97
-            'number' => null,
98
-            'enabled' => BankAccount::where('company_id', $company->id)->where('enabled', true)->doesntExist(),
99
-            'created_by' => $company->owner->id,
100
-            'updated_by' => $company->owner->id,
101
-        ]);
27
+        $chartOfAccountsService->createChartOfAccounts($company);
102
     }
28
     }
103
 }
29
 }

+ 2
- 2
app/Services/AccountService.php View File

38
         return new Money($balances['net_movement'], $account->currency_code);
38
         return new Money($balances['net_movement'], $account->currency_code);
39
     }
39
     }
40
 
40
 
41
-    public function getStartingBalance(Account $account, string $startDate): ?Money
41
+    public function getStartingBalance(Account $account, string $startDate, bool $override = false): ?Money
42
     {
42
     {
43
-        if (in_array($account->category, [AccountCategory::Expense, AccountCategory::Revenue], true)) {
43
+        if ($override === false && in_array($account->category, [AccountCategory::Expense, AccountCategory::Revenue], true)) {
44
             return null;
44
             return null;
45
         }
45
         }
46
 
46
 

+ 84
- 0
app/Services/ChartOfAccountsService.php View File

1
+<?php
2
+
3
+namespace App\Services;
4
+
5
+use App\Enums\Accounting\AccountType;
6
+use App\Enums\Banking\BankAccountType;
7
+use App\Models\Accounting\Account;
8
+use App\Models\Accounting\AccountSubtype;
9
+use App\Models\Banking\BankAccount;
10
+use App\Models\Company;
11
+use App\Utilities\Currency\CurrencyAccessor;
12
+
13
+class ChartOfAccountsService
14
+{
15
+    public function createChartOfAccounts(Company $company): void
16
+    {
17
+        $chartOfAccounts = config('chart-of-accounts.default');
18
+
19
+        foreach ($chartOfAccounts as $type => $subtypes) {
20
+            foreach ($subtypes as $subtypeName => $subtypeConfig) {
21
+                $subtype = AccountSubtype::create([
22
+                    'company_id' => $company->id,
23
+                    'multi_currency' => $subtypeConfig['multi_currency'] ?? false,
24
+                    'category' => AccountType::from($type)->getCategory()->value,
25
+                    'type' => $type,
26
+                    'name' => $subtypeName,
27
+                    'description' => $subtypeConfig['description'] ?? 'No description available.',
28
+                ]);
29
+
30
+                $this->createDefaultAccounts($company, $subtype, $subtypeConfig);
31
+            }
32
+        }
33
+    }
34
+
35
+    private function createDefaultAccounts(Company $company, AccountSubtype $subtype, array $subtypeConfig): void
36
+    {
37
+        if (isset($subtypeConfig['accounts']) && is_array($subtypeConfig['accounts'])) {
38
+            $baseCode = $subtypeConfig['base_code'];
39
+
40
+            foreach ($subtypeConfig['accounts'] as $accountName => $accountDetails) {
41
+                $bankAccount = null;
42
+
43
+                if ($subtypeConfig['multi_currency'] && isset($subtypeConfig['bank_account_type'])) {
44
+                    $bankAccount = $this->createBankAccountForMultiCurrency($company, $subtypeConfig['bank_account_type']);
45
+                }
46
+
47
+                $account = Account::create([
48
+                    'company_id' => $company->id,
49
+                    'subtype_id' => $subtype->id,
50
+                    'category' => $subtype->type->getCategory()->value,
51
+                    'type' => $subtype->type->value,
52
+                    'code' => $baseCode++,
53
+                    'name' => $accountName,
54
+                    'currency_code' => CurrencyAccessor::getDefaultCurrency(),
55
+                    'description' => $accountDetails['description'] ?? 'No description available.',
56
+                    'default' => true,
57
+                    'created_by' => $company->owner->id,
58
+                    'updated_by' => $company->owner->id,
59
+                ]);
60
+
61
+                if ($bankAccount) {
62
+                    $account->bankAccount()->associate($bankAccount);
63
+                }
64
+
65
+                $account->save();
66
+            }
67
+        }
68
+    }
69
+
70
+    private function createBankAccountForMultiCurrency(Company $company, string $bankAccountType): BankAccount
71
+    {
72
+        $bankAccountType = BankAccountType::from($bankAccountType) ?? BankAccountType::Other;
73
+
74
+        return BankAccount::create([
75
+            'company_id' => $company->id,
76
+            'institution_id' => null,
77
+            'type' => $bankAccountType,
78
+            'number' => null,
79
+            'enabled' => BankAccount::where('company_id', $company->id)->where('enabled', true)->doesntExist(),
80
+            'created_by' => $company->owner->id,
81
+            'updated_by' => $company->owner->id,
82
+        ]);
83
+    }
84
+}

+ 19
- 8
app/Services/ExportService.php View File

4
 
4
 
5
 use App\Contracts\ExportableReport;
5
 use App\Contracts\ExportableReport;
6
 use App\Models\Company;
6
 use App\Models\Company;
7
-use Barryvdh\DomPDF\Facade\Pdf;
7
+use Barryvdh\Snappy\Facades\SnappyPdf;
8
 use Illuminate\Support\Carbon;
8
 use Illuminate\Support\Carbon;
9
 use Symfony\Component\HttpFoundation\StreamedResponse;
9
 use Symfony\Component\HttpFoundation\StreamedResponse;
10
 
10
 
11
 class ExportService
11
 class ExportService
12
 {
12
 {
13
-    public function exportToCsv(Company $company, ExportableReport $report, string $startDate, string $endDate): StreamedResponse
13
+    public function exportToCsv(Company $company, ExportableReport $report, string $startDate, string $endDate, bool $separateCategoryHeaders = false): StreamedResponse
14
     {
14
     {
15
         $formattedStartDate = Carbon::parse($startDate)->format('Y-m-d');
15
         $formattedStartDate = Carbon::parse($startDate)->format('Y-m-d');
16
         $formattedEndDate = Carbon::parse($endDate)->format('Y-m-d');
16
         $formattedEndDate = Carbon::parse($endDate)->format('Y-m-d');
35
             fputcsv($file, $report->getHeaders());
35
             fputcsv($file, $report->getHeaders());
36
 
36
 
37
             foreach ($report->getCategories() as $category) {
37
             foreach ($report->getCategories() as $category) {
38
-                fputcsv($file, $category->header);
38
+                if (isset($category->header[0]) && is_array($category->header[0])) {
39
+                    foreach ($category->header as $headerRow) {
40
+                        fputcsv($file, $headerRow);
41
+                    }
42
+                } else {
43
+                    fputcsv($file, $category->header);
44
+                }
39
 
45
 
40
                 foreach ($category->data as $accountRow) {
46
                 foreach ($category->data as $accountRow) {
41
                     fputcsv($file, $accountRow);
47
                     fputcsv($file, $accountRow);
42
                 }
48
                 }
43
 
49
 
44
-                fputcsv($file, $category->summary);
50
+                if (filled($category->summary)) {
51
+                    fputcsv($file, $category->summary);
52
+                }
53
+
45
                 fputcsv($file, []); // Empty row for spacing
54
                 fputcsv($file, []); // Empty row for spacing
46
             }
55
             }
47
 
56
 
48
-            fputcsv($file, $report->getOverallTotals());
57
+            if (filled($report->getOverallTotals())) {
58
+                fputcsv($file, $report->getOverallTotals());
59
+            }
49
 
60
 
50
             fclose($file);
61
             fclose($file);
51
         };
62
         };
62
 
73
 
63
         $filename = $company->name . ' ' . $report->getTitle() . ' ' . $formattedStartDate . ' to ' . $formattedEndDate . ' ' . $timestamp . '.pdf';
74
         $filename = $company->name . ' ' . $report->getTitle() . ' ' . $formattedStartDate . ' to ' . $formattedEndDate . ' ' . $timestamp . '.pdf';
64
 
75
 
65
-        $pdf = Pdf::loadView('components.company.reports.report-pdf', [
76
+        $pdf = SnappyPdf::loadView($report->getPdfView(), [
66
             'company' => $company,
77
             'company' => $company,
67
             'report' => $report,
78
             'report' => $report,
68
             'startDate' => Carbon::parse($startDate)->format('M d, Y'),
79
             'startDate' => Carbon::parse($startDate)->format('M d, Y'),
69
             'endDate' => Carbon::parse($endDate)->format('M d, Y'),
80
             'endDate' => Carbon::parse($endDate)->format('M d, Y'),
70
-        ])->setPaper('letter');
81
+        ]);
71
 
82
 
72
         return response()->streamDownload(function () use ($pdf) {
83
         return response()->streamDownload(function () use ($pdf) {
73
-            echo $pdf->stream();
84
+            echo $pdf->inline();
74
         }, $filename);
85
         }, $filename);
75
     }
86
     }
76
 }
87
 }

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

5
 use App\DTO\AccountBalanceDTO;
5
 use App\DTO\AccountBalanceDTO;
6
 use App\DTO\AccountCategoryDTO;
6
 use App\DTO\AccountCategoryDTO;
7
 use App\DTO\AccountDTO;
7
 use App\DTO\AccountDTO;
8
+use App\DTO\AccountTransactionDTO;
8
 use App\DTO\ReportDTO;
9
 use App\DTO\ReportDTO;
9
 use App\Enums\Accounting\AccountCategory;
10
 use App\Enums\Accounting\AccountCategory;
10
 use App\Models\Accounting\Account;
11
 use App\Models\Accounting\Account;
11
 use App\Support\Column;
12
 use App\Support\Column;
12
 use App\Utilities\Currency\CurrencyAccessor;
13
 use App\Utilities\Currency\CurrencyAccessor;
14
+use Illuminate\Database\Eloquent\Builder;
13
 use Illuminate\Database\Eloquent\Collection;
15
 use Illuminate\Database\Eloquent\Collection;
16
+use Illuminate\Database\Eloquent\Relations\Relation;
14
 
17
 
15
 class ReportService
18
 class ReportService
16
 {
19
 {
90
         }, $balanceFields, $columns);
93
         }, $balanceFields, $columns);
91
     }
94
     }
92
 
95
 
96
+    public function buildAccountTransactionsReport(string $startDate, string $endDate, ?array $columns = null, ?string $accountId = 'all'): ReportDTO
97
+    {
98
+        $columns ??= [];
99
+        $query = Account::whereHas('journalEntries.transaction', function (Builder $query) use ($startDate, $endDate) {
100
+            $query->whereBetween('posted_at', [$startDate, $endDate]);
101
+        })
102
+            ->with(['journalEntries' => function (Relation $query) use ($startDate, $endDate) {
103
+                $query->whereHas('transaction', function (Builder $query) use ($startDate, $endDate) {
104
+                    $query->whereBetween('posted_at', [$startDate, $endDate]);
105
+                })
106
+                    ->with(['transaction' => function (Relation $query) {
107
+                        $query->select(['id', 'description', 'posted_at']);
108
+                    }])
109
+                    ->select(['account_id', 'transaction_id'])
110
+                    ->selectRaw('SUM(CASE WHEN type = "debit" THEN amount ELSE 0 END) AS total_debit')
111
+                    ->selectRaw('SUM(CASE WHEN type = "credit" THEN amount ELSE 0 END) AS total_credit')
112
+                    ->selectRaw('(SELECT MIN(posted_at) FROM transactions WHERE transactions.id = journal_entries.transaction_id) AS earliest_posted_at')
113
+                    ->groupBy('account_id', 'transaction_id')
114
+                    ->orderBy('earliest_posted_at');
115
+            }])
116
+            ->select(['id', 'name', 'category', 'subtype_id', 'currency_code']);
117
+
118
+        if ($accountId !== 'all') {
119
+            $query->where('id', $accountId);
120
+        }
121
+
122
+        $accounts = $query->get();
123
+
124
+        $reportCategories = [];
125
+
126
+        $defaultCurrency = CurrencyAccessor::getDefaultCurrency();
127
+
128
+        foreach ($accounts as $account) {
129
+            $accountTransactions = [];
130
+            $startingBalance = $this->accountService->getStartingBalance($account, $startDate, true);
131
+
132
+            $currentBalance = $startingBalance?->getAmount() ?? 0;
133
+            $totalDebit = 0;
134
+            $totalCredit = 0;
135
+
136
+            $accountTransactions[] = new AccountTransactionDTO(
137
+                date: 'Starting Balance',
138
+                description: '',
139
+                debit: '',
140
+                credit: '',
141
+                balance: $startingBalance?->formatInDefaultCurrency() ?? 0,
142
+            );
143
+
144
+            foreach ($account->journalEntries as $journalEntry) {
145
+                $transaction = $journalEntry->transaction;
146
+                $totalDebit += $journalEntry->total_debit;
147
+                $totalCredit += $journalEntry->total_credit;
148
+
149
+                $currentBalance += $journalEntry->total_debit;
150
+                $currentBalance -= $journalEntry->total_credit;
151
+
152
+                $accountTransactions[] = new AccountTransactionDTO(
153
+                    date: $transaction->posted_at->format('Y-m-d'),
154
+                    description: $transaction->description ?? '',
155
+                    debit: $journalEntry->total_debit ? money($journalEntry->total_debit, $defaultCurrency)->format() : '',
156
+                    credit: $journalEntry->total_credit ? money($journalEntry->total_credit, $defaultCurrency)->format() : '',
157
+                    balance: money($currentBalance, $defaultCurrency)->format(),
158
+                );
159
+            }
160
+
161
+            $balanceChange = $currentBalance - ($startingBalance?->getAmount() ?? 0);
162
+
163
+            $accountTransactions[] = new AccountTransactionDTO(
164
+                date: 'Totals and Ending Balance',
165
+                description: '',
166
+                debit: money($totalDebit, $defaultCurrency)->format(),
167
+                credit: money($totalCredit, $defaultCurrency)->format(),
168
+                balance: money($currentBalance, $defaultCurrency)->format(),
169
+            );
170
+
171
+            $accountTransactions[] = new AccountTransactionDTO(
172
+                date: 'Balance Change',
173
+                description: '',
174
+                debit: '',
175
+                credit: '',
176
+                balance: money($balanceChange, $defaultCurrency)->format(),
177
+            );
178
+
179
+            $reportCategories[] = [
180
+                'category' => $account->name,
181
+                'under' => $account->category->getLabel() . ' > ' . $account->subtype->name,
182
+                'transactions' => $accountTransactions,
183
+            ];
184
+        }
185
+
186
+        return new ReportDTO(categories: $reportCategories, fields: $columns);
187
+    }
188
+
93
     private function buildReport(array $allCategories, Collection $categoryGroupedAccounts, callable $balanceCalculator, array $balanceFields, array $allFields, ?callable $initializeCategoryBalances = null): ReportDTO
189
     private function buildReport(array $allCategories, Collection $categoryGroupedAccounts, callable $balanceCalculator, array $balanceFields, array $allFields, ?callable $initializeCategoryBalances = null): ReportDTO
94
     {
190
     {
95
         $accountCategories = [];
191
         $accountCategories = [];

+ 79
- 0
app/Transformers/AccountTransactionReportTransformer.php View File

1
+<?php
2
+
3
+namespace App\Transformers;
4
+
5
+use App\DTO\AccountTransactionDTO;
6
+use App\DTO\ReportCategoryDTO;
7
+use App\Support\Column;
8
+
9
+class AccountTransactionReportTransformer extends BaseReportTransformer
10
+{
11
+    public function getPdfView(): string
12
+    {
13
+        return 'components.company.reports.account-transactions-report-pdf';
14
+    }
15
+
16
+    public function getTitle(): string
17
+    {
18
+        return 'Account Transactions';
19
+    }
20
+
21
+    public function getHeaders(): array
22
+    {
23
+        return array_map(fn (Column $column) => $column->getLabel(), $this->getColumns());
24
+    }
25
+
26
+    /**
27
+     * @return ReportCategoryDTO[]
28
+     */
29
+    public function getCategories(): array
30
+    {
31
+        $categories = [];
32
+
33
+        foreach ($this->report->categories as $categoryData) {
34
+            // Initialize header with account and category information
35
+
36
+            $header = [
37
+                array_fill(0, count($this->getColumns()), ''),
38
+                array_fill(0, count($this->getColumns()), ''),
39
+            ];
40
+
41
+            foreach ($this->getColumns() as $index => $column) {
42
+                if ($column->getName() === 'date') {
43
+                    $header[0][$index] = $categoryData['category'];
44
+                    $header[1][$index] = $categoryData['under'];
45
+                }
46
+            }
47
+
48
+            // Map transaction data
49
+            $data = array_map(function (AccountTransactionDTO $transaction) {
50
+                $row = [];
51
+
52
+                foreach ($this->getColumns() as $column) {
53
+                    $row[] = match ($column->getName()) {
54
+                        'date' => $transaction->date,
55
+                        'description' => $transaction->description,
56
+                        'debit' => $transaction->debit,
57
+                        'credit' => $transaction->credit,
58
+                        'balance' => $transaction->balance,
59
+                        default => '',
60
+                    };
61
+                }
62
+
63
+                return $row;
64
+            }, $categoryData['transactions']);
65
+
66
+            $categories[] = new ReportCategoryDTO(
67
+                header: $header,
68
+                data: $data,
69
+            );
70
+        }
71
+
72
+        return $categories;
73
+    }
74
+
75
+    public function getOverallTotals(): array
76
+    {
77
+        return [];
78
+    }
79
+}

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

21
         return $this->report->fields;
21
         return $this->report->fields;
22
     }
22
     }
23
 
23
 
24
+    public function getPdfView(): string
25
+    {
26
+        return 'components.company.reports.report-pdf';
27
+    }
28
+
24
     public function getAlignmentClass(int $index): string
29
     public function getAlignmentClass(int $index): string
25
     {
30
     {
26
         $column = $this->getColumns()[$index];
31
         $column = $this->getColumns()[$index];

+ 13
- 3
app/Utilities/Currency/CurrencyAccessor.php View File

5
 use Akaunting\Money\Currency as ISOCurrencies;
5
 use Akaunting\Money\Currency as ISOCurrencies;
6
 use App\Facades\Forex;
6
 use App\Facades\Forex;
7
 use App\Models\Setting\Currency;
7
 use App\Models\Setting\Currency;
8
+use Illuminate\Support\Facades\Cache;
8
 
9
 
9
 class CurrencyAccessor
10
 class CurrencyAccessor
10
 {
11
 {
52
 
53
 
53
     public static function getDefaultCurrency(): ?string
54
     public static function getDefaultCurrency(): ?string
54
     {
55
     {
55
-        return Currency::query()
56
-            ->where('enabled', true)
57
-            ->value('code');
56
+        $companyId = auth()->user()?->currentCompany?->id;
57
+        $cacheKey = "default_currency_{$companyId}";
58
+
59
+        if ($companyId === null) {
60
+            return 'USD';
61
+        }
62
+
63
+        return Cache::rememberForever($cacheKey, function () {
64
+            return Currency::query()
65
+                ->where('enabled', true)
66
+                ->value('code');
67
+        });
58
     }
68
     }
59
 }
69
 }

+ 6
- 1
app/Utilities/Localization/Timezone.php View File

16
             return [];
16
             return [];
17
         }
17
         }
18
 
18
 
19
-        $countryTimezones = DateTimeZone::listIdentifiers(DateTimeZone::PER_COUNTRY, strtoupper($countryCode));
19
+        $countryTimezones = self::getTimezonesForCountry($countryCode);
20
 
20
 
21
         if (empty($countryTimezones)) {
21
         if (empty($countryTimezones)) {
22
             return [];
22
             return [];
55
 
55
 
56
         return now($timezone)->translatedFormat($time_format);
56
         return now($timezone)->translatedFormat($time_format);
57
     }
57
     }
58
+
59
+    public static function getTimezonesForCountry(string $countryCode): array
60
+    {
61
+        return DateTimeZone::listIdentifiers(DateTimeZone::PER_COUNTRY, strtoupper($countryCode));
62
+    }
58
 }
63
 }

+ 5
- 0
app/ValueObjects/Money.php View File

44
         return money($this->getEffectiveAmount(), $this->getCurrencyCode())->format();
44
         return money($this->getEffectiveAmount(), $this->getCurrencyCode())->format();
45
     }
45
     }
46
 
46
 
47
+    public function formatInDefaultCurrency(): string
48
+    {
49
+        return money($this->getEffectiveAmount(), CurrencyAccessor::getDefaultCurrency())->format();
50
+    }
51
+
47
     public function formatSimple(): string
52
     public function formatSimple(): string
48
     {
53
     {
49
         return money($this->getEffectiveAmount(), $this->getCurrencyCode())->formatSimple();
54
         return money($this->getEffectiveAmount(), $this->getCurrencyCode())->formatSimple();

+ 1
- 1
composer.json View File

16
         "andrewdwallo/filament-selectify": "^2.0",
16
         "andrewdwallo/filament-selectify": "^2.0",
17
         "andrewdwallo/transmatic": "^1.1",
17
         "andrewdwallo/transmatic": "^1.1",
18
         "awcodes/filament-table-repeater": "^3.0",
18
         "awcodes/filament-table-repeater": "^3.0",
19
-        "barryvdh/laravel-dompdf": "^2.1",
19
+        "barryvdh/laravel-snappy": "^1.0",
20
         "bezhansalleh/filament-panel-switch": "^1.0",
20
         "bezhansalleh/filament-panel-switch": "^1.0",
21
         "filament/filament": "^3.2.29",
21
         "filament/filament": "^3.2.29",
22
         "guava/filament-clusters": "^1.1",
22
         "guava/filament-clusters": "^1.1",

+ 402
- 474
composer.lock
File diff suppressed because it is too large
View File


+ 0
- 267
config/dompdf.php View File

1
-<?php
2
-
3
-return [
4
-
5
-    /*
6
-    |--------------------------------------------------------------------------
7
-    | Settings
8
-    |--------------------------------------------------------------------------
9
-    |
10
-    | Set some default values. It is possible to add all defines that can be set
11
-    | in dompdf_config.inc.php. You can also override the entire config file.
12
-    |
13
-    */
14
-    'show_warnings' => false,   // Throw an Exception on warnings from dompdf
15
-
16
-    'public_path' => null,  // Override the public path if needed
17
-
18
-    /*
19
-     * Dejavu Sans font is missing glyphs for converted entities, turn it off if you need to show € and £.
20
-     */
21
-    'convert_entities' => true,
22
-
23
-    'options' => [
24
-        /**
25
-         * The location of the DOMPDF font directory
26
-         *
27
-         * The location of the directory where DOMPDF will store fonts and font metrics
28
-         * Note: This directory must exist and be writable by the webserver process.
29
-         * *Please note the trailing slash.*
30
-         *
31
-         * Notes regarding fonts:
32
-         * Additional .afm font metrics can be added by executing load_font.php from command line.
33
-         *
34
-         * Only the original "Base 14 fonts" are present on all pdf viewers. Additional fonts must
35
-         * be embedded in the pdf file or the PDF may not display correctly. This can significantly
36
-         * increase file size unless font subsetting is enabled. Before embedding a font please
37
-         * review your rights under the font license.
38
-         *
39
-         * Any font specification in the source HTML is translated to the closest font available
40
-         * in the font directory.
41
-         *
42
-         * The pdf standard "Base 14 fonts" are:
43
-         * Courier, Courier-Bold, Courier-BoldOblique, Courier-Oblique,
44
-         * Helvetica, Helvetica-Bold, Helvetica-BoldOblique, Helvetica-Oblique,
45
-         * Times-Roman, Times-Bold, Times-BoldItalic, Times-Italic,
46
-         * Symbol, ZapfDingbats.
47
-         */
48
-        'font_dir' => storage_path('fonts'), // advised by dompdf (https://github.com/dompdf/dompdf/pull/782)
49
-
50
-        /**
51
-         * The location of the DOMPDF font cache directory
52
-         *
53
-         * This directory contains the cached font metrics for the fonts used by DOMPDF.
54
-         * This directory can be the same as DOMPDF_FONT_DIR
55
-         *
56
-         * Note: This directory must exist and be writable by the webserver process.
57
-         */
58
-        'font_cache' => storage_path('fonts'),
59
-
60
-        /**
61
-         * The location of a temporary directory.
62
-         *
63
-         * The directory specified must be writeable by the webserver process.
64
-         * The temporary directory is required to download remote images and when
65
-         * using the PDFLib back end.
66
-         */
67
-        'temp_dir' => sys_get_temp_dir(),
68
-
69
-        /**
70
-         * ==== IMPORTANT ====
71
-         *
72
-         * dompdf's "chroot": Prevents dompdf from accessing system files or other
73
-         * files on the webserver.  All local files opened by dompdf must be in a
74
-         * subdirectory of this directory.  DO NOT set it to '/' since this could
75
-         * allow an attacker to use dompdf to read any files on the server.  This
76
-         * should be an absolute path.
77
-         * This is only checked on command line call by dompdf.php, but not by
78
-         * direct class use like:
79
-         * $dompdf = new DOMPDF();  $dompdf->load_html($htmldata); $dompdf->render(); $pdfdata = $dompdf->output();
80
-         */
81
-        'chroot' => realpath(base_path()),
82
-
83
-        /**
84
-         * Protocol whitelist
85
-         *
86
-         * Protocols and PHP wrappers allowed in URIs, and the validation rules
87
-         * that determine if a resource may be loaded. Full support is not guaranteed
88
-         * for the protocols/wrappers specified
89
-         * by this array.
90
-         *
91
-         * @var array
92
-         */
93
-        'allowed_protocols' => [
94
-            'file://' => ['rules' => []],
95
-            'http://' => ['rules' => []],
96
-            'https://' => ['rules' => []],
97
-        ],
98
-
99
-        'log_output_file' => null,
100
-
101
-        /**
102
-         * Whether to enable font subsetting or not.
103
-         */
104
-        'enable_font_subsetting' => true,
105
-
106
-        /**
107
-         * The PDF rendering backend to use
108
-         *
109
-         * Valid settings are 'PDFLib', 'CPDF' (the bundled R&OS PDF class), 'GD' and
110
-         * 'auto'. 'auto' will look for PDFLib and use it if found, or if not it will
111
-         * fall back on CPDF. 'GD' renders PDFs to graphic files. {@link * Canvas_Factory} ultimately determines which rendering class to instantiate
112
-         * based on this setting.
113
-         *
114
-         * Both PDFLib & CPDF rendering backends provide sufficient rendering
115
-         * capabilities for dompdf, however additional features (e.g. object,
116
-         * image and font support, etc.) differ between backends.  Please see
117
-         * {@link PDFLib_Adapter} for more information on the PDFLib backend
118
-         * and {@link CPDF_Adapter} and lib/class.pdf.php for more information
119
-         * on CPDF. Also see the documentation for each backend at the links
120
-         * below.
121
-         *
122
-         * The GD rendering backend is a little different than PDFLib and
123
-         * CPDF. Several features of CPDF and PDFLib are not supported or do
124
-         * not make any sense when creating image files.  For example,
125
-         * multiple pages are not supported, nor are PDF 'objects'.  Have a
126
-         * look at {@link GD_Adapter} for more information.  GD support is
127
-         * experimental, so use it at your own risk.
128
-         *
129
-         * @link http://www.pdflib.com
130
-         * @link http://www.ros.co.nz/pdf
131
-         * @link http://www.php.net/image
132
-         */
133
-        'pdf_backend' => 'CPDF',
134
-
135
-        /**
136
-         * PDFlib license key
137
-         *
138
-         * If you are using a licensed, commercial version of PDFlib, specify
139
-         * your license key here.  If you are using PDFlib-Lite or are evaluating
140
-         * the commercial version of PDFlib, comment out this setting.
141
-         *
142
-         * @link http://www.pdflib.com
143
-         *
144
-         * If pdflib present in web server and auto or selected explicitly above,
145
-         * a real license code must exist!
146
-         */
147
-        //"DOMPDF_PDFLIB_LICENSE" => "your license key here",
148
-
149
-        /**
150
-         * html target media view which should be rendered into pdf.
151
-         * List of types and parsing rules for future extensions:
152
-         * http://www.w3.org/TR/REC-html40/types.html
153
-         *   screen, tty, tv, projection, handheld, print, braille, aural, all
154
-         * Note: aural is deprecated in CSS 2.1 because it is replaced by speech in CSS 3.
155
-         * Note, even though the generated pdf file is intended for print output,
156
-         * the desired content might be different (e.g. screen or projection view of html file).
157
-         * Therefore allow specification of content here.
158
-         */
159
-        'default_media_type' => 'screen',
160
-
161
-        /**
162
-         * The default paper size.
163
-         *
164
-         * North America standard is "letter"; other countries generally "a4"
165
-         *
166
-         * @see CPDF_Adapter::PAPER_SIZES for valid sizes ('letter', 'legal', 'A4', etc.)
167
-         */
168
-        'default_paper_size' => 'a4',
169
-
170
-        /**
171
-         * The default paper orientation.
172
-         *
173
-         * The orientation of the page (portrait or landscape).
174
-         */
175
-        'default_paper_orientation' => 'portrait',
176
-
177
-        /**
178
-         * The default font family
179
-         *
180
-         * Used if no suitable fonts can be found. This must exist in the font folder.
181
-         */
182
-        'default_font' => 'sans-serif',
183
-
184
-        /**
185
-         * Image DPI setting
186
-         *
187
-         * This setting determines the default DPI setting for images and fonts.  The
188
-         * DPI may be overridden for inline images by explicitly setting the
189
-         * image's width & height style attributes (i.e. if the image's native
190
-         * width is 600 pixels and you specify the image's width as 72 points,
191
-         * the image will have a DPI of 600 in the rendered PDF.  The DPI of
192
-         * background images can not be overridden and is controlled entirely
193
-         * via this parameter.
194
-         *
195
-         * For the purposes of DOMPDF, pixels per inch (PPI) = dots per inch (DPI).
196
-         * If a size in html is given as px (or without unit as image size),
197
-         * this tells the corresponding size in pt.
198
-         * This adjusts the relative sizes to be similar to the rendering of the
199
-         * html page in a reference browser.
200
-         *
201
-         * In pdf, always 1 pt = 1/72 inch
202
-         *
203
-         * Rendering resolution of various browsers in px per inch:
204
-         * Windows Firefox and Internet Explorer:
205
-         *   SystemControl->Display properties->FontResolution: Default:96, largefonts:120, custom:?
206
-         * Linux Firefox:
207
-         *   about:config *resolution: Default:96
208
-         *   (xorg screen dimension in mm and Desktop font dpi settings are ignored)
209
-         *
210
-         * Take care about extra font/image zoom factor of browser.
211
-         *
212
-         * In images, <img> size in pixel attribute, img css style, are overriding
213
-         * the real image dimension in px for rendering.
214
-         */
215
-        'dpi' => 96,
216
-
217
-        /**
218
-         * Enable inline PHP
219
-         *
220
-         * If this setting is set to true then DOMPDF will automatically evaluate
221
-         * inline PHP contained within <script type="text/php"> ... </script> tags.
222
-         *
223
-         * Enabling this for documents you do not trust (e.g. arbitrary remote html
224
-         * pages) is a security risk.  Set this option to false if you wish to process
225
-         * untrusted documents.
226
-         */
227
-        'enable_php' => true,
228
-
229
-        /**
230
-         * Enable inline Javascript
231
-         *
232
-         * If this setting is set to true then DOMPDF will automatically insert
233
-         * JavaScript code contained within <script type="text/javascript"> ... </script> tags.
234
-         */
235
-        'enable_javascript' => true,
236
-
237
-        /**
238
-         * Enable remote file access
239
-         *
240
-         * If this setting is set to true, DOMPDF will access remote sites for
241
-         * images and CSS files as required.
242
-         * This is required for part of test case www/test/image_variants.html through www/examples.php
243
-         *
244
-         * Attention!
245
-         * This can be a security risk, in particular in combination with DOMPDF_ENABLE_PHP and
246
-         * allowing remote access to dompdf.php or on allowing remote html code to be passed to
247
-         * $dompdf = new DOMPDF(, $dompdf->load_html(...,
248
-         * This allows anonymous users to download legally doubtful internet content which on
249
-         * tracing back appears to being downloaded by your server, or allows malicious php code
250
-         * in remote html pages to be executed by your server with your account privileges.
251
-         */
252
-        'enable_remote' => true,
253
-
254
-        /**
255
-         * A ratio applied to the fonts height to be more like browsers' line height
256
-         */
257
-        'font_height_ratio' => 1.1,
258
-
259
-        /**
260
-         * Use the HTML5 Lib parser
261
-         *
262
-         * @deprecated This feature is now always on in dompdf 2.x
263
-         */
264
-        'enable_html5_parser' => true,
265
-    ],
266
-
267
-];

+ 63
- 0
config/snappy.php View File

1
+<?php
2
+
3
+return [
4
+
5
+    /*
6
+    |--------------------------------------------------------------------------
7
+    | Snappy PDF / Image Configuration
8
+    |--------------------------------------------------------------------------
9
+    |
10
+    | This option contains settings for PDF generation.
11
+    |
12
+    | Enabled:
13
+    |
14
+    |    Whether to load PDF / Image generation.
15
+    |
16
+    | Binary:
17
+    |
18
+    |    The file path of the wkhtmltopdf / wkhtmltoimage executable.
19
+    |
20
+    | Timeout:
21
+    |
22
+    |    The amount of time to wait (in seconds) before PDF / Image generation is stopped.
23
+    |    Setting this to false disables the timeout (unlimited processing time).
24
+    |
25
+    | Options:
26
+    |
27
+    |    The wkhtmltopdf command options. These are passed directly to wkhtmltopdf.
28
+    |    See https://wkhtmltopdf.org/usage/wkhtmltopdf.txt for all options.
29
+    |
30
+    | Env:
31
+    |
32
+    |    The environment variables to set while running the wkhtmltopdf process.
33
+    |
34
+    */
35
+
36
+    'pdf' => [
37
+        'enabled' => true,
38
+        'binary' => env('WKHTMLTOPDF_BINARY', '/usr/local/bin/wkhtmltopdf'),
39
+        'timeout' => false,
40
+        'options' => [
41
+            'no-pdf-compression' => true,
42
+            'disable-javascript' => true,
43
+            'margin-top' => '10mm',
44
+            'margin-right' => '7.5mm',
45
+            'margin-bottom' => '15mm',
46
+            'margin-left' => '7.5mm',
47
+            'page-size' => 'Letter',
48
+            'footer-right' => 'Page [page] / [toPage]',
49
+            'footer-font-size' => '8',
50
+            'footer-spacing' => '5',
51
+        ],
52
+        'env' => [],
53
+    ],
54
+
55
+    'image' => [
56
+        'enabled' => true,
57
+        'binary' => env('WKHTMLTOIMAGE_BINARY', '/usr/local/bin/wkhtmltoimage'),
58
+        'timeout' => false,
59
+        'options' => [],
60
+        'env' => [],
61
+    ],
62
+
63
+];

+ 37
- 1
database/factories/Accounting/TransactionFactory.php View File

2
 
2
 
3
 namespace Database\Factories\Accounting;
3
 namespace Database\Factories\Accounting;
4
 
4
 
5
+use App\Models\Accounting\Account;
5
 use App\Models\Accounting\Transaction;
6
 use App\Models\Accounting\Transaction;
7
+use App\Models\Banking\BankAccount;
8
+use App\Models\Company;
6
 use Illuminate\Database\Eloquent\Factories\Factory;
9
 use Illuminate\Database\Eloquent\Factories\Factory;
7
 
10
 
8
 /**
11
 /**
10
  */
13
  */
11
 class TransactionFactory extends Factory
14
 class TransactionFactory extends Factory
12
 {
15
 {
16
+    /**
17
+     * The name of the factory's corresponding model.
18
+     */
19
+    protected $model = Transaction::class;
20
+
13
     /**
21
     /**
14
      * Define the model's default state.
22
      * Define the model's default state.
15
      *
23
      *
19
     {
27
     {
20
         return [
28
         return [
21
             'company_id' => 1,
29
             'company_id' => 1,
22
-            'account_id' => $this->faker->numberBetween(1, 30),
23
             'bank_account_id' => 1,
30
             'bank_account_id' => 1,
31
+            'account_id' => $this->faker->numberBetween(2, 30),
24
             'type' => $this->faker->randomElement(['deposit', 'withdrawal', 'journal']),
32
             'type' => $this->faker->randomElement(['deposit', 'withdrawal', 'journal']),
25
             'payment_channel' => $this->faker->randomElement(['online', 'in store', 'other']),
33
             'payment_channel' => $this->faker->randomElement(['online', 'in store', 'other']),
26
             'description' => $this->faker->sentence,
34
             'description' => $this->faker->sentence,
69
             ]);
77
             ]);
70
         });
78
         });
71
     }
79
     }
80
+
81
+    public function forCompanyAndBankAccount(Company $company, BankAccount $bankAccount): static
82
+    {
83
+        return $this->state(function (array $attributes) use ($bankAccount, $company) {
84
+            $type = $this->faker->randomElement(['deposit', 'withdrawal', 'journal']);
85
+
86
+            $associatedAccountTypes = match ($type) {
87
+                'deposit' => ['asset', 'liability', 'equity', 'revenue'],
88
+                'withdrawal' => ['asset', 'liability', 'equity', 'expense'],
89
+                default => ['asset', 'liability', 'equity', 'revenue', 'expense'],
90
+            };
91
+
92
+            $accountIdForBankAccount = $bankAccount->account->id;
93
+
94
+            $account = Account::where('category', $this->faker->randomElement($associatedAccountTypes))
95
+                ->where('company_id', $company->id)
96
+                ->where('id', '<>', $accountIdForBankAccount)
97
+                ->inRandomOrder()
98
+                ->firstOrFail();
99
+
100
+            return [
101
+                'company_id' => $company->id,
102
+                'bank_account_id' => $bankAccount->id,
103
+                'account_id' => $account->id,
104
+                'type' => $type,
105
+            ];
106
+        });
107
+    }
72
 }
108
 }

+ 29
- 22
database/factories/UserFactory.php View File

2
 
2
 
3
 namespace Database\Factories;
3
 namespace Database\Factories;
4
 
4
 
5
-use App\Events\CompanyGenerated;
6
 use App\Models\Company;
5
 use App\Models\Company;
7
 use App\Models\Setting\CompanyProfile;
6
 use App\Models\Setting\CompanyProfile;
8
 use App\Models\User;
7
 use App\Models\User;
8
+use App\Services\ChartOfAccountsService;
9
+use App\Services\CompanyDefaultService;
9
 use Database\Factories\Accounting\TransactionFactory;
10
 use Database\Factories\Accounting\TransactionFactory;
10
 use Illuminate\Database\Eloquent\Factories\Factory;
11
 use Illuminate\Database\Eloquent\Factories\Factory;
12
+use Illuminate\Support\Facades\DB;
11
 use Illuminate\Support\Facades\Hash;
13
 use Illuminate\Support\Facades\Hash;
12
 use Illuminate\Support\Str;
14
 use Illuminate\Support\Str;
13
 use Wallo\FilamentCompanies\FilamentCompanies;
15
 use Wallo\FilamentCompanies\FilamentCompanies;
69
             Company::factory()
71
             Company::factory()
70
                 ->has(CompanyProfile::factory()->withCountry($countryCode), 'profile')
72
                 ->has(CompanyProfile::factory()->withCountry($countryCode), 'profile')
71
                 ->afterCreating(function (Company $company) use ($user, $countryCode) {
73
                 ->afterCreating(function (Company $company) use ($user, $countryCode) {
72
-                    CompanyGenerated::dispatch($user, $company, $countryCode);
74
+                    DB::transaction(function () use ($company, $user, $countryCode) {
75
+                        $companyDefaultService = app()->make(CompanyDefaultService::class);
76
+                        $companyDefaultService->createCompanyDefaults($company, $user, 'USD', $countryCode, 'en');
73
 
77
 
74
-                    $defaultBankAccount = $company->bankAccounts()->where('enabled', true)->first();
75
-                    $defaultCurrency = $company->currencies()->where('enabled', true)->first();
76
-                    $defaultSalesTax = $company->taxes()->where('type', 'sales')->where('enabled', true)->first();
77
-                    $defaultPurchaseTax = $company->taxes()->where('type', 'purchase')->where('enabled', true)->first();
78
-                    $defaultSalesDiscount = $company->discounts()->where('type', 'sales')->where('enabled', true)->first();
79
-                    $defaultPurchaseDiscount = $company->discounts()->where('type', 'purchase')->where('enabled', true)->first();
78
+                        $chartOfAccountsService = app()->make(ChartOfAccountsService::class);
79
+                        $chartOfAccountsService->createChartOfAccounts($company);
80
 
80
 
81
-                    $company->default()->create([
82
-                        'bank_account_id' => $defaultBankAccount?->id,
83
-                        'currency_code' => $defaultCurrency?->code,
84
-                        'sales_tax_id' => $defaultSalesTax?->id,
85
-                        'purchase_tax_id' => $defaultPurchaseTax?->id,
86
-                        'sales_discount_id' => $defaultSalesDiscount?->id,
87
-                        'purchase_discount_id' => $defaultPurchaseDiscount?->id,
88
-                        'created_by' => $user->id,
89
-                        'updated_by' => $user->id,
90
-                    ]);
81
+                        $defaultBankAccount = $company->bankAccounts()->where('enabled', true)->first();
82
+                        $defaultCurrency = $company->currencies()->where('enabled', true)->first();
83
+                        $defaultSalesTax = $company->taxes()->where('type', 'sales')->where('enabled', true)->first();
84
+                        $defaultPurchaseTax = $company->taxes()->where('type', 'purchase')->where('enabled', true)->first();
85
+                        $defaultSalesDiscount = $company->discounts()->where('type', 'sales')->where('enabled', true)->first();
86
+                        $defaultPurchaseDiscount = $company->discounts()->where('type', 'purchase')->where('enabled', true)->first();
91
 
87
 
92
-                    TransactionFactory::new()
93
-                        ->count(2000)
94
-                        ->createQuietly([
95
-                            'company_id' => $company->id,
88
+                        $company->default()->create([
96
                             'bank_account_id' => $defaultBankAccount?->id,
89
                             'bank_account_id' => $defaultBankAccount?->id,
90
+                            'currency_code' => $defaultCurrency?->code,
91
+                            'sales_tax_id' => $defaultSalesTax?->id,
92
+                            'purchase_tax_id' => $defaultPurchaseTax?->id,
93
+                            'sales_discount_id' => $defaultSalesDiscount?->id,
94
+                            'purchase_discount_id' => $defaultPurchaseDiscount?->id,
97
                             'created_by' => $user->id,
95
                             'created_by' => $user->id,
98
                             'updated_by' => $user->id,
96
                             'updated_by' => $user->id,
99
                         ]);
97
                         ]);
98
+
99
+                        TransactionFactory::new()
100
+                            ->forCompanyAndBankAccount($company, $defaultBankAccount)
101
+                            ->count(2000)
102
+                            ->createQuietly([
103
+                                'created_by' => $user->id,
104
+                                'updated_by' => $user->id,
105
+                            ]);
106
+                    });
100
                 })
107
                 })
101
                 ->create([
108
                 ->create([
102
                     'name' => $user->name . '\'s Company',
109
                     'name' => $user->name . '\'s Company',

+ 23
- 23
package-lock.json View File

931
             }
931
             }
932
         },
932
         },
933
         "node_modules/caniuse-lite": {
933
         "node_modules/caniuse-lite": {
934
-            "version": "1.0.30001636",
935
-            "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001636.tgz",
936
-            "integrity": "sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg==",
934
+            "version": "1.0.30001640",
935
+            "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001640.tgz",
936
+            "integrity": "sha512-lA4VMpW0PSUrFnkmVuEKBUovSWKhj7puyCg8StBChgu298N1AtuF1sKWEvfDuimSEDbhlb/KqPKC3fs1HbuQUA==",
937
             "dev": true,
937
             "dev": true,
938
             "funding": [
938
             "funding": [
939
                 {
939
                 {
1079
             "dev": true
1079
             "dev": true
1080
         },
1080
         },
1081
         "node_modules/electron-to-chromium": {
1081
         "node_modules/electron-to-chromium": {
1082
-            "version": "1.4.807",
1083
-            "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.807.tgz",
1084
-            "integrity": "sha512-kSmJl2ZwhNf/bcIuCH/imtNOKlpkLDn2jqT5FJ+/0CXjhnFaOa9cOe9gHKKy71eM49izwuQjZhKk+lWQ1JxB7A==",
1082
+            "version": "1.4.817",
1083
+            "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.817.tgz",
1084
+            "integrity": "sha512-3znu+lZMIbTe8ZOs360OMJvVroVF2NpNI8T5jfLnDetVvj0uNmIucZzQVYMSJfsu9f47Ssox1Gt46PR+R+1JUg==",
1085
             "dev": true
1085
             "dev": true
1086
         },
1086
         },
1087
         "node_modules/emoji-regex": {
1087
         "node_modules/emoji-regex": {
1471
             "dev": true
1471
             "dev": true
1472
         },
1472
         },
1473
         "node_modules/lru-cache": {
1473
         "node_modules/lru-cache": {
1474
-            "version": "10.2.2",
1475
-            "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz",
1476
-            "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==",
1474
+            "version": "10.3.0",
1475
+            "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.3.0.tgz",
1476
+            "integrity": "sha512-CQl19J/g+Hbjbv4Y3mFNNXFEL/5t/KCg8POCuUqd4rMKjGG+j1ybER83hxV58zL+dFI1PTkt3GNFSHRt+d8qEQ==",
1477
             "dev": true,
1477
             "dev": true,
1478
             "engines": {
1478
             "engines": {
1479
                 "node": "14 || >=16.14"
1479
                 "node": "14 || >=16.14"
1532
             }
1532
             }
1533
         },
1533
         },
1534
         "node_modules/minimatch": {
1534
         "node_modules/minimatch": {
1535
-            "version": "9.0.4",
1536
-            "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
1537
-            "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
1535
+            "version": "9.0.5",
1536
+            "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
1537
+            "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
1538
             "dev": true,
1538
             "dev": true,
1539
             "dependencies": {
1539
             "dependencies": {
1540
                 "brace-expansion": "^2.0.1"
1540
                 "brace-expansion": "^2.0.1"
1700
             }
1700
             }
1701
         },
1701
         },
1702
         "node_modules/postcss": {
1702
         "node_modules/postcss": {
1703
-            "version": "8.4.38",
1704
-            "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
1705
-            "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
1703
+            "version": "8.4.39",
1704
+            "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz",
1705
+            "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==",
1706
             "dev": true,
1706
             "dev": true,
1707
             "funding": [
1707
             "funding": [
1708
                 {
1708
                 {
1720
             ],
1720
             ],
1721
             "dependencies": {
1721
             "dependencies": {
1722
                 "nanoid": "^3.3.7",
1722
                 "nanoid": "^3.3.7",
1723
-                "picocolors": "^1.0.0",
1723
+                "picocolors": "^1.0.1",
1724
                 "source-map-js": "^1.2.0"
1724
                 "source-map-js": "^1.2.0"
1725
             },
1725
             },
1726
             "engines": {
1726
             "engines": {
2339
             "dev": true
2339
             "dev": true
2340
         },
2340
         },
2341
         "node_modules/update-browserslist-db": {
2341
         "node_modules/update-browserslist-db": {
2342
-            "version": "1.0.16",
2343
-            "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz",
2344
-            "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==",
2342
+            "version": "1.1.0",
2343
+            "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz",
2344
+            "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==",
2345
             "dev": true,
2345
             "dev": true,
2346
             "funding": [
2346
             "funding": [
2347
                 {
2347
                 {
2375
             "dev": true
2375
             "dev": true
2376
         },
2376
         },
2377
         "node_modules/vite": {
2377
         "node_modules/vite": {
2378
-            "version": "5.3.1",
2379
-            "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.1.tgz",
2380
-            "integrity": "sha512-XBmSKRLXLxiaPYamLv3/hnP/KXDai1NDexN0FpkTaZXTfycHvkRHoenpgl/fvuK/kPbB6xAgoyiryAhQNxYmAQ==",
2378
+            "version": "5.3.3",
2379
+            "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.3.tgz",
2380
+            "integrity": "sha512-NPQdeCU0Dv2z5fu+ULotpuq5yfCS1BzKUIPhNbP3YBfAMGJXbt2nS+sbTFu+qchaqWTD+H3JK++nRwr6XIcp6A==",
2381
             "dev": true,
2381
             "dev": true,
2382
             "dependencies": {
2382
             "dependencies": {
2383
                 "esbuild": "^0.21.3",
2383
                 "esbuild": "^0.21.3",
2384
-                "postcss": "^8.4.38",
2384
+                "postcss": "^8.4.39",
2385
                 "rollup": "^4.13.0"
2385
                 "rollup": "^4.13.0"
2386
             },
2386
             },
2387
             "bin": {
2387
             "bin": {

+ 11
- 3
resources/css/filament/company/theme.css View File

18
 }
18
 }
19
 
19
 
20
 .choices[data-type="select-one"] .choices__inner {
20
 .choices[data-type="select-one"] .choices__inner {
21
-    height: 2.25rem;
21
+    line-height: 1.5;
22
+    display: flex;
23
+    align-items: center;
24
+    min-height: 2.25rem;
25
+    box-sizing: border-box;
26
+}
27
+
28
+.choices__item {
29
+    cursor: pointer;
22
 }
30
 }
23
 
31
 
24
 .table-repeater-container {
32
 .table-repeater-container {
135
 
143
 
136
 .fi-body::before {
144
 .fi-body::before {
137
     content: '';
145
     content: '';
138
-    position: absolute;
146
+    position: fixed;
139
     top: 0;
147
     top: 0;
140
     left: 0;
148
     left: 0;
141
     width: 100%;
149
     width: 100%;
159
 
167
 
160
 :is(.dark .fi-body)::before {
168
 :is(.dark .fi-body)::before {
161
     content: '';
169
     content: '';
162
-    position: absolute;
170
+    position: fixed;
163
     top: 0;
171
     top: 0;
164
     right: 0;
172
     right: 0;
165
     background-image: radial-gradient(
173
     background-image: radial-gradient(

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

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

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

5
     <meta name="viewport" content="width=device-width, initial-scale=1">
5
     <meta name="viewport" content="width=device-width, initial-scale=1">
6
     <title>{{ $report->getTitle() }}</title>
6
     <title>{{ $report->getTitle() }}</title>
7
     <style>
7
     <style>
8
-        @page {
9
-            size: auto;
10
-            margin: 10mm 7.5mm;
11
-        }
12
-
13
         .header {
8
         .header {
14
             color: #374151;
9
             color: #374151;
15
             margin-bottom: 1rem;
10
             margin-bottom: 1rem;

+ 68
- 0
resources/views/components/company/tables/reports/account-transactions.blade.php View File

1
+<table class="w-full table-auto divide-y divide-gray-200 dark:divide-white/5">
2
+    <thead class="divide-y divide-gray-200 dark:divide-white/5">
3
+        <tr class="bg-gray-50 dark:bg-white/5">
4
+            @foreach($report->getHeaders() as $index => $header)
5
+                <th wire:key="header-{{ $index }}" class="px-3 py-3.5 sm:first-of-type:ps-6 sm:last-of-type:pe-6 {{ $report->getAlignmentClass($index) }}">
6
+                    <span class="text-sm font-semibold text-gray-950 dark:text-white">
7
+                        {{ $header }}
8
+                    </span>
9
+                </th>
10
+            @endforeach
11
+        </tr>
12
+    </thead>
13
+    @foreach($report->getCategories() as $categoryIndex => $category)
14
+        <tbody wire:key="category-{{ $categoryIndex }}" class="divide-y divide-gray-200 whitespace-nowrap dark:divide-white/5">
15
+            <!-- Category Header -->
16
+            <tr class="bg-gray-50 dark:bg-white/5">
17
+                <x-filament-tables::cell colspan="{{ count($report->getHeaders()) }}" class="text-left">
18
+                    <div class="px-3 py-2">
19
+                        @foreach ($category->header as $headerRow)
20
+                            <div class="text-sm {{ $loop->first ? 'font-semibold text-gray-950 dark:text-white' : 'text-gray-500 dark:text-white/50' }}">
21
+                                @foreach ($headerRow as $headerValue)
22
+                                    @if (!empty($headerValue))
23
+                                        {{ $headerValue }}
24
+                                    @endif
25
+                                @endforeach
26
+                            </div>
27
+                        @endforeach
28
+                    </div>
29
+                </x-filament-tables::cell>
30
+            </tr>
31
+            <!-- Transactions Data -->
32
+            @foreach($category->data as $dataIndex => $transaction)
33
+                <tr wire:key="category-{{ $categoryIndex }}-data-{{ $dataIndex }}"
34
+                    @class([
35
+                        'bg-gray-50 dark:bg-white/5' => $loop->first || $loop->last || $loop->remaining === 1,
36
+                    ])
37
+                >
38
+                    @foreach($transaction as $cellIndex => $cell)
39
+                        <x-filament-tables::cell
40
+                            wire:key="category-{{ $categoryIndex }}-data-{{ $dataIndex }}-cell-{{ $cellIndex }}"
41
+                             @class([
42
+                                $report->getAlignmentClass($cellIndex),
43
+                                'whitespace-normal' => $cellIndex === 1,
44
+                            ])
45
+                        >
46
+                            <div
47
+                                @class([
48
+                                    'px-3 py-4 text-sm leading-6 text-gray-950 dark:text-white',
49
+                                    'font-semibold' => $loop->parent->first || $loop->parent->last || $loop->parent->remaining === 1,
50
+                                ])
51
+                            >
52
+                                {{ $cell }}
53
+                            </div>
54
+                        </x-filament-tables::cell>
55
+                    @endforeach
56
+                </tr>
57
+            @endforeach
58
+            <!-- Spacer Row -->
59
+            @unless($loop->last)
60
+                <tr wire:key="category-{{ $categoryIndex }}-spacer">
61
+                    <x-filament-tables::cell colspan="{{ count($report->getHeaders()) }}">
62
+                        <div class="px-3 py-2 leading-6 invisible">Hidden Text</div>
63
+                    </x-filament-tables::cell>
64
+                </tr>
65
+            @endunless
66
+        </tbody>
67
+    @endforeach
68
+</table>

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

1
+<x-filament-panels::page>
2
+    <x-filament-tables::container>
3
+        <form wire:submit="loadReportData" class="p-6">
4
+            {{ $this->form }}
5
+        </form>
6
+        <div class="divide-y divide-gray-200 overflow-x-auto overflow-y-hidden dark:divide-white/10 dark:border-t-white/10">
7
+            <div wire:init="loadReportData" class="flex items-center justify-center">
8
+                <div wire:loading.delay wire:target="loadReportData">
9
+                    <x-filament::loading-indicator class="p-6 text-primary-700 dark:text-primary-300" />
10
+                </div>
11
+            </div>
12
+
13
+            <div wire:loading.remove wire:target="loadReportData">
14
+                @if($this->report)
15
+                    <x-company.tables.reports.account-transactions :report="$this->report" />
16
+                @endif
17
+            </div>
18
+        </div>
19
+        <div class="es-table__footer-ctn border-t border-gray-200"></div>
20
+    </x-filament-tables::container>
21
+</x-filament-panels::page>

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

1
 <x-filament-panels::page>
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 }}
2
+    <x-filament-tables::container>
3
+        <div class="p-6 divide-y divide-gray-200 dark:divide-white/5">
4
+            <form wire:submit="loadReportData">
5
+                <div class="flex flex-col md:flex-row items-start md:items-center justify-center gap-4 md:gap-12">
6
+                    {{ $this->form }}
7
+                    @if($this->hasToggleableColumns())
8
                         <x-filament-tables::column-toggle.dropdown
8
                         <x-filament-tables::column-toggle.dropdown
9
-                            class="my-auto"
10
                             :form="$this->toggleTableColumnForm"
9
                             :form="$this->toggleTableColumnForm"
11
                             :trigger-action="$this->toggleColumnsAction"
10
                             :trigger-action="$this->toggleColumnsAction"
12
                         />
11
                         />
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="divide-y divide-gray-200 overflow-x-auto overflow-y-hidden dark:divide-white/10 dark:border-t-white/10">
20
-                <div wire:init="loadReportData" class="flex items-center justify-center">
21
-                    <div wire:loading.delay wire:target="loadReportData">
22
-                        <x-filament::loading-indicator class="p-6 text-primary-700 dark:text-primary-300" />
23
-                    </div>
24
-                </div>
25
-
26
-                <div wire:loading.remove wire:target="loadReportData">
27
-                    @if($this->report)
28
-                        <x-company.tables.reports.detailed-report :report="$this->report" />
29
                     @endif
12
                     @endif
13
+                    <x-filament::button type="submit" wire:target="loadReportData" class="flex-shrink-0">
14
+                        Update Report
15
+                    </x-filament::button>
30
                 </div>
16
                 </div>
17
+            </form>
18
+        </div>
19
+        <div class="divide-y divide-gray-200 overflow-x-auto overflow-y-hidden dark:divide-white/10 dark:border-t-white/10">
20
+            <div wire:init="loadReportData" class="flex items-center justify-center">
21
+                <div wire:loading.delay wire:target="loadReportData">
22
+                    <x-filament::loading-indicator class="p-6 text-primary-700 dark:text-primary-300" />
23
+                </div>
24
+            </div>
25
+
26
+            <div wire:loading.remove wire:target="loadReportData">
27
+                @if($this->report)
28
+                    <x-company.tables.reports.detailed-report :report="$this->report" />
29
+                @endif
31
             </div>
30
             </div>
32
-            <div class="es-table__footer-ctn border-t border-gray-200"></div>
33
-        </x-filament-tables::container>
34
-    </div>
31
+        </div>
32
+        <div class="es-table__footer-ctn border-t border-gray-200"></div>
33
+    </x-filament-tables::container>
35
 </x-filament-panels::page>
34
 </x-filament-panels::page>

+ 0
- 0
resources/views/vendor/filament-clusters/.gitkeep View File


+ 24
- 0
resources/views/vendor/filament-clusters/cluster.blade.php View File

1
+<x-dynamic-component
2
+    :component="$getFieldWrapperView()"
3
+    :field="$field"
4
+>
5
+    <x-filament::input.wrapper
6
+        @class([
7
+            "guava-fi-cl-cluster",
8
+            ...$field->getResponsiveClasses(),
9
+        ])
10
+    >
11
+        {{ $getChildComponentContainer() }}
12
+    </x-filament::input.wrapper>
13
+
14
+
15
+    @foreach($field->getChildComponents() as $child)
16
+        @if ($childStatePath = $child->getStatePath())
17
+            @if($errors->has($childStatePath) )
18
+                <x-filament-forms::field-wrapper.error-message>
19
+                    {{ $errors->first($childStatePath) }}
20
+                </x-filament-forms::field-wrapper.error-message>
21
+            @endif
22
+        @endif
23
+    @endforeach
24
+</x-dynamic-component>

+ 3
- 0
resources/views/vendor/filament-clusters/components/field-wrapper.blade.php View File

1
+<div>
2
+    {{ $slot }}
3
+</div>

Loading…
Cancel
Save