Andrew Wallo 1年前
父节点
当前提交
0837cc79f9

+ 0
- 2
app/Filament/Company/Pages/Accounting/Transactions.php 查看文件

630
                             ->endDateField("{$fieldPrefix}_end_date"),
630
                             ->endDateField("{$fieldPrefix}_end_date"),
631
                         DatePicker::make("{$fieldPrefix}_start_date")
631
                         DatePicker::make("{$fieldPrefix}_start_date")
632
                             ->label("{$label} From")
632
                             ->label("{$label} From")
633
-                            ->displayFormat('Y-m-d')
634
                             ->columnStart(1)
633
                             ->columnStart(1)
635
                             ->afterStateUpdated(static function (Set $set) use ($fieldPrefix) {
634
                             ->afterStateUpdated(static function (Set $set) use ($fieldPrefix) {
636
                                 $set("{$fieldPrefix}_date_range", 'Custom');
635
                                 $set("{$fieldPrefix}_date_range", 'Custom');
637
                             }),
636
                             }),
638
                         DatePicker::make("{$fieldPrefix}_end_date")
637
                         DatePicker::make("{$fieldPrefix}_end_date")
639
                             ->label("{$label} To")
638
                             ->label("{$label} To")
640
-                            ->displayFormat('Y-m-d')
641
                             ->afterStateUpdated(static function (Set $set) use ($fieldPrefix) {
639
                             ->afterStateUpdated(static function (Set $set) use ($fieldPrefix) {
642
                                 $set("{$fieldPrefix}_date_range", 'Custom');
640
                                 $set("{$fieldPrefix}_date_range", 'Custom');
643
                             }),
641
                             }),

+ 29
- 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\IncomeStatement;
7
 use App\Filament\Company\Pages\Reports\TrialBalance;
8
 use App\Filament\Company\Pages\Reports\TrialBalance;
8
 use App\Infolists\Components\ReportEntry;
9
 use App\Infolists\Components\ReportEntry;
9
 use Filament\Infolists\Components\Section;
10
 use Filament\Infolists\Components\Section;
22
         return $infolist
23
         return $infolist
23
             ->state([])
24
             ->state([])
