Quellcode durchsuchen

wip: Accounting Module

3.x
Andrew Wallo vor 2 Jahren
Ursprung
Commit
7891926d80
87 geänderte Dateien mit 5153 neuen und 2654 gelöschten Zeilen
  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 Datei anzeigen

@@ -2,6 +2,7 @@
2 2
 
3 3
 namespace App\Filament\Pages;
4 4
 
5
+use App\Filament\Pages\Widgets\Companies\Charts\CumulativeTotal;
5 6
 use Filament\Pages\Page;
6 7
 use Illuminate\Support\Facades\Auth;
7 8
 use Wallo\FilamentCompanies\FilamentCompanies;
@@ -26,7 +27,8 @@ class Companies extends Page
26 27
     {
27 28
         return [
28 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 32
             Widgets\Companies\Tables\Companies::class,
31 33
         ];
32 34
     }

+ 29
- 0
app/Filament/Pages/DefaultSetting.php Datei anzeigen

@@ -0,0 +1,29 @@
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 Datei anzeigen

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

+ 0
- 1
app/Filament/Pages/Users.php Datei anzeigen

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

+ 12
- 17
app/Filament/Pages/Widgets/Companies/Charts/CompanyStatsOverview.php Datei anzeigen

@@ -7,12 +7,7 @@ use Filament\Widgets\StatsOverviewWidget;
7 7
 
8 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 13
      * Holt's Linear Trend Method
@@ -23,9 +18,9 @@ class CompanyStatsOverview extends StatsOverviewWidget
23 18
         $trend = $data[1] - $data[0];
24 19
 
25 20
         $forecast = [];
26
-        for ($i = 0; $i < count($data); $i++) {
21
+        foreach ($data as $iValue) {
27 22
             $prev_level = $level;
28
-            $level = $alpha * $data[$i] + (1 - $alpha) * ($prev_level + $trend);
23
+            $level = $alpha * $iValue + (1 - $alpha) * ($prev_level + $trend);
29 24
             $trend = $beta * ($level - $prev_level) + (1 - $beta) * $trend;
30 25
             $forecast[] = $level + $trend;
31 26
         }
@@ -43,14 +38,14 @@ class CompanyStatsOverview extends StatsOverviewWidget
43 38
         $bestBeta = $beta;
44 39
 
45 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 44
                 $error = $this->calculateError($data, $forecast);
50 45
                 if ($error < $minError) {
51 46
                     $minError = $error;
52
-                    $bestAlpha = $alpha;
53
-                    $bestBeta = $beta;
47
+                    $bestAlpha = $testAlpha;
48
+                    $bestBeta = $testBeta;
54 49
                 }
55 50
             }
56 51
         }
@@ -64,8 +59,8 @@ class CompanyStatsOverview extends StatsOverviewWidget
64 59
     protected function calculateError($data, $forecast): float
65 60
     {
66 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 66
         return $error;
@@ -99,7 +94,7 @@ class CompanyStatsOverview extends StatsOverviewWidget
99 94
         // Get Weekly Data for Company Data
100 95
         $weeklyData = collect($weeks)->mapWithKeys(static function ($value, $week) use ($companyData) {
101 96
             $matchingData = $companyData->firstWhere('week', $week);
102
-            return [$week => $matchingData ? $matchingData->aggregate : 0];
97
+            return [$week => $matchingData->aggregate ?? 0];
103 98
         });
104 99
 
105 100
         // Calculate total companies per week
@@ -111,7 +106,7 @@ class CompanyStatsOverview extends StatsOverviewWidget
111 106
         // Calculate new companies and percentage change per week
112 107
         $newCompanies = [0];
113 108
         $weeklyPercentageChange = [0];
114
-        for ($i = 1; $i < count($totalCompanies); $i++) {
109
+        for ($i = 1, $iMax = count($totalCompanies); $i < $iMax; $i++) {
115 110
             $newCompanies[] = $totalCompanies[$i] - $totalCompanies[$i - 1];
116 111
             $weeklyPercentageChange[] = ($newCompanies[$i] / $totalCompanies[$i - 1]) * 100;
117 112
         }

+ 0
- 204
app/Filament/Pages/Widgets/Companies/Charts/CumulativeCompanyData.php Datei anzeigen

@@ -1,204 +0,0 @@
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 Datei anzeigen

@@ -0,0 +1,154 @@
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 Datei anzeigen

@@ -0,0 +1,143 @@
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 Datei anzeigen

@@ -13,10 +13,9 @@ use Illuminate\Database\Eloquent\Relations\Relation;
13 13
 
14 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 20
     protected function getTableQuery(): Builder|Relation
22 21
     {
@@ -36,6 +35,7 @@ class Companies extends PageWidget
36 35
         return [
37 36
             Tables\Filters\SelectFilter::make('name')
38 37
                 ->label('Owner')
38
+                ->searchable()
39 39
                 ->relationship('owner', 'name'),
40 40
             Tables\Filters\TernaryFilter::make('personal_company')
41 41
                 ->label('Personal Company')
@@ -52,13 +52,11 @@ class Companies extends PageWidget
52 52
                 ->searchable()
53 53
                 ->grow(false),
54 54
             Tables\Columns\TextColumn::make('name')
55
-                ->weight('semibold')
56 55
                 ->label('Company')
57 56
                 ->sortable()
58 57
                 ->searchable(),
59 58
             Tables\Columns\TextColumn::make('users_count')
60 59
                 ->label('Employees')
61
-                ->weight('semibold')
62 60
                 ->counts('users')
63 61
                 ->sortable(),
64 62
             Tables\Columns\IconColumn::make('personal_company')
@@ -71,4 +69,4 @@ class Companies extends PageWidget
71 69
                 ->falseColor('secondary')
72 70
         ];
73 71
     }
74
-}
72
+}

