浏览代码

wip: Accounting Module

3.x
Andrew Wallo 2 年前
父节点
当前提交
7891926d80
共有 87 个文件被更改,包括 5153 次插入2654 次删除
  1. 3
    1
      app/Filament/Pages/Companies.php
  2. 29
    0
      app/Filament/Pages/DefaultSetting.php
  3. 2
    1
      app/Filament/Pages/Employees.php
  4. 0
    1
      app/Filament/Pages/Users.php
  5. 12
    17
      app/Filament/Pages/Widgets/Companies/Charts/CompanyStatsOverview.php
  6. 0
    204
      app/Filament/Pages/Widgets/Companies/Charts/CumulativeCompanyData.php
  7. 154
    0
      app/Filament/Pages/Widgets/Companies/Charts/CumulativeGrowth.php
  8. 143
    0
      app/Filament/Pages/Widgets/Companies/Charts/CumulativeTotal.php
  9. 5
    7
      app/Filament/Pages/Widgets/Companies/Tables/Companies.php
  10. 0
    177
      app/Filament/Pages/Widgets/Employees/Charts/CumulativeEmployeeData.php
  11. 153
    0
      app/Filament/Pages/Widgets/Employees/Charts/CumulativeGrowth.php
  12. 163
    0
      app/Filament/Pages/Widgets/Employees/Charts/CumulativeRoles.php
  13. 6
    7
      app/Filament/Pages/Widgets/Employees/Tables/Employees.php
  14. 0
    181
      app/Filament/Pages/Widgets/Users/Charts/CumulativeUserData.php
  15. 1
    2
      app/Filament/Pages/Widgets/Users/Tables/Users.php
  16. 183
    114
      app/Filament/Resources/AccountResource.php
  17. 49
    17
      app/Filament/Resources/AccountResource/Pages/CreateAccount.php
  18. 53
    16
      app/Filament/Resources/AccountResource/Pages/EditAccount.php
  19. 140
    0
      app/Filament/Resources/CategoryResource.php
  20. 74
    0
      app/Filament/Resources/CategoryResource/Pages/CreateCategory.php
  21. 104
    0
      app/Filament/Resources/CategoryResource/Pages/EditCategory.php
  22. 19
    0
      app/Filament/Resources/CategoryResource/Pages/ListCategories.php
  23. 15
    15
      app/Filament/Resources/CurrencyResource.php
  24. 37
    18
      app/Filament/Resources/CurrencyResource/Pages/CreateCurrency.php
  25. 44
    18
      app/Filament/Resources/CurrencyResource/Pages/EditCurrency.php
  26. 83
    61
      app/Filament/Resources/CustomerResource.php
  27. 5
    2
      app/Filament/Resources/CustomerResource/Pages/CreateCustomer.php
  28. 14
    0
      app/Filament/Resources/CustomerResource/Pages/EditCustomer.php
  29. 186
    0
      app/Filament/Resources/DiscountResource.php
  30. 12
    0
      app/Filament/Resources/DiscountResource/Pages/CreateDiscount.php
  31. 19
    0
      app/Filament/Resources/DiscountResource/Pages/EditDiscount.php
  32. 19
    0
      app/Filament/Resources/DiscountResource/Pages/ListDiscounts.php
  33. 69
    6
      app/Filament/Resources/InvoiceResource.php
  34. 12
    0
      app/Filament/Resources/InvoiceResource/Pages/CreateInvoice.php
  35. 14
    0
      app/Filament/Resources/InvoiceResource/Pages/EditInvoice.php
  36. 49
    0
      app/Filament/Resources/InvoiceResource/RelationManagers/DocumentItemsRelationManager.php
  37. 199
    0
      app/Filament/Resources/TaxResource.php
  38. 74
    0
      app/Filament/Resources/TaxResource/Pages/CreateTax.php
  39. 106
    0
      app/Filament/Resources/TaxResource/Pages/EditTax.php
  40. 19
    0
      app/Filament/Resources/TaxResource/Pages/ListTaxes.php
  41. 156
    0
      app/Http/Livewire/DefaultSetting.php
  42. 47
    4
      app/Models/Banking/Account.php
  43. 51
    4
      app/Models/Contact.php
  44. 6
    0
      app/Models/Document/Document.php
  45. 6
    0
      app/Models/Document/DocumentItem.php
  46. 6
    0
      app/Models/Document/DocumentTotal.php
  47. 6
    0
      app/Models/Item.php
  48. 16
    0
      app/Models/Setting/Category.php
  49. 2
    0
      app/Models/Setting/Currency.php
  50. 172
    0
      app/Models/Setting/DefaultSetting.php
  51. 37
    1
      app/Models/Setting/Discount.php
  52. 34
    1
      app/Models/Setting/Tax.php
  53. 67
    0
      app/Observers/AccountObserver.php
  54. 13
    0
      app/Providers/AppServiceProvider.php
  55. 11
    0
      app/Providers/EventServiceProvider.php
  56. 27
    0
      app/Providers/SquireServiceProvider.php
  57. 60
    0
      app/Traits/HandlesRecordCreation.php
  58. 6
    1
      composer.json
  59. 1268
    515
      composer.lock
  60. 1
    0
      config/app.php
  61. 1
    1
      config/forms.php
  62. 158
    0
      config/livewire.php
  63. 23
    0
      config/tags.php
  64. 29
    1
      database/factories/CategoryFactory.php
  65. 23
    0
      database/factories/DefaultSettingFactory.php
  66. 3
    1
      database/factories/TaxFactory.php
  67. 1
    0
      database/migrations/2023_05_10_040940_create_currencies_table.php
  68. 11
    2
      database/migrations/2023_05_11_044321_create_accounts_table.php
  69. 2
    1
      database/migrations/2023_05_12_042255_create_categories_table.php
  70. 1
    0
      database/migrations/2023_05_19_042232_create_contacts_table.php
  71. 1
    0
      database/migrations/2023_05_20_080131_create_taxes_table.php
  72. 4
    0
      database/migrations/2023_05_21_163808_create_discounts_table.php
  73. 1
    0
      database/migrations/2023_05_22_073252_create_items_table.php
  74. 2
    1
      database/migrations/2023_05_23_141215_create_documents_table.php
  75. 1
    0
      database/migrations/2023_05_23_151550_create_document_items_table.php
  76. 1
    0
      database/migrations/2023_05_23_173412_create_document_totals_table.php
  77. 36
    0
      database/migrations/2023_05_26_025210_create_tag_tables.php
  78. 37
    0
      database/migrations/2023_07_03_054805_create_default_settings_table.php
  79. 70
    0
      database/seeders/CategorySeeder.php
  80. 68
    0
      database/seeders/TaxSeeder.php
  81. 52
    1247
      package-lock.json
  82. 154
    1
      resources/css/filament.css
  83. 3
    0
      resources/views/filament/pages/default-setting.blade.php
  84. 13
    0
      resources/views/livewire/default-setting.blade.php
  85. 7
    7
      resources/views/vendor/filament-companies/companies/company-employee-manager.blade.php
  86. 171
    0
      resources/views/vendor/forms/components/section.blade.php
  87. 19
    1
      tailwind.config.js

+ 3
- 1
app/Filament/Pages/Companies.php 查看文件

2
 
2
 
3
 namespace App\Filament\Pages;
3
 namespace App\Filament\Pages;
4
 
4
 
5
+use App\Filament\Pages\Widgets\Companies\Charts\CumulativeTotal;
5
 use Filament\Pages\Page;
6
 use Filament\Pages\Page;
6
 use Illuminate\Support\Facades\Auth;
7
 use Illuminate\Support\Facades\Auth;
7
 use Wallo\FilamentCompanies\FilamentCompanies;
8
 use Wallo\FilamentCompanies\FilamentCompanies;
26
     {
27
     {
27
         return [
28
         return [
28
             Widgets\Companies\Charts\CompanyStatsOverview::class,
29
             Widgets\Companies\Charts\CompanyStatsOverview::class,
29
-            Widgets\Companies\Charts\CumulativeCompanyData::class,
30
+            Widgets\Companies\Charts\CumulativeGrowth::class,
31
+            Widgets\Companies\Charts\CumulativeTotal::class,
30
             Widgets\Companies\Tables\Companies::class,
32
             Widgets\Companies\Tables\Companies::class,
31
         ];
33
         ];
32
     }
34
     }

+ 29
- 0
app/Filament/Pages/DefaultSetting.php 查看文件

1
+<?php
2
+
3
+namespace App\Filament\Pages;
4
+
5
+use Filament\Pages\Page;
6
+
7
+class DefaultSetting extends Page
8
+{
9
+    protected static ?string $navigationIcon = 'heroicon-o-adjustments';
10
+
11
+    protected static ?string $navigationLabel = 'Defaults';
12
+
13
+    protected static ?string $navigationGroup = 'Settings';
14
+
15
+    protected static ?string $title = 'Defaults';
16
+
17
+    protected static ?string $slug = 'defaults';
18
+
19
+    protected static string $view = 'filament.pages.default-setting';
20
+
21
+    public function getViewData(): array
22
+    {
23
+        return [
24
+            'defaultSetting' => \App\Models\Setting\DefaultSetting::first(),
25
+        ];
26
+    }
27
+
28
+
29
+}

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

25
     protected function getHeaderWidgets(): array
25
     protected function getHeaderWidgets(): array
26
     {
26
     {
27
         return [
27
         return [
28
-            Widgets\Employees\Charts\CumulativeEmployeeData::class,
28
+            Widgets\Employees\Charts\CumulativeRoles::class,
29
+            Widgets\Employees\Charts\CumulativeGrowth::class,
29
             Widgets\Employees\Tables\Employees::class,
30
             Widgets\Employees\Tables\Employees::class,
30
         ];
31
         ];
31
     }
32
     }

+ 0
- 1
app/Filament/Pages/Users.php 查看文件

25
     protected function getHeaderWidgets(): array
25
     protected function getHeaderWidgets(): array
26
     {
26
     {
27
         return [
27
         return [
28
-            Widgets\Users\Charts\CumulativeUserData::class,
29
             Widgets\Users\Tables\Users::class,
28
             Widgets\Users\Tables\Users::class,
30
         ];
29
         ];
31
     }
30
     }

+ 12
- 17
app/Filament/Pages/Widgets/Companies/Charts/CompanyStatsOverview.php 查看文件

7
 
7
 
8
 class CompanyStatsOverview extends StatsOverviewWidget
8
 class CompanyStatsOverview extends StatsOverviewWidget
