Browse Source

v2: Filament v3 support

3.x
wallo 2 years ago
parent
commit
320f587ab9
100 changed files with 2883 additions and 3534 deletions
  1. 1
    3
      .env.example
  2. 0
    80
      app/Abstracts/Forms/EditFormRecord.php
  3. 5
    7
      app/Actions/FilamentCompanies/CreateNewUser.php
  4. 2
    4
      app/Actions/FilamentCompanies/CreateUserFromProvider.php
  5. 2
    2
      app/Actions/FilamentCompanies/ResolveSocialiteUser.php
  6. 1
    2
      app/Actions/FilamentCompanies/SetUserPassword.php
  7. 3
    5
      app/Actions/FilamentCompanies/UpdateUserPassword.php
  8. 2
    2
      app/Actions/FilamentCompanies/UpdateUserProfileInformation.php
  9. 0
    18
      app/Actions/Fortify/PasswordValidationRules.php
  10. 0
    29
      app/Actions/Fortify/ResetUserPassword.php
  11. 7
    6
      app/Actions/OptionAction/CreateCurrency.php
  12. 35
    0
      app/Casts/MoneyCast.php
  13. 24
    0
      app/Casts/RateCast.php
  14. 14
    0
      app/Casts/TrimLeadingZeroCast.php
  15. 18
    0
      app/Enums/CategoryType.php
  16. 32
    0
      app/Enums/Concerns/Utilities.php
  17. 16
    0
      app/Enums/DiscountComputation.php
  18. 16
    0
      app/Enums/DiscountScope.php
  19. 37
    0
      app/Enums/DiscountType.php
  20. 19
    0
      app/Enums/DocumentAmountColumn.php
  21. 23
    0
      app/Enums/DocumentItemColumn.php
  22. 19
    0
      app/Enums/DocumentPriceColumn.php
  23. 27
    0
      app/Enums/DocumentType.php
  24. 19
    0
      app/Enums/DocumentUnitColumn.php
  25. 30
    0
      app/Enums/EntityType.php
  26. 26
    0
      app/Enums/Font.php
  27. 33
    0
      app/Enums/MaxContentWidth.php
  28. 41
    0
      app/Enums/ModalWidth.php
  29. 44
    0
      app/Enums/PaymentTerms.php
  30. 83
    0
      app/Enums/PrimaryColor.php
  31. 29
    0
      app/Enums/RecordsPerPage.php
  32. 18
    0
      app/Enums/TableSortDirection.php
  33. 17
    0
      app/Enums/TaxComputation.php
  34. 16
    0
      app/Enums/TaxScope.php
  35. 37
    0
      app/Enums/TaxType.php
  36. 19
    0
      app/Enums/Template.php
  37. 23
    0
      app/Events/CompanyDefaultEvent.php
  38. 29
    0
      app/Events/CompanyDefaultUpdated.php
  39. 8
    0
      app/Events/UpdateCompanyDefault.php
  40. 77
    0
      app/Filament/Company/Pages/CreateCompany.php
  41. 282
    0
      app/Filament/Company/Pages/Setting/Appearance.php
  42. 251
    0
      app/Filament/Company/Pages/Setting/CompanyDefault.php
  43. 284
    0
      app/Filament/Company/Pages/Setting/CompanyProfile.php
  44. 334
    0
      app/Filament/Company/Pages/Setting/Invoice.php
  45. 57
    103
      app/Filament/Company/Resources/Banking/AccountResource.php
  46. 4
    3
      app/Filament/Company/Resources/Banking/AccountResource/Pages/CreateAccount.php
  47. 7
    9
      app/Filament/Company/Resources/Banking/AccountResource/Pages/EditAccount.php
  48. 4
    4
      app/Filament/Company/Resources/Banking/AccountResource/Pages/ListAccounts.php
  49. 155
    0
      app/Filament/Company/Resources/Setting/CategoryResource.php
  50. 4
    5
      app/Filament/Company/Resources/Setting/CategoryResource/Pages/CreateCategory.php
  51. 6
    7
      app/Filament/Company/Resources/Setting/CategoryResource/Pages/EditCategory.php
  52. 19
    0
      app/Filament/Company/Resources/Setting/CategoryResource/Pages/ListCategories.php
  53. 189
    0
      app/Filament/Company/Resources/Setting/CurrencyResource.php
  54. 4
    5
      app/Filament/Company/Resources/Setting/CurrencyResource/Pages/CreateCurrency.php
  55. 6
    7
      app/Filament/Company/Resources/Setting/CurrencyResource/Pages/EditCurrency.php
  56. 4
    4
      app/Filament/Company/Resources/Setting/CurrencyResource/Pages/ListCurrencies.php
  57. 197
    0
      app/Filament/Company/Resources/Setting/DiscountResource.php
  58. 6
    6
      app/Filament/Company/Resources/Setting/DiscountResource/Pages/CreateDiscount.php
  59. 6
    7
      app/Filament/Company/Resources/Setting/DiscountResource/Pages/EditDiscount.php
  60. 4
    4
      app/Filament/Company/Resources/Setting/DiscountResource/Pages/ListDiscounts.php
  61. 191
    0
      app/Filament/Company/Resources/Setting/TaxResource.php
  62. 5
    5
      app/Filament/Company/Resources/Setting/TaxResource/Pages/CreateTax.php
  63. 7
    8
      app/Filament/Company/Resources/Setting/TaxResource/Pages/EditTax.php
  64. 4
    4
      app/Filament/Company/Resources/Setting/TaxResource/Pages/ListTaxes.php
  65. 0
    39
      app/Filament/Pages/Companies.php
  66. 0
    51
      app/Filament/Pages/CompanyDetails.php
  67. 0
    10
      app/Filament/Pages/Dashboard.php
  68. 0
    51
      app/Filament/Pages/DefaultSetting.php
  69. 0
    38
      app/Filament/Pages/Employees.php
  70. 0
    51
      app/Filament/Pages/Invoice.php
  71. 0
    36
      app/Filament/Pages/Users.php
  72. 0
    148
      app/Filament/Pages/Widgets/Companies/Charts/CompanyStatsOverview.php
  73. 0
    154
      app/Filament/Pages/Widgets/Companies/Charts/CumulativeGrowth.php
  74. 0
    143
      app/Filament/Pages/Widgets/Companies/Charts/CumulativeTotal.php
  75. 0
    72
      app/Filament/Pages/Widgets/Companies/Tables/Companies.php
  76. 0
    153
      app/Filament/Pages/Widgets/Employees/Charts/CumulativeGrowth.php
  77. 0
    163
      app/Filament/Pages/Widgets/Employees/Charts/CumulativeRoles.php
  78. 0
    73
      app/Filament/Pages/Widgets/Employees/Tables/Employees.php
  79. 0
    45
      app/Filament/Pages/Widgets/Users/Tables/Users.php
  80. 0
    155
      app/Filament/Resources/CategoryResource.php
  81. 0
    23
      app/Filament/Resources/CategoryResource/Pages/ListCategories.php
  82. 0
    251
      app/Filament/Resources/CurrencyResource.php
  83. 0
    253
      app/Filament/Resources/CustomerResource.php
  84. 0
    26
      app/Filament/Resources/CustomerResource/Pages/CreateCustomer.php
  85. 0
    24
      app/Filament/Resources/CustomerResource/Pages/EditCustomer.php
  86. 0
    19
      app/Filament/Resources/CustomerResource/Pages/ListCustomers.php
  87. 0
    202
      app/Filament/Resources/DiscountResource.php
  88. 0
    152
      app/Filament/Resources/InvoiceResource.php
  89. 0
    27
      app/Filament/Resources/InvoiceResource/Pages/CreateInvoice.php
  90. 0
    24
      app/Filament/Resources/InvoiceResource/Pages/EditInvoice.php
  91. 0
    19
      app/Filament/Resources/InvoiceResource/Pages/ListInvoices.php
  92. 0
    49
      app/Filament/Resources/InvoiceResource/RelationManagers/DocumentItemsRelationManager.php
  93. 0
    208
      app/Filament/Resources/TaxResource.php
  94. 0
    26
      app/Forms/Components/Invoice.php
  95. 1
    0
      app/Http/Kernel.php
  96. 0
    13
      app/Http/Livewire/Bill.php
  97. 0
    89
      app/Http/Livewire/CompanyDetails.php
  98. 0
    161
      app/Http/Livewire/DefaultSetting.php
  99. 0
    247
      app/Http/Livewire/Invoice.php
  100. 0
    0
      app/Http/Middleware/ApplyCurrentCompanyScope.php

+ 1
- 3
.env.example View File

51
 PUSHER_SCHEME=https
51
 PUSHER_SCHEME=https
52
 PUSHER_APP_CLUSTER=mt1
52
 PUSHER_APP_CLUSTER=mt1
53
 
53
 
54
+VITE_APP_NAME="${APP_NAME}"
54
 VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
55
 VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
55
 VITE_PUSHER_HOST="${PUSHER_HOST}"
56
 VITE_PUSHER_HOST="${PUSHER_HOST}"
56
 VITE_PUSHER_PORT="${PUSHER_PORT}"
57
 VITE_PUSHER_PORT="${PUSHER_PORT}"
57
 VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
58
 VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
58
 VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
59
 VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
59
-
60
-GITHUB_CLIENT_ID=
61
-GITHUB_CLIENT_SECRET=

+ 0
- 80
app/Abstracts/Forms/EditFormRecord.php View File

1
-<?php
2
-
3
-namespace App\Abstracts\Forms;
4
-
5
-use Filament\Forms\ComponentContainer;
6
-use Filament\Forms\Concerns\InteractsWithForms;
7
-use Filament\Forms\Contracts\HasForms;
8
-use Filament\Notifications\Notification;
9
-use Illuminate\Database\Eloquent\Model;
10
-use Livewire\Component;
11
-
12
-/**
13
- * @property ComponentContainer $form
14
- */
15
-abstract class EditFormRecord extends Component implements HasForms
16
-{
17
-    use InteractsWithForms;
18
-
19
-    public ?array $data = [];
20
-
21
-    abstract protected function getFormModel(): Model|string|null;
22
-
23
-    public function mount(): void
24
-    {
25
-        $this->fillForm();
26
-    }
27
-
28
-    public function fillForm(): void
29
-    {
30
-        $data = $this->getFormModel()->attributesToArray();
31
-
32
-        $data = $this->mutateFormDataBeforeFill($data);
33
-
34
-        $this->form->fill($data);
35
-    }
36
-
37
-    protected function mutateFormDataBeforeFill(array $data): array
38
-    {
39
-        return $data;
40
-    }
41
-
42
-    public function save(): void
43
-    {
44
-        $data = $this->form->getState();
45
-
46
-        $data = $this->mutateFormDataBeforeSave($data);
47
-
48
-        $this->handleRecordUpdate($this->getFormModel(), $data);
49
-
50
-        $this->getSavedNotification()?->send();
51
-    }
52
-
53
-    protected function mutateFormDataBeforeSave(array $data): array
54
-    {
55
-        return $data;
56
-    }
57
-
58
-    protected function handleRecordUpdate(Model $record, array $data): Model
59
-    {
60
-        $record->update($data);
61
-
62
-        return $record;
63
-    }
64
-
65
-    protected function getSavedNotification(): ?Notification
66
-    {
67
-        $title = $this->getSavedNotificationTitle();
68
-        if (blank($title)) {
69
-            return null;
70
-        }
71
-        return Notification::make()
72
-            ->success()
73
-            ->title($title);
74
-    }
75
-
76
-    protected function getSavedNotificationTitle(): ?string
77
-    {
78
-        return __('filament::resources/pages/edit-record.messages.saved');
79
-    }
80
-}

app/Actions/Fortify/CreateNewUser.php → app/Actions/FilamentCompanies/CreateNewUser.php View File

1
 <?php
1
 <?php
2
 
2
 
3
-namespace App\Actions\Fortify;
3
+namespace App\Actions\FilamentCompanies;
4
 
4
 
5
 use App\Models\Company;
5
 use App\Models\Company;
6
 use App\Models\User;
6
 use App\Models\User;
7
 use Illuminate\Support\Facades\DB;
7
 use Illuminate\Support\Facades\DB;
8
 use Illuminate\Support\Facades\Hash;
8
 use Illuminate\Support\Facades\Hash;
9
 use Illuminate\Support\Facades\Validator;
9
 use Illuminate\Support\Facades\Validator;
10
-use Laravel\Fortify\Contracts\CreatesNewUsers;
11
-use Wallo\FilamentCompanies\FilamentCompanies;
10
+use Wallo\FilamentCompanies\Contracts\CreatesNewUsers;
11
+use Wallo\FilamentCompanies\Features;
12
 
12
 
13
 class CreateNewUser implements CreatesNewUsers
13
 class CreateNewUser implements CreatesNewUsers