+ 0
- 177
app/Filament/Pages/Widgets/Employees/Charts/CumulativeEmployeeData.php Datei anzeigen

@@ -1,177 +0,0 @@
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 Datei anzeigen

@@ -0,0 +1,153 @@
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 Datei anzeigen

@@ -0,0 +1,163 @@
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 Datei anzeigen

@@ -13,10 +13,9 @@ use Illuminate\Database\Eloquent\Relations\Relation;
13 13
 
14 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 20
     protected function getTableQuery(): Builder|Relation
22 21
     {
@@ -36,6 +35,7 @@ class Employees extends PageWidget
36 35
         return [
37 36
             Tables\Filters\SelectFilter::make('name')
38 37
                 ->label('Company')
38
+                ->searchable()
39 39
                 ->relationship('companies', 'name', static fn (Builder $query) => $query->whereHas('users')),
40 40
         ];
41 41
     }
@@ -52,8 +52,7 @@ class Employees extends PageWidget
52 52
             Tables\Columns\TextColumn::make('companies.name')
53 53
                 ->label('Company')
54 54
                 ->sortable()
55
-                ->searchable()
56
-                ->weight('semibold'),
55
+                ->searchable(),
57 56
             Tables\Columns\BadgeColumn::make('employeeships.role')
58 57
                 ->label('Role')
59 58
                 ->enum([
@@ -71,4 +70,4 @@ class Employees extends PageWidget
71 70
                 ->sortable(),
72 71
         ];
73 72
     }
74
-}
73
+}

+ 0
- 181
app/Filament/Pages/Widgets/Users/Charts/CumulativeUserData.php Datei anzeigen

@@ -1,181 +0,0 @@
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 Datei anzeigen

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

+ 183
- 114
app/Filament/Resources/AccountResource.php Datei anzeigen

@@ -21,8 +21,8 @@ use Illuminate\Database\Eloquent\SoftDeletingScope;
21 21
 use Illuminate\Support\Collection;
22 22
 use Illuminate\Support\Facades\Auth;
23 23
 use Illuminate\Support\Facades\DB;
24
-use Illuminate\Validation\Rule;
25 24
 use Illuminate\Validation\Rules\Unique;
25
+use Wallo\FilamentSelectify\Components\ToggleButton;
26 26
 
27 27
 class AccountResource extends Resource
28 28
 {
@@ -41,118 +41,173 @@ class AccountResource extends Resource
41 41
     {
42 42
         return $form
43 43
             ->schema([
44
-                Forms\Components\Section::make('General')
44
+                Forms\Components\Group::make()
45 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 51
                                     ->searchable()
80
-                                    ->options(Account::getCurrencyCodes())
52
+                                    ->default('checking')
81 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 55
                                     ->required(),
88
-                                Forms\Components\TextInput::make('currency.name')
56
+                                Forms\Components\TextInput::make('name')
89 57
                                     ->label('Name')
90 58
                                     ->maxLength(100)
91 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 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 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,23 +218,37 @@ class AccountResource extends Resource
163 218
         return $table
164 219
             ->columns([
165 220
                 Tables\Columns\TextColumn::make('name')
221
+                    ->label('Account')
166 222
                     ->searchable()
167 223
                     ->weight('semibold')
168 224
                     ->icon(static fn (Account $record) => $record->enabled ? 'heroicon-o-lock-closed' : null)
169 225
                     ->tooltip(static fn (Account $record) => $record->enabled ? 'Default Account' : null)
170 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 228
                     ->sortable(),
176 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 233
                     ->searchable()
179 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 252
                 Tables\Columns\TextColumn::make('opening_balance')
184 253
                     ->label('Current Balance')
185 254
                     ->sortable()
@@ -222,14 +291,14 @@ class AccountResource extends Resource
222 291
                     }),
223 292
             ]);
224 293
     }
225
-    
294
+
226 295
     public static function getRelations(): array
227 296
     {
228 297
         return [
229 298
             //
230 299
         ];
231 300
     }
232
-    
301
+
233 302
     public static function getPages(): array
234 303
     {
235 304
         return [
@@ -237,5 +306,5 @@ class AccountResource extends Resource
237 306
             'create' => Pages\CreateAccount::route('/create'),
238 307
             'edit' => Pages\EditAccount::route('/{record}/edit'),
239 308
         ];
240
-    }    
309
+    }
241 310
 }

+ 49
- 17
app/Filament/Resources/AccountResource/Pages/CreateAccount.php Datei anzeigen

@@ -4,9 +4,11 @@ namespace App\Filament\Resources\AccountResource\Pages;
4 4
 
5 5
 use App\Filament\Resources\AccountResource;
6 6
 use App\Models\Banking\Account;
7
+use Filament\Notifications\Notification;
7 8
 use Filament\Pages\Actions;
8 9
 use Filament\Resources\Pages\CreateRecord;