9
 {
9
 {
10
-    protected int|string|array $columnSpan = 3;
11
-
12
-    protected function getColumns(): int
13
-    {
14
-        return 3;
15
-    }
10
+    protected static ?int $sort = 0;
16
 
11
 
17
     /**
12
     /**
18
      * Holt's Linear Trend Method
13
      * Holt's Linear Trend Method
23
         $trend = $data[1] - $data[0];
18
         $trend = $data[1] - $data[0];
24
 
19
 
25
         $forecast = [];
20
         $forecast = [];
26
-        for ($i = 0; $i < count($data); $i++) {
21
+        foreach ($data as $iValue) {
27
             $prev_level = $level;
22
             $prev_level = $level;
28
-            $level = $alpha * $data[$i] + (1 - $alpha) * ($prev_level + $trend);
23
+            $level = $alpha * $iValue + (1 - $alpha) * ($prev_level + $trend);
29
             $trend = $beta * ($level - $prev_level) + (1 - $beta) * $trend;
24
             $trend = $beta * ($level - $prev_level) + (1 - $beta) * $trend;
30
             $forecast[] = $level + $trend;
25
             $forecast[] = $level + $trend;
31
         }
26
         }
43
         $bestBeta = $beta;
38
         $bestBeta = $beta;
44
 
39
 
45
         // try different alpha and beta values within a reasonable range
40
         // try different alpha and beta values within a reasonable range
46
-        for ($alpha = 0.1; $alpha <= 1; $alpha += 0.1) {
47
-            for ($beta = 0.1; $beta <= 1; $beta += 0.1) {
48
-                $forecast = $this->holtLinearTrend($data, $alpha, $beta);
41
+        for ($testAlpha = 0.1; $testAlpha <= 1; $testAlpha += 0.1) {
42
+            for ($testBeta = 0.1; $testBeta <= 1; $testBeta += 0.1) {
43
+                $forecast = $this->holtLinearTrend($data, $testAlpha, $testBeta);
49
                 $error = $this->calculateError($data, $forecast);
44
                 $error = $this->calculateError($data, $forecast);
50
                 if ($error < $minError) {
45
                 if ($error < $minError) {
51
                     $minError = $error;
46
                     $minError = $error;
52
-                    $bestAlpha = $alpha;
53
-                    $bestBeta = $beta;
47
+                    $bestAlpha = $testAlpha;
48
+                    $bestBeta = $testBeta;
54
                 }
49
                 }
55
             }
50
             }
56
         }
51
         }
64
     protected function calculateError($data, $forecast): float
59
     protected function calculateError($data, $forecast): float
65
     {
60
     {
66
         $error = 0;
61
         $error = 0;
67
-        for ($i = 0; $i < count($data); $i++) {
68
-            $error += pow($data[$i] - $forecast[$i], 2);
62
+        for ($i = 0, $iMax = count($data); $i < $iMax; $i++) {
63
+            $error += ($data[$i] - $forecast[$i]) ** 2;
69
         }
64
         }
70
 
65
 
71
         return $error;
66
         return $error;
99
         // Get Weekly Data for Company Data
94
         // Get Weekly Data for Company Data
100
         $weeklyData = collect($weeks)->mapWithKeys(static function ($value, $week) use ($companyData) {
95
         $weeklyData = collect($weeks)->mapWithKeys(static function ($value, $week) use ($companyData) {
101
             $matchingData = $companyData->firstWhere('week', $week);
96
             $matchingData = $companyData->firstWhere('week', $week);
102
-            return [$week => $matchingData ? $matchingData->aggregate : 0];
97
+            return [$week => $matchingData->aggregate ?? 0];
103
         });
98
         });
104
 
99
 
105
         // Calculate total companies per week
100
         // Calculate total companies per week
111
         // Calculate new companies and percentage change per week
106
         // Calculate new companies and percentage change per week
112
         $newCompanies = [0];
107
         $newCompanies = [0];
113
         $weeklyPercentageChange = [0];
108
         $weeklyPercentageChange = [0];
114
-        for ($i = 1; $i < count($totalCompanies); $i++) {
109
+        for ($i = 1, $iMax = count($totalCompanies); $i < $iMax; $i++) {
115
             $newCompanies[] = $totalCompanies[$i] - $totalCompanies[$i - 1];
110
             $newCompanies[] = $totalCompanies[$i] - $totalCompanies[$i - 1];
116
             $weeklyPercentageChange[] = ($newCompanies[$i] / $totalCompanies[$i - 1]) * 100;
111
             $weeklyPercentageChange[] = ($newCompanies[$i] / $totalCompanies[$i - 1]) * 100;
117
         }
112
         }

+ 0
- 204
app/Filament/Pages/Widgets/Companies/Charts/CumulativeCompanyData.php 查看文件

1
-<?php
2
-
3
-namespace App\Filament\Pages\Widgets\Companies\Charts;
4
-
5
-use App\Models\Company;
6
-use Leandrocfe\FilamentApexCharts\Widgets\ApexChartWidget;
7
-
8
-class CumulativeCompanyData extends ApexChartWidget
9
-{
10
-    protected int|string|array $columnSpan = [
11
-        'md' => 2,
12
-        'xl' => 3,
13
-    ];
14
-
15
-    /**
16
-     * Chart Id
17
-     *
18
-     * @var string
19
-     */
20
-    protected static string $chartId = 'cumulative-company-data';
21
-
22
-    /**
23
-     * Widget Title
24
-     *
25
-     * @var string|null
26
-     */
27
-    protected static ?string $heading = 'Cumulative Company Data';
28
-
29
-    protected function getOptions(): array
30
-    {
31
-        $startOfYear = today()->startOfYear();
32
-        $today = today();
33
-
34
-        // Company data
35
-        $companyData = Company::selectRaw("COUNT(*) as aggregate, YEARWEEK(created_at, 3) as week")
36
-            ->whereBetween('created_at', [$startOfYear, $today])
37
-            ->groupByRaw('week')
38
-            ->get();
39
-
40
-        $weeks = [];
41
-        for ($week = $startOfYear->copy(); $week->lte($today); $week->addWeek()) {
42
-            $weeks[$week->format('oW')] = 0;
43
-        }
44
-
45
-        $weeklyData = collect($weeks)->mapWithKeys(static function ($value, $week) use ($companyData) {
46
-            $matchingData = $companyData->firstWhere('week', $week);
47
-            return [$week => $matchingData ? $matchingData->aggregate : 0];
48
-        });
49
-
50
-        $totalCompanies = $weeklyData->reduce(static function ($carry, $value) {
51
-            $carry[] = ($carry ? end($carry) : 0) + $value;
52
-            return $carry;
53
-        }, []);
54
-
55
-        // Calculate percentage increase and increase in companies per week
56
-        $newCompanies = [0];
57
-        $weeklyPercentageChange = [0];
58
-
59
-        for ($i = 1; $i < count($totalCompanies); $i++) {
60
-            $newCompanies[] = $totalCompanies[$i] - $totalCompanies[$i - 1];
61
-            $weeklyPercentageChange[] = ($newCompanies[$i] / $totalCompanies[$i - 1]) * 100;
62
-        }
63
-
64
-        // Calculate exponential smoothing for total companies
65
-        $alpha = 0.3; // Smoothing factor, between 0 and 1
66
-        $smoothedTotalCompanies = [];
67
-
68
-        $smoothedTotalCompanies[0] = $totalCompanies[0]; // Initialize the first smoothed value
69
-        for ($i = 1; $i < count($totalCompanies); $i++) {
70
-            $smoothedTotalCompanies[$i] = $alpha * $totalCompanies[$i] + (1 - $alpha) * $smoothedTotalCompanies[$i - 1];
71
-        }
72
-
73
-        $labels = collect($weeks)->keys()->map(static function ($week) {
74
-            $year = substr($week, 0, 4);
75
-            $weekNumber = substr($week, 4);
76
-
77
-            return today()->setISODate($year, $weekNumber)->format('M d');
78
-        });
79
-
80
-        return [
81
-            'chart' => [
82
-                'type' => 'line',
83
-                'height' => 350,
84
-                'stacked' => false,
85
-                'toolbar' => [
86
-                    'show' => false,
87
-                ],
88
-            ],
89
-            'series' => [
90
-                [
91
-                    'name' => 'Weekly Growth Rate',
92
-                    'type' => 'area',
93
-                    'data' => $weeklyPercentageChange,
94
-                ],
95
-                [
96
-                    'name' => 'New Companies',
97
-                    'type' => 'line',
98
-                    'data' => $newCompanies,
99
-                ],
100
-                [
101
-                    'name' => 'Smoothed Total Companies',
102
-                    'type' => 'line',
103
-                    'data' => $smoothedTotalCompanies,
104
-                ],
105
-                [
106
-                    'name' => 'Total Companies',
107
-                    'type' => 'column',
108
-                    'data' => $totalCompanies,
109
-                ],
110
-            ],
111
-            'xaxis' => [
112
-                'categories' => $labels,
113
-                'position' => 'bottom',
114
-                'labels' => [
115
-                    'style' => [
116
-                        'colors' => '#9ca3af',
117
-                        'fontWeight' => 600,
118
-                    ],
119
-                ],
120
-            ],
121
-            'yaxis' => [
122
-                [
123
-                    'seriesName' => 'Weekly Growth Rate',
124
-                    'decimalsInFloat' => 2,
125
-                    'labels' => [
126
-                        'style' => [
127
-                            'colors' => '#9ca3af',
128
-                            'fontWeight' => 600,
129
-                        ],
130
-                    ],
131
-                ],
132
-                [
133
-                    'seriesName' => 'New Companies',
134
-                    'decimalsInFloat' => 0,
135
-                    'opposite' => true,
136
-                    'labels' => [
137
-                        'style' => [
138
-                            'colors' => '#9ca3af',
139
-                            'fontWeight' => 600,
140
-                        ],
141
-                    ],
142
-                ],
143
-                [
144
-                    'seriesName' => 'Smoothed Total Companies',
145
-                    'decimalsInFloat' => 0,
146
-                    'opposite' => true,
147
-                    'labels' => [
148
-                        'style' => [
149
-                            'colors' => '#9ca3af',
150
-                            'fontWeight' => 600,
151
-                        ],
152
-                    ],
153
-                ],
154
-                [
155
-                    'seriesName' => 'Total Companies',
156
-                    'decimalsInFloat' => 0,
157
-                    'opposite' => true,
158
-                    'labels' => [
159
-                        'style' => [
160
-                            'colors' => '#9ca3af',
161
-                            'fontWeight' => 600,
162
-                        ],
163
-                    ],
164
-                ],
165
-            ],
166
-            'legend' => [
167
-                'position' => 'top',
168
-                'horizontalAlign' => 'center',
169
-                'labels' => [
170
-                    'colors' => '#9ca3af',
171
-                    'fontWeight' => 600,
172
-                ],
173
-            ],
174
-            'markers' => [
175
-                'size' => 0,
176
-            ],
177
-            'colors' => ['#d946ef', '#6d28d9', '#14b8a6', '#3b82f6'],
178
-            'fill' => [
179
-                'type' => 'gradient',
180
-                'gradient' => [
181
-                    'shade' => 'dark',
182
-                    'type' => 'vertical',
183
-                    'shadeIntensity' => 0.5,
184
-                    'gradientToColors' => ['#d946ef', '#6d28d9', '#14b8a6', '#0ea5e9'],
185
-                    'inverseColors' => false,
186
-                    'opacityFrom' => [0.85, 1, 1, 0.75],
187
-                    'opacityTo' => [0.4, 0.85, 0.85, 1],
188
-                    'stops' => [0, 20, 80, 100],
189
-                ],
190
-            ],
191
-            'stroke' => [
192
-                'width' => [2, 5, 5, 0],
193
-                'curve' => 'smooth',
194
-            ],
195
-            'plotOptions' => [
196
-                'bar' => [
197
-                    'borderRadius' => 5,
198
-                    'borderRadiusApplication' => 'end',
199
-                    'columnWidth' => '60%',
200
-                ],
201
-            ],
202
-        ];
203
-    }
204
-}

+ 154
- 0
app/Filament/Pages/Widgets/Companies/Charts/CumulativeGrowth.php 查看文件

1
+<?php
2
+
3
+namespace App\Filament\Pages\Widgets\Companies\Charts;
4
+
5
+use App\Models\Company;
6
+use Illuminate\Contracts\View\View;
7
+use Leandrocfe\FilamentApexCharts\Widgets\ApexChartWidget;
8
+
9
+class CumulativeGrowth extends ApexChartWidget
10
+{
11
+    protected static ?int $sort = 1;
12
+
13
+    /**
14
+     * Chart Id
15
+     *
16
+     * @var string
17
+     */
18
+    protected static string $chartId = 'cumulative-growth';
19
+
20
+    protected static ?string $pollingInterval = null;
21
+
22
+
23
+    protected function getOptions(): array
24
+    {
25
+        $startOfYear = today()->startOfYear();
26
+        $today = today();
27
+
28
+        // Company data
29
+        $companyData = Company::selectRaw("COUNT(*) as aggregate, DATE_FORMAT(created_at, '%Y%m') as month")
30
+            ->whereBetween('created_at', [$startOfYear, $today])
31
+            ->groupByRaw('month')
32
+            ->get();
33
+
34
+        $months = [];
35
+        for ($month = $startOfYear->copy(); $month->lte($today); $month->addMonth()) {
36
+            $months[$month->format('Ym')] = 0;
37
+        }
38
+
39
+        $monthlyData = collect($months)->mapWithKeys(static function ($value, $month) use ($companyData) {
40
+            $matchingData = $companyData->firstWhere('month', $month);
41
+            return [$month => $matchingData->aggregate ?? 0];
42
+        });
43
+
44
+        $totalCompanies = $monthlyData->reduce(static function ($carry, $value) {
45
+            $carry[] = ($carry ? end($carry) : 0) + $value;
46
+            return $carry;
47
+        }, []);
48
+
49
+        // Calculate percentage increase and increase in companies per month
50
+        $newCompanies = [0];
51
+        $monthlyPercentageChange = [0];
52
+
53
+        for ($i = 1, $iMax = count($totalCompanies); $i < $iMax; $i++) {
54
+            $newCompanies[] = $totalCompanies[$i] - $totalCompanies[$i - 1];
55
+            $monthlyPercentageChange[] = ($newCompanies[$i] / $totalCompanies[$i - 1]) * 100;
56
+        }
57
+
58
+        $labels = collect($months)->keys()->map(static function ($month) {
59
+            $year = substr($month, 0, 4);
60
+            $monthNumber = substr($month, 4);
61
+
62
+            return today()->startOfYear()->setDate($year, $monthNumber, 1)->format('M');
63
+        });
64
+
65
+        return [
66
+            'chart' => [
67
+                'type' => 'area',
68
+                'height' => 350,
69
+                'fontFamily' => 'inherit',
70
+                'toolbar' => [
71
+                    'show' => false,
72
+                ],
73
+            ],
74
+            'title' => [
75
+                'text' => 'Cumulative Growth',
76
+                'align' => 'left',
77
+                'margin' => 20,
78
+                'style' => [
79
+                    'fontSize' => '20px',
80
+                ],
81
+            ],
82
+            'subtitle' => [
83
+                'text' => 'Monthly',
84
+                'align' => 'left',
85
+                'margin' => 20,
86
+                'style' => [
87
+                    'fontSize' => '14px',
88
+                ],
89
+            ],
90
+            'series' => [
91
+                [
92
+                    'name' => 'Growth Rate',
93
+                    'data' => $monthlyPercentageChange,
94
+                ],
95
+                [
96
+                    'name' => 'New Companies',
97
+                    'data' => $newCompanies,
98
+                ],
99
+            ],
100
+            'xaxis' => [
101
+                'categories' => $labels,
102
+                'position' => 'bottom',
103
+                'labels' => [
104
+                    'show' => true,
105
+                    'style' => [
106
+                        'colors' => '#9ca3af',
107
+                    ],
108
+                ],
109
+            ],
110
+            'yaxis' => [
111
+                'decimalsInFloat' => 2,
112
+                'labels' => [
113
+                    'style' => [
114
+                        'colors' => '#9ca3af',
115
+                    ],
116
+                ],
117
+            ],
118
+            'dataLabels' => [
119
+                'enabled' => false,
120
+            ],
121
+            'legend' => [
122
+                'show' => true,
123
+                'position' => 'bottom',
124
+                'horizontalAlign' => 'center',
125
+                'floating' => false,
126
+                'labels' => [
127
+                    'useSeriesColors' => true,
128
+                ],
129
+                'markers' => [
130
+                    'width' => 30,
131
+                    'height' => 8,
132
+                    'radius' => 0,
133
+                ],
134
+            ],
135
+            'colors' => ['#454DC8', '#22d3ee'],
136
+            'fill' => [
137
+                'type' => 'gradient',
138
+                'gradient' => [
139
+                    'opacityFrom' => 0.6,
140
+                    'opacityTo' => 0.8,
141
+                ],
142
+            ],
143
+            'markers' => [
144
+                'size' => 4,
145
+                'hover' => [
146
+                    'size' => 7,
147
+                ],
148
+            ],
149
+            'stroke' => [
150
+                'curve' => 'smooth',
151
+            ],
152
+        ];
153
+    }
154
+}

+ 143
- 0
app/Filament/Pages/Widgets/Companies/Charts/CumulativeTotal.php 查看文件

1
+<?php
2
+
3
+namespace App\Filament\Pages\Widgets\Companies\Charts;
4
+
5
+use App\Models\Company;
6
+use Illuminate\Contracts\View\View;
7
+use Leandrocfe\FilamentApexCharts\Widgets\ApexChartWidget;
8
+
9
+class CumulativeTotal extends ApexChartWidget
10
+{
11
+    protected static ?int $sort = 2;
12
+
13
+    /**
14
+     * Chart Id
15
+     *
16
+     * @var string
17
+     */
18
+    protected static string $chartId = 'cumulative-total';
19
+
20
+    protected static ?string $pollingInterval = null;
21
+
22
+
23
+    protected function getOptions(): array
24
+    {
25
+        $startOfYear = today()->startOfYear();
26
+        $today = today();
27
+
28
+        // Company data
29
+        $companyData = Company::selectRaw("COUNT(*) as aggregate, DATE_FORMAT(created_at, '%Y%m') as month")
30
+            ->whereBetween('created_at', [$startOfYear, $today])
31
+            ->groupByRaw('month')
32
+            ->get();
33
+
34
+        $months = [];
35
+        for ($month = $startOfYear->copy(); $month->lte($today); $month->addMonth()) {
36
+            $months[$month->format('Ym')] = 0;
37
+        }
38
+
39
+        $monthlyData = collect($months)->mapWithKeys(static function ($value, $month) use ($companyData) {
40
+            $matchingData = $companyData->firstWhere('month', $month);
41
+            return [$month => $matchingData->aggregate ?? 0];
42
+        });
43
+
44
+        $totalCompanies = $monthlyData->reduce(static function ($carry, $value) {
45
+            $carry[] = ($carry ? end($carry) : 0) + $value;
46
+            return $carry;
47
+        }, []);
48
+
49
+        // Calculate exponential smoothing for total companies
50
+        $alpha = 0.3; // Smoothing factor, between 0 and 1
51
+        $smoothedTotalCompanies = [];
52
+
53
+        $smoothedTotalCompanies[0] = $totalCompanies[0]; // Initialize the first smoothed value
54
+        for ($i = 1, $iMax = count($totalCompanies); $i < $iMax; $i++) {
55
+            $smoothedTotalCompanies[$i] = $alpha * $totalCompanies[$i] + (1 - $alpha) * $smoothedTotalCompanies[$i - 1];
56
+        }
57
+
58
+        $labels = collect($months)->keys()->map(static function ($month) {
59
+            $year = substr($month, 0, 4);
60
+            $monthNumber = substr($month, 4);
61
+
62
+            return today()->startOfYear()->setDate($year, $monthNumber, 1)->format('M');
63
+        });
64
+
65
+        return [
66
+            'chart' => [
67
+                'type' => 'line',
68
+                'height' => 350,
69
+                'fontFamily' => 'inherit',
70
+                'toolbar' => [
71
+                    'show' => false,
72
+                ],
73
+            ],
74
+            'title' => [
75
+                'text' => 'Cumulative Total',
76
+                'align' => 'left',
77
+                'margin' => 20,
78
+                'style' => [
79
+                    'fontSize' => '20px',
80
+                ],
81
+            ],
82
+            'subtitle' => [
83
+                'text' => 'Monthly',
84
+                'align' => 'left',
85
+                'margin' => 20,
86
+                'style' => [
87
+                    'fontSize' => '14px',
88
+                ],
89
+            ],
90
+            'series' => [
91
+                [
92
+                    'name' => 'Total Companies',
93
+                    'data' => $totalCompanies,
94
+                ],
95
+                [
96
+                    'name' => 'Smoothed Total Companies',
97
+                    'data' => $smoothedTotalCompanies,
98
+                ],
99
+            ],
100
+            'xaxis' => [
101
+                'type' => 'category',
102
+                'categories' => $labels,
103
+                'position' => 'bottom',
104
+                'labels' => [
105
+                    'show' => true,
106
+                ],
107
+            ],
108
+            'yaxis' => [
109
+                'decimalsInFloat' => 0,
110
+                'labels' => [
111
+                    'show' => true,
112
+                ],
113
+            ],
114
+            'dataLabels' => [
115
+                'enabled' => false,
116
+            ],
117
+            'legend' => [
118
+                'show' => true,
119
+                'position' => 'bottom', // Placing the legend at the right side of the chart.
120
+                'horizontalAlign' => 'center', // Centering the legend items horizontally.
121
+                'floating' => false,
122
+                'labels' => [
123
+                    'useSeriesColors' => true,
124
+                ],
125
+                'markers' => [
126
+                    'width' => 30,
127
+                    'height' => 4,
128
+                    'radius' => 4,
129
+                ],
130
+            ],
131
+            'colors' => ['#454DC8', '#22d3ee'],
132
+            'markers' => [
133
+                'size' => 4,
134
+                'hover' => [
135
+                    'size' => 7,
136
+                ],
137
+            ],
138
+            'stroke' => [
139
+                'curve' => 'smooth',
140
+            ],
141
+        ];
142
+    }
143
+}

+ 5
- 7
app/Filament/Pages/Widgets/Companies/Tables/Companies.php 查看文件

13
 
13
 
14
 class Companies extends PageWidget
14
 class Companies extends PageWidget
15
 {
15
 {
16
-    protected int|string|array $columnSpan = [
17
-        'md' => 2,
18
-        'xl' => 3,
19
-    ];
16
+    protected int | string | array $columnSpan = 'full';
17
+
18
+    protected static ?int $sort = 3;
20
 
19
 
21
     protected function getTableQuery(): Builder|Relation
20
     protected function getTableQuery(): Builder|Relation
22
     {
21
     {
36
         return [
35
         return [
37
             Tables\Filters\SelectFilter::make('name')
36
             Tables\Filters\SelectFilter::make('name')
38
                 ->label('Owner')
37
                 ->label('Owner')
38
+                ->searchable()
39
                 ->relationship('owner', 'name'),
39
                 ->relationship('owner', 'name'),
40
             Tables\Filters\TernaryFilter::make('personal_company')
40
             Tables\Filters\TernaryFilter::make('personal_company')
41
                 ->label('Personal Company')
41
                 ->label('Personal Company')
52
                 ->searchable()
52
                 ->searchable()
53
                 ->grow(false),
53
                 ->grow(false),
54
             Tables\Columns\TextColumn::make('name')
54
             Tables\Columns\TextColumn::make('name')
55
-                ->weight('semibold')
56
                 ->label('Company')
55
                 ->label('Company')
57
                 ->sortable()
56
                 ->sortable()
58
                 ->searchable(),
57
                 ->searchable(),
59
             Tables\Columns\TextColumn::make('users_count')
58
             Tables\Columns\TextColumn::make('users_count')
60
                 ->label('Employees')
59
                 ->label('Employees')
61
-                ->weight('semibold')
62
                 ->counts('users')
60
                 ->counts('users')
63
                 ->sortable(),
61
                 ->sortable(),
64
             Tables\Columns\IconColumn::make('personal_company')
62
             Tables\Columns\IconColumn::make('personal_company')
71
                 ->falseColor('secondary')
69
                 ->falseColor('secondary')
72
         ];
70
         ];
73
     }
71
     }
74
-}
72
+}

+ 0
- 177
app/Filament/Pages/Widgets/Employees/Charts/CumulativeEmployeeData.php 查看文件

1
-<?php
2
-
3
-namespace App\Filament\Pages\Widgets\Employees\Charts;
4
-
5
-use App\Models\Employeeship;
6
-use Leandrocfe\FilamentApexCharts\Widgets\ApexChartWidget;
7
-
8
-class CumulativeEmployeeData extends ApexChartWidget
9
-{
10
-    protected int|string|array $columnSpan = [
11
-        'md' => 2,
12
-        'xl' => 3,
13
-    ];
14
-
15
-    /**
16
-     * Chart Id
17
-     *
18
-     * @var string
19
-     */
20
-    protected static string $chartId = 'cumulative-employee-data';
21
-
22
-    /**
23
-     * Widget Title
24
-     *
25
-     * @var string|null
26
-     */
27
-    protected static ?string $heading = 'Cumulative Employee Data';
28
-
29
-    protected function getOptions(): array
30
-    {
31
-        $startOfYear = today()->startOfYear();
32
-        $today = today();
33
-
34
-        // Company data
35
-        $employeeData = Employeeship::selectRaw("COUNT(*) as aggregate, role, YEARWEEK(created_at, 3) as week")
36
-            ->whereBetween('created_at', [$startOfYear, $today])
37
-            ->groupByRaw('week, role')
38
-            ->get();
39
-
40
-        $weeks = [];
41
-        for ($week = $startOfYear->copy(); $week->lte($today); $week->addWeek()) {
42
-            $weeks[$week->format('oW')] = 0;
43
-        }
44
-
45
-        $weeklyRoleData = collect($weeks)->mapWithKeys(static function ($value, $week) use ($employeeData) {
46
-            $editors = $employeeData->where('role', 'editor')->where('week', $week)->first();
47
-            $admins = $employeeData->where('role', 'admin')->where('week', $week)->first();
48
-
49
-            return [
50
-                $week => [
51
-                    'editors' => $editors ? $editors->aggregate : 0,
52
-                    'admins' => $admins ? $admins->aggregate : 0,
53
-                ]
54
-            ];
55
-        });
56
-
57
-        $cumulativeEditors = $weeklyRoleData->reduce(static function ($carry, $value) {
58
-            $carry[] = ($carry ? end($carry) : 0) + $value['editors'];
59
-            return $carry;
60
-        }, []);
61
-
62
-        $cumulativeAdmins = $weeklyRoleData->reduce(static function ($carry, $value) {
63
-            $carry[] = ($carry ? end($carry) : 0) + $value['admins'];
64
-            return $carry;
65
-        }, []);
66
-
67
-        $totalEmployees = [];
68
-        for ($i = 0; $i < count($cumulativeEditors); $i++) {
69
-            $totalEmployees[] = $cumulativeEditors[$i] + $cumulativeAdmins[$i];
70
-        }
71
-
72
-        $weeklyGrowthRate = [0];
73
-        for ($i = 1; $i < count($totalEmployees); $i++) {
74
-            $growth = (($totalEmployees[$i] - $totalEmployees[$i - 1]) / $totalEmployees[$i - 1]) * 100;
75
-            $weeklyGrowthRate[] = round($growth, 2);
76
-        }
77
-
78
-        $labels = collect($weeks)->keys()->map(static function ($week) {
79
-            $year = substr($week, 0, 4);
80
-            $weekNumber = substr($week, 4);
81
-
82
-            return today()->setISODate($year, $weekNumber)->format('M d');
83
-        });
84
-
85
-
86
-        return [
87
-            'chart' => [
88
-                'type' => 'area',
89
-                'height' => 350,
90
-                'stacked' => true,
91
-                'toolbar' => [
92
-                    'show' => false,
93
-                ],
94
-            ],
95
-            'series' => [
96
-                [
97
-                    'name' => 'Editors',
98
-                    'type' => 'bar',
99
-                    'data' => $cumulativeEditors,
100
-                ],
101
-                [
102
-                    'name' => 'Admins',
103
-                    'type' => 'bar',
104
-                    'data' => $cumulativeAdmins,
105
-                ],
106
-                [
107
-                    'name' => 'Weekly Growth Rate',
108
-                    'type' => 'area',
109
-                    'data' => $weeklyGrowthRate,
110
-                ],
111
-            ],
112
-            'stroke' => [
113
-                'width' => [0, 0, 2],
114
-                'curve' => 'smooth',
115
-            ],
116
-            'xaxis' => [
117
-                'categories' => $labels,
118
-                'position' => 'bottom',
119
-                'labels' => [
120
-                    'style' => [
121
-                        'colors' => '#9ca3af',
122
-                        'fontWeight' => 600,
123
-                    ],
124
-                ],
125
-            ],
126
-            'yaxis' => [
127
-                'labels' => [
128
-                    'style' => [
129
-                        'colors' => '#9ca3af',
130
-                        'fontWeight' => 600,
131
-                    ],
132
-                ],
133
-            ],
134
-            'legend' => [
135
-                'labels' => [
136
-                    'colors' => '#9ca3af',
137
-                    'fontWeight' => 600,
138
-                ],
139
-            ],
140
-            'colors' => ['#d946ef', '#6d28d9', '#3b82f6'],
141
-            'fill' => [
142
-                'type' => 'gradient',
143
-                'gradient' => [
144
-                    'shade' => 'dark',
145
-                    'type' => 'vertical',
146
-                    'shadeIntensity' => 0.2,
147
-                    'gradientToColors' => ['#ec4899', '#8b5cf6', '#0ea5e9'],
148
-                    'inverseColors' => true,
149
-                    'opacityFrom' => [0.85, 0.85, 0.85],
150
-                    'opacityTo' => [0.85, 0.85, 0.4],
151
-                    'stops' => [0, 100, 100],
152
-                ],
153
-            ],
154
-            'plotOptions' => [
155
-                'bar' => [
156
-                    'horizontal' => false,
157
-                    'borderRadius' => 5,
158
-                    'borderRadiusApplication' => 'end',
159
-                    'columnWidth' => '60%',
160
-                    'dataLabels' => [
161
-                        'total' => [
162
-                            'enabled' => true,
163
-                            'style' => [
164
-                                'color' => '#9ca3af',
165
-                                'fontSize' => '14px',
166
-                                'fontWeight' => 600,
167
-                            ],
168
-                        ]
169
-                    ],
170
-                ],
171
-            ],
172
-            'dataLabels' => [
173
-                'enabled' => false,
174
-            ],
175
-        ];
176
-    }
177
-}

+ 153
- 0
app/Filament/Pages/Widgets/Employees/Charts/CumulativeGrowth.php 查看文件

1
+<?php
2
+
3
+namespace App\Filament\Pages\Widgets\Employees\Charts;
4
+
5
+use App\Models\Employeeship;
6
+use Leandrocfe\FilamentApexCharts\Widgets\ApexChartWidget;
7
+
8
+class CumulativeGrowth extends ApexChartWidget
9
+{
10
+    protected static ?int $sort = 1;
11
+
12
+    /**
13
+     * Chart Id
14
+     *
15
+     * @var string
16
+     */
17
+    protected static string $chartId = 'cumulative-growth';
18
+
19
+    protected static ?string $pollingInterval = null;
20
+
21
+    protected function getOptions(): array
22
+    {
23
+        $startOfYear = today()->startOfYear();
24
+        $today = today();
25
+
26
+        // Company data
27
+        $employeeData = Employeeship::selectRaw("COUNT(*) as aggregate, DATE_FORMAT(created_at, '%Y%m') as month")
28
+            ->whereBetween('created_at', [$startOfYear, $today])
29
+            ->groupByRaw('month')
30
+            ->get();
31
+
32
+        $months = [];
33
+        for ($month = $startOfYear->copy(); $month->lte($today); $month->addMonth()) {
34
+            $months[$month->format('Ym')] = 0;
35
+        }
36
+
37
+        $monthlyData = collect($months)->mapWithKeys(static function ($value, $month) use ($employeeData) {
38
+            $matchingData = $employeeData->firstWhere('month', $month);
39
+            return [$month => $matchingData->aggregate ?? 0];
40
+        });
41
+
42
+        $totalEmployees = $monthlyData->reduce(static function ($carry, $value) {
43
+            $carry[] = ($carry ? end($carry) : 0) + $value;
44
+            return $carry;
45
+        }, []);
46
+
47
+        // Calculate percentage increase and increase in companies per month
48
+        $newEmployees = [0];
49
+        $monthlyPercentageChange = [0];
50
+
51
+        for ($i = 1, $iMax = count($totalEmployees); $i < $iMax; $i++) {
52
+            $newEmployees[] = $totalEmployees[$i] - $totalEmployees[$i - 1];
53
+            $monthlyPercentageChange[] = ($newEmployees[$i] / $totalEmployees[$i - 1]) * 100;
54
+        }
55
+
56
+        $labels = collect($months)->keys()->map(static function ($month) {
57
+            $year = substr($month, 0, 4);
58
+            $monthNumber = substr($month, 4);
59
+
60
+            return today()->startOfYear()->setDate($year, $monthNumber, 1)->format('M');
61
+        });
62
+
63
+
64
+        return [
65
+            'chart' => [
66
+                'type' => 'area',
67
+                'height' => 350,
68
+                'fontFamily' => 'inherit',
69
+                'toolbar' => [
70
+                    'show' => false,
71
+                ],
72
+            ],
73
+            'title' => [
74
+                'text' => 'Cumulative Growth',
75
+                'align' => 'left',
76
+                'margin' => 20,
77
+                'style' => [
78
+                    'fontSize' => '20px',
79
+                ],
80
+            ],
81
+            'subtitle' => [
82
+                'text' => 'Monthly',
83
+                'align' => 'left',
84
+                'margin' => 20,
85
+                'style' => [
86
+                    'fontSize' => '14px',
87
+                ],
88
+            ],
89
+            'series' => [
90
+                [
91
+                    'name' => 'Growth Rate',
92
+                    'data' => $monthlyPercentageChange,
93
+                ],
94
+                [
95
+                    'name' => 'New Employees',
96
+                    'data' => $newEmployees,
97
+                ],
98
+            ],
99
+            'xaxis' => [
100
+                'categories' => $labels,
101
+                'position' => 'bottom',
102
+                'labels' => [
103
+                    'show' => true,
104
+                    'style' => [
105
+                        'colors' => '#9ca3af',
106
+                    ],
107
+                ],
108
+            ],
109
+            'yaxis' => [
110
+                'decimalsInFloat' => 2,
111
+                'labels' => [
112
+                    'style' => [
113
+                        'colors' => '#9ca3af',
114
+                    ],
115
+                ],
116
+            ],
117
+            'dataLabels' => [
118
+                'enabled' => false,
119
+            ],
120
+            'legend' => [
121
+                'show' => true,
122
+                'position' => 'bottom',
123
+                'horizontalAlign' => 'center',
124
+                'floating' => false,
125
+                'labels' => [
126
+                    'useSeriesColors' => true,
127
+                ],
128
+                'markers' => [
129
+                    'width' => 30,
130
+                    'height' => 8,
131
+                    'radius' => 0,
132
+                ],
133
+            ],
134
+            'colors' => ['#454DC8', '#22d3ee'],
135
+            'fill' => [
136
+                'type' => 'gradient',
137
+                'gradient' => [
138
+                    'opacityFrom' => 0.6,
139
+                    'opacityTo' => 0.8,
140
+                ],
141
+            ],
142
+            'markers' => [
143
+                'size' => 4,
144
+                'hover' => [
145
+                    'size' => 7,
146
+                ],
147
+            ],
148
+            'stroke' => [
149
+                'curve' => 'smooth',
150
+            ],
151
+        ];
152
+    }
153
+}

+ 163
- 0
app/Filament/Pages/Widgets/Employees/Charts/CumulativeRoles.php 查看文件

1
+<?php
2
+
3
+namespace App\Filament\Pages\Widgets\Employees\Charts;
4
+
5
+use App\Models\Employeeship;
6
+use Leandrocfe\FilamentApexCharts\Widgets\ApexChartWidget;
7
+
8
+class CumulativeRoles extends ApexChartWidget
9
+{
10
+    protected static ?int $sort = 0;
11
+
12
+    /**
13
+     * Chart Id
14
+     *
15
+     * @var string
16
+     */
17
+    protected static string $chartId = 'cumulative-roles';
18
+
19
+    protected static ?string $pollingInterval = null;
20
+
21
+    protected function getOptions(): array
22
+    {
23
+        $startOfYear = today()->startOfYear();
24
+        $today = today();
25
+
26
+        // Company data
27
+        $employeeData = Employeeship::selectRaw("COUNT(*) as aggregate, role, DATE_FORMAT(created_at, '%Y%m') as month")
28
+            ->whereBetween('created_at', [$startOfYear, $today])
29
+            ->groupByRaw('month, role')
30
+            ->get();
31
+
32
+        $months = [];
33
+        for ($month = $startOfYear->copy(); $month->lte($today); $month->addMonth()) {
34
+            $months[$month->format('Ym')] = 0;
35
+        }
36
+
37
+        $monthlyRoleData = collect($months)->mapWithKeys(static function ($value, $month) use ($employeeData) {
38
+            $editors = $employeeData->where('role', 'editor')->where('month', $month)->first();
39
+            $admins = $employeeData->where('role', 'admin')->where('month', $month)->first();
40
+
41
+            return [
42
+                $month => [
43
+                    'editors' => $editors->aggregate ?? 0,
44
+                    'admins' => $admins->aggregate ?? 0,
45
+                ]
46
+            ];
47
+        });
48
+
49
+        $cumulativeEditors = $monthlyRoleData->reduce(static function ($carry, $value) {
50
+            $carry[] = ($carry ? end($carry) : 0) + $value['editors'];
51
+            return $carry;
52
+        }, []);
53
+
54
+        $cumulativeAdmins = $monthlyRoleData->reduce(static function ($carry, $value) {
55
+            $carry[] = ($carry ? end($carry) : 0) + $value['admins'];
56
+            return $carry;
57
+        }, []);
58
+
59
+        $labels = collect($months)->keys()->map(static function ($month) {
60
+            $year = substr($month, 0, 4);
61
+            $monthNumber = substr($month, 4);
62
+
63
+            return today()->startOfYear()->setDate($year, $monthNumber, 1)->format('M');
64
+        });
65
+
66
+        return [
67
+            'chart' => [
68
+                'type' => 'bar',
69
+                'height' => 350,
70
+                'fontFamily' => 'inherit',
71
+                'toolbar' => [
72
+                    'show' => false,
73
+                ],
74
+            ],
75
+            'title' => [
76
+                'text' => 'Cumulative Roles',
77
+                'align' => 'left',
78
+                'margin' => 20,
79
+                'style' => [
80
+                    'fontSize' => '20px',
81
+                ],
82
+            ],
83
+            'subtitle' => [
84
+                'text' => 'Monthly',
85
+                'align' => 'left',
86
+                'margin' => 20,
87
+                'style' => [
88
+                    'fontSize' => '14px',
89
+                ],
90
+            ],
91
+            'series' => [
92
+                [
93
+                    'name' => 'Editors',
94
+                    'data' => $cumulativeEditors,
95
+                ],
96
+                [
97
+                    'name' => 'Admins',
98
+                    'data' => $cumulativeAdmins,
99
+                ],
100
+            ],
101
+            'xaxis' => [
102
+                'categories' => $labels,
103
+                'position' => 'bottom',
104
+                'labels' => [
105
+                    'show' => true,
106
+                    'style' => [
107
+                        'colors' => '#9ca3af',
108
+                    ],
109
+                ],
110
+            ],
111
+            'yaxis' => [
112
+                'decimalsInFloat' => 2,
113
+                'labels' => [
114
+                    'style' => [
115
+                        'colors' => '#9ca3af',
116
+                    ],
117
+                ],
118
+            ],
119
+            'dataLabels' => [
120
+                'enabled' => false,
121
+            ],
122
+            'legend' => [
123
+                'show' => true,
124
+                'position' => 'bottom',
125
+                'horizontalAlign' => 'center',
126
+                'floating' => false,
127
+                'labels' => [
128
+                    'useSeriesColors' => true,
129
+                ],
130
+                'markers' => [
131
+                    'width' => 12,
132
+                    'height' => 12,
133
+                    'radius' => 0,
134
+                ],
135
+            ],
136
+            'tooltip' => [
137
+                'enabled' => true,
138
+                'shared' => true,
139
+                'intersect' => false,
140
+                'x' => [
141
+                    'show' => true,
142
+                ],
143
+            ],
144
+            'colors' => ['#454DC8', '#22d3ee'],
145
+            'plotOptions' => [
146
+                'bar' => [
147
+                    'horizontal' => false,
148
+                    'endingShape' => 'rounded',
149
+                    'columnWidth' => '55%',
150
+                ],
151
+            ],
152
+            'markers' => [
153
+                'size' => 4,
154
+                'hover' => [
155
+                    'size' => 7,
156
+                ],
157
+            ],
158
+            'stroke' => [
159
+                'curve' => 'smooth',
160
+            ],
161
+        ];
162
+    }
163
+}

+ 6
- 7
app/Filament/Pages/Widgets/Employees/Tables/Employees.php 查看文件

13
 
13
 
14
 class Employees extends PageWidget
14
 class Employees extends PageWidget
15
 {
15
 {
16
-    protected int|string|array $columnSpan = [
17
-        'md' => 2,
18
-        'xl' => 3,
19
-    ];
16
+    protected int | string | array $columnSpan = 'full';
17
+
18
+    protected static ?int $sort = 2;
20
 
19
 
21
     protected function getTableQuery(): Builder|Relation
20
     protected function getTableQuery(): Builder|Relation
22
     {
21
     {
36
         return [
35
         return [
37
             Tables\Filters\SelectFilter::make('name')
36
             Tables\Filters\SelectFilter::make('name')
38
                 ->label('Company')
37
                 ->label('Company')
38
+                ->searchable()
39
                 ->relationship('companies', 'name', static fn (Builder $query) => $query->whereHas('users')),
39
                 ->relationship('companies', 'name', static fn (Builder $query) => $query->whereHas('users')),
40
         ];
40
         ];
41
     }
41
     }
52
             Tables\Columns\TextColumn::make('companies.name')
52
             Tables\Columns\TextColumn::make('companies.name')
53
                 ->label('Company')
53
                 ->label('Company')
54
                 ->sortable()
54
                 ->sortable()
55
-                ->searchable()
56
-                ->weight('semibold'),
55
+                ->searchable(),
57
             Tables\Columns\BadgeColumn::make('employeeships.role')
56
             Tables\Columns\BadgeColumn::make('employeeships.role')
58
                 ->label('Role')
57
                 ->label('Role')
59
                 ->enum([
58
                 ->enum([
71
                 ->sortable(),
70
                 ->sortable(),
72
         ];
71
         ];
73
     }
72
     }
74
-}
73
+}

+ 0
- 181
app/Filament/Pages/Widgets/Users/Charts/CumulativeUserData.php 查看文件

1
-<?php
2
-
3
-namespace App\Filament\Pages\Widgets\Users\Charts;
4
-
5
-use App\Models\User;
6
-use Leandrocfe\FilamentApexCharts\Widgets\ApexChartWidget;
7
-
8
-class CumulativeUserData extends ApexChartWidget
9
-{
10
-    protected int|string|array $columnSpan = [
11
-        'md' => 2,
12
-        'xl' => 3,
13
-    ];
14
-
15
-    /**
16
-     * Chart Id
17
-     *
18
-     * @var string
19
-     */
20
-    protected static string $chartId = 'cumulative-user-data';
21
-
22
-    /**
23
-     * Widget Title
24
-     *
25
-     * @var string|null
26
-     */
27
-    protected static ?string $heading = 'Cumulative User Data';
28
-
29
-    protected function getOptions(): array
30
-    {
31
-        $startOfYear = today()->startOfYear();
32
-        $today = today();
33
-
34
-        // Company data
35
-        $userData = User::selectRaw("COUNT(*) as aggregate, YEARWEEK(created_at, 3) as week")
36
-            ->whereBetween('created_at', [$startOfYear, $today])
37
-            ->groupByRaw('week')
38
-            ->get();
39
-
40
-        $weeks = [];
41
-        for ($week = $startOfYear->copy(); $week->lte($today); $week->addWeek()) {
42
-            $weeks[$week->format('oW')] = 0;
43
-        }
44
-
45
-        $weeklyData = collect($weeks)->mapWithKeys(static function ($value, $week) use ($userData) {
46
-            $matchingData = $userData->firstWhere('week', $week);
47
-            return [$week => $matchingData ? $matchingData->aggregate : 0];
48
-        });
49
-
50
-        $totalUsers = $weeklyData->reduce(static function ($carry, $value) {
51
-            $carry[] = ($carry ? end($carry) : 0) + $value;
52
-            return $carry;
53
-        }, []);
54
-
55
-        // Calculate percentage increase and increase in companies per week
56
-        $newUsers = [0];
57
-        $weeklyGrowthRate = [0];
58
-
59
-        $cumulativeDataLength = count($totalUsers);
60
-        for ($key = 1; $key < $cumulativeDataLength; $key++) {
61
-            $value = $totalUsers[$key];
62
-            $previousWeekValue = $totalUsers[$key - 1];
63
-            $newUsers[] = $value - $previousWeekValue;
64
-            $weeklyGrowthRate[] = round((($value - $previousWeekValue) / $previousWeekValue) * 100, 2);
65
-        }
66
-
67
-        $labels = collect($weeks)->keys()->map(static function ($week) {
68
-            $year = substr($week, 0, 4);
69
-            $weekNumber = substr($week, 4);
70
-
71
-            return today()->setISODate($year, $weekNumber)->format('M d');
72
-        });
73
-
74
-        return [
75
-            'chart' => [
76
-                'type' => 'line',
77
-                'height' => 350,
78
-                'stacked' => false,
79
-                'toolbar' => [
80
-                    'show' => false,
81
-                ],
82
-            ],
83
-            'series' => [
84
-                [
85
-                    'name' => 'Weekly Growth Rate',
86
-                    'type' => 'area',
87
-                    'data' => $weeklyGrowthRate,
88
-                ],
89
-                [
90
-                    'name' => 'New Users',
91
-                    'type' => 'line',
92
-                    'data' => $newUsers,
93
-                ],
94
-                [
95
-                    'name' => 'Total Users',
96
-                    'type' => 'column',
97
-                    'data' => $totalUsers,
98
-                ],
99
-            ],
100
-            'xaxis' => [
101
-                'categories' => $labels,
102
-                'position' => 'bottom',
103
-                'labels' => [
104
-                    'style' => [
105
-                        'colors' => '#9ca3af',
106
-                        'fontWeight' => 600,
107
-                    ],
108
-                ],
109
-            ],
110
-            'yaxis' => [
111
-                [
112
-                    'seriesName' => 'Weekly Growth Rate',
113
-                    'labels' => [
114
-                        'style' => [
115
-                            'colors' => '#9ca3af',
116
-                            'fontWeight' => 600,
117
-                        ],
118
-                    ],
119
-                ],
120
-                [
121
-                    'seriesName' => 'New Users',
122
-                    'decimalsInFloat' => 0,
123
-                    'opposite' => true,
124
-                    'labels' => [
125
-                        'style' => [
126
-                            'colors' => '#9ca3af',
127
-                            'fontWeight' => 600,
128
-                        ],
129
-                    ],
130
-                ],
131
-                [
132
-                    'seriesName' => 'Total Users',
133
-                    'decimalsInFloat' => 0,
134
-                    'opposite' => true,
135
-                    'labels' => [
136
-                        'style' => [
137
-                            'colors' => '#9ca3af',
138
-                            'fontWeight' => 600,
139
-                        ],
140
-                    ],
141
-                ],
142
-            ],
143
-            'legend' => [
144
-                'position' => 'top',
145
-                'horizontalAlign' => 'center',
146
-                'labels' => [
147
-                    'colors' => '#9ca3af',
148
-                    'fontWeight' => 600,
149
-                ],
150
-            ],
151
-            'markers' => [
152
-                'size' => 0,
153
-            ],
154
-            'colors' => ['#6d28d9', '#3b82f6', '#d946ef'],
155
-            'fill' => [
156
-                'type' => 'gradient',
157
-                'gradient' => [
158
-                    'shade' => 'dark',
159
-                    'type' => 'vertical',
160
-                    'shadeIntensity' => 0.5,
161
-                    'gradientToColors' => ['#6d28d9', '#0ea5e9', '#d946ef'],
162
-                    'inverseColors' => false,
163
-                    'opacityFrom' => [0.85, 1, 0.75],
164
-                    'opacityTo' => [0.4, 0.85, 1],
165
-                    'stops' => [0, 20, 80, 100],
166
-                ],
167
-            ],
168
-            'stroke' => [
169
-                'width' => [2, 5, 0],
170
-                'curve' => 'smooth',
171
-            ],
172
-            'plotOptions' => [
173
-                'bar' => [
174
-                    'borderRadius' => 5,
175
-                    'borderRadiusApplication' => 'end',
176
-                    'columnWidth' => '60%',
177
-                ],
178
-            ],
179
-        ];
180
-    }
181
-}

+ 1
- 2
app/Filament/Pages/Widgets/Users/Tables/Users.php 查看文件

39
             Tables\Columns\TextColumn::make('owned_companies_count')
39
             Tables\Columns\TextColumn::make('owned_companies_count')
40
                 ->counts('ownedCompanies')
40
                 ->counts('ownedCompanies')
41
                 ->label('Companies')
41
                 ->label('Companies')
42
-                ->weight('semibold')
43
                 ->sortable(),
42
                 ->sortable(),
44
         ];
43
         ];
45
     }
44
     }
46
-}
45
+}

+ 183
- 114
app/Filament/Resources/AccountResource.php 查看文件

21
 use Illuminate\Support\Collection;
21
 use Illuminate\Support\Collection;
22
 use Illuminate\Support\Facades\Auth;
22
 use Illuminate\Support\Facades\Auth;
23
 use Illuminate\Support\Facades\DB;
23
 use Illuminate\Support\Facades\DB;
24
-use Illuminate\Validation\Rule;
25
 use Illuminate\Validation\Rules\Unique;