24
             ->schema([
25
             ->schema([
26
+                Section::make('Financial Statements')
27
+                    ->aside()
28
+                    ->description('Key financial statements that provide a snapshot of your company’s financial health.')
29
+                    ->extraAttributes(['class' => 'es-report-card'])
30
+                    ->schema([
31
+                        ReportEntry::make('income_statement')
32
+                            ->hiddenLabel()
33
+                            ->heading('Income Statement')
34
+                            ->description('Tracks revenue and expenses to show profit or loss over a specific period of time.')
35
+                            ->icon('heroicon-o-chart-bar')
36
+                            ->iconColor(Color::Indigo)
37
+                            ->url(IncomeStatement::getUrl()),
38
+                        ReportEntry::make('balance_sheet')
39
+                            ->hiddenLabel()
40
+                            ->heading('Balance Sheet')
41
+                            ->description('Snapshot of assets, liabilities, and equity at a specific point in time.')
42
+                            ->icon('heroicon-o-clipboard-document-list')
43
+                            ->iconColor(Color::Emerald)
44
+                            ->url('#'),
45
+                        ReportEntry::make('cash_flow_statement')
46
+                            ->hiddenLabel()
47
+                            ->heading('Cash Flow Statement')
48
+                            ->description('Shows cash inflows and outflows over a specific period of time.')
49
+                            ->icon('heroicon-o-document-currency-dollar')
50
+                            ->iconColor(Color::Cyan)
51
+                            ->url('#'),
52
+                    ]),
25
                 Section::make('Detailed Reports')
53
                 Section::make('Detailed Reports')
26
                     ->aside()
54
                     ->aside()
27
-                    ->description('Dig into the details of your business’s transactions, balances, and accounts.')
55
+                    ->description('Dig into the details of your company’s transactions, balances, and accounts.')
28
                     ->extraAttributes(['class' => 'es-report-card'])
56
                     ->extraAttributes(['class' => 'es-report-card'])
29
                     ->schema([
57
                     ->schema([
30
                         ReportEntry::make('account_balances')
58
                         ReportEntry::make('account_balances')

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

50
         return [
50
         return [
51
             Column::make('date')
51
             Column::make('date')
52
                 ->label('Date')
52
                 ->label('Date')
53
+                ->markAsDate()
53
                 ->alignment(Alignment::Left),
54
                 ->alignment(Alignment::Left),
54
             Column::make('description')
55
             Column::make('description')
55
                 ->label('Description')
56
                 ->label('Description')

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

154
 
154
 
155
     public function setDateRange(Carbon $start, Carbon $end): void
155
     public function setDateRange(Carbon $start, Carbon $end): void
156
     {
156
     {
157
-        $this->startDate = $start->toDateString();
158
-        $this->endDate = $end->isFuture() ? now()->toDateString() : $end->toDateString();
157
+        $this->startDate = $start->startOfDay()->toDateTimeString();
158
+        $this->endDate = $end->isFuture() ? now()->endOfDay()->toDateTimeString() : $end->endOfDay()->toDateTimeString();
159
     }
159
     }
160
 
160
 
161
     public function toggleColumnsAction(): Action
161
     public function toggleColumnsAction(): Action
238
     {
238
     {
239
         return DatePicker::make('startDate')
239
         return DatePicker::make('startDate')
240
             ->label('Start Date')
240
             ->label('Start Date')
241
-            ->defaultDateFormat()
242
             ->afterStateUpdated(static function (Set $set) {
241
             ->afterStateUpdated(static function (Set $set) {
243
                 $set('dateRange', 'Custom');
242
                 $set('dateRange', 'Custom');
244
             });
243
             });
248
     {
247
     {
249
         return DatePicker::make('endDate')
248
         return DatePicker::make('endDate')
250
             ->label('End Date')
249
             ->label('End Date')
251
-            ->defaultDateFormat()
252
             ->afterStateUpdated(static function (Set $set) {
250
             ->afterStateUpdated(static function (Set $set) {
253
                 $set('dateRange', 'Custom');
251
                 $set('dateRange', 'Custom');
254
             });
252
             });

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

+ 2
- 2
app/Filament/Forms/Components/DateRangeSelect.php 查看文件

165
 
165
 
166
     public function setDateRange(Carbon $start, Carbon $end, Set $set): void
166
     public function setDateRange(Carbon $start, Carbon $end, Set $set): void
167
     {
167
     {
168
-        $set($this->startDateField, $start->format('Y-m-d'));
169
-        $set($this->endDateField, $end->isFuture() ? now()->format('Y-m-d') : $end->format('Y-m-d'));
168
+        $set($this->startDateField, $start->startOfDay()->toDateTimeString());
169
+        $set($this->endDateField, $end->isFuture() ? now()->endOfDay()->toDateTimeString() : $end->endOfDay()->toDateTimeString());
170
     }
170
     }
171
 }
171
 }

+ 2
- 4
app/Repositories/Accounting/JournalEntryRepository.php 查看文件

4
 
4
 
5
 use App\Models\Accounting\Account;
5
 use App\Models\Accounting\Account;
6
 use Illuminate\Database\Eloquent\Builder;
6
 use Illuminate\Database\Eloquent\Builder;
7
-use Illuminate\Support\Carbon;
8
 
7
 
9
 class JournalEntryRepository
8
 class JournalEntryRepository
10
 {
9
 {
13
         $query = $account->journalEntries()->where('type', $type);
12
         $query = $account->journalEntries()->where('type', $type);
14
 
13
 
15
         if ($startDate && $endDate) {
14
         if ($startDate && $endDate) {
16
-            $endOfDay = Carbon::parse($endDate)->endOfDay();
17
-            $query->whereHas('transaction', static function (Builder $query) use ($startDate, $endOfDay) {
18
-                $query->whereBetween('posted_at', [$startDate, $endOfDay]);
15
+            $query->whereHas('transaction', static function (Builder $query) use ($startDate, $endDate) {
16
+                $query->whereBetween('posted_at', [$startDate, $endDate]);
19
             });
17
             });
20
         } elseif ($startDate) {
18
         } elseif ($startDate) {
21
             $query->whereHas('transaction', static function (Builder $query) use ($startDate) {
19
             $query->whereHas('transaction', static function (Builder $query) use ($startDate) {

+ 9
- 16
app/Services/AccountService.php 查看文件

13
 use Closure;
13
 use Closure;
14
 use Illuminate\Database\Eloquent\Builder;
14
 use Illuminate\Database\Eloquent\Builder;
15
 use Illuminate\Database\Query\JoinClause;
15
 use Illuminate\Database\Query\JoinClause;
16
-use Illuminate\Support\Carbon;
17
 use Illuminate\Support\Facades\DB;
16
 use Illuminate\Support\Facades\DB;
18
 
17
 
19
 class AccountService implements AccountHandler
18
 class AccountService implements AccountHandler
56
 
55
 
57
     public function getTransactionDetailsSubquery(string $startDate, string $endDate): Closure
56
     public function getTransactionDetailsSubquery(string $startDate, string $endDate): Closure
58
     {
57
     {
59
-        $endOfDay = Carbon::parse($endDate)->endOfDay()->toDateTimeString();
60
-
61
-        return static function ($query) use ($startDate, $endOfDay) {
58
+        return static function ($query) use ($startDate, $endDate) {
62
             $query->select(
59
             $query->select(
63
                 'journal_entries.id',
60
                 'journal_entries.id',
64
                 'journal_entries.account_id',
61
                 'journal_entries.account_id',
67
                 'journal_entries.amount',
64
                 'journal_entries.amount',
68
                 DB::raw('journal_entries.amount * IF(journal_entries.type = "debit", 1, -1) AS signed_amount')
65
                 DB::raw('journal_entries.amount * IF(journal_entries.type = "debit", 1, -1) AS signed_amount')
69
             )
66
             )
70
-                ->whereBetween('transactions.posted_at', [$startDate, $endOfDay])
67
+                ->whereBetween('transactions.posted_at', [$startDate, $endDate])
71
                 ->join('transactions', 'transactions.id', '=', 'journal_entries.transaction_id')
68
                 ->join('transactions', 'transactions.id', '=', 'journal_entries.transaction_id')
72
                 ->orderBy('transactions.posted_at')
69
                 ->orderBy('transactions.posted_at')
73
                 ->with('transaction:id,type,description,posted_at');
70
                 ->with('transaction:id,type,description,posted_at');
76
 
73
 
77
     public function getAccountBalances(string $startDate, string $endDate, array $accountIds = []): Builder
74
     public function getAccountBalances(string $startDate, string $endDate, array $accountIds = []): Builder
78
     {
75
     {
79
-        $endOfDay = Carbon::parse($endDate)->endOfDay()->toDateTimeString();
80
-
81
         $query = Account::query()
76
         $query = Account::query()
82
             ->select([
77
             ->select([
83
                 'accounts.id',
78
                 'accounts.id',
110
                 "),
105
                 "),
111
             ])
106
             ])
112
             ->join('journal_entries', 'journal_entries.account_id', '=', 'accounts.id')
107
             ->join('journal_entries', 'journal_entries.account_id', '=', 'accounts.id')
113
-            ->join('transactions', function (JoinClause $join) use ($endOfDay) {
108
+            ->join('transactions', function (JoinClause $join) use ($endDate) {
114
                 $join->on('transactions.id', '=', 'journal_entries.transaction_id')
109
                 $join->on('transactions.id', '=', 'journal_entries.transaction_id')
115
-                    ->where('transactions.posted_at', '<=', $endOfDay);
110
+                    ->where('transactions.posted_at', '<=', $endDate);
116
             })
111
             })
117
             ->groupBy('accounts.id')
112
             ->groupBy('accounts.id')
118
             ->with(['subtype:id,name']);
113
             ->with(['subtype:id,name']);
121
             $query->whereIn('accounts.id', $accountIds);
116
             $query->whereIn('accounts.id', $accountIds);
122
         }
117
         }
123
 
118
 
124
-        $query->addBinding([$startDate, $startDate, $startDate, $startDate, $startDate, $endOfDay, $startDate, $endOfDay], 'select');
119
+        $query->addBinding([$startDate, $startDate, $startDate, $startDate, $startDate, $endDate, $startDate, $endDate], 'select');
125
 
120
 
126
         return $query;
121
         return $query;
127
     }
122
     }
128
 
123
 
129
     public function getTotalBalanceForAllBankAccounts(string $startDate, string $endDate): Money
124
     public function getTotalBalanceForAllBankAccounts(string $startDate, string $endDate): Money
130
     {
125
     {
131
-        $endOfDay = Carbon::parse($endDate)->endOfDay()->toDateTimeString();
132
-
133
         $accountIds = Account::whereHas('bankAccount')
126
         $accountIds = Account::whereHas('bankAccount')
134
             ->pluck('id')
127
             ->pluck('id')
135
             ->toArray();
128
             ->toArray();
139
         }
132
         }
140
 
133
 
141
         $result = DB::table('journal_entries')
134
         $result = DB::table('journal_entries')
142
-            ->join('transactions', function (JoinClause $join) use ($endOfDay) {
135
+            ->join('transactions', function (JoinClause $join) use ($endDate) {
143
                 $join->on('transactions.id', '=', 'journal_entries.transaction_id')
136
                 $join->on('transactions.id', '=', 'journal_entries.transaction_id')
144
-                    ->where('transactions.posted_at', '<=', $endOfDay);
137
+                    ->where('transactions.posted_at', '<=', $endDate);
145
             })
138
             })
146
             ->whereIn('journal_entries.account_id', $accountIds)
139
             ->whereIn('journal_entries.account_id', $accountIds)
147
             ->selectRaw('
140
             ->selectRaw('
159
                 $startDate,
152
                 $startDate,
160
                 $startDate,
153
                 $startDate,
161
                 $startDate,
154
                 $startDate,
162
-                $endOfDay,
155
+                $endDate,
163
                 $startDate,
156
                 $startDate,
164
-                $endOfDay,
157
+                $endDate,
165
             ])
158
             ])
166
             ->first();
159
             ->first();
167
 
160
 

+ 40
- 12
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;
7
 use Barryvdh\Snappy\Facades\SnappyPdf;
8
 use Barryvdh\Snappy\Facades\SnappyPdf;
9
+use Carbon\Exceptions\InvalidFormatException;
8
 use Illuminate\Support\Carbon;
10
 use Illuminate\Support\Carbon;
9
 use Symfony\Component\HttpFoundation\StreamedResponse;
11
 use Symfony\Component\HttpFoundation\StreamedResponse;
10
 
12
 
11
 class ExportService
13
 class ExportService
12
 {
14
 {
13
-    public function exportToCsv(Company $company, ExportableReport $report, string $startDate, string $endDate, bool $separateCategoryHeaders = false): StreamedResponse
15
+    public function exportToCsv(Company $company, ExportableReport $report, string $startDate, string $endDate): StreamedResponse
14
     {
16
     {
15
-        $formattedStartDate = Carbon::parse($startDate)->format('Y-m-d');
16
-        $formattedEndDate = Carbon::parse($endDate)->format('Y-m-d');
17
+        $formattedStartDate = Carbon::parse($startDate)->toDateString();
18
+        $formattedEndDate = Carbon::parse($endDate)->toDateString();
17
 
19
 
18
-        $timestamp = Carbon::now()->format('Y-m-d-H_i');
20
+        $timestamp = Carbon::now()->format('Y-m-d_H-i-s');
19
 
21
 
20
         $filename = $company->name . ' ' . $report->getTitle() . ' ' . $formattedStartDate . ' to ' . $formattedEndDate . ' ' . $timestamp . '.csv';
22
         $filename = $company->name . ' ' . $report->getTitle() . ' ' . $formattedStartDate . ' to ' . $formattedEndDate . ' ' . $timestamp . '.csv';
21
 
23
 
24
             'Content-Disposition' => 'attachment; filename="' . $filename . '"',
26
             'Content-Disposition' => 'attachment; filename="' . $filename . '"',
25
         ];
27
         ];
26
 
28
 
27
-        $callback = function () use ($report, $company, $formattedStartDate, $formattedEndDate) {
29
+        $callback = function () use ($startDate, $endDate, $report, $company) {
28
             $file = fopen('php://output', 'wb');
30
             $file = fopen('php://output', 'wb');
29
 
31
 
32
+            $defaultStartDateFormat = Carbon::parse($startDate)->toDefaultDateFormat();
33
+            $defaultEndDateFormat = Carbon::parse($endDate)->toDefaultDateFormat();
34
+
30
             fputcsv($file, [$report->getTitle()]);
35
             fputcsv($file, [$report->getTitle()]);
31
             fputcsv($file, [$company->name]);
36
             fputcsv($file, [$company->name]);
32
-            fputcsv($file, ['Date Range: ' . $formattedStartDate . ' to ' . $formattedEndDate]);
37
+            fputcsv($file, ['Date Range: ' . $defaultStartDateFormat . ' to ' . $defaultEndDateFormat]);
33
             fputcsv($file, []);
38
             fputcsv($file, []);
34
 
39
 
35
             fputcsv($file, $report->getHeaders());
40
             fputcsv($file, $report->getHeaders());
44
                 }
49
                 }
45
 
50
 
46
                 foreach ($category->data as $accountRow) {
51
                 foreach ($category->data as $accountRow) {
47
-                    fputcsv($file, $accountRow);
52
+                    $row = [];
53
+                    $columns = $report->getColumns();
54
+
55
+                    /**
56
+                     * @var Column $column
57
+                     */
58
+                    foreach ($columns as $index => $column) {
59
+                        $cell = $accountRow[$index] ?? '';
60
+
61
+                        if ($column->isDate()) {
62
+                            try {
63
+                                $row[] = Carbon::parse($cell)->toDateString();
64
+                            } catch (InvalidFormatException) {
65
+                                $row[] = $cell;
66
+                            }
67
+                        } elseif (is_array($cell)) {
68
+                            // Handle array cells by extracting 'name' or 'description'
69
+                            $row[] = $cell['name'] ?? $cell['description'] ?? '';
70
+                        } else {
71
+                            $row[] = $cell;
72
+                        }
73
+                    }
74
+
75
+                    fputcsv($file, $row);
48
                 }
76
                 }
49
 
77
 
50
                 if (filled($category->summary)) {
78
                 if (filled($category->summary)) {
66
 
94
 
67
     public function exportToPdf(Company $company, ExportableReport $report, string $startDate, string $endDate): StreamedResponse
95
     public function exportToPdf(Company $company, ExportableReport $report, string $startDate, string $endDate): StreamedResponse
68
     {
96
     {
69
-        $formattedStartDate = Carbon::parse($startDate)->format('Y-m-d');
70
-        $formattedEndDate = Carbon::parse($endDate)->format('Y-m-d');
97
+        $formattedStartDate = Carbon::parse($startDate)->toDateString();
98
+        $formattedEndDate = Carbon::parse($endDate)->toDateString();
71
 
99
 
72
-        $timestamp = Carbon::now()->format('Y-m-d-H_i');
100
+        $timestamp = Carbon::now()->format('Y-m-d_H-i-s');
73
 
101
 
74
         $filename = $company->name . ' ' . $report->getTitle() . ' ' . $formattedStartDate . ' to ' . $formattedEndDate . ' ' . $timestamp . '.pdf';
102
         $filename = $company->name . ' ' . $report->getTitle() . ' ' . $formattedStartDate . ' to ' . $formattedEndDate . ' ' . $timestamp . '.pdf';
75
 
103
 
76
         $pdf = SnappyPdf::loadView($report->getPdfView(), [
104
         $pdf = SnappyPdf::loadView($report->getPdfView(), [
77
             'company' => $company,
105
             'company' => $company,
78
             'report' => $report,
106
             'report' => $report,
79
-            'startDate' => Carbon::parse($startDate)->format('M d, Y'),
80
-            'endDate' => Carbon::parse($endDate)->format('M d, Y'),
107
+            'startDate' => Carbon::parse($startDate)->toDefaultDateFormat(),
108
+            'endDate' => Carbon::parse($endDate)->toDefaultDateFormat(),
81
         ]);
109
         ]);
82
 
110
 
83
         return response()->streamDownload(function () use ($pdf) {
111
         return response()->streamDownload(function () use ($pdf) {

+ 69
- 3
app/Services/ReportService.php 查看文件

121
 
121
 
122
         $query = $this->accountService->getAccountBalances($startDate, $endDate, $accountIds);
122
         $query = $this->accountService->getAccountBalances($startDate, $endDate, $accountIds);
123
 
123
 
124
-        $query->with(['journalEntries' => $this->accountService->getTransactionDetailsSubquery($startDate, $endDate)]);
125
-
126
-        $accounts = $query->get();
124
+        $accounts = $query->with(['journalEntries' => $this->accountService->getTransactionDetailsSubquery($startDate, $endDate)])->get();
127
 
125
 
128
         $reportCategories = [];
126
         $reportCategories = [];
129
 
127
 
292
 
290
 
293
         return ['debit_balance' => abs($endingBalance), 'credit_balance' => 0];
291
         return ['debit_balance' => abs($endingBalance), 'credit_balance' => 0];
294
     }
292
     }
293
+
294
+    public function buildIncomeStatementReport(string $startDate, string $endDate, array $columns = []): ReportDTO
295
+    {
296
+        $accounts = $this->accountService->getAccountBalances($startDate, $endDate)->get();
297
+
298
+        $accountCategories = [];
299
+        $totalRevenue = 0;
300
+        $cogs = 0;
301
+        $totalExpenses = 0;
302
+
303
+        $categoryGroups = [
304
+            'Revenue' => [
305
+                'accounts' => $accounts->where('category', AccountCategory::Revenue),
306
+                'total' => &$totalRevenue,
307
+            ],
308
+            'Cost of Goods Sold' => [
309
+                'accounts' => $accounts->where('subtype.name', 'Cost of Goods Sold'),
310
+                'total' => &$cogs,
311
+            ],
312
+            'Expenses' => [
313
+                'accounts' => $accounts->where('category', AccountCategory::Expense)->where('subtype.name', '!=', 'Cost of Goods Sold'),
314
+                'total' => &$totalExpenses,
315
+            ],
316
+        ];
317
+
318
+        foreach ($categoryGroups as $label => $group) {
319
+            $categoryAccounts = [];
320
+            $netMovement = 0;
321
+
322
+            foreach ($group['accounts']->sortBy('code', SORT_NATURAL) as $account) {
323
+                $category = null;
324
+
325
+                if ($label === 'Revenue') {
326
+                    $category = AccountCategory::Revenue;
327
+                } elseif ($label === 'Expenses') {
328
+                    $category = AccountCategory::Expense;
329
+                } elseif ($label === 'Cost of Goods Sold') {
330
+                    // COGS is treated as part of Expenses, so we use AccountCategory::Expense
331
+                    $category = AccountCategory::Expense;
332
+                }
333
+
334
+                if ($category !== null) {
335
+                    $accountBalances = $this->calculateAccountBalances($account, $category);
336
+                    $movement = $accountBalances['net_movement'];
337
+                    $netMovement += $movement;
338
+                    $group['total'] += $movement;
339
+
340
+                    $categoryAccounts[] = new AccountDTO(
341
+                        $account->name,
342
+                        $account->code,
343
+                        $account->id,
344
+                        $this->formatBalances(['net_movement' => $movement]),
345
+                    );
346
+                }
347
+            }
348
+
349
+            $accountCategories[$label] = new AccountCategoryDTO(
350
+                $categoryAccounts,
351
+                $this->formatBalances(['net_movement' => $netMovement]),
352
+            );
353
+        }
354
+
355
+        $grossProfit = $totalRevenue - $cogs;
356
+        $netProfit = $grossProfit - $totalExpenses;
357
+        $formattedReportTotalBalances = $this->formatBalances(['net_movement' => $netProfit]);
358
+
359
+        return new ReportDTO($accountCategories, $formattedReportTotalBalances, $columns);
360
+    }
295
 }
361
 }

+ 14
- 0
app/Support/Column.php 查看文件

18
     use HasLabel;
18
     use HasLabel;
19
     use HasName;
19
     use HasName;
20
 
20
 
21
+    protected bool $isDate = false;
22
+
21
     final public function __construct(string $name)
23
     final public function __construct(string $name)
22
     {
24
     {
23
         $this->name($name);
25
         $this->name($name);
40
             default => '',
42
             default => '',
41
         };
43
         };
42
     }
44
     }
45
+
46
+    public function markAsDate(): static
47
+    {
48
+        $this->isDate = true;
49
+
50
+        return $this;
51
+    }
52
+
53
+    public function isDate(): bool
54
+    {
55
+        return $this->isDate;
56
+    }
43
 }
57
 }

+ 130
- 0
app/Transformers/IncomeStatementReportTransformer.php 查看文件

1
+<?php
2
+
3
+namespace App\Transformers;
4
+
5
+use App\DTO\AccountDTO;
6
+use App\DTO\ReportCategoryDTO;
7
+use App\Support\Column;
8
+
9
+class IncomeStatementReportTransformer extends BaseReportTransformer
10
+{
11
+    protected string $totalRevenue = '$0.00';
12
+
13
+    protected string $totalCogs = '$0.00';
14
+
15
+    protected string $totalExpenses = '$0.00';
16
+
17
+    public function getTitle(): string
18
+    {
19
+        return 'Income Statement';
20
+    }
21
+
22
+    public function getHeaders(): array
23
+    {
24
+        return array_map(fn (Column $column) => $column->getLabel(), $this->getColumns());
25
+    }
26
+
27
+    public function calculateTotals(): void
28
+    {
29
+        foreach ($this->report->categories as $accountCategoryName => $accountCategory) {
30
+            match ($accountCategoryName) {
31
+                'Revenue' => $this->totalRevenue = $accountCategory->summary->netMovement ?? '',
32
+                'Cost of Goods Sold' => $this->totalCogs = $accountCategory->summary->netMovement ?? '',
33
+                'Expenses' => $this->totalExpenses = $accountCategory->summary->netMovement ?? '',
34
+            };
35
+        }
36
+    }
37
+
38
+    public function getCategories(): array
39
+    {
40
+        $categories = [];
41
+
42
+        foreach ($this->report->categories as $accountCategoryName => $accountCategory) {
43
+            // Initialize header with empty strings
44
+            $header = [];
45
+
46
+            foreach ($this->getColumns() as $index => $column) {
47
+                if ($column->getName() === 'account_name') {
48
+                    $header[$index] = $accountCategoryName;
49
+                } else {
50
+                    $header[$index] = '';
51
+                }
52
+            }
53
+
54
+            $data = array_map(function (AccountDTO $account) {
55
+                $row = [];
56
+
57
+                foreach ($this->getColumns() as $column) {
58
+                    $row[] = match ($column->getName()) {
59
+                        'account_code' => $account->accountCode,
60
+                        'account_name' => [
61
+                            'name' => $account->accountName,
62
+                            'id' => $account->accountId ?? null,
63
+                        ],
64
+                        'net_movement' => $account->balance->netMovement ?? '',
65
+                        default => '',
66
+                    };
67
+                }
68
+
69
+                return $row;
70
+            }, $accountCategory->accounts);
71
+
72
+            $summary = [];
73
+
74
+            foreach ($this->getColumns() as $column) {
75
+                $summary[] = match ($column->getName()) {
76
+                    'account_name' => 'Total ' . $accountCategoryName,
77
+                    'net_movement' => $accountCategory->summary->netMovement ?? '',
78
+                    default => '',
79
+                };
80
+            }
81
+
82
+            $categories[] = new ReportCategoryDTO(
83
+                header: $header,
84
+                data: $data,
85
+                summary: $summary,
86
+            );
87
+        }
88
+
89
+        return $categories;
90
+    }
91
+
92
+    public function getOverallTotals(): array
93
+    {
94
+        $totals = [];
95
+
96
+        foreach ($this->getColumns() as $column) {
97
+            $totals[] = match ($column->getName()) {
98
+                'account_name' => 'Net Earnings',
99
+                'net_movement' => $this->report->overallTotal->netMovement ?? '',
100
+                default => '',
101
+            };
102
+        }
103
+
104
+        return $totals;
105
+    }
106
+
107
+    public function getSummary(): array
108
+    {
109
+        $this->calculateTotals();
110
+
111
+        return [
112
+            [
113
+                'label' => 'Revenue',
114
+                'value' => $this->totalRevenue,
115
+            ],
116
+            [
117
+                'label' => 'Cost of Goods Sold',
118
+                'value' => $this->totalCogs,
119
+            ],
120
+            [
121
+                'label' => 'Expenses',
122
+                'value' => $this->totalExpenses,
123
+            ],
124
+            [
125
+                'label' => 'Net Earnings',
126
+                'value' => $this->report->overallTotal->netMovement ?? '',
127
+            ],
128
+        ];
129
+    }
130
+}

+ 70
- 70
package-lock.json 查看文件

541
             }
541
             }
542
         },
542
         },
543
         "node_modules/@rollup/rollup-android-arm-eabi": {
543
         "node_modules/@rollup/rollup-android-arm-eabi": {
544
-            "version": "4.20.0",
545
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.20.0.tgz",
546
-            "integrity": "sha512-TSpWzflCc4VGAUJZlPpgAJE1+V60MePDQnBd7PPkpuEmOy8i87aL6tinFGKBFKuEDikYpig72QzdT3QPYIi+oA==",
544
+            "version": "4.21.0",
545
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.0.tgz",
546
+            "integrity": "sha512-WTWD8PfoSAJ+qL87lE7votj3syLavxunWhzCnx3XFxFiI/BA/r3X7MUM8dVrH8rb2r4AiO8jJsr3ZjdaftmnfA==",
547
             "cpu": [
547
             "cpu": [
548
                 "arm"
548
                 "arm"
549
             ],
549
             ],
555
             ]
555
             ]
556
         },
556
         },
557
         "node_modules/@rollup/rollup-android-arm64": {
557
         "node_modules/@rollup/rollup-android-arm64": {
558
-            "version": "4.20.0",
559
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.20.0.tgz",
560
-            "integrity": "sha512-u00Ro/nok7oGzVuh/FMYfNoGqxU5CPWz1mxV85S2w9LxHR8OoMQBuSk+3BKVIDYgkpeOET5yXkx90OYFc+ytpQ==",
558
+            "version": "4.21.0",
559
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.0.tgz",
560
+            "integrity": "sha512-a1sR2zSK1B4eYkiZu17ZUZhmUQcKjk2/j9Me2IDjk1GHW7LB5Z35LEzj9iJch6gtUfsnvZs1ZNyDW2oZSThrkA==",
561
             "cpu": [
561
             "cpu": [
562
                 "arm64"
562
                 "arm64"
563
             ],
563
             ],
569
             ]
569
             ]
570
         },
570
         },
571
         "node_modules/@rollup/rollup-darwin-arm64": {
571
         "node_modules/@rollup/rollup-darwin-arm64": {
572
-            "version": "4.20.0",
573
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.20.0.tgz",
574
-            "integrity": "sha512-uFVfvzvsdGtlSLuL0ZlvPJvl6ZmrH4CBwLGEFPe7hUmf7htGAN+aXo43R/V6LATyxlKVC/m6UsLb7jbG+LG39Q==",
572
+            "version": "4.21.0",
573
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.0.tgz",
574
+            "integrity": "sha512-zOnKWLgDld/svhKO5PD9ozmL6roy5OQ5T4ThvdYZLpiOhEGY+dp2NwUmxK0Ld91LrbjrvtNAE0ERBwjqhZTRAA==",
575
             "cpu": [
575
             "cpu": [
576
                 "arm64"
576
                 "arm64"
577
             ],
577
             ],
583
             ]
583
             ]
584
         },
584
         },
585
         "node_modules/@rollup/rollup-darwin-x64": {
585
         "node_modules/@rollup/rollup-darwin-x64": {
586
-            "version": "4.20.0",
587
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.20.0.tgz",
588
-            "integrity": "sha512-xbrMDdlev53vNXexEa6l0LffojxhqDTBeL+VUxuuIXys4x6xyvbKq5XqTXBCEUA8ty8iEJblHvFaWRJTk/icAQ==",
586
+            "version": "4.21.0",
587
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.0.tgz",
588
+            "integrity": "sha512-7doS8br0xAkg48SKE2QNtMSFPFUlRdw9+votl27MvT46vo44ATBmdZdGysOevNELmZlfd+NEa0UYOA8f01WSrg==",
589
             "cpu": [
589
             "cpu": [
590
                 "x64"
590
                 "x64"
591
             ],
591
             ],
597
             ]
597
             ]
598
         },
598
         },
599
         "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
599
         "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
600
-            "version": "4.20.0",
601
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.20.0.tgz",
602
-            "integrity": "sha512-jMYvxZwGmoHFBTbr12Xc6wOdc2xA5tF5F2q6t7Rcfab68TT0n+r7dgawD4qhPEvasDsVpQi+MgDzj2faOLsZjA==",
600
+            "version": "4.21.0",
601
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.0.tgz",
602
+            "integrity": "sha512-pWJsfQjNWNGsoCq53KjMtwdJDmh/6NubwQcz52aEwLEuvx08bzcy6tOUuawAOncPnxz/3siRtd8hiQ32G1y8VA==",
603
             "cpu": [
603
             "cpu": [
604
                 "arm"
604
                 "arm"
605
             ],
605
             ],
611
             ]
611
             ]
612
         },
612
         },
613
         "node_modules/@rollup/rollup-linux-arm-musleabihf": {
613
         "node_modules/@rollup/rollup-linux-arm-musleabihf": {
614
-            "version": "4.20.0",
615
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.20.0.tgz",
616
-            "integrity": "sha512-1asSTl4HKuIHIB1GcdFHNNZhxAYEdqML/MW4QmPS4G0ivbEcBr1JKlFLKsIRqjSwOBkdItn3/ZDlyvZ/N6KPlw==",
614
+            "version": "4.21.0",
615
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.0.tgz",
616
+            "integrity": "sha512-efRIANsz3UHZrnZXuEvxS9LoCOWMGD1rweciD6uJQIx2myN3a8Im1FafZBzh7zk1RJ6oKcR16dU3UPldaKd83w==",
617
             "cpu": [
617
             "cpu": [
618
                 "arm"
618
                 "arm"
619
             ],
619
             ],
625
             ]
625
             ]
626
         },
626
         },
627
         "node_modules/@rollup/rollup-linux-arm64-gnu": {
627
         "node_modules/@rollup/rollup-linux-arm64-gnu": {
628
-            "version": "4.20.0",
629
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.20.0.tgz",
630
-            "integrity": "sha512-COBb8Bkx56KldOYJfMf6wKeYJrtJ9vEgBRAOkfw6Ens0tnmzPqvlpjZiLgkhg6cA3DGzCmLmmd319pmHvKWWlQ==",
628
+            "version": "4.21.0",
629
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.0.tgz",
630
+            "integrity": "sha512-ZrPhydkTVhyeGTW94WJ8pnl1uroqVHM3j3hjdquwAcWnmivjAwOYjTEAuEDeJvGX7xv3Z9GAvrBkEzCgHq9U1w==",
631
             "cpu": [
631
             "cpu": [
632
                 "arm64"
632
                 "arm64"
633
             ],
633
             ],
639
             ]
639
             ]
640
         },
640
         },
641
         "node_modules/@rollup/rollup-linux-arm64-musl": {
641
         "node_modules/@rollup/rollup-linux-arm64-musl": {
642
-            "version": "4.20.0",
643
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.20.0.tgz",
644
-            "integrity": "sha512-+it+mBSyMslVQa8wSPvBx53fYuZK/oLTu5RJoXogjk6x7Q7sz1GNRsXWjn6SwyJm8E/oMjNVwPhmNdIjwP135Q==",
642
+            "version": "4.21.0",
643
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.0.tgz",
644
+            "integrity": "sha512-cfaupqd+UEFeURmqNP2eEvXqgbSox/LHOyN9/d2pSdV8xTrjdg3NgOFJCtc1vQ/jEke1qD0IejbBfxleBPHnPw==",
645
             "cpu": [
645
             "cpu": [
646
                 "arm64"
646
                 "arm64"
647
             ],
647
             ],
653
             ]
653
             ]
654
         },
654
         },