14
 {
14
 {
15
-    use PasswordValidationRules;
16
-
17
     /**
15
     /**
18
      * Create a newly registered user.
16
      * Create a newly registered user.
19
      *
17
      *
24
         Validator::make($input, [
22
         Validator::make($input, [
25
             'name' => ['required', 'string', 'max:255'],
23
             'name' => ['required', 'string', 'max:255'],
26
             'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
24
             'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
27
-            'password' => $this->passwordRules(),
28
-            'terms' => FilamentCompanies::hasTermsAndPrivacyPolicyFeature() ? ['accepted', 'required'] : '',
25
+            'password' => ['required', 'string', 'min:8', 'confirmed'],
26
+            'terms' => Features::hasTermsAndPrivacyPolicyFeature() ? ['accepted', 'required'] : '',
29
         ])->validate();
27
         ])->validate();
30
 
28
 
31
         return DB::transaction(function () use ($input) {
29
         return DB::transaction(function () use ($input) {

+ 2
- 4
app/Actions/FilamentCompanies/CreateUserFromProvider.php View File

9
 use Wallo\FilamentCompanies\Contracts\CreatesConnectedAccounts;
9
 use Wallo\FilamentCompanies\Contracts\CreatesConnectedAccounts;
10
 use Wallo\FilamentCompanies\Contracts\CreatesUserFromProvider;
10
 use Wallo\FilamentCompanies\Contracts\CreatesUserFromProvider;
11
 use Wallo\FilamentCompanies\Features;
11
 use Wallo\FilamentCompanies\Features;
12
-use Wallo\FilamentCompanies\FilamentCompanies;
13
 use Wallo\FilamentCompanies\Socialite;
12
 use Wallo\FilamentCompanies\Socialite;
14
 
13
 
15
 class CreateUserFromProvider implements CreatesUserFromProvider
14
 class CreateUserFromProvider implements CreatesUserFromProvider
54
 
53
 
55
     private function shouldSetProfilePhoto(ProviderUserContract $providerUser): bool
54
     private function shouldSetProfilePhoto(ProviderUserContract $providerUser): bool
56
     {
55
     {
57
-        return Features::profilePhotos() &&
58
-            Socialite::hasProviderAvatarsFeature() &&
59
-            FilamentCompanies::managesProfilePhotos() &&
56
+        return Socialite::hasProviderAvatarsFeature() &&
57
+            Features::managesProfilePhotos() &&
60
             $providerUser->getAvatar();
58
             $providerUser->getAvatar();
61
     }
59
     }
62
 
60
 

+ 2
- 2
app/Actions/FilamentCompanies/ResolveSocialiteUser.php View File

5
 use Laravel\Socialite\Contracts\User;
5
 use Laravel\Socialite\Contracts\User;
6
 use Laravel\Socialite\Facades\Socialite;
6
 use Laravel\Socialite\Facades\Socialite;
7
 use Wallo\FilamentCompanies\Contracts\ResolvesSocialiteUsers;
7
 use Wallo\FilamentCompanies\Contracts\ResolvesSocialiteUsers;
8
-use Wallo\FilamentCompanies\Features;
8
+use Wallo\FilamentCompanies\Socialite as FilamentCompaniesSocialite;
9
 
9
 
10
 class ResolveSocialiteUser implements ResolvesSocialiteUsers
10
 class ResolveSocialiteUser implements ResolvesSocialiteUsers
11
 {
11
 {
16
     {
16
     {
17
         $user = Socialite::driver($provider)->user();
17
         $user = Socialite::driver($provider)->user();
18
 
18
 
19
-        if (Features::generatesMissingEmails()) {
19
+        if (FilamentCompaniesSocialite::generatesMissingEmails()) {
20
             $user->email = $user->getEmail() ?? ("{$user->id}@{$provider}".config('app.domain'));
20
             $user->email = $user->getEmail() ?? ("{$user->id}@{$provider}".config('app.domain'));
21
         }
21
         }
22
 
22
 

+ 1
- 2
app/Actions/FilamentCompanies/SetUserPassword.php View File

5
 use App\Models\User;
5
 use App\Models\User;
6
 use Illuminate\Support\Facades\Hash;
6
 use Illuminate\Support\Facades\Hash;
7
 use Illuminate\Support\Facades\Validator;
7
 use Illuminate\Support\Facades\Validator;
8
-use Laravel\Fortify\Rules\Password;
9
 use Wallo\FilamentCompanies\Contracts\SetsUserPasswords;
8
 use Wallo\FilamentCompanies\Contracts\SetsUserPasswords;
10
 
9
 
11
 class SetUserPassword implements SetsUserPasswords
10
 class SetUserPassword implements SetsUserPasswords
16
     public function set(User $user, array $input): void
15
     public function set(User $user, array $input): void
17
     {
16
     {
18
         Validator::make($input, [
17
         Validator::make($input, [
19
-            'password' => ['required', 'string', new Password, 'confirmed'],
18
+            'password' => ['required', 'string', 'min:8', 'confirmed'],
20
         ])->validateWithBag('setPassword');
19
         ])->validateWithBag('setPassword');
21
 
20
 
22
         $user->forceFill([
21
         $user->forceFill([

app/Actions/Fortify/UpdateUserPassword.php → app/Actions/FilamentCompanies/UpdateUserPassword.php View File

1
 <?php
1
 <?php
2
 
2
 
3
-namespace App\Actions\Fortify;
3
+namespace App\Actions\FilamentCompanies;
4
 
4
 
5
 use App\Models\User;
5
 use App\Models\User;
6
 use Illuminate\Support\Facades\Hash;
6
 use Illuminate\Support\Facades\Hash;
7
 use Illuminate\Support\Facades\Validator;
7
 use Illuminate\Support\Facades\Validator;
8
-use Laravel\Fortify\Contracts\UpdatesUserPasswords;
8
+use Wallo\FilamentCompanies\Contracts\UpdatesUserPasswords;
9
 
9
 
10
 class UpdateUserPassword implements UpdatesUserPasswords
10
 class UpdateUserPassword implements UpdatesUserPasswords
11
 {
11
 {
12
-    use PasswordValidationRules;
13
-
14
     /**
12
     /**
15
      * Validate and update the user's password.
13
      * Validate and update the user's password.
16
      *
14
      *
20
     {
18
     {
21
         Validator::make($input, [
19
         Validator::make($input, [
22
             'current_password' => ['required', 'string', 'current_password:web'],
20
             'current_password' => ['required', 'string', 'current_password:web'],
23
-            'password' => $this->passwordRules(),
21
+            'password' => ['required', 'string', 'min:8', 'confirmed'],
24
         ], [
22
         ], [
25
             'current_password.current_password' => __('filament-companies::default.errors.password_does_not_match'),
23
             'current_password.current_password' => __('filament-companies::default.errors.password_does_not_match'),
26
         ])->validateWithBag('updatePassword');
24
         ])->validateWithBag('updatePassword');

app/Actions/Fortify/UpdateUserProfileInformation.php → app/Actions/FilamentCompanies/UpdateUserProfileInformation.php View File

1
 <?php
1
 <?php
2
 
2
 
3
-namespace App\Actions\Fortify;
3
+namespace App\Actions\FilamentCompanies;
4
 
4
 
5
 use App\Models\User;
5
 use App\Models\User;
6
 use Illuminate\Contracts\Auth\MustVerifyEmail;
6
 use Illuminate\Contracts\Auth\MustVerifyEmail;
7
 use Illuminate\Support\Facades\Validator;
7
 use Illuminate\Support\Facades\Validator;
8
 use Illuminate\Validation\Rule;
8
 use Illuminate\Validation\Rule;
9
-use Laravel\Fortify\Contracts\UpdatesUserProfileInformation;
9
+use Wallo\FilamentCompanies\Contracts\UpdatesUserProfileInformation;
10
 
10
 
11
 class UpdateUserProfileInformation implements UpdatesUserProfileInformation
11
 class UpdateUserProfileInformation implements UpdatesUserProfileInformation
12
 {
12
 {

+ 0
- 18
app/Actions/Fortify/PasswordValidationRules.php View File

1
-<?php
2
-
3
-namespace App\Actions\Fortify;
4
-
5
-use Laravel\Fortify\Rules\Password;
6
-
7
-trait PasswordValidationRules
8
-{
9
-    /**
10
-     * Get the validation rules used to validate passwords.
11
-     *
12
-     * @return array<int, \Illuminate\Contracts\Validation\Rule|array|string>
13
-     */
14
-    protected function passwordRules(): array
15
-    {
16
-        return ['required', 'string', new Password, 'confirmed'];
17
-    }
18
-}

+ 0
- 29
app/Actions/Fortify/ResetUserPassword.php View File

1
-<?php
2
-
3
-namespace App\Actions\Fortify;
4
-
5
-use App\Models\User;
6
-use Illuminate\Support\Facades\Hash;
7
-use Illuminate\Support\Facades\Validator;
8
-use Laravel\Fortify\Contracts\ResetsUserPasswords;
9
-
10
-class ResetUserPassword implements ResetsUserPasswords
11
-{
12
-    use PasswordValidationRules;
13
-
14
-    /**
15
-     * Validate and reset the user's forgotten password.
16
-     *
17
-     * @param  array<string, string>  $input
18
-     */
19
-    public function reset(User $user, array $input): void
20
-    {
21
-        Validator::make($input, [
22
-            'password' => $this->passwordRules(),
23
-        ])->validate();
24
-
25
-        $user->forceFill([
26
-            'password' => Hash::make($input['password']),
27
-        ])->save();
28
-    }
29
-}

+ 7
- 6
app/Actions/OptionAction/CreateCurrency.php View File

8
 {
8
 {
9
     public function create(string $code, string $name, string $rate): Currency
9
     public function create(string $code, string $name, string $rate): Currency
10
     {
10
     {
11
-        $defaultCurrency = Currency::getDefaultCurrency();
11
+        $defaultCurrency = Currency::getDefaultCurrencyCode();
12
 
12
 
13
         $hasDefaultCurrency = $defaultCurrency !== null;
13
         $hasDefaultCurrency = $defaultCurrency !== null;
14
+        $currency_code = currency($code);
14
 
15
 
15
         return Currency::create([
16
         return Currency::create([
16
             'name' => $name,
17
             'name' => $name,
17
             'code' => $code,
18
             'code' => $code,
18
             'rate' => $rate,
19
             'rate' => $rate,
19
-            'precision' => config("money.{$code}.precision"),
20
-            'symbol' => config("money.{$code}.symbol"),
21
-            'symbol_first' => config("money.{$code}.symbol_first"),
22
-            'decimal_mark' => config("money.{$code}.decimal_mark"),
23
-            'thousands_separator' => config("money.{$code}.thousands_separator"),
20
+            'precision' => $currency_code->getPrecision(),
21
+            'symbol' => $currency_code->getSymbol(),
22
+            'symbol_first' => $currency_code->isSymbolFirst(),
23
+            'decimal_mark' => $currency_code->getDecimalMark(),
24
+            'thousands_separator' => $currency_code->getThousandsSeparator(),
24
             'enabled' => !$hasDefaultCurrency,
25
             'enabled' => !$hasDefaultCurrency,
25
         ]);
26
         ]);
26
     }
27
     }

+ 35
- 0
app/Casts/MoneyCast.php View File

1
+<?php
2
+
3
+namespace App\Casts;
4
+
5
+use App\Models\Setting\Currency;
6
+use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
7
+use UnexpectedValueException;
8
+
9
+class MoneyCast implements CastsAttributes
10
+{
11
+    public function get($model, string $key, $value, array $attributes): string
12
+    {
13
+        $currency_code = $model->currency_code;
14
+
15
+        return money($value, $currency_code)->formatSimple();
16
+    }
17
+
18
+    /**
19
+     * @throws UnexpectedValueException
20
+     */
21
+    public function set($model, string $key, $value, array $attributes): int
22
+    {
23
+        if (is_int($value)) {
24
+            return $value;
25
+        }
26
+
27
+        $currency_code = $model->currency_code ?? Currency::getDefaultCurrencyCode();
28
+
29
+        if (!$currency_code) {
30
+            throw new UnexpectedValueException('Currency code is not set');
31
+        }
32
+
33
+        return money($value, $currency_code, true)->getAmount();
34
+    }
35
+}

+ 24
- 0
app/Casts/RateCast.php View File

1
+<?php
2
+
3
+namespace App\Casts;
4
+
5
+use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
6
+
7
+class RateCast implements CastsAttributes
8
+{
9
+    private const SCALE = 8;
10
+
11
+    public function get($model, string $key, $value, array $attributes): float
12
+    {
13
+        $floatValue = $value / (10 ** self::SCALE);
14
+
15
+        $strValue = rtrim(rtrim(number_format($floatValue, self::SCALE, '.', ''), '0'), '.');
16
+
17
+        return (float) $strValue;
18
+    }
19
+
20
+    public function set($model, string $key, $value, array $attributes): int
21
+    {
22
+        return (int) round($value * (10 ** self::SCALE));
23
+    }
24
+}

+ 14
- 0
app/Casts/TrimLeadingZeroCast.php View File

1
+<?php
2
+
3
+namespace App\Casts;
4
+
5
+use Illuminate\Contracts\Database\Eloquent\CastsInboundAttributes;
6
+use Illuminate\Database\Eloquent\Model;
7
+
8
+class TrimLeadingZeroCast implements CastsInboundAttributes
9
+{
10
+    public function set(Model $model, string $key, mixed $value, array $attributes): int
11
+    {
12
+        return (int) ltrim($value, '0');
13
+    }
14
+}

+ 18
- 0
app/Enums/CategoryType.php View File

1
+<?php
2
+
3
+namespace App\Enums;
4
+
5
+use Filament\Support\Contracts\HasLabel;
6
+
7
+enum CategoryType: string implements HasLabel
8
+{
9
+    case Expense = 'expense';
10
+    case Income = 'income';
11
+    case Item = 'item';
12
+    case Other = 'other';
13
+
14
+    public function getLabel(): ?string
15
+    {
16
+        return $this->name;
17
+    }
18
+}

+ 32
- 0
app/Enums/Concerns/Utilities.php View File

1
+<?php
2
+
3
+namespace App\Enums\Concerns;
4
+
5
+trait Utilities
6
+{
7
+    public static function caseValues(): array
8
+    {
9
+        return array_column(static::cases(), 'value');
10
+    }
11
+
12
+    public static function caseNames(): array
13
+    {
14
+        return array_column(static::cases(), 'name');
15
+    }
16
+
17
+    public static function constantNames(): array
18
+    {
19
+        $allConstants = array_keys((new \ReflectionClass(static::class))->getConstants());
20
+        $caseNames = static::caseNames();
21
+
22
+        return array_values(array_diff($allConstants, $caseNames));
23
+    }
24
+
25
+    public static function constantValues(): array
26
+    {
27
+        $allConstants = array_values((new \ReflectionClass(static::class))->getConstants());
28
+        $caseValues = static::caseValues();
29
+
30
+        return array_values(array_diff_key($allConstants, $caseValues));
31
+    }
32
+}

+ 16
- 0
app/Enums/DiscountComputation.php View File

1
+<?php
2
+
3
+namespace App\Enums;
4
+
5
+use Filament\Support\Contracts\HasLabel;
6
+
7
+enum DiscountComputation: string implements HasLabel
8
+{
9
+    case Percentage = 'percentage';
10
+    case Fixed = 'fixed';
11
+
12
+    public function getLabel(): ?string
13
+    {
14
+        return $this->name;
15
+    }
16
+}

+ 16
- 0
app/Enums/DiscountScope.php View File

1
+<?php
2
+
3
+namespace App\Enums;
4
+
5
+use Filament\Support\Contracts\HasLabel;
6
+
7
+enum DiscountScope: string implements HasLabel
8
+{
9
+    case Product = 'product';
10
+    case Service = 'service';
11
+
12
+    public function getLabel(): ?string
13
+    {
14
+        return $this->name;
15
+    }
16
+}

+ 37
- 0
app/Enums/DiscountType.php View File

1
+<?php
2
+
3
+namespace App\Enums;
4
+
5
+use Filament\Support\Contracts\HasColor;
6
+use Filament\Support\Contracts\HasIcon;
7
+use Filament\Support\Contracts\HasLabel;
8
+
9
+enum DiscountType: string implements HasLabel, HasColor, HasIcon
10
+{
11
+    case Sales = 'sales';
12
+    case Purchase = 'purchase';
13
+    case None = 'none';
14
+
15
+    public function getLabel(): ?string
16
+    {
17
+        return $this->name;
18
+    }
19
+
20
+    public function getColor(): string|array|null
21
+    {
22
+        return match ($this) {
23
+            self::Sales => 'success',
24
+            self::Purchase => 'warning',
25
+            self::None => 'gray',
26
+        };
27
+    }
28
+
29
+    public function getIcon(): ?string
30
+    {
31
+        return match ($this) {
32
+            self::Sales => 'heroicon-o-currency-dollar',
33
+            self::Purchase => 'heroicon-o-shopping-bag',
34
+            self::None => 'heroicon-o-x-circle',
35
+        };
36
+    }
37
+}

+ 19
- 0
app/Enums/DocumentAmountColumn.php View File

1
+<?php
2
+
3
+namespace App\Enums;
4
+
5
+use Filament\Support\Contracts\HasLabel;
6
+
7
+enum DocumentAmountColumn: string implements HasLabel
8
+{
9
+    case Amount = 'amount';
10
+    case Total = 'total';
11
+    case Other = 'other';
12
+
13
+    public const DEFAULT = self::Amount->value;
14
+
15
+    public function getLabel(): ?string
16
+    {
17
+        return $this->name;
18
+    }
19
+}

+ 23
- 0
app/Enums/DocumentItemColumn.php View File

1
+<?php
2
+
3
+namespace App\Enums;
4
+
5
+use App\Enums\Concerns\Utilities;
6
+use Filament\Support\Contracts\HasLabel;
7
+
8
+enum DocumentItemColumn: string implements HasLabel
9
+{
10
+    use Utilities;
11
+
12
+    case Items = 'items';
13
+    case Products = 'products';
14
+    case Services = 'services';
15
+    case Other = 'other';
16
+
17
+    public const DEFAULT = self::Items->value;
18
+
19
+    public function getLabel(): ?string
20
+    {
21
+        return $this->name;
22
+    }
23
+}

+ 19
- 0
app/Enums/DocumentPriceColumn.php View File

1
+<?php
2
+
3
+namespace App\Enums;
4
+
5
+use Filament\Support\Contracts\HasLabel;
6
+
7
+enum DocumentPriceColumn: string implements HasLabel
8
+{
9
+    case Price = 'price';
10
+    case Rate = 'rate';
11
+    case Other = 'other';
12
+
13
+    public const DEFAULT = self::Price->value;
14
+
15
+    public function getLabel(): ?string
16
+    {
17
+        return $this->name;
18
+    }
19
+}

+ 27
- 0
app/Enums/DocumentType.php View File

1
+<?php
2
+
3
+namespace App\Enums;
4
+
5
+use Filament\Support\Contracts\HasIcon;
6
+use Filament\Support\Contracts\HasLabel;
7
+
8
+enum DocumentType: string implements HasLabel, HasIcon
9
+{
10
+    case Invoice = 'invoice';
11
+    case Bill = 'bill';
12
+
13
+    public const DEFAULT = self::Invoice->value;
14
+
15
+    public function getLabel(): ?string
16
+    {
17
+        return $this->name;
18
+    }
19
+
20
+    public function getIcon(): ?string
21
+    {
22
+        return match ($this->value) {
23
+            self::Invoice->value => 'heroicon-o-document-duplicate',
24
+            self::Bill->value => 'heroicon-o-clipboard-document-list',
25
+        };
26
+    }
27
+}

+ 19
- 0
app/Enums/DocumentUnitColumn.php View File

1
+<?php
2
+
3
+namespace App\Enums;
4
+
5
+use Filament\Support\Contracts\HasLabel;
6
+
7
+enum DocumentUnitColumn: string implements HasLabel
8
+{
9
+    case Quantity = 'quantity';
10
+    case Hours = 'hours';
11
+    case Other = 'other';
12
+
13
+    public const DEFAULT = self::Quantity->value;
14
+
15
+    public function getLabel(): ?string
16
+    {
17
+        return $this->name;
18
+    }
19
+}

+ 30
- 0
app/Enums/EntityType.php View File

1
+<?php
2
+
3
+namespace App\Enums;
4
+
5
+use Filament\Support\Contracts\HasLabel;
6
+
7
+enum EntityType: string implements HasLabel
8
+{
9
+    case SoleProprietorship = 'sole_proprietorship';
10
+    case GeneralPartnership = 'general_partnership';
11
+    case LimitedPartnership = 'limited_partnership';
12
+    case LimitedLiabilityPartnership = 'limited_liability_partnership';
13
+    case LimitedLiabilityCompany = 'limited_liability_company';
14
+    case Corporation = 'corporation';
15
+    case Nonprofit = 'nonprofit';
16
+
17
+    public function getLabel(): ?string
18
+    {
19
+        return match ($this) {
20
+            self::SoleProprietorship => 'Sole Proprietorship',
21
+            self::GeneralPartnership => 'General Partnership',
22
+            self::LimitedPartnership => 'Limited Partnership (LP)',
23
+            self::LimitedLiabilityPartnership => 'Limited Liability Partnership (LLP)',
24
+            self::LimitedLiabilityCompany => 'Limited Liability Company (LLC)',
25
+            self::Corporation => 'Corporation',
26
+            self::Nonprofit => 'Nonprofit',
27
+        };
28
+    }
29
+
30
+}

+ 26
- 0
app/Enums/Font.php View File

1
+<?php
2
+
3
+namespace App\Enums;
4
+
5
+use Filament\Support\Contracts\HasLabel;
6
+
7
+enum Font: string implements HasLabel
8
+{
9
+    case Inter = 'inter';
10
+    case Roboto = 'roboto';
11
+    case OpenSans = 'open_sans';
12
+    case Poppins = 'poppins';
13
+    case NotoSans = 'noto_sans';
14
+    case DMSans = 'dm_sans';
15
+    case Arial = 'arial';
16
+    case Helvetica = 'helvetica';
17
+    case Verdana = 'verdana';
18
+    case Rubik = 'rubik';
19
+
20
+    public const DEFAULT = self::Inter->value;
21
+
22
+    public function getLabel(): ?string
23
+    {
24
+        return ucwords(str_replace('_', ' ', $this->value));
25
+    }
26
+}

+ 33
- 0
app/Enums/MaxContentWidth.php View File

1
+<?php
2
+
3
+namespace App\Enums;
4
+
5
+use Filament\Support\Contracts\HasLabel;
6
+
7
+enum MaxContentWidth: string implements HasLabel
8
+{
9
+    case FOUR_XL = '4xl';
10
+    case FIVE_XL = '5xl';
11
+    case SIX_XL = '6xl';
12
+    case SEVEN_XL = '7xl';
13
+    case SCREEN_LG = 'screen-lg';
14
+    case SCREEN_XL = 'screen-xl';
15
+    case SCREEN_2XL = 'screen-2xl';
16
+    case FULL = 'full';
17
+
18
+    public const DEFAULT = self::SEVEN_XL->value;
19
+
20
+    public function getLabel(): ?string
21
+    {
22
+        return match ($this) {
23
+            self::FOUR_XL => '4X Large',
24
+            self::FIVE_XL => '5X Large',
25
+            self::SIX_XL => '6X Large',
26
+            self::SEVEN_XL => '7X Large',
27
+            self::SCREEN_LG => 'Screen Large',
28
+            self::SCREEN_XL => 'Screen Extra Large',
29
+            self::SCREEN_2XL => 'Screen 2X Large',
30
+            self::FULL => 'Full',
31
+        };
32
+    }
33
+}

+ 41
- 0
app/Enums/ModalWidth.php View File

1
+<?php
2
+
3
+namespace App\Enums;
4
+
5
+use Filament\Support\Contracts\HasLabel;
6
+
7
+enum ModalWidth: string implements HasLabel
8
+{
9
+    case XS = 'xs';
10
+    case SM = 'sm';
11
+    case MD = 'md';
12
+    case LG = 'lg';
13
+    case XL = 'xl';
14
+    case TWO_XL = '2xl';
15
+    case THREE_XL = '3xl';
16
+    case FOUR_XL = '4xl';
17
+    case FIVE_XL = '5xl';
18
+    case SIX_XL = '6xl';
19
+    case SEVEN_XL = '7xl';
20
+    case SCREEN = 'screen';
21
+
22
+    public const DEFAULT = self::MD->value;
23
+
24
+    public function getLabel(): ?string
25
+    {
26
+        return match ($this) {
27
+            self::XS => 'Extra Small',
28
+            self::SM => 'Small',
29
+            self::MD => 'Medium',
30
+            self::LG => 'Large',
31
+            self::XL => 'Extra Large',
32
+            self::TWO_XL => '2X Large',
33
+            self::THREE_XL => '3X Large',
34
+            self::FOUR_XL => '4X Large',
35
+            self::FIVE_XL => '5X Large',
36
+            self::SIX_XL => '6X Large',
37
+            self::SEVEN_XL => '7X Large',
38
+            self::SCREEN => 'Screen',
39
+        };
40
+    }
41
+}

+ 44
- 0
app/Enums/PaymentTerms.php View File

1
+<?php
2
+
3
+namespace App\Enums;
4
+
5
+use Filament\Support\Contracts\HasLabel;
6
+
7
+enum PaymentTerms: string implements HasLabel
8
+{
9
+    case DueOnReceipt = 'due_on_receipt';
10
+    case Net7 = 'net_7';
11
+    case Net10 = 'net_10';
12
+    case Net15 = 'net_15';
13
+    case Net30 = 'net_30';
14
+    case Net60 = 'net_60';
15
+    case Net90 = 'net_90';
16
+
17
+    public const DEFAULT = self::DueOnReceipt->value;
18
+
19
+    public function getLabel(): ?string
20
+    {
21
+        return match ($this) {
22
+            self::DueOnReceipt => 'Due on Receipt',
23
+            self::Net7 => 'Net 7',
24
+            self::Net10 => 'Net 10',
25
+            self::Net15 => 'Net 15',
26
+            self::Net30 => 'Net 30',
27
+            self::Net60 => 'Net 60',
28
+            self::Net90 => 'Net 90',
29
+        };
30
+    }
31
+
32
+    public function getDays(): int
33
+    {
34
+        return match ($this) {
35
+            self::DueOnReceipt => 0,
36
+            self::Net7 => 7,
37
+            self::Net10 => 10,
38
+            self::Net15 => 15,
39
+            self::Net30 => 30,
40
+            self::Net60 => 60,
41
+            self::Net90 => 90,
42
+        };
43
+    }
44
+}

+ 83
- 0
app/Enums/PrimaryColor.php View File

1
+<?php
2
+
3
+namespace App\Enums;
4
+
5
+use App\Enums\Concerns\Utilities;
6
+use Filament\Support\Colors\Color;
7
+use Filament\Support\Contracts\HasColor;
8
+use Spatie\Color\Rgb;
9
+use UnexpectedValueException;
10
+
11
+enum PrimaryColor: string implements HasColor
12
+{
13
+    use Utilities;
14
+
15
+    case Slate = 'slate';
16
+    case Gray = 'gray';
17
+    case Zinc = 'zinc';
18
+    case Neutral = 'neutral';
19
+    case Stone = 'stone';
20
+    case Red = 'red';
21
+    case Orange = 'orange';
22
+    case Amber = 'amber';
23
+    case Yellow = 'yellow';
24
+    case Lime = 'lime';
25
+    case Green = 'green';
26
+    case Emerald = 'emerald';
27
+    case Teal = 'teal';
28
+    case Cyan = 'cyan';
29
+    case Sky = 'sky';
30
+    case Blue = 'blue';
31
+    case Indigo = 'indigo';
32
+    case Violet = 'violet';
33
+    case Purple = 'purple';
34
+    case Fuchsia = 'fuchsia';
35
+    case Pink = 'pink';
36
+    case Rose = 'rose';
37
+
38
+    public const DEFAULT = self::Indigo->value;
39
+
40
+    public function getColor(): string|array|null
41
+    {
42
+        return match ($this) {
43
+            self::Slate => Color::Slate,
44
+            self::Gray => Color::Gray,
45
+            self::Zinc => Color::Zinc,
46
+            self::Neutral => Color::Neutral,
47
+            self::Stone => Color::Stone,
48
+            self::Red => Color::Red,
49
+            self::Orange => Color::Orange,
50
+            self::Amber => Color::Amber,
51
+            self::Yellow => Color::Yellow,
52
+            self::Lime => Color::Lime,
53
+            self::Green => Color::Green,
54
+            self::Emerald => Color::Emerald,
55
+            self::Teal => Color::Teal,
56
+            self::Cyan => Color::Cyan,
57
+            self::Sky => Color::Sky,
58
+            self::Blue => Color::Blue,
59
+            self::Indigo => Color::Indigo,
60
+            self::Violet => Color::Violet,
61
+            self::Purple => Color::Purple,
62
+            self::Fuchsia => Color::Fuchsia,
63
+            self::Pink => Color::Pink,
64
+            self::Rose => Color::Rose,
65
+        };
66
+    }
67
+
68
+    /**
69
+     * @throws UnexpectedValueException
70
+     */
71
+    public function getHexCode(): string
72
+    {
73
+        $colorArray = $this->getColor();
74
+
75
+        if ($colorArray !== null && isset($colorArray[600])) {
76
+            $rgbToString = $colorArray[600];
77
+
78
+            return Rgb::fromString("rgb({$rgbToString})")->toHex();
79
+        }
80
+
81
+        throw new UnexpectedValueException("The color {$this->value} does not have a hex code.");
82
+    }
83
+}

+ 29
- 0
app/Enums/RecordsPerPage.php View File

1
+<?php
2
+
3
+namespace App\Enums;
4
+
5
+use App\Enums\Concerns\Utilities;
6
+use Filament\Support\Contracts\HasLabel;
7
+
8
+enum RecordsPerPage: int implements HasLabel
9
+{
10
+    use Utilities;
11
+    case Five = 5;
12
+    case Ten = 10;
13
+    case TwentyFive = 25;
14
+    case Fifty = 50;
15
+    case OneHundred = 100;
16
+
17
+    public const DEFAULT = self::Ten->value;
18
+
19
+    public const FIVE = self::Five->value;
20
+    public const TEN = self::Ten->value;
21
+    public const TWENTY_FIVE = self::TwentyFive->value;
22
+    public const FIFTY = self::Fifty->value;
23
+    public const ONE_HUNDRED = self::OneHundred->value;
24
+
25
+    public function getLabel(): ?string
26
+    {
27
+        return (string)$this->value;
28
+    }
29
+}

+ 18
- 0
app/Enums/TableSortDirection.php View File

1
+<?php
2
+
3
+namespace App\Enums;
4
+
5
+use Filament\Support\Contracts\HasLabel;
6
+
7
+enum TableSortDirection: string implements HasLabel
8
+{
9
+    case Ascending = 'asc';
10
+    case Descending = 'desc';
11
+
12
+    public const DEFAULT = self::Ascending->value;
13
+
14
+    public function getLabel(): ?string
15
+    {
16
+        return $this->name;
17
+    }
18
+}

+ 17
- 0
app/Enums/TaxComputation.php View File

1
+<?php
2
+
3
+namespace App\Enums;
4
+
5
+use Filament\Support\Contracts\HasLabel;
6
+
7
+enum TaxComputation: string implements HasLabel
8
+{
9
+    case Fixed = 'fixed';
10
+    case Percentage = 'percentage';
11
+    case Compound = 'compound';
12
+
13
+    public function getLabel(): ?string
14
+    {
15
+        return $this->name;
16
+    }
17
+}

+ 16
- 0
app/Enums/TaxScope.php View File

1
+<?php
2
+
3
+namespace App\Enums;
4
+
5
+use Filament\Support\Contracts\HasLabel;
6
+
7
+enum TaxScope: string implements HasLabel
8
+{
9
+    case Product = 'product';
10
+    case Service = 'service';
11
+
12
+    public function getLabel(): ?string
13
+    {
14
+        return $this->name;
15
+    }
16
+}

+ 37
- 0
app/Enums/TaxType.php View File

1
+<?php
2
+
3
+namespace App\Enums;
4
+
5
+use Filament\Support\Contracts\HasColor;
6
+use Filament\Support\Contracts\HasIcon;
7
+use Filament\Support\Contracts\HasLabel;
8
+
9
+enum TaxType: string implements HasLabel, HasColor, HasIcon
10
+{
11
+    case Sales = 'sales';
12
+    case Purchase = 'purchase';
13
+    case None = 'none';
14
+
15
+    public function getLabel(): ?string
16
+    {
17
+        return$this->name;
18
+    }
19
+
20
+    public function getColor(): string|array|null
21
+    {
22
+        return match ($this) {
23
+            self::Sales => 'success',
24
+            self::Purchase => 'warning',
25
+            self::None => 'gray',
26
+        };
27
+    }
28
+
29
+    public function getIcon(): ?string
30
+    {
31
+        return match ($this) {
32
+            self::Sales => 'heroicon-o-currency-dollar',
33
+            self::Purchase => 'heroicon-o-shopping-bag',
34
+            self::None => 'heroicon-o-x-circle',
35
+        };
36
+    }
37
+}

+ 19
- 0
app/Enums/Template.php View File

1
+<?php
2
+
3
+namespace App\Enums;
4
+
5
+use Filament\Support\Contracts\HasLabel;
6
+
7
+enum Template: string implements HasLabel
8
+{
9
+    case Default = 'default';
10
+    case Modern = 'modern';
11
+    case Classic = 'classic';
12
+
13
+    public const DEFAULT = self::Default->value;
14
+
15
+    public function getLabel(): ?string
16
+    {
17
+        return $this->name;
18
+    }
19
+}

+ 23
- 0
app/Events/CompanyDefaultEvent.php View File

1
+<?php
2
+
3
+namespace App\Events;
4
+
5
+use Illuminate\Broadcasting\InteractsWithSockets;
6
+use Illuminate\Database\Eloquent\Model;
7
+use Illuminate\Foundation\Events\Dispatchable;
8
+use Illuminate\Queue\SerializesModels;
9
+
10
+class CompanyDefaultEvent
11
+{
12
+    use Dispatchable, InteractsWithSockets, SerializesModels;
13
+
14
+    public Model $model;
15
+
16
+    /**
17
+     * Create a new event instance.
18
+     */
19
+    public function __construct(Model $model)
20
+    {
21
+        $this->model = $model;
22
+    }
23
+}

+ 29
- 0
app/Events/CompanyDefaultUpdated.php View File

1
+<?php
2
+
3
+namespace App\Events;
4
+
5
+use Illuminate\Broadcasting\Channel;
6
+use Illuminate\Broadcasting\InteractsWithSockets;
7
+use Illuminate\Broadcasting\PresenceChannel;
8
+use Illuminate\Broadcasting\PrivateChannel;
9
+use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
10
+use Illuminate\Database\Eloquent\Model;
11
+use Illuminate\Foundation\Events\Dispatchable;
12
+use Illuminate\Queue\SerializesModels;
13
+
14
+class CompanyDefaultUpdated
15
+{
16
+    use Dispatchable, InteractsWithSockets, SerializesModels;
17
+
18
+    public Model $record;
19
+    public array $data;
20
+
21
+    /**
22
+     * Create a new event instance.
23
+     */
24
+    public function __construct(Model $record, array $data)
25
+    {
26
+        $this->record = $record;
27
+        $this->data = $data;
28
+    }
29
+}

+ 8
- 0
app/Events/UpdateCompanyDefault.php View File

1
+<?php
2
+
3
+namespace App\Events;
4
+
5
+class UpdateCompanyDefault
6
+{
7
+
8
+}

+ 77
- 0
app/Filament/Company/Pages/CreateCompany.php View File

1
+<?php
2
+
3
+namespace App\Filament\Company\Pages;
4
+
5
+use App\Enums\EntityType;
6
+use App\Models\Setting\CompanyProfile;
7
+use Filament\Forms\Components\Select;
8
+use Filament\Forms\Components\TextInput;
9
+use Filament\Forms\Form;
10
+use Illuminate\Database\Eloquent\Model;
11
+use Illuminate\Support\Facades\Auth;
12
+use Illuminate\Support\Facades\Gate;
13
+use Wallo\FilamentCompanies\Events\AddingCompany;
14
+use Wallo\FilamentCompanies\FilamentCompanies;
15
+use Wallo\FilamentCompanies\Pages\Company\CreateCompany as FilamentCreateCompany;
16
+
17
+class CreateCompany extends FilamentCreateCompany
18
+{
19
+    public function form(Form $form): Form
20
+    {
21
+        return $form
22
+            ->schema([
23
+                TextInput::make('name')
24
+                    ->label(__('filament-companies::default.labels.company_name'))
25
+                    ->autofocus()
26
+                    ->maxLength(255)
27
+                    ->required(),
28
+                TextInput::make('profile.email')
29
+                    ->label('Company Email')
30
+                    ->email()
31
+                    ->required(),
32
+                Select::make('profile.entity_type')
33
+                    ->label('Entity Type')
34
+                    ->native(false)
35
+                    ->options(EntityType::class)
36
+                    ->required(),
37
+                Select::make('profile.country')
38
+                    ->label('Country')
39
+                    ->native(false)
40
+                    ->searchable()
41
+                    ->options(CompanyProfile::getAvailableCountryOptions())
42
+                    ->required(),
43
+            ])
44
+            ->model(FilamentCompanies::companyModel())
45
+            ->statePath('data');
46
+    }
47
+
48
+    protected function handleRegistration(array $data): Model
49
+    {
50
+        $user = Auth::user();
51
+
52
+        Gate::forUser($user)->authorize('create', FilamentCompanies::newCompanyModel());
53
+
54
+        AddingCompany::dispatch($user);
55
+
56
+        $personalCompany = $user?->personalCompany() === null;
57
+
58
+        $company = $user?->ownedCompanies()->create([
59
+            'name' => $data['name'],
60
+            'personal_company' => $personalCompany,
61
+        ]);
62
+
63
+        $company->profile()->create([
64
+            'email' => $data['profile']['email'],
65
+            'entity_type' => $data['profile']['entity_type'],
66
+            'country' => $data['profile']['country'],
67
+        ]);
68
+
69
+        $user?->switchCompany($company);
70
+
71
+        $name = $data['name'];
72
+
73
+        $this->companyCreated($name);
74
+
75
+        return $company;
76
+    }
77
+}

+ 282
- 0
app/Filament/Company/Pages/Setting/Appearance.php View File

1
+<?php
2
+
3
+namespace App\Filament\Company\Pages\Setting;
4
+
5
+use App\Enums\Font;
6
+use App\Enums\MaxContentWidth;
7
+use App\Enums\ModalWidth;
8
+use App\Enums\PrimaryColor;
9
+use App\Enums\RecordsPerPage;
10
+use App\Enums\TableSortDirection;
11
+use App\Models\Setting\Appearance as AppearanceModel;
12
+use Filament\Actions\Action;
13
+use Filament\Actions\ActionGroup;
14
+use Filament\Forms\Components\Component;
15
+use Filament\Forms\Components\Section;
16
+use Filament\Forms\Components\Select;
17
+use Filament\Forms\Form;
18
+use Filament\Notifications\Notification;
19
+use Filament\Pages\Concerns\InteractsWithFormActions;
20
+use Filament\Pages\Page;
21
+use Filament\Support\Exceptions\Halt;
22
+use Illuminate\Auth\Access\AuthorizationException;
23
+use Illuminate\Database\Eloquent\Model;
24
+use Livewire\Attributes\Locked;
25
+use Wallo\FilamentSelectify\Components\ButtonGroup;
26
+use Wallo\FilamentSelectify\Components\ToggleButton;
27
+use function Filament\authorize;
28
+
29
+/**
30
+ * @property Form $form
31
+ */
32
+class Appearance extends Page
33
+{
34
+    use InteractsWithFormActions;
35
+
36
+    protected static ?string $navigationIcon = 'heroicon-o-paint-brush';
37
+
38
+    protected static ?string $navigationLabel = 'Appearance';
39
+
40
+    protected static ?string $navigationGroup = 'Settings';
41
+
42
+    protected static ?string $slug = 'settings/appearance';
43
+
44
+    protected ?string $heading = 'Appearance';
45
+
46
+    protected static string $view = 'filament.company.pages.setting.appearance';
47
+
48
+    public ?array $data = [];
49
+
50
+    #[Locked]
51
+    public ?AppearanceModel $record = null;
52
+
53
+    public function mount(): void
54
+    {
55
+        $this->record = AppearanceModel::firstOrNew([
56
+            'company_id' => auth()->user()->currentCompany->id,
57
+        ]);
58
+
59
+        abort_unless(static::canView($this->record), 404);
60
+
61
+        $this->fillForm();
62
+    }
63
+
64
+    public function fillForm(): void
65
+    {
66
+        $data = $this->record->attributesToArray();
67
+
68
+        $data = $this->mutateFormDataBeforeFill($data);
69
+
70
+        $this->form->fill($data);
71
+    }
72
+
73
+    protected function mutateFormDataBeforeFill(array $data): array
74
+    {
75
+        return $data;
76
+    }
77
+
78
+    protected function mutateFormDataBeforeSave(array $data): array
79
+    {
80
+        return $data;
81
+    }
82
+
83
+    public function save(): void
84
+    {
85
+        try {
86
+            $data = $this->form->getState();
87
+
88
+            $data = $this->mutateFormDataBeforeSave($data);
89
+
90
+            $this->handleRecordUpdate($this->record, $data);
91
+
92
+        } catch (Halt $exception) {
93
+            return;
94
+        }
95
+
96
+        $this->getSavedNotification()?->send();
97
+
98
+        if ($redirectUrl = $this->getRedirectUrl()) {
99
+            $this->redirect($redirectUrl);
100
+        }
101
+    }
102
+
103
+    protected function getSavedNotification(): ?Notification
104
+    {
105
+        $title = $this->getSavedNotificationTitle();
106
+
107
+        if (blank($title)) {
108
+            return null;
109
+        }
110
+
111
+        return Notification::make()
112
+            ->success()
113
+            ->title($this->getSavedNotificationTitle());
114
+    }
115
+
116
+    protected function getSavedNotificationTitle(): ?string
117
+    {
118
+        return __('filament-panels::pages/tenancy/edit-tenant-profile.notifications.saved.title');
119
+    }
120
+
121
+    protected function getRedirectUrl(): ?string
122
+    {
123
+        return null;
124
+    }
125
+
126
+    public function form(Form $form): Form
127
+    {
128
+        return $form
129
+            ->schema([
130
+                $this->getGeneralSection(),
131
+                $this->getLayoutSection(),
132
+                $this->getDataPresentationSection(),
133
+            ])
134
+            ->model($this->record)
135
+            ->statePath('data')
136
+            ->operation('edit');
137
+    }
138
+
139
+    protected function getGeneralSection(): Component
140
+    {
141
+        return Section::make('General')
142
+            ->schema([
143
+                Select::make('primary_color')
144
+                    ->label('Primary Color')
145
+                    ->native(false)
146
+                    ->allowHtml()
147
+                    ->selectablePlaceholder(false)
148
+                    ->rule('required')
149
+                    ->options(collect(PrimaryColor::cases())
150
+                        ->mapWithKeys(static fn ($case) => [
151
+                            $case->value => "<span class='flex items-center gap-x-4'>
152
+                                <span class='rounded-full w-4 h-4' style='background:rgb(" . $case->getColor()[600] . ")'></span>
153
+                                <span>" . str($case->value)->title() . "</span>
154
+                                </span>"
155
+                        ]),
156
+                    ),
157
+                Select::make('font')
158
+                    ->label('Font')
159
+                    ->native(false)
160
+                    ->selectablePlaceholder(false)
161
+                    ->rule('required')
162
+                    ->allowHtml()
163
+                    ->options(collect(Font::cases())
164
+                        ->mapWithKeys(static fn ($case) => [
165
+                            $case->value => "<span style='font-family:{$case->getLabel()}'>{$case->getLabel()}</span>"
166
+                        ]),
167
+                    ),
168
+            ])->columns();
169
+    }
170
+
171
+    protected function getLayoutSection(): Component
172
+    {
173
+        return Section::make('Layout')
174
+            ->schema([
175
+                Select::make('max_content_width')
176
+                    ->label('Max Content Width')
177
+                    ->native(false)
178
+                    ->selectablePlaceholder(false)
179
+                    ->rule('required')
180
+                    ->options(MaxContentWidth::class),
181
+                Select::make('modal_width')
182
+                    ->label('Modal Width')
183
+                    ->native(false)
184
+                    ->selectablePlaceholder(false)
185
+                    ->rule('required')
186
+                    ->options(ModalWidth::class),
187
+                ButtonGroup::make('has_top_navigation')
188
+                    ->label('Navigation Layout')
189
+                    ->boolean('Top Navigation', 'Side Navigation')
190
+                    ->rule('required'),
191
+                ToggleButton::make('is_table_striped')
192
+                    ->label('Striped Tables')
193
+                    ->onLabel('Enabled')
194
+                    ->offLabel('Disabled')
195
+                    ->rule('required'),
196
+            ])->columns();
197
+    }
198
+
199
+    protected function getDataPresentationSection(): Component
200
+    {
201
+        return Section::make('Data Presentation')
202
+            ->schema([
203
+                Select::make('table_sort_direction')
204
+                    ->label('Table Sort Direction')
205
+                    ->native(false)
206
+                    ->selectablePlaceholder(false)
207
+                    ->rule('required')
208
+                    ->options(TableSortDirection::class),
209
+                Select::make('records_per_page')
210
+                    ->label('Records Per Page')
211
+                    ->native(false)
212
+                    ->selectablePlaceholder(false)
213
+                    ->rule('required')
214
+                    ->options(RecordsPerPage::class),
215
+            ])->columns();
216
+    }
217
+
218
+    protected function handleRecordUpdate(AppearanceModel $record, array $data): AppearanceModel
219
+    {
220
+        $record_array = array_map('strval', $record->toArray());
221
+        $data_array = array_map('strval', $data);
222
+        $diff = array_diff_assoc($data_array, $record_array);
223
+
224
+        $keysToWatch = [
225
+            'primary_color',
226
+            'max_content_width',
227
+            'has_top_navigation',
228
+            'font',
229
+        ];
230
+
231
+        foreach ($diff as $key => $value) {
232
+            if (in_array($key, $keysToWatch, true)) {
233
+                $this->dispatch('appearanceUpdated');
234
+            }
235
+        }
236
+
237
+        // If the primary color or font has changed, we need to update the associated models accent_color column.
238
+        if (array_key_exists('primary_color', $diff) || array_key_exists('font', $diff)) {
239
+            $primaryColorToHex = PrimaryColor::from($data['primary_color'])->getHexCode();
240
+            $font = Font::from($data['font'])->value;
241
+            $this->record->company->defaultBill()->update([
242
+                'accent_color' => $primaryColorToHex,
243
+                'font' => $font,
244
+            ]);
245
+            $this->record->company->defaultInvoice()->update([
246
+                'accent_color' => $primaryColorToHex,
247
+                'font' => $font,
248
+            ]);
249
+        }
250
+
251
+        $record->update($data);
252
+
253
+        return $record;
254
+    }
255
+
256
+    /**
257
+     * @return array<Action | ActionGroup>
258
+     */
259
+    protected function getFormActions(): array
260
+    {
261
+        return [
262
+            $this->getSaveFormAction(),
263
+        ];
264
+    }
265
+
266
+    protected function getSaveFormAction(): Action
267
+    {
268
+        return Action::make('save')
269
+            ->label(__('filament-panels::pages/tenancy/edit-tenant-profile.form.actions.save.label'))
270
+            ->submit('save')
271
+            ->keyBindings(['mod+s']);
272
+    }
273
+
274
+    public static function canView(Model $record): bool
275
+    {
276
+        try {
277
+            return authorize('update', $record)->allowed();
278
+        } catch (AuthorizationException $exception) {
279
+            return $exception->toResponse()->allowed();
280
+        }
281
+    }
282
+}

+ 251
- 0
app/Filament/Company/Pages/Setting/CompanyDefault.php View File

1
+<?php
2
+
3
+namespace App\Filament\Company\Pages\Setting;
4
+
5
+use App\Events\CompanyDefaultUpdated;
6
+use App\Models\Setting\CompanyDefault as CompanyDefaultModel;
7
+use Filament\Actions\Action;
8
+use Filament\Actions\ActionGroup;
9
+use Filament\Forms\Components\Component;
10
+use Filament\Forms\Components\Section;
11
+use Filament\Forms\Components\Select;
12
+use Filament\Forms\Form;
13
+use Filament\Notifications\Notification;
14
+use Filament\Pages\Concerns\InteractsWithFormActions;
15
+use Filament\Pages\Page;
16
+use Filament\Support\Exceptions\Halt;
17
+use Illuminate\Auth\Access\AuthorizationException;
18
+use Illuminate\Database\Eloquent\Model;
19
+use Livewire\Attributes\Locked;
20
+use function Filament\authorize;
21
+
22
+/**
23
+ * @property Form $form
24
+ */
25
+class CompanyDefault extends Page
26
+{
27
+    use InteractsWithFormActions;
28
+
29
+    protected static ?string $navigationIcon = 'heroicon-o-adjustments-vertical';
30
+
31
+    protected static ?string $navigationLabel = 'Default';
32
+
33
+    protected static ?string $navigationGroup = 'Settings';
34
+
35
+    protected static ?string $slug = 'settings/default';
36
+
37
+    protected ?string $heading = 'Default';
38
+
39
+    protected static string $view = 'filament.company.pages.setting.company-default';
40
+
41
+    public ?array $data = [];
42
+
43
+    #[Locked]
44
+    public ?CompanyDefaultModel $record = null;
45
+
46
+    public function mount(): void
47
+    {
48
+        $this->record = CompanyDefaultModel::firstOrNew([
49
+            'company_id' => auth()->user()->currentCompany->id,
50
+        ]);
51
+
52
+        abort_unless(static::canView($this->record), 404);
53
+
54
+        $this->fillForm();
55
+    }
56
+
57
+    public function fillForm(): void
58
+    {
59
+        $data = $this->record->attributesToArray();
60
+
61
+        $data = $this->mutateFormDataBeforeFill($data);
62
+
63
+        $this->form->fill($data);
64
+    }
65
+
66
+    protected function mutateFormDataBeforeFill(array $data): array
67
+    {
68
+        return $data;
69
+    }
70
+
71
+    protected function mutateFormDataBeforeSave(array $data): array
72
+    {
73
+        return $data;
74
+    }
75
+
76
+    public function save(): void
77
+    {
78
+        try {
79
+            $data = $this->form->getState();
80
+
81
+            $data = $this->mutateFormDataBeforeSave($data);
82
+
83
+            $this->handleRecordUpdate($this->record, $data);
84
+
85
+        } catch (Halt $exception) {
86
+            return;
87
+        }
88
+
89
+        $this->getSavedNotification()?->send();
90
+
91
+        if ($redirectUrl = $this->getRedirectUrl()) {
92
+            $this->redirect($redirectUrl);
93
+        }
94
+    }
95
+
96
+    protected function getSavedNotification(): ?Notification
97
+    {
98
+        $title = $this->getSavedNotificationTitle();
99
+
100
+        if (blank($title)) {
101
+            return null;
102
+        }
103
+
104
+        return Notification::make()
105
+            ->success()
106
+            ->title($this->getSavedNotificationTitle());
107
+    }
108
+
109
+    protected function getSavedNotificationTitle(): ?string
110
+    {
111
+        return __('filament-panels::pages/tenancy/edit-tenant-profile.notifications.saved.title');
112
+    }
113
+
114
+    protected function getRedirectUrl(): ?string
115
+    {
116
+        return null;
117
+    }
118
+
119
+    public function form(Form $form): Form
120
+    {
121
+        return $form
122
+            ->schema([
123
+                $this->getGeneralSection(),
124
+                $this->getModifiersSection(),
125
+                $this->getCategoriesSection(),
126
+            ])
127
+            ->model($this->record)
128
+            ->statePath('data')
129
+            ->operation('edit');
130
+    }
131
+
132
+    protected function getGeneralSection(): Component
133
+    {
134
+        return Section::make('General')
135
+            ->schema([
136
+                Select::make('account_id')
137
+                    ->label('Account')
138
+                    ->relationship('account', 'name')
139
+                    ->saveRelationshipsUsing(null)
140
+                    ->selectablePlaceholder(false)
141
+                    ->searchable()
142
+                    ->preload(),
143
+                Select::make('currency_code')
144
+                    ->label('Currency')
145
+                    ->relationship('currency', 'code')
146
+                    ->saveRelationshipsUsing(null)
147
+                    ->selectablePlaceholder(false)
148
+                    ->rule('required')
149
+                    ->searchable()
150
+                    ->preload(),
151
+            ])->columns();
152
+    }
153
+
154
+    protected function getModifiersSection(): Component
155
+    {
156
+        return Section::make('Taxes & Discounts')
157
+            ->schema([
158
+                Select::make('sales_tax_id')
159
+                    ->label('Sales Tax')
160
+                    ->relationship('salesTax', 'name')
161
+                    ->saveRelationshipsUsing(null)
162
+                    ->selectablePlaceholder(false)
163
+                    ->rule('required')
164
+                    ->searchable()
165
+                    ->preload(),
166
+                Select::make('purchase_tax_id')
167
+                    ->label('Purchase Tax')
168
+                    ->relationship('purchaseTax', 'name')
169
+                    ->saveRelationshipsUsing(null)
170
+                    ->selectablePlaceholder(false)
171
+                    ->rule('required')
172
+                    ->searchable()
173
+                    ->preload(),
174
+                Select::make('sales_discount_id')
175
+                    ->label('Sales Discount')
176
+                    ->relationship('salesDiscount', 'name')
177
+                    ->saveRelationshipsUsing(null)
178
+                    ->selectablePlaceholder(false)
179
+                    ->rule('required')
180
+                    ->searchable()
181
+                    ->preload(),
182
+                Select::make('purchase_discount_id')
183
+                    ->label('Purchase Discount')
184
+                    ->relationship('purchaseDiscount', 'name')
185
+                    ->saveRelationshipsUsing(null)
186
+                    ->selectablePlaceholder(false)
187
+                    ->rule('required')
188
+                    ->searchable()
189
+                    ->preload(),
190
+            ])->columns();
191
+    }
192
+
193
+    protected function getCategoriesSection(): Component
194
+    {
195
+        return Section::make('Categories')
196
+            ->schema([
197
+                Select::make('income_category_id')
198
+                    ->label('Income Category')
199
+                    ->relationship('incomeCategory', 'name')
200
+                    ->saveRelationshipsUsing(null)
201
+                    ->selectablePlaceholder(false)
202
+                    ->rule('required')
203
+                    ->searchable()
204
+                    ->preload(),
205
+                Select::make('expense_category_id')
206
+                    ->label('Expense Category')
207
+                    ->relationship('expenseCategory', 'name')
208
+                    ->saveRelationshipsUsing(null)
209
+                    ->selectablePlaceholder(false)
210
+                    ->rule('required')
211
+                    ->searchable()
212
+                    ->preload(),
213
+            ])->columns();
214
+    }
215
+
216
+    protected function handleRecordUpdate(CompanyDefaultModel $record, array $data): CompanyDefaultModel
217
+    {
218
+        CompanyDefaultUpdated::dispatch($record, $data);
219
+
220
+        $record->update($data);
221
+
222
+        return $record;
223
+    }
224
+
225
+    /**
226
+     * @return array<Action | ActionGroup>
227
+     */
228
+    protected function getFormActions(): array
229
+    {
230
+        return [
231
+            $this->getSaveFormAction(),
232
+        ];
233
+    }
234
+
235
+    protected function getSaveFormAction(): Action
236
+    {
237
+        return Action::make('save')
238
+            ->label(__('filament-panels::pages/tenancy/edit-tenant-profile.form.actions.save.label'))
239
+            ->submit('save')
240
+            ->keyBindings(['mod+s']);
241
+    }
242
+
243
+    public static function canView(Model $record): bool
244
+    {
245
+        try {
246
+            return authorize('update', $record)->allowed();
247
+        } catch (AuthorizationException $exception) {
248
+            return $exception->toResponse()->allowed();
249
+        }
250
+    }
251
+}

+ 284
- 0
app/Filament/Company/Pages/Setting/CompanyProfile.php View File

1
+<?php
2
+
3
+namespace App\Filament\Company\Pages\Setting;
4
+
5
+use App\Enums\EntityType;
6
+use App\Events\CompanyDefaultUpdated;
7
+use App\Models\Setting\CompanyDefault as CompanyDefaultModel;
8
+use Filament\Actions\Action;
9
+use Filament\Actions\ActionGroup;
10
+use Filament\Forms\Components\Component;
11
+use Filament\Forms\Components\DatePicker;
12
+use Filament\Forms\Components\FileUpload;
13
+use Filament\Forms\Components\Group;
14
+use Filament\Forms\Components\Section;
15
+use Filament\Forms\Components\Select;
16
+use Filament\Forms\Components\TextInput;
17
+use Filament\Forms\Form;
18
+use Filament\Forms\Get;
19
+use Filament\Notifications\Notification;
20
+use Filament\Pages\Concerns\InteractsWithFormActions;
21
+use Filament\Pages\Page;
22
+use App\Models\Setting\CompanyProfile as CompanyProfileModel;
23
+use Filament\Support\Exceptions\Halt;
24
+use Illuminate\Auth\Access\AuthorizationException;
25
+use Illuminate\Database\Eloquent\Model;
26
+use Illuminate\Support\Facades\Auth;
27
+use Livewire\Attributes\Locked;
28
+use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
29
+use function Filament\authorize;
30
+
31
+/**
32
+ * @property Form $form
33
+ */
34
+class CompanyProfile extends Page
35
+{
36
+    use InteractsWithFormActions;
37
+
38
+    protected static ?string $navigationIcon = 'heroicon-o-building-office-2';
39
+
40
+    protected static ?string $navigationLabel = 'Company Profile';
41
+
42
+    protected static ?string $navigationGroup = 'Settings';
43
+
44
+    protected static ?string $slug = 'settings/company-profile';
45
+
46
+    protected ?string $heading = 'Company Profile';
47
+
48
+    protected static string $view = 'filament.company.pages.setting.company-profile';
49
+
50
+    public ?array $data = [];
51
+
52
+    #[Locked]
53
+    public ?CompanyProfileModel $record = null;
54
+
55
+    public function mount(): void
56
+    {
57
+        $this->record = CompanyProfileModel::firstOrNew([
58
+            'company_id' => auth()->user()->currentCompany->id,
59
+        ]);
60
+
61
+        abort_unless(static::canView($this->record), 404);
62
+
63
+        $this->fillForm();
64
+    }
65
+
66
+    public function fillForm(): void
67
+    {
68
+        $data = $this->record->attributesToArray();
69
+
70
+        $data = $this->mutateFormDataBeforeFill($data);
71
+
72
+        $data['fiscal_year_start'] = now()->startOfYear()->toDateString();
73
+        $data['fiscal_year_end'] = now()->endOfYear()->toDateString();
74
+
75
+        $this->form->fill($data);
76
+    }
77
+
78
+    protected function mutateFormDataBeforeFill(array $data): array
79
+    {
80
+        return $data;
81
+    }
82
+
83
+    protected function mutateFormDataBeforeSave(array $data): array
84
+    {
85
+        return $data;
86
+    }
87
+
88
+    public function save(): void
89
+    {
90
+        try {
91
+            $data = $this->form->getState();
92
+
93
+            $data = $this->mutateFormDataBeforeSave($data);
94
+
95
+            $this->handleRecordUpdate($this->record, $data);
96
+
97
+        } catch (Halt $exception) {
98
+            return;
99
+        }
100
+
101
+        $this->getSavedNotification()?->send();
102
+
103
+        if ($redirectUrl = $this->getRedirectUrl()) {
104
+            $this->redirect($redirectUrl);
105
+        }
106
+    }
107
+
108
+    protected function getSavedNotification(): ?Notification
109
+    {
110
+        $title = $this->getSavedNotificationTitle();
111
+
112
+        if (blank($title)) {
113
+            return null;
114
+        }
115
+
116
+        return Notification::make()
117
+            ->success()
118
+            ->title($this->getSavedNotificationTitle());
119
+    }
120
+
121
+    protected function getSavedNotificationTitle(): ?string
122
+    {
123
+        return __('filament-panels::pages/tenancy/edit-tenant-profile.notifications.saved.title');
124
+    }
125
+
126
+    protected function getRedirectUrl(): ?string
127
+    {
128
+        return null;
129
+    }
130
+
131
+    public function form(Form $form): Form
132
+    {
133
+        return $form
134
+            ->schema([
135
+                $this->getIdentificationSection(),
136
+                $this->getLocationDetailsSection(),
137
+                $this->getLegalAndComplianceSection(),
138
+                $this->getFiscalYearSection(),
139
+            ])
140
+            ->model($this->record)
141
+            ->statePath('data')
142
+            ->operation('edit');
143
+    }
144
+
145
+    protected function getIdentificationSection(): Component
146
+    {
147
+        return Section::make('Identification')
148
+            ->schema([
149
+                FileUpload::make('logo')
150
+                    ->label('Logo')
151
+                    ->disk('public')
152
+                    ->directory('logos/company')
153
+                    ->imageResizeMode('contain')
154
+                    ->imagePreviewHeight('250')
155
+                    ->imageCropAspectRatio('2:1')
156
+                    ->getUploadedFileNameForStorageUsing(
157
+                        static fn (TemporaryUploadedFile $file): string => (string) str($file->getClientOriginalName())
158
+                            ->prepend(Auth::user()->currentCompany->id . '_'),
159
+                    )
160
+                    ->openable()
161
+                    ->maxSize(2048)
162
+                    ->image()
163
+                    ->visibility('public')
164
+                    ->acceptedFileTypes(['image/png', 'image/jpeg']),
165
+                Group::make()
166
+                    ->schema([
167
+                        TextInput::make('email')
168
+                            ->label('Email')
169
+                            ->email()
170
+                            ->maxLength(255)
171
+                            ->required(),
172
+                        TextInput::make('phone_number')
173
+                            ->label('Phone Number')
174
+                            ->tel()
175
+                            ->nullable(),
176
+                    ])->columns(1)
177
+            ])->columns();
178
+    }
179
+
180
+    protected function getLocationDetailsSection(): Component
181
+    {
182
+        return Section::make('Location Details')
183
+            ->schema([
184
+                Select::make('country')
185
+                    ->label('Country')
186
+                    ->native(false)
187
+                    ->live()
188
+                    ->searchable()
189
+                    ->options(CompanyProfileModel::getAvailableCountryOptions())
190
+                    ->required(),
191
+                Select::make('state')
192
+                    ->label('State / Province')
193
+                    ->searchable()
194
+                    ->native(false)
195
+                    ->options(static fn (Get $get) => CompanyProfileModel::getStateOptions($get('country')))
196
+                    ->nullable(),
197
+                Select::make('timezone')
198
+                    ->label('Timezone')
199
+                    ->native(false)
200
+                    ->options(static fn (Get $get) => CompanyProfileModel::getTimezoneOptions($get('country')))
201
+                    ->nullable(),
202
+                TextInput::make('address')
203
+                    ->label('Street Address')
204
+                    ->maxLength(255)
205
+                    ->nullable(),
206
+                TextInput::make('city')
207
+                    ->label('City / Town')
208
+                    ->maxLength(255)
209
+                    ->nullable(),
210
+                TextInput::make('zip_code')
211
+                    ->label('Zip Code')
212
+                    ->maxLength(20)
213
+                    ->nullable(),
214
+            ])->columns();
215
+    }
216
+
217
+    protected function getLegalAndComplianceSection(): Component
218
+    {
219
+        return Section::make('Legal & Compliance')
220
+            ->schema([
221
+                Select::make('entity_type')
222
+                    ->label('Entity Type')
223
+                    ->native(false)
224
+                    ->options(EntityType::class)
225
+                    ->required(),
226
+                TextInput::make('tax_id')
227
+                    ->label('Tax ID')
228
+                    ->maxLength(50)
229
+                    ->nullable(),
230
+            ])->columns();
231
+    }
232
+
233
+    protected function getFiscalYearSection(): Component
234
+    {
235
+        return Section::make('Fiscal Year')
236
+            ->schema([
237
+                DatePicker::make('fiscal_year_start')
238
+                    ->label('Start')
239
+                    ->native(false)
240
+                    ->seconds(false)
241
+                    ->rule('required'),
242
+                DatePicker::make('fiscal_year_end')
243
+                    ->label('End')
244
+                    ->minDate(static fn (Get $get) => $get('fiscal_year_start'))
245
+                    ->native(false)
246
+                    ->seconds(false)
247
+                    ->rule('required'),
248
+            ])->columns();
249
+    }
250
+
251
+    protected function handleRecordUpdate(CompanyProfileModel $record, array $data): CompanyProfileModel
252
+    {
253
+        $record->update($data);
254
+
255
+        return $record;
256
+    }
257
+
258
+    /**
259
+     * @return array<Action | ActionGroup>
260
+     */
261
+    protected function getFormActions(): array
262
+    {
263
+        return [
264
+            $this->getSaveFormAction(),
265
+        ];
266
+    }
267
+
268
+    protected function getSaveFormAction(): Action
269
+    {
270
+        return Action::make('save')
271
+            ->label(__('filament-panels::pages/tenancy/edit-tenant-profile.form.actions.save.label'))
272
+            ->submit('save')
273
+            ->keyBindings(['mod+s']);
274
+    }
275
+
276
+    public static function canView(Model $record): bool
277
+    {
278
+        try {
279
+            return authorize('update', $record)->allowed();
280
+        } catch (AuthorizationException $exception) {
281
+            return $exception->toResponse()->allowed();
282
+        }
283
+    }
284
+}

+ 334
- 0
app/Filament/Company/Pages/Setting/Invoice.php View File

1
+<?php
2
+
3
+namespace App\Filament\Company\Pages\Setting;
4
+
5
+use App\Enums\DocumentAmountColumn;
6
+use App\Enums\DocumentItemColumn;
7
+use App\Enums\DocumentPriceColumn;
8
+use App\Enums\DocumentType;
9
+use App\Enums\DocumentUnitColumn;
10
+use App\Enums\Font;
11
+use App\Enums\PaymentTerms;
12
+use App\Enums\Template;
13
+use App\Models\Setting\DocumentDefault as InvoiceModel;
14
+use Filament\Actions\Action;
15
+use Filament\Actions\ActionGroup;
16
+use Filament\Forms\Components\Checkbox;
17
+use Filament\Forms\Components\ColorPicker;
18
+use Filament\Forms\Components\Component;
19
+use Filament\Forms\Components\FileUpload;
20
+use Filament\Forms\Components\Group;
21
+use Filament\Forms\Components\Radio;
22
+use Filament\Forms\Components\Section;
23
+use Filament\Forms\Components\Select;
24
+use Filament\Forms\Components\Textarea;
25
+use Filament\Forms\Components\TextInput;
26
+use Filament\Forms\Components\ViewField;
27
+use Filament\Forms\Form;
28
+use Filament\Forms\Get;
29
+use Filament\Forms\Set;
30
+use Filament\Notifications\Notification;
31
+use Filament\Pages\Concerns\InteractsWithFormActions;
32
+use Filament\Pages\Page;
33
+use Filament\Support\Exceptions\Halt;
34
+use Illuminate\Auth\Access\AuthorizationException;
35
+use Illuminate\Database\Eloquent\Model;
36
+use Livewire\Attributes\Locked;
37
+use function Filament\authorize;
38
+
39
+/**
40
+ * @property Form $form
41
+ */
42
+class Invoice extends Page
43
+{
44
+    use InteractsWithFormActions;
45
+
46
+    protected static ?string $navigationIcon = 'heroicon-o-document-duplicate';
47
+
48
+    protected static ?string $navigationLabel = 'Invoice';
49
+
50
+    protected static ?string $navigationGroup = 'Settings';
51
+
52
+    protected static ?string $slug = 'settings/invoice';
53
+
54
+    protected ?string $heading = 'Invoice';
55
+
56
+    protected static string $view = 'filament.company.pages.setting.invoice';
57
+
58
+    public ?array $data = [];
59
+
60
+    public ?InvoiceModel $record = null;
61
+
62
+    public function mount(): void
63
+    {
64
+        $this->record = InvoiceModel::invoice()
65
+            ->firstOrNew([
66
+                'company_id' => auth()->user()->currentCompany->id,
67
+                'type' => DocumentType::Invoice->value,
68
+            ]);
69
+
70
+        abort_unless(static::canView($this->record), 404);
71
+
72
+        $this->fillForm();
73
+    }
74
+
75
+    public function fillForm(): void
76
+    {
77
+        $data = $this->record->attributesToArray();
78
+
79
+        $data = $this->mutateFormDataBeforeFill($data);
80
+
81
+        $this->form->fill($data);
82
+    }
83
+
84
+    protected function mutateFormDataBeforeFill(array $data): array
85
+    {
86
+        return $data;
87
+    }
88
+
89
+    protected function mutateFormDataBeforeSave(array $data): array
90
+    {
91
+        return $data;
92
+    }
93
+
94
+    public function save(): void
95
+    {
96
+        try {
97
+            $data = $this->form->getState();
98
+
99
+            $data = $this->mutateFormDataBeforeSave($data);
100
+
101
+            $this->handleRecordUpdate($this->record, $data);
102
+
103
+        } catch (Halt $exception) {
104
+            return;
105
+        }
106
+
107
+        $this->getSavedNotification()?->send();
108
+
109
+        if ($redirectUrl = $this->getRedirectUrl()) {
110
+            $this->redirect($redirectUrl);
111
+        }
112
+    }
113
+
114
+    protected function getSavedNotification(): ?Notification
115
+    {
116
+        $title = $this->getSavedNotificationTitle();
117
+
118
+        if (blank($title)) {
119
+            return null;
120
+        }
121
+
122
+        return Notification::make()
123
+            ->success()
124
+            ->title($this->getSavedNotificationTitle());
125
+    }
126
+
127
+    protected function getSavedNotificationTitle(): ?string
128
+    {
129
+        return __('filament-panels::pages/tenancy/edit-tenant-profile.notifications.saved.title');
130
+    }
131
+
132
+    protected function getRedirectUrl(): ?string
133
+    {
134
+        return null;
135
+    }
136
+
137
+    public function form(Form $form): Form
138
+    {
139
+        return $form
140
+            ->schema([
141
+                $this->getGeneralSection(),
142
+                $this->getContentSection(),
143
+                $this->getTemplateSection(),
144
+            ])
145
+            ->model($this->record)
146
+            ->statePath('data')
147
+            ->operation('edit');
148
+    }
149
+
150
+    protected function getGeneralSection(): Component
151
+    {
152
+        return Section::make('General')
153
+            ->schema([
154
+                TextInput::make('number_prefix')
155
+                    ->label('Number Prefix')
156
+                    ->live()
157
+                    ->required(),
158
+                Select::make('number_digits')
159
+                    ->label('Number Digits')
160
+                    ->options(InvoiceModel::availableNumberDigits())
161
+                    ->native(false)
162
+                    ->live()
163
+                    ->required(),
164
+                TextInput::make('number_next')
165
+                    ->label('Next Number')
166
+                    ->live()
167
+                    ->maxLength(static fn (Get $get) => $get('number_digits'))
168
+                    ->suffix(static function (Get $get, $state) {
169
+                        $number_prefix = $get('number_prefix');
170
+                        $number_digits = $get('number_digits');
171
+                        $number_next = $state;
172
+
173
+                        return InvoiceModel::getNumberNext(true, true, $number_prefix, $number_digits, $number_next);
174
+                    })
175
+                    ->required(),
176
+                Select::make('payment_terms')
177
+                    ->label('Payment Terms')
178
+                    ->options(PaymentTerms::class)
179
+                    ->native(false)
180
+                    ->live()
181
+                    ->required(),
182
+            ])->columns();
183
+    }
184
+
185
+    protected function getContentSection(): Component
186
+    {
187
+        return Section::make('Content')
188
+            ->schema([
189
+                TextInput::make('header')
190
+                    ->label('Header')
191
+                    ->live()
192
+                    ->required(),
193
+                TextInput::make('subheader')
194
+                    ->label('Subheader')
195
+                    ->live()
196
+                    ->nullable(),
197
+                Textarea::make('terms')
198
+                    ->label('Terms')
199
+                    ->live()
200
+                    ->nullable(),
201
+                Textarea::make('footer')
202
+                    ->label('Footer / Notes')
203
+                    ->live()
204
+                    ->nullable(),
205
+            ])->columns();
206
+    }
207
+
208
+    protected function getTemplateSection(): Component
209
+    {
210
+        return Section::make('Template')
211
+            ->description('Choose the template and edit the column names.')
212
+            ->schema([
213
+                Group::make()
214
+                    ->live()
215
+                    ->schema([
216
+                        FileUpload::make('logo')
217
+                            ->label('Logo')
218
+                            ->disk('public')
219
+                            ->directory('logos/documents')
220
+                            ->imageResizeMode('contain')
221
+                            ->imagePreviewHeight('250')
222
+                            ->imageCropAspectRatio('2:1')
223
+                            ->openable()
224
+                            ->preserveFilenames()
225
+                            ->visibility('public')
226
+                            ->image(),
227
+                        Checkbox::make('show_logo')
228
+                            ->label('Show Logo'),
229
+                        ColorPicker::make('accent_color')
230
+                            ->label('Accent Color'),
231
+                        Select::make('font')
232
+                            ->label('Font')
233
+                            ->native(false)
234
+                            ->selectablePlaceholder(false)
235
+                            ->rule('required')
236
+                            ->allowHtml()
237
+                            ->options(collect(Font::cases())
238
+                                ->mapWithKeys(static fn ($case) => [
239
+                                    $case->value => "<span style='font-family:{$case->getLabel()}'>{$case->getLabel()}</span>"
240
+                                ]),
241
+                            ),
242
+                        Select::make('template')
243
+                            ->label('Template')
244
+                            ->options(Template::class)
245
+                            ->required(),
246
+                        Select::make('item_name.option')
247
+                            ->label('Item Name')
248
+                            ->native(false)
249
+                            ->required()
250
+                            ->options(InvoiceModel::getAvailableItemNameOptions()),
251
+                        TextInput::make('item_name.custom')
252
+                            ->hiddenLabel()
253
+                            ->disabled(static fn (callable $get) => $get('item_name.option') !== 'other')
254
+                            ->nullable(),
255
+                        Select::make('unit_name.option')
256
+                            ->label('Unit Name')
257
+                            ->native(false)
258
+                            ->required()
259
+                            ->options(InvoiceModel::getAvailableUnitNameOptions()),
260
+                        TextInput::make('unit_name.custom')
261
+                            ->hiddenLabel()
262
+                            ->disabled(static fn (callable $get) => $get('unit_name.option') !== 'other')
263
+                            ->nullable(),
264
+                        Select::make('price_name.option')
265
+                            ->label('Price Name')
266
+                            ->native(false)
267
+                            ->required()
268
+                            ->options(InvoiceModel::getAvailablePriceNameOptions()),
269
+                        TextInput::make('price_name.custom')
270
+                            ->hiddenLabel()
271
+                            ->disabled(static fn (callable $get) => $get('price_name.option') !== 'other')
272
+                            ->nullable(),
273
+                        Select::make('amount_name.option')
274
+                            ->label('Amount Name')
275
+                            ->native(false)
276
+                            ->required()
277
+                            ->options(InvoiceModel::getAvailableAmountNameOptions()),
278
+                        TextInput::make('amount_name.custom')
279
+                            ->hiddenLabel()
280
+                            ->disabled(static fn (callable $get) => $get('amount_name.option') !== 'other')
281
+                            ->nullable(),
282
+                    ])->columns(1),
283
+                Group::make()
284
+                    ->schema([
285
+                        ViewField::make('preview.default')
286
+                            ->label('Preview')
287
+                            ->visible(static fn (callable $get) => $get('template') === 'default')
288
+                            ->view('components.invoice-layouts.default'),
289
+                        ViewField::make('preview.modern')
290
+                            ->label('Preview')
291
+                            ->visible(static fn (callable $get) => $get('template') === 'modern')
292
+                            ->view('components.invoice-layouts.modern'),
293
+                        ViewField::make('preview.classic')
294
+                            ->label('Preview')
295
+                            ->visible(static fn (callable $get) => $get('template') === 'classic')
296
+                            ->view('components.invoice-layouts.classic'),
297
+                    ])->columnSpan(2),
298
+            ])->columns(3);
299
+    }
300
+
301
+    protected function handleRecordUpdate(InvoiceModel $record, array $data): InvoiceModel
302
+    {
303
+        $record->update($data);
304
+
305
+        return $record;
306
+    }
307
+
308
+    /**
309
+     * @return array<Action | ActionGroup>
310
+     */
311
+    protected function getFormActions(): array
312
+    {
313
+        return [
314
+            $this->getSaveFormAction(),
315
+        ];
316
+    }
317
+
318
+    protected function getSaveFormAction(): Action
319
+    {
320
+        return Action::make('save')
321
+            ->label(__('filament-panels::pages/tenancy/edit-tenant-profile.form.actions.save.label'))
322
+            ->submit('save')
323
+            ->keyBindings(['mod+s']);
324
+    }
325
+
326
+    public static function canView(Model $record): bool
327
+    {
328
+        try {
329
+            return authorize('update', $record)->allowed();
330
+        } catch (AuthorizationException $exception) {
331
+            return $exception->toResponse()->allowed();
332
+        }
333
+    }
334
+}

app/Filament/Resources/AccountResource.php → app/Filament/Company/Resources/Banking/AccountResource.php View File

1
 <?php
1
 <?php
2
 
2
 
3
-namespace App\Filament\Resources;
3
+namespace App\Filament\Company\Resources\Banking;
4
 
4
 
5
 use App\Actions\OptionAction\CreateCurrency;
5
 use App\Actions\OptionAction\CreateCurrency;
6
-use App\Filament\Resources\AccountResource\Pages;
6
+use App\Filament\Company\Resources\Banking\AccountResource\Pages;
7
+use App\Filament\Company\Resources\Banking\AccountResource\RelationManagers;
7
 use App\Models\Banking\Account;
8
 use App\Models\Banking\Account;
8
 use App\Models\Setting\Currency;
9
 use App\Models\Setting\Currency;
9
 use App\Services\CurrencyService;
10
 use App\Services\CurrencyService;
10
-use Closure;
11
-use Exception;
11
+use App\Utilities\CurrencyConverter;
12
 use Filament\Forms;
12
 use Filament\Forms;
13
-use Filament\Forms\Components\TextInput\Mask;
13
+use Filament\Forms\Form;
14
 use Filament\Notifications\Notification;
14
 use Filament\Notifications\Notification;
15
-use Filament\Resources\Form;
16
 use Filament\Resources\Resource;
15
 use Filament\Resources\Resource;
17
-use Filament\Resources\Table;
18
 use Filament\Tables;
16
 use Filament\Tables;
19
-use Illuminate\Support\Collection;
17
+use Filament\Tables\Table;
18
+use Illuminate\Database\Eloquent\SoftDeletingScope;
20
 use Illuminate\Support\Facades\Auth;
19
 use Illuminate\Support\Facades\Auth;
21
 use Illuminate\Support\Facades\DB;
20
 use Illuminate\Support\Facades\DB;
22
 use Illuminate\Validation\Rules\Unique;
21
 use Illuminate\Validation\Rules\Unique;
43
                                     ->options(Account::getAccountTypes())
42
                                     ->options(Account::getAccountTypes())
44
                                     ->searchable()
43
                                     ->searchable()
45
                                     ->default('checking')
44
                                     ->default('checking')
46
-                                    ->reactive()
47
-                                    ->disablePlaceholderSelection()
45
+                                    ->live()
48
                                     ->required(),
46
                                     ->required(),
49
                                 Forms\Components\TextInput::make('name')
47
                                 Forms\Components\TextInput::make('name')
50
                                     ->label('Name')
48
                                     ->label('Name')
52
                                     ->required(),
50
                                     ->required(),
53
                                 Forms\Components\TextInput::make('number')
51
                                 Forms\Components\TextInput::make('number')
54
                                     ->label('Account Number')
52
                                     ->label('Account Number')
55
-                                    ->unique(callback: static function (Unique $rule, $state) {
53
+                                    ->unique(ignoreRecord: true, modifyRuleUsing: static function (Unique $rule, $state) {
56
                                         $companyId = Auth::user()->currentCompany->id;
54
                                         $companyId = Auth::user()->currentCompany->id;
57
 
55
 
58
                                         return $rule->where('company_id', $companyId)->where('number', $state);
56
                                         return $rule->where('company_id', $companyId)->where('number', $state);
59
-                                    }, ignoreRecord: true)
57
+                                    })
60
                                     ->maxLength(20)
58
                                     ->maxLength(20)
61
                                     ->validationAttribute('account number')
59
                                     ->validationAttribute('account number')
62
                                     ->required(),
60
                                     ->required(),
63
                                 ToggleButton::make('enabled')
61
                                 ToggleButton::make('enabled')
64
                                     ->label('Default Account')
62
                                     ->label('Default Account')
65
-                                    ->hidden(static fn (Closure $get) => $get('type') === 'credit_card')
63
+                                    ->hidden(static fn (Forms\Get $get) => $get('type') === 'credit_card')
66
                                     ->offColor('danger')
64
                                     ->offColor('danger')
67
                                     ->onColor('primary'),
65
                                     ->onColor('primary'),
68
                             ])->columns(),
66
                             ])->columns(),