24
 use Illuminate\Validation\Rules\Unique;
25
+use Wallo\FilamentSelectify\Components\ToggleButton;
26
 
26
 
27
 class AccountResource extends Resource
27
 class AccountResource extends Resource
28
 {
28
 {
41
     {
41
     {
42
         return $form
42
         return $form
43
             ->schema([
43
             ->schema([
44
-                Forms\Components\Section::make('General')
44
+                Forms\Components\Group::make()
45
                     ->schema([
45
                     ->schema([
46
-                        Forms\Components\Radio::make('type')
47
-                            ->label('Type')
48
-                            ->options(Account::getAccountTypes())
49
-                            ->inline()
50
-                            ->default('bank')
51
-                            ->reactive()
52
-                            ->afterStateUpdated(static fn (Closure $set, $state) => $state === 'card' ? $set('enabled', 'hidden'): $set('enabled', null))
53
-                            ->required()
54
-                            ->columnSpanFull(),
55
-                        Forms\Components\TextInput::make('name')
56
-                            ->label('Name')
57
-                            ->maxLength(100)
58
-                            ->required(),
59
-                        Forms\Components\TextInput::make('number')
60
-                            ->label('Account Number')
61
-                            ->unique(callback: static function (Unique $rule, $state) {
62
-                                $companyId = Auth::user()->currentCompany->id;
63
-
64
-                                return $rule->where('company_id', $companyId)->where('number', $state);
65
-                            }, ignoreRecord: true)
66
-                            ->maxLength(20)
67
-                            ->required(),
68
-                        Forms\Components\Select::make('currency_code')
69
-                            ->label('Currency')
70
-                            ->relationship('currency', 'name', static fn (Builder $query) => $query->where('company_id', Auth::user()->currentCompany->id))
71
-                            ->preload()
72
-                            ->default(Account::getDefaultCurrencyCode())
73
-                            ->searchable()
74
-                            ->reactive()
75
-                            ->required()
76
-                            ->createOptionForm([
77
-                                Forms\Components\Select::make('currency.code')
78
-                                    ->label('Code')
46
+                        Forms\Components\Section::make('Account Information')
47
+                            ->schema([
48
+                                Forms\Components\Select::make('type')
49
+                                    ->label('Type')
50
+                                    ->options(Account::getAccountTypes())
79
                                     ->searchable()
51
                                     ->searchable()
80
-                                    ->options(Account::getCurrencyCodes())
52
+                                    ->default('checking')
81
                                     ->reactive()
53
                                     ->reactive()
82
-                                    ->afterStateUpdated(static function (callable $set, $state) {
83
-                                        $code = $state;
84
-                                        $name = config("money.{$code}.name");
85
-                                        $set('currency.name', $name);
86
-                                    })
54
+                                    ->disablePlaceholderSelection()
87
                                     ->required(),
55
                                     ->required(),
88
-                                Forms\Components\TextInput::make('currency.name')
56
+                                Forms\Components\TextInput::make('name')
89
                                     ->label('Name')
57
                                     ->label('Name')
90
                                     ->maxLength(100)
58
                                     ->maxLength(100)
91
                                     ->required(),
59
                                     ->required(),
92
-                                Forms\Components\TextInput::make('currency.rate')
93
-                                    ->label('Rate')
94
-                                    ->numeric()
95
-                                    ->mask(static fn (Forms\Components\TextInput\Mask $mask) => $mask
96
-                                        ->numeric()
97
-                                        ->decimalPlaces(4)
98
-                                        ->signed(false)
99
-                                        ->padFractionalZeros(false)
100
-                                        ->normalizeZeros(false)
101
-                                        ->minValue(0.0001)
102
-                                        ->maxValue(999999.9999)
103
-                                        ->lazyPlaceholder(false))
60
+                                Forms\Components\TextInput::make('number')
61
+                                    ->label('Account Number')
62
+                                    ->unique(callback: static function (Unique $rule, $state) {
63
+                                        $companyId = Auth::user()->currentCompany->id;
64
+
65
+                                        return $rule->where('company_id', $companyId)->where('number', $state);
66
+                                    }, ignoreRecord: true)
67
+                                    ->maxLength(20)
68
+                                    ->validationAttribute('account number')
104
                                     ->required(),
69
                                     ->required(),
105
-                            ])->createOptionAction(static function (Forms\Components\Actions\Action $action) {
106
-                                return $action
107
-                                    ->label('Add Currency')
108
-                                    ->modalHeading('Add Currency')
109
-                                    ->modalButton('Add')
110
-                                    ->action(static function (array $data) {
111
-                                        return DB::transaction(static function () use ($data) {
112
-                                            $code = $data['currency']['code'];
113
-                                            $name = $data['currency']['name'];
114
-                                            $rate = $data['currency']['rate'];
70
+                                ToggleButton::make('enabled')
71
+                                    ->label('Default Account')
72
+                                    ->hidden(static fn (Closure $get) => $get('type') === 'credit_card')
73
+                                    ->offColor('danger')
74
+                                    ->onColor('primary'),
75
+                            ])->columns(),
76
+                        Forms\Components\Section::make('Currency & Balance')
77
+                            ->schema([
78
+                                Forms\Components\Select::make('currency_code')
79
+                                    ->label('Currency')
80
+                                    ->relationship('currency', 'name', static fn (Builder $query) => $query->where('company_id', Auth::user()->currentCompany->id))
81
+                                    ->preload()
82
+                                    ->default(Account::getDefaultCurrencyCode())
83
+                                    ->searchable()
84
+                                    ->reactive()
85
+                                    ->required()
86
+                                    ->createOptionForm([
87
+                                        Forms\Components\Select::make('currency.code')
88
+                                            ->label('Code')
89
+                                            ->searchable()
90
+                                            ->options(Account::getCurrencyCodes())
91
+                                            ->reactive()
92
+                                            ->afterStateUpdated(static function (callable $set, $state) {
93
+                                                $code = $state;
94
+                                                $name = config("money.{$code}.name");
95
+                                                $set('currency.name', $name);
96
+                                            })
97
+                                            ->required(),
98
+                                        Forms\Components\TextInput::make('currency.name')
99
+                                            ->label('Name')
100
+                                            ->maxLength(100)
101
+                                            ->required(),
102
+                                        Forms\Components\TextInput::make('currency.rate')
103
+                                            ->label('Rate')
104
+                                            ->numeric()
105
+                                            ->mask(static fn (Forms\Components\TextInput\Mask $mask) => $mask
106
+                                                ->numeric()
107
+                                                ->decimalPlaces(4)
108
+                                                ->signed(false)
109
+                                                ->padFractionalZeros(false)
110
+                                                ->normalizeZeros(false)
111
+                                                ->minValue(0.0001)
112
+                                                ->maxValue(999999.9999)
113
+                                                ->lazyPlaceholder(false))
114
+                                            ->required(),
115
+                                    ])->createOptionAction(static function (Forms\Components\Actions\Action $action) {
116
+                                        return $action
117
+                                            ->label('Add Currency')
118
+                                            ->modalHeading('Add Currency')
119
+                                            ->modalButton('Add')
120
+                                            ->action(static function (array $data) {
121
+                                                return DB::transaction(static function () use ($data) {
122
+                                                    $code = $data['currency']['code'];
123
+                                                    $name = $data['currency']['name'];
124
+                                                    $rate = $data['currency']['rate'];
125
+
126
+                                                    return (new CreateCurrencyFromAccount())->create($code, $name, $rate);
127
+                                                });
128
+                                            });
129
+                                    }),
130
+                                Forms\Components\TextInput::make('opening_balance')
131
+                                    ->label('Opening Balance')
132
+                                    ->required()
133
+                                    ->default('0')
134
+                                    ->numeric()
135
+                                    ->mask(static fn (Forms\Components\TextInput\Mask $mask, Closure $get) => $mask
136
+                                        ->patternBlocks([
137
+                                            'money' => static fn (Mask $mask) => $mask
138
+                                                ->numeric()
139
+                                                ->decimalPlaces(config('money.' . $get('currency_code') . '.precision'))
140
+                                                ->decimalSeparator(config('money.' . $get('currency_code') . '.decimal_mark'))
141
+                                                ->thousandsSeparator(config('money.' . $get('currency_code') . '.thousands_separator'))
142
+                                                ->signed()
143
+                                                ->padFractionalZeros()
144
+                                                ->normalizeZeros(),
145
+                                    ])
146
+                                    ->pattern(config('money.' . $get('currency_code') . '.symbol_first') ? config('money.' . $get('currency_code') . '.symbol') . 'money' : 'money' . config('money.' . $get('currency_code') . '.symbol'))
147
+                                    ->lazyPlaceholder(false)),
148
+                            ])->columns(),
149
+                        Forms\Components\Tabs::make('Account Specifications')
150
+                            ->tabs([
151
+                                Forms\Components\Tabs\Tab::make('Bank Information')
152
+                                    ->icon('heroicon-o-credit-card')
153
+                                    ->schema([
154
+                                        Forms\Components\TextInput::make('bank_name')
155
+                                            ->label('Bank Name')
156
+                                            ->maxLength(100),
157
+                                        Forms\Components\TextInput::make('bank_phone')
158
+                                            ->label('Bank Phone')
159
+                                            ->tel()
160
+                                            ->maxLength(20),
161
+                                        Forms\Components\Textarea::make('bank_address')
162
+                                            ->label('Bank Address')
163
+                                            ->columnSpanFull(),
164
+                                    ])->columns(),
165
+                                Forms\Components\Tabs\Tab::make('Additional Information')
166
+                                    ->icon('heroicon-o-information-circle')
167
+                                    ->schema([
168
+                                        Forms\Components\TextInput::make('description')
169
+                                            ->label('Description')
170
+                                            ->maxLength(100),
171
+                                        Forms\Components\SpatieTagsInput::make('tags')
172
+                                            ->label('Tags')
173
+                                            ->placeholder('Enter tags...')
174
+                                            ->type('statuses')
175
+                                            ->suggestions([
176
+                                                'Business',
177
+                                                'Personal',
178
+                                                'College Fund',
179
+                                            ]),
180
+                                        Forms\Components\MarkdownEditor::make('notes')
181
+                                            ->label('Notes')
182
+                                            ->columnSpanFull(),
183
+                                    ])->columns(),
184
+                            ]),
185
+                    ])->columnSpan(['lg' => 2]),
115
 
186
 
116
-                                            return (new CreateCurrencyFromAccount())->create($code, $name, $rate);
117
-                                        });
118
-                                    });
119
-                            }),
120
-                        Forms\Components\TextInput::make('opening_balance')
121
-                            ->label('Opening Balance')
122
-                            ->required()
123
-                            ->default('0')
124
-                            ->numeric()
125
-                            ->mask(static fn (Forms\Components\TextInput\Mask $mask, Closure $get) => $mask
126
-                                ->patternBlocks([
127
-                                    'money' => static fn (Mask $mask) => $mask
128
-                                        ->numeric()
129
-                                        ->decimalPlaces(config('money.' . $get('currency_code') . '.precision'))
130
-                                        ->decimalSeparator(config('money.' . $get('currency_code') . '.decimal_mark'))
131
-                                        ->thousandsSeparator(config('money.' . $get('currency_code') . '.thousands_separator'))
132
-                                        ->signed()
133
-                                        ->padFractionalZeros()
134
-                                        ->normalizeZeros(false),
135
-                            ])
136
-                            ->pattern(config('money.' . $get('currency_code') . '.symbol_first') ? config('money.' . $get('currency_code') . '.symbol') . 'money' : 'money' . config('money.' . $get('currency_code') . '.symbol'))
137
-                            ->lazyPlaceholder(false)),
138
-                        Forms\Components\Toggle::make('enabled')
139
-                            ->hidden(fn (Closure $get) => $get('type') === 'card')
140
-                            ->label('Default Account'),
141
-                    ])->columns(),
142
-                Forms\Components\Section::make('Bank')
187
+                Forms\Components\Group::make()
143
                     ->schema([
188
                     ->schema([
144
-                        Forms\Components\TextInput::make('bank_name')
145
-                            ->label('Bank Name')
146
-                            ->maxLength(100),
147
-                        Forms\Components\TextInput::make('bank_phone')
148
-                            ->label('Bank Phone')
149
-                            ->mask(static fn (Forms\Components\TextInput\Mask $mask) => $mask->pattern('(000) 000-0000'))
150
-                            ->maxLength(20),
151
-                        Forms\Components\Textarea::make('bank_address')
152
-                            ->label('Bank Address')
153
-                            ->columnSpanFull(),
154
-                        ])->columns(),
155
-            ]);
189
+                        Forms\Components\Section::make('Routing Information')
190
+                            ->schema([
191
+                                Forms\Components\TextInput::make('aba_routing_number')
192
+                                    ->label('ABA Number')
193
+                                    ->integer()
194
+                                    ->length(9),
195
+                                Forms\Components\TextInput::make('ach_routing_number')
196
+                                    ->label('ACH Number')
197
+                                    ->integer()
198
+                                    ->length(9),
199
+                            ]),
200
+                        Forms\Components\Section::make('International Bank Information')
201
+                            ->schema([
202
+                                Forms\Components\TextInput::make('bic_swift_code')
203
+                                    ->label('BIC/SWIFT Code')
204
+                                    ->maxLength(11),
205
+                                Forms\Components\TextInput::make('iban')
206
+                                    ->label('IBAN')
207
+                                    ->maxLength(34),
208
+                            ]),
209
+                    ])->columnSpan(['lg' => 1]),
210
+            ])->columns(3);
156
     }
211
     }
157
 
212
 
158
     /**
213
     /**
163
         return $table
218
         return $table
164
             ->columns([
219
             ->columns([
165
                 Tables\Columns\TextColumn::make('name')
220
                 Tables\Columns\TextColumn::make('name')
221
+                    ->label('Account')
166
                     ->searchable()
222
                     ->searchable()
167
                     ->weight('semibold')
223
                     ->weight('semibold')
168
                     ->icon(static fn (Account $record) => $record->enabled ? 'heroicon-o-lock-closed' : null)
224
                     ->icon(static fn (Account $record) => $record->enabled ? 'heroicon-o-lock-closed' : null)
169
                     ->tooltip(static fn (Account $record) => $record->enabled ? 'Default Account' : null)
225
                     ->tooltip(static fn (Account $record) => $record->enabled ? 'Default Account' : null)
170
                     ->iconPosition('after')
226
                     ->iconPosition('after')
171
-                    ->sortable(),
172
-                Tables\Columns\TextColumn::make('number')
173
-                    ->label('Account Number')
174
-                    ->searchable()
227
+                    ->description(static fn (Account $record) => $record->number ?: 'N/A')
175
                     ->sortable(),
228
                     ->sortable(),
176
                 Tables\Columns\TextColumn::make('bank_name')
229
                 Tables\Columns\TextColumn::make('bank_name')
177
-                    ->label('Bank Name')
230
+                    ->label('Bank')
231
+                    ->placeholder('N/A')
232
+                    ->description(static fn (Account $record) => $record->bank_phone ?: 'N/A')
178
                     ->searchable()
233
                     ->searchable()
179
                     ->sortable(),
234
                     ->sortable(),
180
-                Tables\Columns\TextColumn::make('bank_phone')
181
-                    ->label('Phone')
182
-                    ->formatStateUsing(static fn ($record) => ($record->bank_phone !== '') ? vsprintf('(%d%d%d) %d%d%d-%d%d%d%d', str_split($record->bank_phone)) : '-'),
235
+                Tables\Columns\BadgeColumn::make('status')
236
+                    ->label('Status')
237
+                    ->colors([
238
+                        'primary' => 'open',
239
+                        'success' => 'active',
240
+                        'secondary' => 'dormant',
241
+                        'warning' => 'restricted',
242
+                        'danger' => 'closed',
243
+                    ])
244
+                    ->icons([
245
+                        'heroicon-o-cash' => 'open',
246
+                        'heroicon-o-clock' => 'active',
247
+                        'heroicon-o-status-offline' => 'dormant',
248
+                        'heroicon-o-exclamation' => 'restricted',
249
+                        'heroicon-o-x-circle' => 'closed',
250
+                    ])
251
+                    ->sortable(),
183
                 Tables\Columns\TextColumn::make('opening_balance')
252
                 Tables\Columns\TextColumn::make('opening_balance')
184
                     ->label('Current Balance')
253
                     ->label('Current Balance')
185
                     ->sortable()
254
                     ->sortable()
222
                     }),
291
                     }),
223
             ]);
292
             ]);
224
     }
293
     }
225
-    
294
+
226
     public static function getRelations(): array
295
     public static function getRelations(): array
227
     {
296
     {
228
         return [
297
         return [
229
             //
298
             //
230
         ];
299
         ];
231
     }
300
     }
232
-    
301
+
233
     public static function getPages(): array
302
     public static function getPages(): array
234
     {
303
     {
235
         return [
304
         return [
237
             'create' => Pages\CreateAccount::route('/create'),
306
             'create' => Pages\CreateAccount::route('/create'),
238
             'edit' => Pages\EditAccount::route('/{record}/edit'),
307
             'edit' => Pages\EditAccount::route('/{record}/edit'),
239
         ];
308
         ];
240
-    }    
309
+    }
241
 }
310
 }

+ 49
- 17
app/Filament/Resources/AccountResource/Pages/CreateAccount.php 查看文件

4
 
4
 
5
 use App\Filament\Resources\AccountResource;
5
 use App\Filament\Resources\AccountResource;
6
 use App\Models\Banking\Account;
6
 use App\Models\Banking\Account;
7
+use Filament\Notifications\Notification;
7
 use Filament\Pages\Actions;
8
 use Filament\Pages\Actions;
8
 use Filament\Resources\Pages\CreateRecord;
9
 use Filament\Resources\Pages\CreateRecord;
9
 use Illuminate\Database\Eloquent\Model;
10
 use Illuminate\Database\Eloquent\Model;
11
+use Illuminate\Support\Facades\Auth;
10
 use Illuminate\Support\Facades\DB;
12
 use Illuminate\Support\Facades\DB;
11
 
13
 
12
 class CreateAccount extends CreateRecord
14
 class CreateAccount extends CreateRecord
15
 
17
 
16
     protected function getRedirectUrl(): string
18
     protected function getRedirectUrl(): string
17
     {
19
     {
18
-        return $this->getResource()::getUrl('index');
20
+        return self::getResource()::getUrl('index');
19
     }
21
     }
20
 
22
 
21
     protected function mutateFormDataBeforeCreate(array $data): array
23
     protected function mutateFormDataBeforeCreate(array $data): array
22
     {
24
     {
23
-        $data['company_id'] = auth()->user()->currentCompany->id;
25
+        $data['company_id'] = Auth::user()->currentCompany->id;
26
+        $data['enabled'] = (bool)$data['enabled'];
27
+        $data['created_by'] = Auth::id();
28
+
24
         return $data;
29
         return $data;
25
     }
30
     }
26
 
31
 
27
     protected function handleRecordCreation(array $data): Model
32
     protected function handleRecordCreation(array $data): Model
28
     {
33
     {
29
-        $currentCompanyId = auth()->user()->currentCompany->id;
30
-        $accountId = $data['id'] ?? null;
31
-        $enabledAccountsCount = Account::where('company_id', $currentCompanyId)
34
+        return DB::transaction(function () use ($data) {
35
+            $currentCompanyId = auth()->user()->currentCompany->id;
36
+
37
+            $enabled = (bool)($data['enabled'] ?? false); // Ensure $enabled is always a boolean
38
+
39
+            if ($enabled === true) {
40
+                $this->disableExistingRecord($currentCompanyId);
41
+            } else {
42
+                $this->ensureAtLeastOneEnabled($currentCompanyId, $enabled);
43
+            }
44
+
45
+            $data['enabled'] = $enabled;
46
+
47
+            return parent::handleRecordCreation($data);
48
+        });
49
+    }
50
+
51
+    protected function disableExistingRecord($companyId): void
52
+    {
53
+        $existingEnabledRecord = Account::where('company_id', $companyId)
32
             ->where('enabled', true)
54
             ->where('enabled', true)
33
-            ->where('id', '!=', $accountId)
34
-            ->count();
55
+            ->first();
35
 
56
 
36
-        if ($data['enabled'] === true && $enabledAccountsCount > 0) {
37
-            $this->disableOtherAccounts($currentCompanyId, $accountId);
38
-        } elseif ($data['enabled'] === false && $enabledAccountsCount < 1) {
39
-            $data['enabled'] = true;
57
+        if ($existingEnabledRecord !== null) {
58
+            $existingEnabledRecord->enabled = false;
59
+            $existingEnabledRecord->save();
60
+            $this->defaultAccountChanged();
40
         }
61
         }
62
+    }
41
 
63
 
42
-        return parent::handleRecordCreation($data);
64
+    protected function ensureAtLeastOneEnabled($companyId, &$enabled): void
65
+    {
66
+        $enabledAccountsCount = Account::where('company_id', $companyId)
67
+            ->where('enabled', true)
68
+            ->count();
69
+
70
+        if ($enabledAccountsCount === 0) {
71
+            $enabled = true;
72
+        }
43
     }
73
     }
44
 
74
 
45
-    protected function disableOtherAccounts($companyId, $accountId): void
75
+    protected function defaultAccountChanged(): void
46
     {
76
     {
47
-        DB::table('accounts')
48
-            ->where('company_id', $companyId)
49
-            ->where('id', '!=', $accountId)
50
-            ->update(['enabled' => false]);
77
+        Notification::make()
78
+            ->warning()
79
+            ->title('Default account updated')
80
+            ->body('Your default account has been updated. Please check your account settings to review this change and ensure it is correct.')
81
+            ->persistent()
82
+            ->send();
51
     }
83
     }
52
 }
84
 }

+ 53
- 16
app/Filament/Resources/AccountResource/Pages/EditAccount.php 查看文件

4
 
4
 
5
 use App\Filament\Resources\AccountResource;
5
 use App\Filament\Resources\AccountResource;
6
 use App\Models\Banking\Account;
6
 use App\Models\Banking\Account;
7
+use Filament\Notifications\Notification;
7
 use Filament\Pages\Actions;
8
 use Filament\Pages\Actions;
8
 use Filament\Resources\Pages\EditRecord;
9
 use Filament\Resources\Pages\EditRecord;
9
 use Illuminate\Database\Eloquent\Model;
10
 use Illuminate\Database\Eloquent\Model;
11
+use Illuminate\Support\Facades\Auth;
10
 use Illuminate\Support\Facades\DB;
12
 use Illuminate\Support\Facades\DB;
11
 
13
 
12
 class EditAccount extends EditRecord
14
 class EditAccount extends EditRecord
27
 
29
 
28
     protected function mutateFormDataBeforeSave(array $data): array
30
     protected function mutateFormDataBeforeSave(array $data): array
29
     {
31
     {
30
-        $data['company_id'] = auth()->user()->currentCompany->id;
32
+        $data['company_id'] = Auth::user()->currentCompany->id;
33
+        $data['enabled'] = (bool)$data['enabled'];
34
+        $data['updated_by'] = Auth::id();
35
+
31
         return $data;
36
         return $data;
32
     }
37
     }
33
 
38
 
34
     protected function handleRecordUpdate(Model|Account $record, array $data): Model|Account
39
     protected function handleRecordUpdate(Model|Account $record, array $data): Model|Account
35
     {
40
     {
36
-        $currentCompanyId = auth()->user()->currentCompany->id;
37
-        $accountId = $record->id;
38
-        $enabledAccountsCount = Account::where('company_id', $currentCompanyId)
41
+        return DB::transaction(function () use ($record, $data) {
42
+            $currentCompanyId = auth()->user()->currentCompany->id;
43
+            $recordId = $record->id;
44
+            $enabled = (bool)($data['enabled'] ?? false);
45
+
46
+            // If the record is enabled, disable all other records for the same company
47
+            if ($enabled === true) {
48
+                $this->disableExistingRecord($currentCompanyId, $recordId);
49
+            }
50
+            // If the record is disabled, ensure at least one record remains enabled
51
+            elseif ($enabled === false) {
52
+                $this->ensureAtLeastOneEnabled($currentCompanyId, $recordId, $enabled);
53
+            }
54
+
55
+            $data['enabled'] = $enabled;
56
+
57
+            return parent::handleRecordUpdate($record, $data);
58
+        });
59
+    }
60
+
61
+    protected function disableExistingRecord(int $companyId, int $recordId): void
62
+    {
63
+        $existingEnabledAccount = Account::where('company_id', $companyId)
39
             ->where('enabled', true)
64
             ->where('enabled', true)
40
-            ->where('id', '!=', $accountId)
41
-            ->count();
65
+            ->where('id', '!=', $recordId)
66
+            ->first();
42
 
67
 
43
-        if ($data['enabled'] === true && $enabledAccountsCount > 0) {
44
-            $this->disableOtherAccounts($currentCompanyId, $accountId);
45
-        } elseif ($data['enabled'] === false && $enabledAccountsCount < 1) {
46
-            $data['enabled'] = true;
68
+        if ($existingEnabledAccount !== null) {
69
+            $existingEnabledAccount->enabled = false;
70
+            $existingEnabledAccount->save();
71
+            $this->defaultAccountChanged();
47
         }
72
         }
73
+    }
48
 
74
 
49
-        return parent::handleRecordUpdate($record, $data);
75
+    protected function ensureAtLeastOneEnabled(int $companyId, int $recordId, bool &$enabled): void
76
+    {
77
+        $enabledAccountsCount = Account::where('company_id', $companyId)
78
+            ->where('enabled', true)
79
+            ->where('id', '!=', $recordId)
80
+            ->count();
81
+
82
+        if ($enabledAccountsCount === 0) {
83
+            $enabled = true;
84
+        }
50
     }
85
     }
51
 
86
 
52
-    protected function disableOtherAccounts($companyId, $accountId): void
87
+    protected function defaultAccountChanged(): void
53
     {
88
     {
54
-        DB::table('accounts')
55
-            ->where('company_id', $companyId)
56
-            ->where('id', '!=', $accountId)
57
-            ->update(['enabled' => false]);
89
+        Notification::make()
90
+            ->warning()
91
+            ->title('Default account updated')
92
+            ->body('Your default account has been updated. Please check your account settings to review this change and ensure it is correct.')
93
+            ->persistent()
94
+            ->send();
58
     }
95
     }
59
 }
96
 }

+ 140
- 0
app/Filament/Resources/CategoryResource.php 查看文件

1
+<?php
2
+
3
+namespace App\Filament\Resources;
4
+
5
+use App\Filament\Resources\CategoryResource\Pages;
6
+use App\Filament\Resources\CategoryResource\RelationManagers;
7
+use Wallo\FilamentSelectify\Components\ToggleButton;
8
+use App\Models\Setting\Category;
9
+use Exception;
10
+use Filament\Forms;
11
+use Filament\Notifications\Notification;
12
+use Filament\Resources\Form;
13
+use Filament\Resources\Resource;
14
+use Filament\Resources\Table;
15
+use Filament\Tables;
16
+use Illuminate\Support\Collection;
17
+
18
+class CategoryResource extends Resource
19
+{
20
+    protected static ?string $model = Category::class;
21
+
22
+    protected static ?string $navigationIcon = 'heroicon-o-folder';
23
+
24
+    protected static ?string $navigationGroup = 'Settings';
25
+
26
+    public static function form(Form $form): Form
27
+    {
28
+        return $form
29
+            ->schema([
30
+                Forms\Components\Section::make('General')
31
+                    ->schema([
32
+                        Forms\Components\TextInput::make('name')
33
+                            ->label('Name')
34
+                            ->required(),
35
+                        Forms\Components\ColorPicker::make('color')
36
+                            ->label('Color')
37
+                            ->default('#4f46e5')
38
+                            ->required(),
39
+                        Forms\Components\Select::make('type')
40
+                            ->label('Type')
41
+                            ->options(Category::getCategoryTypes())
42
+                            ->searchable()
43
+                            ->required(),
44
+                        ToggleButton::make('enabled')
45
+                            ->label('Default')
46
+                            ->offColor('danger')
47
+                            ->onColor('primary'),
48
+                    ])->columns(),
49
+            ]);
50
+    }
51
+
52
+    /**
53
+     * @throws Exception
54
+     */
55
+    public static function table(Table $table): Table
56
+    {
57
+        return $table
58
+            ->columns([
59
+                Tables\Columns\TextColumn::make('name')
60
+                    ->label('Name')
61
+                    ->weight('semibold')
62
+                    ->icon(static fn (Category $record) => $record->enabled ? 'heroicon-o-lock-closed' : null)
63
+                    ->tooltip(static fn (Category $record) => $record->enabled ? "Default " .ucwords($record->type) . " Category" : null)
64
+                    ->iconPosition('after')
65
+                    ->searchable()
66
+                    ->sortable(),
67
+                Tables\Columns\TextColumn::make('type')
68
+                    ->label('Type')
69
+                    ->formatStateUsing(static fn (Category $record): string => ucwords($record->type))
70
+                    ->searchable()
71
+                    ->sortable(),
72
+                Tables\Columns\ColorColumn::make('color')
73
+                    ->label('Color')
74
+                    ->copyable()
75
+                    ->copyMessage('Color copied to clipboard.'),
76
+            ])
77
+            ->filters([
78
+                //
79
+            ])
80
+            ->actions([
81
+                Tables\Actions\EditAction::make(),
82
+                Tables\Actions\DeleteAction::make()
83
+                    ->before(static function (Category $record, Tables\Actions\DeleteAction $action) {
84
+                        if ($record->enabled) {
85
+                            Notification::make()
86
+                                ->danger()
87
+                                ->title('Action Denied')
88
+                                ->body(__('The :name category is currently set as your default :Type category and cannot be deleted. Please set a different category as your default before attempting to delete this one.', ['name' => $record->name, 'Type' => ucwords($record->type)]))
89
+                                ->persistent()
90
+                                ->send();
91
+
92
+                            $action->cancel();
93
+                        }
94
+                    }),
95
+            ])
96
+            ->bulkActions([
97
+                Tables\Actions\DeleteBulkAction::make()
98
+                    ->before(static function (Collection $records, Tables\Actions\DeleteBulkAction $action) {
99
+                        $defaultCategories = $records->filter(static function (Category $record) {
100
+                            return $record->enabled;
101
+                        });
102
+
103
+                        if ($defaultCategories->isNotEmpty()) {
104
+                            $defaultCategoryNames = $defaultCategories->pluck('name')->toArray();
105
+
106
+                            Notification::make()
107
+                                ->danger()
108
+                                ->title('Action Denied')
109
+                                ->body(static function () use ($defaultCategoryNames) {
110
+                                    $message = __('The following categories are currently set as your default and cannot be deleted. Please set a different category as your default before attempting to delete these ones.') . "<br><br>";
111
+                                    $message .= implode("<br>", array_map(static function ($name) {
112
+                                        return "&bull; " . $name;
113
+                                    }, $defaultCategoryNames));
114
+                                    return $message;
115
+                                })
116
+                                ->persistent()
117
+                                ->send();
118
+
119
+                            $action->cancel();
120
+                        }
121
+                    }),
122
+            ]);
123
+    }
124
+
125
+    public static function getRelations(): array
126
+    {
127
+        return [
128
+            //
129
+        ];
130
+    }
131
+
132
+    public static function getPages(): array
133
+    {
134
+        return [
135
+            'index' => Pages\ListCategories::route('/'),
136
+            'create' => Pages\CreateCategory::route('/create'),
137
+            'edit' => Pages\EditCategory::route('/{record}/edit'),
138
+        ];
139
+    }
140
+}

+ 74
- 0
app/Filament/Resources/CategoryResource/Pages/CreateCategory.php 查看文件

1
+<?php
2
+
3
+namespace App\Filament\Resources\CategoryResource\Pages;
4
+
5
+use App\Filament\Resources\CategoryResource;
6
+use App\Models\Setting\Category;
7
+use Filament\Pages\Actions;
8
+use Filament\Resources\Pages\CreateRecord;
9
+use Illuminate\Database\Eloquent\Model;
10
+use Illuminate\Support\Facades\Auth;
11
+use Illuminate\Support\Facades\DB;
12
+
13
+class CreateCategory extends CreateRecord
14
+{
15
+    protected static string $resource = CategoryResource::class;
16
+
17
+    protected function getRedirectUrl(): string
18
+    {
19
+        return $this->getResource()::getUrl('index');
20
+    }
21
+
22
+    protected function mutateFormDataBeforeCreate(array $data): array
23
+    {
24
+        $data['company_id'] = Auth::user()->currentCompany->id;
25
+        $data['enabled'] = (bool)$data['enabled'];
26
+        $data['created_by'] = Auth::id();
27
+
28
+        return $data;
29
+    }
30
+
31
+    protected function handleRecordCreation(array $data): Model
32
+    {
33
+        return DB::transaction(function () use ($data) {
34
+            $currentCompanyId = auth()->user()->currentCompany->id;
35
+            $type = $data['type'] ?? null;
36
+            $enabled = (bool)($data['enabled'] ?? false);
37
+
38
+            if ($enabled === true) {
39
+                $this->disableExistingRecord($currentCompanyId, $type);
40
+            } else {
41
+                $this->ensureAtLeastOneEnabled($currentCompanyId, $type, $enabled);
42
+            }
43
+
44
+            $data['enabled'] = $enabled;
45
+
46
+            return parent::handleRecordCreation($data);
47
+        });
48
+    }
49
+
50
+    protected function disableExistingRecord(int $companyId, string $type): void
51
+    {
52
+        $existingEnabledRecord = Category::where('company_id', $companyId)
53
+            ->where('enabled', true)
54
+            ->where('type', $type)
55
+            ->first();
56
+
57
+        if ($existingEnabledRecord !== null) {
58
+            $existingEnabledRecord->enabled = false;
59
+            $existingEnabledRecord->save();
60
+        }
61
+    }
62
+
63
+    protected function ensureAtLeastOneEnabled(int $companyId, string $type, bool &$enabled): void
64
+    {
65
+        $otherEnabledRecords = Category::where('company_id', $companyId)
66
+            ->where('enabled', true)
67
+            ->where('type', $type)
68
+            ->count();
69
+
70
+        if ($otherEnabledRecords === 0) {
71
+            $enabled = true;
72
+        }
73
+    }
74
+}

+ 104
- 0
app/Filament/Resources/CategoryResource/Pages/EditCategory.php 查看文件

1
+<?php
2
+
3
+namespace App\Filament\Resources\CategoryResource\Pages;
4
+
5
+use App\Filament\Resources\CategoryResource;
6
+use App\Models\Setting\Category;
7
+use Filament\Pages\Actions;
8
+use Filament\Resources\Pages\EditRecord;
9
+use Illuminate\Database\Eloquent\Model;
10
+use Illuminate\Support\Facades\Auth;
11
+use Illuminate\Support\Facades\DB;
12
+
13
+class EditCategory extends EditRecord
14
+{
15
+    protected static string $resource = CategoryResource::class;
16
+
17
+    protected function getActions(): array
18
+    {
19
+        return [
20
+            Actions\DeleteAction::make(),
21
+        ];
22
+    }
23
+
24
+    protected function getRedirectUrl(): string
25
+    {
26
+        return $this->getResource()::getUrl('index');
27
+    }
28
+
29
+    protected function mutateFormDataBeforeSave(array $data): array
30
+    {
31
+        $data['company_id'] = Auth::user()->currentCompany->id;
32
+        $data['enabled'] = (bool)$data['enabled'];
33
+        $data['updated_by'] = Auth::id();
34
+
35
+        return $data;
36
+    }
37
+
38
+    protected function handleRecordUpdate(Model|Category $record, array $data): Model|Category
39
+    {
40
+        return DB::transaction(function () use ($record, $data) {
41
+            $currentCompanyId = auth()->user()->currentCompany->id;
42
+            $recordId = $record->id;
43
+            $oldType = $record->type;
44
+            $newType = $data['type'];
45
+            $enabled = (bool)($data['enabled'] ?? false);
46
+
47
+            // If the record type has changed and it was previously enabled
48
+            if ($oldType !== $newType && $record->enabled) {
49
+                $this->changeRecordType($currentCompanyId, $recordId, $oldType);
50
+            }
51
+
52
+            if ($enabled === true) {
53
+                $this->disableExistingRecord($currentCompanyId, $recordId, $newType);
54
+            } elseif ($enabled === false) {
55
+                $this->ensureAtLeastOneEnabled($currentCompanyId, $recordId, $newType, $enabled);
56
+            }
57
+
58
+            $data['enabled'] = $enabled;
59
+
60
+            return parent::handleRecordUpdate($record, $data);
61
+        });
62
+    }
63
+
64
+    protected function changeRecordType(int $companyId, int $recordId, string $oldType): void
65
+    {
66
+        $oldTypeRecord = $this->getCompanyRecord($companyId, $oldType, $recordId);
67
+
68
+        if ($oldTypeRecord) {
69
+            $oldTypeRecord->enabled = true;
70
+            $oldTypeRecord->save();
71
+        }
72
+    }
73
+
74
+    protected function disableExistingRecord(int $companyId, int $recordId, string $newType): void
75
+    {
76
+        $existingEnabledRecord = $this->getCompanyRecord($companyId, $newType, $recordId);
77
+
78
+        if ($existingEnabledRecord !== null) {
79
+            $existingEnabledRecord->enabled = false;
80
+            $existingEnabledRecord->save();
81
+        }
82
+    }
83
+
84
+    protected function ensureAtLeastOneEnabled(int $companyId, int $recordId, string $newType, bool &$enabled): void
85
+    {
86
+        $otherEnabledRecords = Category::where('company_id', $companyId)
87
+            ->where('enabled', true)
88
+            ->where('type', $newType)
89
+            ->where('id', '!=', $recordId)
90
+            ->count();
91
+
92
+        if ($otherEnabledRecords === 0) {
93
+            $enabled = true;
94
+        }
95
+    }
96
+
97
+    protected function getCompanyRecord(int $companyId, string $type, int $recordId): ?Category
98
+    {
99
+        return Category::where('company_id', $companyId)
100
+            ->where('type', $type)
101
+            ->where('id', '!=', $recordId)
102
+            ->first();
103
+    }
104
+}

+ 19
- 0
app/Filament/Resources/CategoryResource/Pages/ListCategories.php 查看文件

1
+<?php
2
+
3
+namespace App\Filament\Resources\CategoryResource\Pages;
4
+
5
+use App\Filament\Resources\CategoryResource;
6
+use Filament\Pages\Actions;
7
+use Filament\Resources\Pages\ListRecords;
8
+
9
+class ListCategories extends ListRecords
10
+{
11
+    protected static string $resource = CategoryResource::class;
12
+
13
+    protected function getActions(): array
14
+    {
15
+        return [
16
+            Actions\CreateAction::make(),
17
+        ];
18
+    }
19
+}

+ 15
- 15
app/Filament/Resources/CurrencyResource.php 查看文件

19
 use Illuminate\Database\Eloquent\SoftDeletingScope;
19
 use Illuminate\Database\Eloquent\SoftDeletingScope;
20
 use Illuminate\Support\Collection;
20
 use Illuminate\Support\Collection;
21
 use Illuminate\Support\Facades\Auth;
21
 use Illuminate\Support\Facades\Auth;
22
+use Illuminate\Support\Facades\Blade;
23
+use Illuminate\Support\HtmlString;
24
+use Wallo\FilamentSelectify\Components\ToggleButton;
22
 
25
 
23
 class CurrencyResource extends Resource
26
 class CurrencyResource extends Resource
24
 {
27
 {
38
         return $form
41
         return $form
39
             ->schema([
42
             ->schema([
40
                 Forms\Components\Section::make('General')
43
                 Forms\Components\Section::make('General')
44
+                    ->description('The default currency is used for all transactions and reports and cannot be deleted. Currency precision determines the number of decimal places to display when formatting currency amounts. The currency rate is set to 1 for the default currency and is utilized as the basis for setting exchange rates for all other currencies.')
41
                     ->schema([
45
                     ->schema([
42
                         Forms\Components\Select::make('code')
46
                         Forms\Components\Select::make('code')
43
                             ->label('Code')
47
                             ->label('Code')
44
                             ->options(Currency::getCurrencyCodes())
48
                             ->options(Currency::getCurrencyCodes())
45
                             ->searchable()
49
                             ->searchable()
46
-                            ->placeholder('- Select Code -')
50
+                            ->placeholder('Select a currency code...')
47
                             ->reactive()
51
                             ->reactive()
48
                             ->afterStateUpdated(static function (Closure $set, $state) {
52
                             ->afterStateUpdated(static function (Closure $set, $state) {
49
                                 $code = $state;
53
                                 $code = $state;
64
                             ->required(),
68
                             ->required(),
65
                         Forms\Components\TextInput::make('name')
69
                         Forms\Components\TextInput::make('name')
66
                             ->translateLabel()
70
                             ->translateLabel()
67
-                            ->placeholder('Enter Name')
68
                             ->maxLength(100)
71
                             ->maxLength(100)
69
                             ->required(),
72
                             ->required(),
70
                         Forms\Components\TextInput::make('rate')
73
                         Forms\Components\TextInput::make('rate')
71
                             ->label('Rate')
74
                             ->label('Rate')
72
-                            ->placeholder('Enter Rate')
73
                             ->dehydrateStateUsing(static fn (Closure $get, $state): bool => $get('enabled') === true ? '1' : $state) // rate is 1 when enabled is true
75
                             ->dehydrateStateUsing(static fn (Closure $get, $state): bool => $get('enabled') === true ? '1' : $state) // rate is 1 when enabled is true
74
                             ->numeric()
76
                             ->numeric()
77
+                            ->reactive()
75
                             ->disabled(static fn (Closure $get): bool => $get('enabled') === true) // disabled is true when enabled is true
78
                             ->disabled(static fn (Closure $get): bool => $get('enabled') === true) // disabled is true when enabled is true
76
                             ->mask(static fn (Forms\Components\TextInput\Mask $mask) => $mask
79
                             ->mask(static fn (Forms\Components\TextInput\Mask $mask) => $mask
77
                                 ->numeric()
80
                                 ->numeric()
86
                         Forms\Components\Select::make('precision')
89
                         Forms\Components\Select::make('precision')
87
                             ->label('Precision')
90
                             ->label('Precision')
88
                             ->searchable()
91
                             ->searchable()
89
-                            ->placeholder('- Select Precision -')
92
+                            ->placeholder('Select the currency precision...')
90
                             ->options(['0', '1', '2', '3', '4'])
93
                             ->options(['0', '1', '2', '3', '4'])
91
                             ->required(),
94
                             ->required(),
92
                         Forms\Components\TextInput::make('symbol')
95
                         Forms\Components\TextInput::make('symbol')
93
                             ->label('Symbol')
96
                             ->label('Symbol')
94
-                            ->placeholder('Enter Symbol')
95
                             ->maxLength(5)
97
                             ->maxLength(5)
96
                             ->required(),
98
                             ->required(),
97
                         Forms\Components\Select::make('symbol_first')
99
                         Forms\Components\Select::make('symbol_first')
98
                             ->label('Symbol Position')
100
                             ->label('Symbol Position')
99
                             ->searchable()
101
                             ->searchable()
100
-                            ->boolean('Before Amount', 'After Amount', '- Select Symbol Position -')
102
+                            ->boolean('Before Amount', 'After Amount', 'Select the currency symbol position...')
101
                             ->required(),
103
                             ->required(),
102
                         Forms\Components\TextInput::make('decimal_mark')
104
                         Forms\Components\TextInput::make('decimal_mark')
103
                             ->label('Decimal Separator')
105
                             ->label('Decimal Separator')
104
-                            ->placeholder('Enter Decimal Separator')
105
                             ->maxLength(1)
106
                             ->maxLength(1)
106
                             ->required(),
107
                             ->required(),
107
                         Forms\Components\TextInput::make('thousands_separator')
108
                         Forms\Components\TextInput::make('thousands_separator')
108
                             ->label('Thousands Separator')
109
                             ->label('Thousands Separator')
109
-                            ->placeholder('Enter Thousands Separator')
110
                             ->maxLength(1)
110
                             ->maxLength(1)
111
                             ->required(),
111
                             ->required(),
112
-                        Forms\Components\Toggle::make('enabled')
112
+                        ToggleButton::make('enabled')
113
                             ->label('Default Currency')
113
                             ->label('Default Currency')
114
                             ->reactive()
114
                             ->reactive()
115
-                            ->inline()
116
-                            ->afterStateUpdated(static fn (Closure $set, $state) => $state ? $set('rate', '1') : $set('rate', null))
117
-                            ->default(false),
115
+                            ->offColor('danger')
116
+                            ->onColor('primary')
117
+                            ->afterStateUpdated(static fn (Closure $set, $state) => $state ? $set('rate', '1') : null),
118
                     ])->columns(),
118
                     ])->columns(),
119
             ]);
119
             ]);
120
     }
120
     }
209
                     }),
209
                     }),
210
             ]);
210
             ]);