655
         "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
655
         "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
656
-            "version": "4.20.0",
657
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.20.0.tgz",
658
-            "integrity": "sha512-yAMvqhPfGKsAxHN8I4+jE0CpLWD8cv4z7CK7BMmhjDuz606Q2tFKkWRY8bHR9JQXYcoLfopo5TTqzxgPUjUMfw==",
656
+            "version": "4.21.0",
657
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.0.tgz",
658
+            "integrity": "sha512-ZKPan1/RvAhrUylwBXC9t7B2hXdpb/ufeu22pG2psV7RN8roOfGurEghw1ySmX/CmDDHNTDDjY3lo9hRlgtaHg==",
659
             "cpu": [
659
             "cpu": [
660
                 "ppc64"
660
                 "ppc64"
661
             ],
661
             ],
667
             ]
667
             ]
668
         },
668
         },
669
         "node_modules/@rollup/rollup-linux-riscv64-gnu": {
669
         "node_modules/@rollup/rollup-linux-riscv64-gnu": {
670
-            "version": "4.20.0",
671
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.20.0.tgz",
672
-            "integrity": "sha512-qmuxFpfmi/2SUkAw95TtNq/w/I7Gpjurx609OOOV7U4vhvUhBcftcmXwl3rqAek+ADBwSjIC4IVNLiszoj3dPA==",
670
+            "version": "4.21.0",
671
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.0.tgz",
672
+            "integrity": "sha512-H1eRaCwd5E8eS8leiS+o/NqMdljkcb1d6r2h4fKSsCXQilLKArq6WS7XBLDu80Yz+nMqHVFDquwcVrQmGr28rg==",
673
             "cpu": [
673
             "cpu": [
674
                 "riscv64"
674
                 "riscv64"
675
             ],
675
             ],
681
             ]
681
             ]
