Переглянути джерело

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

Development 3.x
3.x
Andrew Wallo 11 місяці тому
джерело
коміт
61adbe2fc5
Аккаунт користувача з таким Email не знайдено
55 змінених файлів з 1654 додано та 603 видалено
  1. 6
    0
      app/Contracts/ExportableReport.php
  2. 23
    0
      app/Contracts/HasSummaryReport.php
  3. 5
    3
      app/DTO/AccountCategoryDTO.php
  4. 14
    0
      app/DTO/AccountTypeDTO.php
  5. 9
    3
      app/DTO/ReportCategoryDTO.php
  6. 12
    0
      app/DTO/ReportTypeDTO.php
  7. 22
    0
      app/Enums/Accounting/AccountType.php
  8. 3
    0
      app/Filament/Company/Clusters/Settings/Resources/CurrencyResource.php
  9. 1
    6
      app/Filament/Company/Pages/Accounting/AccountChart.php
  10. 1
    1
      app/Filament/Company/Pages/Accounting/Transactions.php
  11. 2
    1
      app/Filament/Company/Pages/Reports.php
  12. 1
    1
      app/Filament/Company/Pages/Reports/AccountTransactions.php
  13. 87
    0
      app/Filament/Company/Pages/Reports/BalanceSheet.php
  14. 4
    0
      app/Filament/Company/Pages/Reports/IncomeStatement.php
  15. 4
    1
      app/Filament/Company/Resources/Banking/AccountResource.php
  16. 2
    1
      app/Filament/Company/Resources/Banking/AccountResource/Pages/EditAccount.php
  17. 1
    1
      app/Listeners/UpdateAccountBalances.php
  18. 1
    1
      app/Models/Accounting/Transaction.php
  19. 2
    1
      app/Services/AccountService.php
  20. 59
    43
      app/Services/ExportService.php
  21. 112
    6
      app/Services/ReportService.php
  22. 5
    16
      app/Transformers/AccountBalanceReportTransformer.php
  23. 5
    16
      app/Transformers/AccountTransactionReportTransformer.php
  24. 261
    0
      app/Transformers/BalanceSheetReportTransformer.php
  25. 42
    5
      app/Transformers/BaseReportTransformer.php
  26. 92
    21
      app/Transformers/IncomeStatementReportTransformer.php
  27. 34
    0
      app/Transformers/SummaryReportTransformer.php
  28. 5
    15
      app/Transformers/TrialBalanceReportTransformer.php
  29. 211
    208
      composer.lock
  30. 15
    13
      config/chart-of-accounts.php
  31. 3
    4
      database/factories/Accounting/TransactionFactory.php
  32. 5
    0
      database/factories/Setting/CompanyDefaultFactory.php
  33. 2
    2
      database/factories/Setting/CurrencyFactory.php
  34. 28
    0
      database/migrations/2024_10_13_163049_update_posted_at_column_in_transactions_table.php
  35. 27
    27
      package-lock.json
  36. 1
    5
      resources/css/filament/company/theme.css
  37. 3
    3
      resources/views/components/company/reports/account-transactions-report-pdf.blade.php
  38. 48
    3
      resources/views/components/company/reports/report-pdf.blade.php
  39. 20
    0
      resources/views/components/company/tables/category-header.blade.php
  40. 23
    0
      resources/views/components/company/tables/cell.blade.php
  41. 24
    0
      resources/views/components/company/tables/container.blade.php
  42. 15
    0
      resources/views/components/company/tables/footer.blade.php
  43. 16
    0
      resources/views/components/company/tables/header.blade.php
  44. 10
    23
      resources/views/components/company/tables/reports/account-transactions.blade.php
  45. 31
    0
      resources/views/components/company/tables/reports/balance-sheet-summary.blade.php
  46. 131
    0
      resources/views/components/company/tables/reports/balance-sheet.blade.php
  47. 49
    79
      resources/views/components/company/tables/reports/detailed-report.blade.php
  48. 28
    0
      resources/views/components/company/tables/reports/income-statement-summary.blade.php
  49. 12
    6
      resources/views/filament/company/pages/accounting/chart.blade.php
  50. 12
    27
      resources/views/filament/company/pages/reports/account-transactions.blade.php
  51. 89
    0
      resources/views/filament/company/pages/reports/balance-sheet.blade.php
  52. 5
    20
      resources/views/filament/company/pages/reports/detailed-report.blade.php
  53. 23
    18
      resources/views/filament/company/pages/reports/income-statement.blade.php
  54. 5
    20
      resources/views/filament/company/pages/reports/trial-balance.blade.php
  55. 3
    3
      tests/Feature/Accounting/TransactionTest.php

+ 6
- 0
app/Contracts/ExportableReport.php Переглянути файл

3
 namespace App\Contracts;
3
 namespace App\Contracts;
4
 
4
 
5
 use App\DTO\ReportCategoryDTO;
5
 use App\DTO\ReportCategoryDTO;
6
+use App\Support\Column;
6
 
7
 
7
 interface ExportableReport
8
 interface ExportableReport
8
 {
9
 {
17
 
18
 
18
     public function getOverallTotals(): array;
19
     public function getOverallTotals(): array;
19
 
20
 
21
+    /**
22
+     * @return Column[]
23
+     */
20
     public function getColumns(): array;
24
     public function getColumns(): array;
21
 
25
 
22
     public function getPdfView(): string;
26
     public function getPdfView(): string;
27
+
28
+    public function getAlignmentClass(string $columnName): string;
23
 }
29
 }

+ 23
- 0
app/Contracts/HasSummaryReport.php Переглянути файл

1
+<?php
2
+
3
+namespace App\Contracts;
4
+
5
+use App\DTO\ReportCategoryDTO;
6
+use App\Support\Column;
7
+
8
+interface HasSummaryReport
9
+{
10
+    /**
11
+     * @return Column[]
12
+     */
13
+    public function getSummaryColumns(): array;
14
+
15
+    public function getSummaryHeaders(): array;
16
+
17
+    /**
18
+     * @return ReportCategoryDTO[]
19
+     */
20
+    public function getSummaryCategories(): array;
21
+
22
+    public function getSummaryOverallTotals(): array;
23
+}

+ 5
- 3
app/DTO/AccountCategoryDTO.php Переглянути файл

5
 class AccountCategoryDTO
5
 class AccountCategoryDTO
6
 {
6
 {
7
     /**
7
     /**
8
-     * @param  AccountDTO[]  $accounts
8
+     * @param  AccountDTO[]|null  $accounts
9
+     * @param  AccountTypeDTO[]|null  $types
9
      */
10
      */
10
     public function __construct(
11
     public function __construct(
11
-        public array $accounts,
12
-        public AccountBalanceDTO $summary,
12
+        public ?array $accounts = null,
13
+        public ?array $types = null,
14
+        public ?AccountBalanceDTO $summary = null,
13
     ) {}
15
     ) {}
14
 }
16
 }

+ 14
- 0
app/DTO/AccountTypeDTO.php Переглянути файл

1
+<?php
2
+
3
+namespace App\DTO;
4
+
5
+class AccountTypeDTO
6
+{
7
+    /**
8
+     * @param  AccountDTO[]  $accounts
9
+     */
10
+    public function __construct(
11
+        public array $accounts,
12
+        public AccountBalanceDTO $summary,
13
+    ) {}
14
+}

+ 9
- 3
app/DTO/ReportCategoryDTO.php Переглянути файл

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

+ 12
- 0
app/DTO/ReportTypeDTO.php Переглянути файл

1
+<?php
2
+
3
+namespace App\DTO;
4
+
5
+class ReportTypeDTO
6
+{
7
+    public function __construct(
8
+        public ?array $header = null,
9
+        public ?array $data = null,
10
+        public ?array $summary = null,
11
+    ) {}
12
+}

+ 22
- 0
app/Enums/Accounting/AccountType.php Переглянути файл

45
         };
45
         };
46
     }
46
     }
47
 
47
 
48
+    public function getPluralLabel(): ?string
49
+    {
50
+        return match ($this) {
51
+            self::CurrentAsset => 'Current Assets',
52
+            self::NonCurrentAsset => 'Non-Current Assets',
53
+            self::ContraAsset => 'Contra Assets',
54
+            self::CurrentLiability => 'Current Liabilities',
55
+            self::NonCurrentLiability => 'Non-Current Liabilities',
56
+            self::ContraLiability => 'Contra Liabilities',
57
+            self::Equity => 'Equity',
58
+            self::ContraEquity => 'Contra Equity',
59
+            self::OperatingRevenue => 'Operating Revenue',
60
+            self::NonOperatingRevenue => 'Non-Operating Revenue',
61
+            self::ContraRevenue => 'Contra Revenue',
62
+            self::UncategorizedRevenue => 'Uncategorized Revenue',
63
+            self::OperatingExpense => 'Operating Expenses',
64
+            self::NonOperatingExpense => 'Non-Operating Expenses',
65
+            self::ContraExpense => 'Contra Expenses',
66
+            self::UncategorizedExpense => 'Uncategorized Expenses',
67
+        };
68
+    }
69
+
48
     public function getCategory(): AccountCategory
70
     public function getCategory(): AccountCategory
49
     {
71
     {
50
         return match ($this) {
72
         return match ($this) {

+ 3
- 0
app/Filament/Company/Clusters/Settings/Resources/CurrencyResource.php Переглянути файл

185
                                     $action->cancel();
185
                                     $action->cancel();
186
                                 }
186
                                 }
187
                             }
187
                             }
188
+                        })
189
+                        ->hidden(function (Table $table) {
190
+                            return $table->getAllSelectableRecordsCount() === 0;
188
                         }),
191
                         }),
189
                 ]),
192
                 ]),
190
             ])
193
             ])

+ 1
- 6
app/Filament/Company/Pages/Accounting/AccountChart.php Переглянути файл

33
     protected static string $view = 'filament.company.pages.accounting.chart';
33
     protected static string $view = 'filament.company.pages.accounting.chart';
34
 
34
 
35
     #[Url]
35
     #[Url]
36
-    public ?string $activeTab = null;
37
-
38
-    public function mount(): void
39
-    {
40
-        $this->activeTab = $this->activeTab ?? AccountCategory::Asset->value;
41
-    }
36
+    public ?string $activeTab = AccountCategory::Asset->value;
42
 
37
 
43
     protected function configureAction(Action $action): void
38
     protected function configureAction(Action $action): void
44
     {
39
     {

+ 1
- 1
app/Filament/Company/Pages/Accounting/Transactions.php Переглянути файл

459
     protected function getFormDefaultsForType(TransactionType $type): array
459
     protected function getFormDefaultsForType(TransactionType $type): array
460
     {
460
     {
461
         $commonDefaults = [
461
         $commonDefaults = [
462
-            'posted_at' => now()->format('Y-m-d'),
462
+            'posted_at' => today(),
463
         ];
463
         ];
464
 
464
 
465
         return match ($type) {
465
         return match ($type) {

+ 2
- 1
app/Filament/Company/Pages/Reports.php Переглянути файл

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\AccountTransactions;
7
+use App\Filament\Company\Pages\Reports\BalanceSheet;
7
 use App\Filament\Company\Pages\Reports\IncomeStatement;
8
 use App\Filament\Company\Pages\Reports\IncomeStatement;
8
 use App\Filament\Company\Pages\Reports\TrialBalance;
9
 use App\Filament\Company\Pages\Reports\TrialBalance;
9
 use App\Infolists\Components\ReportEntry;
10
 use App\Infolists\Components\ReportEntry;
41
                             ->description('Snapshot of assets, liabilities, and equity at a specific point in time.')
42
                             ->description('Snapshot of assets, liabilities, and equity at a specific point in time.')
42
                             ->icon('heroicon-o-clipboard-document-list')
43
                             ->icon('heroicon-o-clipboard-document-list')
43
                             ->iconColor(Color::Emerald)
44
                             ->iconColor(Color::Emerald)
44
-                            ->url('#'),
45
+                            ->url(BalanceSheet::getUrl()),
45
                         ReportEntry::make('cash_flow_statement')
46
                         ReportEntry::make('cash_flow_statement')
46
                             ->hiddenLabel()
47
                             ->hiddenLabel()
47
                             ->heading('Cash Flow Statement')
48
                             ->heading('Cash Flow Statement')

+ 1
- 1
app/Filament/Company/Pages/Reports/AccountTransactions.php Переглянути файл

71
                 ->label('Credit')
71
                 ->label('Credit')
72
                 ->alignment(Alignment::Right),
72
                 ->alignment(Alignment::Right),
73
             Column::make('balance')
73
             Column::make('balance')
74
-                ->label('Balance')
74
+                ->label('Running Balance')
75
                 ->alignment(Alignment::Right),
75
                 ->alignment(Alignment::Right),
76
         ];
76
         ];
77
     }
77
     }

+ 87
- 0
app/Filament/Company/Pages/Reports/BalanceSheet.php Переглянути файл

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\Services\ExportService;
9
+use App\Services\ReportService;
10
+use App\Support\Column;
11
+use App\Transformers\BalanceSheetReportTransformer;
12
+use Filament\Forms\Form;
13
+use Filament\Support\Enums\Alignment;
14
+use Livewire\Attributes\Url;
15
+use Symfony\Component\HttpFoundation\StreamedResponse;
16
+
17
+class BalanceSheet extends BaseReportPage
18
+{
19
+    protected static string $view = 'filament.company.pages.reports.balance-sheet';
20
+
21
+    protected static bool $shouldRegisterNavigation = false;
22
+
23
+    protected ReportService $reportService;
24
+
25
+    protected ExportService $exportService;
26
+
27
+    #[Url]
28
+    public ?string $activeTab = 'summary';
29
+
30
+    public function boot(ReportService $reportService, ExportService $exportService): void
31
+    {
32
+        $this->reportService = $reportService;
33
+        $this->exportService = $exportService;
34
+    }
35
+
36
+    public function getTable(): array
37
+    {
38
+        return [
39
+            Column::make('account_code')
40
+                ->label('Account Code')
41
+                ->toggleable()
42
+                ->alignment(Alignment::Center),
43
+            Column::make('account_name')
44
+                ->label('Account')
45
+                ->alignment(Alignment::Left),
46
+            Column::make('ending_balance')
47
+                ->label('Amount')
48
+                ->alignment(Alignment::Right),
49
+        ];
50
+    }
51
+
52
+    public function filtersForm(Form $form): Form
53
+    {
54
+        return $form
55
+            ->inlineLabel()
56
+            ->columns(3)
57
+            ->schema([
58
+                DateRangeSelect::make('dateRange')
59
+                    ->label('As of')
60
+                    ->selectablePlaceholder(false)
61
+                    ->endDateField('asOfDate'),
62
+                $this->getAsOfDateFormComponent()
63
+                    ->hiddenLabel()
64
+                    ->extraFieldWrapperAttributes([]),
65
+            ]);
66
+    }
67
+
68
+    protected function buildReport(array $columns): ReportDTO
69
+    {
70
+        return $this->reportService->buildBalanceSheetReport($this->getFormattedAsOfDate(), $columns);
71
+    }
72
+
73
+    protected function getTransformer(ReportDTO $reportDTO): ExportableReport
74
+    {
75
+        return new BalanceSheetReportTransformer($reportDTO);
76
+    }
77
+
78
+    public function exportCSV(): StreamedResponse
79
+    {
80
+        return $this->exportService->exportToCsv($this->company, $this->report, endDate: $this->getFilterState('asOfDate'));
81
+    }
82
+
83
+    public function exportPDF(): StreamedResponse
84
+    {
85
+        return $this->exportService->exportToPdf($this->company, $this->report, endDate: $this->getFilterState('asOfDate'));
86
+    }
87
+}

+ 4
- 0
app/Filament/Company/Pages/Reports/IncomeStatement.php Переглянути файл

11
 use Filament\Forms\Form;
11
 use Filament\Forms\Form;
12
 use Filament\Support\Enums\Alignment;
12
 use Filament\Support\Enums\Alignment;
13
 use Guava\FilamentClusters\Forms\Cluster;
13
 use Guava\FilamentClusters\Forms\Cluster;
14
+use Livewire\Attributes\Url;
14
 use Symfony\Component\HttpFoundation\StreamedResponse;
15
 use Symfony\Component\HttpFoundation\StreamedResponse;
15
 
16
 
16
 class IncomeStatement extends BaseReportPage
17
 class IncomeStatement extends BaseReportPage
25
 
26
 
26
     protected ExportService $exportService;
27
     protected ExportService $exportService;
27
 
28
 
29
+    #[Url]
30
+    public ?string $activeTab = 'summary';
31
+
28
     public function boot(ReportService $reportService, ExportService $exportService): void
32
     public function boot(ReportService $reportService, ExportService $exportService): void
29
     {
33
     {
30
         $this->reportService = $reportService;
34
         $this->reportService = $reportService;

+ 4
- 1
app/Filament/Company/Resources/Banking/AccountResource.php Переглянути файл

143
                 Tables\Actions\BulkActionGroup::make([
143
                 Tables\Actions\BulkActionGroup::make([
144
                     Tables\Actions\DeleteBulkAction::make()
144
                     Tables\Actions\DeleteBulkAction::make()
145
                         ->requiresConfirmation()
145
                         ->requiresConfirmation()
146
-                        ->modalDescription('Are you sure you want to delete the selected accounts? All transactions associated with the accounts will be deleted as well.'),
146
+                        ->modalDescription('Are you sure you want to delete the selected accounts? All transactions associated with the accounts will be deleted as well.')
147
+                        ->hidden(function (Table $table) {
148
+                            return $table->getAllSelectableRecordsCount() === 0;
149
+                        }),
147
                 ]),
150
                 ]),
148
             ])
151
             ])