211
     }
211
     }
212
-    
212
+
213
     public static function getRelations(): array
213
     public static function getRelations(): array
214
     {
214
     {
215
         return [
215
         return [
216
             //
216
             //
217
         ];
217
         ];
218
     }
218
     }
219
-    
219
+
220
     public static function getPages(): array
220
     public static function getPages(): array
221
     {
221
     {
222
         return [
222
         return [
224
             'create' => Pages\CreateCurrency::route('/create'),
224
             'create' => Pages\CreateCurrency::route('/create'),
225
             'edit' => Pages\EditCurrency::route('/{record}/edit'),
225
             'edit' => Pages\EditCurrency::route('/{record}/edit'),
226
         ];
226
         ];
227
-    }    
227
+    }
228
 }
228
 }

+ 37
- 18
app/Filament/Resources/CurrencyResource/Pages/CreateCurrency.php 查看文件

7
 use Filament\Pages\Actions;
7
 use Filament\Pages\Actions;
8
 use Filament\Resources\Pages\CreateRecord;
8
 use Filament\Resources\Pages\CreateRecord;
9
 use Illuminate\Database\Eloquent\Model;
9
 use Illuminate\Database\Eloquent\Model;
10
+use Illuminate\Support\Facades\Auth;
10
 use Illuminate\Support\Facades\DB;
11
 use Illuminate\Support\Facades\DB;
11
 
12
 
12
 class CreateCurrency extends CreateRecord
13
 class CreateCurrency extends CreateRecord
20
 
21
 
21
     protected function mutateFormDataBeforeCreate(array $data): array
22
     protected function mutateFormDataBeforeCreate(array $data): array
22
     {
23
     {
23
-        $data['company_id'] = auth()->user()->currentCompany->id;
24
+        $data['company_id'] = Auth::user()->currentCompany->id;
25
+        $data['enabled'] = (bool)$data['enabled'];
26
+        $data['created_by'] = Auth::id();
24
 
27
 
25
         return $data;
28
         return $data;
26
     }
29
     }
27
 
30
 
28
     protected function handleRecordCreation(array $data): Model
31
     protected function handleRecordCreation(array $data): Model
29
     {
32
     {
30
-        $currentCompanyId = auth()->user()->currentCompany->id;
31
-        $currencyId = $data['id'] ?? null;
32
-        $enabledCurrenciesCount = Currency::where('company_id', $currentCompanyId)
33
-            ->where('enabled', '1')
34
-            ->where('id', '!=', $currencyId)
35
-            ->count();
33
+        return DB::transaction(function () use ($data) {
34
+            $currentCompanyId = auth()->user()->currentCompany->id;
36
 
35
 
37
-        if ($data['enabled'] === '1' && $enabledCurrenciesCount > 0) {
38
-            $this->disableOtherCurrencies($currentCompanyId, $currencyId);
39
-        } elseif ($data['enabled'] === '0' && $enabledCurrenciesCount < 1) {
40
-            $data['enabled'] = '1';
41
-        }
36
+            $enabled = (bool)($data['enabled'] ?? false); // Ensure $enabled is always a boolean
37
+
38
+            if ($enabled === true) {
39
+                $this->disableExistingRecord($currentCompanyId);
40
+            } else {
41
+                $this->ensureAtLeastOneEnabled($currentCompanyId, $enabled);
42
+            }
43
+
44
+            $data['enabled'] = $enabled;
45
+
46
+            return parent::handleRecordCreation($data);
47
+        });
48
+    }
42
 
49
 
43
-        return parent::handleRecordCreation($data);
50
+    protected function disableExistingRecord($companyId): void
51
+    {
52
+        $existingEnabledRecord = Currency::where('company_id', $companyId)
53
+            ->where('enabled', true)
54
+            ->first();
55
+
56
+        if ($existingEnabledRecord !== null) {
57
+            $existingEnabledRecord->enabled = false;
58
+            $existingEnabledRecord->save();
59
+        }
44
     }
60
     }
45
 
61
 
46
-    protected function disableOtherCurrencies($companyId, $currencyId): void
62
+    protected function ensureAtLeastOneEnabled($companyId, &$enabled): void
47
     {
63
     {
48
-        DB::table('currencies')
49
-            ->where('company_id', $companyId)
50
-            ->where('id', '!=', $currencyId)
51
-            ->update(['enabled' => '0']);
64
+        $enabledAccountsCount = Currency::where('company_id', $companyId)
65
+            ->where('enabled', true)
66
+            ->count();
67
+
68
+        if ($enabledAccountsCount === 0) {
69
+            $enabled = true;
70
+        }
52
     }
71
     }
53
 }
72
 }

+ 44
- 18
app/Filament/Resources/CurrencyResource/Pages/EditCurrency.php 查看文件

3
 namespace App\Filament\Resources\CurrencyResource\Pages;
3
 namespace App\Filament\Resources\CurrencyResource\Pages;
4
 
4
 
5
 use App\Filament\Resources\CurrencyResource;
5
 use App\Filament\Resources\CurrencyResource;
6
+use App\Models\Banking\Account;
6
 use App\Models\Setting\Currency;
7
 use App\Models\Setting\Currency;
7
 use Filament\Pages\Actions;
8
 use Filament\Pages\Actions;
8
 use Filament\Resources\Pages\EditRecord;
9
 use Filament\Resources\Pages\EditRecord;
9
 use Illuminate\Database\Eloquent\Model;
10
 use Illuminate\Database\Eloquent\Model;
11
+use Illuminate\Support\Facades\Auth;
10
 use Illuminate\Support\Facades\DB;
12
 use Illuminate\Support\Facades\DB;
11
 
13
 
12
 class EditCurrency extends EditRecord
14
 class EditCurrency extends EditRecord
27
 
29
 
28
     protected function mutateFormDataBeforeSave(array $data): array
30
     protected function mutateFormDataBeforeSave(array $data): array
29
     {
31
     {
30
-        $data['company_id'] = auth()->user()->currentCompany->id;
32
+        $data['company_id'] = Auth::user()->currentCompany->id;
33
+        $data['enabled'] = (bool)$data['enabled'];
34
+        $data['updated_by'] = Auth::id();
35
+
31
         return $data;
36
         return $data;
32
     }
37
     }
33
 
38
 
34
-    protected function handleRecordUpdate(Model|Currency $record, array $data): Model|Currency
39
+    protected function handleRecordUpdate(Model $record, array $data): Model
40
+    {
41
+        return DB::transaction(function () use ($record, $data) {
42
+            $currentCompanyId = auth()->user()->currentCompany->id;
43
+            $recordId = $record->id;
44
+            $enabled = (bool)($data['enabled'] ?? false);
45
+
46
+            // If the record is enabled, disable all other records for the same company
47
+            if ($enabled === true) {
48
+                $this->disableExistingRecord($currentCompanyId, $recordId);
49
+            }
50
+            // If the record is disabled, ensure at least one record remains enabled
51
+            elseif ($enabled === false) {
52
+                $this->ensureAtLeastOneEnabled($currentCompanyId, $recordId, $enabled);
53
+            }
54
+
55
+            $data['enabled'] = $enabled;
56
+
57
+            return parent::handleRecordUpdate($record, $data);
58
+        });
59
+    }
60
+
61
+    protected function disableExistingRecord(int $companyId, int $recordId): void
35
     {
62
     {
36
-        $currentCompanyId = auth()->user()->currentCompany->id;
37
-        $currencyId = $record->id;
38
-        $enabledCurrenciesCount = Currency::where('company_id', $currentCompanyId)
63
+        $existingEnabledAccount = Currency::where('company_id', $companyId)
39
             ->where('enabled', true)
64
             ->where('enabled', true)
40
-            ->where('id', '!=', $currencyId)
41
-            ->count();
65
+            ->where('id', '!=', $recordId)
66
+            ->first();
42
 
67
 
43
-        if ($data['enabled'] === true && $enabledCurrenciesCount > 0) {
44
-            $this->disableOtherCurrencies($currentCompanyId, $currencyId);
45
-        } elseif ($data['enabled'] === false && $enabledCurrenciesCount < 1) {
46
-            $data['enabled'] = true;
68
+        if ($existingEnabledAccount !== null) {
69
+            $existingEnabledAccount->enabled = false;
70
+            $existingEnabledAccount->save();
47
         }
71
         }
48
-
49
-        return parent::handleRecordUpdate($record, $data);
50
     }
72
     }
51
 
73
 
52
-    protected function disableOtherCurrencies($companyId, $currencyId): void
74
+    protected function ensureAtLeastOneEnabled(int $companyId, int $recordId, bool &$enabled): void
53
     {
75
     {
54
-        DB::table('currencies')
55
-            ->where('company_id', $companyId)
56
-            ->where('id', '!=', $currencyId)
57
-            ->update(['enabled' => false]);
76
+        $enabledAccountsCount = Currency::where('company_id', $companyId)
77
+            ->where('enabled', true)
78
+            ->where('id', '!=', $recordId)
79
+            ->count();
80
+
81
+        if ($enabledAccountsCount === 0) {
82
+            $enabled = true;
83
+        }
58
     }
84
     }
59
 }
85
 }

+ 83
- 61
app/Filament/Resources/CustomerResource.php 查看文件

5
 use App\Actions\Banking\CreateCurrencyFromAccount;
5
 use App\Actions\Banking\CreateCurrencyFromAccount;
6
 use App\Filament\Resources\CustomerResource\Pages;
6
 use App\Filament\Resources\CustomerResource\Pages;
7
 use App\Filament\Resources\CustomerResource\RelationManagers;
7
 use App\Filament\Resources\CustomerResource\RelationManagers;
8
+use Wallo\FilamentSelectify\Components\ButtonGroup;
8
 use App\Models\Banking\Account;
9
 use App\Models\Banking\Account;
9
 use App\Models\Contact;
10
 use App\Models\Contact;
10
 use Filament\Forms;
11
 use Filament\Forms;
16
 use Illuminate\Database\Eloquent\SoftDeletingScope;
17
 use Illuminate\Database\Eloquent\SoftDeletingScope;
17
 use Illuminate\Support\Facades\Auth;
18
 use Illuminate\Support\Facades\Auth;
18
 use Illuminate\Support\Facades\DB;
19
 use Illuminate\Support\Facades\DB;
20
+use Squire\Models\Country;
21
+use Squire\Models\Region;
19
 
22
 
20
 class CustomerResource extends Resource
23
 class CustomerResource extends Resource
21
 {
24
 {
22
     protected static ?string $model = Contact::class;
25
     protected static ?string $model = Contact::class;
23
 
26
 
24
-    protected static ?string $navigationIcon = 'heroicon-o-collection';
27
+    protected static ?string $navigationIcon = 'heroicon-o-user-group';
25
 
28
 
26
     protected static ?string $navigationGroup = 'Sales';
29
     protected static ?string $navigationGroup = 'Sales';
27
 
30
 
32
     public static function getEloquentQuery(): Builder
35
     public static function getEloquentQuery(): Builder
33
     {
36
     {
34
         return parent::getEloquentQuery()
37
         return parent::getEloquentQuery()
35
-            ->where('type', 'customer')
38
+            ->customer()
36
             ->where('company_id', Auth::user()->currentCompany->id);
39
             ->where('company_id', Auth::user()->currentCompany->id);
37
     }
40
     }
38
 
41
 
42
             ->schema([
45
             ->schema([
43
                 Forms\Components\Section::make('General')
46
                 Forms\Components\Section::make('General')
44
                     ->schema([
47
                     ->schema([
45
-                        Forms\Components\Radio::make('entity')
46
-                            ->options([
47
-                                'company' => 'Company',
48
-                                'individual' => 'Individual',
49
-                            ])
50
-                            ->inline()
51
-                            ->default('company')
52
-                            ->required()
53
-                            ->columnSpanFull(),
54
-                        Forms\Components\TextInput::make('name')
55
-                            ->maxLength(100)
56
-                            ->placeholder('Enter Name')
57
-                            ->required(),
58
-                        Forms\Components\TextInput::make('email')
59
-                            ->email()
60
-                            ->placeholder('Enter Email')
61
-                            ->nullable(),
62
-                        Forms\Components\TextInput::make('phone')
63
-                            ->label('Phone')
64
-                            ->tel()
65
-                            ->placeholder('Enter Phone')
66
-                            ->maxLength(20),
67
-                        Forms\Components\TextInput::make('website')
68
-                            ->maxLength(100)
69
-                            ->prefix('https://')
70
-                            ->placeholder('Enter Website')
71
-                            ->nullable(),
72
-                        Forms\Components\TextInput::make('reference')
73
-                            ->maxLength(100)
74
-                            ->placeholder('Enter Reference')
75
-                            ->nullable(),
76
-                    ])->columns(2),
48
+                        Forms\Components\Grid::make(3)
49
+                            ->schema([
50
+                                ButtonGroup::make('entity')
51
+                                    ->label('Entity')
52
+                                    ->options([
53
+                                        'individual' => 'Individual',
54
+                                        'company' => 'Company',
55
+                                    ])
56
+                                    ->gridDirection('column')
57
+                                    ->default('individual')
58
+                                    ->columnSpan(1)
59
+                                    ->required(),
60
+                                Forms\Components\Grid::make()
61
+                                    ->schema([
62
+                                        Forms\Components\TextInput::make('name')
63
+                                            ->label('Name')
64
+                                            ->maxLength(100)
65
+                                            ->required(),
66
+                                        Forms\Components\TextInput::make('email')
67
+                                            ->label('Email')
68
+                                            ->email()
69
+                                            ->nullable(),
70
+                                        Forms\Components\TextInput::make('phone')
71
+                                            ->label('Phone')
72
+                                            ->tel()
73
+                                            ->maxLength(20),
74
+                                        Forms\Components\TextInput::make('website')
75
+                                            ->label('Website')
76
+                                            ->maxLength(100)
77
+                                            ->url()
78
+                                            ->nullable(),
79
+                                        Forms\Components\TextInput::make('reference')
80
+                                            ->label('Reference')
81
+                                            ->maxLength(100)
82
+                                            ->columnSpan(2)
83
+                                            ->nullable(),
84
+                                    ])->columnSpan(2),
85
+                            ]),
86
+                    ])->columns(),
77
                 Forms\Components\Section::make('Billing')
87
                 Forms\Components\Section::make('Billing')
78
                     ->schema([
88
                     ->schema([
79
                         Forms\Components\TextInput::make('tax_number')
89
                         Forms\Components\TextInput::make('tax_number')
90
+                            ->label('Tax Number')
80
                             ->maxLength(100)
91
                             ->maxLength(100)
81
-                            ->placeholder('Enter Tax Number')
82
                             ->nullable(),
92
                             ->nullable(),
83
                         Forms\Components\Select::make('currency_code')
93
                         Forms\Components\Select::make('currency_code')
84
                             ->label('Currency')
94
                             ->label('Currency')
135
                 ])->columns(2),
145
                 ])->columns(2),
136
                 Forms\Components\Section::make('Address')
146
                 Forms\Components\Section::make('Address')
137
                     ->schema([
147
                     ->schema([
138
-                        Forms\Components\Textarea::make('address')
148
+                        Forms\Components\TextInput::make('address')
139
                             ->label('Address')
149
                             ->label('Address')
140
                             ->maxLength(100)
150
                             ->maxLength(100)
141
-                            ->placeholder('Enter Address')
142
                             ->columnSpanFull()
151
                             ->columnSpanFull()
143
                             ->nullable(),
152
                             ->nullable(),
153
+                        Forms\Components\Select::make('country')
154
+                            ->label('Country')
155
+                            ->searchable()
156
+                            ->reactive()
157
+                            ->options(Contact::getCountryOptions())
158
+                            ->nullable(),
159
+                        Forms\Components\Select::make('doesnt_exist') // TODO: Remove this when we have a better way to handle the searchable select when disabled
160
+                            ->label('Province/State')
161
+                            ->disabled()
162
+                            ->hidden(static fn (callable $get) => $get('country') !== null),
163
+                        Forms\Components\Select::make('state')
164
+                            ->label('Province/State')
165
+                            ->hidden(static fn (callable $get) => $get('country') === null)
166
+                            ->options(static function (callable $get) {
167
+                                $country = $get('country');
168
+
169
+                                if (! $country) {
170
+                                    return [];
171
+                                }
172
+
173
+                                return Contact::getRegionOptions($country);
174
+                            })
175
+                            ->searchable()
176
+                            ->nullable(),
144
                         Forms\Components\TextInput::make('city')
177
                         Forms\Components\TextInput::make('city')
145
                             ->label('Town/City')
178
                             ->label('Town/City')
146
                             ->maxLength(100)
179
                             ->maxLength(100)
147
-                            ->placeholder('Enter Town/City')
148
                             ->nullable(),
180
                             ->nullable(),
149
                         Forms\Components\TextInput::make('zip_code')
181
                         Forms\Components\TextInput::make('zip_code')
150
                             ->label('Postal/Zip Code')
182
                             ->label('Postal/Zip Code')
151
                             ->maxLength(100)
183
                             ->maxLength(100)
152
-                            ->placeholder('Enter Postal/Zip Code')
153
                             ->nullable(),
184
                             ->nullable(),
154
-                        Forms\Components\TextInput::make('state')
155
-                            ->label('Province/State')
156
-                            ->maxLength(100)
157
-                            ->placeholder('Enter Province/State')
158
-                            ->required(),
159
-                        Forms\Components\TextInput::make('country')
160
-                            ->label('Country')
161
-                            ->maxLength(100)
162
-                            ->placeholder('Enter Country')
163
-                            ->required(),
164
-                    ])->columns(2),
185
+                    ])->columns(),
165
             ]);
186
             ]);
166
     }
187
     }