682
         },
682
         },
683
         "node_modules/@rollup/rollup-linux-s390x-gnu": {
683
         "node_modules/@rollup/rollup-linux-s390x-gnu": {
684
-            "version": "4.20.0",
685
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.20.0.tgz",
686
-            "integrity": "sha512-I0BtGXddHSHjV1mqTNkgUZLnS3WtsqebAXv11D5BZE/gfw5KoyXSAXVqyJximQXNvNzUo4GKlCK/dIwXlz+jlg==",
684
+            "version": "4.21.0",
685
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.0.tgz",
686
+            "integrity": "sha512-zJ4hA+3b5tu8u7L58CCSI0A9N1vkfwPhWd/puGXwtZlsB5bTkwDNW/+JCU84+3QYmKpLi+XvHdmrlwUwDA6kqw==",
687
             "cpu": [
687
             "cpu": [
688
                 "s390x"
688
                 "s390x"
689
             ],
689
             ],
695
             ]
695
             ]
696
         },
696
         },
697
         "node_modules/@rollup/rollup-linux-x64-gnu": {
697
         "node_modules/@rollup/rollup-linux-x64-gnu": {
698
-            "version": "4.20.0",
699
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.20.0.tgz",
700
-            "integrity": "sha512-y+eoL2I3iphUg9tN9GB6ku1FA8kOfmF4oUEWhztDJ4KXJy1agk/9+pejOuZkNFhRwHAOxMsBPLbXPd6mJiCwew==",
698
+            "version": "4.21.0",
699
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.0.tgz",
700
+            "integrity": "sha512-e2hrvElFIh6kW/UNBQK/kzqMNY5mO+67YtEh9OA65RM5IJXYTWiXjX6fjIiPaqOkBthYF1EqgiZ6OXKcQsM0hg==",
701
             "cpu": [
701
             "cpu": [
702
                 "x64"
702
                 "x64"
703
             ],
703
             ],
709
             ]
709
             ]