9 10
 use Illuminate\Database\Eloquent\Model;
11
+use Illuminate\Support\Facades\Auth;
10 12
 use Illuminate\Support\Facades\DB;
11 13
 
12 14
 class CreateAccount extends CreateRecord
@@ -15,38 +17,68 @@ class CreateAccount extends CreateRecord
15 17
 
16 18
     protected function getRedirectUrl(): string
17 19
     {
18
-        return $this->getResource()::getUrl('index');
20
+        return self::getResource()::getUrl('index');
19 21
     }
20 22
 
21 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 29
         return $data;
25 30
     }
26 31
 
27 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 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 Datei anzeigen

@@ -4,9 +4,11 @@ namespace App\Filament\Resources\AccountResource\Pages;
4 4
 
5 5
 use App\Filament\Resources\AccountResource;
6 6
 use App\Models\Banking\Account;
7
+use Filament\Notifications\Notification;
7 8
 use Filament\Pages\Actions;
8 9
 use Filament\Resources\Pages\EditRecord;
9 10
 use Illuminate\Database\Eloquent\Model;
11
+use Illuminate\Support\Facades\Auth;
10 12
 use Illuminate\Support\Facades\DB;
11 13
 
12 14
 class EditAccount extends EditRecord
@@ -27,33 +29,68 @@ class EditAccount extends EditRecord
27 29
 
28 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 36
         return $data;
32 37
     }
33 38
 
34 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 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 Datei anzeigen

@@ -0,0 +1,140 @@
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 Datei anzeigen

@@ -0,0 +1,74 @@
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 Datei anzeigen

@@ -0,0 +1,104 @@
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 Datei anzeigen

@@ -0,0 +1,19 @@
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 Datei anzeigen

@@ -19,6 +19,9 @@ use Illuminate\Database\Eloquent\Model;
19 19
 use Illuminate\Database\Eloquent\SoftDeletingScope;
20 20
 use Illuminate\Support\Collection;
21 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 26
 class CurrencyResource extends Resource
24 27
 {
@@ -38,12 +41,13 @@ class CurrencyResource extends Resource
38 41
         return $form
39 42
             ->schema([
40 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 45
                     ->schema([
42 46
                         Forms\Components\Select::make('code')
43 47
                             ->label('Code')
44 48
                             ->options(Currency::getCurrencyCodes())
45 49
                             ->searchable()
46
-                            ->placeholder('- Select Code -')
50
+                            ->placeholder('Select a currency code...')
47 51
                             ->reactive()
48 52
                             ->afterStateUpdated(static function (Closure $set, $state) {
49 53
                                 $code = $state;
@@ -64,14 +68,13 @@ class CurrencyResource extends Resource
64 68
                             ->required(),
65 69
                         Forms\Components\TextInput::make('name')
66 70
                             ->translateLabel()
67
-                            ->placeholder('Enter Name')
68 71
                             ->maxLength(100)
69 72
                             ->required(),
70 73
                         Forms\Components\TextInput::make('rate')
71 74
                             ->label('Rate')
72
-                            ->placeholder('Enter Rate')
73 75
                             ->dehydrateStateUsing(static fn (Closure $get, $state): bool => $get('enabled') === true ? '1' : $state) // rate is 1 when enabled is true
74 76
                             ->numeric()
77
+                            ->reactive()
75 78
                             ->disabled(static fn (Closure $get): bool => $get('enabled') === true) // disabled is true when enabled is true
76 79
                             ->mask(static fn (Forms\Components\TextInput\Mask $mask) => $mask
77 80
                                 ->numeric()
@@ -86,35 +89,32 @@ class CurrencyResource extends Resource
86 89
                         Forms\Components\Select::make('precision')
87 90
                             ->label('Precision')
88 91
                             ->searchable()
89
-                            ->placeholder('- Select Precision -')
92
+                            ->placeholder('Select the currency precision...')
90 93
                             ->options(['0', '1', '2', '3', '4'])
91 94
                             ->required(),
92 95
                         Forms\Components\TextInput::make('symbol')
93 96
                             ->label('Symbol')
94
-                            ->placeholder('Enter Symbol')
95 97
                             ->maxLength(5)
96 98
                             ->required(),
97 99
                         Forms\Components\Select::make('symbol_first')
98 100
                             ->label('Symbol Position')
99 101
                             ->searchable()
100
-                            ->boolean('Before Amount', 'After Amount', '- Select Symbol Position -')
102
+                            ->boolean('Before Amount', 'After Amount', 'Select the currency symbol position...')
101 103
                             ->required(),
102 104
                         Forms\Components\TextInput::make('decimal_mark')
103 105
                             ->label('Decimal Separator')
104
-                            ->placeholder('Enter Decimal Separator')
105 106
                             ->maxLength(1)
106 107
                             ->required(),
107 108
                         Forms\Components\TextInput::make('thousands_separator')
108 109
                             ->label('Thousands Separator')
109
-                            ->placeholder('Enter Thousands Separator')
110 110
                             ->maxLength(1)
111 111
                             ->required(),
112
-                        Forms\Components\Toggle::make('enabled')
112
+                        ToggleButton::make('enabled')
113 113
                             ->label('Default Currency')
114 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 118
                     ])->columns(),
119 119
             ]);
120 120
     }
@@ -209,14 +209,14 @@ class CurrencyResource extends Resource
209 209
                     }),
210 210
             ]);