167
 
188
 
170
         return $table
191
         return $table
171
             ->columns([
192
             ->columns([
172
                 Tables\Columns\TextColumn::make('name')
193
                 Tables\Columns\TextColumn::make('name')
194
+                    ->label('Name')
173
                     ->weight('semibold')
195
                     ->weight('semibold')
174
-                    ->searchable()
175
-                    ->sortable(),
176
-                Tables\Columns\TextColumn::make('tax_number')
196
+                    ->description(static fn (Contact $record) => $record->tax_number ?: 'N/A')
177
                     ->searchable()
197
                     ->searchable()
178
                     ->sortable(),
198
                     ->sortable(),
179
                 Tables\Columns\TextColumn::make('email')
199
                 Tables\Columns\TextColumn::make('email')
180
-                    ->searchable(),
181
-                Tables\Columns\TextColumn::make('phone')
182
-                    ->searchable(),
183
-                Tables\Columns\TextColumn::make('country')
200
+                    ->label('Email')
201
+                    ->formatStateUsing(static fn (Contact $record) => $record->email ?: 'N/A')
202
+                    ->description(static fn (Contact $record) => $record->phone ?: 'N/A')
184
                     ->searchable()
203
                     ->searchable()
185
                     ->sortable(),
204
                     ->sortable(),
186
-                Tables\Columns\TextColumn::make('currency.name')
205
+                Tables\Columns\TextColumn::make('country')
206
+                    ->label('Country')
187
                     ->searchable()
207
                     ->searchable()
208
+                    ->formatStateUsing(static fn (Contact $record) => $record->country ?: 'N/A')
209
+                    ->description(static fn (Contact $record) => $record->currency->name ?: 'N/A')
188
                     ->sortable(),
210
                     ->sortable(),
189
             ])
211
             ])
190
             ->filters([
212
             ->filters([
197
                 Tables\Actions\DeleteBulkAction::make(),
219
                 Tables\Actions\DeleteBulkAction::make(),
198
             ]);
220
             ]);
199
     }
221
     }
200
-    
222
+
201
     public static function getRelations(): array
223
     public static function getRelations(): array
202
     {
224
     {
203
         return [
225
         return [
204
             //
226
             //
205
         ];
227
         ];
206
     }
228
     }
207
-    
229
+
208
     public static function getPages(): array
230
     public static function getPages(): array
209
     {
231
     {
210
         return [
232
         return [
212
             'create' => Pages\CreateCustomer::route('/create'),
234
             'create' => Pages\CreateCustomer::route('/create'),
213
             'edit' => Pages\EditCustomer::route('/{record}/edit'),
235
             'edit' => Pages\EditCustomer::route('/{record}/edit'),
214
         ];
236
         ];
215
-    }    
237
+    }
216
 }
238
 }

+ 5
- 2
app/Filament/Resources/CustomerResource/Pages/CreateCustomer.php 查看文件

5
 use App\Filament\Resources\CustomerResource;
5
 use App\Filament\Resources\CustomerResource;
6
 use Filament\Pages\Actions;
6
 use Filament\Pages\Actions;
7
 use Filament\Resources\Pages\CreateRecord;
7
 use Filament\Resources\Pages\CreateRecord;
8
+use Illuminate\Support\Facades\Auth;
9
+use Squire\Models\Region;
8
 
10
 
9
 class CreateCustomer extends CreateRecord
11
 class CreateCustomer extends CreateRecord
10
 {
12
 {
17
 
19
 
18
     protected function mutateFormDataBeforeCreate(array $data): array
20
     protected function mutateFormDataBeforeCreate(array $data): array
19
     {
21
     {
20
-        $data['company_id'] = auth()->user()->currentCompany->id;
22
+        $data['company_id'] = Auth::user()->currentCompany->id;
21
         $data['type'] = 'customer';
23
         $data['type'] = 'customer';
22
-        $data['created_by'] = auth()->id();
24
+        $data['created_by'] = Auth::id();
25
+
23
         return $data;
26
         return $data;
24
     }
27
     }
25
 }
28
 }

+ 14
- 0
app/Filament/Resources/CustomerResource/Pages/EditCustomer.php 查看文件

5
 use App\Filament\Resources\CustomerResource;
5
 use App\Filament\Resources\CustomerResource;
6
 use Filament\Pages\Actions;
6
 use Filament\Pages\Actions;
7
 use Filament\Resources\Pages\EditRecord;
7
 use Filament\Resources\Pages\EditRecord;
8
+use Illuminate\Support\Facades\Auth;
8
 
9
 
9
 class EditCustomer extends EditRecord
10
 class EditCustomer extends EditRecord
10
 {
11
 {
16
             Actions\DeleteAction::make(),
17
             Actions\DeleteAction::make(),
17
         ];
18
         ];
18
     }
19
     }
20
+
21
+    protected function getRedirectUrl(): string
22
+    {
23
+        return $this->getResource()::getUrl('index');
24
+    }
25
+
26
+    protected function mutateFormDataBeforeSave(array $data): array
27
+    {
28
+        $data['company_id'] = Auth::user()->currentCompany->id;
29
+        $data['updated_by'] = Auth::id();
30
+
31
+        return $data;
32
+    }
19
 }
33
 }

+ 186
- 0
app/Filament/Resources/DiscountResource.php 查看文件

1
+<?php
2
+
3
+namespace App\Filament\Resources;
4
+
5
+use App\Filament\Resources\DiscountResource\Pages;
6
+use App\Filament\Resources\DiscountResource\RelationManagers;
7
+use App\Models\Setting\Discount;
8
+use App\Models\Setting\Tax;
9
+use Filament\Forms;
10
+use Filament\Forms\Components\TextInput\Mask;
11
+use Filament\Resources\Form;
12
+use Filament\Resources\Resource;
13
+use Filament\Resources\Table;
14
+use Filament\Tables;
15
+use Illuminate\Database\Eloquent\Builder;
16
+use Illuminate\Database\Eloquent\SoftDeletingScope;
17
+use Wallo\FilamentSelectify\Components\ToggleButton;
18
+
19
+class DiscountResource extends Resource
20
+{
21
+    protected static ?string $model = Discount::class;
22
+
23
+    protected static ?string $navigationIcon = 'heroicon-o-tag';
24
+
25
+    protected static ?string $navigationGroup = 'Settings';
26
+
27
+    public static function form(Form $form): Form
28
+    {
29
+        return $form
30
+            ->schema([
31
+                Forms\Components\Section::make('General')
32
+                ->schema([
33
+                    Forms\Components\TextInput::make('name')
34
+                        ->label('Name')
35
+                        ->required(),
36
+                    Forms\Components\TextInput::make('description')
37
+                        ->label('Description'),
38
+                    Forms\Components\Select::make('computation')
39
+                        ->label('Computation')
40
+                        ->options(Discount::getComputationTypes())
41
+                        ->reactive()
42
+                        ->searchable()
43
+                        ->default('percentage')
44
+                        ->required(),
45
+                    Forms\Components\TextInput::make('rate')
46
+                        ->label('Rate')
47
+                        ->mask(static fn (Mask $mask) => $mask
48
+                            ->numeric()
49
+                            ->decimalPlaces(4)
50
+                            ->decimalSeparator('.')
51
+                            ->thousandsSeparator(',')
52
+                            ->minValue(0)
53
+                            ->normalizeZeros()
54
+                            ->padFractionalZeros()
55
+                        )
56
+                        ->suffix(static function (callable $get) {
57
+                            $computation = $get('computation');
58
+
59
+                            if ($computation === 'percentage') {
60
+                                return '%';
61
+                            }
62
+
63
+                            return null;
64
+                        })
65
+                        ->default(0.0000)
66
+                        ->required(),
67
+                    Forms\Components\Select::make('type')
68
+                        ->label('Type')
69
+                        ->options(Discount::getDiscountTypes())
70
+                        ->searchable()
71
+                        ->default('sales')
72
+                        ->required(),
73
+                    Forms\Components\Select::make('scope')
74
+                        ->label('Scope')
75
+                        ->options(Discount::getDiscountScopes())
76
+                        ->searchable(),
77
+                    Forms\Components\DateTimePicker::make('start_date')
78
+                        ->label('Start Date')
79
+                        ->minDate(now())
80
+                        ->maxDate(now()->addYear())
81
+                        ->format('Y-m-d H:i:s')
82
+                        ->displayFormat('F d, Y H:i')
83
+                        ->withoutSeconds()
84
+                        ->disabled(static fn (Discount $record) => $record->start_date->isPast())
85
+                        ->helperText(static fn (Forms\Components\DateTimePicker $component) => $component->isDisabled() ? 'Start date cannot be changed after the discount has begun.' : null),
86
+                    Forms\Components\DateTimePicker::make('end_date')
87
+                        ->label('End Date')
88
+                        ->minDate(now())
89
+                        ->maxDate(now()->addYears(2))
90
+                        ->format('Y-m-d H:i:s')
91
+                        ->displayFormat('F d, Y H:i')
92
+                        ->withoutSeconds(),
93
+                    ToggleButton::make('enabled')
94
+                        ->label('Default')
95
+                        ->offColor('danger')
96
+                        ->onColor('primary'),
97
+                ])->columns(),
98
+            ]);
99
+    }
100
+
101
+    public static function table(Table $table): Table
102
+    {
103
+        return $table
104
+            ->columns([
105
+                Tables\Columns\TextColumn::make('name')
106
+                    ->label('Name')
107
+                    ->weight('semibold')
108
+                    ->icon(static fn (Discount $record) => $record->enabled ? 'heroicon-o-lock-closed' : null)
109
+                    ->tooltip(static fn (Discount $record) => $record->enabled ? "Default ". ucwords($record->type) . " Discount" : null)
110
+                    ->iconPosition('after')
111
+                    ->searchable()
112
+                    ->sortable(),
113
+                Tables\Columns\TextColumn::make('computation')
114
+                    ->label('Computation')
115
+                    ->formatStateUsing(static fn (Discount $record) => ucwords($record->computation))
116
+                    ->searchable()
117
+                    ->sortable(),
118
+                Tables\Columns\TextColumn::make('rate')
119
+                    ->label('Rate')
120
+                    ->formatStateUsing(static function (Discount $record) {
121
+                        $rate = $record->rate;
122
+
123
+                        if (str_contains($rate, '.')) {
124
+                            $rate = rtrim(rtrim($rate, '0'), '.');
125
+                        }
126
+
127
+                        return $rate . ($record->computation === 'percentage' ? '%' : null);
128
+                    })
129
+                    ->searchable()
130
+                    ->sortable(),
131
+                Tables\Columns\BadgeColumn::make('type')
132
+                    ->label('Type')
133
+                    ->formatStateUsing(static fn (Discount $record) => ucwords($record->type))
134
+                    ->colors([
135
+                        'success' => 'sales',
136
+                        'warning' => 'purchase',
137
+                        'secondary' => 'none',
138
+                    ])
139
+                    ->icons([
140
+                        'heroicon-o-cash' => 'sales',
141
+                        'heroicon-o-shopping-bag' => 'purchase',
142
+                        'heroicon-o-x-circle' => 'none',
143
+                    ])
144
+                    ->searchable()
145
+                    ->sortable(),
146
+                Tables\Columns\TextColumn::make('start_date')
147
+                ->label('Start Date')
148
+                ->formatStateUsing(static fn (Discount $record) => $record->start_date ? $record->start_date->format('F d, Y H:i') : null)
149
+                ->searchable()
150
+                ->sortable(),
151
+                Tables\Columns\TextColumn::make('end_date')
152
+                ->label('End Date')
153
+                ->formatStateUsing(static fn (Discount $record) => $record->end_date ? $record->end_date->format('F d, Y H:i') : null)
154
+                ->color(static fn(Discount $record) => $record->end_date?->isPast() ? 'danger' : null)
155
+                ->searchable()
156
+                ->sortable(),
157
+            ])
158
+            ->filters([
159
+                //
160
+            ])
161
+            ->actions([
162
+                // Create a cron job to update recurring discounts once they have expired
163
+                Tables\Actions\EditAction::make(),
164
+                Tables\Actions\DeleteAction::make(),
165
+            ])
166
+            ->bulkActions([
167
+                Tables\Actions\DeleteBulkAction::make(),
168
+            ]);
169
+    }
170
+
171
+    public static function getRelations(): array
172
+    {
173
+        return [
174
+            //
175
+        ];
176
+    }
177
+
178
+    public static function getPages(): array
179
+    {
180
+        return [
181
+            'index' => Pages\ListDiscounts::route('/'),
182
+            'create' => Pages\CreateDiscount::route('/create'),
183
+            'edit' => Pages\EditDiscount::route('/{record}/edit'),
184
+        ];
185
+    }
186
+}

+ 12
- 0
app/Filament/Resources/DiscountResource/Pages/CreateDiscount.php 查看文件

1
+<?php
2
+
3
+namespace App\Filament\Resources\DiscountResource\Pages;
4
+
5
+use App\Filament\Resources\DiscountResource;
6
+use Filament\Pages\Actions;
7
+use Filament\Resources\Pages\CreateRecord;
8
+
9
+class CreateDiscount extends CreateRecord
10
+{
11
+    protected static string $resource = DiscountResource::class;
12
+}

+ 19
- 0
app/Filament/Resources/DiscountResource/Pages/EditDiscount.php 查看文件

1
+<?php
2
+
3
+namespace App\Filament\Resources\DiscountResource\Pages;
4
+
5
+use App\Filament\Resources\DiscountResource;
6
+use Filament\Pages\Actions;
7
+use Filament\Resources\Pages\EditRecord;
8
+
9
+class EditDiscount extends EditRecord
10
+{
11
+    protected static string $resource = DiscountResource::class;
12
+
13
+    protected function getActions(): array
14
+    {
15
+        return [
16
+            Actions\DeleteAction::make(),
17
+        ];
18
+    }
19
+}

+ 19
- 0
app/Filament/Resources/DiscountResource/Pages/ListDiscounts.php 查看文件

1
+<?php
2
+
3
+namespace App\Filament\Resources\DiscountResource\Pages;
4
+
5
+use App\Filament\Resources\DiscountResource;
6
+use Filament\Pages\Actions;
7
+use Filament\Resources\Pages\ListRecords;
8
+
9
+class ListDiscounts extends ListRecords
10
+{
11
+    protected static string $resource = DiscountResource::class;
12
+
13
+    protected function getActions(): array
14
+    {
15
+        return [
16
+            Actions\CreateAction::make(),
17
+        ];
18
+    }
19
+}

+ 69
- 6
app/Filament/Resources/InvoiceResource.php 查看文件

4
 
4
 
5
 use App\Filament\Resources\InvoiceResource\Pages;
5
 use App\Filament\Resources\InvoiceResource\Pages;
6
 use App\Filament\Resources\InvoiceResource\RelationManagers;
6
 use App\Filament\Resources\InvoiceResource\RelationManagers;
7
+use Wallo\FilamentSelectify\Components\ButtonGroup;
8
+use App\Models\Banking\Account;
7
 use App\Models\Document\Document;
9
 use App\Models\Document\Document;
8
 use Filament\Forms;
10
 use Filament\Forms;
9
 use Filament\Resources\Form;
11
 use Filament\Resources\Form;
18
 {
20
 {
19
     protected static ?string $model = Document::class;
21
     protected static ?string $model = Document::class;
20
 
22
 
21
-    protected static ?string $navigationIcon = 'heroicon-o-collection';
23
+    protected static ?string $navigationIcon = 'heroicon-o-document-text';
22
 
24
 
23
     protected static ?string $navigationGroup = 'Sales';
25
     protected static ?string $navigationGroup = 'Sales';
24
 
26
 
37
     {
39
     {
38
         return $form
40
         return $form
39
             ->schema([
41
             ->schema([
40
-                //
42
+                Forms\Components\Section::make('Billing')
43
+                    ->schema([
44
+                        Forms\Components\Grid::make(3)
45
+                            ->schema([
46
+                                Forms\Components\Select::make('contact_id')
47
+                                    ->label('Customer')
48
+                                    ->preload()
49
+                                    ->placeholder('Select a customer')
50
+                                    ->relationship('contact', 'name', static fn (Builder $query) => $query->where('type', 'customer')->where('company_id', Auth::user()->currentCompany->id))
51
+                                    ->searchable()
52
+                                    ->required()
53
+                                    ->createOptionForm([
54
+                                        ButtonGroup::make('contact.entity')
55
+                                            ->label('Entity')
56
+                                            ->options([
57
+                                                'company' => 'Company',
58
+                                                'individual' => 'Individual',
59
+                                            ])
60
+                                            ->default('company')
61
+                                            ->required(),
62
+                                        Forms\Components\TextInput::make('contact.name')
63
+                                            ->label('Name')
64
+                                            ->maxLength(100)
65
+                                            ->required(),
66
+                                        Forms\Components\TextInput::make('contact.email')
67
+                                            ->label('Email')
68
+                                            ->email()
69
+                                            ->nullable(),
70
+                                        Forms\Components\TextInput::make('contact.phone')
71
+                                            ->label('Phone')
72
+                                            ->tel()
73
+                                            ->maxLength(20),
74
+                                        Forms\Components\Select::make('contact.currency_code')
75
+                                            ->label('Currency')
76
+                                            ->relationship('currency', 'name', static fn (Builder $query) => $query->where('company_id', Auth::user()->currentCompany->id))
77
+                                            ->preload()
78
+                                            ->default(Account::getDefaultCurrencyCode())
79
+                                            ->searchable()
80
+                                            ->reactive()
81
+                                            ->required(),
82
+                                    ])->columnSpan(1),
83
+                                Forms\Components\Grid::make(2)
84
+                                    ->schema([
85
+                                        Forms\Components\DatePicker::make('document_date')
86
+                                            ->label('Invoice Date')
87
+                                            ->default(now())
88
+                                            ->format('Y-m-d')
89
+                                            ->required(),
90
+                                        Forms\Components\DatePicker::make('due_date')
91
+                                            ->label('Due Date')
92
+                                            ->default(now())
93
+                                            ->format('Y-m-d')
94
+                                            ->required(),
95
+                                        Forms\Components\TextInput::make('document_number')
96
+                                            ->label('Invoice Number')
97
+                                            ->required(),
98
+                                        Forms\Components\TextInput::make('order_number')
99
+                                            ->label('Order Number')
100
+                                            ->nullable(),
101
+                                    ])->columnSpan(2),
102
+                            ])->columns(3),
103
+                    ])->columns(3),
41
             ]);
104
             ]);
42
     }
105
     }
43
 
106
 
57
                 Tables\Actions\DeleteBulkAction::make(),
120
                 Tables\Actions\DeleteBulkAction::make(),
58
             ]);
121
             ]);
59
     }
122
     }
60
-    
123
+
61
     public static function getRelations(): array
124
     public static function getRelations(): array
62
     {
125
     {
63
         return [
126
         return [
64
-            //
127
+            RelationManagers\DocumentItemsRelationManager::class,
65
         ];
128
         ];
66
     }
129
     }
67
-    
130
+
68
     public static function getPages(): array
131
     public static function getPages(): array
69
     {
132
     {
70
         return [
133
         return [
72
             'create' => Pages\CreateInvoice::route('/create'),
135
             'create' => Pages\CreateInvoice::route('/create'),
73
             'edit' => Pages\EditInvoice::route('/{record}/edit'),
136
             'edit' => Pages\EditInvoice::route('/{record}/edit'),
74
         ];
137
         ];
75
-    }    
138
+    }
76
 }
139
 }

+ 12
- 0
app/Filament/Resources/InvoiceResource/Pages/CreateInvoice.php 查看文件

5
 use App\Filament\Resources\InvoiceResource;
5
 use App\Filament\Resources\InvoiceResource;
6
 use Filament\Pages\Actions;
6
 use Filament\Pages\Actions;
7
 use Filament\Resources\Pages\CreateRecord;
7
 use Filament\Resources\Pages\CreateRecord;
8
+use Illuminate\Support\Facades\Auth;
8
 
9
 
9
 class CreateInvoice extends CreateRecord
10
 class CreateInvoice extends CreateRecord
10
 {
11
 {
11
     protected static string $resource = InvoiceResource::class;
12
     protected static string $resource = InvoiceResource::class;
13
+
14
+    protected function mutateFormDataBeforeCreate(array $data): array
15
+    {
16
+        $data['company_id'] = Auth::user()->currentCompany->id;
17
+        $data['type'] = 'invoice';
18
+        $data['status'] = 'draft';
19
+        $data['amount'] = 0;
20
+        $data['created_by'] = Auth::id();
21
+
22
+        return $data;
23
+    }
12
 }
24
 }

+ 14
- 0
app/Filament/Resources/InvoiceResource/Pages/EditInvoice.php 查看文件

5
 use App\Filament\Resources\InvoiceResource;
5
 use App\Filament\Resources\InvoiceResource;
6
 use Filament\Pages\Actions;
6
 use Filament\Pages\Actions;
7
 use Filament\Resources\Pages\EditRecord;
7
 use Filament\Resources\Pages\EditRecord;
8
+use Illuminate\Support\Facades\Auth;
8
 
9
 
9
 class EditInvoice extends EditRecord
10
 class EditInvoice extends EditRecord
10
 {
11
 {
16
             Actions\DeleteAction::make(),
17
             Actions\DeleteAction::make(),
17
         ];
18
         ];
18
     }
19
     }
20
+
21
+    protected function getRedirectUrl(): string
22
+    {
23
+        return $this->getResource()::getUrl('index');
24
+    }
25
+
26
+    protected function mutateFormDataBeforeSave(array $data): array
27
+    {
28
+        $data['company_id'] = Auth::user()->currentCompany->id;
29
+        $data['updated_by'] = Auth::id();
30
+
31
+        return $data;
32
+    }
19
 }
33
 }

+ 49
- 0
app/Filament/Resources/InvoiceResource/RelationManagers/DocumentItemsRelationManager.php 查看文件

1
+<?php
2
+
3
+namespace App\Filament\Resources\InvoiceResource\RelationManagers;
4
+
5
+use Filament\Forms;
6
+use Filament\Resources\Form;
7
+use Filament\Resources\RelationManagers\RelationManager;
8
+use Filament\Resources\Table;
9
+use Filament\Tables;
10
+use Illuminate\Database\Eloquent\Builder;
11
+use Illuminate\Database\Eloquent\SoftDeletingScope;
12
+
13
+class DocumentItemsRelationManager extends RelationManager
14
+{
15
+    protected static string $relationship = 'items';
16
+
17
+    protected static ?string $recordTitleAttribute = 'invoice';
18
+
19
+    public static function form(Form $form): Form
20
+    {
21
+        return $form
22
+            ->schema([
23
+                Forms\Components\TextInput::make('item')
24
+                    ->required()
25
+                    ->maxLength(255),
26
+            ]);
27
+    }
28
+
29
+    public static function table(Table $table): Table
30
+    {
31
+        return $table
32
+            ->columns([
33
+                Tables\Columns\TextColumn::make('item'),
34
+            ])
35
+            ->filters([
36
+                //
37
+            ])
38
+            ->headerActions([
39
+                Tables\Actions\CreateAction::make(),
40
+            ])
41
+            ->actions([
42
+                Tables\Actions\EditAction::make(),
43
+                Tables\Actions\DeleteAction::make(),
44
+            ])
45
+            ->bulkActions([
46
+                Tables\Actions\DeleteBulkAction::make(),
47
+            ]);
48
+    }    
49
+}

+ 199
- 0
app/Filament/Resources/TaxResource.php 查看文件

1
+<?php
2
+
3
+namespace App\Filament\Resources;
4
+
5
+use App\Filament\Resources\TaxResource\Pages;
6
+use App\Filament\Resources\TaxResource\RelationManagers;
7
+use App\Models\Setting\Category;
8
+use App\Models\Setting\Tax;
9
+use Closure;
10
+use Exception;
11
+use Filament\Forms;
12
+use Filament\Forms\Components\TextInput\Mask;
13
+use Filament\Notifications\Notification;
14
+use Filament\Resources\Form;
15
+use Filament\Resources\Resource;
16
+use Filament\Resources\Table;
17
+use Filament\Tables;
18
+use Illuminate\Support\Collection;
19
+use Wallo\FilamentSelectify\Components\ToggleButton;
20
+
21
+class TaxResource extends Resource
22
+{
23
+    protected static ?string $model = Tax::class;
24
+
25
+    protected static ?string $navigationIcon = 'heroicon-o-receipt-tax';
26
+
27
+    protected static ?string $navigationGroup = 'Settings';
28
+
29
+    public static function form(Form $form): Form
30
+    {
31
+        return $form
32
+            ->schema([
33
+                Forms\Components\Section::make('General')
34
+                    ->schema([
35
+                        Forms\Components\TextInput::make('name')
36
+                            ->label('Name')
37
+                            ->required(),
38
+                        Forms\Components\TextInput::make('description')
39
+                            ->label('Description'),
40
+                        Forms\Components\Select::make('computation')
41
+                            ->label('Computation')
42
+                            ->options(Tax::getComputationTypes())
43
+                            ->reactive()
44
+                            ->searchable()
45
+                            ->default('percentage')
46
+                            ->required(),
47
+                        Forms\Components\TextInput::make('rate')
48
+                            ->label('Rate')
49
+                            ->mask(static fn (Mask $mask) => $mask
50
+                                ->numeric()
51
+                                ->decimalPlaces(4)
52
+                                ->decimalSeparator('.')
53
+                                ->thousandsSeparator(',')
54
+                                ->minValue(0)
55
+                                ->normalizeZeros()
56
+                                ->padFractionalZeros()
57
+                            )
58
+                            ->suffix(static function (callable $get) {
59
+                                $computation = $get('computation');
60
+
61
+                                if ($computation === 'percentage' || $computation === 'compound') {
62
+                                    return '%';
63
+                                }
64
+
65
+                                return null;
66
+                            })
67
+                            ->default(0.0000)
68
+                            ->required(),
69
+                        Forms\Components\Select::make('type')
70
+                            ->label('Type')
71
+                            ->options(Tax::getTaxTypes())
72
+                            ->searchable()
73
+                            ->default('sales')
74
+                            ->required(),
75
+                        Forms\Components\Select::make('scope')
76
+                            ->label('Scope')
77
+                            ->options(Tax::getTaxScopes())
78
+                            ->searchable(),
79
+                        ToggleButton::make('enabled')
80
+                            ->label('Default')
81
+                            ->offColor('danger')
82
+                            ->onColor('primary'),
83
+                    ])->columns(),
84
+            ]);
85
+    }
86
+
87
+    /**
88
+     * @throws Exception
89
+     */
90
+    public static function table(Table $table): Table
91
+    {
92
+        return $table
93
+            ->columns([
94
+                Tables\Columns\TextColumn::make('name')
95
+                    ->label('Name')
96
+                    ->weight('semibold')
97
+                    ->icon(static fn (Tax $record) => $record->enabled ? 'heroicon-o-lock-closed' : null)
98
+                    ->tooltip(static fn (Tax $record) => $record->enabled ? "Default " .ucwords($record->type) . " Tax" : null)
99
+                    ->iconPosition('after')
100
+                    ->searchable()
101
+                    ->sortable(),
102
+                Tables\Columns\TextColumn::make('computation')
103
+                    ->label('Computation')
104
+                    ->formatStateUsing(static fn (Tax $record) => ucwords($record->computation))
105
+                    ->searchable()
106
+                    ->sortable(),
107
+                Tables\Columns\TextColumn::make('rate')
108
+                    ->label('Rate')
109
+                    ->formatStateUsing(static function (Tax $record) {
110
+                        $rate = $record->rate;
111
+
112
+                        if (str_contains($rate, '.')) {
113
+                            $rate = rtrim(rtrim($rate, '0'), '.');
114
+                        }
115
+
116
+                        return $rate . ($record->computation === 'percentage' || $record->computation === 'compound' ? '%' : null);
117
+                    })
118
+                    ->searchable()
119
+                    ->sortable(),
120
+                Tables\Columns\BadgeColumn::make('type')
121
+                    ->label('Type')
122
+                    ->formatStateUsing(static fn (Tax $record) => ucwords($record->type))
123
+                    ->colors([
124
+                        'success' => 'sales',
125
+                        'warning' => 'purchase',
126
+                        'secondary' => 'none',
127
+                    ])
128
+                    ->icons([
129
+                        'heroicon-o-cash' => 'sales',
130
+                        'heroicon-o-shopping-bag' => 'purchase',
131
+                        'heroicon-o-x-circle' => 'none',
132
+                    ])
133
+                    ->searchable()
134
+                    ->sortable(),
135
+            ])
136
+            ->filters([
137
+                //
138
+            ])
139
+            ->actions([
140
+                Tables\Actions\EditAction::make(),
141
+                Tables\Actions\DeleteAction::make()
142
+                    ->before(static function (Tables\Actions\DeleteAction $action, Tax $record) {
143
+                        if ($record->enabled) {
144
+                            Notification::make()
145
+                                ->danger()
146
+                                ->title('Action Denied')
147
+                                ->body(__('The :name tax is currently set as your default :Type tax and cannot be deleted. Please set a different tax as your default before attempting to delete this one.', ['name' => $record->name, 'Type' => ucwords($record->type)]))
148
+                                ->persistent()
149
+                                ->send();
150
+
151
+                            $action->cancel();
152
+                        }
153
+                    }),
154
+            ])
155
+            ->bulkActions([
156
+                Tables\Actions\DeleteBulkAction::make()
157
+                    ->before(static function (Collection $records, Tables\Actions\DeleteBulkAction $action) {
158
+                        $defaultTaxes = $records->filter(static function (Tax $record) {
159
+                            return $record->enabled;
160
+                        });
161
+
162
+                        if ($defaultTaxes->isNotEmpty()) {
163
+                            $defaultTaxNames = $defaultTaxes->pluck('name')->toArray();
164
+
165
+                            Notification::make()
166
+                                ->danger()
167
+                                ->title('Action Denied')
168
+                                ->body(static function () use ($defaultTaxNames) {
169
+                                    $message = __('The following taxes are currently set as your default and cannot be deleted. Please set a different tax as your default before attempting to delete these ones.') . "<br><br>";
170
+                                    $message .= implode("<br>", array_map(static function ($name) {
171
+                                        return "&bull; " . $name;
172
+                                    }, $defaultTaxNames));
173
+                                    return $message;
174
+                                })
175
+                                ->persistent()
176
+                                ->send();
177
+
178
+                            $action->cancel();
179
+                        }
180
+                    }),
181
+            ]);
182
+    }
183
+
184
+    public static function getRelations(): array
185
+    {
186
+        return [
187
+            //
188
+        ];
189
+    }
190
+
191
+    public static function getPages(): array
192
+    {
193
+        return [
194
+            'index' => Pages\ListTaxes::route('/'),
195
+            'create' => Pages\CreateTax::route('/create'),
196
+            'edit' => Pages\EditTax::route('/{record}/edit'),
197
+        ];
198
+    }
199
+}