710
         },
710
         },
711
         "node_modules/@rollup/rollup-linux-x64-musl": {
711
         "node_modules/@rollup/rollup-linux-x64-musl": {
712
-            "version": "4.20.0",
713
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.20.0.tgz",
714
-            "integrity": "sha512-hM3nhW40kBNYUkZb/r9k2FKK+/MnKglX7UYd4ZUy5DJs8/sMsIbqWK2piZtVGE3kcXVNj3B2IrUYROJMMCikNg==",
712
+            "version": "4.21.0",
713
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.0.tgz",
714
+            "integrity": "sha512-1vvmgDdUSebVGXWX2lIcgRebqfQSff0hMEkLJyakQ9JQUbLDkEaMsPTLOmyccyC6IJ/l3FZuJbmrBw/u0A0uCQ==",
715
             "cpu": [
715
             "cpu": [
716
                 "x64"
716
                 "x64"
717
             ],
717
             ],
723
             ]
723
             ]
724
         },
724
         },
725
         "node_modules/@rollup/rollup-win32-arm64-msvc": {
725
         "node_modules/@rollup/rollup-win32-arm64-msvc": {
726
-            "version": "4.20.0",
727
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.20.0.tgz",
728
-            "integrity": "sha512-psegMvP+Ik/Bg7QRJbv8w8PAytPA7Uo8fpFjXyCRHWm6Nt42L+JtoqH8eDQ5hRP7/XW2UiIriy1Z46jf0Oa1kA==",
726
+            "version": "4.21.0",
727
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.0.tgz",
728
+            "integrity": "sha512-s5oFkZ/hFcrlAyBTONFY1TWndfyre1wOMwU+6KCpm/iatybvrRgmZVM+vCFwxmC5ZhdlgfE0N4XorsDpi7/4XQ==",
729
             "cpu": [
729
             "cpu": [
730
                 "arm64"
730
                 "arm64"
731
             ],
731
             ],
737
             ]
737
             ]