149
             ->checkIfRecordIsSelectableUsing(static function (BankAccount $record) {
152
             ->checkIfRecordIsSelectableUsing(static function (BankAccount $record) {

+ 2
- 1
app/Filament/Company/Resources/Banking/AccountResource/Pages/EditAccount.php Переглянути файл

3
 namespace App\Filament\Company\Resources\Banking\AccountResource\Pages;
3
 namespace App\Filament\Company\Resources\Banking\AccountResource\Pages;
4
 
4
 
5
 use App\Filament\Company\Resources\Banking\AccountResource;
5
 use App\Filament\Company\Resources\Banking\AccountResource;
6
+use Filament\Actions;
6
 use Filament\Resources\Pages\EditRecord;
7
 use Filament\Resources\Pages\EditRecord;
7
 
8
 
8
 class EditAccount extends EditRecord
9
 class EditAccount extends EditRecord
12
     protected function getHeaderActions(): array
13
     protected function getHeaderActions(): array
13
     {
14
     {
14
         return [
15
         return [
15
-            //
16
+            Actions\DeleteAction::make(),
16
         ];
17
         ];
17
     }
18
     }
18
 
19
 

+ 1
- 1
app/Listeners/UpdateAccountBalances.php Переглянути файл

58
                         'type' => $transactionType,
58
                         'type' => $transactionType,
59
                         'amount' => $formattedSimpleDifference,
59
                         'amount' => $formattedSimpleDifference,
60
                         'payment_channel' => 'other',
60
                         'payment_channel' => 'other',
61
-                        'posted_at' => now(),
61
+                        'posted_at' => today(),
62
                         'description' => $description,
62
                         'description' => $description,
63
                         'pending' => false,
63
                         'pending' => false,
64
                         'reviewed' => false,
64
                         'reviewed' => false,

+ 1
- 1
app/Models/Accounting/Transaction.php Переглянути файл

48
         'amount' => TransactionAmountCast::class,
48
         'amount' => TransactionAmountCast::class,
49
         'pending' => 'boolean',
49
         'pending' => 'boolean',
50
         'reviewed' => 'boolean',
50
         'reviewed' => 'boolean',
51
-        'posted_at' => 'datetime',
51
+        'posted_at' => 'date',
52
     ];
52
     ];
53
 
53
 
54
     public function account(): BelongsTo
54
     public function account(): BelongsTo

+ 2
- 1
app/Services/AccountService.php Переглянути файл

138
                 'accounts.id',
138
                 'accounts.id',
139
                 'accounts.name',
139
                 'accounts.name',
140
                 'accounts.category',
140
                 'accounts.category',
141
+                'accounts.type',
141
                 'accounts.subtype_id',
142
                 'accounts.subtype_id',
142
                 'accounts.currency_code',
143
                 'accounts.currency_code',
143
                 'accounts.code',
144
                 'accounts.code',
227
     {
228
     {
228
         $earliestDate = Transaction::min('posted_at');
229
         $earliestDate = Transaction::min('posted_at');
229
 
230
 
230
-        return $earliestDate ?? now()->toDateTimeString();
231
+        return $earliestDate ?? today()->toDateTimeString();
231
     }
232
     }
232
 }
233
 }

+ 59
- 43
app/Services/ExportService.php Переглянути файл

4
 
4
 
5
 use App\Contracts\ExportableReport;
5
 use App\Contracts\ExportableReport;
6
 use App\Models\Company;
6
 use App\Models\Company;
7
-use App\Support\Column;
8
 use Barryvdh\Snappy\Facades\SnappyPdf;
7
 use Barryvdh\Snappy\Facades\SnappyPdf;
9
 use Carbon\Exceptions\InvalidFormatException;
8
 use Carbon\Exceptions\InvalidFormatException;
10
 use Illuminate\Support\Carbon;
9
 use Illuminate\Support\Carbon;
10
+use League\Csv\Bom;
11
+use League\Csv\CannotInsertRecord;
12
+use League\Csv\Exception;
13
+use League\Csv\Writer;
11
 use Symfony\Component\HttpFoundation\StreamedResponse;
14
 use Symfony\Component\HttpFoundation\StreamedResponse;
12
 
15
 
13
 class ExportService
16
 class ExportService
33
         ];
36
         ];
34
 
37
 
35
         $callback = function () use ($startDate, $endDate, $report, $company) {
38
         $callback = function () use ($startDate, $endDate, $report, $company) {
36
-            $file = fopen('php://output', 'wb');
39
+            $csv = Writer::createFromStream(fopen('php://output', 'wb'));
40
+            $csv->setOutputBOM(Bom::Utf8);
37
 
41
 
38
             if ($startDate && $endDate) {
42
             if ($startDate && $endDate) {
39
                 $defaultStartDateFormat = Carbon::parse($startDate)->toDefaultDateFormat();
43
                 $defaultStartDateFormat = Carbon::parse($startDate)->toDefaultDateFormat();
43
                 $dateLabel = 'As of ' . Carbon::parse($endDate)->toDefaultDateFormat();
47
                 $dateLabel = 'As of ' . Carbon::parse($endDate)->toDefaultDateFormat();
44
             }
48
             }
45
 
49
 
46
-            fputcsv($file, [$report->getTitle()]);
47
-            fputcsv($file, [$company->name]);
48
-            fputcsv($file, [$dateLabel]);
49
-            fputcsv($file, []);
50
+            $csv->insertOne([$report->getTitle()]);
51
+            $csv->insertOne([$company->name]);
52
+            $csv->insertOne([$dateLabel]);
53
+            $csv->insertOne([]);
50
 
54
 
51
-            fputcsv($file, $report->getHeaders());
55
+            $csv->insertOne($report->getHeaders());
52
 
56
 
53
             foreach ($report->getCategories() as $category) {
57
             foreach ($report->getCategories() as $category) {
54
-                if (isset($category->header[0]) && is_array($category->header[0])) {
55
-                    foreach ($category->header as $headerRow) {
56
-                        fputcsv($file, $headerRow);
57
-                    }
58
-                } else {
59
-                    fputcsv($file, $category->header);
60
-                }
58
+                $this->writeDataRowsToCsv($csv, $category->header, $category->data, $report->getColumns());
61
 
59
 
62
-                foreach ($category->data as $accountRow) {
63
-                    $row = [];
64
-                    $columns = $report->getColumns();
65
-
66
-                    /**
67
-                     * @var Column $column
68
-                     */
69
-                    foreach ($columns as $index => $column) {
70
-                        $cell = $accountRow[$index] ?? '';
71
-
72
-                        if ($column->isDate()) {
73
-                            try {
74
-                                $row[] = Carbon::parse($cell)->toDateString();
75
-                            } catch (InvalidFormatException) {
76
-                                $row[] = $cell;
77
-                            }
78
-                        } elseif (is_array($cell)) {
79
-                            // Handle array cells by extracting 'name' or 'description'
80
-                            $row[] = $cell['name'] ?? $cell['description'] ?? '';
81
-                        } else {
82
-                            $row[] = $cell;
83
-                        }
84
-                    }
60
+                foreach ($category->types ?? [] as $type) {
61
+                    $this->writeDataRowsToCsv($csv, $type->header, $type->data, $report->getColumns());
85
 
62
 
86
-                    fputcsv($file, $row);
63
+                    if (filled($type->summary)) {
64
+                        $csv->insertOne($type->summary);
65
+                    }
87
                 }
66
                 }
88
 
67
 
89
                 if (filled($category->summary)) {
68
                 if (filled($category->summary)) {
90
-                    fputcsv($file, $category->summary);
69
+                    $csv->insertOne($category->summary);
91
                 }
70
                 }
92
 
71
 
93
-                fputcsv($file, []); // Empty row for spacing
72
+                $csv->insertOne([]);
94
             }
73
             }
95
 
74
 
96
             if (filled($report->getOverallTotals())) {
75
             if (filled($report->getOverallTotals())) {
97
-                fputcsv($file, $report->getOverallTotals());
76
+                $csv->insertOne($report->getOverallTotals());
98
             }
77
             }
99
-
100
-            fclose($file);
101
         };
78
         };
102
 
79
 
103
         return response()->streamDownload($callback, $filename, $headers);
80
         return response()->streamDownload($callback, $filename, $headers);
129
             echo $pdf->inline();
106
             echo $pdf->inline();
130
         }, $filename);
107
         }, $filename);
131
     }
108
     }
109
+
110
+    /**
111
+     * @throws CannotInsertRecord
112
+     * @throws Exception
113
+     */
114
+    protected function writeDataRowsToCsv(Writer $csv, array $header, array $data, array $columns): void
115
+    {
116
+        if (isset($header[0]) && is_array($header[0])) {
117
+            foreach ($header as $headerRow) {
118
+                $csv->insertOne($headerRow);
119
+            }
120
+        } else {
121
+            $csv->insertOne($header);
122
+        }
123
+
124
+        // Output data rows
125
+        foreach ($data as $rowData) {
126
+            $row = [];
127
+
128
+            foreach ($columns as $column) {
129
+                $columnName = $column->getName();
130
+                $cell = $rowData[$columnName] ?? '';
131
+
132
+                if ($column->isDate()) {
133
+                    try {
134
+                        $row[] = Carbon::parse($cell)->toDateString();
135
+                    } catch (InvalidFormatException) {
136
+                        $row[] = $cell;
137
+                    }
138
+                } elseif (is_array($cell)) {
139
+                    $row[] = $cell['name'] ?? $cell['description'] ?? '';
140
+                } else {
141
+                    $row[] = $cell;
142
+                }
143
+            }
144
+
145
+            $csv->insertOne($row);
146
+        }
147
+    }
132
 }
148
 }

+ 112
- 6
app/Services/ReportService.php Переглянути файл

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\AccountTransactionDTO;
9
+use App\DTO\AccountTypeDTO;
9
 use App\DTO\ReportDTO;
10
 use App\DTO\ReportDTO;
10
 use App\Enums\Accounting\AccountCategory;
11
 use App\Enums\Accounting\AccountCategory;
12
+use App\Enums\Accounting\AccountType;
11
 use App\Models\Accounting\Account;
13
 use App\Models\Accounting\Account;
12
 use App\Support\Column;
14
 use App\Support\Column;
13
 use App\Utilities\Currency\CurrencyAccessor;
15
 use App\Utilities\Currency\CurrencyAccessor;
86
             $formattedCategorySummaryBalances = $this->formatBalances($categorySummaryBalances);
88
             $formattedCategorySummaryBalances = $this->formatBalances($categorySummaryBalances);
87
 
89
 
88
             $accountCategories[$category->getPluralLabel()] = new AccountCategoryDTO(
90
             $accountCategories[$category->getPluralLabel()] = new AccountCategoryDTO(
89
-                $categoryAccounts,
90
-                $formattedCategorySummaryBalances,
91
+                accounts: $categoryAccounts,
92
+                summary: $formattedCategorySummaryBalances,
91
             );
93
             );
92
         }
94
         }
93
 
95
 
305
             $formattedCategorySummaryBalances = $this->formatBalances($categorySummaryBalances);
307
             $formattedCategorySummaryBalances = $this->formatBalances($categorySummaryBalances);
306
 
308
 
307
             $accountCategories[$category->getPluralLabel()] = new AccountCategoryDTO(
309
             $accountCategories[$category->getPluralLabel()] = new AccountCategoryDTO(
308
-                $categoryAccounts,
309
-                $formattedCategorySummaryBalances,
310
+                accounts: $categoryAccounts,
311
+                summary: $formattedCategorySummaryBalances,
310
             );
312
             );
311
         }
313
         }
312
 
314
 
417
             }
419
             }
418
 
420
 
419
             $accountCategories[$label] = new AccountCategoryDTO(
421
             $accountCategories[$label] = new AccountCategoryDTO(
420
-                $categoryAccounts,
421
-                $this->formatBalances(['net_movement' => $netMovement])
422
+                accounts: $categoryAccounts,
423
+                summary: $this->formatBalances(['net_movement' => $netMovement])
422
             );
424
             );
423
         }
425
         }
424
 
426
 
429
 
431
 
430
         return new ReportDTO($accountCategories, $formattedReportTotalBalances, $columns);
432
         return new ReportDTO($accountCategories, $formattedReportTotalBalances, $columns);
431
     }
433
     }
434
+
435
+    public function buildBalanceSheetReport(string $asOfDate, array $columns = []): ReportDTO
436
+    {
437
+        $asOfDateCarbon = Carbon::parse($asOfDate);
438
+        $startDateCarbon = Carbon::parse($this->accountService->getEarliestTransactionDate());
439
+
440
+        $orderedCategories = array_filter(AccountCategory::getOrderedCategories(), fn (AccountCategory $category) => $category->isReal());
441
+
442
+        $accounts = $this->accountService->getAccountBalances($startDateCarbon->toDateTimeString(), $asOfDateCarbon->toDateTimeString())
443
+            ->whereIn('category', $orderedCategories)
444
+            ->orderByRaw('LENGTH(code), code')
445
+            ->get();
446
+
447
+        $accountCategories = [];
448
+        $reportTotalBalances = [
449
+            'assets' => 0,
450
+            'liabilities' => 0,
451
+            'equity' => 0,
452
+        ];
453
+
454
+        foreach ($orderedCategories as $category) {
455
+            $categorySummaryBalances = ['ending_balance' => 0];
456
+
457
+            $categoryAccountsByType = [];
458
+            $categoryAccounts = [];
459
+            $subCategoryTotals = [];
460
+
461
+            /** @var Account $account */
462
+            foreach ($accounts as $account) {
463
+                if ($account->type->getCategory() === $category) {
464
+                    $accountBalances = $this->calculateAccountBalances($account, $category);
465
+                    $endingBalance = $accountBalances['ending_balance'] ?? $accountBalances['net_movement'];
466
+
467
+                    $categorySummaryBalances['ending_balance'] += $endingBalance;
468
+
469
+                    $formattedAccountBalances = $this->formatBalances($accountBalances);
470
+
471
+                    $accountDTO = new AccountDTO(
472
+                        $account->name,
473
+                        $account->code,
474
+                        $account->id,
475
+                        $formattedAccountBalances,
476
+                        startDate: $startDateCarbon->toDateString(),
477
+                        endDate: $asOfDateCarbon->toDateString(),
478
+                    );
479
+
480
+                    if ($category === AccountCategory::Equity && $account->type === AccountType::Equity) {
481
+                        $categoryAccounts[] = $accountDTO;
482
+                    } else {
483
+                        $accountType = $account->type->getPluralLabel();
484
+                        $categoryAccountsByType[$accountType][] = $accountDTO;
485
+                        $subCategoryTotals[$accountType] = ($subCategoryTotals[$accountType] ?? 0) + $endingBalance;
486
+                    }
487
+                }
488
+            }
489
+
490
+            if ($category === AccountCategory::Equity) {
491
+                $retainedEarningsAmount = $this->calculateRetainedEarnings($startDateCarbon->toDateTimeString(), $asOfDateCarbon->toDateTimeString())->getAmount();
492
+
493
+                $categorySummaryBalances['ending_balance'] += $retainedEarningsAmount;
494
+
495
+                $retainedEarningsDTO = new AccountDTO(
496
+                    'Retained Earnings',
497
+                    'RE',
498
+                    null,
499
+                    $this->formatBalances(['ending_balance' => $retainedEarningsAmount]),
500
+                    startDate: $startDateCarbon->toDateString(),
501
+                    endDate: $asOfDateCarbon->toDateString(),
502
+                );
503
+
504
+                $categoryAccounts[] = $retainedEarningsDTO;
505
+            }
506
+
507
+            $subCategories = [];
508
+            foreach ($categoryAccountsByType as $accountType => $accountsInType) {
509
+                $subCategorySummary = $this->formatBalances([
510
+                    'ending_balance' => $subCategoryTotals[$accountType] ?? 0,
511
+                ]);
512
+
513
+                $subCategories[$accountType] = new AccountTypeDTO(
514
+                    accounts: $accountsInType,
515
+                    summary: $subCategorySummary
516
+                );
517
+            }
518
+
519
+            $reportTotalBalances[match ($category) {
520
+                AccountCategory::Asset => 'assets',
521
+                AccountCategory::Liability => 'liabilities',
522
+                AccountCategory::Equity => 'equity',
523
+            }] += $categorySummaryBalances['ending_balance'];
524
+
525
+            $accountCategories[$category->getPluralLabel()] = new AccountCategoryDTO(
526
+                accounts: $categoryAccounts,
527
+                types: $subCategories,
528
+                summary: $this->formatBalances($categorySummaryBalances),
529
+            );
530
+        }
531
+
532
+        $netAssets = $reportTotalBalances['assets'] - $reportTotalBalances['liabilities'];
533
+
534
+        $formattedReportTotalBalances = $this->formatBalances(['ending_balance' => $netAssets]);
535
+
536
+        return new ReportDTO($accountCategories, $formattedReportTotalBalances, $columns);
537
+    }
432
 }