+ 74
- 0
app/Filament/Resources/TaxResource/Pages/CreateTax.php 查看文件

1
+<?php
2
+
3
+namespace App\Filament\Resources\TaxResource\Pages;
4
+
5
+use App\Filament\Resources\TaxResource;
6
+use App\Models\Setting\Tax;
7
+use Filament\Pages\Actions;
8
+use Filament\Resources\Pages\CreateRecord;
9
+use Illuminate\Database\Eloquent\Model;
10
+use Illuminate\Support\Facades\Auth;
11
+use Illuminate\Support\Facades\DB;
12
+
13
+class CreateTax extends CreateRecord
14
+{
15
+    protected static string $resource = TaxResource::class;
16
+
17
+    protected function getRedirectUrl(): string
18
+    {
19
+        return $this->getResource()::getUrl('index');
20
+    }
21
+
22
+    protected function mutateFormDataBeforeCreate(array $data): array
23
+    {
24
+        $data['company_id'] = Auth::user()->currentCompany->id;
25
+        $data['enabled'] = (bool)$data['enabled'];
26
+        $data['created_by'] = Auth::id();
27
+
28
+        return $data;
29
+    }
30
+
31
+    protected function handleRecordCreation(array $data): Model
32
+    {
33
+        return DB::transaction(function () use ($data) {
34
+            $currentCompanyId = auth()->user()->currentCompany->id;
35
+            $type = $data['type'] ?? null;
36
+            $enabled = (bool)($data['enabled'] ?? false);
37
+
38
+            if ($enabled === true) {
39
+                $this->disableExistingRecord($currentCompanyId, $type);
40
+            } else {
41
+                $this->ensureAtLeastOneEnabled($currentCompanyId, $type, $enabled);
42
+            }
43
+
44
+            $data['enabled'] = $enabled;
45
+
46
+            return parent::handleRecordCreation($data);
47
+        });
48
+    }
49
+
50
+    protected function disableExistingRecord(int $companyId, string $type): void
51
+    {
52
+        $existingEnabledRecord = Tax::where('company_id', $companyId)
53
+            ->where('enabled', true)
54
+            ->where('type', $type)
55
+            ->first();
56
+
57
+        if ($existingEnabledRecord !== null) {
58
+            $existingEnabledRecord->enabled = false;
59
+            $existingEnabledRecord->save();
60
+        }
61
+    }
62
+
63
+    protected function ensureAtLeastOneEnabled(int $companyId, string $type, bool &$enabled): void
64
+    {
65
+        $otherEnabledRecords = Tax::where('company_id', $companyId)
66
+            ->where('enabled', true)
67
+            ->where('type', $type)
68
+            ->count();
69
+
70
+        if ($otherEnabledRecords === 0) {
71
+            $enabled = true;
72
+        }
73
+    }
74
+}

+ 106
- 0
app/Filament/Resources/TaxResource/Pages/EditTax.php 查看文件

1
+<?php
2
+
3
+namespace App\Filament\Resources\TaxResource\Pages;
4
+
5
+use App\Filament\Resources\TaxResource;
6
+use App\Models\Setting\Tax;
7
+use Filament\Pages\Actions;
8
+use Filament\Resources\Pages\EditRecord;
9
+use Illuminate\Database\Eloquent\Model;
10
+use Illuminate\Support\Facades\Auth;
11
+use Illuminate\Support\Facades\DB;
12
+
13
+class EditTax extends EditRecord
14
+{
15
+    protected static string $resource = TaxResource::class;
16
+
17
+    protected function getActions(): array
18
+    {
19
+        return [
20
+            Actions\DeleteAction::make(),
21
+        ];
22
+    }
23
+
24
+    protected function getRedirectUrl(): string
25
+    {
26
+        return $this->getResource()::getUrl('index');
27
+    }
28
+
29
+    protected function mutateFormDataBeforeUpdate(array $data): array
30
+    {
31
+        $data['company_id'] = Auth::user()->currentCompany->id;
32
+        $data['enabled'] = (bool)$data['enabled'];
33
+        $data['updated_by'] = Auth::id();
34
+
35
+        return $data;
36
+    }
37
+
38
+    protected function handleRecordUpdate(Model|Tax $record, array $data): Model|Tax
39
+    {
40
+        return DB::transaction(function () use ($record, $data) {
41
+            $currentCompanyId = auth()->user()->currentCompany->id;
42
+            $recordId = $record->id;
43
+            $oldType = $record->type;
44
+            $newType = $data['type'];
45
+            $enabled = (bool)($data['enabled'] ?? false);
46
+
47
+            // If the record type has changed and it was previously enabled
48
+            if ($oldType !== $newType && $record->enabled) {
49
+                $this->changeRecordType($currentCompanyId, $recordId, $oldType);
50
+            }
51
+
52
+            if ($enabled === true) {
53
+                $this->disableExistingRecord($currentCompanyId, $recordId, $newType);
54
+            } elseif ($enabled === false) {
55
+                $this->ensureAtLeastOneEnabled($currentCompanyId, $recordId, $newType, $enabled);
56
+            }
57
+
58
+            $data['enabled'] = $enabled;
59
+
60
+            return parent::handleRecordUpdate($record, $data);
61
+        });
62
+    }
63
+
64
+    protected function changeRecordType(int $companyId, int $recordId, string $oldType): void
65
+    {
66
+        $oldTypeRecord = $this->getCompanyCategoryRecord($companyId, $oldType, $recordId);
67
+
68
+        if ($oldTypeRecord) {
69
+            $oldTypeRecord->enabled = true;
70
+            $oldTypeRecord->save();
71
+        }
72
+    }
73
+
74
+    protected function disableExistingRecord(int $companyId, int $recordId, string $newType): void
75
+    {
76
+        $existingEnabledRecord = $this->getCompanyCategoryRecord($companyId, $newType, $recordId);
77
+
78
+        if ($existingEnabledRecord !== null) {
79
+            $existingEnabledRecord->enabled = false;
80
+            $existingEnabledRecord->save();
81
+        }
82
+    }
83
+
84
+    protected function ensureAtLeastOneEnabled(int $companyId, int $recordId, string $newType, bool &$enabled): void
85
+    {
86
+        $otherEnabledRecords = Tax::where('company_id', $companyId)
87
+            ->where('enabled', true)
88
+            ->where('type', $newType)
89
+            ->where('id', '!=', $recordId)
90
+            ->count();
91
+
92
+        if ($otherEnabledRecords === 0) {
93
+            $enabled = true;
94
+        }
95
+    }
96
+
97
+    protected function getCompanyCategoryRecord(int $companyId, string $type, int $recordId): ?Tax
98
+    {
99
+        return Tax::where('company_id', $companyId)
100
+            ->where('type', $type)
101
+            ->where('id', '!=', $recordId)
102
+            ->first();
103
+    }
104
+
105
+
106
+}

+ 19
- 0
app/Filament/Resources/TaxResource/Pages/ListTaxes.php 查看文件

1
+<?php
2
+
3
+namespace App\Filament\Resources\TaxResource\Pages;
4
+
5
+use App\Filament\Resources\TaxResource;
6
+use Filament\Pages\Actions;
7
+use Filament\Resources\Pages\ListRecords;
8
+
9
+class ListTaxes extends ListRecords
10
+{
11
+    protected static string $resource = TaxResource::class;
12
+
13
+    protected function getActions(): array
14
+    {
15
+        return [
16
+            Actions\CreateAction::make(),
17
+        ];
18
+    }
19
+}

+ 156
- 0
app/Http/Livewire/DefaultSetting.php 查看文件

1
+<?php
2
+
3
+namespace App\Http\Livewire;
4
+
5
+use App\Models\Banking\Account;
6
+use App\Models\Setting\Category;
7
+use App\Models\Setting\Currency;
8
+use App\Models\Setting\DefaultSetting as Defaults;
9
+use App\Models\Setting\Tax;
10
+use App\Traits\HandlesRecordCreation;
11
+use Filament\Forms\ComponentContainer;
12
+use Filament\Forms\Components\Section;
13
+use Filament\Forms\Components\Select;
14
+use Filament\Forms\Concerns\InteractsWithForms;
15
+use Filament\Forms\Contracts\HasForms;
16
+use Filament\Notifications\Notification;
17
+use Illuminate\Contracts\View\View;
18
+use Illuminate\Support\Facades\Auth;
19
+use Livewire\Component;
20
+
21
+/**
22
+ * @property ComponentContainer $form
23
+ */
24
+class DefaultSetting extends Component implements HasForms
25
+{
26
+    use InteractsWithForms, HandlesRecordCreation;
27
+
28
+    public Defaults $defaultSetting;
29
+
30
+    public $data;
31
+
32
+    public $record;
33
+
34
+    public function mount():void
35
+    {
36
+        $this->form->fill();
37
+    }
38
+
39
+    protected function getFormSchema(): array
40
+    {
41
+        return [
42
+            Section::make('General')
43
+                ->schema([
44
+                    Select::make('account_id')
45
+                        ->label('Account')
46
+                        ->options(Defaults::getAccounts())
47
+                        ->default(Defaults::getDefaultAccount())
48
+                        ->searchable()
49
+                        ->validationAttribute('Account')
50
+                        ->required(),
51
+                    Select::make('currency_code')
52
+                        ->label('Currency')
53
+                        ->options(Defaults::getCurrencies())
54
+                        ->default(Defaults::getDefaultCurrency())
55
+                        ->searchable()
56
+                        ->validationAttribute('Currency')
57
+                        ->required(),
58
+                ])->columns(),
59
+            Section::make('Taxes')
60
+                ->schema([
61
+                    Select::make('sales_tax_id')
62
+                        ->label('Sales Tax')
63
+                        ->options(Defaults::getSalesTaxes())
64
+                        ->default(Defaults::getDefaultSalesTax())
65
+                        ->searchable()
66
+                        ->validationAttribute('Sales Tax')
67
+                        ->required(),
68
+                    Select::make('purchase_tax_id')
69
+                        ->label('Purchase Tax')
70
+                        ->options(Defaults::getPurchaseTaxes())
71
+                        ->default(Defaults::getDefaultPurchaseTax())
72
+                        ->searchable()
73
+                        ->validationAttribute('Purchase Tax')
74
+                        ->required(),
75
+                ])->columns(),
76
+            Section::make('Categories')
77
+                ->schema([
78
+                    Select::make('income_category_id')
79
+                        ->label('Income Category')
80
+                        ->options(Defaults::getIncomeCategories())
81
+                        ->default(Defaults::getDefaultIncomeCategory())
82
+                        ->searchable()
83
+                        ->validationAttribute('Income Category')
84
+                        ->required(),
85
+                    Select::make('expense_category_id')
86
+                        ->label('Expense Category')
87
+                        ->options(Defaults::getExpenseCategories())
88
+                        ->default(Defaults::getDefaultExpenseCategory())
89
+                        ->searchable()
90
+                        ->validationAttribute('Expense Category')
91
+                        ->required(),
92
+                ])->columns(),
93
+        ];
94
+    }
95
+
96
+    public function create(): void
97
+    {
98
+        $data = $this->form->getState();
99
+
100
+        $data = $this->mutateFormDataBeforeCreate($data);
101
+
102
+        $this->record = $this->handleRecordCreation($data);
103
+
104
+        $this->form->model($this->record)->saveRelationships();
105
+
106
+        $this->getSavedNotification()?->send();
107
+    }
108
+
109
+    protected function mutateFormDataBeforeCreate(array $data): array
110
+    {
111
+        $data['company_id'] = Auth::user()->currentCompany->id;
112
+        $data['updated_by'] = Auth::id();
113
+
114
+        return $data;
115
+    }
116
+
117
+    protected function getRelatedEntities(): array
118
+    {
119
+        return [
120
+            'account_id' => [Account::class, 'id'],
121
+            'currency_code' => [Currency::class, 'code'],
122
+            'sales_tax_id' => [Tax::class, 'id', 'sales'],
123
+            'purchase_tax_id' => [Tax::class, 'id', 'purchase'],
124
+            'income_category_id' => [Category::class, 'id', 'income'],
125
+            'expense_category_id' => [Category::class, 'id', 'expense'],
126
+        ];
127
+    }
128
+
129
+    protected function getFormModel(): string
130
+    {
131
+        return Defaults::class;
132
+    }
133
+
134
+    protected function getSavedNotification():?Notification
135
+    {
136
+        $title = $this->getSavedNotificationTitle();
137
+
138
+        if (blank($title)) {
139
+            return null;
140
+        }
141
+
142
+        return Notification::make()
143
+            ->success()
144
+            ->title($title);
145
+    }
146
+
147
+    protected function getSavedNotificationTitle(): ?string
148
+    {
149
+        return __('filament::resources/pages/edit-record.messages.saved');
150
+    }
151
+
152
+    public function render(): View
153
+    {
154
+        return view('livewire.default-setting');
155
+    }
156
+}

+ 47
- 4
app/Models/Banking/Account.php 查看文件

3
 namespace App\Models\Banking;
3
 namespace App\Models\Banking;
4
 
4
 
5
 use App\Models\Setting\Currency;
5
 use App\Models\Setting\Currency;
6
+use App\Models\Setting\DefaultSetting;
6
 use Database\Factories\AccountFactory;
7
 use Database\Factories\AccountFactory;
7
 use Illuminate\Database\Eloquent\Factories\Factory;
8
 use Illuminate\Database\Eloquent\Factories\Factory;
8
 use Illuminate\Database\Eloquent\Factories\HasFactory;
9
 use Illuminate\Database\Eloquent\Factories\HasFactory;
9
 use Illuminate\Database\Eloquent\Model;
10
 use Illuminate\Database\Eloquent\Model;
10
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
11
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
12
+use Illuminate\Database\Eloquent\Relations\HasMany;
11
 use Illuminate\Support\Facades\Auth;
13
 use Illuminate\Support\Facades\Auth;
12
 use Illuminate\Support\Facades\Config;
14
 use Illuminate\Support\Facades\Config;
15
+use Spatie\Tags\HasTags;
13
 use Wallo\FilamentCompanies\FilamentCompanies;
16
 use Wallo\FilamentCompanies\FilamentCompanies;
14
 
17
 
15
 class Account extends Model
18
 class Account extends Model
16
 {
19
 {
17
     use HasFactory;
20
     use HasFactory;
21
+    use HasTags;
18
 
22
 
19
     protected $table = 'accounts';
23
     protected $table = 'accounts';
20
 
24
 
21
     protected $fillable = [
25
     protected $fillable = [
26
+        'company_id',
22
         'type',
27
         'type',
23
         'name',
28
         'name',
24
         'number',
29
         'number',
25
         'currency_code',
30
         'currency_code',
26
         'opening_balance',
31
         'opening_balance',
27
-        'enabled',
32
+        'description',
33
+        'notes',
34
+        'status',
28
         'bank_name',
35
         'bank_name',
29
         'bank_phone',
36
         'bank_phone',
30
         'bank_address',
37
         'bank_address',
31
-        'company_id',
38
+        'bank_website',
39
+        'bic_swift_code',
40
+        'iban',
41
+        'aba_routing_number',
42
+        'ach_routing_number',
43
+        'enabled',
44
+        'created_by',
45
+        'updated_by',
32
     ];
46
     ];
33
 
47
 
34
     protected $casts = [
48
     protected $casts = [
50
         return $this->belongsTo(Currency::class, 'currency_code', 'code');
64
         return $this->belongsTo(Currency::class, 'currency_code', 'code');
51
     }
65
     }
52
 
66
 
67
+    public function createdBy(): BelongsTo
68
+    {
69
+        return $this->belongsTo(FilamentCompanies::userModel(), 'created_by');
70
+    }
71
+
72
+    public function updatedBy(): BelongsTo
73
+    {
74
+        return $this->belongsTo(FilamentCompanies::userModel(), 'updated_by');
75
+    }
76
+
77
+    public function default_settings(): HasMany
78
+    {
79
+        return $this->hasMany(DefaultSetting::class, 'account_id', 'id');
80
+    }
81
+
53
     public static function getAccountTypes(): array
82
     public static function getAccountTypes(): array
54
     {
83
     {
55
         return [
84
         return [
56
-            'bank' => 'Bank',
57
-            'card' => 'Credit Card',
85
+            'checking' => 'Checking',
86
+            'savings' => 'Savings',
87
+            'money_market' => 'Money Market',
88
+            'certificate_of_deposit' => 'Certificate of Deposit',
89
+            'credit_card' => 'Credit Card',
90
+        ];
91
+    }
92
+
93
+    public static function getAccountStatuses(): array
94
+    {
95
+        return [
96
+            'open' => 'Open',
97
+            'active' => 'Active',
98
+            'dormant' => 'Dormant',
99
+            'restricted' => 'Restricted',
100
+            'closed' => 'Closed',
58
         ];
101
         ];
59
     }
102
     }
60
 
103
 

+ 51
- 4
app/Models/Contact.php 查看文件

8
 use Illuminate\Database\Eloquent\Model;
8
 use Illuminate\Database\Eloquent\Model;
9
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
9
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
10
 use Illuminate\Database\Eloquent\Relations\HasMany;
10
 use Illuminate\Database\Eloquent\Relations\HasMany;
11
+use Illuminate\Http\Request;
12
+use Squire\Models\Country;
13
+use Squire\Models\Region;
11
 use Wallo\FilamentCompanies\FilamentCompanies;
14
 use Wallo\FilamentCompanies\FilamentCompanies;
12
 
15
 
13
 class Contact extends Model
16
 class Contact extends Model
33
         'currency_code',
36
         'currency_code',
34
         'reference',
37
         'reference',
35
         'created_by',
38
         'created_by',
36
-    ];
37
-
38
-    protected $casts = [
39
-        'enabled' => 'boolean',
39
+        'updated_by',
40
     ];
40
     ];
41
 
41
 
42
     public function company(): BelongsTo
42
     public function company(): BelongsTo
49
         return $this->belongsTo(FilamentCompanies::userModel(), 'created_by');
49
         return $this->belongsTo(FilamentCompanies::userModel(), 'created_by');
50
     }
50
     }
51
 
51
 
52
+    public function updatedBy(): BelongsTo
53
+    {
54
+        return $this->belongsTo(FilamentCompanies::userModel(), 'updated_by');
55
+    }
56
+
52
     public function currency(): BelongsTo
57
     public function currency(): BelongsTo
53
     {
58
     {
54
         return $this->belongsTo(Currency::class, 'currency_code', 'code');
59
         return $this->belongsTo(Currency::class, 'currency_code', 'code');
59
         return $this->hasMany(Document::class);
64
         return $this->hasMany(Document::class);
60
     }
65
     }
61
 
66
 
67
+    public static function getCountryOptions(): array
68
+    {
69
+        $allCountries = Country::all();
70
+
71
+        // Default countries to show at the top of the options list
72
+        $defaultCountryNames = ['United States', 'Canada', 'United Kingdom', 'Australia']; // replace with actual country names
73
+
74
+        $defaultCountryOptions = [];
75
+        $countryOptions = [];
76
+
77
+        foreach ($allCountries as $country) {
78
+            if (in_array($country->name, $defaultCountryNames, true)) {
79
+                $defaultCountryOptions[$country->name] = $country->name;
80
+            } else {
81
+                $countryOptions[$country->name] = $country->name;
82
+            }
83
+        }
84
+
85
+        // Guarantee the order of default countries
86
+        $orderedDefaultCountryOptions = [];
87
+        foreach ($defaultCountryNames as $name) {
88
+            if (isset($defaultCountryOptions[$name])) {
89
+                $orderedDefaultCountryOptions[$name] = $defaultCountryOptions[$name];
90
+            }
91
+        }
92
+
93
+        return $orderedDefaultCountryOptions + $countryOptions;
94
+    }
95
+
96
+    public static function getRegionOptions(string $countryName): array
97
+    {
98
+        $country = Country::where('name', $countryName)->first();
99
+
100
+        if (!$country) {
101
+            return [];
102
+        }
103
+
104
+        return Region::where('country_id', $country->id)
105
+            ->pluck('name', 'name')
106
+            ->toArray();
107
+    }
108
+
62
     public function bills(): HasMany
109
     public function bills(): HasMany
63
     {
110
     {
64
         return $this->documents()->where('type', 'bill');
111
         return $this->documents()->where('type', 'bill');

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

40
         'contact_id',
40
         'contact_id',
41
         'notes',
41
         'notes',
42
         'created_by',
42
         'created_by',
43
+        'updated_by',
43
     ];
44
     ];
44
 
45
 
45
     protected $casts = [
46
     protected $casts = [
58
         return $this->belongsTo(FilamentCompanies::userModel(), 'created_by');
59
         return $this->belongsTo(FilamentCompanies::userModel(), 'created_by');
59
     }
60
     }
60
 
61
 
62
+    public function updatedBy(): BelongsTo
63
+    {
64
+        return $this->belongsTo(FilamentCompanies::userModel(), 'updated_by');
65
+    }
66
+
61
     public function tax(): BelongsTo
67
     public function tax(): BelongsTo
62
     {
68
     {
63
         return $this->belongsTo(Tax::class);
69
         return $this->belongsTo(Tax::class);

+ 6
- 0
app/Models/Document/DocumentItem.php 查看文件

31
         'discount_id',
31
         'discount_id',
32
         'total',
32
         'total',
33
         'created_by',
33
         'created_by',
34
+        'updated_by',
34
     ];
35
     ];
35
 
36
 
36
     protected $casts = [
37
     protected $casts = [
49
         return $this->belongsTo(FilamentCompanies::userModel(), 'created_by');
50
         return $this->belongsTo(FilamentCompanies::userModel(), 'created_by');
50
     }
51
     }
51
 
52
 
53
+    public function updatedBy(): BelongsTo
54
+    {
55
+        return $this->belongsTo(FilamentCompanies::userModel(), 'updated_by');
56
+    }
57
+
52
     public function document(): BelongsTo
58
     public function document(): BelongsTo
53
     {
59
     {
54
         return $this->belongsTo(Document::class);
60
         return $this->belongsTo(Document::class);

+ 6
- 0
app/Models/Document/DocumentTotal.php 查看文件

26
         'tax',
26
         'tax',
27
         'total',
27
         'total',
28
         'created_by',
28
         'created_by',
29
+        'updated_by',
29
     ];
30
     ];
30
 
31
 
31
     public function company(): BelongsTo
32
     public function company(): BelongsTo
38
         return $this->belongsTo(FilamentCompanies::userModel(), 'created_by');
39
         return $this->belongsTo(FilamentCompanies::userModel(), 'created_by');
39
     }
40
     }
40
 
41
 
42
+    public function updatedBy(): BelongsTo
43
+    {
44
+        return $this->belongsTo(FilamentCompanies::userModel(), 'updated_by');
45
+    }
46
+
41
     public function document(): BelongsTo
47
     public function document(): BelongsTo
42
     {
48
     {
43
         return $this->belongsTo(Document::class);
49
         return $this->belongsTo(Document::class);

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

32
         'discount_id',
32
         'discount_id',
33
         'enabled',
33
         'enabled',
34
         'created_by',
34
         'created_by',
35
+        'updated_by',
35
     ];
36
     ];
36
 
37
 
37
     protected $casts = [
38
     protected $casts = [
48
         return $this->belongsTo(FilamentCompanies::userModel(), 'created_by');
49
         return $this->belongsTo(FilamentCompanies::userModel(), 'created_by');
49
     }
50
     }
50
 
51
 
52
+    public function updatedBy(): BelongsTo
53
+    {
54
+        return $this->belongsTo(FilamentCompanies::userModel(), 'updated_by');
55
+    }
56
+
51
     public function category(): BelongsTo
57
     public function category(): BelongsTo