71
                                 Forms\Components\Select::make('currency_code')
69
                                 Forms\Components\Select::make('currency_code')
72
                                     ->label('Currency')
70
                                     ->label('Currency')
73
                                     ->relationship('currency', 'name')
71
                                     ->relationship('currency', 'name')
72
+                                    ->default(Currency::getDefaultCurrencyCode())
73
+                                    ->saveRelationshipsUsing(null)
74
                                     ->preload()
74
                                     ->preload()
75
-                                    ->default(Currency::getDefaultCurrency())
76
                                     ->searchable()
75
                                     ->searchable()
77
-                                    ->reactive()
76
+                                    ->live()
77
+                                    ->afterStateUpdated(static function (Forms\Set $set, $state, $old, Forms\Get $get) {
78
+                                        $opening_balance = CurrencyConverter::convertAndSet($state, $old, $get('opening_balance'));
79
+
80
+                                        if ($opening_balance !== null) {
81
+                                            $set('opening_balance', $opening_balance);
82
+                                        }
83
+                                    })
78
                                     ->required()
84
                                     ->required()
79
-                                    ->saveRelationshipsUsing(null)
80
                                     ->createOptionForm([
85
                                     ->createOptionForm([
81
                                         Forms\Components\Select::make('currency.code')
86
                                         Forms\Components\Select::make('currency.code')
82
                                             ->label('Code')
87
                                             ->label('Code')
83
                                             ->searchable()
88
                                             ->searchable()
84
-                                            ->options(Currency::getCurrencyCodes())
85
-                                            ->reactive()
89
+                                            ->options(Currency::getAvailableCurrencyCodes())
90
+                                            ->live()
86
                                             ->afterStateUpdated(static function (callable $set, $state) {
91
                                             ->afterStateUpdated(static function (callable $set, $state) {
87
                                                 if ($state === null) {
92
                                                 if ($state === null) {
88
                                                     return;
93
                                                     return;
89
                                                 }
94
                                                 }
90
 
95
 
91
-                                                $code = $state;
92
-                                                $currencyConfig = config("money.{$code}", []);
96
+                                                $currency_code = currency($state);
93
                                                 $currencyService = app(CurrencyService::class);
97
                                                 $currencyService = app(CurrencyService::class);
94
 
98
 
95
-                                                $defaultCurrency = Currency::getDefaultCurrency();
96
-
99
+                                                $defaultCurrencyCode = Currency::getDefaultCurrencyCode();
97
                                                 $rate = 1;
100
                                                 $rate = 1;
98
 
101
 
99
-                                                if ($defaultCurrency !== null) {
100
-                                                    $rate = $currencyService->getCachedExchangeRate($defaultCurrency, $code);
102
+                                                if ($defaultCurrencyCode !== null) {
103
+                                                    $rate = $currencyService->getCachedExchangeRate($defaultCurrencyCode, $state);
101
                                                 }
104
                                                 }
102
 
105
 
103
-                                                $set('currency.name', $currencyConfig['name'] ?? '');
106
+                                                $set('currency.name', $currency_code->getName() ?? '');
104
                                                 $set('currency.rate', $rate);
107
                                                 $set('currency.rate', $rate);
105
                                             })
108
                                             })
106
                                             ->required(),
109
                                             ->required(),
116
                                         return $action
119
                                         return $action
117
                                             ->label('Add Currency')
120
                                             ->label('Add Currency')
118
                                             ->modalHeading('Add Currency')
121
                                             ->modalHeading('Add Currency')
119
-                                            ->modalButton('Add')
122
+                                            ->modalSubmitActionLabel('Add')
123
+                                            ->slideOver()
120
                                             ->action(static function (array $data) {
124
                                             ->action(static function (array $data) {
121
                                                 return DB::transaction(static function () use ($data) {
125
                                                 return DB::transaction(static function () use ($data) {
122
                                                     $code = $data['currency']['code'];
126
                                                     $code = $data['currency']['code'];
130
                                 Forms\Components\TextInput::make('opening_balance')
134
                                 Forms\Components\TextInput::make('opening_balance')
131
                                     ->label('Opening Balance')
135
                                     ->label('Opening Balance')
132
                                     ->required()
136
                                     ->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)),
137
+                                    ->currency(static fn (Forms\Get $get) => $get('currency_code'))
148
                             ])->columns(),
138
                             ])->columns(),
149
                         Forms\Components\Tabs::make('Account Specifications')
139
                         Forms\Components\Tabs::make('Account Specifications')
150
                             ->tabs([
140
                             ->tabs([
210
             ])->columns(3);
200
             ])->columns(3);
211
     }
201
     }
212
 
202
 
213
-    /**
214
-     * @throws Exception
215
-     */
216
     public static function table(Table $table): Table
203
     public static function table(Table $table): Table
217
     {
204
     {
218
         return $table
205
         return $table
232
                     ->description(static fn (Account $record) => $record->bank_phone ?: 'N/A')
219
                     ->description(static fn (Account $record) => $record->bank_phone ?: 'N/A')
233
                     ->searchable()
220
                     ->searchable()
234
                     ->sortable(),
221
                     ->sortable(),
235
-                Tables\Columns\BadgeColumn::make('status')
222
+                Tables\Columns\TextColumn::make('status')
223
+                    ->badge()
236
                     ->label('Status')
224
                     ->label('Status')
237
                     ->colors([
225
                     ->colors([
238
                         'primary' => 'open',
226
                         'primary' => 'open',
242
                         'danger' => 'closed',
230
                         'danger' => 'closed',
243
                     ])
231
                     ])
244
                     ->icons([
232
                     ->icons([
245
-                        'heroicon-o-cash' => 'open',
233
+                        'heroicon-o-currency-dollar' => 'open',
246
                         'heroicon-o-clock' => 'active',
234
                         'heroicon-o-clock' => 'active',
247
                         'heroicon-o-status-offline' => 'dormant',
235
                         'heroicon-o-status-offline' => 'dormant',
248
                         'heroicon-o-exclamation' => 'restricted',
236
                         'heroicon-o-exclamation' => 'restricted',
252
                 Tables\Columns\TextColumn::make('opening_balance')
240
                 Tables\Columns\TextColumn::make('opening_balance')
253
                     ->label('Current Balance')
241
                     ->label('Current Balance')
254
                     ->sortable()
242
                     ->sortable()
255
-                    ->money(static fn ($record) => $record->currency_code, true),
243
+                    ->currency(static fn (Account $record) => $record->currency_code, true),
256
             ])
244
             ])
257
             ->filters([
245
             ->filters([
258
                 //
246
                 //
259
             ])
247
             ])
260
             ->actions([
248
             ->actions([
249
+                Tables\Actions\EditAction::make(),
261
                 Tables\Actions\Action::make('update_balance')
250
                 Tables\Actions\Action::make('update_balance')
262
-                    ->hidden(static fn (Account $record) => $record->currency_code === Currency::getDefaultCurrency())
251
+                    ->hidden(static fn (Account $record) => $record->currency_code === Currency::getDefaultCurrencyCode())
263
                     ->label('Update Balance')
252
                     ->label('Update Balance')
264
                     ->icon('heroicon-o-currency-dollar')
253
                     ->icon('heroicon-o-currency-dollar')
265
                     ->requiresConfirmation()
254
                     ->requiresConfirmation()
266
-                    ->modalSubheading('Are you sure you want to update the balance with the latest exchange rate?')
255
+                    ->modalDescription('Are you sure you want to update the balance with the latest exchange rate?')
267
                     ->before(static function (Tables\Actions\Action $action, Account $record) {
256
                     ->before(static function (Tables\Actions\Action $action, Account $record) {
268
-                        if ($record->currency_code !== Currency::getDefaultCurrency()) {
257
+                        if ($record->currency_code !== Currency::getDefaultCurrencyCode()) {
269
                             $currencyService = app(CurrencyService::class);
258
                             $currencyService = app(CurrencyService::class);
270
-                            $defaultCurrency = Currency::getDefaultCurrency();
259
+                            $defaultCurrency = Currency::getDefaultCurrencyCode();
271
                             $cachedExchangeRate = $currencyService->getCachedExchangeRate($defaultCurrency, $record->currency_code);
260
                             $cachedExchangeRate = $currencyService->getCachedExchangeRate($defaultCurrency, $record->currency_code);
272
                             $oldExchangeRate = $record->currency->rate;
261
                             $oldExchangeRate = $record->currency->rate;
273
 
262
 
284
                         }
273
                         }
285
                     })
274
                     })
286
                     ->action(static function (Account $record) {
275
                     ->action(static function (Account $record) {
287
-                        if ($record->currency_code !== Currency::getDefaultCurrency()) {
276
+                        if ($record->currency_code !== Currency::getDefaultCurrencyCode()) {
288
                             $currencyService = app(CurrencyService::class);
277
                             $currencyService = app(CurrencyService::class);
289
-                            $defaultCurrency = Currency::getDefaultCurrency();
278
+                            $defaultCurrency = Currency::getDefaultCurrencyCode();
290
                             $cachedExchangeRate = $currencyService->getCachedExchangeRate($defaultCurrency, $record->currency_code);
279
                             $cachedExchangeRate = $currencyService->getCachedExchangeRate($defaultCurrency, $record->currency_code);
291
                             $oldExchangeRate = $record->currency->rate;
280
                             $oldExchangeRate = $record->currency->rate;
292
 
281
 
293
-                            $originalBalanceInOriginalCurrency = $record->opening_balance / $oldExchangeRate;
294
-                            $currencyPrecision = $record->currency->precision;
295
-
296
                             if ($cachedExchangeRate !== $oldExchangeRate) {
282
                             if ($cachedExchangeRate !== $oldExchangeRate) {
297
-                                $record->opening_balance = round($originalBalanceInOriginalCurrency * $cachedExchangeRate, $currencyPrecision);
283
+
284
+                                $scale = 10 ** $record->currency->precision;
285
+                                $cleanedBalance = (int)filter_var($record->opening_balance, FILTER_SANITIZE_NUMBER_INT);
286
+
287
+                                $newBalance = ($cachedExchangeRate / $oldExchangeRate) * $cleanedBalance;
288
+                                $newBalanceInt = (int)round($newBalance, $scale);
289
+
290
+                                $record->opening_balance = money($newBalanceInt, $record->currency_code)->getValue();
298
                                 $record->currency->rate = $cachedExchangeRate;
291
                                 $record->currency->rate = $cachedExchangeRate;
292
+
299
                                 $record->currency->save();
293
                                 $record->currency->save();
300
                                 $record->save();
294
                                 $record->save();
301
                             }
295
                             }
307
                                 ->send();
301
                                 ->send();
308
                         }
302
                         }
309
                     }),
303
                     }),
310
-                Tables\Actions\EditAction::make(),
311
-                Tables\Actions\DeleteAction::make()
312
-                    ->modalHeading('Delete Account')
313
-                    ->requiresConfirmation()
314
-                    ->before(static function (Tables\Actions\DeleteAction $action, Account $record) {
315
-                        if ($record->enabled) {
316
-                            Notification::make()
317
-                                ->danger()
318
-                                ->title('Action Denied')
319
-                                ->body(__('The :name account is currently set as your default account and cannot be deleted. Please set a different account as your default before attempting to delete this one.', ['name' => $record->name]))
320
-                                ->persistent()
321
-                                ->send();
322
-
323
-                            $action->cancel();
324
-                        }
325
-                    }),
326
             ])
304
             ])
327
             ->bulkActions([
305
             ->bulkActions([
328
-                Tables\Actions\DeleteBulkAction::make()
329
-                    ->before(static function (Tables\Actions\DeleteBulkAction $action, Collection $records) {
330
-                        foreach ($records as $record) {
331
-                            if ($record->enabled) {
332
-                                Notification::make()
333
-                                    ->danger()
334
-                                    ->title('Action Denied')
335
-                                    ->body(__('The :name account is currently set as your default account and cannot be deleted. Please set a different account as your default before attempting to delete this one.', ['name' => $record->name]))
336
-                                    ->persistent()
337
-                                    ->send();
338
-
339
-                                $action->cancel();
340
-                            }
341
-                        }
342
-                    }),
306
+                Tables\Actions\BulkActionGroup::make([
307
+                    Tables\Actions\DeleteBulkAction::make(),
308
+                ]),
309
+            ])
310
+            ->emptyStateActions([
311
+                Tables\Actions\CreateAction::make(),
343
             ]);
312
             ]);
344
     }
313
     }
345
 
314
 
346
-    public static function getSlug(): string
347
-    {
348
-        return '{company}/banking/accounts';
349
-    }
350
-
351
-    public static function getUrl($name = 'index', $params = [], $isAbsolute = true): string
352
-    {
353
-        $routeBaseName = static::getRouteBaseName();
354
-
355
-        return route("{$routeBaseName}.{$name}", [
356
-            'company' => Auth::user()->currentCompany,
357
-            'record' => $params['record'] ?? null,
358
-        ], $isAbsolute);
359
-    }
360
-
361
     public static function getRelations(): array
315
     public static function getRelations(): array
362
     {
316
     {
363
         return [
317
         return [

app/Filament/Resources/AccountResource/Pages/CreateAccount.php → app/Filament/Company/Resources/Banking/AccountResource/Pages/CreateAccount.php View File

1
 <?php
1
 <?php
2
 
2
 
3
-namespace App\Filament\Resources\AccountResource\Pages;
3
+namespace App\Filament\Company\Resources\Banking\AccountResource\Pages;
4
 
4
 
5
-use App\Filament\Resources\AccountResource;
5
+use App\Filament\Company\Resources\Banking\AccountResource;
6
 use App\Models\Banking\Account;
6
 use App\Models\Banking\Account;
7
 use App\Traits\HandlesResourceRecordCreation;
7
 use App\Traits\HandlesResourceRecordCreation;
8
+use Filament\Actions;
8
 use Filament\Resources\Pages\CreateRecord;
9
 use Filament\Resources\Pages\CreateRecord;
9
 use Filament\Support\Exceptions\Halt;
10
 use Filament\Support\Exceptions\Halt;
10
 use Illuminate\Database\Eloquent\Model;
11
 use Illuminate\Database\Eloquent\Model;
12
 
13
 
13
 class CreateAccount extends CreateRecord
14
 class CreateAccount extends CreateRecord
14
 {
15
 {
15
-    use  HandlesResourceRecordCreation;
16
+    use HandlesResourceRecordCreation;
16
 
17
 
17
     protected static string $resource = AccountResource::class;
18
     protected static string $resource = AccountResource::class;
18
 
19
 

app/Filament/Resources/AccountResource/Pages/EditAccount.php → app/Filament/Company/Resources/Banking/AccountResource/Pages/EditAccount.php View File

1
 <?php
1
 <?php
2
 
2
 
3
-namespace App\Filament\Resources\AccountResource\Pages;
3
+namespace App\Filament\Company\Resources\Banking\AccountResource\Pages;
4
 
4
 
5
-use App\Filament\Resources\AccountResource;
5
+use App\Filament\Company\Resources\Banking\AccountResource;
6
 use App\Models\Banking\Account;
6
 use App\Models\Banking\Account;
7
 use App\Models\Setting\Currency;
7
 use App\Models\Setting\Currency;
8
 use App\Traits\HandlesResourceRecordUpdate;
8
 use App\Traits\HandlesResourceRecordUpdate;
9
-use Filament\Pages\Actions;
9
+use Filament\Actions;
10
 use Filament\Resources\Pages\EditRecord;
10
 use Filament\Resources\Pages\EditRecord;
11
 use Filament\Support\Exceptions\Halt;
11
 use Filament\Support\Exceptions\Halt;
12
 use Illuminate\Database\Eloquent\Model;
12
 use Illuminate\Database\Eloquent\Model;
18
 
18
 
19
     protected static string $resource = AccountResource::class;
19
     protected static string $resource = AccountResource::class;
20
 
20
 
21
-    protected function getActions(): array
21
+    protected function getHeaderActions(): array
22
     {
22
     {
23
         return [
23
         return [
24
             Actions\DeleteAction::make(),
24
             Actions\DeleteAction::make(),
25
         ];
25
         ];
26
     }
26
     }
27
 
27
 
28
-    protected function getRedirectUrl(): string
28
+    protected function getRedirectUrl(): ?string
29
     {
29
     {
30
         return $this->previousUrl;
30
         return $this->previousUrl;
31
     }
31
     }
40
     /**
40
     /**
41
      * @throws Halt
41
      * @throws Halt
42
      */
42
      */
43
-    protected function handleRecordUpdate(Model|Account $record, array $data): Model|Account
43
+    protected function handleRecordUpdate(Account|Model $record, array $data): Model|Account
44
     {
44
     {
45
         $user = Auth::user();
45
         $user = Auth::user();
46
 
46
 
59
             );
59
             );
60
         }
60
         }
61
 
61
 
62
-        $this->handleRecordUpdateWithUniqueField($record, $data, $user);
63
-
64
-        return parent::handleRecordUpdate($record, $data);
62
+        return $this->handleRecordUpdateWithUniqueField($record, $data, $user);
65
     }
63
     }
66
 }
64
 }

app/Filament/Resources/AccountResource/Pages/ListAccounts.php → app/Filament/Company/Resources/Banking/AccountResource/Pages/ListAccounts.php View File

1
 <?php
1
 <?php
2
 
2
 
3
-namespace App\Filament\Resources\AccountResource\Pages;
3
+namespace App\Filament\Company\Resources\Banking\AccountResource\Pages;
4
 
4
 
5
-use App\Filament\Resources\AccountResource;
6
-use Filament\Pages\Actions;
5
+use App\Filament\Company\Resources\Banking\AccountResource;
6
+use Filament\Actions;
7
 use Filament\Resources\Pages\ListRecords;
7
 use Filament\Resources\Pages\ListRecords;
8
 
8
 
9
 class ListAccounts extends ListRecords
9
 class ListAccounts extends ListRecords
10
 {
10
 {
11
     protected static string $resource = AccountResource::class;
11
     protected static string $resource = AccountResource::class;
12
 
12
 
13
-    protected function getActions(): array
13
+    protected function getHeaderActions(): array
14
     {
14
     {
15
         return [
15
         return [
16
             Actions\CreateAction::make(),
16
             Actions\CreateAction::make(),

+ 155
- 0
app/Filament/Company/Resources/Setting/CategoryResource.php View File

1
+<?php
2
+
3
+namespace App\Filament\Company\Resources\Setting;
4
+
5
+use App\Enums\CategoryType;
6
+use App\Filament\Company\Resources\Setting\CategoryResource\Pages;
7
+use App\Models\Setting\Category;
8
+use Closure;
9
+use Exception;
10
+use Filament\Forms;
11
+use Filament\Forms\Form;
12
+use Filament\Notifications\Notification;
13
+use Filament\Resources\Resource;
14
+use Filament\Tables;
15
+use Filament\Tables\Table;
16
+use Illuminate\Database\Eloquent\Collection;
17
+use Wallo\FilamentSelectify\Components\ToggleButton;
18
+
19
+class CategoryResource extends Resource
20
+{
21
+    protected static ?string $model = Category::class;
22
+
23
+    protected static ?string $navigationIcon = 'heroicon-o-folder';
24
+
25
+    protected static ?string $navigationGroup = 'Settings';
26
+
27
+    protected static ?string $slug = 'settings/categories';
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
+                            ->autofocus()
38
+                            ->required()
39
+                            ->maxLength(255)
40
+                            ->rule(static function (Forms\Get $get, Forms\Components\Component $component): Closure {
41
+                                return static function (string $attribute, $value, Closure $fail) use ($get, $component) {
42
+                                    $existingCategory = Category::where('company_id', auth()->user()->currentCompany->id)
43
+                                                                ->where('name', $value)
44
+                                                                ->where('type', $get('type'))
45
+                                                                ->first();
46
+
47
+                                    if ($existingCategory && $existingCategory->getKey() !== $component->getRecord()?->getKey()) {
48
+                                        $type = ucwords($get('type'));
49
+                                        $fail("The {$type} category \"{$value}\" already exists.");
50
+                                    }
51
+                                };
52
+                            }),
53
+                        Forms\Components\Select::make('type')
54
+                            ->options(CategoryType::class)
55
+                            ->required()
56
+                            ->native(false)
57
+                            ->label('Type'),
58
+                        Forms\Components\ColorPicker::make('color')
59
+                            ->required()
60
+                            ->label('Color'),
61
+                        ToggleButton::make('enabled')
62
+                            ->label('Default'),
63
+                    ])->columns(),
64
+            ]);
65
+    }
66
+
67
+    /**
68
+     * @throws Exception
69
+     */
70
+    public static function table(Table $table): Table
71
+    {
72
+        return $table
73
+            ->columns([
74
+                Tables\Columns\TextColumn::make('name')
75
+                    ->label('Name')
76
+                    ->weight('semibold')
77
+                    ->icon(static fn (Category $record) => $record->enabled ? 'heroicon-o-lock-closed' : null)
78
+                    ->tooltip(static fn (Category $record) => $record->enabled ? "Default {$record->type->getLabel()} Category" : null)
79
+                    ->iconPosition('after')
80
+                    ->searchable()
81
+                    ->sortable(),
82
+                Tables\Columns\TextColumn::make('type')
83
+                    ->label('Type')
84
+                    ->sortable()
85
+                    ->searchable(),
86
+                Tables\Columns\ColorColumn::make('color')
87
+                    ->label('Color')
88
+                    ->copyable()
89
+                    ->copyMessage('Color code copied'),
90
+            ])
91
+            ->filters([
92
+                Tables\Filters\SelectFilter::make('type')
93
+                    ->label('Type')
94
+                    ->multiple()
95
+                    ->options(CategoryType::class),
96
+            ])
97
+            ->actions([
98
+                Tables\Actions\EditAction::make(),
99
+                Tables\Actions\DeleteAction::make()
100
+                    ->before(static function (Category $record, Tables\Actions\DeleteAction $action) {
101
+                        if ($record->enabled) {
102
+                            Notification::make()
103
+                                ->danger()
104
+                                ->title('Action Denied')
105
+                                ->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' => $record->type->getLabel()]))
106
+                                ->persistent()
107
+                                ->send();
108
+
109
+                            $action->cancel();
110
+                        }
111
+                    }),
112
+            ])
113
+            ->bulkActions([
114
+                Tables\Actions\BulkActionGroup::make([
115
+                    Tables\Actions\DeleteBulkAction::make()
116
+                        ->before(static function (Collection $records, Tables\Actions\DeleteBulkAction $action) {
117
+                            $defaultCategories = $records->filter(static function (Category $record) {
118
+                                return $record->enabled;
119
+                            });
120
+
121
+                            if ($defaultCategories->isNotEmpty()) {
122
+                                $defaultCategoryNames = $defaultCategories->pluck('name')->toArray();
123
+
124
+                                Notification::make()
125
+                                    ->danger()
126
+                                    ->title('Action Denied')
127
+                                    ->body(static function () use ($defaultCategoryNames) {
128
+                                        $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>";
129
+                                        $message .= implode("<br>", array_map(static function ($name) {
130
+                                            return "&bull; " . $name;
131
+                                        }, $defaultCategoryNames));
132
+                                        return $message;
133
+                                    })
134
+                                    ->persistent()
135
+                                    ->send();
136
+
137
+                                $action->cancel();
138
+                            }
139
+                        }),
140
+                ]),
141
+            ])
142
+            ->emptyStateActions([
143
+                Tables\Actions\CreateAction::make(),
144
+            ]);
145
+    }
146
+
147
+    public static function getPages(): array
148
+    {
149
+        return [
150
+            'index' => Pages\ListCategories::route('/'),
151
+            'create' => Pages\CreateCategory::route('/create'),
152
+            'edit' => Pages\EditCategory::route('/{record}/edit'),
153
+        ];
154
+    }
155
+}

app/Filament/Resources/CategoryResource/Pages/CreateCategory.php → app/Filament/Company/Resources/Setting/CategoryResource/Pages/CreateCategory.php View File

1
 <?php
1
 <?php
2
 
2
 
3
-namespace App\Filament\Resources\CategoryResource\Pages;
3
+namespace App\Filament\Company\Resources\Setting\CategoryResource\Pages;
4
 
4
 
5
-use App\Filament\Resources\CategoryResource;
5
+use App\Filament\Company\Resources\Setting\CategoryResource;
6
 use App\Models\Setting\Category;
6
 use App\Models\Setting\Category;
7
 use App\Traits\HandlesResourceRecordCreation;
7
 use App\Traits\HandlesResourceRecordCreation;
8
 use Filament\Resources\Pages\CreateRecord;
8
 use Filament\Resources\Pages\CreateRecord;
9
 use Filament\Support\Exceptions\Halt;
9
 use Filament\Support\Exceptions\Halt;
10
 use Illuminate\Database\Eloquent\Model;
10
 use Illuminate\Database\Eloquent\Model;
11
-use Illuminate\Support\Facades\Auth;
12
 
11
 
13
 class CreateCategory extends CreateRecord
12
 class CreateCategory extends CreateRecord
14
 {
13
 {
33
      */
32
      */
34
     protected function handleRecordCreation(array $data): Model
33
     protected function handleRecordCreation(array $data): Model
35
     {
34
     {
36
-        $user = Auth::user();
35
+        $user = auth()->user();
37
 
36
 
38
         if (!$user) {
37
         if (!$user) {
39
-            throw new Halt('No authenticated user found.');
38
+            throw new Halt('No authenticated user found');
40
         }
39
         }
41
 
40
 
42
         return $this->handleRecordCreationWithUniqueField($data, new Category(), $user, 'type');
41
         return $this->handleRecordCreationWithUniqueField($data, new Category(), $user, 'type');

app/Filament/Resources/CategoryResource/Pages/EditCategory.php → app/Filament/Company/Resources/Setting/CategoryResource/Pages/EditCategory.php View File

1
 <?php
1
 <?php
2
 
2
 
3
-namespace App\Filament\Resources\CategoryResource\Pages;
3
+namespace App\Filament\Company\Resources\Setting\CategoryResource\Pages;
4
 
4
 
5
-use App\Filament\Resources\CategoryResource;
5
+use App\Filament\Company\Resources\Setting\CategoryResource;
6
 use App\Traits\HandlesResourceRecordUpdate;
6
 use App\Traits\HandlesResourceRecordUpdate;
7
-use Filament\Pages\Actions;
7
+use Filament\Actions;
8
 use Filament\Resources\Pages\EditRecord;
8
 use Filament\Resources\Pages\EditRecord;
9
 use Filament\Support\Exceptions\Halt;
9
 use Filament\Support\Exceptions\Halt;
10
 use Illuminate\Database\Eloquent\Model;
10
 use Illuminate\Database\Eloquent\Model;
11
-use Illuminate\Support\Facades\Auth;
12
 
11
 
13
 class EditCategory extends EditRecord
12
 class EditCategory extends EditRecord
14
 {
13
 {
16
 
15
 
17
     protected static string $resource = CategoryResource::class;
16
     protected static string $resource = CategoryResource::class;
18
 
17
 
19
-    protected function getActions(): array
18
+    protected function getHeaderActions(): array
20
     {
19
     {
21
         return [
20
         return [
22
             Actions\DeleteAction::make(),
21
             Actions\DeleteAction::make(),
40
      */
39
      */
41
     protected function handleRecordUpdate(Model $record, array $data): Model
40
     protected function handleRecordUpdate(Model $record, array $data): Model
42
     {
41
     {
43
-        $user = Auth::user();
42
+        $user = auth()->user();
44
 
43
 
45
         if (!$user) {
44
         if (!$user) {
46
-            throw new Halt('No authenticated user found.');
45
+            throw new Halt('No authenticated user found');
47
         }
46
         }
48
 
47
 
49
         return $this->handleRecordUpdateWithUniqueField($record, $data, $user, 'type');
48
         return $this->handleRecordUpdateWithUniqueField($record, $data, $user, 'type');

+ 19
- 0
app/Filament/Company/Resources/Setting/CategoryResource/Pages/ListCategories.php View File

1
+<?php
2
+
3
+namespace App\Filament\Company\Resources\Setting\CategoryResource\Pages;
4
+
5
+use App\Filament\Company\Resources\Setting\CategoryResource;
6
+use Filament\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 getHeaderActions(): array
14
+    {
15
+        return [
16
+            Actions\CreateAction::make(),
17
+        ];
18
+    }
19
+}

+ 189
- 0
app/Filament/Company/Resources/Setting/CurrencyResource.php View File

1
+<?php
2
+
3
+namespace App\Filament\Company\Resources\Setting;
4
+
5
+use App\Filament\Company\Resources\Setting\CurrencyResource\Pages;
6
+use App\Filament\Company\Resources\Setting\CurrencyResource\RelationManagers;
7
+use App\Models\Setting\Currency;
8
+use App\Services\CurrencyService;
9
+use Filament\Forms;
10
+use Filament\Forms\Form;
11
+use Filament\Resources\Resource;
12
+use Filament\Support\Colors\Color;
13
+use Filament\Tables;
14
+use Filament\Tables\Table;
15
+use Illuminate\Database\Eloquent\Builder;
16
+use Illuminate\Database\Eloquent\SoftDeletingScope;
17
+use Wallo\FilamentSelectify\Components\ButtonGroup;
18
+use Wallo\FilamentSelectify\Components\ToggleButton;
19
+
20
+class CurrencyResource extends Resource
21
+{
22
+    protected static ?string $model = Currency::class;
23
+
24
+    protected static ?string $navigationIcon = 'heroicon-o-currency-dollar';
25
+
26
+    protected static ?string $navigationGroup = 'Settings';
27
+
28
+    protected static ?string $slug = 'settings/currencies';
29
+
30
+    public static function form(Form $form): Form
31
+    {
32
+        return $form
33
+            ->schema([
34
+                Forms\Components\Section::make('General')
35
+                    ->schema([
36
+                        Forms\Components\Select::make('code')
37
+                        ->label('Code')
38
+                        ->options(Currency::getAvailableCurrencyCodes())
39
+                        ->searchable()
40
+                        ->placeholder('Select a currency code...')
41
+                        ->live()
42
+                        ->required()
43
+                        ->hidden(static fn (Forms\Get $get): bool => $get('enabled'))
44
+                        ->afterStateUpdated(static function (Forms\Set $set, $state) {
45
+                            if ($state === null) {
46
+                                return;
47
+                            }
48
+
49
+                            $code = $state;
50
+
51
+                            $allCurrencies = Currency::getAllCurrencies();
52
+
53
+                            $selectedCurrencyCode = $allCurrencies[$code] ?? [];
54
+
55
+                            $currencyService = app(CurrencyService::class);
56
+                            $defaultCurrencyCode = Currency::getDefaultCurrencyCode();
57
+                            $rate = 1;
58
+
59
+                            if ($defaultCurrencyCode !== null) {
60
+                                $rate = $currencyService->getCachedExchangeRate($defaultCurrencyCode, $code);
61
+                            }
62
+
63
+                            $set('name', $selectedCurrencyCode['name'] ?? '');
64
+                            $set('rate', $rate);
65
+                            $set('precision', $selectedCurrencyCode['precision'] ?? '');
66
+                            $set('symbol', $selectedCurrencyCode['symbol'] ?? '');
67
+                            $set('symbol_first', $selectedCurrencyCode['symbol_first'] ?? '');
68
+                            $set('decimal_mark', $selectedCurrencyCode['decimal_mark'] ?? '');
69
+                            $set('thousands_separator', $selectedCurrencyCode['thousands_separator'] ?? '');
70
+                        }),
71
+                        Forms\Components\TextInput::make('code')
72
+                            ->label('Code')
73
+                            ->hidden(static fn (Forms\Get $get): bool => !$get('enabled'))
74
+                            ->disabled(static fn (Forms\Get $get): bool => $get('enabled'))
75
+                            ->required(),
76
+                        Forms\Components\TextInput::make('name')
77
+                            ->label('Name')
78
+                            ->maxLength(50)
79
+                            ->required(),
80
+                        Forms\Components\TextInput::make('rate')
81
+                            ->label('Rate')
82
+                            ->dehydrateStateUsing(static fn (Forms\Get $get, $state): float => $get('enabled') ? '1.0' : (float) $state)
83
+                            ->numeric()
84
+                            ->live()
85
+                            ->disabled(static fn (Forms\Get $get): bool => $get('enabled'))
86
+                            ->required(),
87
+                        Forms\Components\Select::make('precision')
88
+                            ->label('Precision')
89
+                            ->searchable()
90
+                            ->placeholder('Select a precision...')
91
+                            ->options(['0', '1', '2', '3', '4'])
92
+                            ->required(),
93
+                        Forms\Components\TextInput::make('symbol')
94
+                            ->label('Symbol')
95
+                            ->maxLength(5)
96
+                            ->required(),
97
+                        Forms\Components\Select::make('symbol_first')
98
+                            ->label('Symbol Position')
99
+                            ->searchable()
100
+                            ->boolean('Before Amount', 'After Amount', 'Select the currency symbol position...')
101
+                            ->required(),
102
+                        Forms\Components\TextInput::make('decimal_mark')
103
+                            ->label('Decimal Separator')
104
+                            ->maxLength(1)
105
+                            ->required(),
106
+                        Forms\Components\TextInput::make('thousands_separator')
107
+                            ->label('Thousands Separator')
108
+                            ->maxLength(1)
109
+                            ->required(),
110
+                        ToggleButton::make('enabled')
111
+                            ->label('Default Currency')
112
+                            ->live()
113
+                            ->offColor(Color::Red)
114
+                            ->onColor(Color::Indigo)
115
+                            ->afterStateUpdated(static function (Forms\Set $set, Forms\Get $get, $state) {
116
+                                $enabled = $state;
117
+                                $code = $get('code');
118
+                                $currencyService = app(CurrencyService::class);
119
+
120
+                                if ($enabled) {
121
+                                    $rate = 1;
122
+                                } else {
123
+                                    $defaultCurrencyCode = Currency::getDefaultCurrencyCode();
124
+                                    $rate = $defaultCurrencyCode ? $currencyService->getCachedExchangeRate($defaultCurrencyCode, $code) : 1;
125
+                                }
126
+
127
+                                $set('rate', $rate);
128
+                            }),
129
+                    ])->columns(),
130
+            ]);
131
+    }
132
+
133
+    public static function table(Table $table): Table
134
+    {
135
+        return $table
136
+            ->columns([
137
+                Tables\Columns\TextColumn::make('name')
138
+                    ->label('Name')
139
+                    ->weight('semibold')
140
+                    ->icon(static fn (Currency $record) => $record->enabled ? 'heroicon-o-lock-closed' : null)
141
+                    ->tooltip(static fn (Currency $record) => $record->enabled ? 'Default Currency' : null)
142
+                    ->iconPosition('after')
143
+                    ->searchable()
144
+                    ->sortable(),
145
+                Tables\Columns\TextColumn::make('code')
146
+                    ->label('Code')
147
+                    ->searchable()
148
+                    ->sortable(),
149
+                Tables\Columns\TextColumn::make('symbol')
150
+                    ->label('Symbol')
151
+                    ->searchable()
152
+                    ->sortable(),
153
+                Tables\Columns\TextColumn::make('rate')
154
+                    ->label('Rate')
155
+                    ->searchable()
156
+                    ->sortable(),
157
+            ])
158
+            ->filters([
159
+                //
160
+            ])
161
+            ->actions([
162
+                Tables\Actions\EditAction::make(),
163
+            ])
164
+            ->bulkActions([
165
+                Tables\Actions\BulkActionGroup::make([
166
+                    Tables\Actions\DeleteBulkAction::make(),
167
+                ]),
168
+            ])
169
+            ->emptyStateActions([
170
+                Tables\Actions\CreateAction::make(),
171
+            ]);
172
+    }
173
+
174
+    public static function getRelations(): array
175
+    {
176
+        return [
177
+            //
178
+        ];
179
+    }
180
+
181
+    public static function getPages(): array
182
+    {
183
+        return [
184
+            'index' => Pages\ListCurrencies::route('/'),
185
+            'create' => Pages\CreateCurrency::route('/create'),
186
+            'edit' => Pages\EditCurrency::route('/{record}/edit'),
187
+        ];
188
+    }
189
+}

app/Filament/Resources/CurrencyResource/Pages/CreateCurrency.php → app/Filament/Company/Resources/Setting/CurrencyResource/Pages/CreateCurrency.php View File

1
 <?php
1
 <?php
2
 
2
 
3
-namespace App\Filament\Resources\CurrencyResource\Pages;
3
+namespace App\Filament\Company\Resources\Setting\CurrencyResource\Pages;
4
 
4
 
5
-use App\Filament\Resources\CurrencyResource;
5
+use App\Filament\Company\Resources\Setting\CurrencyResource;
6
 use App\Models\Setting\Currency;
6
 use App\Models\Setting\Currency;
7
 use App\Traits\HandlesResourceRecordCreation;
7
 use App\Traits\HandlesResourceRecordCreation;
8
-use Filament\Pages\Actions;
8
+use Filament\Actions;
9
 use Filament\Resources\Pages\CreateRecord;
9
 use Filament\Resources\Pages\CreateRecord;
10
 use Filament\Support\Exceptions\Halt;
10
 use Filament\Support\Exceptions\Halt;
11
 use Illuminate\Database\Eloquent\Model;
11
 use Illuminate\Database\Eloquent\Model;
12
 use Illuminate\Support\Facades\Auth;
12
 use Illuminate\Support\Facades\Auth;
13
-use Illuminate\Support\Facades\DB;
14
 
13
 
15
 class CreateCurrency extends CreateRecord
14
 class CreateCurrency extends CreateRecord
16
 {
15
 {
38
         $user = Auth::user();
37
         $user = Auth::user();
39
 
38
 
40
         if (!$user) {
39
         if (!$user) {
41
-            throw new Halt('No authenticated user found.');
40
+            throw new Halt('No authenticated user found');
42
         }
41
         }
43
 
42
 
44
         return $this->handleRecordCreationWithUniqueField($data, new Currency(), $user);
43
         return $this->handleRecordCreationWithUniqueField($data, new Currency(), $user);

app/Filament/Resources/CurrencyResource/Pages/EditCurrency.php → app/Filament/Company/Resources/Setting/CurrencyResource/Pages/EditCurrency.php View File

1
 <?php
1
 <?php
2
 
2
 
3
-namespace App\Filament\Resources\CurrencyResource\Pages;
3
+namespace App\Filament\Company\Resources\Setting\CurrencyResource\Pages;
4
 
4
 
5
-use App\Filament\Resources\CurrencyResource;
5
+use App\Filament\Company\Resources\Setting\CurrencyResource;
6
 use App\Traits\HandlesResourceRecordUpdate;
6
 use App\Traits\HandlesResourceRecordUpdate;
7
-use Filament\Pages\Actions;
7
+use Filament\Actions;
8
 use Filament\Resources\Pages\EditRecord;
8
 use Filament\Resources\Pages\EditRecord;
9
 use Filament\Support\Exceptions\Halt;
9
 use Filament\Support\Exceptions\Halt;
10
 use Illuminate\Database\Eloquent\Model;
10
 use Illuminate\Database\Eloquent\Model;
16
 
16
 
17
     protected static string $resource = CurrencyResource::class;
17
     protected static string $resource = CurrencyResource::class;
18
 
18
 
19
-    protected function getActions(): array
19
+    protected function getHeaderActions(): array
20
     {
20
     {
21
         return [
21
         return [
22
             Actions\DeleteAction::make(),
22
             Actions\DeleteAction::make(),
23
         ];
23
         ];
24
     }
24
     }
25
 
25
 
26
-    protected function getRedirectUrl(): string
26
+    protected function getRedirectUrl(): ?string
27
     {
27
     {
28
         return $this->previousUrl;
28
         return $this->previousUrl;
29
     }
29
     }
43
         $user = Auth::user();
43
         $user = Auth::user();
44
 
44
 
45
         if (!$user) {
45
         if (!$user) {
46
-            throw new Halt('No authenticated user found.');
46
+            throw new Halt('No authenticated user found');
47
         }
47
         }
48
 
48
 
49
         return $this->handleRecordUpdateWithUniqueField($record, $data, $user);
49
         return $this->handleRecordUpdateWithUniqueField($record, $data, $user);
50
     }
50
     }
51
-
52
 }
51
 }

app/Filament/Resources/CurrencyResource/Pages/ListCurrencies.php → app/Filament/Company/Resources/Setting/CurrencyResource/Pages/ListCurrencies.php View File

1
 <?php
1
 <?php
2
 
2
 
3
-namespace App\Filament\Resources\CurrencyResource\Pages;
3
+namespace App\Filament\Company\Resources\Setting\CurrencyResource\Pages;
4
 
4
 
5
-use App\Filament\Resources\CurrencyResource;
6
-use Filament\Pages\Actions;
5
+use App\Filament\Company\Resources\Setting\CurrencyResource;
6
+use Filament\Actions;
7
 use Filament\Resources\Pages\ListRecords;
7
 use Filament\Resources\Pages\ListRecords;
8
 
8
 
9
 class ListCurrencies extends ListRecords
9
 class ListCurrencies extends ListRecords
10
 {
10
 {
11
     protected static string $resource = CurrencyResource::class;
11
     protected static string $resource = CurrencyResource::class;
12
 
12
 
13
-    protected function getActions(): array
13
+    protected function getHeaderActions(): array
14
     {
14
     {
15
         return [
15
         return [
16
             Actions\CreateAction::make(),
16
             Actions\CreateAction::make(),

+ 197
- 0
app/Filament/Company/Resources/Setting/DiscountResource.php View File

1
+<?php
2
+
3
+namespace App\Filament\Company\Resources\Setting;
4
+
5
+use App\Enums\DiscountComputation;
6
+use App\Enums\DiscountScope;
7
+use App\Enums\DiscountType;
8
+use App\Filament\Company\Resources\Setting\DiscountResource\Pages;
9
+use App\Filament\Company\Resources\Setting\DiscountResource\RelationManagers;
10
+use App\Models\Setting\Category;
11
+use App\Models\Setting\Discount;
12
+use Closure;
13
+use Filament\Forms;
14
+use Filament\Forms\Form;
15
+use Filament\Resources\Resource;
16
+use Filament\Tables;
17
+use Filament\Tables\Table;
18
+use Illuminate\Database\Eloquent\Builder;
19
+use Illuminate\Database\Eloquent\SoftDeletingScope;
20
+use Wallo\FilamentSelectify\Components\ToggleButton;
21
+
22
+class DiscountResource extends Resource
23
+{
24
+    protected static ?string $model = Discount::class;
25
+
26
+    protected static ?string $navigationIcon = 'heroicon-o-tag';
27
+
28
+    protected static ?string $navigationGroup = 'Settings';
29
+
30
+    protected static ?string $slug = 'settings/discounts';
31
+
32
+    public static function form(Form $form): Form
33
+    {
34
+        return $form
35
+            ->schema([
36
+                Forms\Components\Section::make('General')
37
+                    ->schema([
38
+                        Forms\Components\TextInput::make('name')
39
+                            ->label('Name')
40
+                            ->autofocus()
41
+                            ->required()
42
+                            ->maxLength(255)
43
+                            ->rule(static function (Forms\Get $get, Forms\Components\Component $component): Closure {
44
+                                return static function (string $attribute, $value, Closure $fail) use ($get, $component) {
45
+                                    $existingCategory = Discount::where('company_id', auth()->user()->currentCompany->id)
46
+                                                                ->where('name', $value)
47
+                                                                ->where('type', $get('type'))
48
+                                                                ->first();
49
+
50
+                                    if ($existingCategory && $existingCategory->getKey() !== $component->getRecord()?->getKey()) {
51
+                                        $type = $get('type')->getLabel();
52
+                                        $fail("The {$type} discount \"{$value}\" already exists.");
53
+                                    }
54
+                                };
55
+                            }),
56
+                        Forms\Components\TextInput::make('description')
57
+                            ->label('Description'),
58
+                        Forms\Components\Select::make('computation')
59
+                            ->label('Computation')
60
+                            ->options(DiscountComputation::class)
61
+                            ->default(DiscountComputation::Percentage)
62
+                            ->live()
63
+                            ->native(false)
64
+                            ->required(),
65
+                        Forms\Components\TextInput::make('rate')
66
+                            ->label('Rate')
67
+                            ->numeric()
68
+                            ->suffix(static function (Forms\Get $get) {
69
+                                $computation = $get('computation');
70
+
71
+                                if ($computation === DiscountComputation::Percentage) {
72
+                                    return '%';
73
+                                }
74
+
75
+                                return null;
76
+                            })
77
+                            ->required(),
78
+                        Forms\Components\Select::make('type')
79
+                            ->label('Type')
80
+                            ->options(DiscountType::class)
81
+                            ->default(DiscountType::Sales)
82
+                            ->native(false)
83
+                            ->required(),
84
+                        Forms\Components\Select::make('scope')
85
+                            ->label('Scope')
86
+                            ->options(DiscountScope::class)
87
+                            ->native(false),
88
+                        Forms\Components\DateTimePicker::make('start_date')
89
+                            ->label('Start Date')
90
+                            ->native(false)
91
+                            ->minDate(static function ($context, Discount|null $record = null) {
92
+                                if ($context === 'create') {
93
+                                    return today()->addDay();
94
+                                }
95
+
96
+                                return $record?->start_date?->isFuture() ? today()->addDay() : $record?->start_date;
97
+                            })
98
+                            ->maxDate(static function (callable $get, Discount|null $record = null) {
99
+                                $end_date = $get('end_date') ?? $record?->end_date;
100
+
101
+                                return $end_date ?: today()->addYear();
102
+                            })
103
+                            ->format('Y-m-d H:i:s')
104
+                            ->displayFormat('F d, Y H:i')
105
+                            ->seconds(false)
106
+                            ->live()
107
+                            ->disabled(static fn ($context, Discount|null $record = null) => $context === 'edit' && $record?->start_date?->isPast() ?? false)
108
+                            ->helperText(static fn (Forms\Components\DateTimePicker $component) => $component->isDisabled() ? 'Start date cannot be changed after the discount has begun.' : null),
109
+                        Forms\Components\DateTimePicker::make('end_date')
110
+                            ->label('End Date')
111
+                            ->native(false)
112
+                            ->live()
113
+                            ->minDate(static function (callable $get, Discount|null $record = null) {
114
+                                $start_date = $get('start_date') ?? $record?->start_date;
115
+
116
+                                return $start_date ?: today()->addDay();
117
+                            })
118
+                            ->maxDate(today()->addYear())
119
+                            ->format('Y-m-d H:i:s')
120
+                            ->displayFormat('F d, Y H:i')
121
+                            ->seconds(false),
122
+                        ToggleButton::make('enabled')
123
+                            ->label('Default'),
124
+                ])->columns(),
125
+            ]);
126
+    }
127
+
128
+    public static function table(Table $table): Table
129
+    {
130
+        return $table
131
+            ->columns([
132
+                Tables\Columns\TextColumn::make('name')
133
+                    ->label('Name')
134
+                    ->weight('semibold')
135
+                    ->icon(static fn (Discount $record) => $record->enabled ? 'heroicon-o-lock-closed' : null)
136
+                    ->tooltip(static fn (Discount $record) => $record->enabled ? "Default {$record->type->getLabel()} Discount" : null)
137
+                    ->iconPosition('after')
138
+                    ->searchable()
139
+                    ->sortable(),
140
+                Tables\Columns\TextColumn::make('computation')
141
+                    ->label('Computation')
142
+                    ->searchable()
143
+                    ->sortable(),
144
+                Tables\Columns\TextColumn::make('rate')
145
+                    ->label('Rate')
146
+                    ->formatStateUsing(static fn (Discount $record) => $record->rate . ($record->computation === DiscountComputation::Percentage ? '%' : null))
147
+                    ->searchable()
148
+                    ->sortable(),
149
+                Tables\Columns\TextColumn::make('type')
150
+                    ->label('Type')
151
+                    ->badge()
152
+                    ->searchable()
153
+                    ->sortable(),
154
+                Tables\Columns\TextColumn::make('start_date')
155
+                    ->label('Start Date')
156
+                    ->formatStateUsing(static fn (Discount $record) => $record->start_date ? $record->start_date->format('F d, Y H:i') : 'N/A')
157
+                    ->searchable()
158
+                    ->sortable(),
159
+                Tables\Columns\TextColumn::make('end_date')
160
+                    ->label('End Date')
161
+                    ->formatStateUsing(static fn (Discount $record) => $record->end_date ? $record->end_date->format('F d, Y H:i') : 'N/A')
162
+                    ->color(static fn(Discount $record) => $record->end_date?->isPast() ? 'danger' : null)
163
+                    ->searchable()
164
+                    ->sortable(),
165
+            ])
166
+            ->filters([
167
+                //
168
+            ])
169
+            ->actions([
170
+                Tables\Actions\EditAction::make(),
171
+            ])
172
+            ->bulkActions([
173
+                Tables\Actions\BulkActionGroup::make([
174
+                    Tables\Actions\DeleteBulkAction::make(),
175
+                ]),
176
+            ])
177
+            ->emptyStateActions([
178
+                Tables\Actions\CreateAction::make(),
179
+            ]);
180
+    }
181
+
182
+    public static function getRelations(): array
183
+    {
184
+        return [
185
+            //
186
+        ];
187
+    }
188
+
189
+    public static function getPages(): array
190
+    {
191
+        return [
192
+            'index' => Pages\ListDiscounts::route('/'),
193
+            'create' => Pages\CreateDiscount::route('/create'),
194
+            'edit' => Pages\EditDiscount::route('/{record}/edit'),
195
+        ];
196
+    }
197
+}

app/Filament/Resources/DiscountResource/Pages/CreateDiscount.php → app/Filament/Company/Resources/Setting/DiscountResource/Pages/CreateDiscount.php View File

1
 <?php
1
 <?php
2
 
2
 
3
-namespace App\Filament\Resources\DiscountResource\Pages;
3
+namespace App\Filament\Company\Resources\Setting\DiscountResource\Pages;
4
 
4
 
5
-use App\Filament\Resources\DiscountResource;
5
+use App\Filament\Company\Resources\Setting\DiscountResource;
6
 use App\Models\Setting\Discount;
6
 use App\Models\Setting\Discount;
7
 use App\Traits\HandlesResourceRecordCreation;
7
 use App\Traits\HandlesResourceRecordCreation;
8
+use Filament\Actions;
8
 use Filament\Resources\Pages\CreateRecord;
9
 use Filament\Resources\Pages\CreateRecord;
9
 use Filament\Support\Exceptions\Halt;
10
 use Filament\Support\Exceptions\Halt;
10
 use Illuminate\Database\Eloquent\Model;
11
 use Illuminate\Database\Eloquent\Model;
11
-use Illuminate\Support\Facades\Auth;
12
 
12
 
13
 class CreateDiscount extends CreateRecord
13
 class CreateDiscount extends CreateRecord
14
 {
14
 {
23
 
23
 
24
     protected function mutateFormDataBeforeCreate(array $data): array
24
     protected function mutateFormDataBeforeCreate(array $data): array
25
     {
25
     {
26
-        $data['enabled'] = (bool)($data['enabled']);
26
+        $data['enabled'] = (bool)$data['enabled'];
27
 
27
 
28
         return $data;
28
         return $data;
29
     }
29
     }
33
      */
33
      */
34
     protected function handleRecordCreation(array $data): Model
34
     protected function handleRecordCreation(array $data): Model
35
     {
35
     {
36
-        $user = Auth::user();
36
+        $user = auth()->user();
37
 
37
 
38
         if (!$user) {
38
         if (!$user) {
39
-            throw new Halt('No authenticated user found.');
39
+            throw new Halt('No authenticated user found');
40
         }
40
         }
41
 
41
 
42
         return $this->handleRecordCreationWithUniqueField($data, new Discount(), $user, 'type');
42
         return $this->handleRecordCreationWithUniqueField($data, new Discount(), $user, 'type');

app/Filament/Resources/DiscountResource/Pages/EditDiscount.php → app/Filament/Company/Resources/Setting/DiscountResource/Pages/EditDiscount.php View File

1
 <?php
1
 <?php
2
 
2
 
3
-namespace App\Filament\Resources\DiscountResource\Pages;
3
+namespace App\Filament\Company\Resources\Setting\DiscountResource\Pages;
4
 
4
 
5
-use App\Filament\Resources\DiscountResource;
5
+use App\Filament\Company\Resources\Setting\DiscountResource;
6
 use App\Traits\HandlesResourceRecordUpdate;
6
 use App\Traits\HandlesResourceRecordUpdate;
7
-use Filament\Pages\Actions;
7
+use Filament\Actions;
8
 use Filament\Resources\Pages\EditRecord;
8
 use Filament\Resources\Pages\EditRecord;
9
 use Filament\Support\Exceptions\Halt;
9
 use Filament\Support\Exceptions\Halt;
10
 use Illuminate\Database\Eloquent\Model;
10
 use Illuminate\Database\Eloquent\Model;
11
-use Illuminate\Support\Facades\Auth;
12
 
11
 
13
 class EditDiscount extends EditRecord
12
 class EditDiscount extends EditRecord
14
 {
13
 {
16
 
15
 
17
     protected static string $resource = DiscountResource::class;
16
     protected static string $resource = DiscountResource::class;
18
 
17
 
19
-    protected function getActions(): array
18
+    protected function getHeaderActions(): array
20
     {
19
     {
21
         return [
20
         return [
22
             Actions\DeleteAction::make(),
21
             Actions\DeleteAction::make(),
40
      */
39
      */
41
     protected function handleRecordUpdate(Model $record, array $data): Model
40
     protected function handleRecordUpdate(Model $record, array $data): Model
42
     {
41
     {
43
-        $user = Auth::user();
42
+        $user = auth()->user();
44
 
43
 
45
         if (!$user) {
44
         if (!$user) {
46
-            throw new Halt('No authenticated user found.');
45
+            throw new Halt('No authenticated user found');
47
         }
46
         }
48
 
47
 
49
         return $this->handleRecordUpdateWithUniqueField($record, $data, $user, 'type');
48
         return $this->handleRecordUpdateWithUniqueField($record, $data, $user, 'type');

app/Filament/Resources/DiscountResource/Pages/ListDiscounts.php → app/Filament/Company/Resources/Setting/DiscountResource/Pages/ListDiscounts.php View File

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

+ 191
- 0
app/Filament/Company/Resources/Setting/TaxResource.php View File

1
+<?php
2
+
3
+namespace App\Filament\Company\Resources\Setting;
4
+
5
+use App\Enums\TaxComputation;
6
+use App\Enums\TaxScope;
7
+use App\Enums\TaxType;
8
+use App\Filament\Company\Resources\Setting\TaxResource\Pages;
9
+use App\Filament\Company\Resources\Setting\TaxResource\RelationManagers;
10
+use App\Models\Setting\Category;
11
+use App\Models\Setting\Tax;
12
+use Closure;
13
+use Filament\Forms;
14
+use Filament\Forms\Form;
15
+use Filament\Notifications\Notification;
16
+use Filament\Resources\Resource;
17
+use Filament\Tables;
18
+use Filament\Tables\Table;
19
+use Illuminate\Database\Eloquent\Builder;
20
+use Illuminate\Database\Eloquent\Collection;
21
+use Illuminate\Database\Eloquent\SoftDeletingScope;
22
+use Wallo\FilamentSelectify\Components\ToggleButton;
23
+
24
+class TaxResource extends Resource
25
+{
26
+    protected static ?string $model = Tax::class;
27
+
28
+    protected static ?string $navigationIcon = 'heroicon-o-receipt-percent';
29
+
30
+    protected static ?string $navigationGroup = 'Settings';
31
+
32
+    protected static ?string $slug = 'settings/taxes';
33
+
34
+    public static function form(Form $form): Form
35
+    {
36
+        return $form
37
+            ->schema([
38
+                Forms\Components\Section::make('General')
39
+                    ->schema([
40
+                        Forms\Components\TextInput::make('name')
41
+                            ->label('Name')
42
+                            ->autofocus()
43
+                            ->required()
44
+                            ->maxLength(255)
45
+                            ->rule(static function (Forms\Get $get, Forms\Components\Component $component): Closure {
46
+                                return static function (string $attribute, $value, Closure $fail) use ($get, $component) {
47
+                                    $existingCategory = Tax::where('company_id', auth()->user()->currentCompany->id)
48
+                                                                ->where('name', $value)
49
+                                                                ->where('type', $get('type'))
50
+                                                                ->first();
51
+
52
+                                    if ($existingCategory && $existingCategory->getKey() !== $component->getRecord()?->getKey()) {
53
+                                        $type = $get('type')->getLabel();
54
+                                        $fail("The {$type} tax \"{$value}\" already exists.");
55
+                                    }
56
+                                };
57
+                            }),
58
+                        Forms\Components\TextInput::make('description')
59
+                            ->label('Description'),
60
+                        Forms\Components\Select::make('computation')
61
+                            ->label('Computation')
62
+                            ->options(TaxComputation::class)
63
+                            ->default(TaxComputation::Percentage)
64
+                            ->live()
65
+                            ->native(false)
66
+                            ->required(),
67
+                        Forms\Components\TextInput::make('rate')
68
+                            ->label('Rate')
69
+                            ->numeric()
70
+                            ->suffix(static function (Forms\Get $get) {
71
+                                $computation = $get('computation');
72
+
73
+                                if ($computation === TaxComputation::Percentage) {
74
+                                    return '%';
75
+                                }
76
+
77
+                                return null;
78
+                            })
79
+                            ->required(),
80
+                        Forms\Components\Select::make('type')
81
+                            ->label('Type')
82
+                            ->options(TaxType::class)
83
+                            ->default(TaxType::Sales)
84
+                            ->native(false)
85
+                            ->required(),
86
+                        Forms\Components\Select::make('scope')
87
+                            ->label('Scope')
88
+                            ->options(TaxScope::class)
89
+                            ->native(false),
90
+                        ToggleButton::make('enabled')
91
+                            ->label('Enabled'),
92
+                    ])->columns(),
93
+            ]);
94
+    }
95
+
96
+    public static function table(Table $table): Table
97
+    {
98
+        return $table
99
+            ->columns([
100
+                Tables\Columns\TextColumn::make('name')
101
+                    ->label('Name')
102
+                    ->weight('semibold')
103
+                    ->icon(static fn (Tax $record) => $record->enabled ? 'heroicon-o-lock-closed' : null)
104
+                    ->tooltip(static fn (Tax $record) => $record->enabled ? "Default {$record->type->getLabel()} Tax" : null)
105
+                    ->iconPosition('after')
106
+                    ->searchable()
107
+                    ->sortable(),
108
+                Tables\Columns\TextColumn::make('computation')
109
+                    ->label('Computation')
110
+                    ->searchable()
111
+                    ->sortable(),
112
+                Tables\Columns\TextColumn::make('rate')
113
+                    ->label('Rate')
114
+                    ->formatStateUsing(static fn (Tax $record) => $record->rate . ($record->computation === TaxComputation::Percentage ? '%' : null))
115
+                    ->searchable()
116
+                    ->sortable(),
117
+                Tables\Columns\TextColumn::make('type')
118
+                    ->label('Type')
119
+                    ->badge()
120
+                    ->searchable()
121
+                    ->sortable(),
122
+            ])
123
+            ->filters([
124
+                //
125
+            ])
126
+            ->actions([
127
+                Tables\Actions\EditAction::make(),
128
+                Tables\Actions\DeleteAction::make()
129
+                    ->before(static function (Tables\Actions\DeleteAction $action, Tax $record) {
130
+                        if ($record->enabled) {
131
+                            Notification::make()
132
+                                ->danger()
133
+                                ->title('Action Denied')
134
+                                ->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' => $record->type->getLabel()]))
135
+                                ->persistent()
136
+                                ->send();
137
+
138
+                            $action->cancel();
139
+                        }
140
+                    }),
141
+            ])
142
+            ->bulkActions([
143
+                Tables\Actions\BulkActionGroup::make([
144
+                    Tables\Actions\DeleteBulkAction::make()
145
+                        ->before(static function (Collection $records, Tables\Actions\DeleteBulkAction $action) {
146
+                            $defaultTaxes = $records->filter(static function (Tax $record) {
147
+                                return $record->enabled;
148
+                            });
149
+
150
+                            if ($defaultTaxes->isNotEmpty()) {
151
+                                $defaultTaxNames = $defaultTaxes->pluck('name')->toArray();
152
+
153
+                                Notification::make()
154
+                                    ->danger()
155
+                                    ->title('Action Denied')
156
+                                    ->body(static function () use ($defaultTaxNames) {
157
+                                        $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>";
158
+                                        $message .= implode("<br>", array_map(static function ($name) {
159
+                                            return "&bull; " . $name;
160
+                                        }, $defaultTaxNames));
161
+                                        return $message;
162
+                                    })
163
+                                    ->persistent()
164
+                                    ->send();
165
+
166
+                                $action->cancel();
167
+                            }
168
+                        }),
169
+                ]),
170
+            ])
171
+            ->emptyStateActions([
172
+                Tables\Actions\CreateAction::make(),
173
+            ]);
174
+    }
175
+
176
+    public static function getRelations(): array
177
+    {
178
+        return [
179
+            //
180
+        ];
181
+    }
182
+
183
+    public static function getPages(): array
184
+    {
185
+        return [
186
+            'index' => Pages\ListTaxes::route('/'),
187
+            'create' => Pages\CreateTax::route('/create'),
188
+            'edit' => Pages\EditTax::route('/{record}/edit'),
189
+        ];
190
+    }
191
+}

app/Filament/Resources/TaxResource/Pages/CreateTax.php → app/Filament/Company/Resources/Setting/TaxResource/Pages/CreateTax.php View File

1
 <?php
1
 <?php
2
 
2
 
3
-namespace App\Filament\Resources\TaxResource\Pages;
3
+namespace App\Filament\Company\Resources\Setting\TaxResource\Pages;
4
 
4
 
5
-use App\Filament\Resources\TaxResource;
5
+use App\Filament\Company\Resources\Setting\TaxResource;
6
 use App\Models\Setting\Tax;
6
 use App\Models\Setting\Tax;
7
 use App\Traits\HandlesResourceRecordCreation;
7
 use App\Traits\HandlesResourceRecordCreation;
8
+use Filament\Actions;
8
 use Filament\Resources\Pages\CreateRecord;
9
 use Filament\Resources\Pages\CreateRecord;
9
 use Filament\Support\Exceptions\Halt;
10
 use Filament\Support\Exceptions\Halt;
10
 use Illuminate\Database\Eloquent\Model;
11
 use Illuminate\Database\Eloquent\Model;
11
-use Illuminate\Support\Facades\Auth;
12
 
12
 
13
 class CreateTax extends CreateRecord
13
 class CreateTax extends CreateRecord
14
 {
14
 {
33
      */
33
      */
34
     protected function handleRecordCreation(array $data): Model
34
     protected function handleRecordCreation(array $data): Model
35
     {
35
     {
36
-        $user = Auth::user();
36
+        $user = auth()->user();
37
 
37
 
38
         if (!$user) {
38
         if (!$user) {
39
-            throw new Halt('No authenticated user found.');
39
+            throw new Halt('No authenticated user found');
40
         }
40
         }
41
 
41
 
42
         return $this->handleRecordCreationWithUniqueField($data, new Tax(), $user, 'type');
42
         return $this->handleRecordCreationWithUniqueField($data, new Tax(), $user, 'type');

app/Filament/Resources/TaxResource/Pages/EditTax.php → app/Filament/Company/Resources/Setting/TaxResource/Pages/EditTax.php View File

1
 <?php
1
 <?php
2
 
2
 
3
-namespace App\Filament\Resources\TaxResource\Pages;
3
+namespace App\Filament\Company\Resources\Setting\TaxResource\Pages;
4
 
4
 
5
-use App\Filament\Resources\TaxResource;
5
+use App\Filament\Company\Resources\Setting\TaxResource;
6
 use App\Traits\HandlesResourceRecordUpdate;
6
 use App\Traits\HandlesResourceRecordUpdate;
7
-use Filament\Pages\Actions;
7
+use Filament\Actions;
8
 use Filament\Resources\Pages\EditRecord;
8
 use Filament\Resources\Pages\EditRecord;
9
 use Filament\Support\Exceptions\Halt;
9
 use Filament\Support\Exceptions\Halt;
10
 use Illuminate\Database\Eloquent\Model;
10
 use Illuminate\Database\Eloquent\Model;
11
-use Illuminate\Support\Facades\Auth;
12
 
11
 
13
 class EditTax extends EditRecord
12
 class EditTax extends EditRecord
14
 {
13
 {
16
 
15
 
17
     protected static string $resource = TaxResource::class;
16
     protected static string $resource = TaxResource::class;
18
 
17
 
19
-    protected function getActions(): array
18
+    protected function getHeaderActions(): array
20
     {
19
     {
21
         return [
20
         return [
22
             Actions\DeleteAction::make(),
21
             Actions\DeleteAction::make(),
28
         return $this->previousUrl;
27
         return $this->previousUrl;
29
     }
28
     }
30
 
29
 
31
-    protected function mutateFormDataBeforeUpdate(array $data): array
30
+    protected function mutateFormDataBeforeSave(array $data): array
32
     {
31
     {
33
         $data['enabled'] = (bool)$data['enabled'];
32
         $data['enabled'] = (bool)$data['enabled'];
34
 
33
 
40
      */
39
      */
41
     protected function handleRecordUpdate(Model $record, array $data): Model
40
     protected function handleRecordUpdate(Model $record, array $data): Model
42
     {
41
     {
43
-        $user = Auth::user();
42
+        $user = auth()->user();
44
 
43
 
45
         if (!$user) {
44
         if (!$user) {
46
-            throw new Halt('No authenticated user found.');
45
+            throw new Halt('No authenticated user found');
47
         }
46
         }
48
 
47
 
49
         return $this->handleRecordUpdateWithUniqueField($record, $data, $user, 'type');
48
         return $this->handleRecordUpdateWithUniqueField($record, $data, $user, 'type');

app/Filament/Resources/TaxResource/Pages/ListTaxes.php → app/Filament/Company/Resources/Setting/TaxResource/Pages/ListTaxes.php View File

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

+ 0
- 39
app/Filament/Pages/Companies.php View File

1
-<?php
2
-
3
-namespace App\Filament\Pages;
4
-
5
-use Filament\Pages\Page;
6
-use Illuminate\Support\Facades\Auth;
7
-use Wallo\FilamentCompanies\FilamentCompanies;
8
-
9
-class Companies extends Page
10
-{
11
-    protected static ?string $navigationIcon = 'heroicon-o-office-building';
12
-
13
-    protected static string $view = 'filament.pages.companies';
14
-
15
-    protected static function shouldRegisterNavigation(): bool
16
-    {
17
-        return Auth::user()->currentCompany->id === 1;
18
-    }
19
-
20
-    public function mount(): void
21
-    {
22
-        abort_unless(Auth::user()->currentCompany->id === 1, 403);
23
-    }
24
-
25
-    protected function getHeaderWidgets(): array
26
-    {
27
-        return [
28
-            Widgets\Companies\Charts\CompanyStatsOverview::class,
29
-            Widgets\Companies\Charts\CumulativeGrowth::class,
30
-            Widgets\Companies\Charts\CumulativeTotal::class,
31
-            Widgets\Companies\Tables\Companies::class,
32
-        ];
33
-    }
34
-
35
-    protected static function getNavigationBadge(): ?string
36
-    {
37
-        return FilamentCompanies::companyModel()::count();
38
-    }
39
-}

+ 0
- 51
app/Filament/Pages/CompanyDetails.php View File

1
-<?php
2
-
3
-namespace App\Filament\Pages;
4
-
5
-use Filament\Pages\Page;
6
-use Illuminate\Support\Facades\Auth;
7
-use Illuminate\Support\Facades\Gate;
8
-use Wallo\FilamentCompanies\FilamentCompanies;
9
-
10
-class CompanyDetails extends Page
11
-{
12
-    public mixed $company;
13
-
14
-    protected static ?string $navigationIcon = 'heroicon-o-document-text';
15
-
16
-    protected static ?string $navigationLabel = 'Company';
17
-
18
-    protected static ?string $navigationGroup = 'Settings';
19
-
20
-    protected static ?string $title = 'Company';
21
-
22
-    protected static string $view = 'filament.pages.company-details';
23
-
24
-    public function mount($company): void
25
-    {
26
-        $this->company = FilamentCompanies::newCompanyModel()->findOrFail($company);
27
-        $this->authorizeAccess();
28
-    }
29
-
30
-    protected function authorizeAccess(): void
31
-    {
32
-        Gate::authorize('view', $this->company);
33
-    }
34
-
35
-    public static function getSlug(): string
36
-    {
37
-        return '{company}/settings/company';
38
-    }
39
-
40
-    public static function getUrl(array $parameters = [], bool $isAbsolute = true): string
41
-    {
42
-        return route(static::getRouteName(), ['company' => Auth::user()->currentCompany], $isAbsolute);
43
-    }
44
-
45
-    protected function getBreadcrumbs(): array
46
-    {
47
-        return [
48
-            'company' => 'Company',
49
-        ];
50
-    }
51
-}

+ 0
- 10
app/Filament/Pages/Dashboard.php View File

1
-<?php
2
-
3
-namespace App\Filament\Pages;
4
-
5
-use Filament\Pages\Dashboard as BasePage;
6
-
7
-class Dashboard extends BasePage
8
-{
9
-    //
10
-}

+ 0
- 51
app/Filament/Pages/DefaultSetting.php View File

1
-<?php
2
-
3
-namespace App\Filament\Pages;
4
-
5
-use Filament\Pages\Page;
6
-use Illuminate\Support\Facades\Auth;
7
-use Illuminate\Support\Facades\Gate;
8
-use Wallo\FilamentCompanies\FilamentCompanies;
9
-
10
-class DefaultSetting extends Page
11
-{
12
-    public mixed $company;
13
-
14
-    protected static ?string $navigationIcon = 'heroicon-o-adjustments';
15
-
16
-    protected static ?string $navigationLabel = 'Default';
17
-
18
-    protected static ?string $navigationGroup = 'Settings';
19
-
20
-    protected static ?string $title = 'Default';
21
-
22
-    protected static string $view = 'filament.pages.default-setting';
23
-
24
-    public function mount($company): void
25
-    {
26
-        $this->company = FilamentCompanies::newCompanyModel()->findOrFail($company);
27
-        $this->authorizeAccess();
28
-    }
29
-
30
-    protected function authorizeAccess(): void
31
-    {
32
-        Gate::authorize('view', $this->company);
33
-    }
34
-
35
-    public static function getSlug(): string
36
-    {
37
-        return '{company}/settings/default';
38
-    }
39
-
40
-    public static function getUrl(array $parameters = [], bool $isAbsolute = true): string
41
-    {
42
-        return route(static::getRouteName(), ['company' => Auth::user()->currentCompany], $isAbsolute);
43
-    }
44
-
45
-    protected function getBreadcrumbs(): array
46
-    {
47
-        return [
48
-            'default' => 'Default',
49
-        ];
50
-    }
51
-}

+ 0
- 38
app/Filament/Pages/Employees.php View File

1
-<?php
2
-
3
-namespace App\Filament\Pages;
4
-
5
-use Filament\Pages\Page;
6
-use Illuminate\Support\Facades\Auth;
7
-use Wallo\FilamentCompanies\FilamentCompanies;
8
-
9
-class Employees extends Page
10
-{
11
-    protected static ?string $navigationIcon = 'heroicon-o-users';
12
-
13
-    protected static string $view = 'filament.pages.employees';
14
-
15
-    protected static function shouldRegisterNavigation(): bool
16
-    {
17
-        return Auth::user()->currentCompany->id === 1;
18
-    }
19
-
20
-    public function mount(): void
21
-    {
22
-        abort_unless(Auth::user()->currentCompany->id === 1, 403);
23
-    }
24
-
25
-    protected function getHeaderWidgets(): array
26
-    {
27
-        return [
28
-            Widgets\Employees\Charts\CumulativeRoles::class,
29
-            Widgets\Employees\Charts\CumulativeGrowth::class,
30
-            Widgets\Employees\Tables\Employees::class,
31
-        ];
32
-    }
33
-
34
-    protected static function getNavigationBadge(): ?string
35
-    {
36
-        return FilamentCompanies::employeeshipModel()::count();
37
-    }
38
-}

+ 0
- 51
app/Filament/Pages/Invoice.php View File

1
-<?php
2
-
3
-namespace App\Filament\Pages;
4
-
5
-use Filament\Pages\Page;
6
-use Illuminate\Support\Facades\Auth;
7
-use Illuminate\Support\Facades\Gate;
8
-use Wallo\FilamentCompanies\FilamentCompanies;
9
-
10
-class Invoice extends Page
11
-{
12
-    public mixed $company;
13
-
14
-    protected static ?string $navigationIcon = 'heroicon-o-document-text';
15
-
16
-    protected static ?string $navigationLabel = 'Invoice';
17
-
18
-    protected static ?string $navigationGroup = 'Settings';
19
-
20
-    protected static ?string $title = 'Invoice';
21
-
22
-    protected static string $view = 'filament.pages.invoice';
23
-
24
-    public function mount($company): void
25
-    {
26
-        $this->company = FilamentCompanies::newCompanyModel()->findOrFail($company);
27
-        $this->authorizeAccess();
28
-    }
29
-
30
-    protected function authorizeAccess(): void
31
-    {
32
-        Gate::authorize('view', $this->company);
33
-    }
34
-
35
-    public static function getSlug(): string
36
-    {
37
-        return '{company}/settings/invoice';
38
-    }
39
-
40
-    public static function getUrl(array $parameters = [], bool $isAbsolute = true): string
41
-    {
42
-        return route(static::getRouteName(), ['company' => Auth::user()->currentCompany], $isAbsolute);
43
-    }
44
-
45
-    protected function getBreadcrumbs(): array
46
-    {
47
-        return [
48
-            'invoice' => 'Invoice',
49
-        ];
50
-    }
51
-}

+ 0
- 36
app/Filament/Pages/Users.php View File

1
-<?php
2
-
3
-namespace App\Filament\Pages;
4
-
5
-use Filament\Pages\Page;
6
-use Illuminate\Support\Facades\Auth;
7
-use Wallo\FilamentCompanies\FilamentCompanies;
8
-
9
-class Users extends Page
10
-{
11
-    protected static ?string $navigationIcon = 'heroicon-o-user-group';
12
-
13
-    protected static string $view = 'filament.pages.users';
14
-
15
-    protected static function shouldRegisterNavigation(): bool
16
-    {
17
-        return Auth::user()->currentCompany->id === 1;
18
-    }
19
-
20
-    public function mount(): void
21
-    {
22
-        abort_unless(Auth::user()->currentCompany->id === 1, 403);
23
-    }
24
-
25
-    protected function getHeaderWidgets(): array
26
-    {
27
-        return [
28
-            Widgets\Users\Tables\Users::class,
29
-        ];
30
-    }
31
-
32
-    protected static function getNavigationBadge(): ?string
33
-    {
34
-        return FilamentCompanies::userModel()::count();
35
-    }
36
-}

+ 0
- 148
app/Filament/Pages/Widgets/Companies/Charts/CompanyStatsOverview.php View File

1
-<?php
2
-
3
-namespace App\Filament\Pages\Widgets\Companies\Charts;
4
-
5
-use App\Models\Company;
6
-use Exception;
7
-use Filament\Widgets\StatsOverviewWidget;
8
-use InvalidArgumentException;
9
-
10
-class CompanyStatsOverview extends StatsOverviewWidget
11
-{
12
-    protected static ?int $sort = 0;
13
-
14
-    /**
15
-     * Holt's Linear Trend Method
16
-     * @throws Exception
17
-     */
18
-    protected function holtLinearTrend($data, $alpha, $beta): array
19
-    {
20
-        if (count($data) < 2 || array_filter($data, 'is_numeric') !== $data) {
21
-            throw new InvalidArgumentException('Insufficient or invalid data for Holt\'s Linear Trend calculation', 400);
22
-        }
23
-
24
-        $level = $data[0];
25
-        $trend = $data[1] - $data[0];
26
-
27
-        $forecast = [];
28
-        foreach ($data as $iValue) {
29
-            $prev_level = $level;
30
-            $level = $alpha * $iValue + (1 - $alpha) * ($prev_level + $trend);
31
-            $trend = $beta * ($level - $prev_level) + (1 - $beta) * $trend;
32
-            $forecast[] = $level + $trend;
33
-        }
34
-
35
-        return $forecast;
36
-    }
37
-
38
-    /**
39
-     * Adjusts the alpha and beta parameters based on the model's performance
40
-     * @throws Exception
41
-     */
42
-    protected function adjustTrendParameters($data, $alpha, $beta): array
43
-    {
44
-        $minError = PHP_INT_MAX;
45
-        $bestAlpha = $alpha;
46
-        $bestBeta = $beta;
47
-
48
-        // try different alpha and beta values within a reasonable range
49
-        for ($testAlpha = 0.1; $testAlpha <= 1; $testAlpha += 0.1) {
50
-            for ($testBeta = 0.1; $testBeta <= 1; $testBeta += 0.1) {
51
-                $forecast = $this->holtLinearTrend($data, $testAlpha, $testBeta);
52
-                $error = $this->calculateError($data, $forecast);
53
-                if ($error < $minError) {
54
-                    $minError = $error;
55
-                    $bestAlpha = $testAlpha;
56
-                    $bestBeta = $testBeta;
57
-                }
58
-            }
59
-        }
60
-
61
-        return [$bestAlpha, $bestBeta];
62
-    }
63
-
64
-    /**
65
-     * Calculates the sum of squared errors between the actual data and the forecast
66
-     */
67
-    protected function calculateError($data, $forecast): float
68
-    {
69
-        $error = 0;
70
-        for ($i = 0, $iMax = count($data); $i < $iMax; $i++) {
71
-            $error += ($data[$i] - $forecast[$i]) ** 2;
72
-        }
73
-
74
-        return $error;
75
-    }
76
-
77
-    /**
78
-     * Chart Options
79
-     * @throws Exception
80
-     */
81
-    protected function getCards(): array
82
-    {
83
-        // Define constants
84
-        $alpha = 0.8;
85
-        $beta = 0.2;
86
-
87
-        // Define time variables
88
-        $startOfYear = today()->startOfYear();
89
-        $today = today();
90
-
91
-        // Get Company Data
92
-        $companyData = Company::selectRaw("COUNT(*) as aggregate, YEARWEEK(created_at, 3) as week")
93
-            ->whereBetween('created_at', [$startOfYear, $today])
94
-            ->groupByRaw('week')
95
-            ->get();
96
-
97
-        // Initialize weeks
98
-        $weeks = [];
99
-        for ($week = $startOfYear->copy(); $week->lte($today); $week->addWeek()) {
100
-            $weeks[$week->format('oW')] = 0;
101
-        }
102
-
103
-        // Get Weekly Data for Company Data
104
-        $weeklyData = collect($weeks)->mapWithKeys(static function ($value, $week) use ($companyData) {
105
-            $matchingData = $companyData->firstWhere('week', $week);
106
-            return [$week => $matchingData->aggregate ?? 0];
107
-        });
108
-
109
-        // Calculate total companies per week
110
-        $totalCompanies = $weeklyData->reduce(static function ($carry, $value) {
111
-            $carry[] = ($carry ? end($carry) : 0) + $value;
112
-            return $carry;
113
-        }, []);
114
-
115
-        // Calculate new companies and percentage change per week
116
-        $newCompanies = [0];
117
-        $weeklyPercentageChange = [0];
118
-
119
-        for ($i = 1, $iMax = count($totalCompanies); $i < $iMax; $i++) {
120
-            $newCompanies[] = $totalCompanies[$i] - $totalCompanies[$i - 1];
121
-            $weeklyPercentageChange[] = $totalCompanies[$i - 1] !== 0 ? ($newCompanies[$i] / $totalCompanies[$i - 1]) * 100 : 0;
122
-        }
123
-
124
-        // Ensure $weeklyDataArray contains at least two values and all values are numeric
125
-        $weeklyDataArray = $weeklyData->values()->toArray();
126
-        if (count($weeklyDataArray) < 2 || array_filter($weeklyDataArray, 'is_numeric') !== $weeklyDataArray) {
127
-            throw new InvalidArgumentException('Insufficient or invalid data for Holt\'s Linear Trend calculation', 400);
128
-        }
129
-
130
-        // Adjust alpha and beta parameters
131
-        [$alpha, $beta] = $this->adjustTrendParameters($weeklyDataArray, $alpha, $beta);
132
-
133
-        // Calculate Holt's Linear Trend Forecast for next week
134
-        $holtForecast = $this->holtLinearTrend($weeklyDataArray, $alpha, $beta);
135
-        $expectedNewCompanies = round(end($holtForecast));
136
-
137
-        // Calculate average weekly growth rate
138
-        $totalWeeks = $startOfYear->diffInWeeks($today);
139
-        $averageWeeklyGrowthRate = round(array_sum($weeklyPercentageChange) / $totalWeeks, 2);
140
-
141
-        // Company Stats Overview Cards
142
-        return [
143
-            StatsOverviewWidget\Card::make("New Companies Forecast (Holt's Linear Trend)", $expectedNewCompanies),
144
-            StatsOverviewWidget\Card::make('Average Weekly Growth Rate', $averageWeeklyGrowthRate . '%'),
145
-            StatsOverviewWidget\Card::make('Personal Companies', Company::sum('personal_company')),
146
-        ];
147
-    }
148
-}

+ 0
- 154
app/Filament/Pages/Widgets/Companies/Charts/CumulativeGrowth.php View File

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[] = $totalCompanies[$i - 1] !== 0 ? ($newCompanies[$i] / $totalCompanies[$i - 1]) * 100 : 0;
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
-}

+ 0
- 143
app/Filament/Pages/Widgets/Companies/Charts/CumulativeTotal.php View File

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
-}

+ 0
- 72
app/Filament/Pages/Widgets/Companies/Tables/Companies.php View File

1
-<?php
2
-
3
-namespace App\Filament\Pages\Widgets\Companies\Tables;
4
-
5
-use App\Models\Company;
6
-use Closure;
7
-use Exception;
8
-use Filament\Tables;
9
-use Filament\Widgets\TableWidget as PageWidget;
10
-use Illuminate\Contracts\Support\Htmlable;
11
-use Illuminate\Database\Eloquent\Builder;
12
-use Illuminate\Database\Eloquent\Relations\Relation;
13
-
14
-class Companies extends PageWidget
15
-{
16
-    protected int | string | array $columnSpan = 'full';
17
-
18
-    protected static ?int $sort = 3;
19
-
20
-    protected function getTableQuery(): Builder|Relation
21
-    {
22
-        return Company::query();
23
-    }
24
-
25
-    protected function getTableHeading(): string|Htmlable|Closure|null
26
-    {
27
-        return null;
28
-    }
29
-
30
-    /**
31
-     * @throws Exception
32
-     */
33
-    protected function getTableFilters(): array
34
-    {
35
-        return [
36
-            Tables\Filters\SelectFilter::make('name')
37
-                ->label('Owner')
38
-                ->searchable()
39
-                ->relationship('owner', 'name'),
40
-            Tables\Filters\TernaryFilter::make('personal_company')
41
-                ->label('Personal Company')
42
-        ];
43
-    }
44
-
45
-    protected function getTableColumns(): array
46
-    {
47
-        return [
48
-            Tables\Columns\ViewColumn::make('owner.name')
49
-                ->view('filament.components.companies.avatar-column')
50
-                ->label('Owner')
51
-                ->sortable()
52
-                ->searchable()
53
-                ->grow(false),
54
-            Tables\Columns\TextColumn::make('name')
55
-                ->label('Company')
56
-                ->sortable()
57
-                ->searchable(),
58
-            Tables\Columns\TextColumn::make('users_count')
59
-                ->label('Employees')
60
-                ->counts('users')
61
-                ->sortable(),
62
-            Tables\Columns\IconColumn::make('personal_company')
63
-                ->label('Personal Company')
64
-                ->boolean()
65
-                ->sortable()
66
-                ->trueIcon('heroicon-o-badge-check')
67
-                ->falseIcon('heroicon-o-x-circle')
68
-                ->trueColor('primary')
69
-                ->falseColor('secondary')
70
-        ];
71
-    }
72
-}

+ 0
- 153
app/Filament/Pages/Widgets/Employees/Charts/CumulativeGrowth.php View File

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[] = $totalEmployees[$i - 1] !== 0 ? ($newEmployees[$i] / $totalEmployees[$i - 1]) * 100 : 0;
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
-}

+ 0
- 163
app/Filament/Pages/Widgets/Employees/Charts/CumulativeRoles.php View File

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
-}

+ 0
- 73
app/Filament/Pages/Widgets/Employees/Tables/Employees.php View File

1
-<?php
2
-
3
-namespace App\Filament\Pages\Widgets\Employees\Tables;
4
-
5
-use App\Models\User;
6
-use Closure;
7
-use Exception;
8
-use Filament\Tables;
9
-use Filament\Widgets\TableWidget as PageWidget;
10
-use Illuminate\Contracts\Support\Htmlable;
11
-use Illuminate\Database\Eloquent\Builder;
12
-use Illuminate\Database\Eloquent\Relations\Relation;
13
-
14
-class Employees extends PageWidget
15
-{
16
-    protected int | string | array $columnSpan = 'full';
17
-
18
-    protected static ?int $sort = 2;
19
-
20
-    protected function getTableQuery(): Builder|Relation
21
-    {
22
-        return User::whereHas('employeeships');
23
-    }
24
-
25
-    protected function getTableHeading(): string|Htmlable|Closure|null
26
-    {
27
-        return null;
28
-    }
29
-
30
-    /**
31
-     * @throws Exception
32
-     */
33
-    protected function getTableFilters(): array
34
-    {
35
-        return [
36
-            Tables\Filters\SelectFilter::make('name')
37
-                ->label('Company')
38
-                ->searchable()
39
-                ->relationship('companies', 'name', static fn (Builder $query) => $query->whereHas('users')),
40
-        ];
41
-    }
42
-
43
-    protected function getTableColumns(): array
44
-    {
45
-        return [
46
-            Tables\Columns\ViewColumn::make('name')
47
-                ->view('filament.components.users.avatar-column')
48
-                ->label('Name')
49
-                ->sortable()
50
-                ->searchable()
51
-                ->grow(false),
52
-            Tables\Columns\TextColumn::make('companies.name')
53
-                ->label('Company')
54
-                ->sortable()
55
-                ->searchable(),
56
-            Tables\Columns\BadgeColumn::make('employeeships.role')
57
-                ->label('Role')
58
-                ->enum([
59
-                    'admin' => 'Administrator',
60
-                    'editor' => 'Editor',
61
-                ])
62
-                ->icons([
63
-                    'heroicon-o-shield-check' => 'admin',
64
-                    'heroicon-o-pencil' => 'editor',
65
-                ])
66
-                ->colors([
67
-                    'primary' => 'admin',
68
-                    'warning' => 'editor',
69
-                ])
70
-                ->sortable(),
71
-        ];
72
-    }
73
-}

+ 0
- 45
app/Filament/Pages/Widgets/Users/Tables/Users.php View File

1
-<?php
2
-
3
-namespace App\Filament\Pages\Widgets\Users\Tables;
4
-
5
-use App\Models\User;
6
-use Closure;
7
-use Filament\Tables;
8
-use Filament\Widgets\TableWidget as PageWidget;
9
-use Illuminate\Contracts\Support\Htmlable;
10
-use Illuminate\Database\Eloquent\Builder;
11
-use Illuminate\Database\Eloquent\Relations\Relation;
12
-
13
-class Users extends PageWidget
14
-{
15
-    protected int|string|array $columnSpan = [
16
-        'md' => 2,
17
-        'xl' => 3,
18
-    ];
19
-
20
-    protected function getTableQuery(): Builder|Relation
21
-    {
22
-        return User::query();
23
-    }
24
-
25
-    protected function getTableHeading(): string|Htmlable|Closure|null
26
-    {
27
-        return null;
28
-    }
29
-
30
-    protected function getTableColumns(): array
31
-    {
32
-        return [
33
-            Tables\Columns\ViewColumn::make('name')
34
-                ->view('filament.components.users.avatar-column')
35
-                ->label('Name')
36
-                ->sortable()
37
-                ->searchable()
38
-                ->grow(false),
39
-            Tables\Columns\TextColumn::make('owned_companies_count')
40
-                ->counts('ownedCompanies')
41
-                ->label('Companies')
42
-                ->sortable(),
43
-        ];
44
-    }
45
-}

+ 0
- 155
app/Filament/Resources/CategoryResource.php View File

1
-<?php
2
-
3
-namespace App\Filament\Resources;
4
-
5
-use App\Filament\Resources\CategoryResource\Pages;
6
-use Illuminate\Support\Facades\Auth;
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 getSlug(): string
126
-    {
127
-        return '{company}/settings/categories';
128
-    }
129
-
130
-    public static function getUrl($name = 'index', $params = [], $isAbsolute = true): string
131
-    {
132
-        $routeBaseName = static::getRouteBaseName();
133
-
134
-        return route("{$routeBaseName}.{$name}", [
135
-                'company' => Auth::user()->currentCompany,
136
-                'record' => $params['record'] ?? null,
137
-        ], $isAbsolute);
138
-    }
139
-
140
-    public static function getRelations(): array
141
-    {
142
-        return [
143
-            //
144
-        ];
145
-    }
146
-
147
-    public static function getPages(): array
148
-    {
149
-        return [
150
-            'index' => Pages\ListCategories::route('/'),
151
-            'create' => Pages\CreateCategory::route('/create'),
152
-            'edit' => Pages\EditCategory::route('/{record}/edit'),
153
-        ];
154
-    }
155
-}

+ 0
- 23
app/Filament/Resources/CategoryResource/Pages/ListCategories.php View File

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

+ 0
- 251
app/Filament/Resources/CurrencyResource.php View File

1
-<?php
2
-
3
-namespace App\Filament\Resources;
4
-
5
-use App\Filament\Resources\CurrencyResource\Pages;
6
-use App\Models\Banking\Account;
7
-use App\Models\Setting\Currency;
8
-use App\Services\CurrencyService;
9
-use Closure;
10
-use Exception;
11
-use Filament\Forms;
12
-use Filament\Notifications\Notification;
13
-use Filament\Resources\Form;
14
-use Filament\Resources\Resource;
15
-use Filament\Resources\Table;
16
-use Filament\Tables;
17
-use Illuminate\Support\Collection;
18
-use Illuminate\Support\Facades\Auth;
19
-use Wallo\FilamentSelectify\Components\ToggleButton;
20
-
21
-class CurrencyResource extends Resource
22
-{
23
-    protected static ?string $model = Currency::class;
24
-
25
-    protected static ?string $navigationIcon = 'heroicon-o-currency-dollar';
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
-                    ->description('Upon selecting a currency code, the corresponding values based on real-world currencies will auto-populate. 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. Alterations to default values are allowed but manage such changes wisely as any confusion or discrepancies are your responsibility.')
35
-                    ->schema([
36
-                        Forms\Components\Select::make('code')
37
-                            ->label('Code')
38
-                            ->options(Currency::getCurrencyCodes())
39
-                            ->searchable()
40
-                            ->placeholder('Select a currency code...')
41
-                            ->reactive()
42
-                            ->hidden(static fn (Closure $get): bool => $get('enabled'))
43
-                            ->afterStateUpdated(static function (Closure $set, $state) {
44
-                                if ($state === null) {
45
-                                    return;
46
-                                }
47
-
48
-                                $code = $state;
49
-                                $currencyConfig = config("money.{$code}", []);
50
-                                $currencyService = app(CurrencyService::class);
51
-
52
-                                $defaultCurrency = Currency::getDefaultCurrency();
53
-
54
-                                $rate = 1;
55
-
56
-                                if ($defaultCurrency !== null) {
57
-                                   $rate = $currencyService->getCachedExchangeRate($defaultCurrency, $code);
58
-                                }
59
-
60
-                                $set('name', $currencyConfig['name'] ?? '');
61
-                                $set('rate', $rate);
62
-                                $set('precision', $currencyConfig['precision'] ?? '');
63
-                                $set('symbol', $currencyConfig['symbol'] ?? '');
64
-                                $set('symbol_first', $currencyConfig['symbol_first'] ?? '');
65
-                                $set('decimal_mark', $currencyConfig['decimal_mark'] ?? '');
66
-                                $set('thousands_separator', $currencyConfig['thousands_separator'] ?? '');
67
-                            })
68
-                            ->required(),
69
-                        Forms\Components\TextInput::make('code')
70
-                            ->label('Code')
71
-                            ->hidden(static fn (Closure $get): bool => !$get('enabled'))
72
-                            ->disabled(static fn (Closure $get): bool => $get('enabled'))
73
-                            ->required(),
74
-                        Forms\Components\TextInput::make('name')
75
-                            ->translateLabel()
76
-                            ->maxLength(100)
77
-                            ->required(),
78
-                        Forms\Components\TextInput::make('rate')
79
-                            ->label('Rate')
80
-                            ->dehydrateStateUsing(static fn (Closure $get, $state) => $get('enabled') ? '1' : $state)
81
-                            ->numeric()
82
-                            ->reactive()
83
-                            ->disabled(static fn (Closure $get): bool => $get('enabled'))
84
-                            ->required(),
85
-                        Forms\Components\Select::make('precision')
86
-                            ->label('Precision')
87
-                            ->searchable()
88
-                            ->placeholder('Select the currency precision...')
89
-                            ->options(['0', '1', '2', '3', '4'])
90
-                            ->required(),
91
-                        Forms\Components\TextInput::make('symbol')
92
-                            ->label('Symbol')
93
-                            ->maxLength(5)
94
-                            ->required(),
95
-                        Forms\Components\Select::make('symbol_first')
96
-                            ->label('Symbol Position')
97
-                            ->searchable()
98
-                            ->boolean('Before Amount', 'After Amount', 'Select the currency symbol position...')
99
-                            ->required(),
100
-                        Forms\Components\TextInput::make('decimal_mark')
101
-                            ->label('Decimal Separator')
102
-                            ->maxLength(1)
103
-                            ->required(),
104
-                        Forms\Components\TextInput::make('thousands_separator')
105
-                            ->label('Thousands Separator')
106
-                            ->maxLength(1)
107
-                            ->required(),
108
-                        ToggleButton::make('enabled')
109
-                            ->label('Default Currency')
110
-                            ->reactive()
111
-                            ->offColor('danger')
112
-                            ->onColor('primary')
113
-                            ->afterStateUpdated(static function (Closure $set, Closure $get, $state) {
114
-                                $enabled = $state;
115
-                                $code = $get('code');
116
-                                $currencyService = app(CurrencyService::class);
117
-
118
-                                if ($enabled) {
119
-                                    $rate = 1;
120
-                                } else {
121
-                                    $defaultCurrency = Currency::getDefaultCurrency();
122
-                                    $rate = $defaultCurrency ? $currencyService->getCachedExchangeRate($defaultCurrency, $code) : 1;
123
-                                }
124
-
125
-                                $set('rate', $rate);
126
-                            }),
127
-                    ])->columns(),
128
-            ]);
129
-    }
130
-
131
-    /**
132
-     * @throws Exception
133
-     */
134
-    public static function table(Table $table): Table
135
-    {
136
-        return $table
137
-            ->columns([
138
-                Tables\Columns\TextColumn::make('name')
139
-                    ->label('Name')
140
-                    ->weight('semibold')
141
-                    ->icon(static fn (Currency $record) => $record->enabled ? 'heroicon-o-lock-closed' : null)
142
-                    ->tooltip(static fn (Currency $record) => $record->enabled ? 'Default Currency' : null)
143
-                    ->iconPosition('after')
144
-                    ->searchable()
145
-                    ->sortable(),
146
-                Tables\Columns\TextColumn::make('code')
147
-                    ->label('Code')
148
-                    ->searchable()
149
-                    ->sortable(),
150
-                Tables\Columns\TextColumn::make('symbol')
151
-                    ->label('Symbol')
152
-                    ->searchable()
153
-                    ->sortable(),
154
-                Tables\Columns\TextColumn::make('rate')
155
-                    ->label('Rate')
156
-                    ->searchable()
157
-                    ->sortable(),
158
-            ])
159
-            ->filters([
160
-                //
161
-            ])
162
-            ->actions([
163
-                Tables\Actions\EditAction::make(),
164
-                Tables\Actions\DeleteAction::make()
165
-                    ->before(static function (Tables\Actions\DeleteAction $action, Currency $record) {
166
-                        $defaultCurrency = $record->enabled;
167
-                        $accountUsesCurrency = Account::where('currency_code', $record->code)->exists();
168
-
169
-                        if ($defaultCurrency) {
170
-                            Notification::make()
171
-                                ->danger()
172
-                                ->title('Action Denied')
173
-                                ->body(__('The :name currency is currently set as the default currency and cannot be deleted. Please set a different currency as your default before attempting to delete this one.', ['name' => $record->name]))
174
-                                ->persistent()
175
-                                ->send();
176
-
177
-                            $action->cancel();
178
-                        } elseif ($accountUsesCurrency) {
179
-                            Notification::make()
180
-                                ->danger()
181
-                                ->title('Action Denied')
182
-                                ->body(__('The :name currency is currently in use by one or more accounts and cannot be deleted. Please remove this currency from all accounts before attempting to delete it.', ['name' => $record->name]))
183
-                                ->persistent()
184
-                                ->send();
185
-
186
-                            $action->cancel();
187
-                        }
188
-                    }),
189
-            ])
190
-            ->bulkActions([
191
-                Tables\Actions\DeleteBulkAction::make()
192
-                    ->before(static function (Tables\Actions\DeleteBulkAction $action, Collection $records) {
193
-                        foreach ($records as $record) {
194
-                            $defaultCurrency = $record->enabled;
195
-                            $accountUsesCurrency = Account::where('currency_code', $record->code)->exists();
196
-
197
-                            if ($defaultCurrency) {
198
-                                Notification::make()
199
-                                    ->danger()
200
-                                    ->title('Action Denied')
201
-                                    ->body(__('The :name currency is currently set as the default currency and cannot be deleted. Please set a different currency as your default before attempting to delete this one.', ['name' => $record->name]))
202
-                                    ->persistent()
203
-                                    ->send();
204
-
205
-                                $action->cancel();
206
-                            } elseif ($accountUsesCurrency) {
207
-                                Notification::make()
208
-                                    ->danger()
209
-                                    ->title('Action Denied')
210
-                                    ->body(__('The :name currency is currently in use by one or more accounts and cannot be deleted. Please remove this currency from all accounts before attempting to delete it.', ['name' => $record->name]))
211
-                                    ->persistent()
212
-                                    ->send();
213
-
214
-                                $action->cancel();
215
-                            }
216
-                        }
217
-                    }),
218
-            ]);
219
-    }
220
-
221
-    public static function getSlug(): string
222
-    {
223
-        return '{company}/settings/currencies';
224
-    }
225
-
226
-    public static function getUrl($name = 'index', $params = [], $isAbsolute = true): string
227
-    {
228
-        $routeBaseName = static::getRouteBaseName();
229
-
230
-        return route("{$routeBaseName}.{$name}", [
231
-            'company' => Auth::user()->currentCompany,
232
-            'record' => $params['record'] ?? null,
233
-        ], $isAbsolute);
234
-    }
235
-
236
-    public static function getRelations(): array
237
-    {
238
-        return [
239
-            //
240
-        ];
241
-    }
242
-
243
-    public static function getPages(): array
244
-    {
245
-        return [
246
-            'index' => Pages\ListCurrencies::route('/'),
247
-            'create' => Pages\CreateCurrency::route('/create'),
248
-            'edit' => Pages\EditCurrency::route('/{record}/edit'),
249
-        ];
250
-    }
251
-}

+ 0
- 253
app/Filament/Resources/CustomerResource.php View File

1
-<?php
2
-
3
-namespace App\Filament\Resources;
4
-
5
-use App\Actions\OptionAction\CreateCurrency;
6
-use App\Filament\Resources\CustomerResource\Pages;
7
-use App\Models\Setting\Currency;
8
-use App\Services\CurrencyService;
9
-use Illuminate\Support\Facades\Auth;
10
-use Wallo\FilamentSelectify\Components\ButtonGroup;
11
-use App\Models\Contact;
12
-use Filament\Forms;
13
-use Filament\Resources\Form;
14
-use Filament\Resources\Resource;
15
-use Filament\Resources\Table;
16
-use Filament\Tables;
17
-use Illuminate\Database\Eloquent\Builder;
18
-use Illuminate\Support\Facades\DB;
19
-
20
-class CustomerResource extends Resource
21
-{
22
-    protected static ?string $model = Contact::class;
23
-
24
-    protected static ?string $navigationIcon = 'heroicon-o-user-group';
25
-
26
-    protected static ?string $navigationGroup = 'Sales';
27
-
28
-    protected static ?string $navigationLabel = 'Customers';
29
-
30
-    protected static ?string $modelLabel = 'customer';
31
-
32
-    public static function getEloquentQuery(): Builder
33
-    {
34
-        return parent::getEloquentQuery()->customer();
35
-    }
36
-
37
-    public static function form(Form $form): Form
38
-    {
39
-        return $form
40
-            ->schema([
41
-                Forms\Components\Section::make('General')
42
-                    ->schema([
43
-                        Forms\Components\Grid::make(3)
44
-                            ->schema([
45
-                                ButtonGroup::make('entity')
46
-                                    ->label('Entity')
47
-                                    ->options([
48
-                                        'individual' => 'Individual',
49
-                                        'company' => 'Company',
50
-                                    ])
51
-                                    ->gridDirection('column')
52
-                                    ->default('individual')
53
-                                    ->columnSpan(1),
54
-                                Forms\Components\Grid::make()
55
-                                    ->schema([
56
-                                        Forms\Components\TextInput::make('name')
57
-                                            ->label('Name')
58
-                                            ->maxLength(100)
59
-                                            ->required(),
60
-                                        Forms\Components\TextInput::make('email')
61
-                                            ->label('Email')
62
-                                            ->email()
63
-                                            ->nullable(),
64
-                                        Forms\Components\TextInput::make('phone')
65
-                                            ->label('Phone')
66
-                                            ->tel()
67
-                                            ->maxLength(20),
68
-                                        Forms\Components\TextInput::make('website')
69
-                                            ->label('Website')
70
-                                            ->maxLength(100)
71
-                                            ->url()
72
-                                            ->nullable(),
73
-                                        Forms\Components\TextInput::make('reference')
74
-                                            ->label('Reference')
75
-                                            ->maxLength(100)
76
-                                            ->columnSpan(2)
77
-                                            ->nullable(),
78
-                                    ])->columnSpan(2),
79
-                            ]),
80
-                    ])->columns(),
81
-                Forms\Components\Section::make('Billing')
82
-                    ->schema([
83
-                        Forms\Components\TextInput::make('tax_number')
84
-                            ->label('Tax Number')
85
-                            ->maxLength(100)
86
-                            ->nullable(),
87
-                        Forms\Components\Select::make('currency_code')
88
-                            ->label('Currency')
89
-                            ->relationship('currency', 'name')
90
-                            ->preload()
91
-                            ->default(Currency::getDefaultCurrency())
92
-                            ->searchable()
93
-                            ->reactive()
94
-                            ->required()
95
-                            ->createOptionForm([
96
-                                Forms\Components\Select::make('currency.code')
97
-                                    ->label('Code')
98
-                                    ->searchable()
99
-                                    ->options(Currency::getCurrencyCodes())
100
-                                    ->reactive()
101
-                                    ->afterStateUpdated(static function (callable $set, $state) {
102
-                                        if ($state === null) {
103
-                                            return;
104
-                                        }
105
-
106
-                                        $code = $state;
107
-                                        $currencyConfig = config("money.{$code}", []);
108
-                                        $currencyService = app(CurrencyService::class);
109
-
110
-                                        $defaultCurrency = Currency::getDefaultCurrency();
111
-
112
-                                        $rate = 1;
113
-
114
-                                        if ($defaultCurrency !== null) {
115
-                                            $rate = $currencyService->getCachedExchangeRate($defaultCurrency, $code);
116
-                                        }
117
-
118
-                                        $set('currency.name', $currencyConfig['name'] ?? '');
119
-                                        $set('currency.rate', $rate);
120
-                                    })
121
-                                    ->required(),
122
-                                Forms\Components\TextInput::make('currency.name')
123
-                                    ->label('Name')
124
-                                    ->maxLength(100)
125
-                                    ->required(),
126
-                                Forms\Components\TextInput::make('currency.rate')
127
-                                    ->label('Rate')
128
-                                    ->numeric()
129
-                                    ->required(),
130
-                            ])->createOptionAction(static function (Forms\Components\Actions\Action $action) {
131
-                                return $action
132
-                                    ->label('Add Currency')
133
-                                    ->modalHeading('Add Currency')
134
-                                    ->modalButton('Add')
135
-                                    ->action(static function (array $data) {
136
-                                        return DB::transaction(static function () use ($data) {
137
-                                            $code = $data['currency']['code'];
138
-                                            $name = $data['currency']['name'];
139
-                                            $rate = $data['currency']['rate'];
140
-
141
-                                            return (new CreateCurrency())->create($code, $name, $rate);
142
-                                        });
143
-                                    });
144
-                            }),
145
-                    ])->columns(),
146
-                Forms\Components\Section::make('Address')
147
-                    ->schema([
148
-                        Forms\Components\TextInput::make('address')
149
-                            ->label('Address')
150
-                            ->maxLength(100)
151
-                            ->columnSpanFull()
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(),
177
-                        Forms\Components\TextInput::make('city')
178
-                            ->label('Town/City')
179
-                            ->maxLength(100)
180
-                            ->nullable(),
181
-                        Forms\Components\TextInput::make('zip_code')
182
-                            ->label('Postal/Zip Code')
183
-                            ->maxLength(100)
184
-                            ->nullable(),
185
-                    ])->columns(),
186
-            ]);
187
-    }
188
-
189
-    public static function table(Table $table): Table
190
-    {
191
-        return $table
192
-            ->columns([
193
-                Tables\Columns\TextColumn::make('name')
194
-                    ->label('Name')
195
-                    ->weight('semibold')
196
-                    ->description(static fn (Contact $record) => $record->tax_number ?: 'N/A')
197
-                    ->searchable()
198
-                    ->sortable(),
199
-                Tables\Columns\TextColumn::make('email')
200
-                    ->label('Email')
201
-                    ->formatStateUsing(static fn (Contact $record) => $record->email ?: 'N/A')
202
-                    ->description(static fn (Contact $record) => $record->phone ?: 'N/A')
203
-                    ->searchable()
204
-                    ->sortable(),
205
-                Tables\Columns\TextColumn::make('country')
206
-                    ->label('Country')
207
-                    ->searchable()
208
-                    ->formatStateUsing(static fn (Contact $record) => $record->country ?: 'N/A')
209
-                    ->description(static fn (Contact $record) => $record->currency->name ?: 'N/A')
210
-                    ->sortable(),
211
-            ])
212
-            ->filters([
213
-                //
214
-            ])
215
-            ->actions([
216
-                Tables\Actions\EditAction::make(),
217
-            ])
218
-            ->bulkActions([
219
-                Tables\Actions\DeleteBulkAction::make(),
220
-            ]);
221
-    }
222
-
223
-    public static function getSlug(): string
224
-    {
225
-        return '{company}/sales/customers';
226
-    }
227
-
228
-    public static function getUrl($name = 'index', $params = [], $isAbsolute = true): string
229
-    {
230
-        $routeBaseName = static::getRouteBaseName();
231
-
232
-        return route("{$routeBaseName}.{$name}", [
233
-            'company' => Auth::user()->currentCompany,
234
-            'record' => $params['record'] ?? null,
235
-        ], $isAbsolute);
236
-    }
237
-
238
-    public static function getRelations(): array
239
-    {
240
-        return [
241
-            //
242
-        ];
243
-    }
244
-
245
-    public static function getPages(): array
246
-    {
247
-        return [
248
-            'index' => Pages\ListCustomers::route('/'),
249
-            'create' => Pages\CreateCustomer::route('/create'),
250
-            'edit' => Pages\EditCustomer::route('/{record}/edit'),
251
-        ];
252
-    }
253
-}

+ 0
- 26
app/Filament/Resources/CustomerResource/Pages/CreateCustomer.php View File

1
-<?php
2
-
3
-namespace App\Filament\Resources\CustomerResource\Pages;
4
-
5
-use App\Filament\Resources\CustomerResource;
6
-use Filament\Pages\Actions;
7
-use Filament\Resources\Pages\CreateRecord;
8
-use Illuminate\Support\Facades\Auth;
9
-use Squire\Models\Region;
10
-
11
-class CreateCustomer extends CreateRecord
12
-{
13
-    protected static string $resource = CustomerResource::class;
14
-
15
-    protected function getRedirectUrl(): string
16
-    {
17
-        return $this->previousUrl;
18
-    }
19
-
20
-    protected function mutateFormDataBeforeCreate(array $data): array
21
-    {
22
-        $data['type'] = 'customer';
23
-
24
-        return $data;
25
-    }
26
-}

+ 0
- 24
app/Filament/Resources/CustomerResource/Pages/EditCustomer.php View File

1
-<?php
2
-
3
-namespace App\Filament\Resources\CustomerResource\Pages;
4
-
5
-use App\Filament\Resources\CustomerResource;
6
-use Filament\Pages\Actions;
7
-use Filament\Resources\Pages\EditRecord;
8
-
9
-class EditCustomer extends EditRecord
10
-{
11
-    protected static string $resource = CustomerResource::class;
12
-
13
-    protected function getActions(): array
14
-    {
15
-        return [
16
-            Actions\DeleteAction::make(),
17
-        ];
18
-    }
19
-
20
-    protected function getRedirectUrl(): string
21
-    {
22
-        return $this->previousUrl;
23
-    }
24
-}

+ 0
- 19
app/Filament/Resources/CustomerResource/Pages/ListCustomers.php View File

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

+ 0
- 202
app/Filament/Resources/DiscountResource.php View File

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

+ 0
- 152
app/Filament/Resources/InvoiceResource.php View File

1
-<?php
2
-
3
-namespace App\Filament\Resources;
4
-
5
-use App\Filament\Resources\InvoiceResource\Pages;
6
-use App\Filament\Resources\InvoiceResource\RelationManagers;
7
-use App\Models\Setting\Currency;
8
-use Illuminate\Support\Facades\Auth;
9
-use Wallo\FilamentSelectify\Components\ButtonGroup;
10
-use App\Models\Document\Document;
11
-use Filament\Forms;
12
-use Filament\Resources\Form;
13
-use Filament\Resources\Resource;
14
-use Filament\Resources\Table;
15
-use Filament\Tables;
16
-use Illuminate\Database\Eloquent\Builder;
17
-
18
-class InvoiceResource extends Resource
19
-{
20
-    protected static ?string $model = Document::class;
21
-
22
-    protected static ?string $navigationIcon = 'heroicon-o-document-text';
23
-
24
-    protected static ?string $navigationGroup = 'Sales';
25
-
26
-    protected static ?string $navigationLabel = 'Invoices';
27
-
28
-    protected static ?string $modelLabel = 'invoice';
29
-
30
-    public static function getEloquentQuery(): Builder
31
-    {
32
-        return parent::getEloquentQuery()
33
-            ->where('type', 'invoice');
34
-    }
35
-
36
-    public static function form(Form $form): Form
37
-    {
38
-        return $form
39
-            ->schema([
40
-                Forms\Components\Section::make('Billing')
41
-                    ->schema([
42
-                        Forms\Components\Grid::make(3)
43
-                            ->schema([
44
-                                Forms\Components\Select::make('contact_id')
45
-                                    ->label('Customer')
46
-                                    ->preload()
47
-                                    ->placeholder('Select a customer')
48
-                                    ->relationship('contact', 'name', static fn (Builder $query) => $query->where('type', 'customer'))
49
-                                    ->searchable()
50
-                                    ->required()
51
-                                    ->createOptionForm([
52
-                                        ButtonGroup::make('contact.entity')
53
-                                            ->label('Entity')
54
-                                            ->options([
55
-                                                'company' => 'Company',
56
-                                                'individual' => 'Individual',
57
-                                            ])
58
-                                            ->default('company')
59
-                                            ->required(),
60
-                                        Forms\Components\TextInput::make('contact.name')
61
-                                            ->label('Name')
62
-                                            ->maxLength(100)
63
-                                            ->required(),
64
-                                        Forms\Components\TextInput::make('contact.email')
65
-                                            ->label('Email')
66
-                                            ->email()
67
-                                            ->nullable(),
68
-                                        Forms\Components\TextInput::make('contact.phone')
69
-                                            ->label('Phone')
70
-                                            ->tel()
71
-                                            ->maxLength(20),
72
-                                        Forms\Components\Select::make('contact.currency_code')
73
-                                            ->label('Currency')
74
-                                            ->relationship('currency', 'name')
75
-                                            ->preload()
76
-                                            ->default(Currency::getDefaultCurrency())
77
-                                            ->searchable()
78
-                                            ->reactive()
79
-                                            ->required(),
80
-                                    ])->columnSpan(1),
81
-                                Forms\Components\Grid::make(2)
82
-                                    ->schema([
83
-                                        Forms\Components\DatePicker::make('document_date')
84
-                                            ->label('Invoice Date')
85
-                                            ->default(now())
86
-                                            ->format('Y-m-d')
87
-                                            ->required(),
88
-                                        Forms\Components\DatePicker::make('due_date')
89
-                                            ->label('Due Date')
90
-                                            ->default(now())
91
-                                            ->format('Y-m-d')
92
-                                            ->required(),
93
-                                        Forms\Components\TextInput::make('document_number')
94
-                                            ->label('Invoice Number')
95
-                                            ->required(),
96
-                                        Forms\Components\TextInput::make('order_number')
97
-                                            ->label('Order Number')
98
-                                            ->nullable(),
99
-                                    ])->columnSpan(2),
100
-                            ])->columns(3),
101
-                    ])->columns(3),
102
-            ]);
103
-    }
104
-
105
-    public static function table(Table $table): Table
106
-    {
107
-        return $table
108
-            ->columns([
109
-                //
110
-            ])
111
-            ->filters([
112
-                //
113
-            ])
114
-            ->actions([
115
-                Tables\Actions\EditAction::make(),
116
-            ])
117
-            ->bulkActions([
118
-                Tables\Actions\DeleteBulkAction::make(),
119
-            ]);
120
-    }
121
-
122
-    public static function getSlug(): string
123
-    {
124
-        return '{company}/sales/invoices';
125
-    }
126
-
127
-    public static function getUrl($name = 'index', $params = [], $isAbsolute = true): string
128
-    {
129
-        $routeBaseName = static::getRouteBaseName();
130
-
131
-        return route("{$routeBaseName}.{$name}", [
132
-            'company' => Auth::user()->currentCompany,
133
-            'record' => $params['record'] ?? null,
134
-        ], $isAbsolute);
135
-    }
136
-
137
-    public static function getRelations(): array
138
-    {
139
-        return [
140
-            RelationManagers\DocumentItemsRelationManager::class,
141
-        ];
142
-    }
143
-
144
-    public static function getPages(): array
145
-    {
146
-        return [
147
-            'index' => Pages\ListInvoices::route('/'),
148
-            'create' => Pages\CreateInvoice::route('/create'),
149
-            'edit' => Pages\EditInvoice::route('/{record}/edit'),
150
-        ];
151
-    }
152
-}

+ 0
- 27
app/Filament/Resources/InvoiceResource/Pages/CreateInvoice.php View File

1
-<?php
2
-
3
-namespace App\Filament\Resources\InvoiceResource\Pages;
4
-
5
-use App\Filament\Resources\InvoiceResource;
6
-use Filament\Pages\Actions;
7
-use Filament\Resources\Pages\CreateRecord;
8
-use Illuminate\Support\Facades\Auth;
9
-
10
-class CreateInvoice extends CreateRecord
11
-{
12
-    protected static string $resource = InvoiceResource::class;
13
-
14
-    protected function getRedirectUrl(): string
15
-    {
16
-        return $this->previousUrl;
17
-    }
18
-
19
-    protected function mutateFormDataBeforeCreate(array $data): array
20
-    {
21
-        $data['type'] = 'invoice';
22
-        $data['status'] = 'draft';
23
-        $data['amount'] = 0;
24
-
25
-        return $data;
26
-    }
27
-}

+ 0
- 24
app/Filament/Resources/InvoiceResource/Pages/EditInvoice.php View File

1
-<?php
2
-
3
-namespace App\Filament\Resources\InvoiceResource\Pages;
4
-
5
-use App\Filament\Resources\InvoiceResource;
6
-use Filament\Pages\Actions;
7
-use Filament\Resources\Pages\EditRecord;
8
-
9
-class EditInvoice extends EditRecord
10
-{
11
-    protected static string $resource = InvoiceResource::class;
12
-
13
-    protected function getActions(): array
14
-    {
15
-        return [
16
-            Actions\DeleteAction::make(),
17
-        ];
18
-    }
19
-
20
-    protected function getRedirectUrl(): string
21
-    {
22
-        return $this->previousUrl;
23
-    }
24
-}

+ 0
- 19
app/Filament/Resources/InvoiceResource/Pages/ListInvoices.php View File

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

+ 0
- 49
app/Filament/Resources/InvoiceResource/RelationManagers/DocumentItemsRelationManager.php View File

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
-}

+ 0
- 208
app/Filament/Resources/TaxResource.php View File

1
-<?php
2
-
3
-namespace App\Filament\Resources;
4
-
5
-use App\Filament\Resources\TaxResource\Pages;
6
-use App\Models\Setting\Tax;
7
-use Exception;
8
-use Filament\Forms;
9
-use Filament\Forms\Components\TextInput\Mask;
10
-use Filament\Notifications\Notification;
11
-use Filament\Resources\Form;
12
-use Filament\Resources\Resource;
13
-use Filament\Resources\Table;
14
-use Filament\Tables;
15
-use Illuminate\Support\Collection;
16
-use Illuminate\Support\Facades\Auth;
17
-use Wallo\FilamentSelectify\Components\ToggleButton;
18
-
19
-class TaxResource extends Resource
20
-{
21
-    protected static ?string $model = Tax::class;
22
-
23
-    protected static ?string $navigationIcon = 'heroicon-o-receipt-tax';
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(Tax::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' || $computation === 'compound') {
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(Tax::getTaxTypes())
70
-                            ->searchable()
71
-                            ->default('sales')
72
-                            ->required(),
73
-                        Forms\Components\Select::make('scope')
74
-                            ->label('Scope')
75
-                            ->options(Tax::getTaxScopes())
76
-                            ->searchable(),
77
-                        ToggleButton::make('enabled')
78
-                            ->label('Default')
79
-                            ->offColor('danger')
80
-                            ->onColor('primary'),
81
-                    ])->columns(),
82
-            ]);
83
-    }
84
-
85
-    /**
86
-     * @throws Exception
87
-     */
88
-    public static function table(Table $table): Table
89
-    {
90
-        return $table
91
-            ->columns([
92
-                Tables\Columns\TextColumn::make('name')
93
-                    ->label('Name')
94
-                    ->weight('semibold')
95
-                    ->icon(static fn (Tax $record) => $record->enabled ? 'heroicon-o-lock-closed' : null)
96
-                    ->tooltip(static fn (Tax $record) => $record->enabled ? "Default " .ucwords($record->type) . " Tax" : null)
97
-                    ->iconPosition('after')
98
-                    ->searchable()
99
-                    ->sortable(),
100
-                Tables\Columns\TextColumn::make('computation')
101
-                    ->label('Computation')
102
-                    ->formatStateUsing(static fn (Tax $record) => ucwords($record->computation))
103
-                    ->searchable()
104
-                    ->sortable(),
105
-                Tables\Columns\TextColumn::make('rate')
106
-                    ->label('Rate')
107
-                    ->formatStateUsing(static function (Tax $record) {
108
-                        $rate = $record->rate;
109
-
110
-                        return $rate . ($record->computation === 'percentage' || $record->computation === 'compound' ? '%' : null);
111
-                    })
112
-                    ->searchable()
113
-                    ->sortable(),
114
-                Tables\Columns\BadgeColumn::make('type')
115
-                    ->label('Type')
116
-                    ->formatStateUsing(static fn (Tax $record) => ucwords($record->type))
117
-                    ->colors([
118
-                        'success' => 'sales',
119
-                        'warning' => 'purchase',
120
-                        'secondary' => 'none',
121
-                    ])
122
-                    ->icons([
123
-                        'heroicon-o-cash' => 'sales',
124
-                        'heroicon-o-shopping-bag' => 'purchase',
125
-                        'heroicon-o-x-circle' => 'none',
126
-                    ])
127
-                    ->searchable()
128
-                    ->sortable(),
129
-            ])
130
-            ->filters([
131
-                //
132
-            ])
133
-            ->actions([
134
-                Tables\Actions\EditAction::make(),
135
-                Tables\Actions\DeleteAction::make()
136
-                    ->before(static function (Tables\Actions\DeleteAction $action, Tax $record) {
137
-                        if ($record->enabled) {
138
-                            Notification::make()
139
-                                ->danger()
140
-                                ->title('Action Denied')
141
-                                ->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)]))
142
-                                ->persistent()
143
-                                ->send();
144
-
145
-                            $action->cancel();
146
-                        }
147
-                    }),
148
-            ])
149
-            ->bulkActions([
150
-                Tables\Actions\DeleteBulkAction::make()
151
-                    ->before(static function (Collection $records, Tables\Actions\DeleteBulkAction $action) {
152
-                        $defaultTaxes = $records->filter(static function (Tax $record) {
153
-                            return $record->enabled;
154
-                        });
155
-
156
-                        if ($defaultTaxes->isNotEmpty()) {
157
-                            $defaultTaxNames = $defaultTaxes->pluck('name')->toArray();
158
-
159
-                            Notification::make()
160
-                                ->danger()
161
-                                ->title('Action Denied')
162
-                                ->body(static function () use ($defaultTaxNames) {
163
-                                    $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>";
164
-                                    $message .= implode("<br>", array_map(static function ($name) {
165
-                                        return "&bull; " . $name;
166
-                                    }, $defaultTaxNames));
167
-                                    return $message;
168
-                                })
169
-                                ->persistent()
170
-                                ->send();
171
-
172
-                            $action->cancel();
173
-                        }
174
-                    }),
175
-            ]);
176
-    }
177
-
178
-    public static function getSlug(): string
179
-    {
180
-        return '{company}/settings/taxes';
181
-    }
182
-
183
-    public static function getUrl($name = 'index', $params = [], $isAbsolute = true): string
184
-    {
185
-        $routeBaseName = static::getRouteBaseName();
186
-
187
-        return route("{$routeBaseName}.{$name}", [
188
-            'company' => Auth::user()->currentCompany,
189
-            'record' => $params['record'] ?? null,
190
-        ], $isAbsolute);
191
-    }
192
-
193
-    public static function getRelations(): array
194
-    {
195
-        return [
196
-            //
197
-        ];
198
-    }
199
-
200
-    public static function getPages(): array
201
-    {
202
-        return [
203
-            'index' => Pages\ListTaxes::route('/'),
204
-            'create' => Pages\CreateTax::route('/create'),
205
-            'edit' => Pages\EditTax::route('/{record}/edit'),
206
-        ];
207
-    }
208
-}

+ 0
- 26
app/Forms/Components/Invoice.php View File

1
-<?php
2
-
3
-namespace App\Forms\Components;
4
-
5
-use Filament\Forms\Components\Field;
6
-
7
-class Invoice extends Field
8
-{
9
-    protected string $view = 'forms.components.invoice';
10
-
11
-    protected ?string $companyName = null;
12
-
13
-    protected ?string $companyAddress = null;
14
-
15
-    protected ?string $companyCity = null;
16
-
17
-    protected ?string $companyState = null;
18
-
19
-    protected ?string $companyZip = null;
20
-
21
-    protected ?string $companyCountry = null;
22
-
23
-    protected ?string $documentNumberPrefix = null;
24
-
25
-    protected ?string $documentNumberDigits = null;
26
-}

+ 1
- 0
app/Http/Kernel.php View File

60
         'can' => \Illuminate\Auth\Middleware\Authorize::class,
60
         'can' => \Illuminate\Auth\Middleware\Authorize::class,
61
         'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
61
         'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
62
         'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
62
         'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
63
+        'precognitive' => \Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class,
63
         'signed' => \App\Http\Middleware\ValidateSignature::class,
64
         'signed' => \App\Http\Middleware\ValidateSignature::class,
64
         'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
65
         'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
65
         'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
66
         'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,

+ 0
- 13
app/Http/Livewire/Bill.php View File

1
-<?php
2
-
3
-namespace App\Http\Livewire;
4
-
5
-use Livewire\Component;
6
-
7
-class Bill extends Component
8
-{
9
-    public function render()
10
-    {
11
-        return view('livewire.bill');
12
-    }
13
-}

+ 0
- 89
app/Http/Livewire/CompanyDetails.php View File

1
-<?php
2
-
3
-namespace App\Http\Livewire;
4
-
5
-use App\Abstracts\Forms\EditFormRecord;
6
-use App\Models\Company;
7
-use Filament\Forms\ComponentContainer;
8
-use Filament\Forms\Components\FileUpload;
9
-use Filament\Forms\Components\Group;
10
-use Filament\Forms\Components\Section;
11
-use Filament\Forms\Components\TextInput;
12
-use Illuminate\Contracts\View\View;
13
-use Illuminate\Database\Eloquent\Model;
14
-
15
-/**
16
- * @property ComponentContainer $form
17
- */
18
-class CompanyDetails extends EditFormRecord
19
-{
20
-    public Company $company;
21
-
22
-    protected function getFormModel(): Model|string|null
23
-    {
24
-        return $this->company;
25
-    }
26
-
27
-    protected function getFormSchema(): array
28
-    {
29
-        return [
30
-            Section::make('General')
31
-                ->schema([
32
-                    Group::make()
33
-                        ->schema([
34
-                            TextInput::make('email')
35
-                                ->label('Email')
36
-                                ->email()
37
-                                ->nullable(),
38
-                            TextInput::make('phone')
39
-                                ->label('Phone')
40
-                                ->tel()
41
-                                ->maxLength(20),
42
-                        ])->columns(1),
43
-                    Group::make()
44
-                        ->schema([
45
-                            FileUpload::make('logo')
46
-                                ->label('Logo')
47
-                                ->disk('public')
48
-                                ->directory('logos/company')
49
-                                ->imageResizeMode('cover')
50
-                                ->imagePreviewHeight('150')
51
-                                ->imageCropAspectRatio('2:1')
52
-                                ->panelAspectRatio('2:1')
53
-                                ->reactive()
54
-                                ->enableOpen()
55
-                                ->preserveFilenames()
56
-                                ->visibility('public')
57
-                                ->image(),
58
-                        ])->columns(1),
59
-                ])->columns(),
60
-            Section::make('Address')
61
-                ->schema([
62
-                    TextInput::make('address')
63
-                        ->label('Address')
64
-                        ->maxLength(100)
65
-                        ->columnSpanFull()
66
-                        ->nullable(),
67
-                    TextInput::make('country')
68
-                        ->label('Country')
69
-                        ->nullable(),
70
-                    TextInput::make('state')
71
-                        ->label('Province/State')
72
-                        ->nullable(),
73
-                    TextInput::make('city')
74
-                        ->label('Town/City')
75
-                        ->maxLength(100)
76
-                        ->nullable(),
77
-                    TextInput::make('zip_code')
78
-                        ->label('Postal/Zip Code')
79
-                        ->maxLength(100)
80
-                        ->nullable(),
81
-                ])->columns(),
82
-        ];
83
-    }
84
-
85
-    public function render(): View
86
-    {
87
-        return view('livewire.company-details');
88
-    }
89
-}

+ 0
- 161
app/Http/Livewire/DefaultSetting.php View File

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\HandlesDefaultSettingRecordUpdate;
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\Database\Eloquent\Model;
19
-use Livewire\Component;
20
-
21
-/**
22
- * @property ComponentContainer $form
23
- */
24
-class DefaultSetting extends Component implements HasForms
25
-{
26
-    use InteractsWithForms, HandlesDefaultSettingRecordUpdate;
27
-
28
-    public $data;
29
-
30
-    public Defaults $record;
31
-
32
-    public function mount():void
33
-    {
34
-        $this->record = Defaults::firstOrNew();
35
-
36
-        $this->form->fill([
37
-            'account_id' => Defaults::getDefaultAccount(),
38
-            'currency_code' => Defaults::getDefaultCurrency(),
39
-            'sales_tax_id' => Defaults::getDefaultSalesTax(),
40
-            'purchase_tax_id' => Defaults::getDefaultPurchaseTax(),
41
-            'sales_discount_id' => Defaults::getDefaultSalesDiscount(),
42
-            'purchase_discount_id' => Defaults::getDefaultPurchaseDiscount(),
43
-            'income_category_id' => Defaults::getDefaultIncomeCategory(),
44
-            'expense_category_id' => Defaults::getDefaultExpenseCategory(),
45
-        ]);
46
-    }
47
-
48
-    protected function getFormSchema(): array
49
-    {
50
-        return [
51
-            Section::make('General')
52
-                ->schema([
53
-                    Select::make('account_id')
54
-                        ->label('Account')
55
-                        ->options(Defaults::getAccounts())
56
-                        ->searchable()
57
-                        ->validationAttribute('Account')
58
-                        ->nullable(),
59
-                    Select::make('currency_code')
60
-                        ->label('Currency')
61
-                        ->options(Defaults::getCurrencies())
62
-                        ->searchable()
63
-                        ->validationAttribute('Currency')
64
-                        ->nullable(),
65
-                ])->columns(),
66
-            Section::make('Taxes & Discounts')
67
-                ->schema([
68
-                    Select::make('sales_tax_id')
69
-                        ->label('Sales Tax')
70
-                        ->options(Defaults::getSalesTaxes())
71
-                        ->searchable()
72
-                        ->validationAttribute('Sales Tax')
73
-                        ->nullable(),
74
-                    Select::make('purchase_tax_id')
75
-                        ->label('Purchase Tax')
76
-                        ->options(Defaults::getPurchaseTaxes())
77
-                        ->searchable()
78
-                        ->validationAttribute('Purchase Tax')
79
-                        ->nullable(),
80
-                    Select::make('sales_discount_id')
81
-                        ->label('Sales Discount')
82
-                        ->options(Defaults::getSalesDiscounts())
83
-                        ->searchable()
84
-                        ->validationAttribute('Sales Discount')
85
-                        ->nullable(),
86
-                    Select::make('purchase_discount_id')
87
-                        ->label('Purchase Discount')
88
-                        ->options(Defaults::getPurchaseDiscounts())
89
-                        ->searchable()
90
-                        ->validationAttribute('Purchase Discount')
91
-                        ->nullable(),
92
-                ])->columns(),
93
-            Section::make('Categories')
94
-                ->schema([
95
-                    Select::make('income_category_id')
96
-                        ->label('Income Category')
97
-                        ->options(Defaults::getIncomeCategories())
98
-                        ->searchable()
99
-                        ->validationAttribute('Income Category')
100
-                        ->nullable(),
101
-                    Select::make('expense_category_id')
102
-                        ->label('Expense Category')
103
-                        ->options(Defaults::getExpenseCategories())
104
-                        ->searchable()
105
-                        ->validationAttribute('Expense Category')
106
-                        ->nullable(),
107
-                ])->columns(),
108
-        ];
109
-    }
110
-
111
-    public function save(): void
112
-    {
113
-        $data = $this->form->getState();
114
-
115
-        $this->handleRecordUpdate($this->getFormModel(), $data);
116
-
117
-        $this->getSavedNotification()?->send();
118
-    }
119
-
120
-    protected function getFormModel(): Model
121
-    {
122
-        return $this->record;
123
-    }
124
-
125
-    protected function getRelatedEntities(): array
126
-    {
127
-        return [
128
-            'account_id' => [Account::class, 'id'],
129
-            'currency_code' => [Currency::class, 'code'],
130
-            'sales_tax_id' => [Tax::class, 'id', 'sales'],
131
-            'purchase_tax_id' => [Tax::class, 'id', 'purchase'],
132
-            'sales_discount_id' => [Tax::class, 'id', 'sales'],
133
-            'purchase_discount_id' => [Tax::class, 'id', 'purchase'],
134
-            'income_category_id' => [Category::class, 'id', 'income'],
135
-            'expense_category_id' => [Category::class, 'id', 'expense'],
136
-        ];
137
-    }
138
-
139
-    protected function getSavedNotification(): ?Notification
140
-    {
141
-        $title = $this->getSavedNotificationTitle();
142
-
143
-        if (blank($title)) {
144
-            return null;
145
-        }
146
-
147
-        return Notification::make()
148
-            ->success()
149
-            ->title($title);
150
-    }
151
-
152
-    protected function getSavedNotificationTitle(): ?string
153
-    {
154
-        return __('filament::resources/pages/edit-record.messages.saved');
155
-    }
156
-
157
-    public function render(): View
158
-    {
159
-        return view('livewire.default-setting');
160
-    }
161
-}

+ 0
- 247
app/Http/Livewire/Invoice.php View File

1
-<?php
2
-
3
-namespace App\Http\Livewire;
4
-
5
-use App\Abstracts\Forms\EditFormRecord;
6
-use App\Models\Setting\DocumentDefault;
7
-use Filament\Forms\ComponentContainer;
8
-use Filament\Forms\Components\ColorPicker;
9
-use Filament\Forms\Components\FileUpload;
10
-use Filament\Forms\Components\Group;
11
-use Filament\Forms\Components\Radio;
12
-use Filament\Forms\Components\Section;
13
-use Filament\Forms\Components\Select;
14
-use Filament\Forms\Components\Textarea;
15
-use Filament\Forms\Components\TextInput;
16
-use Filament\Forms\Components\ViewField;
17
-use Illuminate\Contracts\View\View;
18
-use Illuminate\Database\Eloquent\Model;
19
-
20
-/**
21
- * @property ComponentContainer $form
22
- */
23
-class Invoice extends EditFormRecord
24
-{
25
-    public DocumentDefault $invoice;
26
-
27
-    protected function getFormModel(): Model|string|null
28
-    {
29
-        $this->invoice = DocumentDefault::where('type', 'invoice')->firstOrNew();
30
-
31
-        return $this->invoice;
32
-    }
33
-
34
-    public function mount(): void
35
-    {
36
-        $this->fillForm();
37
-    }
38
-
39
-    public function fillForm(): void
40
-    {
41
-        $data = $this->getFormModel()->attributesToArray();
42
-
43
-        unset($data['id']);
44
-
45
-        $data = $this->mutateFormDataBeforeFill($data);
46
-
47
-        $this->form->fill($data);
48
-    }
49
-
50
-    protected function getFormSchema(): array
51
-    {
52
-        return [
53
-            Section::make('General')
54
-                ->schema([
55
-                    TextInput::make('number_prefix')
56
-                        ->label('Number Prefix')
57
-                        ->default('INV-')
58
-                        ->reactive()
59
-                        ->required(),
60
-                    Select::make('number_digits')
61
-                        ->label('Number Digits')
62
-                        ->options($this->invoice->getAvailableNumberDigits())
63
-                        ->default($this->invoice->getDefaultNumberDigits())
64
-                        ->reactive()
65
-                        ->afterStateUpdated(function (callable $set, $state) {
66
-                            $numDigits = $state;
67
-                            $nextNumber = $this->invoice->getNextDocumentNumber($numDigits);
68
-
69
-                            return $set('number_next', $nextNumber);
70
-                        })
71
-                        ->required()
72
-                        ->searchable(),
73
-                    TextInput::make('number_next')
74
-                        ->label('Next Number')
75
-                        ->reactive()
76
-                        ->required()
77
-                        ->default($this->invoice->getNextDocumentNumber($this->invoice->getDefaultNumberDigits())),
78
-                    Select::make('payment_terms')
79
-                        ->label('Payment Terms')
80
-                        ->options($this->invoice->getPaymentTerms())
81
-                        ->default($this->invoice->getDefaultPaymentTerms())
82
-                        ->searchable()
83
-                        ->reactive()
84
-                        ->required(),
85
-                ])->columns(),
86
-            Section::make('Content')
87
-                ->schema([
88
-                    TextInput::make('title')
89
-                        ->label('Title')
90
-                        ->reactive()
91
-                        ->default('Invoice')
92
-                        ->nullable(),
93
-                    TextInput::make('subheading')
94
-                        ->label('Subheading')
95
-                        ->reactive()
96
-                        ->nullable(),
97
-                    Textarea::make('footer')
98
-                        ->label('Footer')
99
-                        ->reactive()
100
-                        ->nullable(),
101
-                    Textarea::make('terms')
102
-                        ->label('Notes / Terms')
103
-                        ->nullable()
104
-                        ->reactive(),
105
-                ])->columns(),
106
-            Section::make('Template Settings')
107
-                ->description('Choose the template and edit the titles of the columns on your invoices.')
108
-                ->schema([
109
-                    Group::make()
110
-                        ->schema([
111
-                            FileUpload::make('document_logo')
112
-                                ->label('Logo')
113
-                                ->disk('public')
114
-                                ->directory('logos/documents')
115
-                                ->imageResizeMode('contain')
116
-                                ->imagePreviewHeight('250')
117
-                                ->imageCropAspectRatio('2:1')
118
-                                ->reactive()
119
-                                ->enableOpen()
120
-                                ->preserveFilenames()
121
-                                ->visibility('public')
122
-                                ->image(),
123
-                            ColorPicker::make('accent_color')
124
-                                ->label('Accent Color')
125
-                                ->reactive()
126
-                                ->default('#d9d9d9'),
127
-                            Select::make('template')
128
-                                ->label('Template')
129
-                                ->options([
130
-                                    'default' => 'Default',
131
-                                    'modern' => 'Modern',
132
-                                ])
133
-                                ->reactive()
134
-                                ->default('modern')
135
-                                ->required(),
136
-                            Radio::make('item_column')
137
-                                ->label('Items')
138
-                                ->options($this->invoice->getItemColumns())
139
-                                ->dehydrateStateUsing(static function (callable $get, $state) {
140
-                                    return $state === 'other' ? $get('custom_item_column') : $state;
141
-                                })
142
-                                ->afterStateHydrated(function (callable $set, callable $get, $state, Radio $component) {
143
-                                    if (isset($this->invoice->getItemColumns()[$state])) {
144
-                                        $component->state($state);
145
-                                    } else {
146
-                                        $component->state('other');
147
-                                        $set('custom_item_column', $state);
148
-                                    }
149
-                                })
150
-                                ->default($this->invoice->getDefaultItemColumn())
151
-                                ->reactive(),
152
-                            TextInput::make('custom_item_column')
153
-                                ->reactive()
154
-                                ->disableLabel()
155
-                                ->disabled(static fn (callable $get) => $get('item_column') !== 'other')
156
-                                ->nullable(),
157
-                            Radio::make('unit_column')
158
-                                ->label('Units')
159
-                                ->options(DocumentDefault::getUnitColumns())
160
-                                ->dehydrateStateUsing(static function (callable $get, $state) {
161
-                                    return $state === 'other' ? $get('custom_unit_column') : $state;
162
-                                })
163
-                                ->afterStateHydrated(function (callable $set, callable $get, $state, Radio $component) {
164
-                                    if (isset($this->invoice->getUnitColumns()[$state])) {
165
-                                        $component->state($state);
166
-                                    } else {
167
-                                        $component->state('other');
168
-                                        $set('custom_unit_column', $state);
169
-                                    }
170
-                                })
171
-                                ->default($this->invoice->getDefaultUnitColumn())
172
-                                ->reactive(),
173
-                            TextInput::make('custom_unit_column')
174
-                                ->reactive()
175
-                                ->disableLabel()
176
-                                ->disabled(static fn (callable $get) => $get('unit_column') !== 'other')
177
-                                ->nullable(),
178
-                            Radio::make('price_column')
179
-                                ->label('Price')
180
-                                ->options($this->invoice->getPriceColumns())
181
-                                ->dehydrateStateUsing(static function (callable $get, $state) {
182
-                                    return $state === 'other' ? $get('custom_price_column') : $state;
183
-                                })
184
-                                ->afterStateHydrated(function (callable $set, callable $get, $state, Radio $component) {
185
-                                    if (isset($this->invoice->getPriceColumns()[$state])) {
186
-                                        $component->state($state);
187
-                                    } else {
188
-                                        $component->state('other');
189
-                                        $set('custom_price_column', $state);
190
-                                    }
191
-                                })
192
-                                ->default($this->invoice->getDefaultPriceColumn())
193
-                                ->reactive(),
194
-                            TextInput::make('custom_price_column')
195
-                                ->reactive()
196
-                                ->disableLabel()
197
-                                ->disabled(static fn (callable $get) => $get('price_column') !== 'other')
198
-                                ->nullable(),
199
-                            Radio::make('amount_column')
200
-                                ->label('Amount')
201
-                                ->options($this->invoice->getAmountColumns())
202
-                                ->dehydrateStateUsing(static function (callable $get, $state) {
203
-                                    return $state === 'other' ? $get('custom_amount_column') : $state;
204
-                                })
205
-                                ->afterStateHydrated(function (callable $set, callable $get, $state, Radio $component) {
206
-                                    if (isset($this->invoice->getAmountColumns()[$state])) {
207
-                                        $component->state($state);
208
-                                    } else {
209
-                                        $component->state('other');
210
-                                        $set('custom_amount_column', $state);
211
-                                    }
212
-                                })
213
-                                ->default($this->invoice->getDefaultAmountColumn())
214
-                                ->reactive(),
215
-                            TextInput::make('custom_amount_column')
216
-                                ->reactive()
217
-                                ->disableLabel()
218
-                                ->disabled(static fn (callable $get) => $get('amount_column') !== 'other')
219
-                                ->nullable(),
220
-                        ])->columns(1),
221
-                    Group::make()
222
-                        ->schema([
223
-                            ViewField::make('preview.default')
224
-                                ->label('Preview')
225
-                                ->visible(static fn (callable $get) => $get('template') === 'default')
226
-                                ->view('components.invoice-layouts.default'),
227
-                            ViewField::make('preview.modern')
228
-                                ->label('Preview')
229
-                                ->visible(static fn (callable $get) => $get('template') === 'modern')
230
-                                ->view('components.invoice-layouts.modern'),
231
-                        ])->columnSpan(2),
232
-                ])->columns(3),
233
-        ];
234
-    }
235
-
236
-    protected function mutateFormDataBeforeSave(array $data): array
237
-    {
238
-        $data['type'] = 'invoice';
239
-
240
-        return $data;
241
-    }
242
-
243
-    public function render(): View
244
-    {
245
-        return view('livewire.invoice');
246
-    }
247
-}

+ 0
- 0
app/Http/Middleware/ApplyCurrentCompanyScope.php View File


Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save