538
 }

+ 5
- 16
app/Transformers/AccountBalanceReportTransformer.php Переглянути файл

4
 
4
 
5
 use App\DTO\AccountDTO;
5
 use App\DTO\AccountDTO;
6
 use App\DTO\ReportCategoryDTO;
6
 use App\DTO\ReportCategoryDTO;
7
-use App\Support\Column;
8
 
7
 
9
 class AccountBalanceReportTransformer extends BaseReportTransformer
8
 class AccountBalanceReportTransformer extends BaseReportTransformer
10
 {
9
 {
13
         return 'Account Balances';
12
         return 'Account Balances';
14
     }
13
     }
15
 
14
 
16
-    public function getHeaders(): array
17
-    {
18
-        return array_map(fn (Column $column) => $column->getLabel(), $this->getColumns());
19
-    }
20
-
21
     /**
15
     /**
22
      * @return ReportCategoryDTO[]
16
      * @return ReportCategoryDTO[]
23
      */
17
      */
26
         $categories = [];
20
         $categories = [];
27
 
21
 
28
         foreach ($this->report->categories as $accountCategoryName => $accountCategory) {
22
         foreach ($this->report->categories as $accountCategoryName => $accountCategory) {
29
-            // Initialize header with empty strings
30
             $header = [];
23
             $header = [];
31
 
24
 
32
-            foreach ($this->getColumns() as $index => $column) {
33
-                if ($column->getName() === 'account_name') {
34
-                    $header[$index] = $accountCategoryName;
35
-                } else {
36
-                    $header[$index] = '';
37
-                }
25
+            foreach ($this->getColumns() as $column) {
26
+                $header[$column->getName()] = $column->getName() === 'account_name' ? $accountCategoryName : '';
38
             }
27
             }
39
 
28
 
40
             $data = array_map(function (AccountDTO $account) {
29
             $data = array_map(function (AccountDTO $account) {
41
                 $row = [];
30
                 $row = [];
42
 
31
 
43
                 foreach ($this->getColumns() as $column) {
32
                 foreach ($this->getColumns() as $column) {
44
-                    $row[] = match ($column->getName()) {
33
+                    $row[$column->getName()] = match ($column->getName()) {
45
                         'account_code' => $account->accountCode,
34
                         'account_code' => $account->accountCode,
46
                         'account_name' => [
35
                         'account_name' => [
47
                             'name' => $account->accountName,
36
                             'name' => $account->accountName,
64
             $summary = [];
53
             $summary = [];
65
 
54
 
66
             foreach ($this->getColumns() as $column) {
55
             foreach ($this->getColumns() as $column) {
67
-                $summary[] = match ($column->getName()) {
56
+                $summary[$column->getName()] = match ($column->getName()) {
68
                     'account_name' => 'Total ' . $accountCategoryName,
57
                     'account_name' => 'Total ' . $accountCategoryName,
69
                     'starting_balance' => $accountCategory->summary->startingBalance ?? '',
58
                     'starting_balance' => $accountCategory->summary->startingBalance ?? '',
70
                     'debit_balance' => $accountCategory->summary->debitBalance,
59
                     'debit_balance' => $accountCategory->summary->debitBalance,
90
         $totals = [];
79
         $totals = [];
91
 
80
 
92
         foreach ($this->getColumns() as $column) {
81
         foreach ($this->getColumns() as $column) {
93
-            $totals[] = match ($column->getName()) {
82
+            $totals[$column->getName()] = match ($column->getName()) {
94
                 'account_name' => 'Total for all accounts',
83
                 'account_name' => 'Total for all accounts',
95
                 'debit_balance' => $this->report->overallTotal->debitBalance,
84
                 'debit_balance' => $this->report->overallTotal->debitBalance,
96
                 'credit_balance' => $this->report->overallTotal->creditBalance,
85
                 'credit_balance' => $this->report->overallTotal->creditBalance,

+ 5
- 16
app/Transformers/AccountTransactionReportTransformer.php Переглянути файл

4
 
4
 
5
 use App\DTO\AccountTransactionDTO;
5
 use App\DTO\AccountTransactionDTO;
6
 use App\DTO\ReportCategoryDTO;
6
 use App\DTO\ReportCategoryDTO;
7
-use App\Support\Column;
8
 
7
 
9
 class AccountTransactionReportTransformer extends BaseReportTransformer
8
 class AccountTransactionReportTransformer extends BaseReportTransformer
10
 {
9
 {
18
         return 'Account Transactions';
17
         return 'Account Transactions';
19
     }
18
     }
20
 
19
 
21
-    public function getHeaders(): array
22
-    {
23
-        return array_map(fn (Column $column) => $column->getLabel(), $this->getColumns());
24
-    }
25
-
26
     /**
20
     /**
27
      * @return ReportCategoryDTO[]
21
      * @return ReportCategoryDTO[]
28
      */
22
      */
31
         $categories = [];
25
         $categories = [];
32
 
26
 
33
         foreach ($this->report->categories as $categoryData) {
27
         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
-            ];
28
+            $header = [];
40
 
29
 
41
-            foreach ($this->getColumns() as $index => $column) {
30
+            foreach ($this->getColumns() as $column) {
42
                 if ($column->getName() === 'date') {
31
                 if ($column->getName() === 'date') {
43
-                    $header[0][$index] = $categoryData['category'];
44
-                    $header[1][$index] = $categoryData['under'];
32
+                    $header[0][$column->getName()] = $categoryData['category'];
33
+                    $header[1][$column->getName()] = $categoryData['under'];
45
                 }
34
                 }
46
             }
35
             }
47
 
36
 
50
                 $row = [];
39
                 $row = [];
51
 
40
 
52
                 foreach ($this->getColumns() as $column) {
41
                 foreach ($this->getColumns() as $column) {
53
-                    $row[] = match ($column->getName()) {
42
+                    $row[$column->getName()] = match ($column->getName()) {
54
                         'date' => $transaction->date,
43
                         'date' => $transaction->date,
55
                         'description' => [
44
                         'description' => [
56
                             'id' => $transaction->id,
45
                             'id' => $transaction->id,

+ 261
- 0
app/Transformers/BalanceSheetReportTransformer.php Переглянути файл

1
+<?php
2
+
3
+namespace App\Transformers;
4
+
5
+use App\DTO\AccountDTO;
6
+use App\DTO\ReportCategoryDTO;
7
+use App\DTO\ReportDTO;
8
+use App\DTO\ReportTypeDTO;
9
+use App\Utilities\Currency\CurrencyAccessor;
10
+
11
+class BalanceSheetReportTransformer extends SummaryReportTransformer
12
+{
13
+    protected string $totalAssets;
14
+
15
+    protected string $totalLiabilities;
16
+
17
+    protected string $totalEquity;
18
+
19
+    public function __construct(ReportDTO $report)
20
+    {
21
+        parent::__construct($report);
22
+
23
+        $this->calculateTotals();
24
+    }
25
+
26
+    public function getTitle(): string
27
+    {
28
+        return 'Balance Sheet';
29
+    }
30
+
31
+    public function calculateTotals(): void
32
+    {
33
+        foreach ($this->report->categories as $accountCategoryName => $accountCategory) {
34
+            match ($accountCategoryName) {
35
+                'Assets' => $this->totalAssets = $accountCategory->summary->endingBalance ?? '',
36
+                'Liabilities' => $this->totalLiabilities = $accountCategory->summary->endingBalance ?? '',
37
+                'Equity' => $this->totalEquity = $accountCategory->summary->endingBalance ?? '',
38
+            };
39
+        }
40
+    }
41
+
42
+    public function getCategories(): array
43
+    {
44
+        $categories = [];
45
+
46
+        foreach ($this->report->categories as $accountCategoryName => $accountCategory) {
47
+            // Header for the main category
48
+            $header = [];
49
+
50
+            foreach ($this->getColumns() as $column) {
51
+                $header[$column->getName()] = $column->getName() === 'account_name' ? $accountCategoryName : '';
52
+            }
53
+
54
+            // Category-level summary
55
+            $categorySummary = [];
56
+            foreach ($this->getColumns() as $column) {
57
+                $categorySummary[$column->getName()] = match ($column->getName()) {
58
+                    'account_name' => 'Total ' . $accountCategoryName,
59
+                    'ending_balance' => $accountCategory->summary->endingBalance ?? '',
60
+                    default => '',
61
+                };
62
+            }
63
+
64
+            // Accounts directly under the main category
65
+            $data = array_map(function (AccountDTO $account) {
66
+                $row = [];
67
+
68
+                foreach ($this->getColumns() as $column) {
69
+                    $row[$column->getName()] = match ($column->getName()) {
70
+                        'account_code' => $account->accountCode,
71
+                        'account_name' => [
72
+                            'name' => $account->accountName,
73
+                            'id' => $account->accountId ?? null,
74
+                            'start_date' => $account->startDate,
75
+                            'end_date' => $account->endDate,
76
+                        ],
77
+                        'ending_balance' => $account->balance->endingBalance ?? '',
78
+                        default => '',
79
+                    };
80
+                }
81
+
82
+                return $row;
83
+            }, $accountCategory->accounts ?? []);
84
+
85
+            // Subcategories (types) under the main category
86
+            $types = [];
87
+            foreach ($accountCategory->types as $typeName => $type) {
88
+                // Header for subcategory (type)
89
+                $typeHeader = [];
90
+                foreach ($this->getColumns() as $column) {
91
+                    $typeHeader[$column->getName()] = $column->getName() === 'account_name' ? $typeName : '';
92
+                }
93
+
94
+                // Account data for the subcategory
95
+                $typeData = array_map(function (AccountDTO $account) {
96
+                    $row = [];
97
+
98
+                    foreach ($this->getColumns() as $column) {
99
+                        $row[$column->getName()] = match ($column->getName()) {
100
+                            'account_code' => $account->accountCode,
101
+                            'account_name' => [
102
+                                'name' => $account->accountName,
103
+                                'id' => $account->accountId ?? null,
104
+                                'start_date' => $account->startDate,
105
+                                'end_date' => $account->endDate,
106
+                            ],
107
+                            'ending_balance' => $account->balance->endingBalance ?? '',
108
+                            default => '',
109
+                        };
110
+                    }
111
+
112
+                    return $row;
113
+                }, $type->accounts);
114
+
115
+                // Subcategory (type) summary
116
+                $typeSummary = [];
117
+                foreach ($this->getColumns() as $column) {
118
+                    $typeSummary[$column->getName()] = match ($column->getName()) {
119
+                        'account_name' => 'Total ' . $typeName,
120
+                        'ending_balance' => $type->summary->endingBalance ?? '',
121
+                        default => '',
122
+                    };
123
+                }
124
+
125
+                // Add subcategory (type) to the list
126
+                $types[$typeName] = new ReportTypeDTO(
127
+                    header: $typeHeader,
128
+                    data: $typeData,
129
+                    summary: $typeSummary,
130
+                );
131
+            }
132
+
133
+            // Add the category to the final array with its direct accounts and subcategories (types)
134
+            $categories[$accountCategoryName] = new ReportCategoryDTO(
135
+                header: $header,
136
+                data: $data, // Direct accounts under the category
137
+                summary: $categorySummary,
138
+                types: $types, // Subcategories (types) under the category
139
+            );
140
+        }
141
+
142
+        return $categories;
143
+    }
144
+
145
+    public function getSummaryCategories(): array
146
+    {
147
+        $summaryCategories = [];
148
+
149
+        $columns = $this->getSummaryColumns();
150
+
151
+        foreach ($this->report->categories as $accountCategoryName => $accountCategory) {
152
+            $categoryHeader = [];
153
+
154
+            foreach ($columns as $column) {
155
+                $categoryHeader[$column->getName()] = $column->getName() === 'account_name' ? $accountCategoryName : '';
156
+            }
157
+
158
+            $categorySummary = [];
159
+            foreach ($columns as $column) {
160
+                $categorySummary[$column->getName()] = match ($column->getName()) {
161
+                    'account_name' => 'Total ' . $accountCategoryName,
162
+                    'ending_balance' => $accountCategory->summary->endingBalance ?? '',
163
+                    default => '',
164
+                };
165
+            }
166
+
167
+            $types = [];
168
+            $totalTypeSummaries = 0;
169
+
170
+            // Iterate through each account type and calculate type summaries
171
+            foreach ($accountCategory->types as $typeName => $type) {
172
+                $typeSummary = [];
173
+                $typeEndingBalance = 0;
174
+
175
+                foreach ($columns as $column) {
176
+                    $typeSummary[$column->getName()] = match ($column->getName()) {
177
+                        'account_name' => 'Total ' . $typeName,
178
+                        'ending_balance' => $type->summary->endingBalance ?? '',
179
+                        default => '',
180
+                    };
181
+
182
+                    if ($column->getName() === 'ending_balance') {
183
+                        $typeEndingBalance = $type->summary->endingBalance ?? 0;
184
+                    }
185
+                }
186
+
187
+                $typeEndingBalance = money($typeEndingBalance, CurrencyAccessor::getDefaultCurrency())->getAmount();
188
+
189
+                $totalTypeSummaries += $typeEndingBalance;
190
+
191
+                $types[$typeName] = new ReportTypeDTO(
192
+                    header: [],
193
+                    data: [],
194
+                    summary: $typeSummary,
195
+                );
196
+            }
197
+
198
+            // Only for the "Equity" category, calculate and add "Total Other Equity"
199
+            if ($accountCategoryName === 'Equity') {
200
+                $totalEquitySummary = $accountCategory->summary->endingBalance ?? 0;
201
+                $totalEquitySummary = money($totalEquitySummary, CurrencyAccessor::getDefaultCurrency())->getAmount();
202
+                $totalOtherEquity = $totalEquitySummary - $totalTypeSummaries;
203
+                $totalOtherEquity = money($totalOtherEquity, CurrencyAccessor::getDefaultCurrency(), true)->format();
204
+
205
+                // Add "Total Other Equity" as a new "type"
206
+                $otherEquitySummary = [];
207
+                foreach ($columns as $column) {
208
+                    $otherEquitySummary[$column->getName()] = match ($column->getName()) {
209
+                        'account_name' => 'Total Other Equity',
210
+                        'ending_balance' => $totalOtherEquity,
211
+                        default => '',
212
+                    };
213
+                }
214
+
215
+                $types['Total Other Equity'] = new ReportTypeDTO(
216
+                    header: [],
217
+                    data: [],
218
+                    summary: $otherEquitySummary,
219
+                );
220
+            }
221
+
222
+            // Add the category with its types and summary to the final array
223
+            $summaryCategories[$accountCategoryName] = new ReportCategoryDTO(
224
+                header: $categoryHeader,
225
+                data: [],
226
+                summary: $categorySummary,
227
+                types: $types,
228
+            );
229
+        }
230
+
231
+        return $summaryCategories;
232
+    }
233
+
234
+    public function getOverallTotals(): array
235
+    {
236
+        return [];
237
+    }
238
+
239
+    public function getSummaryOverallTotals(): array
240
+    {
241
+        return [];
242
+    }
243
+
244
+    public function getSummary(): array
245
+    {
246
+        return [
247
+            [
248
+                'label' => 'Total Assets',
249
+                'value' => $this->totalAssets,
250
+            ],
251
+            [
252
+                'label' => 'Total Liabilities',
253
+                'value' => $this->totalLiabilities,
254
+            ],
255
+            [
256
+                'label' => 'Net Assets',
257
+                'value' => $this->report->overallTotal->endingBalance ?? '',
258
+            ],
259
+        ];
260
+    }
261
+}

+ 42
- 5
app/Transformers/BaseReportTransformer.php Переглянути файл

4
 
4
 
5
 use App\Contracts\ExportableReport;
5
 use App\Contracts\ExportableReport;
6
 use App\DTO\ReportDTO;
6
 use App\DTO\ReportDTO;
7
+use App\Support\Column;
7
 use Filament\Support\Enums\Alignment;
8
 use Filament\Support\Enums\Alignment;
8
 
9
 
9
 abstract class BaseReportTransformer implements ExportableReport
10
 abstract class BaseReportTransformer implements ExportableReport
15
         $this->report = $report;
16
         $this->report = $report;
16
     }
17
     }
17
 
18
 
19
+    /**
20
+     * @return Column[]
21
+     */
18
     public function getColumns(): array
22
     public function getColumns(): array
19
     {
23
     {
20
-        return $this->report->fields;
24
+        return once(function (): array {
25
+            return $this->report->fields;
26
+        });
27
+    }
28
+
29
+    public function getHeaders(): array
30
+    {
31
+        return once(function (): array {
32
+            $headers = [];
33
+
34
+            foreach ($this->getColumns() as $column) {
35
+                $headers[$column->getName()] = $column->getLabel();
36
+            }
37
+
38
+            return $headers;
39
+        });
21
     }
40
     }
22
 
41
 
23
     public function getPdfView(): string
42
     public function getPdfView(): string
25
         return 'components.company.reports.report-pdf';
44
         return 'components.company.reports.report-pdf';
26
     }
45
     }
27
 
46
 
28
-    public function getAlignmentClass(int $index): string
47
+    public function getAlignment(int $index): string
29
     {
48
     {
30
         $column = $this->getColumns()[$index];
49
         $column = $this->getColumns()[$index];
31
 
50
 
32
         if ($column->getAlignment() === Alignment::Right) {
51
         if ($column->getAlignment() === Alignment::Right) {
33
-            return 'text-right';
52
+            return 'right';
34
         }
53
         }
35
 
54
 
36
         if ($column->getAlignment() === Alignment::Center) {
55
         if ($column->getAlignment() === Alignment::Center) {
37
-            return 'text-center';
56
+            return 'center';
38
         }
57
         }
39
 
58
 
40
-        return 'text-left';
59
+        return 'left';
60
+    }
61
+
62
+    public function getAlignmentClass(string $columnName): string
63
+    {
64
+        return once(function () use ($columnName): string {
65
+            /** @var Column|null $column */
66
+            $column = collect($this->getColumns())->first(fn (Column $column) => $column->getName() === $columnName);
67
+
68
+            if ($column?->getAlignment() === Alignment::Right) {
69
+                return 'text-right';
70
+            }
71
+
72
+            if ($column?->getAlignment() === Alignment::Center) {
73
+                return 'text-center';
74
+            }
75
+
76
+            return 'text-left';
77
+        });
41
     }
78
     }
42
 }
79
 }

+ 92
- 21
app/Transformers/IncomeStatementReportTransformer.php Переглянути файл

4
 
4
 
5
 use App\DTO\AccountDTO;
5
 use App\DTO\AccountDTO;
6
 use App\DTO\ReportCategoryDTO;
6
 use App\DTO\ReportCategoryDTO;
7
-use App\Support\Column;
7
+use App\DTO\ReportDTO;
8
+use App\Utilities\Currency\CurrencyAccessor;
8
 
9
 
9
-class IncomeStatementReportTransformer extends BaseReportTransformer
10
+class IncomeStatementReportTransformer extends SummaryReportTransformer
10
 {
11
 {
11
-    protected string $totalRevenue = '$0.00';
12
+    protected string $totalRevenue;
12
 
13
 
13
-    protected string $totalCogs = '$0.00';
14
+    protected string $totalCogs;
14
 
15
 
15
-    protected string $totalExpenses = '$0.00';
16
+    protected string $totalExpenses;
16
 
17
 
17
-    public function getTitle(): string
18
+    public function __construct(ReportDTO $report)
18
     {
19
     {
19
-        return 'Income Statement';
20
+        parent::__construct($report);
21
+
22
+        $this->calculateTotals();
20
     }
23
     }
21
 
24
 
22
-    public function getHeaders(): array
25
+    public function getTitle(): string
23
     {
26
     {
24
-        return array_map(fn (Column $column) => $column->getLabel(), $this->getColumns());
27
+        return 'Income Statement';
25
     }
28
     }
26
 
29
 
27
     public function calculateTotals(): void
30
     public function calculateTotals(): void
40
         $categories = [];
43
         $categories = [];
41
 
44
 
42
         foreach ($this->report->categories as $accountCategoryName => $accountCategory) {
45
         foreach ($this->report->categories as $accountCategoryName => $accountCategory) {
43
-            // Initialize header with empty strings
44
             $header = [];
46
             $header = [];
45
 
47
 
46
-            foreach ($this->getColumns() as $index => $column) {
47
-                if ($column->getName() === 'account_name') {
48
-                    $header[$index] = $accountCategoryName;
49
-                } else {
50
-                    $header[$index] = '';
51
-                }
48
+            foreach ($this->getColumns() as $column) {
49
+                $header[$column->getName()] = $column->getName() === 'account_name' ? $accountCategoryName : '';
52
             }
50
             }
53
 
51
 
54
             $data = array_map(function (AccountDTO $account) {
52
             $data = array_map(function (AccountDTO $account) {
55
                 $row = [];
53
                 $row = [];
56
 
54
 
57
                 foreach ($this->getColumns() as $column) {
55
                 foreach ($this->getColumns() as $column) {
58
-                    $row[] = match ($column->getName()) {
56
+                    $row[$column->getName()] = match ($column->getName()) {
59
                         'account_code' => $account->accountCode,
57
                         'account_code' => $account->accountCode,
60
                         'account_name' => [
58
                         'account_name' => [
61
                             'name' => $account->accountName,
59
                             'name' => $account->accountName,
74
             $summary = [];
72
             $summary = [];
75
 
73
 
76
             foreach ($this->getColumns() as $column) {
74
             foreach ($this->getColumns() as $column) {
77
-                $summary[] = match ($column->getName()) {
75
+                $summary[$column->getName()] = match ($column->getName()) {
78
                     'account_name' => 'Total ' . $accountCategoryName,
76
                     'account_name' => 'Total ' . $accountCategoryName,
79
                     'net_movement' => $accountCategory->summary->netMovement ?? '',
77
                     'net_movement' => $accountCategory->summary->netMovement ?? '',
80
                     default => '',
78
                     default => '',
91
         return $categories;
89
         return $categories;
92
     }
90
     }
93
 
91
 
92
+    public function getSummaryCategories(): array
93
+    {
94
+        $summaryCategories = [];
95
+
96
+        $columns = $this->getSummaryColumns();
97
+
98
+        foreach ($this->report->categories as $accountCategoryName => $accountCategory) {
99
+            // Header for the main category
100
+            $categoryHeader = [];
101
+
102
+            foreach ($columns as $column) {
103
+                $categoryHeader[$column->getName()] = $column->getName() === 'account_name' ? $accountCategoryName : '';
104
+            }
105
+
106
+            // Category-level summary
107
+            $categorySummary = [];
108
+            foreach ($columns as $column) {
109
+                $categorySummary[$column->getName()] = match ($column->getName()) {
110
+                    'account_name' => $accountCategoryName,
111
+                    'net_movement' => $accountCategory->summary->netMovement ?? '',
112
+                    default => '',
113
+                };
114
+            }
115
+
116
+            // Add the category summary to the final array
117
+            $summaryCategories[$accountCategoryName] = new ReportCategoryDTO(
118
+                header: $categoryHeader,
119
+                data: [], // No direct accounts are needed here, only summaries
120
+                summary: $categorySummary,
121
+                types: [] // No types for the income statement
122
+            );
123
+        }
124
+
125
+        return $summaryCategories;
126
+    }
127
+
128
+    public function getGrossProfit(): array
129
+    {
130
+        $grossProfit = [];
131
+
132
+        $columns = $this->getSummaryColumns();
133
+
134
+        $revenue = money($this->totalRevenue, CurrencyAccessor::getDefaultCurrency())->getAmount();
135
+        $cogs = money($this->totalCogs, CurrencyAccessor::getDefaultCurrency())->getAmount();
136
+
137
+        $grossProfitAmount = $revenue - $cogs;
138
+        $grossProfitFormatted = money($grossProfitAmount, CurrencyAccessor::getDefaultCurrency(), true)->format();
139
+
140
+        foreach ($columns as $column) {
141
+            $grossProfit[$column->getName()] = match ($column->getName()) {
142
+                'account_name' => 'Gross Profit',
143
+                'net_movement' => $grossProfitFormatted,
144
+                default => '',
145
+            };
146
+        }
147
+
148
+        return $grossProfit;
149
+    }
150
+
94
     public function getOverallTotals(): array
151
     public function getOverallTotals(): array
95
     {
152
     {
96
         $totals = [];
153
         $totals = [];
97
 
154
 
98
         foreach ($this->getColumns() as $column) {
155
         foreach ($this->getColumns() as $column) {
99
-            $totals[] = match ($column->getName()) {
156
+            $totals[$column->getName()] = match ($column->getName()) {
100
                 'account_name' => 'Net Earnings',
157
                 'account_name' => 'Net Earnings',
101
                 'net_movement' => $this->report->overallTotal->netMovement ?? '',
158
                 'net_movement' => $this->report->overallTotal->netMovement ?? '',
102
                 default => '',
159
                 default => '',
106
         return $totals;
163
         return $totals;
107
     }
164
     }
108
 
165
 
109
-    public function getSummary(): array
166
+    public function getSummaryOverallTotals(): array
110
     {
167
     {
111
-        $this->calculateTotals();
168
+        $totals = [];
169
+        $columns = $this->getSummaryColumns();
170
+
171
+        foreach ($columns as $column) {
172
+            $totals[$column->getName()] = match ($column->getName()) {
173
+                'account_name' => 'Net Earnings',
174
+                'net_movement' => $this->report->overallTotal->netMovement ?? '',
175
+                default => '',
176
+            };
177
+        }
178
+
179
+        return $totals;
180
+    }
112
 
181
 
182
+    public function getSummary(): array
183
+    {
113
         return [
184
         return [
114
             [
185
             [
115
                 'label' => 'Revenue',
186
                 'label' => 'Revenue',

+ 34
- 0
app/Transformers/SummaryReportTransformer.php Переглянути файл

1
+<?php
2
+
3
+namespace App\Transformers;
4
+
5
+use App\Contracts\HasSummaryReport;
6
+use App\Support\Column;
7
+
8
+abstract class SummaryReportTransformer extends BaseReportTransformer implements HasSummaryReport
9
+{
10
+    /**
11
+     * @return Column[]
12
+     */
13
+    public function getSummaryColumns(): array
14
+    {
15
+        return once(function (): array {
16
+            return collect($this->getColumns())
17
+                ->reject(fn (Column $column) => $column->getName() === 'account_code')
18
+                ->toArray();
19
+        });
20
+    }
21
+
22
+    public function getSummaryHeaders(): array
23
+    {
24
+        return once(function (): array {
25
+            $headers = [];
26
+
27
+            foreach ($this->getSummaryColumns() as $column) {
28
+                $headers[$column->getName()] = $column->getLabel();
29
+            }
30
+
31
+            return $headers;
32
+        });
33
+    }
34
+}

+ 5
- 15
app/Transformers/TrialBalanceReportTransformer.php Переглянути файл

4
 
4
 
5
 use App\DTO\AccountDTO;
5
 use App\DTO\AccountDTO;
6
 use App\DTO\ReportCategoryDTO;
6
 use App\DTO\ReportCategoryDTO;
7
-use App\Support\Column;
8
 
7
 
9
 class TrialBalanceReportTransformer extends BaseReportTransformer
8
 class TrialBalanceReportTransformer extends BaseReportTransformer
10
 {
9
 {
16
         };
15
         };
17
     }
16
     }
18
 
17
 
19
-    public function getHeaders(): array
20
-    {
21
-        return array_map(fn (Column $column) => $column->getLabel(), $this->getColumns());
22
-    }
23
-
24
     /**
18
     /**
25
      * @return ReportCategoryDTO[]
19
      * @return ReportCategoryDTO[]
26
      */
20
      */
32
             // Initialize header with empty strings
26
             // Initialize header with empty strings
33
             $header = [];
27
             $header = [];
34
 
28
 
35
-            foreach ($this->getColumns() as $index => $column) {
36
-                if ($column->getName() === 'account_name') {
37
-                    $header[$index] = $accountCategoryName;
38
-                } else {
39
-                    $header[$index] = '';
40
-                }
29
+            foreach ($this->getColumns() as $column) {
30
+                $header[$column->getName()] = $column->getName() === 'account_name' ? $accountCategoryName : '';
41
             }
31
             }
42
 
32
 
43
             $data = array_map(function (AccountDTO $account) {
33
             $data = array_map(function (AccountDTO $account) {
44
                 $row = [];
34
                 $row = [];
45
 
35
 
46
                 foreach ($this->getColumns() as $column) {
36
                 foreach ($this->getColumns() as $column) {
47
-                    $row[] = match ($column->getName()) {
37
+                    $row[$column->getName()] = match ($column->getName()) {
48
                         'account_code' => $account->accountCode,
38
                         'account_code' => $account->accountCode,
49
                         'account_name' => [
39
                         'account_name' => [
50
                             'name' => $account->accountName,
40
                             'name' => $account->accountName,
64
             $summary = [];
54
             $summary = [];
65
 
55
 
66
             foreach ($this->getColumns() as $column) {
56
             foreach ($this->getColumns() as $column) {
67
-                $summary[] = match ($column->getName()) {
57
+                $summary[$column->getName()] = match ($column->getName()) {
68
                     'account_name' => 'Total ' . $accountCategoryName,
58
                     'account_name' => 'Total ' . $accountCategoryName,
69
                     'debit_balance' => $accountCategory->summary->debitBalance,
59
                     'debit_balance' => $accountCategory->summary->debitBalance,
70
                     'credit_balance' => $accountCategory->summary->creditBalance,
60
                     'credit_balance' => $accountCategory->summary->creditBalance,
87
         $totals = [];
77
         $totals = [];
88
 
78
 
89
         foreach ($this->getColumns() as $column) {
79
         foreach ($this->getColumns() as $column) {
90
-            $totals[] = match ($column->getName()) {
80
+            $totals[$column->getName()] = match ($column->getName()) {
91
                 'account_name' => 'Total for all accounts',
81
                 'account_name' => 'Total for all accounts',
92
                 'debit_balance' => $this->report->overallTotal->debitBalance,
82
                 'debit_balance' => $this->report->overallTotal->debitBalance,
93
                 'credit_balance' => $this->report->overallTotal->creditBalance,
83
                 'credit_balance' => $this->report->overallTotal->creditBalance,

+ 211
- 208
composer.lock
Різницю між файлами не показано, бо вона завелика
Переглянути файл


+ 15
- 13
config/chart-of-accounts.php Переглянути файл

67
                 'description' => 'Accounts that accumulate depreciation of tangible assets and amortization of intangible assets, reflecting the reduction in value over time.',
67
                 'description' => 'Accounts that accumulate depreciation of tangible assets and amortization of intangible assets, reflecting the reduction in value over time.',
68
                 'multi_currency' => false,
68
                 'multi_currency' => false,
69
                 'base_code' => '1900',
69
                 'base_code' => '1900',
70
+                'accounts' => [
71
+                    'Accumulated Depreciation' => [
72
+                        'description' => 'Used to account for the depreciation of fixed assets over time, offsetting assets like equipment or property.',
73
+                    ],
74
+                ],
70
             ],
75
             ],
71
             'Allowances for Receivables' => [
76
             'Allowances for Receivables' => [
72
                 'description' => 'Accounts representing estimated uncollected receivables, used to adjust the value of gross receivables to a realistic collectible amount.',
77
                 'description' => 'Accounts representing estimated uncollected receivables, used to adjust the value of gross receivables to a realistic collectible amount.',
73
                 'multi_currency' => false,
78
                 'multi_currency' => false,
74
                 'base_code' => '1940',
79
                 'base_code' => '1940',
80
+                'accounts' => [
81
+                    'Allowance for Doubtful Accounts' => [
82
+                        'description' => 'Used to account for potential bad debts that may not be collectable, offsetting receivables.',
83
+                    ],
84
+                ],
75
             ],
85
             ],
76
             'Valuation Adjustments' => [
86
             'Valuation Adjustments' => [
77
                 'description' => 'Accounts used to record adjustments in asset values due to impairments, market changes, or other factors affecting their recoverable amount.',
87
                 'description' => 'Accounts used to record adjustments in asset values due to impairments, market changes, or other factors affecting their recoverable amount.',
154
                     'Owner\'s Investment' => [
164
                     'Owner\'s Investment' => [
155
                         'description' => 'The amount of money invested by the owner(s) or shareholders to start or expand the business.',
165
                         'description' => 'The amount of money invested by the owner(s) or shareholders to start or expand the business.',
156
                     ],
166
                     ],
157
-                    'Owner\'s Drawings' => [
158
-                        'description' => 'The amount of money withdrawn by the owner(s) or shareholders from the business for personal use.',
159
-                    ],
160
-                ],
161
-            ],
162
-            'Retained Earnings: Profit' => [
163
-                'description' => 'Cumulative profits retained in the business and not distributed as dividends. Indicates the company\'s financial health and profit-generating ability.',
164
-                'multi_currency' => false,
165
-                'base_code' => '3100',
166
-                'accounts' => [
167
-                    'Owner\'s Equity' => [
168
-                        'description' => 'Owner\'s equity is what remains after you subtract business liabilities from business assets. In other words, it\'s what\'s left over for you if you sell all your assets and pay all your debts.',
169
-                    ],
170
                 ],
167
                 ],
171
             ],
168
             ],
172
         ],
169
         ],
175
                 'description' => 'Equity that is deducted from gross equity to arrive at net equity. This includes treasury stock, which is stock that has been repurchased by the company.',
172
                 'description' => 'Equity that is deducted from gross equity to arrive at net equity. This includes treasury stock, which is stock that has been repurchased by the company.',
176
                 'multi_currency' => false,
173
                 'multi_currency' => false,
177
                 'base_code' => '3900',
174
                 'base_code' => '3900',
175
+                'accounts' => [
176
+                    'Owner\'s Drawings' => [
177
+                        'description' => 'The amount of money withdrawn by the owner(s) or shareholders from the business for personal use, reducing equity.',
178
+                    ],
179
+                ],
178
             ],
180
             ],
179
         ],
181
         ],
180
         'operating_revenue' => [
182
         'operating_revenue' => [

+ 3
- 4
database/factories/Accounting/TransactionFactory.php Переглянути файл

57
 
57
 
58
             $account = Account::where('category', $this->faker->randomElement($associatedAccountTypes))
58
             $account = Account::where('category', $this->faker->randomElement($associatedAccountTypes))
59
                 ->where('company_id', $company->id)
59
                 ->where('company_id', $company->id)
60
-                ->where('id', '<>', $accountIdForBankAccount)
60
+                ->whereKeyNot($accountIdForBankAccount)
61
                 ->inRandomOrder()
61
                 ->inRandomOrder()
62
                 ->first();
62
                 ->first();
63
 
63
 
64
-            // If no matching account is found, use a fallback
65
             if (! $account) {
64
             if (! $account) {
66
                 $account = Account::where('company_id', $company->id)
65
                 $account = Account::where('company_id', $company->id)
67
-                    ->where('id', '<>', $accountIdForBankAccount)
66
+                    ->whereKeyNot($accountIdForBankAccount)
68
                     ->inRandomOrder()
67
                     ->inRandomOrder()
69
-                    ->firstOrFail(); // Ensure there is at least some account
68
+                    ->firstOrFail();
70
             }
69
             }
71
 
70
 
72
             return [
71
             return [

+ 5
- 0
database/factories/Setting/CompanyDefaultFactory.php Переглянути файл

72
     {
72
     {
73
         return Currency::factory()->forCurrency($currencyCode)->createQuietly([
73
         return Currency::factory()->forCurrency($currencyCode)->createQuietly([
74
             'company_id' => $company->id,
74
             'company_id' => $company->id,
75
+            'enabled' => true,
75
             'created_by' => $user->id,
76
             'created_by' => $user->id,
76
             'updated_by' => $user->id,
77
             'updated_by' => $user->id,
77
         ]);
78
         ]);
81
     {
82
     {
82
         return Tax::factory()->salesTax()->createQuietly([
83
         return Tax::factory()->salesTax()->createQuietly([
83
             'company_id' => $company->id,
84
             'company_id' => $company->id,
85
+            'enabled' => true,
84
             'created_by' => $user->id,
86
             'created_by' => $user->id,
85
             'updated_by' => $user->id,
87
             'updated_by' => $user->id,
86
         ]);
88
         ]);
90
     {
92
     {
91
         return Tax::factory()->purchaseTax()->createQuietly([
93
         return Tax::factory()->purchaseTax()->createQuietly([
92
             'company_id' => $company->id,
94
             'company_id' => $company->id,
95
+            'enabled' => true,
93
             'created_by' => $user->id,
96
             'created_by' => $user->id,
94
             'updated_by' => $user->id,
97
             'updated_by' => $user->id,
95
         ]);
98
         ]);
99
     {
102
     {
100
         return Discount::factory()->salesDiscount()->createQuietly([
103
         return Discount::factory()->salesDiscount()->createQuietly([
101
             'company_id' => $company->id,
104
             'company_id' => $company->id,
105
+            'enabled' => true,
102
             'created_by' => $user->id,
106
             'created_by' => $user->id,
103
             'updated_by' => $user->id,
107
             'updated_by' => $user->id,
104
         ]);
108
         ]);
108
     {
112
     {
109
         return Discount::factory()->purchaseDiscount()->createQuietly([
113
         return Discount::factory()->purchaseDiscount()->createQuietly([
110
             'company_id' => $company->id,
114
             'company_id' => $company->id,
115
+            'enabled' => true,
111
             'created_by' => $user->id,
116
             'created_by' => $user->id,
112
             'updated_by' => $user->id,
117
             'updated_by' => $user->id,
113
         ]);
118
         ]);

+ 2
- 2
database/factories/Setting/CurrencyFactory.php Переглянути файл

33
             'symbol_first' => $defaultCurrency->isSymbolFirst(),
33
             'symbol_first' => $defaultCurrency->isSymbolFirst(),
34
             'decimal_mark' => $defaultCurrency->getDecimalMark(),
34
             'decimal_mark' => $defaultCurrency->getDecimalMark(),
35
             'thousands_separator' => $defaultCurrency->getThousandsSeparator(),
35
             'thousands_separator' => $defaultCurrency->getThousandsSeparator(),
36
-            'enabled' => true,
36
+            'enabled' => false,
37
         ];
37
         ];
38
     }
38
     }
39
 
39
 
53
             'symbol_first' => $currency->isSymbolFirst(),
53
             'symbol_first' => $currency->isSymbolFirst(),
54
             'decimal_mark' => $currency->getDecimalMark(),
54
             'decimal_mark' => $currency->getDecimalMark(),
55
             'thousands_separator' => $currency->getThousandsSeparator(),
55
             'thousands_separator' => $currency->getThousandsSeparator(),
56
-            'enabled' => true,
56
+            'enabled' => false,
57
         ]);
57
         ]);
58
     }
58
     }
59
 }
59
 }

+ 28
- 0
database/migrations/2024_10_13_163049_update_posted_at_column_in_transactions_table.php Переглянути файл

1
+<?php
2
+
3
+use Illuminate\Database\Migrations\Migration;
4
+use Illuminate\Database\Schema\Blueprint;
5
+use Illuminate\Support\Facades\Schema;
6
+
7
+return new class extends Migration
8
+{
9
+    /**
10
+     * Run the migrations.
11
+     */
12
+    public function up(): void
13
+    {
14
+        Schema::table('transactions', function (Blueprint $table) {
15
+            $table->date('posted_at')->change();
16
+        });
17
+    }
18
+
19
+    /**
20
+     * Reverse the migrations.
21
+     */
22
+    public function down(): void
23
+    {
24
+        Schema::table('transactions', function (Blueprint $table) {
25
+            $table->dateTime('posted_at')->change();
26
+        });
27
+    }
28
+};

+ 27
- 27
package-lock.json Переглянути файл

955
             }
955
             }
956
         },
956
         },
957
         "node_modules/browserslist": {
957
         "node_modules/browserslist": {
958
-            "version": "4.24.0",
959
-            "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz",
960
-            "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==",
958
+            "version": "4.24.2",
959
+            "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz",
960
+            "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==",
961
             "dev": true,
961
             "dev": true,
962
             "funding": [
962
             "funding": [
963
                 {
963
                 {
975
             ],
975
             ],
976
             "license": "MIT",
976
             "license": "MIT",
977
             "dependencies": {
977
             "dependencies": {
978
-                "caniuse-lite": "^1.0.30001663",
979
-                "electron-to-chromium": "^1.5.28",
978
+                "caniuse-lite": "^1.0.30001669",
979
+                "electron-to-chromium": "^1.5.41",
980
                 "node-releases": "^2.0.18",
980
                 "node-releases": "^2.0.18",
981
-                "update-browserslist-db": "^1.1.0"
981
+                "update-browserslist-db": "^1.1.1"
982
             },
982
             },
983
             "bin": {
983
             "bin": {
984
                 "browserslist": "cli.js"
984
                 "browserslist": "cli.js"
998
             }
998
             }
999
         },
999
         },
1000
         "node_modules/caniuse-lite": {
1000
         "node_modules/caniuse-lite": {
1001
-            "version": "1.0.30001667",
1002
-            "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001667.tgz",
1003
-            "integrity": "sha512-7LTwJjcRkzKFmtqGsibMeuXmvFDfZq/nzIjnmgCGzKKRVzjD72selLDK1oPF/Oxzmt4fNcPvTDvGqSDG4tCALw==",
1001
+            "version": "1.0.30001669",
1002
+            "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001669.tgz",
1003
+            "integrity": "sha512-DlWzFDJqstqtIVx1zeSpIMLjunf5SmwOw0N2Ck/QSQdS8PLS4+9HrLaYei4w8BIAL7IB/UEDu889d8vhCTPA0w==",
1004
             "dev": true,
1004
             "dev": true,
1005
             "funding": [
1005
             "funding": [
1006
                 {
1006
                 {
1159
             "license": "MIT"
1159
             "license": "MIT"
1160
         },
1160
         },
1161
         "node_modules/electron-to-chromium": {
1161
         "node_modules/electron-to-chromium": {
1162
-            "version": "1.5.33",
1163
-            "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.33.tgz",
1164
-            "integrity": "sha512-+cYTcFB1QqD4j4LegwLfpCNxifb6dDFUAwk6RsLusCwIaZI6or2f+q8rs5tTB2YC53HhOlIbEaqHMAAC8IOIwA==",
1162
+            "version": "1.5.41",
1163
+            "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.41.tgz",
1164
+            "integrity": "sha512-dfdv/2xNjX0P8Vzme4cfzHqnPm5xsZXwsolTYr0eyW18IUmNyG08vL+fttvinTfhKfIKdRoqkDIC9e9iWQCNYQ==",
1165
             "dev": true,
1165
             "dev": true,
1166
             "license": "ISC"
1166
             "license": "ISC"
1167
         },
1167
         },
1313
             }
1313
             }
1314
         },
1314
         },
1315
         "node_modules/form-data": {
1315
         "node_modules/form-data": {
1316
-            "version": "4.0.0",
1317
-            "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
1318
-            "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
1316
+            "version": "4.0.1",
1317
+            "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
1318
+            "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
1319
             "dev": true,
1319
             "dev": true,
1320
             "license": "MIT",
1320
             "license": "MIT",
1321
             "dependencies": {
1321
             "dependencies": {
1786
             }
1786
             }
1787
         },
1787
         },
1788
         "node_modules/picocolors": {
1788
         "node_modules/picocolors": {
1789
-            "version": "1.1.0",
1790
-            "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
1791
-            "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
1789
+            "version": "1.1.1",
1790
+            "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
1791
+            "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
1792
             "dev": true,
1792
             "dev": true,
1793
             "license": "ISC"
1793
             "license": "ISC"
1794
         },
1794
         },
2417
             }
2417
             }
2418
         },
2418
         },
2419
         "node_modules/tailwindcss": {
2419
         "node_modules/tailwindcss": {
2420
-            "version": "3.4.13",
2421
-            "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.13.tgz",
2422
-            "integrity": "sha512-KqjHOJKogOUt5Bs752ykCeiwvi0fKVkr5oqsFNt/8px/tA8scFPIlkygsf6jXrfCqGHz7VflA6+yytWuM+XhFw==",
2420
+            "version": "3.4.14",
2421
+            "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.14.tgz",
2422
+            "integrity": "sha512-IcSvOcTRcUtQQ7ILQL5quRDg7Xs93PdJEk1ZLbhhvJc7uj/OAhYOnruEiwnGgBvUtaUAJ8/mhSw1o8L2jCiENA==",
2423
             "dev": true,
2423
             "dev": true,
2424
             "license": "MIT",
2424
             "license": "MIT",
2425
             "dependencies": {
2425
             "dependencies": {
2550
             "license": "MIT"
2550
             "license": "MIT"
2551
         },
2551
         },
2552
         "node_modules/vite": {
2552
         "node_modules/vite": {
2553
-            "version": "5.4.8",
2554
-            "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz",
2555
-            "integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==",
2553
+            "version": "5.4.9",
2554
+            "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.9.tgz",
2555
+            "integrity": "sha512-20OVpJHh0PAM0oSOELa5GaZNWeDjcAvQjGXy2Uyr+Tp+/D2/Hdz6NLgpJLsarPTA2QJ6v8mX2P1ZfbsSKvdMkg==",
2556
             "dev": true,
2556
             "dev": true,
2557
             "license": "MIT",
2557
             "license": "MIT",
2558
             "dependencies": {
2558
             "dependencies": {
2735
             }
2735
             }
2736
         },
2736
         },
2737
         "node_modules/yaml": {
2737
         "node_modules/yaml": {
2738
-            "version": "2.5.1",
2739
-            "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz",
2740
-            "integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==",
2738
+            "version": "2.6.0",
2739
+            "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz",
2740
+            "integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==",
2741
             "dev": true,
2741
             "dev": true,
2742
             "license": "ISC",
2742
             "license": "ISC",
2743
             "bin": {
2743
             "bin": {

+ 1
- 5
resources/css/filament/company/theme.css Переглянути файл

107
 }
107
 }
108
 
108
 
109
 .es-table__header-ctn, .es-table__footer-ctn {
109
 .es-table__header-ctn, .es-table__footer-ctn {
110
-    @apply divide-y divide-gray-200 dark:divide-white/10 h-12;
111
-}
112
-
113
-.es-table__row {
114
-    @apply [@media(hover:hover)]:transition [@media(hover:hover)]:duration-75 hover:bg-gray-50 dark:hover:bg-white/5;
110
+    @apply divide-y divide-gray-200 dark:divide-white/10 min-h-12;
115
 }
111
 }
116
 
112
 
117
 .es-table .es-table__rowgroup td:first-child {
113
 .es-table .es-table__rowgroup td:first-child {

+ 3
- 3
resources/views/components/company/reports/account-transactions-report-pdf.blade.php Переглянути файл

49
 
49
 
50
         .company-name {
50
         .company-name {
51
             font-size: 1.125rem;
51
             font-size: 1.125rem;
52
-            font-weight: 600;
52
+            font-weight: bold;
53
         }
53
         }
54
 
54
 
55
         .date-range {
55
         .date-range {
71
 
71
 
72
         .category-header-row > td {
72
         .category-header-row > td {
73
             background-color: #f3f4f6; /* Gray background for category names */
73
             background-color: #f3f4f6; /* Gray background for category names */
74
-            font-weight: 600;
74
+            font-weight: bold;
75
         }
75
         }
76
 
76
 
77
         .table-body tr {
77
         .table-body tr {
84
 
84
 
85
         .category-summary-row > td,
85
         .category-summary-row > td,
86
         .table-footer-row > td {
86
         .table-footer-row > td {
87
-            font-weight: 600;
87
+            font-weight: bold;
88
             background-color: #ffffff; /* White background for footer */
88
             background-color: #ffffff; /* White background for footer */
89
         }
89
         }
90
     </style>
90
     </style>

+ 48
- 3
resources/views/components/company/reports/report-pdf.blade.php Переглянути файл

49
 
49
 
50
         .company-name {
50
         .company-name {
51
             font-size: 1.125rem;
51
             font-size: 1.125rem;
52
-            font-weight: 600;
52
+            font-weight: bold;
53
         }
53
         }
54
 
54
 
55
         .date-range {
55
         .date-range {
69
             border-bottom: 1px solid #d1d5db; /* Gray border for all rows */
69
             border-bottom: 1px solid #d1d5db; /* Gray border for all rows */
70
         }
70
         }
71
 
71
 
72
-        .category-header-row > td {
72
+        .category-header-row > td,
73
+        .type-header-row > td {
73
             background-color: #f3f4f6; /* Gray background for category names */
74
             background-color: #f3f4f6; /* Gray background for category names */
74
-            font-weight: 600;
75
+            font-weight: bold;
76
+        }
77
+
78
+        .type-header-row > td,
79
+        .type-data-row > td,
80
+        .type-summary-row > td {
81
+            padding-left: 1.5rem; /* Indentation for type rows */
75
         }
82
         }
76
 
83
 
77
         .table-body tr {
84
         .table-body tr {
83
         }
90
         }
84
 
91
 
85
         .category-summary-row > td,
92
         .category-summary-row > td,
93
+        .type-summary-row > td,
86
         .table-footer-row > td {
94
         .table-footer-row > td {
87
             font-weight: bold;
95
             font-weight: bold;
88
             background-color: #ffffff; /* White background for footer */
96
             background-color: #ffffff; /* White background for footer */
131
                 @endforeach
139
                 @endforeach
132
             </tr>
140
             </tr>
133
         @endforeach
141
         @endforeach
142
+
143
+        <!-- Category Types -->
144
+        @foreach($category->types ?? [] as $type)
145
+            <!-- Type Header -->
146
+            <tr class="type-header-row">
147
+                @foreach($type->header as $index => $header)
148
+                    <td class="{{ $report->getAlignmentClass($index) }}">
149
+                        {{ $header }}
150
+                    </td>
151
+                @endforeach
152
+            </tr>
153
+
154
+            <!-- Type Data -->
155
+            @foreach($type->data as $typeRow)
156
+                <tr class="type-data-row">
157
+                    @foreach($typeRow as $index => $cell)
158
+                        <td class="{{ $report->getAlignmentClass($index) }} {{ $index === 'account_name' ? 'whitespace-normal' : 'whitespace-nowrap' }}">
159
+                            @if(is_array($cell) && isset($cell['name']))
160
+                                {{ $cell['name'] }}
161
+                            @else
162
+                                {{ $cell }}
163
+                            @endif
164
+                        </td>
165
+                    @endforeach
166
+                </tr>
167
+            @endforeach
168
+
169
+            <!-- Type Summary -->
170
+            <tr class="type-summary-row">
171
+                @foreach($type->summary as $index => $cell)
172
+                    <td class="{{ $report->getAlignmentClass($index) }}">
173
+                        {{ $cell }}
174
+                    </td>
175
+                @endforeach
176
+            </tr>
177
+        @endforeach
178
+
134
         <tr class="category-summary-row">
179
         <tr class="category-summary-row">
135
             @foreach($category->summary as $index => $cell)
180
             @foreach($category->summary as $index => $cell)
136
                 <td class="{{ $report->getAlignmentClass($index) }}">
181
                 <td class="{{ $report->getAlignmentClass($index) }}">

+ 20
- 0
resources/views/components/company/tables/category-header.blade.php Переглянути файл

1
+@props([
2
+    'categoryHeaders',
3
+    'alignmentClass' => null,
4
+])
5
+
6
+
7
+<tr class="bg-gray-50 dark:bg-white/5">
8
+    @foreach($categoryHeaders as $index => $header)
9
+        <th
10
+            @class([
11
+                'px-3 py-3.5 sm:first-of-type:ps-6 sm:last-of-type:pe-6',
12
+                $alignmentClass($index) => $alignmentClass,
13
+            ])
14
+        >
15
+            <span class="text-sm font-semibold leading-6 text-gray-950 dark:text-white">
16
+                {{ $header }}
17
+            </span>
18
+        </th>
19
+    @endforeach
20
+</tr>

+ 23
- 0
resources/views/components/company/tables/cell.blade.php Переглянути файл

1
+@props([
2
+    'alignmentClass',
3
+    'indent' => false,
4
+    'bold' => false,
5
+])
6
+
7
+<td
8
+    @class([
9
+        $alignmentClass,
10
+        'last-of-type:pe-1 sm:last-of-type:pe-3',
11
+        'ps-3 sm:ps-6' => $indent,
12
+        'p-0 first-of-type:ps-1 sm:first-of-type:ps-3' => ! $indent,
13
+    ])
14
+>
15
+    <div
16
+        @class([
17
+            'px-3 py-4 text-sm leading-6 text-gray-950 dark:text-white',
18
+            'font-semibold' => $bold,
19
+        ])
20
+    >
21
+        {{ $slot }}
22
+    </div>
23
+</td>

+ 24
- 0
resources/views/components/company/tables/container.blade.php Переглянути файл

1
+@props([
2
+    'reportLoaded' => false,
3
+])
4
+
5
+<x-filament-tables::container>
6
+    <div class="es-table__header-ctn"></div>
7
+    <div
8
+        class="relative divide-y divide-gray-200 overflow-x-auto dark:divide-white/10 dark:border-t-white/10 min-h-64">
9
+        <div wire:init="applyFilters">
10
+            <div wire:loading.class="flex items-center justify-center w-full h-full absolute inset-0 z-10">
11
+                <div wire:loading wire:target="applyFilters">
12
+                    <x-filament::loading-indicator class="p-6 text-primary-700 dark:text-primary-300"/>
13
+                </div>
14
+            </div>
15
+
16
+            @if($reportLoaded)
17
+                <div wire:loading.remove wire:target="applyFilters">
18
+                    {{ $slot }}
19
+                </div>
20
+            @endif
21
+        </div>
22
+    </div>
23
+    <div class="es-table__footer-ctn border-t border-gray-200"></div>
24
+</x-filament-tables::container>

+ 15
- 0
resources/views/components/company/tables/footer.blade.php Переглянути файл

1
+@props(['totals', 'alignmentClass'])
2
+
3
+@if(!empty($totals))
4
+    <tfoot>
5
+    <tr class="bg-gray-50 dark:bg-white/5">
6
+        @foreach($totals as $totalIndex => $totalCell)
7
+            <x-filament-tables::cell class="{{ $alignmentClass($totalIndex) }}">
8
+                <div class="px-3 py-3 text-sm leading-6 font-semibold text-gray-950 dark:text-white">
9
+                    {{ $totalCell }}
10
+                </div>
11
+            </x-filament-tables::cell>
12
+        @endforeach
13
+    </tr>
14
+    </tfoot>
15
+@endif

+ 16
- 0
resources/views/components/company/tables/header.blade.php Переглянути файл

1
+@props([
2
+    'headers',
3
+    'alignmentClass',
4
+])
5
+
6
+<thead class="divide-y divide-gray-200 dark:divide-white/5">
7
+<tr class="bg-gray-50 dark:bg-white/5">
8
+    @foreach($headers as $headerIndex => $headerCell)
9
+        <th class="px-3 py-3.5 sm:first-of-type:ps-6 sm:last-of-type:pe-6 {{ $alignmentClass($headerIndex) }}">
10
+            <span class="text-sm font-semibold leading-6 text-gray-950 dark:text-white">
11
+                {{ $headerCell }}
12
+            </span>
13
+        </th>
14
+    @endforeach
15
+</tr>
16
+</thead>

+ 10
- 23
resources/views/components/company/tables/reports/account-transactions.blade.php Переглянути файл

1
 <table class="w-full table-auto divide-y divide-gray-200 dark:divide-white/5">
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 }}"
6
-                class="px-3 py-3.5 sm:first-of-type:ps-6 sm:last-of-type:pe-6 {{ $report->getAlignmentClass($index) }}">
7
-                    <span class="text-sm font-semibold text-gray-950 dark:text-white">
8
-                        {{ $header }}
9
-                    </span>
10
-            </th>
11
-        @endforeach
12
-    </tr>
13
-    </thead>
2
+    <x-company.tables.header :headers="$report->getHeaders()" :alignmentClass="[$report, 'getAlignmentClass']"/>
14
     @foreach($report->getCategories() as $categoryIndex => $category)
3
     @foreach($report->getCategories() as $categoryIndex => $category)
15
-        <tbody wire:key="category-{{ $categoryIndex }}"
16
-               class="divide-y divide-gray-200 whitespace-nowrap dark:divide-white/5">
4
+        <tbody class="divide-y divide-gray-200 whitespace-nowrap dark:divide-white/5">
17
         <!-- Category Header -->
5
         <!-- Category Header -->
18
         <tr class="bg-gray-50 dark:bg-white/5">
6
         <tr class="bg-gray-50 dark:bg-white/5">
19
-            <x-filament-tables::cell colspan="{{ count($report->getHeaders()) }}" class="text-left">
20
-                <div class="px-3 py-2">
7
+            <x-filament-tables::cell tag="th" colspan="{{ count($report->getHeaders()) }}" class="text-left">
8
+                <div class="px-3 py-3.5">
21
                     @foreach ($category->header as $headerRow)
9
                     @foreach ($category->header as $headerRow)
22
                         <div
10
                         <div
23
-                            class="text-sm {{ $loop->first ? 'font-semibold text-gray-950 dark:text-white' : 'text-gray-500 dark:text-white/50' }}">
11
+                            class="text-sm {{ $loop->first ? 'font-semibold text-gray-950 dark:text-white' : 'font-normal text-gray-500 dark:text-white/50' }}">
24
                             @foreach ($headerRow as $headerValue)
12
                             @foreach ($headerRow as $headerValue)
25
                                 @if (!empty($headerValue))
13
                                 @if (!empty($headerValue))
26
                                     {{ $headerValue }}
14
                                     {{ $headerValue }}
33
         </tr>
21
         </tr>
34
         <!-- Transactions Data -->
22
         <!-- Transactions Data -->
35
         @foreach($category->data as $dataIndex => $transaction)
23
         @foreach($category->data as $dataIndex => $transaction)
36
-            <tr wire:key="category-{{ $categoryIndex }}-data-{{ $dataIndex }}"
24
+            <tr
37
                 @class([
25
                 @class([
38
                     'bg-gray-50 dark:bg-white/5' => $loop->first || $loop->last || $loop->remaining === 1,
26
                     'bg-gray-50 dark:bg-white/5' => $loop->first || $loop->last || $loop->remaining === 1,
39
                 ])
27
                 ])
40
             >
28
             >
41
                 @foreach($transaction as $cellIndex => $cell)
29
                 @foreach($transaction as $cellIndex => $cell)
42
                     <x-filament-tables::cell
30
                     <x-filament-tables::cell
43
-                        wire:key="category-{{ $categoryIndex }}-data-{{ $dataIndex }}-cell-{{ $cellIndex }}"
44
                         @class([
31
                         @class([
45
                            $report->getAlignmentClass($cellIndex),
32
                            $report->getAlignmentClass($cellIndex),
46
                            'whitespace-normal' => $cellIndex === 1,
33
                            'whitespace-normal' => $cellIndex === 1,
80
         @endforeach
67
         @endforeach
81
         <!-- Spacer Row -->
68
         <!-- Spacer Row -->
82
         @unless($loop->last)
69
         @unless($loop->last)
83
-            <tr wire:key="category-{{ $categoryIndex }}-spacer">
84
-                <x-filament-tables::cell colspan="{{ count($report->getHeaders()) }}">
85
-                    <div class="px-3 py-2 leading-6 invisible">Hidden Text</div>
86
-                </x-filament-tables::cell>
70
+            <tr>
71
+                <td colspan="{{ count($report->getHeaders()) }}">
72
+                    <div class="min-h-12"></div>
73
+                </td>
87
             </tr>
74
             </tr>
88
         @endunless
75
         @endunless
89
         </tbody>
76
         </tbody>

+ 31
- 0
resources/views/components/company/tables/reports/balance-sheet-summary.blade.php Переглянути файл

1
+<table class="w-full table-auto divide-y divide-gray-200 dark:divide-white/5">
2
+    <x-company.tables.header :headers="$report->getSummaryHeaders()" :alignment-class="[$report, 'getAlignmentClass']"/>
3
+    @foreach($report->getSummaryCategories() as $accountCategory)
4
+        <tbody class="divide-y divide-gray-200 whitespace-nowrap dark:divide-white/5">
5
+        <x-company.tables.category-header :category-headers="$accountCategory->header"
6
+                                          :alignment-class="[$report, 'getAlignmentClass']"/>
7
+        @foreach($accountCategory->types as $accountType)
8
+            <tr>
9
+                @foreach($accountType->summary as $accountTypeSummaryIndex => $accountTypeSummaryCell)
10
+                    <x-company.tables.cell :alignment-class="$report->getAlignmentClass($accountTypeSummaryIndex)">
11
+                        {{ $accountTypeSummaryCell }}
12
+                    </x-company.tables.cell>
13
+                @endforeach
14
+            </tr>
15
+        @endforeach
16
+        <tr>
17
+            @foreach($accountCategory->summary as $accountCategorySummaryIndex => $accountCategorySummaryCell)
18
+                <x-company.tables.cell :alignment-class="$report->getAlignmentClass($accountCategorySummaryIndex)"
19
+                                       bold="true">
20
+                    {{ $accountCategorySummaryCell }}
21
+                </x-company.tables.cell>
22
+            @endforeach
23
+        </tr>
24
+        <tr>
25
+            <td colspan="{{ count($report->getSummaryHeaders()) }}">
26
+                <div class="min-h-12"></div>
27
+            </td>
28
+        </tr>
29
+        </tbody>
30
+    @endforeach
31
+</table>

+ 131
- 0
resources/views/components/company/tables/reports/balance-sheet.blade.php Переглянути файл

1
+<table class="w-full table-auto divide-y divide-gray-200 dark:divide-white/5">
2
+    <x-company.tables.header :headers="$report->getHeaders()" :alignment-class="[$report, 'getAlignmentClass']"/>
3
+    @foreach($report->getCategories() as $accountCategory)
4
+        <tbody class="divide-y divide-gray-200 whitespace-nowrap dark:divide-white/5">
5
+        <x-company.tables.category-header :category-headers="$accountCategory->header"
6
+                                          :alignment-class="[$report, 'getAlignmentClass']"/>
7
+        @foreach($accountCategory->data as $categoryAccount)
8
+            <tr>
9
+                @foreach($categoryAccount as $accountIndex => $categoryAccountCell)
10
+                    <x-company.tables.cell :alignment-class="$report->getAlignmentClass($accountIndex)"
11
+                                           indent="true">
12
+                        @if(is_array($categoryAccountCell) && isset($categoryAccountCell['name']))
13
+                            @if($categoryAccountCell['name'] === 'Retained Earnings' && isset($categoryAccountCell['start_date']) && isset($categoryAccountCell['end_date']))
14
+                                <x-filament::link
15
+                                    color="primary"
16
+                                    target="_blank"
17
+                                    icon="heroicon-o-arrow-top-right-on-square"
18
+                                    :icon-position="\Filament\Support\Enums\IconPosition::After"
19
+                                    :icon-size="\Filament\Support\Enums\IconSize::Small"
20
+                                    href="{{ \App\Filament\Company\Pages\Reports\IncomeStatement::getUrl([
21
+                                            'startDate' => $categoryAccountCell['start_date'],
22
+                                            'endDate' => $categoryAccountCell['end_date']
23
+                                        ]) }}"
24
+                                >
25
+                                    {{ $categoryAccountCell['name'] }}
26
+                                </x-filament::link>
27
+                            @elseif(isset($categoryAccountCell['id']) && isset($categoryAccountCell['start_date']) && isset($categoryAccountCell['end_date']))
28
+                                <x-filament::link
29
+                                    color="primary"
30
+                                    target="_blank"
31
+                                    icon="heroicon-o-arrow-top-right-on-square"
32
+                                    :icon-position="\Filament\Support\Enums\IconPosition::After"
33
+                                    :icon-size="\Filament\Support\Enums\IconSize::Small"
34
+                                    href="{{ \App\Filament\Company\Pages\Reports\AccountTransactions::getUrl([
35
+                                            'startDate' => $categoryAccountCell['start_date'],
36
+                                            'endDate' => $categoryAccountCell['end_date'],
37
+                                            'selectedAccount' => $categoryAccountCell['id']
38
+                                        ]) }}"
39
+                                >
40
+                                    {{ $categoryAccountCell['name'] }}
41
+                                </x-filament::link>
42
+                            @else
43
+                                {{ $categoryAccountCell['name'] }}
44
+                            @endif
45
+                        @else
46
+                            {{ $categoryAccountCell }}
47
+                        @endif
48
+                    </x-company.tables.cell>
49
+                @endforeach
50
+            </tr>
51
+        @endforeach
52
+        @foreach($accountCategory->types as $accountType)
53
+            <tr class="bg-gray-50 dark:bg-white/5">
54
+                @foreach($accountType->header as $accountTypeHeaderIndex => $accountTypeHeaderCell)
55
+                    <x-company.tables.cell :alignment-class="$report->getAlignmentClass($accountTypeHeaderIndex)"
56
+                                           indent="true" bold="true">
57
+                        {{ $accountTypeHeaderCell }}
58
+                    </x-company.tables.cell>
59
+                @endforeach
60
+            </tr>
61
+            @foreach($accountType->data as $typeAccount)
62
+                <tr>
63
+                    @foreach($typeAccount as $accountIndex => $typeAccountCell)
64
+                        <x-company.tables.cell :alignment-class="$report->getAlignmentClass($accountIndex)"
65
+                                               indent="true">
66
+                            @if(is_array($typeAccountCell) && isset($typeAccountCell['name']))
67
+                                @if($typeAccountCell['name'] === 'Retained Earnings' && isset($typeAccountCell['start_date']) && isset($typeAccountCell['end_date']))
68
+                                    <x-filament::link
69
+                                        color="primary"
70
+                                        target="_blank"
71
+                                        icon="heroicon-o-arrow-top-right-on-square"
72
+                                        :icon-position="\Filament\Support\Enums\IconPosition::After"
73
+                                        :icon-size="\Filament\Support\Enums\IconSize::Small"
74
+                                        href="{{ \App\Filament\Company\Pages\Reports\IncomeStatement::getUrl([
75
+                                            'startDate' => $typeAccountCell['start_date'],
76
+                                            'endDate' => $typeAccountCell['end_date']
77
+                                        ]) }}"
78
+                                    >
79
+                                        {{ $typeAccountCell['name'] }}
80
+                                    </x-filament::link>
81
+                                @elseif(isset($typeAccountCell['id']) && isset($typeAccountCell['start_date']) && isset($typeAccountCell['end_date']))
82
+                                    <x-filament::link
83
+                                        color="primary"
84
+                                        target="_blank"
85
+                                        icon="heroicon-o-arrow-top-right-on-square"
86
+                                        :icon-position="\Filament\Support\Enums\IconPosition::After"
87
+                                        :icon-size="\Filament\Support\Enums\IconSize::Small"
88
+                                        href="{{ \App\Filament\Company\Pages\Reports\AccountTransactions::getUrl([
89
+                                            'startDate' => $typeAccountCell['start_date'],
90
+                                            'endDate' => $typeAccountCell['end_date'],
91
+                                            'selectedAccount' => $typeAccountCell['id']
92
+                                        ]) }}"
93
+                                    >
94
+                                        {{ $typeAccountCell['name'] }}
95
+                                    </x-filament::link>
96
+                                @else
97
+                                    {{ $typeAccountCell['name'] }}
98
+                                @endif
99
+                            @else
100
+                                {{ $typeAccountCell }}
101
+                            @endif
102
+                        </x-company.tables.cell>
103
+                    @endforeach
104
+                </tr>
105
+            @endforeach
106
+            <tr>
107
+                @foreach($accountType->summary as $accountTypeSummaryIndex => $accountTypeSummaryCell)
108
+                    <x-company.tables.cell :alignment-class="$report->getAlignmentClass($accountTypeSummaryIndex)"
109
+                                           indent="true" bold="true">
110
+                        {{ $accountTypeSummaryCell }}
111
+                    </x-company.tables.cell>
112
+                @endforeach
113
+            </tr>
114
+        @endforeach
115
+        <tr>
116
+            @foreach($accountCategory->summary as $accountCategorySummaryIndex => $accountCategorySummaryCell)
117
+                <x-company.tables.cell :alignment-class="$report->getAlignmentClass($accountCategorySummaryIndex)"
118
+                                       bold="true">
119
+                    {{ $accountCategorySummaryCell }}
120
+                </x-company.tables.cell>
121
+            @endforeach
122
+        </tr>
123
+        <tr>
124
+            <td colspan="{{ count($report->getHeaders()) }}">
125
+                <div class="min-h-12"></div>
126
+            </td>
127
+        </tr>
128
+        </tbody>
129
+    @endforeach
130
+    <x-company.tables.footer :totals="$report->getOverallTotals()" :alignment-class="[$report, 'getAlignmentClass']"/>
131
+</table>

+ 49
- 79
resources/views/components/company/tables/reports/detailed-report.blade.php Переглянути файл

1
 <table class="w-full table-auto divide-y divide-gray-200 dark:divide-white/5">
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 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)
2
+    <x-company.tables.header :headers="$report->getHeaders()" :alignment-class="[$report, 'getAlignmentClass']"/>
3
+    @foreach($report->getCategories() as $accountCategory)
14
         <tbody class="divide-y divide-gray-200 whitespace-nowrap dark:divide-white/5">
4
         <tbody 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 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)
5
+        <x-company.tables.category-header :category-headers="$accountCategory->header"
6
+                                          :alignment-class="[$report, 'getAlignmentClass']"/>
7
+        @foreach($accountCategory->data as $categoryAccount)
25
             <tr>
8
             <tr>
26
-                @foreach($account as $cellIndex => $cell)
27
-                    <x-filament-tables::cell class="{{ $report->getAlignmentClass($cellIndex) }}">
28
-                        <div class="px-3 py-4 text-sm leading-6 text-gray-950 dark:text-white">
29
-                            @if(is_array($cell) && isset($cell['name']))
30
-                                @if($cell['name'] === 'Retained Earnings' && isset($cell['start_date']) && isset($cell['end_date']))
31
-                                    <x-filament::link
32
-                                        color="primary"
33
-                                        target="_blank"
34
-                                        icon="heroicon-o-arrow-top-right-on-square"
35
-                                        :icon-position="\Filament\Support\Enums\IconPosition::After"
36
-                                        :icon-size="\Filament\Support\Enums\IconSize::Small"
37
-                                        href="{{ \App\Filament\Company\Pages\Reports\IncomeStatement::getUrl([
38
-                                            'startDate' => $cell['start_date'],
39
-                                            'endDate' => $cell['end_date']
9
+                @foreach($categoryAccount as $accountIndex => $categoryAccountCell)
10
+                    <x-company.tables.cell :alignment-class="$report->getAlignmentClass($accountIndex)">
11
+                        @if(is_array($categoryAccountCell) && isset($categoryAccountCell['name']))
12
+                            @if($categoryAccountCell['name'] === 'Retained Earnings' && isset($categoryAccountCell['start_date']) && isset($categoryAccountCell['end_date']))
13
+                                <x-filament::link
14
+                                    color="primary"
15
+                                    target="_blank"
16
+                                    icon="heroicon-o-arrow-top-right-on-square"
17
+                                    :icon-position="\Filament\Support\Enums\IconPosition::After"
18
+                                    :icon-size="\Filament\Support\Enums\IconSize::Small"
19
+                                    href="{{ \App\Filament\Company\Pages\Reports\IncomeStatement::getUrl([
20
+                                            'startDate' => $categoryAccountCell['start_date'],
21
+                                            'endDate' => $categoryAccountCell['end_date']
40
                                         ]) }}"
22
                                         ]) }}"
41
-                                    >
42
-                                        {{ $cell['name'] }}
43
-                                    </x-filament::link>
44
-                                @elseif(isset($cell['id']) && isset($cell['start_date']) && isset($cell['end_date']))
45
-                                    <x-filament::link
46
-                                        color="primary"
47
-                                        target="_blank"
48
-                                        icon="heroicon-o-arrow-top-right-on-square"
49
-                                        :icon-position="\Filament\Support\Enums\IconPosition::After"
50
-                                        :icon-size="\Filament\Support\Enums\IconSize::Small"
51
-                                        href="{{ \App\Filament\Company\Pages\Reports\AccountTransactions::getUrl([
52
-                                            'startDate' => $cell['start_date'],
53
-                                            'endDate' => $cell['end_date'],
54
-                                            'selectedAccount' => $cell['id']
23
+                                >
24
+                                    {{ $categoryAccountCell['name'] }}
25
+                                </x-filament::link>
26
+                            @elseif(isset($categoryAccountCell['id']) && isset($categoryAccountCell['start_date']) && isset($categoryAccountCell['end_date']))
27
+                                <x-filament::link
28
+                                    color="primary"
29
+                                    target="_blank"
30
+                                    icon="heroicon-o-arrow-top-right-on-square"
31
+                                    :icon-position="\Filament\Support\Enums\IconPosition::After"
32
+                                    :icon-size="\Filament\Support\Enums\IconSize::Small"
33
+                                    href="{{ \App\Filament\Company\Pages\Reports\AccountTransactions::getUrl([
34
+                                            'startDate' => $categoryAccountCell['start_date'],
35
+                                            'endDate' => $categoryAccountCell['end_date'],
36
+                                            'selectedAccount' => $categoryAccountCell['id']
55
                                         ]) }}"
37
                                         ]) }}"
56
-                                    >
57
-                                        {{ $cell['name'] }}
58
-                                    </x-filament::link>
59
-                                @else
60
-                                    {{ $cell['name'] }}
61
-                                @endif
38
+                                >
39
+                                    {{ $categoryAccountCell['name'] }}
40
+                                </x-filament::link>
62
                             @else
41
                             @else
63
-                                {{ $cell }}
42
+                                {{ $categoryAccountCell['name'] }}
64
                             @endif
43
                             @endif
65
-                        </div>
66
-                    </x-filament-tables::cell>
44
+                        @else
45
+                            {{ $categoryAccountCell }}
46
+                        @endif
47
+                    </x-company.tables.cell>
67
                 @endforeach
48
                 @endforeach
68
             </tr>
49
             </tr>
69
         @endforeach
50
         @endforeach
70
         <tr>
51
         <tr>
71
-            @foreach($category->summary as $summaryIndex => $cell)
72
-                <x-filament-tables::cell class="{{ $report->getAlignmentClass($summaryIndex) }}">
73
-                    <div class="px-3 py-2 text-sm leading-6 font-semibold text-gray-950 dark:text-white">
74
-                        {{ $cell }}
75
-                    </div>
76
-                </x-filament-tables::cell>
52
+            @foreach($accountCategory->summary as $accountCategorySummaryIndex => $accountCategorySummaryCell)
53
+                <x-company.tables.cell :alignment-class="$report->getAlignmentClass($accountCategorySummaryIndex)"
54
+                                       bold="true">
55
+                    {{ $accountCategorySummaryCell }}
56
+                </x-company.tables.cell>
77
             @endforeach
57
             @endforeach
78
         </tr>
58
         </tr>
79
         <tr>
59
         <tr>
80
-            <x-filament-tables::cell colspan="{{ count($report->getHeaders()) }}">
81
-                <div class="px-3 py-2 leading-6 invisible">Hidden Text</div>
82
-            </x-filament-tables::cell>
60
+            <td colspan="{{ count($report->getHeaders()) }}">
61
+                <div class="min-h-12"></div>
62
+            </td>
83
         </tr>
63
         </tr>
84
         </tbody>
64
         </tbody>
85
     @endforeach
65
     @endforeach
86
-    <tfoot>
87
-    <tr class="bg-gray-50 dark:bg-white/5">
88
-        @foreach($report->getOverallTotals() as $index => $total)
89
-            <x-filament-tables::cell class="{{ $report->getAlignmentClass($index) }}">
90
-                <div class="px-3 py-2 text-sm leading-6 font-semibold text-gray-950 dark:text-white">
91
-                    {{ $total }}
92
-                </div>
93
-            </x-filament-tables::cell>
94
-        @endforeach
95
-    </tr>
96
-    </tfoot>
66
+    <x-company.tables.footer :totals="$report->getOverallTotals()" :alignment-class="[$report, 'getAlignmentClass']"/>
97
 </table>
67
 </table>

+ 28
- 0
resources/views/components/company/tables/reports/income-statement-summary.blade.php Переглянути файл

1
+<table class="w-full table-auto divide-y divide-gray-200 dark:divide-white/5">
2
+    <x-company.tables.header :headers="$report->getSummaryHeaders()" :alignment-class="[$report, 'getAlignmentClass']"/>
3
+    @foreach($report->getSummaryCategories() as $accountCategory)
4
+        <tbody class="divide-y divide-gray-200 whitespace-nowrap dark:divide-white/5">
5
+        <tr>
6
+            @foreach($accountCategory->summary as $accountCategorySummaryIndex => $accountCategorySummaryCell)
7
+                <x-company.tables.cell :alignment-class="$report->getAlignmentClass($accountCategorySummaryIndex)">
8
+                    {{ $accountCategorySummaryCell }}
9
+                </x-company.tables.cell>
10
+            @endforeach
11
+        </tr>
12
+
13
+        @if($accountCategory->header['account_name'] === 'Cost of Goods Sold')
14
+            <tr class="bg-gray-50 dark:bg-white/5">
15
+                @foreach($report->getGrossProfit() as $grossProfitIndex => $grossProfitCell)
16
+                    <x-filament-tables::cell class="{{ $report->getAlignmentClass($grossProfitIndex) }}">
17
+                        <div class="px-3 py-2 text-sm leading-6 font-semibold text-gray-950 dark:text-white">
18
+                            {{ $grossProfitCell }}
19
+                        </div>
20
+                    </x-filament-tables::cell>
21
+                @endforeach
22
+            </tr>
23
+        @endif
24
+        </tbody>
25
+    @endforeach
26
+    <x-company.tables.footer :totals="$report->getSummaryOverallTotals()"
27
+                             :alignment-class="[$report, 'getAlignmentClass']"/>
28
+</table>

+ 12
- 6
resources/views/filament/company/pages/accounting/chart.blade.php Переглянути файл

15
 
15
 
16
         @foreach($this->categories as $categoryValue => $subtypes)
16
         @foreach($this->categories as $categoryValue => $subtypes)
17
             @if($activeTab === $categoryValue)
17
             @if($activeTab === $categoryValue)
18
-                <div class="es-table__container overflow-hidden rounded-xl bg-white shadow-sm ring-1 ring-gray-950/5 dark:divide-white/10 dark:bg-gray-900 dark:ring-white/10">
18
+                <div
19
+                    class="es-table__container overflow-hidden rounded-xl bg-white shadow-sm ring-1 ring-gray-950/5 dark:divide-white/10 dark:bg-gray-900 dark:ring-white/10">
19
                     <div class="es-table__header-ctn"></div>
20
                     <div class="es-table__header-ctn"></div>
20
                     <div class="es-table__content overflow-x-auto">
21
                     <div class="es-table__content overflow-x-auto">
21
-                        <table class="es-table table-fixed w-full divide-y divide-gray-200 text-start text-sm dark:divide-white/5">
22
+                        <table
23
+                            class="es-table table-fixed w-full divide-y divide-gray-200 text-start text-sm dark:divide-white/5">
22
                             <colgroup>
24
                             <colgroup>
23
                                 <col span="1" style="width: 12.5%;">
25
                                 <col span="1" style="width: 12.5%;">
24
                                 <col span="1" style="width: 20%;">
26
                                 <col span="1" style="width: 20%;">
28
                                 <col span="1" style="width: 7.5%;">
30
                                 <col span="1" style="width: 7.5%;">
29
                             </colgroup>
31
                             </colgroup>
30
                             @foreach($subtypes as $subtype)
32
                             @foreach($subtypes as $subtype)
31
-                                <tbody class="es-table__rowgroup divide-y divide-gray-200 whitespace-nowrap dark:divide-white/5">
33
+                                <tbody
34
+                                    class="es-table__rowgroup divide-y divide-gray-200 whitespace-nowrap dark:divide-white/5">
32
                                 <!-- Subtype Name Header Row -->
35
                                 <!-- Subtype Name Header Row -->
33
                                 <tr class="es-table__row--header bg-gray-50 dark:bg-white/5">
36
                                 <tr class="es-table__row--header bg-gray-50 dark:bg-white/5">
34
                                     <td colspan="6" class="es-table__cell px-4 py-4">
37
                                     <td colspan="6" class="es-table__cell px-4 py-4">
35
                                         <div class="es-table__row-content flex items-center space-x-2">
38
                                         <div class="es-table__row-content flex items-center space-x-2">
36
-                                            <span class="es-table__row-title text-gray-800 dark:text-gray-200 font-semibold tracking-wider">
39
+                                            <span
40
+                                                class="es-table__row-title text-gray-800 dark:text-gray-200 font-semibold tracking-wider">
37
                                                 {{ $subtype->name }}
41
                                                 {{ $subtype->name }}
38
                                             </span>
42
                                             </span>
39
                                             <x-tooltip
43
                                             <x-tooltip
61
                                                 @endif
65
                                                 @endif
62
                                             </small>
66
                                             </small>
63
                                         </td>
67
                                         </td>
64
-                                        <td colspan="2" class="es-table__cell px-4 py-4">{{ $account->description }}</td>
68
+                                        <td colspan="2"
69
+                                            class="es-table__cell px-4 py-4">{{ $account->description }}</td>
65
                                         <td colspan="1" class="es-table__cell px-4 py-4">
70
                                         <td colspan="1" class="es-table__cell px-4 py-4">
66
                                             @if($account->archived)
71
                                             @if($account->archived)
67
                                                 <x-filament::badge color="gray" size="sm">
72
                                                 <x-filament::badge color="gray" size="sm">
80
                                 @empty
85
                                 @empty
81
                                     <!-- No Accounts Available Row -->
86
                                     <!-- No Accounts Available Row -->
82
                                     <tr class="es-table__row">
87
                                     <tr class="es-table__row">
83
-                                        <td colspan="5" class="es-table__cell px-4 py-4 italic text-xs text-gray-500 dark:text-gray-400">
88
+                                        <td colspan="5"
89
+                                            class="es-table__cell px-4 py-4 italic text-xs text-gray-500 dark:text-gray-400">
84
                                             {{ __("You haven't added any {$subtype->name} accounts yet.") }}
90
                                             {{ __("You haven't added any {$subtype->name} accounts yet.") }}
85
                                         </td>
91
                                         </td>
86
                                     </tr>
92
                                     </tr>

+ 12
- 27
resources/views/filament/company/pages/reports/account-transactions.blade.php Переглянути файл

5
         @endif
5
         @endif
6
     </x-filament::section>
6
     </x-filament::section>
7
 
7
 
8
-    <x-filament-tables::container>
9
-        <div class="es-table__header-ctn"></div>
10
-        <div
11
-            class="relative divide-y divide-gray-200 overflow-x-auto dark:divide-white/10 dark:border-t-white/10 min-h-64">
12
-            <div wire:init="applyFilters" class="flex items-center justify-center w-full h-full absolute">
13
-                <div wire:loading wire:target="applyFilters">
14
-                    <x-filament::loading-indicator class="p-6 text-primary-700 dark:text-primary-300"/>
15
-                </div>
16
-            </div>
17
-
18
-            @if($this->reportLoaded)
19
-                <div wire:loading.remove wire:target="applyFilters">
20
-                    @if($this->report && !$this->tableHasEmptyState())
21
-                        <x-company.tables.reports.account-transactions :report="$this->report"/>
22
-                    @else
23
-                        <x-filament-tables::empty-state
24
-                            :actions="$this->getEmptyStateActions()"
25
-                            :description="$this->getEmptyStateDescription()"
26
-                            :heading="$this->getEmptyStateHeading()"
27
-                            :icon="$this->getEmptyStateIcon()"
28
-                        />
29
-                    @endif
30
-                </div>
31
-            @endif
32
-        </div>
33
-        <div class="es-table__footer-ctn border-t border-gray-200"></div>
34
-    </x-filament-tables::container>
8
+    <x-company.tables.container :report-loaded="$this->reportLoaded">
9
+        @if($this->report && ! $this->tableHasEmptyState())
10
+            <x-company.tables.reports.account-transactions :report="$this->report"/>
11
+        @else
12
+            <x-filament-tables::empty-state
13
+                :actions="$this->getEmptyStateActions()"
14
+                :description="$this->getEmptyStateDescription()"
15
+                :heading="$this->getEmptyStateHeading()"
16
+                :icon="$this->getEmptyStateIcon()"
17
+            />
18
+        @endif
19
+    </x-company.tables.container>
35
 </x-filament-panels::page>
20
 </x-filament-panels::page>

+ 89
- 0
resources/views/filament/company/pages/reports/balance-sheet.blade.php Переглянути файл

1
+<x-filament-panels::page>
2
+    <x-filament::section>
3
+        <div class="flex flex-col lg:flex-row items-start lg:items-end justify-between gap-4">
4
+            <!-- Form Container -->
5
+            @if(method_exists($this, 'filtersForm'))
6
+                {{ $this->filtersForm }}
7
+            @endif
8
+
9
+            <!-- Grouping Button and Column Toggle -->
10
+            @if($this->hasToggleableColumns())
11
+                <div class="lg:mb-1">
12
+                    <x-filament-tables::column-toggle.dropdown
13
+                        :form="$this->getTableColumnToggleForm()"
14
+                        :trigger-action="$this->getToggleColumnsTriggerAction()"
15
+                    />
16
+                </div>
17
+            @endif
18
+
19
+            <div class="inline-flex items-center min-w-0 lg:min-w-[9.5rem] justify-end">
20
+                {{ $this->applyFiltersAction }}
21
+            </div>
22
+        </div>
23
+    </x-filament::section>
24
+
25
+
26
+    <x-filament::section>
27
+        <!-- Summary Section -->
28
+        @if($this->reportLoaded)
29
+            <div
30
+                class="flex flex-col md:flex-row items-center md:items-end text-center justify-center gap-4 md:gap-8">
31
+                @foreach($this->report->getSummary() as $summary)
32
+                    <div class="text-sm">
33
+                        <div class="text-gray-600 font-medium mb-2">{{ $summary['label'] }}</div>
34
+
35
+                        @php
36
+                            $isNetAssets = $summary['label'] === 'Net Assets';
37
+                            $isPositive = money($summary['value'], \App\Utilities\Currency\CurrencyAccessor::getDefaultCurrency())->isPositive();
38
+                        @endphp
39
+
40
+                        <strong
41
+                            @class([
42
+                                'text-lg',
43
+                                'text-green-700' => $isNetAssets && $isPositive,
44
+                                'text-danger-700' => $isNetAssets && ! $isPositive,
45
+                            ])
46
+                        >
47
+                            {{ $summary['value'] }}
48
+                        </strong>
49
+                    </div>
50
+
51
+                    @if(! $loop->last)
52
+                        <div class="flex items-center justify-center px-2">
53
+                            <strong class="text-lg">
54
+                                {{ $loop->remaining === 1 ? '=' : '-' }}
55
+                            </strong>
56
+                        </div>
57
+                    @endif
58
+                @endforeach
59
+            </div>
60
+        @endif
61
+    </x-filament::section>
62
+
63
+    <x-filament::tabs>
64
+        <x-filament::tabs.item
65
+            :active="$activeTab === 'summary'"
66
+            wire:click="$set('activeTab', 'summary')"
67
+        >
68
+            Summary
69
+        </x-filament::tabs.item>
70
+
71
+        <x-filament::tabs.item
72
+            :active="$activeTab === 'details'"
73
+            wire:click="$set('activeTab', 'details')"
74
+        >
75
+            Details
76
+        </x-filament::tabs.item>
77
+    </x-filament::tabs>
78
+
79
+    <x-company.tables.container :report-loaded="$this->reportLoaded">
80
+        @if($this->report)
81
+            @if($activeTab === 'summary')
82
+                <x-company.tables.reports.balance-sheet-summary :report="$this->report"/>
83
+            @elseif($activeTab === 'details')
84
+                <x-company.tables.reports.balance-sheet :report="$this->report"/>
85
+            @endif
86
+        @endif
87
+    </x-company.tables.container>
88
+</x-filament-panels::page>
89
+

+ 5
- 20
resources/views/filament/company/pages/reports/detailed-report.blade.php Переглянути файл

20
         </div>
20
         </div>
21
     </x-filament::section>
21
     </x-filament::section>
22
 
22
 
23
-    <x-filament-tables::container>
24
-        <div class="es-table__header-ctn"></div>
25
-        <div
26
-            class="relative divide-y divide-gray-200 overflow-x-auto dark:divide-white/10 dark:border-t-white/10 min-h-64">
27
-            <div wire:init="applyFilters" class="flex items-center justify-center w-full h-full absolute">
28
-                <div wire:loading wire:target="applyFilters">
29
-                    <x-filament::loading-indicator class="p-6 text-primary-700 dark:text-primary-300"/>
30
-                </div>
31
-            </div>
32
-
33
-            @if($this->reportLoaded)
34
-                <div wire:loading.remove wire:target="applyFilters">
35
-                    @if($this->report)
36
-                        <x-company.tables.reports.detailed-report :report="$this->report"/>
37
-                    @endif
38
-                </div>
39
-            @endif
40
-        </div>
41
-        <div class="es-table__footer-ctn border-t border-gray-200"></div>
42
-    </x-filament-tables::container>
23
+    <x-company.tables.container :report-loaded="$this->reportLoaded">
24
+        @if($this->report)
25
+            <x-company.tables.reports.detailed-report :report="$this->report"/>
26
+        @endif
27
+    </x-company.tables.container>
43
 </x-filament-panels::page>
28
 </x-filament-panels::page>

+ 23
- 18
resources/views/filament/company/pages/reports/income-statement.blade.php Переглянути файл

58
         @endif
58
         @endif
59
     </x-filament::section>
59
     </x-filament::section>
60
 
60
 
61
-    <x-filament-tables::container>
62
-        <div class="es-table__header-ctn"></div>
63
-        <div
64
-            class="relative divide-y divide-gray-200 overflow-x-auto dark:divide-white/10 dark:border-t-white/10 min-h-64">
65
-            <div wire:init="applyFilters" class="flex items-center justify-center w-full h-full absolute">
66
-                <div wire:loading wire:target="applyFilters">
67
-                    <x-filament::loading-indicator class="p-6 text-primary-700 dark:text-primary-300"/>
68
-                </div>
69
-            </div>
61
+    <x-filament::tabs>
62
+        <x-filament::tabs.item
63
+            :active="$activeTab === 'summary'"
64
+            wire:click="$set('activeTab', 'summary')"
65
+        >
66
+            Summary
67
+        </x-filament::tabs.item>
70
 
68
 
71
-            @if($this->reportLoaded)
72
-                <div wire:loading.remove wire:target="applyFilters">
73
-                    @if($this->report)
74
-                        <x-company.tables.reports.detailed-report :report="$this->report"/>
75
-                    @endif
76
-                </div>
69
+        <x-filament::tabs.item
70
+            :active="$activeTab === 'details'"
71
+            wire:click="$set('activeTab', 'details')"
72
+        >
73
+            Details
74
+        </x-filament::tabs.item>
75
+    </x-filament::tabs>
76
+
77
+    <x-company.tables.container :report-loaded="$this->reportLoaded">
78
+        @if($this->report)
79
+            @if($activeTab === 'summary')
80
+                <x-company.tables.reports.income-statement-summary :report="$this->report"/>
81
+            @elseif($activeTab === 'details')
82
+                <x-company.tables.reports.detailed-report :report="$this->report"/>
77
             @endif
83
             @endif
78
-        </div>
79
-        <div class="es-table__footer-ctn border-t border-gray-200"></div>
80
-    </x-filament-tables::container>
84
+        @endif
85
+    </x-company.tables.container>
81
 </x-filament-panels::page>
86
 </x-filament-panels::page>

+ 5
- 20
resources/views/filament/company/pages/reports/trial-balance.blade.php Переглянути файл

22
         </div>
22
         </div>
23
     </x-filament::section>
23
     </x-filament::section>
24
 
24
 
25
-    <x-filament-tables::container>
26
-        <div class="es-table__header-ctn"></div>
27
-        <div
28
-            class="relative divide-y divide-gray-200 overflow-x-auto dark:divide-white/10 dark:border-t-white/10 min-h-64">
29
-            <div wire:init="applyFilters" class="flex items-center justify-center w-full h-full absolute">
30
-                <div wire:loading wire:target="applyFilters">
31
-                    <x-filament::loading-indicator class="p-6 text-primary-700 dark:text-primary-300"/>
32
-                </div>
33
-            </div>
34
-
35
-            @if($this->reportLoaded)
36
-                <div wire:loading.remove wire:target="applyFilters">
37
-                    @if($this->report)
38
-                        <x-company.tables.reports.detailed-report :report="$this->report"/>
39
-                    @endif
40
-                </div>
41
-            @endif
42
-        </div>
43
-        <div class="es-table__footer-ctn border-t border-gray-200"></div>
44
-    </x-filament-tables::container>
25
+    <x-company.tables.container :report-loaded="$this->reportLoaded">
26
+        @if($this->report)
27
+            <x-company.tables.reports.detailed-report :report="$this->report"/>
28
+        @endif
29
+    </x-company.tables.container>
45
 </x-filament-panels::page>
30
 </x-filament-panels::page>

+ 3
- 3
tests/Feature/Accounting/TransactionTest.php Переглянути файл

226
     livewire(Transactions::class)
226
     livewire(Transactions::class)
227
         ->mountAction($actionName)
227
         ->mountAction($actionName)
228
         ->assertActionDataSet([
228
         ->assertActionDataSet([
229
-            'posted_at' => now()->toDateTimeString(),
229
+            'posted_at' => today(),
230
             'type' => $transactionType,
230
             'type' => $transactionType,
231
             'bank_account_id' => $defaultBankAccount->id,
231
             'bank_account_id' => $defaultBankAccount->id,
232
             'amount' => '0.00',
232
             'amount' => '0.00',
260
     livewire(Transactions::class)
260
     livewire(Transactions::class)
261
         ->mountAction('addTransfer')
261
         ->mountAction('addTransfer')
262
         ->assertActionDataSet([
262
         ->assertActionDataSet([
263
-            'posted_at' => now()->toDateTimeString(),
263
+            'posted_at' => today(),
264
             'type' => TransactionType::Transfer,
264
             'type' => TransactionType::Transfer,
265
             'bank_account_id' => $sourceBankAccount->id,
265
             'bank_account_id' => $sourceBankAccount->id,
266
             'amount' => '0.00',
266
             'amount' => '0.00',
293
     livewire(Transactions::class)
293
     livewire(Transactions::class)
294
         ->mountAction('addJournalTransaction')
294
         ->mountAction('addJournalTransaction')
295
         ->assertActionDataSet([
295
         ->assertActionDataSet([
296
-            'posted_at' => now()->toDateTimeString(),
296
+            'posted_at' => today(),
297
             'journalEntries' => [
297
             'journalEntries' => [
298
                 ['type' => JournalEntryType::Debit, 'account_id' => $defaultDebitAccount->id, 'amount' => '0.00'],
298
                 ['type' => JournalEntryType::Debit, 'account_id' => $defaultDebitAccount->id, 'amount' => '0.00'],
299
                 ['type' => JournalEntryType::Credit, 'account_id' => $defaultCreditAccount->id, 'amount' => '0.00'],
299
                 ['type' => JournalEntryType::Credit, 'account_id' => $defaultCreditAccount->id, 'amount' => '0.00'],

Завантаження…
Відмінити
Зберегти