52
     {
58
     {
53
         return $this->belongsTo(Category::class, 'category_id')->withDefault([
59
         return $this->belongsTo(Category::class, 'category_id')->withDefault([

+ 16
- 0
app/Models/Setting/Category.php 查看文件

25
         'color',
25
         'color',
26
         'enabled',
26
         'enabled',
27
         'created_by',
27
         'created_by',
28
+        'updated_by',
28
     ];
29
     ];
29
 
30
 
30
     protected $casts = [
31
     protected $casts = [
31
         'enabled' => 'boolean',
32
         'enabled' => 'boolean',
32
     ];
33
     ];
33
 
34
 
35
+    public static function getCategoryTypes(): array
36
+    {
37
+        return [
38
+            'expense' => 'Expense',
39
+            'income' => 'Income',
40
+            'item' => 'Item',
41
+            'other' => 'Other',
42
+        ];
43
+    }
44
+
34
     public function company(): BelongsTo
45
     public function company(): BelongsTo
35
     {
46
     {
36
         return $this->belongsTo(FilamentCompanies::companyModel(), 'company_id');
47
         return $this->belongsTo(FilamentCompanies::companyModel(), 'company_id');
41
         return $this->belongsTo(FilamentCompanies::userModel(), 'created_by');
52
         return $this->belongsTo(FilamentCompanies::userModel(), 'created_by');
42
     }
53
     }
43
 
54
 
55
+    public function updatedBy(): BelongsTo
56
+    {
57
+        return $this->belongsTo(FilamentCompanies::userModel(), 'updated_by');
58
+    }
59
+
44
     public function items(): HasMany
60
     public function items(): HasMany
45
     {
61
     {
46
         return $this->hasMany(Item::class);
62
         return $this->hasMany(Item::class);

+ 2
- 0
app/Models/Setting/Currency.php 查看文件

30
         'thousands_separator',
30
         'thousands_separator',
31
         'enabled',
31
         'enabled',
32
         'company_id',
32
         'company_id',
33
+        'created_by',
34
+        'updated_by',
33
     ];
35
     ];
34
 
36
 
35
     protected $casts = [
37
     protected $casts = [

+ 172
- 0
app/Models/Setting/DefaultSetting.php 查看文件

1
+<?php
2
+
3
+namespace App\Models\Setting;
4
+
5
+use App\Models\Banking\Account;
6
+use Illuminate\Database\Eloquent\Factories\HasFactory;
7
+use Illuminate\Database\Eloquent\Model;
8
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
9
+use Illuminate\Support\Facades\Auth;
10
+use Wallo\FilamentCompanies\FilamentCompanies;
11
+
12
+class DefaultSetting extends Model
13
+{
14
+    use HasFactory;
15
+
16
+    protected $table = 'default_settings';
17
+
18
+    protected $fillable = [
19
+        'company_id',
20
+        'account_id',
21
+        'currency_code',
22
+        'sales_tax_id',
23
+        'purchase_tax_id',
24
+        'income_category_id',
25
+        'expense_category_id',
26
+        'updated_by',
27
+    ];
28
+
29
+    public function company(): BelongsTo
30
+    {
31
+        return $this->belongsTo(FilamentCompanies::companyModel(), 'company_id');
32
+    }
33
+
34
+    public function account(): BelongsTo
35
+    {
36
+        return $this->belongsTo(Account::class, 'account_id');
37
+    }
38
+
39
+    public function currency(): BelongsTo
40
+    {
41
+        return $this->belongsTo(Currency::class, 'currency_code', 'code');
42
+    }
43
+
44
+    public function salesTax(): BelongsTo
45
+    {
46
+        return $this->belongsTo(Tax::class,'sales_tax_id', 'id');
47
+    }
48
+
49
+    public function purchaseTax(): BelongsTo
50
+    {
51
+        return $this->belongsTo(Tax::class,'purchase_tax_id', 'id');
52
+    }
53
+
54
+    public function incomeCategory(): BelongsTo
55
+    {
56
+        return $this->belongsTo(Category::class,'income_category_id', 'id');
57
+    }
58
+
59
+    public function expenseCategory(): BelongsTo
60
+    {
61
+        return $this->belongsTo(Category::class,'expense_category_id', 'id');
62
+    }
63
+
64
+    public function updatedBy(): BelongsTo
65
+    {
66
+        return $this->belongsTo(FilamentCompanies::userModel(), 'updated_by');
67
+    }
68
+
69
+    public static function getAccounts(): array
70
+    {
71
+        return Account::where('company_id', Auth::user()->currentCompany->id)
72
+            ->pluck('name', 'id')
73
+            ->toArray();
74
+    }
75
+
76
+    public static function getCurrencies(): array
77
+    {
78
+        return Currency::where('company_id', Auth::user()->currentCompany->id)
79
+            ->pluck('name', 'code')
80
+            ->toArray();
81
+    }
82
+
83
+    public static function getSalesTaxes(): array
84
+    {
85
+        return Tax::where('company_id', Auth::user()->currentCompany->id)
86
+            ->where('type', 'sales')
87
+            ->pluck('name', 'id')
88
+            ->toArray();
89
+    }
90
+
91
+    public static function getPurchaseTaxes(): array
92
+    {
93
+        return Tax::where('company_id', Auth::user()->currentCompany->id)
94
+            ->where('type', 'purchase')
95
+            ->pluck('name', 'id')
96
+            ->toArray();
97
+    }
98
+
99
+    public static function getIncomeCategories(): array
100
+    {
101
+        return Category::where('company_id', Auth::user()->currentCompany->id)
102
+            ->where('type', 'income')
103
+            ->pluck('name', 'id')
104
+            ->toArray();
105
+    }
106
+
107
+    public static function getExpenseCategories(): array
108
+    {
109
+        return Category::where('company_id', Auth::user()->currentCompany->id)
110
+            ->where('type', 'expense')
111
+            ->pluck('name', 'id')
112
+            ->toArray();
113
+    }
114
+
115
+    public static function getDefaultAccount()
116
+    {
117
+        $defaultAccount = Account::where('enabled', true)
118
+            ->where('company_id', Auth::user()->currentCompany->id)
119
+            ->first();
120
+
121
+        return $defaultAccount->id ?? null;
122
+    }
123
+
124
+    public static function getDefaultCurrency()
125
+    {
126
+        $defaultCurrency = Currency::where('enabled', true)
127
+            ->where('company_id', Auth::user()->currentCompany->id)
128
+            ->first();
129
+
130
+        return $defaultCurrency->code ?? null;
131
+    }
132
+
133
+    public static function getDefaultSalesTax()
134
+    {
135
+        $defaultSalesTax = Tax::where('enabled', true)
136
+            ->where('company_id', Auth::user()->currentCompany->id)
137
+            ->where('type', 'sales')
138
+            ->first();
139
+
140
+        return $defaultSalesTax->id ?? null;
141
+    }
142
+
143
+    public static function getDefaultPurchaseTax()
144
+    {
145
+        $defaultPurchaseTax = Tax::where('enabled', true)
146
+            ->where('company_id', Auth::user()->currentCompany->id)
147
+            ->where('type', 'purchase')
148
+            ->first();
149
+
150
+        return $defaultPurchaseTax->id ?? null;
151
+    }
152
+
153
+    public static function getDefaultIncomeCategory()
154
+    {
155
+        $defaultIncomeCategory = Category::where('enabled', true)
156
+            ->where('company_id', Auth::user()->currentCompany->id)
157
+            ->where('type', 'income')
158
+            ->first();
159
+
160
+        return $defaultIncomeCategory->id ?? null;
161
+    }
162
+
163
+    public static function getDefaultExpenseCategory()
164
+    {
165
+        $defaultExpenseCategory = Category::where('enabled', true)
166
+            ->where('company_id', Auth::user()->currentCompany->id)
167
+            ->where('type', 'expense')
168
+            ->first();
169
+
170
+        return $defaultExpenseCategory->id ?? null;
171
+    }
172
+}

+ 37
- 1
app/Models/Setting/Discount.php 查看文件

11
 use Illuminate\Database\Eloquent\Model;
11
 use Illuminate\Database\Eloquent\Model;
12
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
12
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
13
 use Illuminate\Database\Eloquent\Relations\HasMany;
13
 use Illuminate\Database\Eloquent\Relations\HasMany;
14
+use Wallo\FilamentCompanies\FilamentCompanies;
14
 
15
 
15
 class Discount extends Model
16
 class Discount extends Model
16
 {
17
 {
25
         'computation',
26
         'computation',
26
         'type',
27
         'type',
27
         'scope',
28
         'scope',
29
+        'start_date',
30
+        'end_date',
28
         'enabled',
31
         'enabled',
29
         'created_by',
32
         'created_by',
33
+        'updated_by',
30
     ];
34
     ];
31
 
35
 
32
     protected $casts = [
36
     protected $casts = [
33
         'enabled' => 'boolean',
37
         'enabled' => 'boolean',
38
+        'start_date' => 'datetime',
39
+        'end_date' => 'datetime',
34
     ];
40
     ];
35
 
41
 
36
     public function createdBy(): BelongsTo
42
     public function createdBy(): BelongsTo
37
     {
43
     {
38
-        return $this->belongsTo(User::class, 'created_by');
44
+        return $this->belongsTo(FilamentCompanies::userModel(), 'created_by');
45
+    }
46
+
47
+    public function updatedBy(): BelongsTo
48
+    {
49
+        return $this->belongsTo(FilamentCompanies::userModel(), 'updated_by');
39
     }
50
     }
40
 
51
 
41
     public function items(): HasMany
52
     public function items(): HasMany
58
         return $this->document_items()->where('type', 'invoice');
69
         return $this->document_items()->where('type', 'invoice');
59
     }
70
     }
60
 
71
 
72
+    public static function getComputationTypes(): array
73
+    {
74
+        return [
75
+            'percentage' => 'Percentage',
76
+            'fixed' => 'Fixed',
77
+        ];
78
+    }
79
+
80
+    public static function getDiscountTypes(): array
81
+    {
82
+        return [
83
+            'sales' => 'Sales',
84
+            'purchase' => 'Purchase',
85
+            'none' => 'None',
86
+        ];
87
+    }
88
+
89
+    public static function getDiscountScopes(): array
90
+    {
91
+        return [
92
+            'product' => 'Product',
93
+            'service' => 'Service',
94
+        ];
95
+    }
96
+
61
     protected static function newFactory(): Factory
97
     protected static function newFactory(): Factory
62
     {
98
     {
63
         return DiscountFactory::new();
99
         return DiscountFactory::new();

+ 34
- 1
app/Models/Setting/Tax.php 查看文件

12
 use Illuminate\Database\Eloquent\Model;
12
 use Illuminate\Database\Eloquent\Model;
13
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
13
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
14
 use Illuminate\Database\Eloquent\Relations\HasMany;
14
 use Illuminate\Database\Eloquent\Relations\HasMany;
15
+use Wallo\FilamentCompanies\FilamentCompanies;
15
 
16
 
16
 class Tax extends Model
17
 class Tax extends Model
17
 {
18
 {
29
         'scope',
30
         'scope',
30
         'enabled',
31
         'enabled',
31
         'created_by',
32
         'created_by',
33
+        'updated_by',
32
     ];
34
     ];
33
 
35
 
34
     protected $casts = [
36
     protected $casts = [
42
 
44
 
43
     public function createdBy(): BelongsTo
45
     public function createdBy(): BelongsTo
44
     {
46
     {
45
-        return $this->belongsTo(User::class, 'created_by');
47
+        return $this->belongsTo(FilamentCompanies::userModel(), 'created_by');
48
+    }
49
+
50
+    public function updatedBy(): BelongsTo
51
+    {
52
+        return $this->belongsTo(FilamentCompanies::userModel(), 'updated_by');
46
     }
53
     }
47
 
54
 
48
     public function items(): HasMany
55
     public function items(): HasMany
65
         return $this->document_items()->where('type', 'invoice');
72
         return $this->document_items()->where('type', 'invoice');
66
     }
73
     }
67
 
74
 
75
+    public static function getComputationTypes(): array
76
+    {
77
+        return [
78
+            'fixed' => 'Fixed',
79
+            'percentage' => 'Percentage',
80
+            'compound' => 'Compound',
81
+        ];
82
+    }
83
+
84
+    public static function getTaxTypes(): array
85
+    {
86
+        return [
87
+            'sales' => 'Sales',
88
+            'purchase' => 'Purchase',
89
+            'none' => 'None',
90
+        ];
91
+    }
92
+
93
+    public static function getTaxScopes(): array
94
+    {
95
+        return [
96
+            'product' => 'Product',
97
+            'service' => 'Service',
98
+        ];
99
+    }
100
+
68
     protected static function newFactory(): Factory
101
     protected static function newFactory(): Factory
69
     {
102
     {
70
         return TaxFactory::new();
103
         return TaxFactory::new();

+ 67
- 0
app/Observers/AccountObserver.php 查看文件

1
+<?php
2
+
3
+namespace App\Observers;
4
+
5
+use App\Models\Banking\Account;
6
+use Illuminate\Support\Facades\Auth;
7
+
8
+class AccountObserver
9
+{
10
+    /**
11
+     * Handle the account "creating" event.
12
+     */
13
+    public function creating(Account $account): void
14
+    {
15
+        $account->company()->associate(Auth::user()->currentCompany->id);
16
+        $account->company_id = Auth::user()->currentCompany->id;
17
+        $account->created_by = Auth::id();
18
+    }
19
+
20
+    /**
21
+     * Handle the Account "created" event.
22
+     */
23
+    public function created(Account $account): void
24
+    {
25
+        //
26
+    }
27
+
28
+    /**
29
+     * Handle the Account "updating" event.
30
+     */
31
+    public function updating(Account $account): void
32
+    {
33
+        $account->updated_by = Auth::id();
34
+    }
35
+
36
+    /**
37
+     * Handle the Account "updated" event.
38
+     */
39
+    public function updated(Account $account): void
40
+    {
41
+        //
42
+    }
43
+
44
+    /**
45
+     * Handle the Account "deleted" event.
46
+     */
47
+    public function deleted(Account $account): void
48
+    {
49
+        //
50
+    }
51
+
52
+    /**
53
+     * Handle the Account "restored" event.
54
+     */
55
+    public function restored(Account $account): void
56
+    {
57
+        //
58
+    }
59
+
60
+    /**
61
+     * Handle the Account "force deleted" event.
62
+     */
63
+    public function forceDeleted(Account $account): void
64
+    {
65
+        //
66
+    }
67
+}

+ 13
- 0
app/Providers/AppServiceProvider.php 查看文件

3
 namespace App\Providers;
3
 namespace App\Providers;
4
 
4
 
5
 use Filament\Facades\Filament;
5
 use Filament\Facades\Filament;
6
+use Filament\Forms\Components\Field;
7
+use Filament\Forms\Components\Actions\Action;
8
+use Illuminate\Support\HtmlString;
6
 use Illuminate\Support\ServiceProvider;
9
 use Illuminate\Support\ServiceProvider;
7
 
10
 
8
 class AppServiceProvider extends ServiceProvider
11
 class AppServiceProvider extends ServiceProvider
23
         Filament::serving(static function () {
26
         Filament::serving(static function () {
24
             Filament::registerViteTheme('resources/css/filament.css');
27
             Filament::registerViteTheme('resources/css/filament.css');
25
         });
28
         });
29
+
30
+        Field::macro('tooltip', function (string $tooltip) {
31
+            return $this->label(
32
+                Action::make('info')
33
+                    ->label('')
34
+                    ->icon('heroicon-o-information-circle')
35
+                    ->extraAttributes(['class' => 'text-gray-500'])
36
+                    ->tooltip($tooltip),
37
+            );
38
+        });
26
     }
39
     }
27
 }
40
 }

+ 11
- 0
app/Providers/EventServiceProvider.php 查看文件

2
 
2
 
3
 namespace App\Providers;
3
 namespace App\Providers;
4
 
4
 
5
+use App\Models\Banking\Account;
6
+use App\Observers\AccountObserver;
5
 use Illuminate\Auth\Events\Registered;
7
 use Illuminate\Auth\Events\Registered;
6
 use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
8
 use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
7
 use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
9
 use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
35
     {
37
     {
36
         return false;
38
         return false;
37
     }
39
     }
40
+
41
+    /**
42
+     * The model observers for the application.
43
+     *
44
+     * @var array
45
+     */
46
+    protected $observers = [
47
+        Account::class => [AccountObserver::class],
48
+    ];
38
 }
49
 }

+ 27
- 0
app/Providers/SquireServiceProvider.php 查看文件

1
+<?php
2
+
3
+namespace App\Providers;
4
+
5
+use App\Models\Contact;
6
+use Squire\Repository;
7
+use Illuminate\Support\ServiceProvider;
8
+
9
+class SquireServiceProvider extends ServiceProvider
10
+{
11
+    /**
12
+     * Register any application services.
13
+     */
14
+    public function register(): void
15
+    {
16
+        //
17
+    }
18
+
19
+    /**
20
+     * Bootstrap any application services.
21
+     */
22
+    public function boot(): void
23
+    {
24
+        Repository::registerSource(Contact::class, 'en', base_path('vendor/squirephp/regions-en/resources/data.csv'));
25
+        Repository::registerSource(Contact::class, 'en', base_path('vendor/squirephp/countries-en/resources/data.csv'));
26
+    }
27
+}

+ 60
- 0
app/Traits/HandlesRecordCreation.php 查看文件

1
+<?php
2
+
3
+namespace App\Traits;
4
+
5
+use Illuminate\Database\Eloquent\Model;
6
+use Illuminate\Support\Facades\Auth;
7
+
8
+trait HandlesRecordCreation
9
+{
10
+    abstract protected function getRelatedEntities(): array;
11
+    abstract protected function getFormModel(): string;
12
+
13
+    protected function handleRecordCreation(array $data): Model
14
+    {
15
+        $relatedEntities = $this->getRelatedEntities();
16
+
17
+        $model = $this->getFormModel();
18
+
19
+        $existingRecord = $model::where('company_id', Auth::user()->currentCompany->id)
20
+            ->latest()
21
+            ->first();
22
+
23
+        $newData = [
24
+            'company_id' => Auth::user()->currentCompany->id,
25
+            'updated_by' => Auth::id(),
26
+        ];
27
+
28
+        foreach ($relatedEntities as $field => $params) {
29
+            [$class, $key, $type] = array_pad($params, 3, null);
30
+
31
+            if (isset($data[$field]) && $data[$field] !== $existingRecord->{$field}) {
32
+                $this->updateEnabledRecord($class, $key, $existingRecord->{$field}, $type, false);
33
+                $this->updateEnabledRecord($class, $key, $data[$field], $type, true);
34
+
35
+                $newData[$field] = $data[$field];
36
+            } else {
37
+                $newData[$field] = $existingRecord->{$field};
38
+            }
39
+        }
40
+
41
+        return $model::create($newData);
42
+    }
43
+
44
+    protected function updateEnabledRecord($class, $key, $value, $type = null, $enabled = true): void
45
+    {
46
+        $query = $class::where('company_id', Auth::user()->currentCompany->id)
47
+            ->where('enabled', !$enabled);
48
+
49
+        if ($type !== null) {
50
+            $query = $query->where('type', $type);
51
+        }
52
+
53
+        $query->where($key, $value)
54
+            ->update([
55
+                'enabled' => $enabled,
56
+                'updated_by' => Auth::id(),
57
+            ]);
58
+    }
59
+}
60
+

+ 6
- 1
composer.json 查看文件

7
     "require": {
7
     "require": {
8
         "php": "^8.1",
8
         "php": "^8.1",
9
         "andrewdwallo/filament-companies": "^2.0",
9
         "andrewdwallo/filament-companies": "^2.0",
10
+        "andrewdwallo/filament-selectify": "^1.0",
10
         "filament/filament": "^2.17",
11
         "filament/filament": "^2.17",
12
+        "filament/spatie-laravel-tags-plugin": "^2.17",
11
         "flowframe/laravel-trend": "^0.1.5",
13
         "flowframe/laravel-trend": "^0.1.5",
12
         "guzzlehttp/guzzle": "^7.2",
14
         "guzzlehttp/guzzle": "^7.2",
13
         "laravel/framework": "^10.8",
15
         "laravel/framework": "^10.8",
14
         "laravel/sanctum": "^3.2",
16
         "laravel/sanctum": "^3.2",
15
         "laravel/tinker": "^2.8",
17
         "laravel/tinker": "^2.8",
16
-        "leandrocfe/filament-apex-charts": "^1.0"
18
+        "leandrocfe/filament-apex-charts": "^1.0",
19
+        "squirephp/countries-en": "^3.4",
20
+        "squirephp/regions-en": "^3.4",
21
+        "squirephp/repository": "^3.4"
17
     },
22
     },
18
     "require-dev": {
23
     "require-dev": {
19
         "doctrine/dbal": "^3.6",
24
         "doctrine/dbal": "^3.6",

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


+ 1
- 0
config/app.php 查看文件

170
         App\Providers\RouteServiceProvider::class,
170
         App\Providers\RouteServiceProvider::class,
171
         App\Providers\FortifyServiceProvider::class,
171
         App\Providers\FortifyServiceProvider::class,
172
         App\Providers\FilamentCompaniesServiceProvider::class,
172
         App\Providers\FilamentCompaniesServiceProvider::class,
173
+        App\Providers\SquireServiceProvider::class,
173
     ])->toArray(),
174
     ])->toArray(),
174
 
175
 
175
     /*
176
     /*

+ 1
- 1
config/forms.php 查看文件

63
     |
63
     |
64
     */
64
     */
65
 
65
 
66
-    'dark_mode' => false,
66
+    'dark_mode' => true,
67
 
67
 
68
 ];
68
 ];

+ 158
- 0
config/livewire.php 查看文件

1
+<?php
2
+
3
+return [
4
+
5
+    /*
6
+    |--------------------------------------------------------------------------
7
+    | Class Namespace
8
+    |--------------------------------------------------------------------------
9
+    |
10
+    | This value sets the root namespace for Livewire component classes in
11
+    | your application. This value affects component auto-discovery and
12
+    | any Livewire file helper commands, like `artisan make:livewire`.
13
+    |
14
+    | After changing this item, run: `php artisan livewire:discover`.
15
+    |
16
+    */
17
+
18
+    'class_namespace' => 'App\\Http\\Livewire',
19
+
20
+    /*
21
+    |--------------------------------------------------------------------------
22
+    | View Path
23
+    |--------------------------------------------------------------------------
24
+    |
25
+    | This value sets the path for Livewire component views. This affects
26
+    | file manipulation helper commands like `artisan make:livewire`.
27
+    |
28
+    */
29
+
30
+    'view_path' => resource_path('views/livewire'),
31
+
32
+    /*
33
+    |--------------------------------------------------------------------------
34
+    | Layout
35
+    |--------------------------------------------------------------------------
36
+    | The default layout view that will be used when rendering a component via
37
+    | Route::get('/some-endpoint', SomeComponent::class);. In this case the
38
+    | the view returned by SomeComponent will be wrapped in "layouts.app"
39
+    |
40
+    */
41
+
42
+    'layout' => 'layouts.app',
43
+
44
+    /*
45
+    |--------------------------------------------------------------------------
46
+    | Livewire Assets URL
47
+    |--------------------------------------------------------------------------
48
+    |
49
+    | This value sets the path to Livewire JavaScript assets, for cases where
50
+    | your app's domain root is not the correct path. By default, Livewire
51
+    | will load its JavaScript assets from the app's "relative root".
52
+    |
53
+    | Examples: "/assets", "myurl.com/app".
54
+    |
55
+    */
56
+
57
+    'asset_url' => null,
58
+
59
+    /*
60
+    |--------------------------------------------------------------------------
61
+    | Livewire App URL
62
+    |--------------------------------------------------------------------------
63
+    |
64
+    | This value should be used if livewire assets are served from CDN.
65
+    | Livewire will communicate with an app through this url.
66
+    |
67
+    | Examples: "https://my-app.com", "myurl.com/app".
68
+    |
69
+    */
70
+
71
+    'app_url' => null,
72
+
73
+    /*
74
+    |--------------------------------------------------------------------------
75
+    | Livewire Endpoint Middleware Group
76
+    |--------------------------------------------------------------------------
77
+    |
78
+    | This value sets the middleware group that will be applied to the main
79
+    | Livewire "message" endpoint (the endpoint that gets hit everytime
80
+    | a Livewire component updates). It is set to "web" by default.
81
+    |
82
+    */
83
+
84
+    'middleware_group' => 'web',
85
+
86
+    /*
87
+    |--------------------------------------------------------------------------
88
+    | Livewire Temporary File Uploads Endpoint Configuration
89
+    |--------------------------------------------------------------------------
90
+    |
91
+    | Livewire handles file uploads by storing uploads in a temporary directory
92
+    | before the file is validated and stored permanently. All file uploads
93
+    | are directed to a global endpoint for temporary storage. The config
94
+    | items below are used for customizing the way the endpoint works.
95
+    |
96
+    */
97
+
98
+    'temporary_file_upload' => [
99
+        'disk' => null,        // Example: 'local', 's3'              Default: 'default'
100
+        'rules' => null,       // Example: ['file', 'mimes:png,jpg']  Default: ['required', 'file', 'max:12288'] (12MB)
101
+        'directory' => null,   // Example: 'tmp'                      Default  'livewire-tmp'
102
+        'middleware' => null,  // Example: 'throttle:5,1'             Default: 'throttle:60,1'
103
+        'preview_mimes' => [   // Supported file types for temporary pre-signed file URLs.
104
+            'png', 'gif', 'bmp', 'svg', 'wav', 'mp4',
105
+            'mov', 'avi', 'wmv', 'mp3', 'm4a',
106
+            'jpg', 'jpeg', 'mpga', 'webp', 'wma',
107
+        ],
108
+        'max_upload_time' => 5, // Max duration (in minutes) before an upload gets invalidated.
109
+    ],
110
+
111
+    /*
112
+    |--------------------------------------------------------------------------
113
+    | Manifest File Path
114
+    |--------------------------------------------------------------------------
115
+    |
116
+    | This value sets the path to the Livewire manifest file.
117
+    | The default should work for most cases (which is
118
+    | "<app_root>/bootstrap/cache/livewire-components.php"), but for specific
119
+    | cases like when hosting on Laravel Vapor, it could be set to a different value.
120
+    |
121
+    | Example: for Laravel Vapor, it would be "/tmp/storage/bootstrap/cache/livewire-components.php".
122
+    |
123
+    */
124
+
125
+    'manifest_path' => null,
126
+
127
+    /*
128
+    |--------------------------------------------------------------------------
129
+    | Back Button Cache
130
+    |--------------------------------------------------------------------------
131
+    |
132
+    | This value determines whether the back button cache will be used on pages
133
+    | that contain Livewire. By disabling back button cache, it ensures that
134
+    | the back button shows the correct state of components, instead of
135
+    | potentially stale, cached data.
136
+    |
137
+    | Setting it to "false" (default) will disable back button cache.
138
+    |
139
+    */
140
+
141
+    'back_button_cache' => false,
142
+
143
+    /*
144
+    |--------------------------------------------------------------------------
145
+    | Render On Redirect
146
+    |--------------------------------------------------------------------------
147
+    |
148
+    | This value determines whether Livewire will render before it's redirected
149
+    | or not. Setting it to "false" (default) will mean the render method is
150
+    | skipped when redirecting. And "true" will mean the render method is
151
+    | run before redirecting. Browsers bfcache can store a potentially
152
+    | stale view if render is skipped on redirect.
153
+    |
154
+    */
155
+
156
+    'render_on_redirect' => false,
157
+
158
+];

+ 23
- 0
config/tags.php 查看文件

1
+<?php
2
+
3
+return [
4
+
5
+    /*
6
+     * The given function generates a URL friendly "slug" from the tag name property before saving it.
7
+     * Defaults to Str::slug (https://laravel.com/docs/master/helpers#method-str-slug)
8
+     */
9
+    'slugger' => null,
10
+
11
+    /*
12
+     * The fully qualified class name of the tag model.
13
+     */
14
+    'tag_model' => Spatie\Tags\Tag::class,
15
+
16
+    /*
17
+     * The name of the table associated with the taggable morph relation.
18
+     */
19
+    'taggable' => [
20
+        'table_name' => 'taggables',
21
+        'morph_name' => 'taggable',
22
+    ]
23
+];

+ 29
- 1
database/factories/CategoryFactory.php 查看文件

25
     public function definition(): array
25
     public function definition(): array
26
     {
26
     {
27
         return [
27
         return [
28
-            //
28
+            'color' => $this->faker->hexColor,
29
         ];
29
         ];
30
     }
30
     }
31
+
32
+    /**
33
+     * Indicate that the category is of income type.
34
+     *
35
+     * @return Factory<Category>
36
+     */
37
+    public function income(): Factory
38
+    {
39
+        return $this->state(function (array $attributes) {
40
+            return [
41
+                'type' => 'income',
42
+            ];
43
+        });
44
+    }
45
+
46
+    /**
47
+     * Indicate that the category is of expense type.
48
+     *
49
+     * @return Factory<Category>
50
+     */
51
+    public function expense(): Factory
52
+    {
53
+        return $this->state(function (array $attributes) {
54
+            return [
55
+                'type' => 'expense',
56
+            ];
57
+        });
58
+    }
31
 }
59
 }

+ 23
- 0
database/factories/DefaultSettingFactory.php 查看文件

1
+<?php
2
+
3
+namespace Database\Factories;
4
+
5
+use Illuminate\Database\Eloquent\Factories\Factory;
6
+
7
+/**
8
+ * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\DefaultSetting>
9
+ */
10
+class DefaultSettingFactory extends Factory
11
+{
12
+    /**
13
+     * Define the model's default state.
14
+     *
15
+     * @return array<string, mixed>
16
+     */
17
+    public function definition(): array
18
+    {
19
+        return [
20
+            //
21
+        ];
22
+    }
23
+}

+ 3
- 1
database/factories/TaxFactory.php 查看文件

25
     public function definition(): array
25
     public function definition(): array
26
     {
26
     {
27
         return [
27
         return [
28
-            //
28
+            'rate' => $this->faker->randomFloat(4, 0, 20),
29
+            'computation' => $this->faker->randomElement(Tax::getComputationTypes()),
30
+            'scope' => $this->faker->randomElement(Tax::getTaxScopes()),
29
         ];
31
         ];
30
     }
32
     }
31
 }
33
 }

+ 1
- 0
database/migrations/2023_05_10_040940_create_currencies_table.php 查看文件

24
             $table->string('thousands_separator')->default(',');
24
             $table->string('thousands_separator')->default(',');
25
             $table->boolean('enabled')->default(true);
25
             $table->boolean('enabled')->default(true);
26
             $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
26
             $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
27
+            $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
27
             $table->timestamps();
28
             $table->timestamps();
28
 
29
 
29
             $table->unique(['company_id', 'code']);
30
             $table->unique(['company_id', 'code']);

+ 11
- 2
database/migrations/2023_05_11_044321_create_accounts_table.php 查看文件

14
         Schema::create('accounts', function (Blueprint $table) {
14
         Schema::create('accounts', function (Blueprint $table) {
15
             $table->id();
15
             $table->id();
16
             $table->foreignId('company_id')->constrained()->cascadeOnDelete();
16
             $table->foreignId('company_id')->constrained()->cascadeOnDelete();
17
-            $table->string('type')->default('bank');
18
-            $table->string('name', 100);
17
+            $table->string('type')->default('checking');
18
+            $table->string('name', 100)->index();
19
             $table->string('number', 20);
19
             $table->string('number', 20);
20
             $table->string('currency_code')->default('USD');
20
             $table->string('currency_code')->default('USD');
21
             $table->decimal('opening_balance', 15, 4)->default(0.0000);
21
             $table->decimal('opening_balance', 15, 4)->default(0.0000);
22
+            $table->string('description')->nullable();
23
+            $table->text('notes')->nullable();
24
+            $table->string('status')->default('open');
22
             $table->string('bank_name', 100)->nullable();
25
             $table->string('bank_name', 100)->nullable();
23
             $table->string('bank_phone', 20)->nullable();
26
             $table->string('bank_phone', 20)->nullable();
24
             $table->text('bank_address')->nullable();
27
             $table->text('bank_address')->nullable();
28
+            $table->string('bank_website', 255)->nullable();
29
+            $table->string('bic_swift_code', 11)->nullable();
30
+            $table->string('iban', 34)->nullable();
31
+            $table->string('aba_routing_number', 9)->nullable();
32
+            $table->string('ach_routing_number', 9)->nullable();
25
             $table->boolean('enabled')->default(true);
33
             $table->boolean('enabled')->default(true);
26
             $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
34
             $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
35
+            $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
27
             $table->timestamps();
36
             $table->timestamps();
28
 
37
 
29
             $table->unique(['company_id', 'number']);
38
             $table->unique(['company_id', 'number']);

+ 2
- 1
database/migrations/2023_05_12_042255_create_categories_table.php 查看文件

14
         Schema::create('categories', function (Blueprint $table) {
14
         Schema::create('categories', function (Blueprint $table) {
15
             $table->id();
15
             $table->id();
16
             $table->foreignId('company_id')->constrained()->cascadeOnDelete();
16
             $table->foreignId('company_id')->constrained()->cascadeOnDelete();
17
-            $table->string('name');
17
+            $table->string('name')->index();
18
             $table->string('type'); // expense, income, item, other
18
             $table->string('type'); // expense, income, item, other
19
             $table->string('color');
19
             $table->string('color');
20
             $table->boolean('enabled')->default(true);
20
             $table->boolean('enabled')->default(true);
21
             $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
21
             $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
22
+            $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
22
             $table->timestamps();
23
             $table->timestamps();
23
         });
24
         });
24
     }
25
     }

+ 1
- 0
database/migrations/2023_05_19_042232_create_contacts_table.php 查看文件

29
             $table->string('currency_code')->default('USD');
29
             $table->string('currency_code')->default('USD');
30
             $table->string('reference')->nullable();
30
             $table->string('reference')->nullable();
31
             $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
31
             $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
32
+            $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
32
             $table->timestamps();
33
             $table->timestamps();
33
 
34
 
34
             $table->index(['company_id', 'type']);
35
             $table->index(['company_id', 'type']);

+ 1
- 0
database/migrations/2023_05_20_080131_create_taxes_table.php 查看文件

22
             $table->string('scope')->nullable(); // product, service, none
22
             $table->string('scope')->nullable(); // product, service, none
23
             $table->boolean('enabled')->default(true);
23
             $table->boolean('enabled')->default(true);
24
             $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
24
             $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
25
+            $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
25
             $table->timestamps();
26
             $table->timestamps();
26
         });
27
         });
27
     }
28
     }

+ 4
- 0
database/migrations/2023_05_21_163808_create_discounts_table.php 查看文件

