Andrew Wallo 11 месяцев назад
Родитель
Сommit
add50b01d5

+ 6
- 0
app/Contracts/ExportableReport.php Просмотреть файл

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

+ 23
- 0
app/Contracts/HasSummaryReport.php Просмотреть файл

@@ -0,0 +1,23 @@
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
+}

+ 59
- 43
app/Services/ExportService.php Просмотреть файл

@@ -4,10 +4,13 @@ namespace App\Services;
4 4
 
5 5
 use App\Contracts\ExportableReport;
6 6
 use App\Models\Company;
7
-use App\Support\Column;
8 7
 use Barryvdh\Snappy\Facades\SnappyPdf;
9 8
 use Carbon\Exceptions\InvalidFormatException;
10 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 14
 use Symfony\Component\HttpFoundation\StreamedResponse;
12 15
 
13 16
 class ExportService
@@ -33,7 +36,8 @@ class ExportService
33 36
         ];
34 37
 
35 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 42
             if ($startDate && $endDate) {
39 43
                 $defaultStartDateFormat = Carbon::parse($startDate)->toDefaultDateFormat();
@@ -43,61 +47,34 @@ class ExportService
43 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 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 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 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 80
         return response()->streamDownload($callback, $filename, $headers);
@@ -129,4 +106,43 @@ class ExportService
129 106
             echo $pdf->inline();
130 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
 }

+ 5
- 16
app/Transformers/AccountBalanceReportTransformer.php Просмотреть файл

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

+ 5
- 16
app/Transformers/AccountTransactionReportTransformer.php Просмотреть файл

@@ -4,7 +4,6 @@ namespace App\Transformers;
4 4
 
5 5
 use App\DTO\AccountTransactionDTO;
6 6
 use App\DTO\ReportCategoryDTO;
7
-use App\Support\Column;
8 7
 
9 8
 class AccountTransactionReportTransformer extends BaseReportTransformer