211 211
     }
212
-    
212
+
213 213
     public static function getRelations(): array
214 214
     {
215 215
         return [
216 216
             //
217 217
         ];
218 218
     }
219
-    
219
+
220 220
     public static function getPages(): array
221 221
     {
222 222
         return [
@@ -224,5 +224,5 @@ class CurrencyResource extends Resource
224 224
             'create' => Pages\CreateCurrency::route('/create'),
225 225
             'edit' => Pages\EditCurrency::route('/{record}/edit'),
226 226
         ];
227
-    }    
227
+    }
228 228
 }

+ 37
- 18
app/Filament/Resources/CurrencyResource/Pages/CreateCurrency.php Datei anzeigen

@@ -7,6 +7,7 @@ use App\Models\Setting\Currency;
7 7
 use Filament\Pages\Actions;
8 8
 use Filament\Resources\Pages\CreateRecord;
9 9
 use Illuminate\Database\Eloquent\Model;
10
+use Illuminate\Support\Facades\Auth;
10 11
 use Illuminate\Support\Facades\DB;
11 12
 
12 13
 class CreateCurrency extends CreateRecord
@@ -20,34 +21,52 @@ class CreateCurrency extends CreateRecord
20 21
 
21 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 28
         return $data;
26 29
     }
27 30
 
28 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 Datei anzeigen

@@ -3,10 +3,12 @@
3 3
 namespace App\Filament\Resources\CurrencyResource\Pages;
4 4
 
5 5
 use App\Filament\Resources\CurrencyResource;
6
+use App\Models\Banking\Account;
6 7
 use App\Models\Setting\Currency;
7 8
 use Filament\Pages\Actions;
8 9
 use Filament\Resources\Pages\EditRecord;
9 10
 use Illuminate\Database\Eloquent\Model;
11
+use Illuminate\Support\Facades\Auth;
10 12
 use Illuminate\Support\Facades\DB;
11 13
 
12 14
 class EditCurrency extends EditRecord
@@ -27,33 +29,57 @@ class EditCurrency extends EditRecord
27 29
 
28 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 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 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 Datei anzeigen

@@ -5,6 +5,7 @@ namespace App\Filament\Resources;
5 5
 use App\Actions\Banking\CreateCurrencyFromAccount;
6 6
 use App\Filament\Resources\CustomerResource\Pages;
7 7
 use App\Filament\Resources\CustomerResource\RelationManagers;
8
+use Wallo\FilamentSelectify\Components\ButtonGroup;
8 9
 use App\Models\Banking\Account;
9 10
 use App\Models\Contact;
10 11
 use Filament\Forms;
@@ -16,12 +17,14 @@ use Illuminate\Database\Eloquent\Builder;
16 17
 use Illuminate\Database\Eloquent\SoftDeletingScope;
17 18
 use Illuminate\Support\Facades\Auth;
18 19
 use Illuminate\Support\Facades\DB;
20
+use Squire\Models\Country;
21
+use Squire\Models\Region;
19 22
 
20 23
 class CustomerResource extends Resource