13
     {
13
     {
14
         Schema::create('discounts', function (Blueprint $table) {
14
         Schema::create('discounts', function (Blueprint $table) {
15
             $table->id();
15
             $table->id();
16
+            $table->foreignId('company_id')->constrained()->cascadeOnDelete();
16
             $table->string('name');
17
             $table->string('name');
17
             $table->text('description')->nullable();
18
             $table->text('description')->nullable();
18
             $table->decimal('rate', 15, 4);
19
             $table->decimal('rate', 15, 4);
19
             $table->string('computation')->default('percentage'); // percentage, fixed
20
             $table->string('computation')->default('percentage'); // percentage, fixed
20
             $table->string('type')->default('sales'); // sales, purchases
21
             $table->string('type')->default('sales'); // sales, purchases
21
             $table->string('scope')->nullable(); // product, service, none
22
             $table->string('scope')->nullable(); // product, service, none
23
+            $table->dateTime('start_date')->nullable();
24
+            $table->dateTime('end_date')->nullable();
22
             $table->boolean('enabled')->default(true);
25
             $table->boolean('enabled')->default(true);
23
             $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
26
             $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
27
+            $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
24
             $table->timestamps();
28
             $table->timestamps();
25
         });
29
         });
26
     }
30
     }

+ 1
- 0
database/migrations/2023_05_22_073252_create_items_table.php 查看文件

26
             $table->foreignId('discount_id')->nullable()->constrained()->nullOnDelete();
26
             $table->foreignId('discount_id')->nullable()->constrained()->nullOnDelete();
27
             $table->boolean('enabled')->default(true);
27
             $table->boolean('enabled')->default(true);
28
             $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
28
             $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
29
+            $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
29
             $table->timestamps();
30
             $table->timestamps();
30
         });
31
         });
31
     }
32
     }

+ 2
- 1
database/migrations/2023_05_23_141215_create_documents_table.php 查看文件

17
             $table->string('type'); // invoice, bill
17
             $table->string('type'); // invoice, bill
18
             $table->string('document_number');
18
             $table->string('document_number');
19
             $table->string('order_number')->nullable();
19
             $table->string('order_number')->nullable();
20
-            $table->string('status');
20
+            $table->string('status'); // draft, sent, paid, cancelled, approved
21
             $table->dateTime('document_date');
21
             $table->dateTime('document_date');
22
             $table->dateTime('due_date');
22
             $table->dateTime('due_date');
23
             $table->dateTime('paid_date')->nullable();
23
             $table->dateTime('paid_date')->nullable();
30
             $table->foreignId('contact_id')->nullable()->constrained()->nullOnDelete();
30
             $table->foreignId('contact_id')->nullable()->constrained()->nullOnDelete();
31
             $table->text('notes')->nullable();
31
             $table->text('notes')->nullable();
32
             $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
32
             $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
33
+            $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
33
             $table->timestamps();
34
             $table->timestamps();
34
 
35
 
35
             $table->foreign('currency_code')->references('code')->on('currencies')->restrictOnDelete();
36
             $table->foreign('currency_code')->references('code')->on('currencies')->restrictOnDelete();

+ 1
- 0
database/migrations/2023_05_23_151550_create_document_items_table.php 查看文件

25
             $table->foreignId('discount_id')->nullable()->constrained()->nullOnDelete();
25
             $table->foreignId('discount_id')->nullable()->constrained()->nullOnDelete();
26
             $table->decimal('total', 15, 4);
26
             $table->decimal('total', 15, 4);
27
             $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
27
             $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
28
+            $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
28
             $table->timestamps();
29
             $table->timestamps();
29
         });
30
         });
30
     }
31
     }

+ 1
- 0
database/migrations/2023_05_23_173412_create_document_totals_table.php 查看文件

23
             $table->decimal('tax', 15, 4)->default(0);
23
             $table->decimal('tax', 15, 4)->default(0);
24
             $table->decimal('total', 15, 4);
24
             $table->decimal('total', 15, 4);
25
             $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
25
             $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
26
+            $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
26
             $table->timestamps();
27
             $table->timestamps();
27
         });
28
         });
28
     }
29
     }

+ 36
- 0
database/migrations/2023_05_26_025210_create_tag_tables.php 查看文件

1
+<?php
2
+
3
+use Illuminate\Database\Migrations\Migration;
4
+use Illuminate\Database\Schema\Blueprint;
5
+use Illuminate\Support\Facades\Schema;
6
+
7
+return new class extends Migration
8
+{
9
+    public function up(): void
10
+    {
11
+        Schema::create('tags', function (Blueprint $table) {
12
+            $table->id();
13
+
14
+            $table->json('name');
15
+            $table->json('slug');
16
+            $table->string('type')->nullable();
17
+            $table->integer('order_column')->nullable();
18
+
19
+            $table->timestamps();
20
+        });
21
+
22
+        Schema::create('taggables', function (Blueprint $table) {
23
+            $table->foreignId('tag_id')->constrained()->cascadeOnDelete();
24
+
25
+            $table->morphs('taggable');
26
+
27
+            $table->unique(['tag_id', 'taggable_id', 'taggable_type']);
28
+        });
29
+    }
30
+
31
+    public function down(): void
32
+    {
33
+        Schema::dropIfExists('taggables');
34
+        Schema::dropIfExists('tags');
35
+    }
36
+};

+ 37
- 0
database/migrations/2023_07_03_054805_create_default_settings_table.php 查看文件

1
+<?php
2
+
3
+use Illuminate\Database\Migrations\Migration;
4
+use Illuminate\Database\Schema\Blueprint;
5
+use Illuminate\Support\Facades\Schema;
6
+
7
+return new class extends Migration
8
+{
9
+    /**
10
+     * Run the migrations.
11
+     */
12
+    public function up(): void
13
+    {
14
+        Schema::create('default_settings', function (Blueprint $table) {
15
+            $table->id();
16
+            $table->foreignId('company_id')->constrained()->onDelete('cascade');
17
+            $table->foreignId('account_id')->constrained('accounts')->restrictOnDelete();
18
+            $table->string('currency_code')->default('USD');
19
+            $table->foreignId('sales_tax_id')->constrained('taxes')->restrictOnDelete();
20
+            $table->foreignId('purchase_tax_id')->constrained('taxes')->restrictOnDelete();
21
+            $table->foreignId('income_category_id')->constrained('categories')->restrictOnDelete();
22
+            $table->foreignId('expense_category_id')->constrained('categories')->restrictOnDelete();
23
+            $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
24
+            $table->timestamps();
25
+
26
+            $table->foreign('currency_code')->references('code')->on('currencies')->restrictOnDelete();
27
+        });
28
+    }
29
+
30
+    /**
31
+     * Reverse the migrations.
32
+     */
33
+    public function down(): void
34
+    {
35
+        Schema::dropIfExists('default_settings');
36
+    }
37
+};

+ 70
- 0
database/seeders/CategorySeeder.php 查看文件

1
+<?php
2
+
3
+namespace Database\Seeders;
4
+
5
+use App\Models\Setting\Category;
6
+use Illuminate\Database\Console\Seeds\WithoutModelEvents;
7
+use Illuminate\Database\Seeder;
8
+use Illuminate\Support\Facades\DB;
9
+
10
+class CategorySeeder extends Seeder
11
+{
12
+    /**
13
+     * Run the database seeds.
14
+     */
15
+    public function run(): void
16
+    {
17
+        $companyId = DB::table('companies')->first()->id;
18
+        $userId = DB::table('users')->first()->id;
19
+
20
+        $incomeCategories = [
21
+            'Salary',
22
+            'Bonus',
23
+            'Interest',
24
+            'Dividends',
25
+            'Rentals',
26
+        ];
27
+
28
+        $expenseCategories = [
29
+            'Rent',
30
+            'Utilities',
31
+            'Food',
32
+            'Transportation',
33
+            'Entertainment',
34
+        ];
35
+
36
+        // Merge and shuffle the sales and purchase taxes
37
+        $shuffledCategories = [
38
+            ...array_map(static fn($name) => ['name' => $name, 'type' => 'income'], $incomeCategories),
39
+            ...array_map(static fn($name) => ['name' => $name, 'type' => 'expense'], $expenseCategories)
40
+        ];
41
+
42
+        shuffle($shuffledCategories);
43
+
44
+        $allCategories = $shuffledCategories;
45
+
46
+        // Create each category
47
+        foreach ($allCategories as $category) {
48
+            Category::factory()->create([
49
+                'company_id' => $companyId,
50
+                'name' => $category['name'],
51
+                'type' => $category['type'],
52
+                'enabled' => false,
53
+                'created_by' => $userId,
54
+                'updated_by' => $userId,
55
+            ]);
56
+        }
57
+
58
+        // Set the first income category as enabled
59
+        Category::where('type', 'income')
60
+            ->where('company_id', $companyId)
61
+            ->first()
62
+            ->update(['enabled' => true]);
63
+
64
+        // Set the first expense category as enabled
65
+        Category::where('type', 'expense')
66
+            ->where('company_id', $companyId)
67
+            ->first()
68
+            ->update(['enabled' => true]);
69
+    }
70
+}

+ 68
- 0
database/seeders/TaxSeeder.php 查看文件

1
+<?php
2
+
3
+namespace Database\Seeders;
4
+
5
+use App\Models\Setting\Tax;
6
+use Illuminate\Database\Seeder;
7
+use Illuminate\Support\Facades\DB;
8
+
9
+class TaxSeeder extends Seeder
10
+{
11
+    /**
12
+     * Run the database seeds.
13
+     */
14
+    public function run(): void
15
+    {
16
+        $companyId = DB::table('companies')->first()->id;
17
+        $userId = DB::table('users')->first()->id;
18
+
19
+        $salesTaxes = [
20
+            'Goods and Services Tax (GST)',
21
+            'Value Added Tax (VAT)',
22
+            'State Sales Tax',
23
+            'Local Sales Tax',
24
+            'Excise Tax',
25
+        ];
26
+
27
+        $purchaseTaxes = [
28
+            'Import Duty',
29
+            'Customs Duty',
30
+            'Value Added Tax (VAT)',
31
+            'Luxury Tax',
32
+            'Environmental Tax',
33
+        ];
34
+
35
+        // Merge and shuffle the sales and purchase taxes
36
+        $shuffledTaxes = [
37
+            ...array_map(static fn($name) => ['name' => $name, 'type' => 'sales'], $salesTaxes),
38
+            ...array_map(static fn($name) => ['name' => $name, 'type' => 'purchase'], $purchaseTaxes)
39
+        ];
40
+
41
+        shuffle($shuffledTaxes);
42
+
43
+        $allTaxes = $shuffledTaxes;
44
+
45
+        foreach ($allTaxes as $tax) {
46
+            Tax::factory()->create([
47
+                'company_id' => $companyId,
48
+                'name' => $tax['name'],
49
+                'type' => $tax['type'],
50
+                'enabled' => false,
51
+                'created_by' => $userId,
52
+                'updated_by' => $userId,
53
+            ]);
54
+        }
55
+
56
+        // Set the first sales tax as enabled
57
+        Tax::where('type', 'sales')
58
+            ->where('company_id', $companyId)
59
+            ->first()
60
+            ->update(['enabled' => true]);
61
+
62
+        // Set the first purchase tax as enabled
63
+        Tax::where('type', 'purchase')
64
+            ->where('company_id', $companyId)
65
+            ->first()
66
+            ->update(['enabled' => true]);
67
+    }
68
+}

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


+ 154
- 1
resources/css/filament.css 查看文件

1
-@import '../../vendor/filament/filament/resources/css/app.css';
1
+@import '../../vendor/filament/filament/resources/css/app.css';
2
+
3
+
4
+.filament-companies-navigation-menu-button {
5
+    @apply bg-transparent;
6
+}
7
+
8
+.filament-tables-container {
9
+    @apply rounded-xl border border-gray-300 bg-moonlight shadow-sm dark:!border-gray-700 dark:!bg-gray-800;
10
+}
11
+
12
+[type="text"],
13
+[type="email"],
14
+[type="url"],
15
+[type="password"],
16
+[type="number"],
17
+[type="date"],
18
+[type="datetime-local"],
19
+[type="month"],
20
+[type="search"],
21
+[type="tel"],
22
+[type="time"],
23
+[type="week"],
24
+[multiple],
25
+textarea,
26
+select,
27
+[type="checkbox"],
28
+[type="radio"],
29
+.filament-forms-repeater-component * {
30
+    @apply !shadow-none bg-moonlight;
31
+}
32
+
33
+div.p-2.space-y-2.bg-white.rounded-xl {
34
+    @apply bg-moonlight dark:!bg-gray-800;
35
+}
36
+
37
+.filament-tables-empty-state {
38
+    @apply bg-moonlight;
39
+}
40
+
41
+.filament-tables-row {
42
+    @apply transition hover:!bg-gray-100 dark:hover:!bg-gray-500/10 even:!bg-platinum dark:even:!bg-gray-900;
43
+}
44
+
45
+.filament-forms-markdown-editor-component-toolbar-button {
46
+    @apply bg-moonlight;
47
+}
48
+
49
+button.relative.w-full.cursor-default {
50
+    @apply bg-moonlight;
51
+}
52
+
53
+div.absolute.z-10.my-1.hidden {
54
+    @apply bg-moonlight;
55
+}
56
+
57
+textarea[id="data.notes"] {
58
+    @apply bg-moonlight dark:!bg-gray-700;
59
+}
60
+
61
+.filament-modal-window {
62
+    @apply bg-moonlight;
63
+}
64
+
65
+.filament-stats-card {
66
+    @apply bg-moonlight;
67
+}
68
+
69
+
70
+.filament-dropdown-panel {
71
+    @apply bg-moonlight;
72
+}
73
+
74
+.filament-forms-tabs-component {
75
+    @apply bg-moonlight;
76
+}
77
+
78
+.filament-forms-tabs-component-button-active {
79
+    @apply bg-moonlight;
80
+}
81
+
82
+.choices__inner {
83
+    @apply bg-moonlight;
84
+}
85
+
86
+.choices__list--dropdown,
87
+.choices__list[aria-expanded] {
88
+    @apply bg-moonlight;
89
+}
90
+
91
+div.prose.block.h-full {
92
+    @apply bg-moonlight;
93
+}
94
+
95
+.filament-body {
96
+    @apply bg-platinum;
97
+}
98
+
99
+.filepond--root[data-style-panel-aspect-ratio] .filepond--drop-label {
100
+    transform: translateY(-50%) !important;
101
+    position: absolute;
102
+    top: 50%;
103
+}
104
+
105
+.filament-main-topbar,
106
+.filament-sidebar-header {
107
+    @apply bg-translucent dark:!bg-gray-800;
108
+}
109
+
110
+
111
+.apexcharts-title-text {
112
+    @apply fill-gray-600 dark:fill-gray-100 font-semibold !important;
113
+}
114
+
115
+.apexcharts-subtitle-text {
116
+    @apply fill-gray-500 dark:fill-gray-400 font-normal !important;
117
+}
118
+
119
+.apexcharts-legend-text {
120
+    @apply text-gray-500 dark:text-gray-400 font-medium !important;
121
+}
122
+
123
+.apexcharts-text, .apexcharts-xaxis-label, .apexcharts-yaxis-label {
124
+    @apply fill-gray-400 dark:fill-gray-500 font-medium !important;
125
+}
126
+
127
+@screen lg {
128
+    .filament-sidebar {
129
+        @apply bg-transparent dark:!bg-transparent shadow-none lg:!border-r-0 rtl:lg:!border-l-0;
130
+    }
131
+
132
+    .filament-sidebar-header {
133
+        @apply border-r rtl:border-l;
134
+    }
135
+}
136
+
137
+.filament-sidebar-group > button {
138
+    @apply outline-offset-8 rounded-sm;
139
+}
140
+
141
+.filament-sidebar nav li:not(.filament-sidebar-group) div {
142
+    border-top: none;
143
+}
144
+
145
+.filament-sidebar-group button p {
146
+    @apply text-primary-600 dark:!text-primary-500;
147
+}
148
+
149
+.filament-versions-nav-component {
150
+    @apply border-t-0;
151
+}
152
+
153
+
154
+

+ 3
- 0
resources/views/filament/pages/default-setting.blade.php 查看文件

1
+<x-filament::page>
2
+    @livewire('default-setting')
3
+</x-filament::page>

+ 13
- 0
resources/views/livewire/default-setting.blade.php 查看文件

1
+<div>
2
+    <form wire:submit.prevent="create">
3
+        {{ $this->form }}
4
+
5
+        <div class="mt-6">
6
+            <div class="flex flex-wrap items-center gap-4 justify-start">
7
+                <x-filament::button type="submit">
8
+                    {{ __('Save Changes') }}
9
+                </x-filament::button>
10
+            </div>
11
+        </div>
12
+    </form>
13
+</div>

+ 7
- 7
resources/views/vendor/filament-companies/companies/company-employee-manager.blade.php 查看文件

85
                 {{ __('filament-companies::default.action_section_descriptions.pending_company_invitations') }}
85
                 {{ __('filament-companies::default.action_section_descriptions.pending_company_invitations') }}
86
             </x-slot>
86
             </x-slot>
87
 
87
 
88
-            <div class="overflow-x-auto space-y-2 bg-white rounded-xl shadow dark:border-gray-600 dark:bg-gray-800 col-span-2 mt-5 sm:col-span-1 md:col-start-2 md:mt-0">
88
+            <div class="overflow-x-auto space-y-2 bg-moonlight rounded-xl shadow dark:border-gray-600 dark:bg-gray-800 col-span-2 mt-5 sm:col-span-1 md:col-start-2 md:mt-0">
89
                 <table class="w-full divide-y divide-gray-200 dark:divide-gray-700">
89
                 <table class="w-full divide-y divide-gray-200 dark:divide-gray-700">
90
-                    <thead class="bg-gray-100 dark:bg-gray-800">
90
+                    <thead class="bg-moonlight dark:bg-gray-800">
91
                     <tr>
91
                     <tr>
92
                         <th colspan="3" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
92
                         <th colspan="3" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
93
                             {{ __('filament-companies::default.fields.email') }}
93
                             {{ __('filament-companies::default.fields.email') }}
136
             </x-slot>
136
             </x-slot>
137
 
137
 
138
             <!-- Company Employee List -->
138
             <!-- Company Employee List -->
139
-            <div class="overflow-x-auto space-y-2 bg-white rounded-xl shadow dark:border-gray-600 dark:bg-gray-800 col-span-2 mt-5 sm:col-span-1 md:col-start-2 md:mt-0">
139
+            <div class="overflow-x-auto space-y-2 bg-moonlight rounded-xl shadow dark:border-gray-600 dark:bg-gray-800 col-span-2 mt-5 sm:col-span-1 md:col-start-2 md:mt-0">
140
                 <table class="w-full divide-y divide-gray-200 dark:divide-gray-700">
140
                 <table class="w-full divide-y divide-gray-200 dark:divide-gray-700">
141
-                    <thead class="bg-white dark:bg-gray-800">
141
+                    <thead class="bg-moonlight dark:bg-gray-800">
142
                     <tr>
142
                     <tr>
143
                         <th scope="col" colspan="3" class="px-6 py-3 text-left text-xs font-medium text-gray-600 dark:text-gray-400 uppercase tracking-wider">
143
                         <th scope="col" colspan="3" class="px-6 py-3 text-left text-xs font-medium text-gray-600 dark:text-gray-400 uppercase tracking-wider">
144
                             {{ __('filament-companies::default.fields.name') }}
144
                             {{ __('filament-companies::default.fields.name') }}
162
                             <td colspan="1" class="px-6 py-4 whitespace-nowrap">
162
                             <td colspan="1" class="px-6 py-4 whitespace-nowrap">
163
                                 <div class="space-x-2 text-right">
163
                                 <div class="space-x-2 text-right">
164
                                     <!-- Manage Company Employee Role -->
164
                                     <!-- Manage Company Employee Role -->
165
-                                    @if (Gate::check('addCompanyEmployee', $company) && Wallo\FilamentCompanies\FilamentCompanies::hasRoles())
166
-                                        <x-filament::button size="sm" outlined="true" :icon="(Wallo\FilamentCompanies\FilamentCompanies::findRole($user->employeeship->role)->key == 'admin') ? 'heroicon-o-shield-check' : 'heroicon-o-pencil'" :color="(Wallo\FilamentCompanies\FilamentCompanies::findRole($user->employeeship->role)->key == 'admin') ? 'primary' : 'warning'" wire:click="manageRole('{{ $user->id }}')">
165
+                                    @if (Gate::check('updateCompanyEmployee', $company) && Wallo\FilamentCompanies\FilamentCompanies::hasRoles())
166
+                                        <x-filament::button size="sm" outlined="true" :icon="(Wallo\FilamentCompanies\FilamentCompanies::findRole($user->employeeship->role)->key === 'admin') ? 'heroicon-o-shield-check' : 'heroicon-o-pencil'" :color="(Wallo\FilamentCompanies\FilamentCompanies::findRole($user->employeeship->role)->key === 'admin') ? 'primary' : 'warning'" wire:click="manageRole('{{ $user->id }}')">
167
                                             {{ Wallo\FilamentCompanies\FilamentCompanies::findRole($user->employeeship->role)->name }}
167
                                             {{ Wallo\FilamentCompanies\FilamentCompanies::findRole($user->employeeship->role)->name }}
168
                                         </x-filament::button>
168
                                         </x-filament::button>
169
                                     @elseif (Wallo\FilamentCompanies\FilamentCompanies::hasRoles())
169
                                     @elseif (Wallo\FilamentCompanies\FilamentCompanies::hasRoles())
170
-                                        <x-filament::button size="sm" disabled="true" outlined="true" :icon="(Wallo\FilamentCompanies\FilamentCompanies::findRole($user->employeeship->role)->key == 'admin') ? 'heroicon-o-shield-check' : 'heroicon-o-pencil'" color="secondary">
170
+                                        <x-filament::button size="sm" disabled="true" outlined="true" :icon="(Wallo\FilamentCompanies\FilamentCompanies::findRole($user->employeeship->role)->key === 'admin') ? 'heroicon-o-shield-check' : 'heroicon-o-pencil'" color="secondary">
171
                                             {{ Wallo\FilamentCompanies\FilamentCompanies::findRole($user->employeeship->role)->name }}
171
                                             {{ Wallo\FilamentCompanies\FilamentCompanies::findRole($user->employeeship->role)->name }}
172
                                         </x-filament::button>
172
                                         </x-filament::button>
173
                                     @endif
173
                                     @endif

+ 171
- 0
resources/views/vendor/forms/components/section.blade.php 查看文件

1
+@php
2
+    $isAside = $isAside();
3
+    $isCollapsed = $isCollapsed();
4
+    $isCollapsible = $isCollapsible() && (! $isAside);
5
+    $isCompact = $isCompact();
6
+    $isFormBefore = $isFormBefore();
7
+@endphp
8
+
9
+<div
10
+        @if ($isCollapsible)
11
+            x-data="{
12
+            isCollapsed: @js($isCollapsed),
13
+        }"
14
+        x-on:open-form-section.window="if ($event.detail.id == $el.id) isCollapsed = false"
15
+        x-on:collapse-form-section.window="if ($event.detail.id == $el.id) isCollapsed = true"
16
+        x-on:toggle-form-section.window="if ($event.detail.id == $el.id) isCollapsed = ! isCollapsed"
17
+        x-on:expand-concealing-component.window="
18
+            error = $el.querySelector('[data-validation-error]')
19
+
20
+            if (! error) {
21
+                return
22
+            }
23
+
24
+            isCollapsed = false
25
+
26
+            if (document.body.querySelector('[data-validation-error]') !== error) {
27
+                return
28
+            }
29
+
30
+            setTimeout(
31
+                () =>
32
+                    $el.scrollIntoView({
33
+                        behavior: 'smooth',
34
+                        block: 'start',
35
+                        inline: 'start',
36
+                    }),
37
+                200,
38
+            )
39
+        "
40
+        @endif
41
+        id="{{ $getId() }}"
42
+        {{
43
+            $attributes
44
+                ->merge($getExtraAttributes())
45
+                ->class([
46
+                    'filament-forms-section-component',
47
+                    'rounded-xl border border-gray-300 bg-moonlight' => ! $isAside,
48
+                    'grid grid-cols-1' => $isAside,
49
+                    'md:grid-cols-2' => $isAside && ! $isCompact,
50
+                    'md:grid-cols-3' => $isAside && $isCompact,
51
+                    'md:order-last' => $isFormBefore,
52
+                    'dark:border-gray-600 dark:bg-gray-800' => config('forms.dark_mode') && ! $isAside,
53
+                ])
54
+        }}
55
+        {{ $getExtraAlpineAttributeBag() }}
56
+>
57
+    <div
58
+            @class([
59
+                'filament-forms-section-header-wrapper flex overflow-hidden rounded-t-xl rtl:space-x-reverse',
60
+                'min-h-[40px]' => $isCompact,
61
+                'min-h-[56px]' => ! $isCompact,
62
+                'pb-4' => $isAside,
63
+                'pr-6' => $isAside && ! $isFormBefore,
64
+                'pl-6' => $isAside && $isFormBefore,
65
+                'items-center bg-moonlight px-6 py-4 border-b-2 border-b-platinum' => ! $isAside,
66
+                'dark:bg-gray-800 dark:border-b-gray-900' => config('forms.dark_mode') && (! $isAside),
67
+            ])
68
+            @if ($isCollapsible)
69
+                x-bind:class="{ 'rounded-b-xl': isCollapsed }"
70
+            x-on:click="isCollapsed = ! isCollapsed"
71
+            @endif
72
+    >
73
+        <div
74
+                @class([
75
+                    'filament-forms-section-header flex-1 space-y-1',
76
+                    'cursor-pointer' => $isCollapsible,
77
+                ])
78
+        >
79
+            <h3
80
+                    @class([
81
+                        'pointer-events-none flex flex-row items-center font-bold tracking-tight',
82
+                        'text-xl' => ! $isCompact || $isAside,
83
+                    ])
84
+            >
85
+                @if ($icon = $getIcon())
86
+                    <x-dynamic-component
87
+                            :component="$icon"
88
+                            @class([
89
+                                'mr-1',
90
+                                'h-4 w-4' => $isCompact && ! $isAside,
91
+                                'h-6 w-6' => ! $isCompact || $isAside,
92
+                            ])
93
+                    />
94
+                @endif
95
+
96
+                {{ $getHeading() }}
97
+            </h3>
98
+
99
+            @if ($description = $getDescription())
100
+                <p
101
+                        @class([
102
+                            'text-gray-500',
103
+                            'text-sm' => $isCompact && ! $isAside,
104
+                            'text-base' => ! $isCompact || $isAside,
105
+                        ])
106
+                >
107
+                    {{ $description }}
108
+                </p>
109
+            @endif
110
+        </div>
111
+
112
+        @if ($isCollapsible)
113
+            <button
114
+                    x-on:click.stop="isCollapsed = ! isCollapsed"
115
+                    x-bind:class="{
116
+                    '-rotate-180': ! isCollapsed,
117
+                }"
118
+                    type="button"
119
+                    @class([
120
+                        'flex transform items-center justify-center rounded-full text-primary-500 outline-none hover:bg-gray-500/5 focus:bg-primary-500/10',
121
+                        'h-10 w-10' => ! $isCompact,
122
+                        '-my-1 h-8 w-8' => $isCompact,
123
+                        '-rotate-180' => ! $isCollapsed,
124
+                    ])
125
+            >
126
+                <svg
127
+                        @class([
128
+                            'h-7 w-7' => ! $isCompact,
129
+                            'h-5 w-5' => $isCompact,
130
+                        ])
131
+                        xmlns="http://www.w3.org/2000/svg"
132
+                        fill="none"
133
+                        viewBox="0 0 24 24"
134
+                        stroke="currentColor"
135
+                >
136
+                    <path
137
+                            stroke-linecap="round"
138
+                            stroke-linejoin="round"
139
+                            stroke-width="2"
140
+                            d="M19 9l-7 7-7-7"
141
+                    />
142
+                </svg>
143
+            </button>
144
+        @endif
145
+    </div>
146
+
147
+    <div
148
+            @if ($isCollapsible)
149
+                x-bind:class="{ 'invisible h-0 !m-0 overflow-y-hidden': isCollapsed }"
150
+            x-bind:aria-expanded="(! isCollapsed).toString()"
151
+            @if ($isCollapsed) x-cloak @endif
152
+            @endif
153
+            @class([
154
+                'filament-forms-section-content-wrapper',
155
+                'col-span-2' => $isAside && $isCompact,
156
+                'md:order-first' => $isFormBefore,
157
+            ])
158
+    >
159
+        <div
160
+                @class([
161
+                    'filament-forms-section-content',
162
+                    'rounded-xl border border-gray-300 bg-moonlight' => $isAside,
163
+                    'dark:border-gray-600 dark:bg-gray-800' => config('forms.dark_mode') && $isAside,
164
+                    'p-6' => ! $isCompact || $isAside,
165
+                    'p-4' => $isCompact && ! $isAside,
166
+                ])
167
+        >
168
+            {{ $getChildComponentContainer() }}
169
+        </div>
170
+    </div>
171
+</div>

+ 19
- 1
tailwind.config.js 查看文件

1
 const colors = require('tailwindcss/colors')
1
 const colors = require('tailwindcss/colors')
2
+const defaultTheme = require('tailwindcss/defaultTheme')
2
 
3
 
3
 /** @type {import('tailwindcss').Config} */
4
 /** @type {import('tailwindcss').Config} */
4
 module.exports = {
5
 module.exports = {
12
     extend: {
13
     extend: {
13
       colors: {
14
       colors: {
14
         danger: colors.rose,
15
         danger: colors.rose,
15
-        primary: colors.blue,
16
+        primary: {
17
+          50: '#F2F3FA',
18
+          100: '#D8DBF5',
19
+          200: '#B4B9EB',
20
+          300: '#9297E1',
21
+          400: '#7075D7',
22
+          500: '#454DC8',
23
+          600: '#27285C',
24
+          700: '#2D2F6A',
25
+          800: '#37387E',
26
+          900: '#414292',
27
+        },
16
         success: colors.green,
28
         success: colors.green,
17
         warning: colors.yellow,
29
         warning: colors.yellow,
30
+        platinum: '#E8E9EB',
31
+        moonlight: '#F6F5F3',
32
+        translucent: 'rgba(54, 54, 52, 0.06)',
18
       },
33
       },
34
+      fontFamily: {
35
+        sans: ['DM Sans', ...defaultTheme.fontFamily.sans],
36
+      }
19
     },
37
     },
20
   },
38
   },
21
   plugins: [
39
   plugins: [

正在加载...
取消
保存