738
         },
738
         },
739
         "node_modules/@rollup/rollup-win32-ia32-msvc": {
739
         "node_modules/@rollup/rollup-win32-ia32-msvc": {
740
-            "version": "4.20.0",
741
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.20.0.tgz",
742
-            "integrity": "sha512-GabekH3w4lgAJpVxkk7hUzUf2hICSQO0a/BLFA11/RMxQT92MabKAqyubzDZmMOC/hcJNlc+rrypzNzYl4Dx7A==",
740
+            "version": "4.21.0",
741
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.0.tgz",
742
+            "integrity": "sha512-G9+TEqRnAA6nbpqyUqgTiopmnfgnMkR3kMukFBDsiyy23LZvUCpiUwjTRx6ezYCjJODXrh52rBR9oXvm+Fp5wg==",
743
             "cpu": [
743
             "cpu": [
744
                 "ia32"
744
                 "ia32"
745
             ],
745
             ],
751
             ]
751
             ]
752
         },
752
         },
753
         "node_modules/@rollup/rollup-win32-x64-msvc": {
753
         "node_modules/@rollup/rollup-win32-x64-msvc": {
754
-            "version": "4.20.0",
755
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.20.0.tgz",
756
-            "integrity": "sha512-aJ1EJSuTdGnM6qbVC4B5DSmozPTqIag9fSzXRNNo+humQLG89XpPgdt16Ia56ORD7s+H8Pmyx44uczDQ0yDzpg==",
754
+            "version": "4.21.0",
755
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.0.tgz",
756
+            "integrity": "sha512-2jsCDZwtQvRhejHLfZ1JY6w6kEuEtfF9nzYsZxzSlNVKDX+DpsDJ+Rbjkm74nvg2rdx0gwBS+IMdvwJuq3S9pQ==",
757
             "cpu": [
757
             "cpu": [
758
                 "x64"
758
                 "x64"
759
             ],
759
             ],
1159
             "license": "MIT"
1159
             "license": "MIT"
1160
         },
1160
         },