21 24
 {
22 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 29
     protected static ?string $navigationGroup = 'Sales';
27 30
 
@@ -32,7 +35,7 @@ class CustomerResource extends Resource
32 35
     public static function getEloquentQuery(): Builder
33 36
     {
34 37
         return parent::getEloquentQuery()
35
-            ->where('type', 'customer')
38
+            ->customer()
36 39
             ->where('company_id', Auth::user()->currentCompany->id);
37 40
     }
38 41
 
@@ -42,43 +45,50 @@ class CustomerResource extends Resource
42 45
             ->schema([
43 46
                 Forms\Components\Section::make('General')
44 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 87
                 Forms\Components\Section::make('Billing')
78 88
                     ->schema([
79 89
                         Forms\Components\TextInput::make('tax_number')
90
+                            ->label('Tax Number')
80 91
                             ->maxLength(100)
81
-                            ->placeholder('Enter Tax Number')
82 92
                             ->nullable(),
83 93
                         Forms\Components\Select::make('currency_code')
84 94
                             ->label('Currency')
@@ -135,33 +145,44 @@ class CustomerResource extends Resource
135 145
                 ])->columns(2),
136 146
                 Forms\Components\Section::make('Address')
137 147
                     ->schema([
138
-                        Forms\Components\Textarea::make('address')
148
+                        Forms\Components\TextInput::make('address')
139 149
                             ->label('Address')
140 150
                             ->maxLength(100)
141
-                            ->placeholder('Enter Address')
142 151
                             ->columnSpanFull()
143 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 177
                         Forms\Components\TextInput::make('city')
145 178
                             ->label('Town/City')
146 179
                             ->maxLength(100)
147
-                            ->placeholder('Enter Town/City')
148 180
                             ->nullable(),
149 181
                         Forms\Components\TextInput::make('zip_code')
150 182
                             ->label('Postal/Zip Code')
151 183
                             ->maxLength(100)
152
-                            ->placeholder('Enter Postal/Zip Code')
153 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,21 +191,22 @@ class CustomerResource extends Resource
170 191
         return $table
171 192
             ->columns([
172 193
                 Tables\Columns\TextColumn::make('name')
194
+                    ->label('Name')
173 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 197
                     ->searchable()
178 198
                     ->sortable(),
179 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 203
                     ->searchable()
185 204
                     ->sortable(),
186
-                Tables\Columns\TextColumn::make('currency.name')
205
+                Tables\Columns\TextColumn::make('country')
206
+                    ->label('Country')
187 207
                     ->searchable()
208
+                    ->formatStateUsing(static fn (Contact $record) => $record->country ?: 'N/A')
209
+                    ->description(static fn (Contact $record) => $record->currency->name ?: 'N/A')
188 210
                     ->sortable(),
189 211
             ])
190 212
             ->filters([
@@ -197,14 +219,14 @@ class CustomerResource extends Resource
197 219
                 Tables\Actions\DeleteBulkAction::make(),
198 220
             ]);
199 221
     }
200
-    
222
+
201 223
     public static function getRelations(): array
202 224
     {
203 225
         return [
204 226
             //
205 227
         ];
206 228
     }
207
-    
229
+
208 230
     public static function getPages(): array
209 231
     {
210 232
         return [
@@ -212,5 +234,5 @@ class CustomerResource extends Resource
212 234
             'create' => Pages\CreateCustomer::route('/create'),
213 235
             'edit' => Pages\EditCustomer::route('/{record}/edit'),
214 236
         ];
215
-    }    
237
+    }
216 238
 }

+ 5
- 2
app/Filament/Resources/CustomerResource/Pages/CreateCustomer.php Datei anzeigen

@@ -5,6 +5,8 @@ namespace App\Filament\Resources\CustomerResource\Pages;
5 5
 use App\Filament\Resources\CustomerResource;
6 6
 use Filament\Pages\Actions;
7 7
 use Filament\Resources\Pages\CreateRecord;
8
+use Illuminate\Support\Facades\Auth;
9
+use Squire\Models\Region;
8 10
 
9 11
 class CreateCustomer extends CreateRecord
10 12
 {
@@ -17,9 +19,10 @@ class CreateCustomer extends CreateRecord
17 19
 
18 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 23
         $data['type'] = 'customer';
22
-        $data['created_by'] = auth()->id();
24
+        $data['created_by'] = Auth::id();
25
+
23 26
         return $data;
24 27
     }
25 28
 }

+ 14
- 0
app/Filament/Resources/CustomerResource/Pages/EditCustomer.php Datei anzeigen

@@ -5,6 +5,7 @@ namespace App\Filament\Resources\CustomerResource\Pages;
5 5
 use App\Filament\Resources\CustomerResource;
6 6
 use Filament\Pages\Actions;
7 7
 use Filament\Resources\Pages\EditRecord;
8
+use Illuminate\Support\Facades\Auth;
8 9
 
9 10
 class EditCustomer extends EditRecord
10 11
 {
@@ -16,4 +17,17 @@ class EditCustomer extends EditRecord
16 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 Datei anzeigen

@@ -0,0 +1,186 @@
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 Datei anzeigen

@@ -0,0 +1,12 @@
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 Datei anzeigen

@@ -0,0 +1,19 @@
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 Datei anzeigen

@@ -0,0 +1,19 @@
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 Datei anzeigen

@@ -4,6 +4,8 @@ namespace App\Filament\Resources;
4 4
 
5 5
 use App\Filament\Resources\InvoiceResource\Pages;
6 6
 use App\Filament\Resources\InvoiceResource\RelationManagers;
7
+use Wallo\FilamentSelectify\Components\ButtonGroup;
8
+use App\Models\Banking\Account;
7 9
 use App\Models\Document\Document;
8 10
 use Filament\Forms;
9 11
 use Filament\Resources\Form;
@@ -18,7 +20,7 @@ class InvoiceResource extends Resource
18 20
 {
19 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 25
     protected static ?string $navigationGroup = 'Sales';
24 26
 
@@ -37,7 +39,68 @@ class InvoiceResource extends Resource
37 39
     {
38 40
         return $form
39 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,14 +120,14 @@ class InvoiceResource extends Resource
57 120
                 Tables\Actions\DeleteBulkAction::make(),
58 121
             ]);
59 122
     }
60
-    
123
+
61 124
     public static function getRelations(): array
62 125
     {
63 126
         return [
64
-            //
127
+            RelationManagers\DocumentItemsRelationManager::class,
65 128
         ];
66 129
     }
67
-    
130
+
68 131
     public static function getPages(): array
69 132
     {
70 133
         return [
@@ -72,5 +135,5 @@ class InvoiceResource extends Resource
72 135
             'create' => Pages\CreateInvoice::route('/create'),
73 136
             'edit' => Pages\EditInvoice::route('/{record}/edit'),
74 137
         ];
75
-    }    
138
+    }
76 139
 }

+ 12
- 0
app/Filament/Resources/InvoiceResource/Pages/CreateInvoice.php Datei anzeigen

@@ -5,8 +5,20 @@ namespace App\Filament\Resources\InvoiceResource\Pages;
5 5
 use App\Filament\Resources\InvoiceResource;
6 6
 use Filament\Pages\Actions;
7 7
 use Filament\Resources\Pages\CreateRecord;
8
+use Illuminate\Support\Facades\Auth;
8 9
 
9 10
 class CreateInvoice extends CreateRecord