10 9
 {
@@ -18,11 +17,6 @@ class AccountTransactionReportTransformer extends BaseReportTransformer
18 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 21
      * @return ReportCategoryDTO[]
28 22
      */
@@ -31,17 +25,12 @@ class AccountTransactionReportTransformer extends BaseReportTransformer
31 25
         $categories = [];
32 26
 
33 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 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,7 +39,7 @@ class AccountTransactionReportTransformer extends BaseReportTransformer
50 39
                 $row = [];
51 40
 
52 41
                 foreach ($this->getColumns() as $column) {
53
-                    $row[] = match ($column->getName()) {
42
+                    $row[$column->getName()] = match ($column->getName()) {
54 43
                         'date' => $transaction->date,
55 44
                         'description' => [
56 45
                             'id' => $transaction->id,

+ 34
- 33
app/Transformers/BalanceSheetReportTransformer.php Просмотреть файл

@@ -4,26 +4,28 @@ namespace App\Transformers;
4 4
 
5 5
 use App\DTO\AccountDTO;
6 6
 use App\DTO\ReportCategoryDTO;
7
+use App\DTO\ReportDTO;
7 8
 use App\DTO\ReportTypeDTO;
8
-use App\Support\Column;
9 9
 use App\Utilities\Currency\CurrencyAccessor;
10 10
 
11
-class BalanceSheetReportTransformer extends BaseReportTransformer
11
+class BalanceSheetReportTransformer extends SummaryReportTransformer
12 12
 {
13
-    protected string $totalAssets = '$0.00';
13
+    protected string $totalAssets;
14 14
 
15
-    protected string $totalLiabilities = '$0.00';
15
+    protected string $totalLiabilities;
16 16
 
17
-    protected string $totalEquity = '$0.00';
17
+    protected string $totalEquity;
18 18
 
19
-    public function getTitle(): string
19
+    public function __construct(ReportDTO $report)
20 20
     {
21
-        return 'Balance Sheet';
21
+        parent::__construct($report);
22
+
23
+        $this->calculateTotals();
22 24
     }
23 25
 
24
-    public function getHeaders(): array
26
+    public function getTitle(): string
25 27
     {
26
-        return array_map(fn (Column $column) => $column->getLabel(), $this->getColumns());
28
+        return 'Balance Sheet';
27 29
     }
28 30
 
29 31
     public function calculateTotals(): void
@@ -44,18 +46,15 @@ class BalanceSheetReportTransformer extends BaseReportTransformer
44 46
         foreach ($this->report->categories as $accountCategoryName => $accountCategory) {
45 47
             // Header for the main category
46 48
             $header = [];
47
-            foreach ($this->getColumns() as $index => $column) {
48
-                if ($column->getName() === 'account_name') {
49
-                    $header[$index] = $accountCategoryName;
50
-                } else {
51
-                    $header[$index] = '';
52
-                }
49
+
50
+            foreach ($this->getColumns() as $column) {
51
+                $header[$column->getName()] = $column->getName() === 'account_name' ? $accountCategoryName : '';
53 52
             }
54 53
 
55 54
             // Category-level summary
56 55
             $categorySummary = [];
57 56
             foreach ($this->getColumns() as $column) {
58
-                $categorySummary[] = match ($column->getName()) {
57
+                $categorySummary[$column->getName()] = match ($column->getName()) {
59 58
                     'account_name' => 'Total ' . $accountCategoryName,
60 59
                     'ending_balance' => $accountCategory->summary->endingBalance ?? '',
61 60
                     default => '',
@@ -65,8 +64,9 @@ class BalanceSheetReportTransformer extends BaseReportTransformer
65 64
             // Accounts directly under the main category
66 65
             $data = array_map(function (AccountDTO $account) {
67 66
                 $row = [];
67
+
68 68
                 foreach ($this->getColumns() as $column) {
69
-                    $row[] = match ($column->getName()) {
69
+                    $row[$column->getName()] = match ($column->getName()) {
70 70
                         'account_code' => $account->accountCode,
71 71
                         'account_name' => [
72 72
                             'name' => $account->accountName,
@@ -87,15 +87,16 @@ class BalanceSheetReportTransformer extends BaseReportTransformer
87 87
             foreach ($accountCategory->types as $typeName => $type) {
88 88
                 // Header for subcategory (type)
89 89
                 $typeHeader = [];
90
-                foreach ($this->getColumns() as $index => $column) {
91
-                    $typeHeader[$index] = $column->getName() === 'account_name' ? $typeName : '';
90
+                foreach ($this->getColumns() as $column) {
91
+                    $typeHeader[$column->getName()] = $column->getName() === 'account_name' ? $typeName : '';
92 92
                 }
93 93
 
94 94
                 // Account data for the subcategory
95 95
                 $typeData = array_map(function (AccountDTO $account) {
96 96
                     $row = [];
97
+
97 98
                     foreach ($this->getColumns() as $column) {
98
-                        $row[] = match ($column->getName()) {
99
+                        $row[$column->getName()] = match ($column->getName()) {
99 100
                             'account_code' => $account->accountCode,
100 101
                             'account_name' => [
101 102
                                 'name' => $account->accountName,
@@ -114,7 +115,7 @@ class BalanceSheetReportTransformer extends BaseReportTransformer
114 115
                 // Subcategory (type) summary
115 116
                 $typeSummary = [];
116 117
                 foreach ($this->getColumns() as $column) {
117
-                    $typeSummary[] = match ($column->getName()) {
118
+                    $typeSummary[$column->getName()] = match ($column->getName()) {
118 119
                         'account_name' => 'Total ' . $typeName,
119 120
                         'ending_balance' => $type->summary->endingBalance ?? '',
120 121
                         default => '',
@@ -145,21 +146,18 @@ class BalanceSheetReportTransformer extends BaseReportTransformer
145 146
     {
146 147
         $summaryCategories = [];
147 148
 
148
-        $columns = [
149
-            'account_name',
150
-            'ending_balance',
151
-        ];
149
+        $columns = $this->getSummaryColumns();
152 150
 
153 151
         foreach ($this->report->categories as $accountCategoryName => $accountCategory) {
154 152
             $categoryHeader = [];
155 153
 
156
-            foreach ($columns as $index => $column) {
157
-                $categoryHeader[$index] = $column === 'account_name' ? $accountCategoryName : '';
154
+            foreach ($columns as $column) {
155
+                $categoryHeader[$column->getName()] = $column->getName() === 'account_name' ? $accountCategoryName : '';
158 156
             }
159 157
 
160 158
             $categorySummary = [];
161 159
             foreach ($columns as $column) {
162
-                $categorySummary[] = match ($column) {
160
+                $categorySummary[$column->getName()] = match ($column->getName()) {
163 161
                     'account_name' => 'Total ' . $accountCategoryName,
164 162
                     'ending_balance' => $accountCategory->summary->endingBalance ?? '',
165 163
                     default => '',
@@ -175,13 +173,13 @@ class BalanceSheetReportTransformer extends BaseReportTransformer
175 173
                 $typeEndingBalance = 0;
176 174
 
177 175
                 foreach ($columns as $column) {
178
-                    $typeSummary[] = match ($column) {
176
+                    $typeSummary[$column->getName()] = match ($column->getName()) {
179 177
                         'account_name' => 'Total ' . $typeName,
180 178
                         'ending_balance' => $type->summary->endingBalance ?? '',
181 179
                         default => '',
182 180
                     };
183 181
 
184
-                    if ($column === 'ending_balance') {
182
+                    if ($column->getName() === 'ending_balance') {
185 183
                         $typeEndingBalance = $type->summary->endingBalance ?? 0;
186 184
                     }
187 185
                 }
@@ -207,7 +205,7 @@ class BalanceSheetReportTransformer extends BaseReportTransformer
207 205
                 // Add "Total Other Equity" as a new "type"
208 206
                 $otherEquitySummary = [];
209 207
                 foreach ($columns as $column) {
210
-                    $otherEquitySummary[] = match ($column) {
208
+                    $otherEquitySummary[$column->getName()] = match ($column->getName()) {
211 209
                         'account_name' => 'Total Other Equity',
212 210
                         'ending_balance' => $totalOtherEquity,
213 211
                         default => '',
@@ -238,10 +236,13 @@ class BalanceSheetReportTransformer extends BaseReportTransformer
238 236
         return [];
239 237
     }
240 238
 
241
-    public function getSummary(): array
239
+    public function getSummaryOverallTotals(): array
242 240
     {
243
-        $this->calculateTotals();
241
+        return [];
242
+    }
244 243
 
244
+    public function getSummary(): array
245
+    {
245 246
         return [
246 247
             [
247 248
                 'label' => 'Total Assets',

+ 32
- 10
app/Transformers/BaseReportTransformer.php Просмотреть файл

@@ -4,6 +4,7 @@ namespace App\Transformers;
4 4
 
5 5
 use App\Contracts\ExportableReport;
6 6
 use App\DTO\ReportDTO;
7
+use App\Support\Column;
7 8
 use Filament\Support\Enums\Alignment;
8 9
 
9 10
 abstract class BaseReportTransformer implements ExportableReport
@@ -15,9 +16,27 @@ abstract class BaseReportTransformer implements ExportableReport
15 16
         $this->report = $report;
16 17
     }
17 18
 
19
+    /**
20
+     * @return Column[]
21
+     */
18 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 42
     public function getPdfView(): string
@@ -40,18 +59,21 @@ abstract class BaseReportTransformer implements ExportableReport
40 59
         return 'left';
41 60
     }
42 61
 
43
-    public function getAlignmentClass(int $index): string
62
+    public function getAlignmentClass(string $columnName): string
44 63
     {
45
-        $column = $this->getColumns()[$index];
64
+        return once(function () use ($columnName): string {
65
+            /** @var Column|null $column */
66
+            $column = collect($this->getColumns())->first(fn (Column $column) => $column->getName() === $columnName);
46 67
 
47
-        if ($column->getAlignment() === Alignment::Right) {
48
-            return 'text-right';
49
-        }
68
+            if ($column?->getAlignment() === Alignment::Right) {
69
+                return 'text-right';
70
+            }
50 71
 
51
-        if ($column->getAlignment() === Alignment::Center) {
52
-            return 'text-center';
53
-        }
72
+            if ($column?->getAlignment() === Alignment::Center) {
73
+                return 'text-center';
74
+            }
54 75
 
55
-        return 'text-left';
76
+            return 'text-left';
77
+        });
56 78
     }
57 79
 }

+ 28
- 44
app/Transformers/IncomeStatementReportTransformer.php Просмотреть файл

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

+ 34
- 0
app/Transformers/SummaryReportTransformer.php Просмотреть файл

@@ -0,0 +1,34 @@
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,7 +4,6 @@ namespace App\Transformers;
4 4
 
5 5
 use App\DTO\AccountDTO;
6 6
 use App\DTO\ReportCategoryDTO;
7
-use App\Support\Column;
8 7
 
9 8
 class TrialBalanceReportTransformer extends BaseReportTransformer
10 9
 {
@@ -16,11 +15,6 @@ class TrialBalanceReportTransformer extends BaseReportTransformer
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 19
      * @return ReportCategoryDTO[]
26 20
      */
@@ -32,19 +26,15 @@ class TrialBalanceReportTransformer extends BaseReportTransformer
32 26
             // Initialize header with empty strings
33 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 33
             $data = array_map(function (AccountDTO $account) {
44 34
                 $row = [];
45 35
 
46 36
                 foreach ($this->getColumns() as $column) {
47
-                    $row[] = match ($column->getName()) {
37
+                    $row[$column->getName()] = match ($column->getName()) {
48 38
                         'account_code' => $account->accountCode,
49 39
                         'account_name' => [
50 40
                             'name' => $account->accountName,
@@ -64,7 +54,7 @@ class TrialBalanceReportTransformer extends BaseReportTransformer
64 54
             $summary = [];
65 55
 
66 56
             foreach ($this->getColumns() as $column) {
67
-                $summary[] = match ($column->getName()) {
57
+                $summary[$column->getName()] = match ($column->getName()) {
68 58
                     'account_name' => 'Total ' . $accountCategoryName,
69 59
                     'debit_balance' => $accountCategory->summary->debitBalance,
70 60
                     'credit_balance' => $accountCategory->summary->creditBalance,
@@ -87,7 +77,7 @@ class TrialBalanceReportTransformer extends BaseReportTransformer
87 77
         $totals = [];
88 78
 
89 79
         foreach ($this->getColumns() as $column) {
90
-            $totals[] = match ($column->getName()) {
80
+            $totals[$column->getName()] = match ($column->getName()) {
91 81
                 'account_name' => 'Total for all accounts',
92 82
                 'debit_balance' => $this->report->overallTotal->debitBalance,
93 83
                 'credit_balance' => $this->report->overallTotal->creditBalance,

+ 17
- 17
composer.lock Просмотреть файл

@@ -9925,16 +9925,16 @@
9925 9925
         },
9926 9926
         {
9927 9927
             "name": "pestphp/pest",
9928
-            "version": "v3.4.1",
9928
+            "version": "v3.4.2",
9929 9929
             "source": {
9930 9930
                 "type": "git",
9931 9931
                 "url": "https://github.com/pestphp/pest.git",
9932
-                "reference": "67f217852ce153134fbea8857487a3829904c086"
9932
+                "reference": "2903a7e62100171d5faf4e50cda755ffeadedc57"
9933 9933
             },
9934 9934
             "dist": {
9935 9935
                 "type": "zip",
9936
-                "url": "https://api.github.com/repos/pestphp/pest/zipball/67f217852ce153134fbea8857487a3829904c086",
9937
-                "reference": "67f217852ce153134fbea8857487a3829904c086",
9936
+                "url": "https://api.github.com/repos/pestphp/pest/zipball/2903a7e62100171d5faf4e50cda755ffeadedc57",
9937
+                "reference": "2903a7e62100171d5faf4e50cda755ffeadedc57",
9938 9938
                 "shasum": ""
9939 9939
             },
9940 9940
             "require": {
@@ -9945,11 +9945,11 @@
9945 9945
                 "pestphp/pest-plugin-arch": "^3.0.0",
9946 9946
                 "pestphp/pest-plugin-mutate": "^3.0.5",
9947 9947
                 "php": "^8.2.0",
9948
-                "phpunit/phpunit": "^11.4.1"
9948
+                "phpunit/phpunit": "^11.4.2"
9949 9949
             },
9950 9950
             "conflict": {
9951 9951
                 "filp/whoops": "<2.16.0",
9952
-                "phpunit/phpunit": ">11.4.1",
9952
+                "phpunit/phpunit": ">11.4.2",
9953 9953
                 "sebastian/exporter": "<6.0.0",
9954 9954
                 "webmozart/assert": "<1.11.0"
9955 9955
             },
@@ -10021,7 +10021,7 @@
10021 10021
             ],
10022 10022
             "support": {
10023 10023
                 "issues": "https://github.com/pestphp/pest/issues",
10024
-                "source": "https://github.com/pestphp/pest/tree/v3.4.1"
10024
+                "source": "https://github.com/pestphp/pest/tree/v3.4.2"
10025 10025
             },
10026 10026
             "funding": [
10027 10027
                 {
@@ -10033,7 +10033,7 @@
10033 10033
                     "type": "github"
10034 10034
                 }
10035 10035
             ],
10036
-            "time": "2024-10-15T16:17:09+00:00"
10036
+            "time": "2024-10-20T11:47:25+00:00"
10037 10037
         },
10038 10038
         {
10039 10039
             "name": "pestphp/pest-plugin",
@@ -11164,16 +11164,16 @@
11164 11164
         },
11165 11165
         {
11166 11166
             "name": "phpunit/phpunit",
11167
-            "version": "11.4.1",
11167
+            "version": "11.4.2",
11168 11168
             "source": {
11169 11169
                 "type": "git",
11170 11170
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
11171
-                "reference": "7875627f15f4da7e7f0823d1f323f7295a77334e"
11171
+                "reference": "1863643c3f04ad03dcb9c6996c294784cdda4805"
11172 11172
             },
11173 11173
             "dist": {
11174 11174
                 "type": "zip",
11175
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/7875627f15f4da7e7f0823d1f323f7295a77334e",
11176
-                "reference": "7875627f15f4da7e7f0823d1f323f7295a77334e",
11175
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/1863643c3f04ad03dcb9c6996c294784cdda4805",
11176
+                "reference": "1863643c3f04ad03dcb9c6996c294784cdda4805",
11177 11177
                 "shasum": ""
11178 11178
             },
11179 11179
             "require": {
@@ -11187,21 +11187,21 @@
11187 11187
                 "phar-io/manifest": "^2.0.4",
11188 11188
                 "phar-io/version": "^3.2.1",
11189 11189
                 "php": ">=8.2",
11190
-                "phpunit/php-code-coverage": "^11.0.6",
11190
+                "phpunit/php-code-coverage": "^11.0.7",
11191 11191
                 "phpunit/php-file-iterator": "^5.1.0",
11192 11192
                 "phpunit/php-invoker": "^5.0.1",
11193 11193
                 "phpunit/php-text-template": "^4.0.1",
11194 11194
                 "phpunit/php-timer": "^7.0.1",
11195 11195
                 "sebastian/cli-parser": "^3.0.2",
11196 11196
                 "sebastian/code-unit": "^3.0.1",
11197
-                "sebastian/comparator": "^6.1.0",
11197
+                "sebastian/comparator": "^6.1.1",
11198 11198
                 "sebastian/diff": "^6.0.2",
11199 11199
                 "sebastian/environment": "^7.2.0",
11200 11200
                 "sebastian/exporter": "^6.1.3",
11201 11201
                 "sebastian/global-state": "^7.0.2",
11202 11202
                 "sebastian/object-enumerator": "^6.0.1",
11203 11203
                 "sebastian/type": "^5.1.0",
11204
-                "sebastian/version": "^5.0.1"
11204
+                "sebastian/version": "^5.0.2"
11205 11205
             },
11206 11206
             "suggest": {
11207 11207
                 "ext-soap": "To be able to generate mocks based on WSDL files"
@@ -11244,7 +11244,7 @@
11244 11244
             "support": {
11245 11245
                 "issues": "https://github.com/sebastianbergmann/phpunit/issues",
11246 11246
                 "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
11247
-                "source": "https://github.com/sebastianbergmann/phpunit/tree/11.4.1"
11247
+                "source": "https://github.com/sebastianbergmann/phpunit/tree/11.4.2"
11248 11248
             },
11249 11249
             "funding": [
11250 11250
                 {
@@ -11260,7 +11260,7 @@
11260 11260
                     "type": "tidelift"
11261 11261
                 }
11262 11262
             ],
11263
-            "time": "2024-10-08T15:38:37+00:00"
11263
+            "time": "2024-10-19T13:05:19+00:00"
11264 11264
         },
11265 11265
         {
11266 11266
             "name": "rector/rector",

+ 1
- 5
resources/css/filament/company/theme.css Просмотреть файл

@@ -107,11 +107,7 @@
107 107
 }
108 108
 
109 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 113
 .es-table .es-table__rowgroup td:first-child {

+ 2
- 2
resources/views/components/company/tables/reports/account-transactions.blade.php Просмотреть файл

@@ -4,11 +4,11 @@
4 4
         <tbody class="divide-y divide-gray-200 whitespace-nowrap dark:divide-white/5">
5 5
         <!-- Category Header -->
6 6
         <tr class="bg-gray-50 dark:bg-white/5">
7
-            <x-filament-tables::cell colspan="{{ count($report->getHeaders()) }}" class="text-left">
7
+            <x-filament-tables::cell tag="th" colspan="{{ count($report->getHeaders()) }}" class="text-left">
8 8
                 <div class="px-3 py-3.5">
9 9
                     @foreach ($category->header as $headerRow)
10 10
                         <div
11
-                            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' }}">
12 12
                             @foreach ($headerRow as $headerValue)
13 13
                                 @if (!empty($headerValue))
14 14
                                     {{ $headerValue }}

+ 11
- 35
resources/views/components/company/tables/reports/balance-sheet-summary.blade.php Просмотреть файл

@@ -1,52 +1,28 @@
1 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
-        <th class="px-3 py-3.5 sm:first-of-type:ps-6 sm:last-of-type:pe-6 text-left">
5
-            <span class="text-sm font-semibold leading-6 text-gray-950 dark:text-white">
6
-                Accounts
7
-            </span>
8
-        </th>
9
-        <th class="px-3 py-3.5 sm:first-of-type:ps-6 sm:last-of-type:pe-6 text-right">
10
-            <span class="text-sm font-semibold leading-6 text-gray-950 dark:text-white">
11
-                Amount
12
-            </span>
13
-        </th>
14
-    </tr>
15
-    </thead>
2
+    <x-company.tables.header :headers="$report->getSummaryHeaders()" :alignment-class="[$report, 'getAlignmentClass']"/>
16 3
     @foreach($report->getSummaryCategories() as $accountCategory)
17 4
         <tbody class="divide-y divide-gray-200 whitespace-nowrap dark:divide-white/5">
18
-        <tr class="bg-gray-50 dark:bg-white/5">
19
-            @foreach($accountCategory->header as $accountCategoryHeaderIndex => $accountCategoryHeaderCell)
20
-                <th class="px-3 py-3.5 sm:first-of-type:ps-6 sm:last-of-type:pe-6 {{ $accountCategoryHeaderIndex === 0 ? 'text-left' : 'text-right' }}">
21
-                    <span class="text-sm font-semibold leading-6 text-gray-950 dark:text-white">
22
-                        {{ $accountCategoryHeaderCell }}
23
-                    </span>
24
-                </th>
25
-            @endforeach
26
-        </tr>
5
+        <x-company.tables.category-header :category-headers="$accountCategory->header"
6
+                                          :alignment-class="[$report, 'getAlignmentClass']"/>
27 7
         @foreach($accountCategory->types as $accountType)
28 8
             <tr>
29 9
                 @foreach($accountType->summary as $accountTypeSummaryIndex => $accountTypeSummaryCell)
30
-                    <x-filament-tables::cell
31
-                        class="{{ $accountTypeSummaryIndex === 0 ? 'text-left' : 'text-right' }} ps-8">
32
-                        <div class="px-3 py-4 text-sm leading-6 text-gray-950 dark:text-white">
33
-                            {{ $accountTypeSummaryCell }}
34
-                        </div>
35
-                    </x-filament-tables::cell>
10
+                    <x-company.tables.cell :alignment-class="$report->getAlignmentClass($accountTypeSummaryIndex)">
11
+                        {{ $accountTypeSummaryCell }}
12
+                    </x-company.tables.cell>
36 13
                 @endforeach
37 14
             </tr>
38 15
         @endforeach
39 16
         <tr>
40 17
             @foreach($accountCategory->summary as $accountCategorySummaryIndex => $accountCategorySummaryCell)
41
-                <x-filament-tables::cell class="{{ $accountCategorySummaryIndex === 0 ? 'text-left' : 'text-right' }}">
42
-                    <div class="px-3 py-4 text-sm leading-6 font-semibold text-gray-950 dark:text-white">
43
-                        {{ $accountCategorySummaryCell }}
44
-                    </div>
45
-                </x-filament-tables::cell>
18
+                <x-company.tables.cell :alignment-class="$report->getAlignmentClass($accountCategorySummaryIndex)"
19
+                                       bold="true">
20
+                    {{ $accountCategorySummaryCell }}
21
+                </x-company.tables.cell>
46 22
             @endforeach
47 23
         </tr>
48 24
         <tr>
49
-            <td colspan="2">
25
+            <td colspan="{{ count($report->getSummaryHeaders()) }}">
50 26
                 <div class="min-h-12"></div>
51 27
             </td>
52 28
         </tr>

+ 8
- 34
resources/views/components/company/tables/reports/income-statement-summary.blade.php Просмотреть файл

@@ -1,34 +1,19 @@
1 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
-        <th class="px-3 py-3.5 sm:first-of-type:ps-6 sm:last-of-type:pe-6 text-left">
5
-            <span class="text-sm font-semibold leading-6 text-gray-950 dark:text-white">
6
-                Accounts
7
-            </span>
8
-        </th>
9
-        <th class="px-3 py-3.5 sm:first-of-type:ps-6 sm:last-of-type:pe-6 text-right">
10
-            <span class="text-sm font-semibold leading-6 text-gray-950 dark:text-white">
11
-                Amount
12
-            </span>
13
-        </th>
14
-    </tr>
15
-    </thead>
2
+    <x-company.tables.header :headers="$report->getSummaryHeaders()" :alignment-class="[$report, 'getAlignmentClass']"/>
16 3
     @foreach($report->getSummaryCategories() as $accountCategory)
17 4
         <tbody class="divide-y divide-gray-200 whitespace-nowrap dark:divide-white/5">
18 5
         <tr>
19 6
             @foreach($accountCategory->summary as $accountCategorySummaryIndex => $accountCategorySummaryCell)
20
-                <x-filament-tables::cell class="{{ $accountCategorySummaryIndex === 0 ? 'text-left' : 'text-right' }}">
21
-                    <div class="px-3 py-4 text-sm leading-6 font-normal text-gray-950 dark:text-white">
22
-                        {{ $accountCategorySummaryCell }}
23
-                    </div>
24
-                </x-filament-tables::cell>
7
+                <x-company.tables.cell :alignment-class="$report->getAlignmentClass($accountCategorySummaryIndex)">
8
+                    {{ $accountCategorySummaryCell }}
9
+                </x-company.tables.cell>
25 10
             @endforeach
26 11
         </tr>
27 12
 
28
-        @if($accountCategory->header[0] === 'Cost of Goods Sold')
13
+        @if($accountCategory->header['account_name'] === 'Cost of Goods Sold')
29 14
             <tr class="bg-gray-50 dark:bg-white/5">
30 15
                 @foreach($report->getGrossProfit() as $grossProfitIndex => $grossProfitCell)
31
-                    <x-filament-tables::cell class="{{ $grossProfitIndex === 0 ? 'text-left' : 'text-right' }}">
16
+                    <x-filament-tables::cell class="{{ $report->getAlignmentClass($grossProfitIndex) }}">
32 17
                         <div class="px-3 py-2 text-sm leading-6 font-semibold text-gray-950 dark:text-white">
33 18
                             {{ $grossProfitCell }}
34 19
                         </div>
@@ -38,17 +23,6 @@
38 23
         @endif
39 24
         </tbody>
40 25
     @endforeach
41
-    @if(! empty($report->getSummaryTotals()))
42
-        <tfoot>
43
-        <tr class="bg-gray-50 dark:bg-white/5">
44
-            @foreach($report->getSummaryTotals() as $index => $total)
45
-                <x-filament-tables::cell class="{{ $index === 0 ? 'text-left' : 'text-right' }}">
46
-                    <div class="px-3 py-2 text-sm leading-6 font-semibold text-gray-950 dark:text-white">
47
-                        {{ $total }}
48
-                    </div>
49
-                </x-filament-tables::cell>
50
-            @endforeach
51
-        </tr>
52
-        </tfoot>
53
-    @endif
26
+    <x-company.tables.footer :totals="$report->getSummaryOverallTotals()"
27
+                             :alignment-class="[$report, 'getAlignmentClass']"/>
54 28
 </table>

Загрузка…
Отмена
Сохранить