1161
         "node_modules/electron-to-chromium": {
1161
         "node_modules/electron-to-chromium": {
1162
-            "version": "1.5.10",
1163
-            "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.10.tgz",
1164
-            "integrity": "sha512-C3RDERDjrNW262GCRvpoer3a0Ksd66CtgDLxMHhzShQ8fhL4kwnpVXsJPAKg9xJjIROXUbLBrvtOzVAjALMIWA==",
1162
+            "version": "1.5.11",
1163
+            "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.11.tgz",
1164
+            "integrity": "sha512-R1CccCDYqndR25CaXFd6hp/u9RaaMcftMkphmvuepXr5b1vfLkRml6aWVeBhXJ7rbevHkKEMJtz8XqPf7ffmew==",
1165
             "dev": true,
1165
             "dev": true,
1166
             "license": "ISC"
1166
             "license": "ISC"
1167
         },
1167
         },
2171
             }
2171
             }
2172
         },
2172
         },
2173
         "node_modules/rollup": {
2173
         "node_modules/rollup": {
2174
-            "version": "4.20.0",
2175
-            "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.20.0.tgz",
2176
-            "integrity": "sha512-6rbWBChcnSGzIlXeIdNIZTopKYad8ZG8ajhl78lGRLsI2rX8IkaotQhVas2Ma+GPxJav19wrSzvRvuiv0YKzWw==",
2174
+            "version": "4.21.0",
2175
+            "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.0.tgz",
2176
+            "integrity": "sha512-vo+S/lfA2lMS7rZ2Qoubi6I5hwZwzXeUIctILZLbHI+laNtvhhOIon2S1JksA5UEDQ7l3vberd0fxK44lTYjbQ==",
2177
             "dev": true,
2177
             "dev": true,
2178
             "license": "MIT",
2178
             "license": "MIT",
2179
             "dependencies": {
2179
             "dependencies": {
2187
                 "npm": ">=8.0.0"
2187
                 "npm": ">=8.0.0"
2188
             },
2188
             },
2189
             "optionalDependencies": {
2189
             "optionalDependencies": {
2190
-                "@rollup/rollup-android-arm-eabi": "4.20.0",
2191
-                "@rollup/rollup-android-arm64": "4.20.0",
2192
-                "@rollup/rollup-darwin-arm64": "4.20.0",
2193
-                "@rollup/rollup-darwin-x64": "4.20.0",
2194
-                "@rollup/rollup-linux-arm-gnueabihf": "4.20.0",
2195
-                "@rollup/rollup-linux-arm-musleabihf": "4.20.0",
2196
-                "@rollup/rollup-linux-arm64-gnu": "4.20.0",
2197
-                "@rollup/rollup-linux-arm64-musl": "4.20.0",
2198
-                "@rollup/rollup-linux-powerpc64le-gnu": "4.20.0",
2199
-                "@rollup/rollup-linux-riscv64-gnu": "4.20.0",
2200
-                "@rollup/rollup-linux-s390x-gnu": "4.20.0",
2201
-                "@rollup/rollup-linux-x64-gnu": "4.20.0",
2202
-                "@rollup/rollup-linux-x64-musl": "4.20.0",
2203
-                "@rollup/rollup-win32-arm64-msvc": "4.20.0",
2204
-                "@rollup/rollup-win32-ia32-msvc": "4.20.0",
2205
-                "@rollup/rollup-win32-x64-msvc": "4.20.0",
2190
+                "@rollup/rollup-android-arm-eabi": "4.21.0",
2191
+                "@rollup/rollup-android-arm64": "4.21.0",
2192
+                "@rollup/rollup-darwin-arm64": "4.21.0",
2193
+                "@rollup/rollup-darwin-x64": "4.21.0",
2194
+                "@rollup/rollup-linux-arm-gnueabihf": "4.21.0",
2195
+                "@rollup/rollup-linux-arm-musleabihf": "4.21.0",
2196
+                "@rollup/rollup-linux-arm64-gnu": "4.21.0",
2197
+                "@rollup/rollup-linux-arm64-musl": "4.21.0",
2198
+                "@rollup/rollup-linux-powerpc64le-gnu": "4.21.0",
2199
+                "@rollup/rollup-linux-riscv64-gnu": "4.21.0",
2200
+                "@rollup/rollup-linux-s390x-gnu": "4.21.0",
2201
+                "@rollup/rollup-linux-x64-gnu": "4.21.0",
2202
+                "@rollup/rollup-linux-x64-musl": "4.21.0",
2203
+                "@rollup/rollup-win32-arm64-msvc": "4.21.0",
2204
+                "@rollup/rollup-win32-ia32-msvc": "4.21.0",
2205
+                "@rollup/rollup-win32-x64-msvc": "4.21.0",
2206
                 "fsevents": "~2.3.2"
2206
                 "fsevents": "~2.3.2"
2207
             }
2207
             }
2208
         },
2208
         },