10 11
 {
11 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 Datei anzeigen

@@ -5,6 +5,7 @@ namespace App\Filament\Resources\InvoiceResource\Pages;
5 5
 use App\Filament\Resources\InvoiceResource;
6 6
 use Filament\Pages\Actions;
7 7
 use Filament\Resources\Pages\EditRecord;
8
+use Illuminate\Support\Facades\Auth;
8 9
 
9 10
 class EditInvoice extends EditRecord
10 11
 {
@@ -16,4 +17,17 @@ class EditInvoice extends EditRecord
16 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 Datei anzeigen

@@ -0,0 +1,49 @@
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 Datei anzeigen

@@ -0,0 +1,199 @@
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 Datei anzeigen

@@ -0,0 +1,74 @@
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 Datei anzeigen

@@ -0,0 +1,106 @@
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 Datei anzeigen

@@ -0,0 +1,19 @@
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 Datei anzeigen

@@ -0,0 +1,156 @@
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 Datei anzeigen

@@ -3,32 +3,46 @@
3 3
 namespace App\Models\Banking;
4 4
 
5 5
 use App\Models\Setting\Currency;
6
+use App\Models\Setting\DefaultSetting;
6 7
 use Database\Factories\AccountFactory;
7 8
 use Illuminate\Database\Eloquent\Factories\Factory;
8 9
 use Illuminate\Database\Eloquent\Factories\HasFactory;
9 10
 use Illuminate\Database\Eloquent\Model;
10 11
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
12
+use Illuminate\Database\Eloquent\Relations\HasMany;
11 13
 use Illuminate\Support\Facades\Auth;
12 14
 use Illuminate\Support\Facades\Config;
15
+use Spatie\Tags\HasTags;
13 16
 use Wallo\FilamentCompanies\FilamentCompanies;
14 17
 
15 18
 class Account extends Model
16 19
 {
17 20
     use HasFactory;
21
+    use HasTags;
18 22
 
19 23
     protected $table = 'accounts';
20 24
 
21 25
     protected $fillable = [
26
+        'company_id',
22 27
         'type',
23 28
         'name',
24 29
         'number',
25 30
         'currency_code',
26 31
         'opening_balance',
27
-        'enabled',
32
+        'description',
33
+        'notes',
34
+        'status',
28 35
         'bank_name',
29 36
         'bank_phone',
30 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 48
     protected $casts = [
@@ -50,11 +64,40 @@ class Account extends Model
50 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 82
     public static function getAccountTypes(): array
54 83
     {
55 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 Datei anzeigen

@@ -8,6 +8,9 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
8 8
 use Illuminate\Database\Eloquent\Model;
9 9
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
10 10
 use Illuminate\Database\Eloquent\Relations\HasMany;
11
+use Illuminate\Http\Request;
12
+use Squire\Models\Country;
13
+use Squire\Models\Region;
11 14
 use Wallo\FilamentCompanies\FilamentCompanies;
12 15
 
13 16
 class Contact extends Model
@@ -33,10 +36,7 @@ class Contact extends Model
33 36
         'currency_code',
34 37
         'reference',
35 38
         'created_by',
36
-    ];
37
-
38
-    protected $casts = [
39
-        'enabled' => 'boolean',
39
+        'updated_by',
40 40
     ];
41 41
 
42 42
     public function company(): BelongsTo
@@ -49,6 +49,11 @@ class Contact extends Model
49 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 57
     public function currency(): BelongsTo
53 58
     {
54 59
         return $this->belongsTo(Currency::class, 'currency_code', 'code');
@@ -59,6 +64,48 @@ class Contact extends Model
59 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 109
     public function bills(): HasMany
63 110
     {
64 111
         return $this->documents()->where('type', 'bill');

+ 6
- 0
app/Models/Document/Document.php Datei anzeigen

@@ -40,6 +40,7 @@ class Document extends Model
40 40
         'contact_id',
41 41
         'notes',
42 42
         'created_by',
43
+        'updated_by',
43 44
     ];
44 45
 
45 46
     protected $casts = [
@@ -58,6 +59,11 @@ class Document extends Model
58 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 67
     public function tax(): BelongsTo
62 68
     {
63 69
         return $this->belongsTo(Tax::class);

+ 6
- 0
app/Models/Document/DocumentItem.php Datei anzeigen

@@ -31,6 +31,7 @@ class DocumentItem extends Model
31 31
         'discount_id',
32 32
         'total',
33 33
         'created_by',
34
+        'updated_by',
34 35
     ];
35 36
 
36 37
     protected $casts = [
@@ -49,6 +50,11 @@ class DocumentItem extends Model
49 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 58
     public function document(): BelongsTo
53 59
     {
54 60
         return $this->belongsTo(Document::class);

+ 6
- 0
app/Models/Document/DocumentTotal.php Datei anzeigen

@@ -26,6 +26,7 @@ class DocumentTotal extends Model
26 26
         'tax',
27 27
         'total',
28 28
         'created_by',
29
+        'updated_by',
29 30
     ];
30 31
 
31 32
     public function company(): BelongsTo
@@ -38,6 +39,11 @@ class DocumentTotal extends Model
38 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 47
     public function document(): BelongsTo
42 48
     {
43 49
         return $this->belongsTo(Document::class);

+ 6
- 0
app/Models/Item.php Datei anzeigen

@@ -32,6 +32,7 @@ class Item extends Model
32 32
         'discount_id',
33 33
         'enabled',
34 34
         'created_by',
35
+        'updated_by',
35 36
     ];
36 37
 
37 38
     protected $casts = [
@@ -48,6 +49,11 @@ class Item extends Model
48 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 57
     public function category(): BelongsTo
52 58
     {
53 59
         return $this->belongsTo(Category::class, 'category_id')->withDefault([

+ 16
- 0
app/Models/Setting/Category.php Datei anzeigen

@@ -25,12 +25,23 @@ class Category extends Model
25 25
         'color',
26 26
         'enabled',
27 27
         'created_by',
28
+        'updated_by',
28 29
     ];
29 30
 
30 31
     protected $casts = [
31 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 45
     public function company(): BelongsTo
35 46
     {
36 47
         return $this->belongsTo(FilamentCompanies::companyModel(), 'company_id');
@@ -41,6 +52,11 @@ class Category extends Model
41 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 60
     public function items(): HasMany
45 61
     {
46 62
         return $this->hasMany(Item::class);

+ 2
- 0
app/Models/Setting/Currency.php Datei anzeigen

@@ -30,6 +30,8 @@ class Currency extends Model
30 30
         'thousands_separator',
31 31
         'enabled',
32 32
         'company_id',
33
+        'created_by',
34
+        'updated_by',
33 35
     ];
34 36
 
35 37
     protected $casts = [

+ 172
- 0
app/Models/Setting/DefaultSetting.php Datei anzeigen

@@ -0,0 +1,172 @@
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 Datei anzeigen

@@ -11,6 +11,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
11 11
 use Illuminate\Database\Eloquent\Model;
12 12
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
13 13
 use Illuminate\Database\Eloquent\Relations\HasMany;
14
+use Wallo\FilamentCompanies\FilamentCompanies;
14 15
 
15 16
 class Discount extends Model
16 17
 {
@@ -25,17 +26,27 @@ class Discount extends Model
25 26
         'computation',
26 27
         'type',
27 28
         'scope',
29
+        'start_date',
30
+        'end_date',
28 31
         'enabled',
29 32
         'created_by',
33
+        'updated_by',
30 34
     ];
31 35
 
32 36
     protected $casts = [
33 37
         'enabled' => 'boolean',
38
+        'start_date' => 'datetime',
39
+        'end_date' => 'datetime',
34 40
     ];
35 41
 
36 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 52
     public function items(): HasMany
@@ -58,6 +69,31 @@ class Discount extends Model
58 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 97
     protected static function newFactory(): Factory
62 98
     {
63 99
         return DiscountFactory::new();

+ 34
- 1
app/Models/Setting/Tax.php Datei anzeigen

@@ -12,6 +12,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
12 12
 use Illuminate\Database\Eloquent\Model;
13 13
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
14 14
 use Illuminate\Database\Eloquent\Relations\HasMany;
15
+use Wallo\FilamentCompanies\FilamentCompanies;
15 16
 
16 17
 class Tax extends Model
17 18
 {
@@ -29,6 +30,7 @@ class Tax extends Model
29 30
         'scope',
30 31
         'enabled',
31 32
         'created_by',
33
+        'updated_by',
32 34
     ];
33 35
 
34 36
     protected $casts = [
@@ -42,7 +44,12 @@ class Tax extends Model
42 44
 
43 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 55
     public function items(): HasMany
@@ -65,6 +72,32 @@ class Tax extends Model
65 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 101
     protected static function newFactory(): Factory
69 102
     {
70 103
         return TaxFactory::new();

+ 67
- 0
app/Observers/AccountObserver.php Datei anzeigen

@@ -0,0 +1,67 @@
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 Datei anzeigen

@@ -3,6 +3,9 @@
3 3
 namespace App\Providers;
4 4
 
5 5
 use Filament\Facades\Filament;
6
+use Filament\Forms\Components\Field;
7
+use Filament\Forms\Components\Actions\Action;
8
+use Illuminate\Support\HtmlString;
6 9
 use Illuminate\Support\ServiceProvider;
7 10
 
8 11
 class AppServiceProvider extends ServiceProvider
@@ -23,5 +26,15 @@ class AppServiceProvider extends ServiceProvider
23 26
         Filament::serving(static function () {
24 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 Datei anzeigen

@@ -2,6 +2,8 @@
2 2
 
3 3
 namespace App\Providers;
4 4
 
5
+use App\Models\Banking\Account;
6
+use App\Observers\AccountObserver;
5 7
 use Illuminate\Auth\Events\Registered;
6 8
 use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
7 9
 use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
@@ -35,4 +37,13 @@ class EventServiceProvider extends ServiceProvider
35 37
     {
36 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 Datei anzeigen

@@ -0,0 +1,27 @@
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 Datei anzeigen

@@ -0,0 +1,60 @@
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 Datei anzeigen

@@ -7,13 +7,18 @@
7 7
     "require": {
8 8
         "php": "^8.1",
9 9
         "andrewdwallo/filament-companies": "^2.0",
10
+        "andrewdwallo/filament-selectify": "^1.0",
10 11
         "filament/filament": "^2.17",
12
+        "filament/spatie-laravel-tags-plugin": "^2.17",
11 13
         "flowframe/laravel-trend": "^0.1.5",
12 14
         "guzzlehttp/guzzle": "^7.2",
13 15
         "laravel/framework": "^10.8",
14 16
         "laravel/sanctum": "^3.2",
15 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 23
     "require-dev": {
19 24
         "doctrine/dbal": "^3.6",

+ 1268
- 515
composer.lock
Datei-Diff unterdrückt, da er zu groß ist
Datei anzeigen


+ 1
- 0
config/app.php Datei anzeigen

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

+ 1
- 1
config/forms.php Datei anzeigen

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

+ 158
- 0
config/livewire.php Datei anzeigen

@@ -0,0 +1,158 @@
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 Datei anzeigen

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

@@ -25,7 +25,35 @@ class CategoryFactory extends Factory
25 25
     public function definition(): array
26 26
     {
27 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 Datei anzeigen

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

@@ -25,7 +25,9 @@ class TaxFactory extends Factory
25 25
     public function definition(): array
26 26
     {
27 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 Datei anzeigen

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

+ 11
- 2
database/migrations/2023_05_11_044321_create_accounts_table.php Datei anzeigen

@@ -14,16 +14,25 @@ return new class extends Migration
14 14
         Schema::create('accounts', function (Blueprint $table) {
15 15
             $table->id();
16 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 19
             $table->string('number', 20);
20 20
             $table->string('currency_code')->default('USD');
21 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 25
             $table->string('bank_name', 100)->nullable();
23 26
             $table->string('bank_phone', 20)->nullable();
24 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 33
             $table->boolean('enabled')->default(true);
26 34
             $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
35
+            $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
27 36
             $table->timestamps();
28 37
 
29 38
             $table->unique(['company_id', 'number']);

+ 2
- 1
database/migrations/2023_05_12_042255_create_categories_table.php Datei anzeigen

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

+ 1
- 0
database/migrations/2023_05_19_042232_create_contacts_table.php Datei anzeigen

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

+ 1
- 0
database/migrations/2023_05_20_080131_create_taxes_table.php Datei anzeigen

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

+ 4
- 0
database/migrations/2023_05_21_163808_create_discounts_table.php Datei anzeigen

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

+ 1
- 0
database/migrations/2023_05_22_073252_create_items_table.php Datei anzeigen

@@ -26,6 +26,7 @@ return new class extends Migration
26 26
             $table->foreignId('discount_id')->nullable()->constrained()->nullOnDelete();
27 27
             $table->boolean('enabled')->default(true);
28 28
             $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
29
+            $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
29 30
             $table->timestamps();
30 31
         });
31 32
     }

+ 2
- 1
database/migrations/2023_05_23_141215_create_documents_table.php Datei anzeigen

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

+ 1
- 0
database/migrations/2023_05_23_151550_create_document_items_table.php Datei anzeigen

@@ -25,6 +25,7 @@ return new class extends Migration
25 25
             $table->foreignId('discount_id')->nullable()->constrained()->nullOnDelete();
26 26
             $table->decimal('total', 15, 4);
27 27
             $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
28
+            $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
28 29
             $table->timestamps();
29 30
         });
30 31
     }

+ 1
- 0
database/migrations/2023_05_23_173412_create_document_totals_table.php Datei anzeigen

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

+ 36
- 0
database/migrations/2023_05_26_025210_create_tag_tables.php Datei anzeigen

@@ -0,0 +1,36 @@
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 Datei anzeigen

@@ -0,0 +1,37 @@
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 Datei anzeigen

@@ -0,0 +1,70 @@
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 Datei anzeigen

@@ -0,0 +1,68 @@
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
Datei-Diff unterdrückt, da er zu groß ist
Datei anzeigen


+ 154
- 1
resources/css/filament.css Datei anzeigen

@@ -1 +1,154 @@
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 Datei anzeigen

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

+ 13
- 0
resources/views/livewire/default-setting.blade.php Datei anzeigen

@@ -0,0 +1,13 @@
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 Datei anzeigen

@@ -85,9 +85,9 @@
85 85
                 {{ __('filament-companies::default.action_section_descriptions.pending_company_invitations') }}
86 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 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 91
                     <tr>
92 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 93
                             {{ __('filament-companies::default.fields.email') }}
@@ -136,9 +136,9 @@
136 136
             </x-slot>
137 137
 
138 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 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 142
                     <tr>
143 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 144
                             {{ __('filament-companies::default.fields.name') }}
@@ -162,12 +162,12 @@
162 162
                             <td colspan="1" class="px-6 py-4 whitespace-nowrap">
163 163
                                 <div class="space-x-2 text-right">
164 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 167
                                             {{ Wallo\FilamentCompanies\FilamentCompanies::findRole($user->employeeship->role)->name }}
168 168
                                         </x-filament::button>
169 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 171
                                             {{ Wallo\FilamentCompanies\FilamentCompanies::findRole($user->employeeship->role)->name }}
172 172
                                         </x-filament::button>
173 173
                                     @endif

+ 171
- 0
resources/views/vendor/forms/components/section.blade.php Datei anzeigen

@@ -0,0 +1,171 @@
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 Datei anzeigen

@@ -1,4 +1,5 @@
1 1
 const colors = require('tailwindcss/colors')
2
+const defaultTheme = require('tailwindcss/defaultTheme')
2 3
 
3 4
 /** @type {import('tailwindcss').Config} */
4 5
 module.exports = {
@@ -12,10 +13,27 @@ module.exports = {
12 13
     extend: {
13 14
       colors: {
14 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 28
         success: colors.green,
17 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 39
   plugins: [

Laden…
Abbrechen
Speichern