+ 3
- 3
resources/views/components/company/reports/report-pdf.blade.php 查看文件

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>
139
         </tr>
139
         </tr>
140
         </tbody>
140
         </tbody>
141
     @endforeach
141
     @endforeach
142
-    <tfoot>
142
+    <tbody>
143
     <tr class="table-footer-row">
143
     <tr class="table-footer-row">
144
         @foreach ($report->getOverallTotals() as $index => $total)
144
         @foreach ($report->getOverallTotals() as $index => $total)
145
             <td class="{{ $report->getAlignmentClass($index) }}">
145
             <td class="{{ $report->getAlignmentClass($index) }}">
147
             </td>
147
             </td>
148
         @endforeach
148
         @endforeach
149
     </tr>
149
     </tr>
150
-    </tfoot>
150
+    </tbody>
151
 </table>
151
 </table>
152
 </body>
152
 </body>
153
 </html>
153
 </html>

+ 10
- 5
resources/views/filament/company/pages/reports/account-transactions.blade.php 查看文件

1
 <x-filament-panels::page>
1
 <x-filament-panels::page>
2
-    <x-filament-tables::container>
3
-        <form wire:submit="loadReportData" class="p-6">
2
+    <x-filament::section>
3
+        <form wire:submit="loadReportData">
4
             {{ $this->form }}
4
             {{ $this->form }}
5
         </form>
5
         </form>
6
-        <div class="relative divide-y divide-gray-200 overflow-x-auto dark:divide-white/10 dark:border-t-white/10 min-h-64">
6
+    </x-filament::section>
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">
7
             <div wire:init="loadReportData" class="flex items-center justify-center w-full h-full absolute">
12
             <div wire:init="loadReportData" class="flex items-center justify-center w-full h-full absolute">
8
                 <div wire:loading wire:target="loadReportData">
13
                 <div wire:loading wire:target="loadReportData">
9
-                    <x-filament::loading-indicator class="p-6 text-primary-700 dark:text-primary-300" />
14
+                    <x-filament::loading-indicator class="p-6 text-primary-700 dark:text-primary-300"/>
10
                 </div>
15
                 </div>
11
             </div>
16
             </div>
12
 
17
 
13
             @if($this->reportLoaded)
18
             @if($this->reportLoaded)
14
                 <div wire:loading.remove wire:target="loadReportData">
19
                 <div wire:loading.remove wire:target="loadReportData">
15
                     @if($this->report && !$this->tableHasEmptyState())
20
                     @if($this->report && !$this->tableHasEmptyState())
16
-                        <x-company.tables.reports.account-transactions :report="$this->report" />
21
+                        <x-company.tables.reports.account-transactions :report="$this->report"/>
17
                     @else
22
                     @else
18
                         <x-filament-tables::empty-state
23
                         <x-filament-tables::empty-state
19
                             :actions="$this->getEmptyStateActions()"
24
                             :actions="$this->getEmptyStateActions()"

+ 30
- 21
resources/views/filament/company/pages/reports/detailed-report.blade.php 查看文件

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

+ 84
- 0
resources/views/filament/company/pages/reports/income-statement.blade.php 查看文件

1
+<x-filament-panels::page>
2
+    <x-filament::section>
3
+        <form wire:submit="loadReportData">
4
+            <div class="flex flex-col md:flex-row items-start md:items-center justify-between gap-4 md:gap-8">
5
+                <!-- Form Container -->
6
+                <div class="flex-grow">
7
+                    {{ $this->form }}
8
+                </div>
9
+
10
+                <!-- Grouping Button and Column Toggle -->
11
+                <div class="flex flex-col md:flex-row items-start md:items-center gap-4 md:gap-8 flex-shrink-0">
12
+                    @if($this->hasToggleableColumns())
13
+                        <x-filament-tables::column-toggle.dropdown
14
+                            :form="$this->toggleTableColumnForm"
15
+                            :trigger-action="$this->toggleColumnsAction"
16
+                        />
17
+                    @endif
18
+                    <x-filament::button type="submit" wire:target="loadReportData" class="flex-shrink-0">
19
+                        Update Report
20
+                    </x-filament::button>
21
+                </div>
22
+            </div>
23
+        </form>
24
+    </x-filament::section>
25
+
26
+
27
+    <x-filament::section>
28
+        <!-- Summary Section -->
29
+        @if($this->reportLoaded)
30
+            <div
31
+                class="flex flex-col md:flex-row items-center md:items-end text-center justify-center gap-4 md:gap-8">
32
+                @foreach($this->report->getSummary() as $summary)
33
+                    <div class="text-sm">
34
+                        <div class="text-gray-600 font-medium mb-2">{{ $summary['label'] }}</div>
35
+
36
+                        @php
37
+                            $isNetEarnings = $summary['label'] === 'Net Earnings';
38
+                            $isPositive = money($summary['value'], \App\Utilities\Currency\CurrencyAccessor::getDefaultCurrency())->isPositive();
39
+                        @endphp
40
+
41
+                        <strong
42
+                            @class([
43
+                                'text-lg',
44
+                                'text-green-700' => $isNetEarnings && $isPositive,
45
+                                'text-danger-700' => $isNetEarnings && ! $isPositive,
46
+                            ])
47
+                        >
48
+                            {{ $summary['value'] }}
49
+                        </strong>
50
+                    </div>
51
+
52
+                    @if(! $loop->last)
53
+                        <div class="flex items-center justify-center px-2">
54
+                            <strong class="text-lg">
55
+                                {{ $loop->remaining === 1 ? '=' : '-' }}
56
+                            </strong>
57
+                        </div>
58
+                    @endif
59
+                @endforeach
60
+            </div>
61
+        @endif
62
+    </x-filament::section>
63
+
64
+    <x-filament-tables::container>
65
+        <div class="es-table__header-ctn"></div>
66
+        <div wire:init="loadReportData"
67
+             class="relative divide-y divide-gray-200 overflow-x-auto dark:divide-white/10 dark:border-t-white/10 min-h-64">
68
+            @if($this->reportLoaded)
69
+                <div wire:loading.remove wire:target="loadReportData">
70
+                    @if($this->report)
71
+                        <x-company.tables.reports.detailed-report :report="$this->report"/>
72
+                    @endif
73
+                </div>
74
+            @else
75
+                <div class="absolute inset-0 flex items-center justify-center">
76
+                    <div wire:loading wire:target="loadReportData">
77
+                        <x-filament::loading-indicator class="p-6 text-primary-700 dark:text-primary-300"/>
78
+                    </div>
79
+                </div>
80
+            @endif
81
+        </div>
82
+        <div class="es-table__footer-ctn border-t border-gray-200"></div>
83
+    </x-filament-tables::container>
84
+</x-filament-panels::page>

正在加载...
取消
保存