Переглянути джерело

Enhance application with Live Currency feature and Transmatic integration

3.x
wallo 1 рік тому
джерело
коміт
91393d88f9
100 змінених файлів з 2536 додано та 1270 видалено
  1. 43
    22
      README.md
  2. 6
    3
      app/Actions/FilamentCompanies/AddCompanyEmployee.php
  3. 4
    2
      app/Actions/FilamentCompanies/CreateCompany.php
  4. 2
    1
      app/Actions/FilamentCompanies/CreateConnectedAccount.php
  5. 5
    2
      app/Actions/FilamentCompanies/CreateNewUser.php
  6. 6
    3
      app/Actions/FilamentCompanies/CreateUserFromProvider.php
  7. 4
    2
      app/Actions/FilamentCompanies/DeleteUser.php
  8. 5
    2
      app/Actions/FilamentCompanies/InviteCompanyEmployee.php
  9. 2
    1
      app/Actions/FilamentCompanies/RemoveCompanyEmployee.php
  10. 2
    1
      app/Actions/FilamentCompanies/SetUserPassword.php
  11. 4
    2
      app/Actions/FilamentCompanies/UpdateCompanyName.php
  12. 2
    1
      app/Actions/FilamentCompanies/UpdateUserPassword.php
  13. 2
    1
      app/Actions/OptionAction/CreateCurrency.php
  14. 24
    0
      app/Casts/CurrencyRateCast.php
  15. 7
    6
      app/Casts/MoneyCast.php
  16. 49
    7
      app/Casts/RateCast.php
  17. 0
    36
      app/Console/Commands/CacheData.php
  18. 0
    71
      app/Console/Commands/ConvertTimezones.php
  19. 72
    0
      app/Console/Commands/InitializeCurrencies.php
  20. 0
    71
      app/Console/Commands/SortCsv.php
  21. 18
    0
      app/Contracts/CurrencyHandler.php
  22. 1
    1
      app/Contracts/DocumentNumber.php
  23. 45
    0
      app/Enums/AccountStatus.php
  24. 23
    0
      app/Enums/AccountType.php
  25. 1
    1
      app/Enums/CategoryType.php
  26. 3
    1
      app/Enums/ContactType.php
  27. 41
    0
      app/Enums/DateFormat.php
  28. 3
    1
      app/Enums/DiscountComputation.php
  29. 1
    1
      app/Enums/DiscountScope.php
  30. 6
    2
      app/Enums/DiscountType.php
  31. 0
    19
      app/Enums/DocumentAmountColumn.php
  32. 0
    23
      app/Enums/DocumentItemColumn.php
  33. 0
    19
      app/Enums/DocumentPriceColumn.php
  34. 2
    1
      app/Enums/DocumentType.php
  35. 0
    19
      app/Enums/DocumentUnitColumn.php
  36. 3
    1
      app/Enums/EntityType.php
  37. 3
    1
      app/Enums/MaxContentWidth.php
  38. 3
    1
      app/Enums/ModalWidth.php
  39. 106
    0
      app/Enums/NumberFormat.php
  40. 8
    14
      app/Enums/PaymentTerms.php
  41. 7
    1
      app/Enums/PrimaryColor.php
  42. 0
    10
      app/Enums/RecordsPerPage.php
  43. 1
    1
      app/Enums/TableSortDirection.php
  44. 3
    1
      app/Enums/TaxComputation.php
  45. 1
    1
      app/Enums/TaxScope.php
  46. 6
    2
      app/Enums/TaxType.php
  47. 1
    1
      app/Enums/Template.php
  48. 26
    0
      app/Enums/TimeFormat.php
  49. 23
    0
      app/Enums/WeekStart.php
  50. 25
    0
      app/Events/CompanyConfigured.php
  51. 9
    2
      app/Events/CompanyGenerated.php
  52. 25
    0
      app/Events/CurrencyRateChanged.php
  53. 0
    7
      app/Events/UpdateCompanyDefault.php
  54. 30
    0
      app/Facades/Forex.php
  55. 19
    0
      app/Faker/CurrencyCode.php
  56. 1
    1
      app/Faker/PhoneNumber.php
  57. 1
    3
      app/Faker/State.php
  58. 33
    5
      app/Filament/Company/Pages/CreateCompany.php
  59. 69
    0
      app/Filament/Company/Pages/Service/LiveCurrency.php
  60. 52
    86
      app/Filament/Company/Pages/Setting/Appearance.php
  61. 103
    76
      app/Filament/Company/Pages/Setting/CompanyDefault.php
  62. 72
    103
      app/Filament/Company/Pages/Setting/CompanyProfile.php
  63. 86
    108
      app/Filament/Company/Pages/Setting/Invoice.php
  64. 239
    0
      app/Filament/Company/Pages/Setting/Localization.php
  65. 87
    103
      app/Filament/Company/Resources/Banking/AccountResource.php
  66. 0
    12
      app/Filament/Company/Resources/Banking/AccountResource/Pages/EditAccount.php
  67. 37
    9
      app/Filament/Company/Resources/Core/DepartmentResource.php
  68. 25
    0
      app/Filament/Company/Resources/Core/DepartmentResource/Pages/ListDepartments.php
  69. 90
    0
      app/Filament/Company/Resources/Core/DepartmentResource/RelationManagers/ChildrenRelationManager.php
  70. 49
    59
      app/Filament/Company/Resources/Setting/CategoryResource.php
  71. 4
    1
      app/Filament/Company/Resources/Setting/CategoryResource/Pages/CreateCategory.php
  72. 4
    1
      app/Filament/Company/Resources/Setting/CategoryResource/Pages/EditCategory.php
  73. 90
    102
      app/Filament/Company/Resources/Setting/CurrencyResource.php
  74. 0
    5
      app/Filament/Company/Resources/Setting/CurrencyResource/Pages/EditCurrency.php
  75. 75
    49
      app/Filament/Company/Resources/Setting/DiscountResource.php
  76. 4
    1
      app/Filament/Company/Resources/Setting/DiscountResource/Pages/CreateDiscount.php
  77. 4
    1
      app/Filament/Company/Resources/Setting/DiscountResource/Pages/EditDiscount.php
  78. 58
    84
      app/Filament/Company/Resources/Setting/TaxResource.php
  79. 4
    1
      app/Filament/Company/Resources/Setting/TaxResource/Pages/CreateTax.php
  80. 4
    1
      app/Filament/Company/Resources/Setting/TaxResource/Pages/EditTax.php
  81. 130
    0
      app/Helpers/format.php
  82. 0
    13
      app/Http/Controllers/Controller.php
  83. 11
    1
      app/Http/Middleware/ConfigureCurrentCompany.php
  84. 60
    10
      app/Listeners/ConfigureCompanyDefault.php
  85. 3
    4
      app/Listeners/CreateCompanyDefaults.php
  86. 15
    1
      app/Listeners/SyncWithCompanyDefaults.php
  87. 48
    0
      app/Listeners/UpdateAccountBalances.php
  88. 24
    8
      app/Listeners/UpdateCurrencyRates.php
  89. 134
    0
      app/Livewire/Company/Service/LiveCurrency/ListCompanyCurrencies.php
  90. 61
    0
      app/Livewire/Company/Service/LiveCurrency/ListCurrencies.php
  91. 17
    21
      app/Models/Banking/Account.php
  92. 6
    9
      app/Models/Common/Contact.php
  93. 27
    5
      app/Models/Company.php
  94. 3
    1
      app/Models/ConnectedAccount.php
  95. 10
    6
      app/Models/Core/Department.php
  96. 4
    2
      app/Models/Employeeship.php
  97. 64
    0
      app/Models/History/AccountHistory.php
  98. 3
    5
      app/Models/Locale/City.php
  99. 43
    8
      app/Models/Locale/Country.php
  100. 0
    0
      app/Models/Locale/State.php

+ 43
- 22
README.md Переглянути файл

@@ -75,7 +75,7 @@ Run the database seeder
75 75
 
76 76
     php artisan migrate:refresh
77 77
 
78
-## Currency Exchange Rates
78
+## Live Currency
79 79
 
80 80
 ### Overview
81 81
 
@@ -85,6 +85,22 @@ This application offers support for real-time currency exchange rates. This feat
85 85
 
86 86
 Once you have your API key, you can enable the feature by setting the `CURRENCY_API_KEY` environment variable in your `.env` file.
87 87
 
88
+### Initial Setup
89
+
90
+After setting your API key in the `.env` file, it is essential to prepare your database to store the currency data. Start by running a fresh database migration:
91
+
92
+```bash
93
+php artisan migrate:fresh
94
+```
95
+
96
+This ensures that your database is in the correct state to store the currency information. Afterward, use the following command to generate and populate the Currency List with supported currencies for the Live Currency page:
97
+
98
+```bash
99
+php artisan currency:init
100
+```
101
+
102
+This command fetches and stores the list of currencies supported by your configured exchange rate service.
103
+
88 104
 ### Configuration
89 105
 
90 106
 Of course, you may use any service you wish to retrieve currency exchange rates. If you decide to use a different service, you can update the `config/services.php` file with your choice:
@@ -96,28 +112,11 @@ Of course, you may use any service you wish to retrieve currency exchange rates.
96 112
 ],
97 113
 ```
98 114
 
99
-Additionally, you may update the following method in the `app/Services/CurrencyService.php` file which is responsible for retrieving the exchange rates:
100
-
101
-```php
102
-public function getExchangeRates($base)
103
-{
104
-    $api_key = config('services.currency_api.key');
105
-    $base_url = config('services.currency_api.base_url');
106
-
107
-    $req_url = "{$base_url}/{$api_key}/latest/{$base}";
115
+Then, adjust the implementation of the `App\Services\CurrencyService` class to use your chosen service.
108 116
 
109
-    $response = Http::get($req_url);
117
+### Live Currency Page
110 118
 
111
-    if ($response->successful()) {
112
-        $responseData = $response->json();
113
-        if (isset($responseData['conversion_rates'])) {
114
-            return $responseData['conversion_rates'];
115
-        }
116
-    }
117
-
118
-    return null;
119
-}
120
-```
119
+Once enabled, the "Live Currency" feature provides access to a dedicated page in the application, listing all supported currencies from the configured exchange rate service. Users can view available currencies and update exchange rates for their company's currencies as needed.
121 120
 
122 121
 ### Important Information
123 122
 
@@ -125,12 +124,34 @@ public function getExchangeRates($base)
125 124
 - Your API key is sensitive information and should be kept secret. Do not commit it to your repository or share it with anyone.
126 125
 - Note that API rate limits may apply depending on the service you choose. Make sure to review the terms for your chosen service.
127 126
 
127
+## Automatic Translation
128
+
129
+The application now supports automatic translation using machine translation services like AWS, using the [andrewdwallo/transmatic](https://github.com/andrewdwallo/transmatic) package. This feature enhances the application's accessibility to a global audience. The application is currently configured to support English, Arabic, German, Spanish, French, Indonesian, Italian, Dutch, Portuguese, Turkish, and Chinese. The application's default language is English.
130
+
131
+### Configuration & Usage
132
+
133
+To utilize this feature for additional languages or custom translations:
134
+1. Follow the documentation provided in the [andrewdwallo/transmatic](https://github.com/andrewdwallo/transmatic) package.
135
+2. Configure the package with your preferred translation service credentials.
136
+3. Run the translation commands as per the package instructions to generate new translations.
137
+
138
+Once you have configured the package, you may update the following method in the `app/Models/Setting/Localization.php` file to generate translations based on the selected language in the application UI:
139
+
140
+Change to the following:
141
+```php
142
+public static function getAllLanguages(): array
143
+{
144
+    return Languages::getNames(app()->getLocale());
145
+}
146
+```
147
+
128 148
 ## Dependencies
129 149
 
130 150
 - [filamentphp/filament](https://github.com/filamentphp/filament) - A collection of beautiful full-stack components
131 151
 - [andrewdwallo/filament-companies](https://github.com/andrewdwallo/filament-companies) - A complete authentication system kit based on companies built for Filament
152
+- [andrewdwallo/transmatic](https://github.com/andrewdwallo/transmatic) - A package for automatic translation using machine translation services
132 153
 - [akaunting/laravel-money](https://github.com/akaunting/laravel-money) - Currency formatting and conversion package for Laravel
133
-- [squirephp/squire](https://github.com/squirephp/squire) - A library of static Eloquent models for common fixture data.
154
+- [squirephp/squire](https://github.com/squirephp/squire) - A library of static Eloquent models for common fixture data
134 155
 
135 156
 ***Note*** : It is recommended to read the documentation for all dependencies to get yourself familiar with how the application works.
136 157
 

+ 6
- 3
app/Actions/FilamentCompanies/AddCompanyEmployee.php Переглянути файл

@@ -2,13 +2,16 @@
2 2
 
3 3
 namespace App\Actions\FilamentCompanies;
4 4
 
5
-use App\Models\{Company, User};
5
+use App\Models\Company;
6
+use App\Models\User;
6 7
 use Closure;
7 8
 use Illuminate\Auth\Access\AuthorizationException;
8 9
 use Illuminate\Contracts\Validation\Rule;
9
-use Illuminate\Support\Facades\{Gate, Validator};
10
+use Illuminate\Support\Facades\Gate;
11
+use Illuminate\Support\Facades\Validator;
10 12
 use Wallo\FilamentCompanies\Contracts\AddsCompanyEmployees;
11
-use Wallo\FilamentCompanies\Events\{AddingCompanyEmployee, CompanyEmployeeAdded};
13
+use Wallo\FilamentCompanies\Events\AddingCompanyEmployee;
14
+use Wallo\FilamentCompanies\Events\CompanyEmployeeAdded;
12 15
 use Wallo\FilamentCompanies\FilamentCompanies;
13 16
 use Wallo\FilamentCompanies\Rules\Role;
14 17
 

+ 4
- 2
app/Actions/FilamentCompanies/CreateCompany.php Переглянути файл

@@ -2,9 +2,11 @@
2 2
 
3 3
 namespace App\Actions\FilamentCompanies;
4 4
 
5
-use App\Models\{Company, User};
5
+use App\Models\Company;
6
+use App\Models\User;
6 7
 use Illuminate\Auth\Access\AuthorizationException;
7
-use Illuminate\Support\Facades\{Gate, Validator};
8
+use Illuminate\Support\Facades\Gate;
9
+use Illuminate\Support\Facades\Validator;
8 10
 use Wallo\FilamentCompanies\Contracts\CreatesCompanies;
9 11
 use Wallo\FilamentCompanies\Events\AddingCompany;
10 12
 use Wallo\FilamentCompanies\FilamentCompanies;

+ 2
- 1
app/Actions/FilamentCompanies/CreateConnectedAccount.php Переглянути файл

@@ -4,8 +4,9 @@ namespace App\Actions\FilamentCompanies;
4 4
 
5 5
 use Illuminate\Contracts\Auth\Authenticatable;
6 6
 use Laravel\Socialite\Contracts\User as ProviderUser;
7
+use Wallo\FilamentCompanies\ConnectedAccount;
7 8
 use Wallo\FilamentCompanies\Contracts\CreatesConnectedAccounts;
8
-use Wallo\FilamentCompanies\{ConnectedAccount, Socialite};
9
+use Wallo\FilamentCompanies\Socialite;
9 10
 
10 11
 class CreateConnectedAccount implements CreatesConnectedAccounts
11 12
 {

+ 5
- 2
app/Actions/FilamentCompanies/CreateNewUser.php Переглянути файл

@@ -2,8 +2,11 @@
2 2
 
3 3
 namespace App\Actions\FilamentCompanies;
4 4
 
5
-use App\Models\{Company, User};
6
-use Illuminate\Support\Facades\{DB, Hash, Validator};
5
+use App\Models\Company;
6
+use App\Models\User;
7
+use Illuminate\Support\Facades\DB;
8
+use Illuminate\Support\Facades\Hash;
9
+use Illuminate\Support\Facades\Validator;
7 10
 use Wallo\FilamentCompanies\Contracts\CreatesNewUsers;
8 11
 use Wallo\FilamentCompanies\Features;
9 12
 

+ 6
- 3
app/Actions/FilamentCompanies/CreateUserFromProvider.php Переглянути файл

@@ -2,11 +2,14 @@
2 2
 
3 3
 namespace App\Actions\FilamentCompanies;
4 4
 
5
-use App\Models\{Company, User};
5
+use App\Models\Company;
6
+use App\Models\User;
6 7
 use Illuminate\Support\Facades\DB;
7 8
 use Laravel\Socialite\Contracts\User as ProviderUserContract;
8
-use Wallo\FilamentCompanies\Contracts\{CreatesConnectedAccounts, CreatesUserFromProvider};
9
-use Wallo\FilamentCompanies\{Features, Socialite};
9
+use Wallo\FilamentCompanies\Contracts\CreatesConnectedAccounts;
10
+use Wallo\FilamentCompanies\Contracts\CreatesUserFromProvider;
11
+use Wallo\FilamentCompanies\Features;
12
+use Wallo\FilamentCompanies\Socialite;
10 13
 
11 14
 class CreateUserFromProvider implements CreatesUserFromProvider
12 15
 {

+ 4
- 2
app/Actions/FilamentCompanies/DeleteUser.php Переглянути файл

@@ -2,9 +2,11 @@
2 2
 
3 3
 namespace App\Actions\FilamentCompanies;
4 4
 
5
-use App\Models\{Company, User};
5
+use App\Models\Company;
6
+use App\Models\User;
6 7
 use Illuminate\Support\Facades\DB;
7
-use Wallo\FilamentCompanies\Contracts\{DeletesCompanies, DeletesUsers};
8
+use Wallo\FilamentCompanies\Contracts\DeletesCompanies;
9
+use Wallo\FilamentCompanies\Contracts\DeletesUsers;
8 10
 
9 11
 class DeleteUser implements DeletesUsers
10 12
 {

+ 5
- 2
app/Actions/FilamentCompanies/InviteCompanyEmployee.php Переглянути файл

@@ -2,11 +2,14 @@
2 2
 
3 3
 namespace App\Actions\FilamentCompanies;
4 4
 
5
-use App\Models\{Company, User};
5
+use App\Models\Company;
6
+use App\Models\User;
6 7
 use Closure;
7 8
 use Illuminate\Auth\Access\AuthorizationException;
8 9
 use Illuminate\Database\Query\Builder;
9
-use Illuminate\Support\Facades\{Gate, Mail, Validator};
10
+use Illuminate\Support\Facades\Gate;
11
+use Illuminate\Support\Facades\Mail;
12
+use Illuminate\Support\Facades\Validator;
10 13
 use Illuminate\Validation\Rule;
11 14
 use Wallo\FilamentCompanies\Contracts\InvitesCompanyEmployees;
12 15
 use Wallo\FilamentCompanies\Events\InvitingCompanyEmployee;

+ 2
- 1
app/Actions/FilamentCompanies/RemoveCompanyEmployee.php Переглянути файл

@@ -2,7 +2,8 @@
2 2
 
3 3
 namespace App\Actions\FilamentCompanies;
4 4
 
5
-use App\Models\{Company, User};
5
+use App\Models\Company;
6
+use App\Models\User;
6 7
 use Illuminate\Auth\Access\AuthorizationException;
7 8
 use Illuminate\Support\Facades\Gate;
8 9
 use Illuminate\Validation\ValidationException;

+ 2
- 1
app/Actions/FilamentCompanies/SetUserPassword.php Переглянути файл

@@ -3,7 +3,8 @@
3 3
 namespace App\Actions\FilamentCompanies;
4 4
 
5 5
 use App\Models\User;
6
-use Illuminate\Support\Facades\{Hash, Validator};
6
+use Illuminate\Support\Facades\Hash;
7
+use Illuminate\Support\Facades\Validator;
7 8
 use Wallo\FilamentCompanies\Contracts\SetsUserPasswords;
8 9
 
9 10
 class SetUserPassword implements SetsUserPasswords

+ 4
- 2
app/Actions/FilamentCompanies/UpdateCompanyName.php Переглянути файл

@@ -2,9 +2,11 @@
2 2
 
3 3
 namespace App\Actions\FilamentCompanies;
4 4
 
5
-use App\Models\{Company, User};
5
+use App\Models\Company;
6
+use App\Models\User;
6 7
 use Illuminate\Auth\Access\AuthorizationException;
7
-use Illuminate\Support\Facades\{Gate, Validator};
8
+use Illuminate\Support\Facades\Gate;
9
+use Illuminate\Support\Facades\Validator;
8 10
 use Wallo\FilamentCompanies\Contracts\UpdatesCompanyNames;
9 11
 
10 12
 class UpdateCompanyName implements UpdatesCompanyNames

+ 2
- 1
app/Actions/FilamentCompanies/UpdateUserPassword.php Переглянути файл

@@ -3,7 +3,8 @@
3 3
 namespace App\Actions\FilamentCompanies;
4 4
 
5 5
 use App\Models\User;
6
-use Illuminate\Support\Facades\{Hash, Validator};
6
+use Illuminate\Support\Facades\Hash;
7
+use Illuminate\Support\Facades\Validator;
7 8
 use Wallo\FilamentCompanies\Contracts\UpdatesUserPasswords;
8 9
 
9 10
 class UpdateUserPassword implements UpdatesUserPasswords

+ 2
- 1
app/Actions/OptionAction/CreateCurrency.php Переглянути файл

@@ -3,12 +3,13 @@
3 3
 namespace App\Actions\OptionAction;
4 4
 
5 5
 use App\Models\Setting\Currency;
6
+use App\Utilities\Currency\CurrencyAccessor;
6 7
 
7 8
 class CreateCurrency
8 9
 {
9 10
     public function create(string $code, string $name, string $rate): Currency
10 11
     {
11
-        $defaultCurrency = Currency::getDefaultCurrencyCode();
12
+        $defaultCurrency = CurrencyAccessor::getDefaultCurrency();
12 13
 
13 14
         $hasDefaultCurrency = $defaultCurrency !== null;
14 15
         $currency_code = currency($code);

+ 24
- 0
app/Casts/CurrencyRateCast.php Переглянути файл

@@ -0,0 +1,24 @@
1
+<?php
2
+
3
+namespace App\Casts;
4
+
5
+use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
6
+
7
+class CurrencyRateCast 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
+}

+ 7
- 6
app/Casts/MoneyCast.php Переглянути файл

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

+ 49
- 7
app/Casts/RateCast.php Переглянути файл

@@ -2,23 +2,65 @@
2 2
 
3 3
 namespace App\Casts;
4 4
 
5
+use App\Enums\NumberFormat;
6
+use App\Models\Setting\Localization;
7
+use App\Utilities\Currency\CurrencyAccessor;
5 8
 use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
9
+use Illuminate\Database\Eloquent\Model;
6 10
 
7 11
 class RateCast implements CastsAttributes
8 12
 {
9
-    private const SCALE = 8;
13
+    private const PRECISION = 4;
10 14
 
11
-    public function get($model, string $key, $value, array $attributes): float
15
+    public function get($model, string $key, $value, array $attributes): string
12 16
     {
13
-        $floatValue = $value / (10 ** self::SCALE);
17
+        $currency_code = $this->getDefaultCurrencyCode();
18
+        $computation = $attributes['computation'] ?? null;
14 19
 
15
-        $strValue = rtrim(rtrim(number_format($floatValue, self::SCALE, '.', ''), '0'), '.');
20
+        if ($computation === 'fixed') {
21
+            return money($value, $currency_code)->formatSimple();
22
+        }
16 23
 
17
-        return (float) $strValue;
24
+        $floatValue = $value / (10 ** self::PRECISION);
25
+
26
+        $format = Localization::firstOrFail()->number_format->value;
27
+        [$decimal_mark, $thousands_separator] = NumberFormat::from($format)->getFormattingParameters();
28
+
29
+        return $this->formatWithoutTrailingZeros($floatValue, $decimal_mark, $thousands_separator);
18 30
     }
19 31
 
20
-    public function set($model, string $key, $value, array $attributes): int
32
+    public function set(Model $model, string $key, mixed $value, array $attributes): int
21 33
     {
22
-        return (int) round($value * (10 ** self::SCALE));
34
+        if (is_int($value)) {
35
+            return $value;
36
+        }
37
+
38
+        $computation = $attributes['computation'] ?? null;
39
+
40
+        $currency_code = $this->getDefaultCurrencyCode();
41
+
42
+        if ($computation === 'fixed') {
43
+            return money($value, $currency_code, true)->getAmount();
44
+        }
45
+
46
+        $format = Localization::firstOrFail()->number_format->value;
47
+        [$decimal_mark, $thousands_separator] = NumberFormat::from($format)->getFormattingParameters();
48
+
49
+        $intValue = str_replace([$thousands_separator, $decimal_mark], ['', '.'], $value);
50
+
51
+        return (int) round((float) $intValue * (10 ** self::PRECISION));
52
+    }
53
+
54
+    private function getDefaultCurrencyCode(): string
55
+    {
56
+        return CurrencyAccessor::getDefaultCurrency();
57
+    }
58
+
59
+    private function formatWithoutTrailingZeros($floatValue, $decimal_mark, $thousands_separator): string
60
+    {
61
+        $formatted = number_format($floatValue, self::PRECISION, $decimal_mark, $thousands_separator);
62
+        $formatted = rtrim($formatted, '0');
63
+
64
+        return rtrim($formatted, $decimal_mark);
23 65
     }
24 66
 }

+ 0
- 36
app/Console/Commands/CacheData.php Переглянути файл

@@ -1,36 +0,0 @@
1
-<?php
2
-
3
-namespace App\Console\Commands;
4
-
5
-use App\Utilities\ModelCacheManager;
6
-use Illuminate\Console\Command;
7
-
8
-class CacheData extends Command
9
-{
10
-    /**
11
-     * The name and signature of the console command.
12
-     *
13
-     * @var string
14
-     */
15
-    protected $signature = 'cache:data';
16
-
17
-    /**
18
-     * The console command description.
19
-     *
20
-     * @var string
21
-     */
22
-    protected $description = 'Cache data from CSV files into the database.';
23
-
24
-    /**
25
-     * Execute the console command.
26
-     */
27
-    public function handle(): void
28
-    {
29
-        // ModelCacheManager::cacheData(resource_path('data/countries.csv'), 'countries');
30
-        ModelCacheManager::cacheData(resource_path('data/currencies.csv'), 'currencies');
31
-        // ModelCacheManager::cacheData(resource_path('data/states.csv'), 'states');
32
-        // ModelCacheManager::cacheData(resource_path('data/cities.csv'), 'cities');
33
-
34
-        $this->info('Data cached successfully.');
35
-    }
36
-}

+ 0
- 71
app/Console/Commands/ConvertTimezones.php Переглянути файл

@@ -1,71 +0,0 @@
1
-<?php
2
-
3
-namespace App\Console\Commands;
4
-
5
-use Illuminate\Console\Command;
6
-
7
-class ConvertTimezones extends Command
8
-{
9
-    /**
10
-     * The name and signature of the console command.
11
-     *
12
-     * @var string
13
-     */
14
-    protected $signature = 'convert:timezones';
15
-
16
-    /**
17
-     * The console command description.
18
-     *
19
-     * @var string
20
-     */
21
-    protected $description = 'Converts countries csv to generate a timezones csv file';
22
-
23
-    /**
24
-     * Execute the console command.
25
-     */
26
-    public function handle(): int
27
-    {
28
-        $sourcePath = resource_path('data/countries.csv');
29
-        $destinationPath = resource_path('data/timezones.csv');
30
-
31
-        $source = fopen($sourcePath, 'rb');
32
-        $destination = fopen($destinationPath, 'wb');
33
-
34
-        fputcsv($destination, ['id', 'country_id', 'country_code', 'name', 'gmt_offset', 'gmt_offset_name', 'abbreviation', 'tz_name']);
35
-
36
-        $idCounter = 1;
37
-
38
-        $headers = fgetcsv($source);
39
-
40
-        while (($row = fgetcsv($source)) !== false) {
41
-            $rowAssoc = array_combine($headers, $row);
42
-            $countryId = $rowAssoc['id'];
43
-            $countryCode = $rowAssoc['iso_code_2'];
44
-            $timezonesJson = $rowAssoc['timezones'];
45
-
46
-            $timezonesArray = json_decode($timezonesJson, true);
47
-
48
-            foreach ($timezonesArray as $timezone) {
49
-                $newRow = [
50
-                    $idCounter++,
51
-                    $countryId,
52
-                    $countryCode,
53
-                    $timezone['zoneName'],
54
-                    $timezone['gmtOffset'],
55
-                    $timezone['gmtOffsetName'],
56
-                    $timezone['abbreviation'],
57
-                    $timezone['tzName'],
58
-                ];
59
-
60
-                fputcsv($destination, $newRow);
61
-            }
62
-        }
63
-
64
-        fclose($source);
65
-        fclose($destination);
66
-
67
-        $this->info('Timezones csv file generated successfully.');
68
-
69
-        return 0;
70
-    }
71
-}

+ 72
- 0
app/Console/Commands/InitializeCurrencies.php Переглянути файл

@@ -0,0 +1,72 @@
1
+<?php
2
+
3
+namespace App\Console\Commands;
4
+
5
+use Akaunting\Money\Currency;
6
+use App\Contracts\CurrencyHandler;
7
+use App\Facades\Forex;
8
+use App\Models\Service\CurrencyList;
9
+use Illuminate\Console\Command;
10
+
11
+class InitializeCurrencies extends Command
12
+{
13
+    /**
14
+     * The name and signature of the console command.
15
+     *
16
+     * @var string
17
+     */
18
+    protected $signature = 'currency:init';
19
+
20
+    /**
21
+     * The console command description.
22
+     *
23
+     * @var string
24
+     */
25
+    protected $description = 'Initialize currencies from the API';
26
+
27
+    public function __construct(private readonly CurrencyHandler $currencyService)
28
+    {
29
+        parent::__construct();
30
+    }
31
+
32
+    /**
33
+     * Execute the console command.
34
+     */
35
+    public function handle(): void
36
+    {
37
+        $this->info('Fetching supported currencies from the API...');
38
+
39
+        $apiSupportedCurrencies = $this->currencyService->getSupportedCurrencies();
40
+
41
+        if (Forex::isDisabled()) {
42
+            $this->error('The Currency Exchange Rate feature is disabled.');
43
+
44
+            return;
45
+        }
46
+
47
+        if (empty($apiSupportedCurrencies)) {
48
+            $this->error('Failed to fetch supported currencies from the API.');
49
+
50
+            return;
51
+        }
52
+
53
+        $appSupportedCurrencies = array_keys(Currency::getCurrencies());
54
+
55
+        foreach ($appSupportedCurrencies as $appSupportedCurrency) {
56
+            $isAvailable = in_array($appSupportedCurrency, $apiSupportedCurrencies, true);
57
+            $currencyAttributes = [
58
+                'code' => $appSupportedCurrency,
59
+                'name' => currency($appSupportedCurrency)->getName(),
60
+                'entity' => currency($appSupportedCurrency)->getEntity(),
61
+                'available' => $isAvailable,
62
+            ];
63
+
64
+            CurrencyList::updateOrCreate(
65
+                ['code' => $appSupportedCurrency],
66
+                $currencyAttributes
67
+            );
68
+        }
69
+
70
+        $this->info('Successfully initialized currencies.');
71
+    }
72
+}

+ 0
- 71
app/Console/Commands/SortCsv.php Переглянути файл

@@ -1,71 +0,0 @@
1
-<?php
2
-
3
-namespace App\Console\Commands;
4
-
5
-use Illuminate\Console\Command;
6
-
7
-class SortCsv extends Command
8
-{
9
-    /**
10
-     * The name and signature of the console command.
11
-     *
12
-     * @var string
13
-     */
14
-    protected $signature = 'sort:csv';
15
-
16
-    /**
17
-     * The console command description.
18
-     *
19
-     * @var string
20
-     */
21
-    protected $description = 'Sort the cities CSV file by country code and state code';
22
-
23
-    /**
24
-     * Execute the console command.
25
-     */
26
-    public function handle(): void
27
-    {
28
-        $inputPath = resource_path('data/cities.csv');
29
-        $outputPath = resource_path('data/cities-sorted.csv');
30
-
31
-        $fileInput = fopen($inputPath, 'rb');
32
-        $fileOutput = fopen($outputPath, 'wb');
33
-
34
-        // Write header to output file
35
-        if (($header = fgetcsv($fileInput, 1000, ',')) !== false) {
36
-            fputcsv($fileOutput, $header);
37
-        }
38
-
39
-        $buffer = [];
40
-        while (($row = fgetcsv($fileInput, 1000, ',')) !== false) {
41
-            $buffer[] = array_combine($header, $row);
42
-
43
-            // When buffer reaches some size, sort and write to file
44
-            if (count($buffer) >= 10000) {  // Adjust this number based on your available memory
45
-                $this->sortAndWriteBuffer($buffer, $fileOutput);
46
-                $buffer = [];
47
-            }
48
-        }
49
-
50
-        // Sort and write any remaining rows
51
-        $this->sortAndWriteBuffer($buffer, $fileOutput);
52
-
53
-        fclose($fileInput);
54
-        fclose($fileOutput);
55
-    }
56
-
57
-    protected function sortAndWriteBuffer(array $buffer, $fileOutput): void
58
-    {
59
-        usort($buffer, static function ($a, $b) {
60
-            if ($a['country_code'] === $b['country_code']) {
61
-                return (int) $a['state_id'] - (int) $b['state_id'];
62
-            }
63
-
64
-            return strcmp($a['country_code'], $b['country_code']);
65
-        });
66
-
67
-        foreach ($buffer as $row) {
68
-            fputcsv($fileOutput, $row);
69
-        }
70
-    }
71
-}

+ 18
- 0
app/Contracts/CurrencyHandler.php Переглянути файл

@@ -0,0 +1,18 @@
1
+<?php
2
+
3
+namespace App\Contracts;
4
+
5
+interface CurrencyHandler
6
+{
7
+    public function isEnabled(): bool;
8
+
9
+    public function getSupportedCurrencies(): ?array;
10
+
11
+    public function getExchangeRates(string $baseCurrency, array $targetCurrencies): ?array;
12
+
13
+    public function getCachedExchangeRates(string $baseCurrency, array $targetCurrencies): ?array;
14
+
15
+    public function getCachedExchangeRate(string $baseCurrency, string $targetCurrency): ?float;
16
+
17
+    public function updateCurrencyRatesCache(string $baseCurrency): ?array;
18
+}

app/Interfaces/Utility/DocumentNumber.php → app/Contracts/DocumentNumber.php Переглянути файл

@@ -1,6 +1,6 @@
1 1
 <?php
2 2
 
3
-namespace App\Interfaces\Utility;
3
+namespace App\Contracts;
4 4
 
5 5
 use Illuminate\Database\Eloquent\Model;
6 6
 

+ 45
- 0
app/Enums/AccountStatus.php Переглянути файл

@@ -0,0 +1,45 @@
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 AccountStatus: string implements HasColor, HasIcon, HasLabel
10
+{
11
+    case Open = 'open';
12
+    case Active = 'active';
13
+    case Inactive = 'inactive';
14
+    case Restricted = 'restricted';
15
+    case Closed = 'closed';
16
+
17
+    public const DEFAULT = self::Open->value;
18
+
19
+    public function getLabel(): ?string
20
+    {
21
+        return translate($this->name);
22
+    }
23
+
24
+    public function getColor(): string | array | null
25
+    {
26
+        return match ($this) {
27
+            self::Open => 'primary',
28
+            self::Active => 'success',
29
+            self::Inactive => 'gray',
30
+            self::Restricted => 'warning',
31
+            self::Closed => 'danger',
32
+        };
33
+    }
34
+
35
+    public function getIcon(): ?string
36
+    {
37
+        return match ($this) {
38
+            self::Open => 'heroicon-o-currency-dollar',
39
+            self::Active => 'heroicon-o-clock',
40
+            self::Inactive => 'heroicon-o-status-offline',
41
+            self::Restricted => 'heroicon-o-exclamation',
42
+            self::Closed => 'heroicon-o-x-circle',
43
+        };
44
+    }
45
+}

+ 23
- 0
app/Enums/AccountType.php Переглянути файл

@@ -0,0 +1,23 @@
1
+<?php
2
+
3
+namespace App\Enums;
4
+
5
+use Filament\Support\Contracts\HasLabel;
6
+
7
+enum AccountType: string implements HasLabel
8
+{
9
+    case Checking = 'checking';
10
+    case Savings = 'savings';
11
+    case MoneyMarket = 'money_market';
12
+    case CreditCard = 'credit_card';
13
+    case Merchant = 'merchant';
14
+
15
+    public const DEFAULT = self::Checking->value;
16
+
17
+    public function getLabel(): ?string
18
+    {
19
+        $label = ucwords(str_replace('_', ' ', $this->value));
20
+
21
+        return translate($label);
22
+    }
23
+}

+ 1
- 1
app/Enums/CategoryType.php Переглянути файл

@@ -13,6 +13,6 @@ enum CategoryType: string implements HasLabel
13 13
 
14 14
     public function getLabel(): ?string
15 15
     {
16
-        return $this->name;
16
+        return translate($this->name);
17 17
     }
18 18
 }

+ 3
- 1
app/Enums/ContactType.php Переглянути файл

@@ -3,7 +3,9 @@
3 3
 namespace App\Enums;
4 4
 
5 5
 use Filament\Support\Colors\Color;
6
-use Filament\Support\Contracts\{HasColor, HasIcon, HasLabel};
6
+use Filament\Support\Contracts\HasColor;
7
+use Filament\Support\Contracts\HasIcon;
8
+use Filament\Support\Contracts\HasLabel;
7 9
 
8 10
 enum ContactType: string implements HasColor, HasIcon, HasLabel
9 11
 {

+ 41
- 0
app/Enums/DateFormat.php Переглянути файл

@@ -0,0 +1,41 @@
1
+<?php
2
+
3
+namespace App\Enums;
4
+
5
+use Filament\Support\Contracts\HasLabel;
6
+
7
+enum DateFormat: string implements HasLabel
8
+{
9
+    // Day-Month-Year Formats
10
+    case DMY_SLASH = 'd/m/Y'; // 31/12/2021
11
+    case DMY_DASH = 'd-m-Y'; // 31-12-2021
12
+    case DMY_DOT = 'd.m.Y'; // 31.12.2021
13
+    case DMY_SPACE = 'd m Y'; // 31 12 2021
14
+    case DMY_LONG = 'd F Y'; // 31 December 2021
15
+    case DMY_SHORT = 'd M Y'; // 31 Dec 2021
16
+
17
+    // Month-Day-Year Formats
18
+    case MDY_SLASH = 'm/d/Y'; // 12/31/2021
19
+    case MDY_DASH = 'm-d-Y'; // 12-31-2021
20
+    case MDY_DOT = 'm.d.Y'; // 12.31.2021
21
+    case MDY_SPACE = 'm d Y'; // 12 31 2021
22
+    case MDY_LONG_SPACE = 'F d Y'; // December 31 2021
23
+    case MDY_LONG_COMMA = 'F j, Y'; // December 31, 2021
24
+    case MDY_SHORT_SPACE = 'M d Y'; // Dec 31 2021
25
+    case MDY_SHORT_COMMA = 'M j, Y'; // Dec 31, 2021
26
+
27
+    // Year-Month-Day Formats
28
+    case YMD_SLASH = 'Y/m/d'; // 2021/12/31
29
+    case YMD_DASH = 'Y-m-d'; // 2021-12-31
30
+    case YMD_DOT = 'Y.m.d'; // 2021.12.31
31
+    case YMD_SPACE = 'Y m d'; // 2021 12 31
32
+    case YMD_LONG = 'Y F d'; // 2021 December 31
33
+    case YMD_SHORT = 'Y M d'; // 2021 Dec 31
34
+
35
+    public const DEFAULT = self::MDY_SHORT_COMMA->value;
36
+
37
+    public function getLabel(): ?string
38
+    {
39
+        return now()->translatedFormat($this->value);
40
+    }
41
+}

+ 3
- 1
app/Enums/DiscountComputation.php Переглянути файл

@@ -9,8 +9,10 @@ enum DiscountComputation: string implements HasLabel
9 9
     case Percentage = 'percentage';
10 10
     case Fixed = 'fixed';
11 11
 
12
+    public const DEFAULT = self::Percentage->value;
13
+
12 14
     public function getLabel(): ?string
13 15
     {
14
-        return $this->name;
16
+        return translate($this->name);
15 17
     }
16 18
 }

+ 1
- 1
app/Enums/DiscountScope.php Переглянути файл

@@ -11,6 +11,6 @@ enum DiscountScope: string implements HasLabel
11 11
 
12 12
     public function getLabel(): ?string
13 13
     {
14
-        return $this->name;
14
+        return translate($this->name);
15 15
     }
16 16
 }

+ 6
- 2
app/Enums/DiscountType.php Переглянути файл

@@ -2,7 +2,9 @@
2 2
 
3 3
 namespace App\Enums;
4 4
 
5
-use Filament\Support\Contracts\{HasColor, HasIcon, HasLabel};
5
+use Filament\Support\Contracts\HasColor;
6
+use Filament\Support\Contracts\HasIcon;
7
+use Filament\Support\Contracts\HasLabel;
6 8
 
7 9
 enum DiscountType: string implements HasColor, HasIcon, HasLabel
8 10
 {
@@ -10,9 +12,11 @@ enum DiscountType: string implements HasColor, HasIcon, HasLabel
10 12
     case Purchase = 'purchase';
11 13
     case None = 'none';
12 14
 
15
+    public const DEFAULT = self::Sales->value;
16
+
13 17
     public function getLabel(): ?string
14 18
     {
15
-        return $this->name;
19
+        return translate($this->name);
16 20
     }
17 21
 
18 22
     public function getColor(): string | array | null

+ 0
- 19
app/Enums/DocumentAmountColumn.php Переглянути файл

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

+ 0
- 23
app/Enums/DocumentItemColumn.php Переглянути файл

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

+ 0
- 19
app/Enums/DocumentPriceColumn.php Переглянути файл

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

+ 2
- 1
app/Enums/DocumentType.php Переглянути файл

@@ -2,7 +2,8 @@
2 2
 
3 3
 namespace App\Enums;
4 4
 
5
-use Filament\Support\Contracts\{HasIcon, HasLabel};
5
+use Filament\Support\Contracts\HasIcon;
6
+use Filament\Support\Contracts\HasLabel;
6 7
 
7 8
 enum DocumentType: string implements HasIcon, HasLabel
8 9
 {

+ 0
- 19
app/Enums/DocumentUnitColumn.php Переглянути файл

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

+ 3
- 1
app/Enums/EntityType.php Переглянути файл

@@ -16,7 +16,7 @@ enum EntityType: string implements HasLabel
16 16
 
17 17
     public function getLabel(): ?string
18 18
     {
19
-        return match ($this) {
19
+        $label = match ($this) {
20 20
             self::SoleProprietorship => 'Sole Proprietorship',
21 21
             self::GeneralPartnership => 'General Partnership',
22 22
             self::LimitedPartnership => 'Limited Partnership (LP)',
@@ -25,5 +25,7 @@ enum EntityType: string implements HasLabel
25 25
             self::Corporation => 'Corporation',
26 26
             self::Nonprofit => 'Nonprofit',
27 27
         };
28
+
29
+        return translate($label);
28 30
     }
29 31
 }

+ 3
- 1
app/Enums/MaxContentWidth.php Переглянути файл

@@ -19,7 +19,7 @@ enum MaxContentWidth: string implements HasLabel
19 19
 
20 20
     public function getLabel(): ?string
21 21
     {
22
-        return match ($this) {
22
+        $label = match ($this) {
23 23
             self::FOUR_XL => '4X Large',
24 24
             self::FIVE_XL => '5X Large',
25 25
             self::SIX_XL => '6X Large',
@@ -29,5 +29,7 @@ enum MaxContentWidth: string implements HasLabel
29 29
             self::SCREEN_2XL => 'Screen 2X Large',
30 30
             self::FULL => 'Full',
31 31
         };
32
+
33
+        return translate($label);
32 34
     }
33 35
 }

+ 3
- 1
app/Enums/ModalWidth.php Переглянути файл

@@ -23,7 +23,7 @@ enum ModalWidth: string implements HasLabel
23 23
 
24 24
     public function getLabel(): ?string
25 25
     {
26
-        return match ($this) {
26
+        $label = match ($this) {
27 27
             self::XS => 'Extra Small',
28 28
             self::SM => 'Small',
29 29
             self::MD => 'Medium',
@@ -37,5 +37,7 @@ enum ModalWidth: string implements HasLabel
37 37
             self::SEVEN_XL => '7X Large',
38 38
             self::SCREEN => 'Screen',
39 39
         };
40
+
41
+        return translate($label);
40 42
     }
41 43
 }

+ 106
- 0
app/Enums/NumberFormat.php Переглянути файл

@@ -0,0 +1,106 @@
1
+<?php
2
+
3
+namespace App\Enums;
4
+
5
+use Filament\Support\Contracts\HasLabel;
6
+use NumberFormatter;
7
+
8
+enum NumberFormat: string implements HasLabel
9
+{
10
+    case CommaDot = 'comma_dot';
11
+    case DotComma = 'dot_comma';
12
+    case IndianGrouping = 'indian_grouping';
13
+
14
+    case ApostropheDot = 'apostrophe_dot';
15
+
16
+    case SpaceComma = 'space_comma';
17
+    case SpaceDot = 'space_dot';
18
+
19
+    public const DEFAULT = self::CommaDot->value;
20
+
21
+    public function getLabel(): ?string
22
+    {
23
+        return match ($this) {
24
+            self::CommaDot => '#,###,###.##',
25
+            self::DotComma => '#.###.###,##',
26
+            self::IndianGrouping => '#,##,###.##',
27
+            self::ApostropheDot => '#\'###\'###.##',
28
+            self::SpaceComma => '# ### ###,##',
29
+            self::SpaceDot => '# ### ###.##',
30
+        };
31
+    }
32
+
33
+    public function getDecimalMark(): string
34
+    {
35
+        return match ($this) {
36
+            self::CommaDot, self::SpaceDot, self::IndianGrouping, self::ApostropheDot => '.',
37
+            self::DotComma, self::SpaceComma => ',',
38
+        };
39
+    }
40
+
41
+    public function getThousandsSeparator(): string
42
+    {
43
+        return match ($this) {
44
+            self::CommaDot, self::IndianGrouping => ',',
45
+            self::DotComma => '.',
46
+            self::SpaceComma, self::SpaceDot => ' ',
47
+            self::ApostropheDot => '\'',
48
+        };
49
+    }
50
+
51
+    public function getFormattedExample(): string
52
+    {
53
+        $exampleNumber = 1234567.89;
54
+        $formatter = new NumberFormatter($this->getAssociatedLocale(), NumberFormatter::DECIMAL);
55
+
56
+        return $formatter->format($exampleNumber);
57
+    }
58
+
59
+    public function getAssociatedLocale(): string
60
+    {
61
+        return match ($this) {
62
+            self::CommaDot => 'en_US',
63
+            self::DotComma => 'de_DE',
64
+            self::IndianGrouping => 'en_IN',
65
+            self::ApostropheDot => 'fr_FR',
66
+            self::SpaceComma => 'fr_CH',
67
+            self::SpaceDot => 'xh_ZA',
68
+        };
69
+    }
70
+
71
+    public static function fromLanguageAndCountry(string $language, string $countryCode): string
72
+    {
73
+        $testNumber = 1234567.8912;
74
+        $fullLocale = "{$language}_{$countryCode}";
75
+
76
+        $numberFormatter = new NumberFormatter($fullLocale, NumberFormatter::DECIMAL);
77
+        $formattedNumber = $numberFormatter->format($testNumber);
78
+
79
+        return self::fromFormattedNumber($formattedNumber);
80
+    }
81
+
82
+    public static function fromFormattedNumber(string $formattedNumber): string
83
+    {
84
+        $commaDot = strpos($formattedNumber, '.') && strpos($formattedNumber, ',');
85
+        $dotComma = strpos($formattedNumber, ',') && strpos($formattedNumber, '.');
86
+        $indianGrouping = strpos($formattedNumber, ',') && ! strpos($formattedNumber, '.');
87
+        $apostropheDot = strpos($formattedNumber, '\'') && strpos($formattedNumber, '.');
88
+        $spaceComma = strpos($formattedNumber, ' ') && strpos($formattedNumber, ',');
89
+        $spaceDot = strpos($formattedNumber, ' ') && strpos($formattedNumber, '.');
90
+
91
+        return match (true) {
92
+            $commaDot => self::CommaDot->value,
93
+            $dotComma => self::DotComma->value,
94
+            $indianGrouping => self::IndianGrouping->value,
95
+            $apostropheDot => self::ApostropheDot->value,
96
+            $spaceComma => self::SpaceComma->value,
97
+            $spaceDot => self::SpaceDot->value,
98
+            default => self::DEFAULT,
99
+        };
100
+    }
101
+
102
+    public function getFormattingParameters(): array
103
+    {
104
+        return [$this->getDecimalMark(), $this->getThousandsSeparator()];
105
+    }
106
+}

+ 8
- 14
app/Enums/PaymentTerms.php Переглянути файл

@@ -6,7 +6,7 @@ use Filament\Support\Contracts\HasLabel;
6 6
 
7 7
 enum PaymentTerms: string implements HasLabel
8 8
 {
9
-    case DueOnReceipt = 'due_on_receipt';
9
+    case DueUponReceipt = 'due_upon_receipt';
10 10
     case Net7 = 'net_7';
11 11
     case Net10 = 'net_10';
12 12
     case Net15 = 'net_15';
@@ -14,25 +14,19 @@ enum PaymentTerms: string implements HasLabel
14 14
     case Net60 = 'net_60';
15 15
     case Net90 = 'net_90';
16 16
 
17
-    public const DEFAULT = self::DueOnReceipt->value;
17
+    public const DEFAULT = self::DueUponReceipt->value;
18 18
 
19 19
     public function getLabel(): ?string
20 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
-        };
21
+        $label = ucwords(str_replace('_', ' ', $this->value));
22
+
23
+        return translate($label);
30 24
     }
31 25
 
32 26
     public function getDays(): int
33 27
     {
34 28
         return match ($this) {
35
-            self::DueOnReceipt => 0,
29
+            self::DueUponReceipt => 0,
36 30
             self::Net7 => 7,
37 31
             self::Net10 => 10,
38 32
             self::Net15 => 15,
@@ -42,10 +36,10 @@ enum PaymentTerms: string implements HasLabel
42 36
         };
43 37
     }
44 38
 
45
-    public function getDueDate(): string
39
+    public function getDueDate(string $format): string
46 40
     {
47 41
         $days = $this->getDays() ?? 0;
48 42
 
49
-        return now()->addDays($days)->format('M d, Y');
43
+        return now()->addDays($days)->translatedFormat($format);
50 44
     }
51 45
 }

+ 7
- 1
app/Enums/PrimaryColor.php Переглянути файл

@@ -5,10 +5,11 @@ namespace App\Enums;
5 5
 use App\Enums\Concerns\Utilities;
6 6
 use Filament\Support\Colors\Color;
7 7
 use Filament\Support\Contracts\HasColor;
8
+use Filament\Support\Contracts\HasLabel;
8 9
 use Spatie\Color\Rgb;
9 10
 use UnexpectedValueException;
10 11
 
11
-enum PrimaryColor: string implements HasColor
12
+enum PrimaryColor: string implements HasColor, HasLabel
12 13
 {
13 14
     use Utilities;
14 15
 
@@ -65,6 +66,11 @@ enum PrimaryColor: string implements HasColor
65 66
         };
66 67
     }
67 68
 
69
+    public function getLabel(): ?string
70
+    {
71
+        return ucfirst(translate($this->value));
72
+    }
73
+
68 74
     /**
69 75
      * @throws UnexpectedValueException
70 76
      */

+ 0
- 10
app/Enums/RecordsPerPage.php Переглянути файл

@@ -16,16 +16,6 @@ enum RecordsPerPage: int implements HasLabel
16 16
 
17 17
     public const DEFAULT = self::Ten->value;
18 18
 
19
-    public const FIVE = self::Five->value;
20
-
21
-    public const TEN = self::Ten->value;
22
-
23
-    public const TWENTY_FIVE = self::TwentyFive->value;
24
-
25
-    public const FIFTY = self::Fifty->value;
26
-
27
-    public const ONE_HUNDRED = self::OneHundred->value;
28
-
29 19
     public function getLabel(): ?string
30 20
     {
31 21
         return (string) $this->value;

+ 1
- 1
app/Enums/TableSortDirection.php Переглянути файл

@@ -13,6 +13,6 @@ enum TableSortDirection: string implements HasLabel
13 13
 
14 14
     public function getLabel(): ?string
15 15
     {
16
-        return $this->name;
16
+        return translate($this->name);
17 17
     }
18 18
 }

+ 3
- 1
app/Enums/TaxComputation.php Переглянути файл

@@ -10,8 +10,10 @@ enum TaxComputation: string implements HasLabel
10 10
     case Percentage = 'percentage';
11 11
     case Compound = 'compound';
12 12
 
13
+    public const DEFAULT = self::Percentage->value;
14
+
13 15
     public function getLabel(): ?string
14 16
     {
15
-        return $this->name;
17
+        return translate($this->name);
16 18
     }
17 19
 }

+ 1
- 1
app/Enums/TaxScope.php Переглянути файл

@@ -11,6 +11,6 @@ enum TaxScope: string implements HasLabel
11 11
 
12 12
     public function getLabel(): ?string
13 13
     {
14
-        return $this->name;
14
+        return translate($this->name);
15 15
     }
16 16
 }

+ 6
- 2
app/Enums/TaxType.php Переглянути файл

@@ -2,7 +2,9 @@
2 2
 
3 3
 namespace App\Enums;
4 4
 
5
-use Filament\Support\Contracts\{HasColor, HasIcon, HasLabel};
5
+use Filament\Support\Contracts\HasColor;
6
+use Filament\Support\Contracts\HasIcon;
7
+use Filament\Support\Contracts\HasLabel;
6 8
 
7 9
 enum TaxType: string implements HasColor, HasIcon, HasLabel
8 10
 {
@@ -10,9 +12,11 @@ enum TaxType: string implements HasColor, HasIcon, HasLabel
10 12
     case Purchase = 'purchase';
11 13
     case None = 'none';
12 14
 
15
+    public const DEFAULT = self::Sales->value;
16
+
13 17
     public function getLabel(): ?string
14 18
     {
15
-        return $this->name;
19
+        return translate($this->name);
16 20
     }
17 21
 
18 22
     public function getColor(): string | array | null

+ 1
- 1
app/Enums/Template.php Переглянути файл

@@ -14,6 +14,6 @@ enum Template: string implements HasLabel
14 14
 
15 15
     public function getLabel(): ?string
16 16
     {
17
-        return $this->name;
17
+        return translate($this->name);
18 18
     }
19 19
 }

+ 26
- 0
app/Enums/TimeFormat.php Переглянути файл

@@ -0,0 +1,26 @@
1
+<?php
2
+
3
+namespace App\Enums;
4
+
5
+use Filament\Support\Contracts\HasLabel;
6
+use Illuminate\Support\Carbon;
7
+
8
+enum TimeFormat: string implements HasLabel
9
+{
10
+    // 12-Hour Formats
11
+    case G12_CAP = 'g:i A'; // 5:30 AM
12
+    case G12_LOW = 'g:i a'; // 5:30 am
13
+    case H12_CAP = 'h:i A'; // 05:30 AM
14
+    case H12_LOW = 'h:i a'; // 05:30 am
15
+
16
+    // 24-Hour Formats
17
+    case G24 = 'G:i'; // 5:30
18
+    case H24 = 'H:i'; // 05:30
19
+
20
+    public const DEFAULT = self::G12_CAP->value;
21
+
22
+    public function getLabel(): ?string
23
+    {
24
+        return Carbon::createFromTime(5, 30)->translatedFormat($this->value);
25
+    }
26
+}

+ 23
- 0
app/Enums/WeekStart.php Переглянути файл

@@ -0,0 +1,23 @@
1
+<?php
2
+
3
+namespace App\Enums;
4
+
5
+use Filament\Support\Contracts\HasLabel;
6
+
7
+enum WeekStart: int implements HasLabel
8
+{
9
+    case Monday = 1;
10
+    case Tuesday = 2;
11
+    case Wednesday = 3;
12
+    case Thursday = 4;
13
+    case Friday = 5;
14
+    case Saturday = 6;
15
+    case Sunday = 7;
16
+
17
+    public const DEFAULT = self::Monday->value;
18
+
19
+    public function getLabel(): ?string
20
+    {
21
+        return today()->isoWeekday($this->value)->dayName;
22
+    }
23
+}

+ 25
- 0
app/Events/CompanyConfigured.php Переглянути файл

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

+ 9
- 2
app/Events/CompanyGenerated.php Переглянути файл

@@ -2,7 +2,8 @@
2 2
 
3 3
 namespace App\Events;
4 4
 
5
-use App\Models\{Company, User};
5
+use App\Models\Company;
6
+use App\Models\User;
6 7
 use Illuminate\Foundation\Events\Dispatchable;
7 8
 use Illuminate\Queue\SerializesModels;
8 9
 
@@ -17,13 +18,19 @@ class CompanyGenerated
17 18
 
18 19
     public string $country;
19 20
 
21
+    public string $language;
22
+
23
+    public string $currency;
24
+
20 25
     /**
21 26
      * Create a new event instance.
22 27
      */
23
-    public function __construct(User $user, Company $company, string $country)
28
+    public function __construct(User $user, Company $company, string $country, string $language = 'en', string $currency = 'USD')
24 29
     {
25 30
         $this->user = $user;
26 31
         $this->company = $company;
27 32
         $this->country = $country;
33
+        $this->language = $language;
34
+        $this->currency = $currency;
28 35
     }
29 36
 }

+ 25
- 0
app/Events/CurrencyRateChanged.php Переглянути файл

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

+ 0
- 7
app/Events/UpdateCompanyDefault.php Переглянути файл

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

+ 30
- 0
app/Facades/Forex.php Переглянути файл

@@ -0,0 +1,30 @@
1
+<?php
2
+
3
+namespace App\Facades;
4
+
5
+use App\Contracts\CurrencyHandler;
6
+use Illuminate\Support\Facades\Facade;
7
+
8
+/**
9
+ * @method static bool isEnabled()
10
+ * @method static array|null getSupportedCurrencies()
11
+ * @method static array|null getCachedExchangeRates(string $baseCurrency, array $targetCurrencies)
12
+ * @method static float|null getCachedExchangeRate(string $baseCurrency, string $targetCurrency)
13
+ *
14
+ * @see CurrencyHandler
15
+ */
16
+class Forex extends Facade
17
+{
18
+    protected static function getFacadeAccessor(): string
19
+    {
20
+        return CurrencyHandler::class;
21
+    }
22
+
23
+    /**
24
+     * Determine if the Currency Exchange Rate feature is disabled.
25
+     */
26
+    public static function isDisabled(): bool
27
+    {
28
+        return ! static::isEnabled();
29
+    }
30
+}

+ 19
- 0
app/Faker/CurrencyCode.php Переглянути файл

@@ -0,0 +1,19 @@
1
+<?php
2
+
3
+namespace App\Faker;
4
+
5
+use App\Models\Locale\Country;
6
+use Faker\Provider\Base;
7
+use OutOfBoundsException;
8
+
9
+class CurrencyCode extends Base
10
+{
11
+    public function currencyCode(string $countryCode): string
12
+    {
13
+        try {
14
+            return Country::where('id', $countryCode)->pluck('currency_code')->first();
15
+        } catch (OutOfBoundsException $e) {
16
+            return 'USD';
17
+        }
18
+    }
19
+}

+ 1
- 1
app/Faker/PhoneNumber.php Переглянути файл

@@ -9,7 +9,7 @@ class PhoneNumber extends BasePhoneNumber
9 9
 {
10 10
     public function phoneNumberForCountryCode(string $countryCode): string
11 11
     {
12
-        $phoneCode = Country::where('iso_code_2', $countryCode)->first()->phone_code;
12
+        $phoneCode = Country::where('id', $countryCode)->pluck('phone_code')->first();
13 13
 
14 14
         $filteredFormats = array_filter(
15 15
             static::$e164Formats,

+ 1
- 3
app/Faker/State.php Переглянути файл

@@ -9,8 +9,6 @@ class State extends Base
9 9
 {
10 10
     public function state(string $countryCode, string $column = 'id'): mixed
11 11
     {
12
-        $state = StateModel::where('country_code', $countryCode)->inRandomOrder()->first();
13
-
14
-        return $state?->{$column};
12
+        return StateModel::where('country_id', $countryCode)->inRandomOrder()->first()?->{$column};
15 13
     }
16 14
 }

+ 33
- 5
app/Filament/Company/Pages/CreateCompany.php Переглянути файл

@@ -4,11 +4,16 @@ namespace App\Filament\Company\Pages;
4 4
 
5 5
 use App\Enums\EntityType;
6 6
 use App\Events\CompanyGenerated;
7
+use App\Models\Company;
7 8
 use App\Models\Locale\Country;
8
-use Filament\Forms\Components\{Select, TextInput};
9
+use App\Utilities\Currency\CurrencyAccessor;
10
+use Filament\Forms\Components\Select;
11
+use Filament\Forms\Components\TextInput;
9 12
 use Filament\Forms\Form;
13
+use Filament\Forms\Get;
10 14
 use Illuminate\Database\Eloquent\Model;
11
-use Illuminate\Support\Facades\{Auth, Gate};
15
+use Illuminate\Support\Facades\Auth;
16
+use Illuminate\Support\Facades\Gate;
12 17
 use Wallo\FilamentCompanies\Events\AddingCompany;
13 18
 use Wallo\FilamentCompanies\FilamentCompanies;
14 19
 use Wallo\FilamentCompanies\Pages\Company\CreateCompany as FilamentCreateCompany;
@@ -30,15 +35,37 @@ class CreateCompany extends FilamentCreateCompany
30 35
                     ->required(),
31 36
                 Select::make('profile.entity_type')
32 37
                     ->label('Entity Type')
33
-                    ->native(false)
34 38
                     ->options(EntityType::class)
35 39
                     ->required(),
36 40
                 Select::make('profile.country')
37 41
                     ->label('Country')
38
-                    ->native(false)
42
+                    ->live()
39 43
                     ->searchable()
40 44
                     ->options(Country::getAvailableCountryOptions())
41 45
                     ->required(),
46
+                Select::make('locale.language')
47
+                    ->label('Language')
48
+                    ->searchable()
49
+                    ->options(static fn (Get $get) => Country::getLanguagesByCountryCode($get('profile.country')))
50
+                    ->getSearchResultsUsing(static function (string $search) {
51
+                        $allLanguages = Country::getLanguagesByCountryCode();
52
+
53
+                        return array_filter($allLanguages, static function ($language) use ($search) {
54
+                            return stripos($language, $search) !== false;
55
+                        });
56
+                    })
57
+                    ->getOptionLabelUsing(static function ($value) {
58
+                        $allLanguages = Country::getLanguagesByCountryCode();
59
+
60
+                        return $allLanguages[$value] ?? $value;
61
+                    })
62
+                    ->required(),
63
+                Select::make('currencies.code')
64
+                    ->label('Currency')
65
+                    ->searchable()
66
+                    ->options(CurrencyAccessor::getAllCurrencyOptions())
67
+                    ->optionsLimit(5)
68
+                    ->required(),
42 69
             ])
43 70
             ->model(FilamentCompanies::companyModel())
44 71
             ->statePath('data');
@@ -54,6 +81,7 @@ class CreateCompany extends FilamentCreateCompany
54 81
 
55 82
         $personalCompany = $user?->personalCompany() === null;
56 83
 
84
+        /** @var Company $company */
57 85
         $company = $user?->ownedCompanies()->create([
58 86
             'name' => $data['name'],
59 87
             'personal_company' => $personalCompany,
@@ -69,7 +97,7 @@ class CreateCompany extends FilamentCreateCompany
69 97
 
70 98
         $name = $data['name'];
71 99
 
72
-        CompanyGenerated::dispatch($user, $company, $data['profile']['country']);
100
+        CompanyGenerated::dispatch($user ?? Auth::user(), $company, $data['profile']['country'], $data['locale']['language'], $data['currencies']['code']);
73 101
 
74 102
         $this->companyCreated($name);
75 103
 

+ 69
- 0
app/Filament/Company/Pages/Service/LiveCurrency.php Переглянути файл

@@ -0,0 +1,69 @@
1
+<?php
2
+
3
+namespace App\Filament\Company\Pages\Service;
4
+
5
+use App\Facades\Forex;
6
+use App\Models\Service\CurrencyList;
7
+use App\Models\Setting\Currency;
8
+use Filament\Pages\Page;
9
+use Illuminate\Contracts\Support\Htmlable;
10
+use Livewire\Attributes\Url;
11
+
12
+class LiveCurrency extends Page
13
+{
14
+    protected static ?string $navigationIcon = 'icon-currency-exchange';
15
+
16
+    protected static ?string $title = 'Live Currency';
17
+
18
+    protected static ?string $navigationGroup = 'Services';
19
+
20
+    protected static ?string $slug = 'services/live-currency';
21
+
22
+    protected static string $view = 'filament.company.pages.service.live-currency';
23
+
24
+    #[Url]
25
+    public ?string $activeTab = null;
26
+
27
+    public function getTitle(): string | Htmlable
28
+    {
29
+        return translate(static::$title);
30
+    }
31
+
32
+    public static function getNavigationLabel(): string
33
+    {
34
+        return translate(static::$title);
35
+    }
36
+
37
+    public static function shouldRegisterNavigation(): bool
38
+    {
39
+        return Forex::isEnabled();
40
+    }
41
+
42
+    public function mount(): void
43
+    {
44
+        $this->loadDefaultActiveTab();
45
+        abort_unless(Forex::isEnabled(), 403);
46
+    }
47
+
48
+    protected function loadDefaultActiveTab(): void
49
+    {
50
+        if (filled($this->activeTab)) {
51
+            return;
52
+        }
53
+
54
+        $this->activeTab = $this->getDefaultActiveTab();
55
+    }
56
+
57
+    public function getDefaultActiveTab(): string | int | null
58
+    {
59
+        return 'currency-list';
60
+    }
61
+
62
+    public function getViewData(): array
63
+    {
64
+        return [
65
+            'currencyListQuery' => CurrencyList::query()->count(),
66
+            'companyCurrenciesQuery' => Currency::query()->count(),
67
+        ];
68
+    }
69
+}

+ 52
- 86
app/Filament/Company/Pages/Setting/Appearance.php Переглянути файл

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

+ 103
- 76
app/Filament/Company/Pages/Setting/CompanyDefault.php Переглянути файл

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

+ 72
- 103
app/Filament/Company/Pages/Setting/CompanyProfile.php Переглянути файл

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

+ 86
- 108
app/Filament/Company/Pages/Setting/Invoice.php Переглянути файл

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

+ 239
- 0
app/Filament/Company/Pages/Setting/Localization.php Переглянути файл

@@ -0,0 +1,239 @@
1
+<?php
2
+
3
+namespace App\Filament\Company\Pages\Setting;
4
+
5
+use App\Enums\DateFormat;
6
+use App\Enums\NumberFormat;
7
+use App\Enums\TimeFormat;
8
+use App\Enums\WeekStart;
9
+use App\Models\Setting\Localization as LocalizationModel;
10
+use App\Utilities\Localization\Timezone;
11
+use Filament\Actions\Action;
12
+use Filament\Actions\ActionGroup;
13
+use Filament\Forms\Components\Component;
14
+use Filament\Forms\Components\DatePicker;
15
+use Filament\Forms\Components\Section;
16
+use Filament\Forms\Components\Select;
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 Filament\Support\Exceptions\Halt;
23
+use Illuminate\Auth\Access\AuthorizationException;
24
+use Illuminate\Contracts\Support\Htmlable;
25
+use Illuminate\Database\Eloquent\Model;
26
+use Illuminate\Support\Str;
27
+use Livewire\Attributes\Locked;
28
+
29
+use function Filament\authorize;
30
+
31
+/**
32
+ * @property Form $form
33
+ */
34
+class Localization extends Page
35
+{
36
+    use InteractsWithFormActions;
37
+
38
+    protected static ?string $navigationIcon = 'heroicon-o-language';
39
+
40
+    protected static ?string $title = 'Localization';
41
+
42
+    protected static ?string $navigationGroup = 'Settings';
43
+
44
+    protected static ?string $slug = 'settings/localization';
45
+
46
+    protected static string $view = 'filament.company.pages.setting.localization';
47
+
48
+    public ?array $data = [];
49
+
50
+    #[Locked]
51
+    public ?LocalizationModel $record = null;
52
+
53
+    public function getTitle(): string | Htmlable
54
+    {
55
+        return translate(static::$title);
56
+    }
57
+
58
+    public static function getNavigationLabel(): string
59
+    {
60
+        return translate(static::$title);
61
+    }
62
+
63
+    public function mount(): void
64
+    {
65
+        $this->record = LocalizationModel::firstOrNew([
66
+            'company_id' => auth()->user()->currentCompany->id,
67
+        ]);
68
+
69
+        abort_unless(static::canView($this->record), 404);
70
+
71
+        $this->fillForm();
72
+    }
73
+
74
+    public function fillForm(): void
75
+    {
76
+        $data = $this->record->attributesToArray();
77
+
78
+        $this->form->fill($data);
79
+    }
80
+
81
+    public function save(): void
82
+    {
83
+        try {
84
+            $data = $this->form->getState();
85
+
86
+            $this->handleRecordUpdate($this->record, $data);
87
+
88
+        } catch (Halt $exception) {
89
+            return;
90
+        }
91
+
92
+        $this->getSavedNotification()->send();
93
+    }
94
+
95
+    protected function getSavedNotification(): Notification
96
+    {
97
+        return Notification::make()
98
+            ->success()
99
+            ->title(__('filament-panels::resources/pages/edit-record.notifications.saved.title'));
100
+    }
101
+
102
+    public function form(Form $form): Form
103
+    {
104
+        return $form
105
+            ->schema([
106
+                $this->getGeneralSection(),
107
+                $this->getDateAndTimeSection(),
108
+                $this->getFinancialAndFiscalSection(),
109
+            ])
110
+            ->model($this->record)
111
+            ->statePath('data')
112
+            ->operation('edit');
113
+    }
114
+
115
+    protected function getGeneralSection(): Component
116
+    {
117
+        return Section::make('General')
118
+            ->schema([
119
+                Select::make('language')
120
+                    ->softRequired()
121
+                    ->localizeLabel()
122
+                    ->options(LocalizationModel::getAllLanguages())
123
+                    ->searchable(),
124
+                Select::make('timezone')
125
+                    ->localizeLabel()
126
+                    ->options(Timezone::getTimezoneOptions(\App\Models\Setting\CompanyProfile::find(auth()->user()->currentCompany->id)->country))
127
+                    ->searchable()
128
+                    ->nullable(),
129
+            ])->columns();
130
+    }
131
+
132
+    protected function getDateAndTimeSection(): Component
133
+    {
134
+        return Section::make('Date & Time')
135
+            ->schema([
136
+                Select::make('date_format')
137
+                    ->softRequired()
138
+                    ->localizeLabel()
139
+                    ->options(DateFormat::class)
140
+                    ->live(),
141
+                Select::make('time_format')
142
+                    ->softRequired()
143
+                    ->localizeLabel()
144
+                    ->options(TimeFormat::class),
145
+                Select::make('week_start')
146
+                    ->softRequired()
147
+                    ->localizeLabel()
148
+                    ->options(WeekStart::class),
149
+            ])->columns();
150
+    }
151
+
152
+    protected function getFinancialAndFiscalSection(): Component
153
+    {
154
+        $beforeNumber = translate('Before Number');
155
+        $afterNumber = translate('After Number');
156
+        $selectPosition = translate('Select Position');
157
+
158
+        return Section::make('Financial & Fiscal')
159
+            ->schema([
160
+                DatePicker::make('fiscal_year_start')
161
+                    ->localizeLabel()
162
+                    ->live()
163
+                    ->extraAttributes(['wire:key' => Str::random()]) // Required to reinitialize the datepicker when the date_format state changes
164
+                    ->maxDate(static fn (Get $get) => $get('fiscal_year_end'))
165
+                    ->displayFormat(static function (LocalizationModel $record, Get $get) {
166
+                        return $get('date_format') ?? DateFormat::DEFAULT;
167
+                    })
168
+                    ->seconds(false)
169
+                    ->softRequired(),
170
+                DatePicker::make('fiscal_year_end')
171
+                    ->softRequired()
172
+                    ->localizeLabel()
173
+                    ->live()
174
+                    ->extraAttributes(['wire:key' => Str::random()]) // Required to reinitialize the datepicker when the date_format state changes
175
+                    ->minDate(static fn (Get $get) => $get('fiscal_year_start'))
176
+                    ->disabled(static fn (Get $get): bool => ! filled($get('fiscal_year_start')))
177
+                    ->displayFormat(static function (LocalizationModel $record, Get $get) {
178
+                        return $get('date_format') ?? DateFormat::DEFAULT;
179
+                    })
180
+                    ->seconds(false),
181
+                Select::make('number_format')
182
+                    ->softRequired()
183
+                    ->localizeLabel()
184
+                    ->options(NumberFormat::class),
185
+                Select::make('percent_first')
186
+                    ->softRequired()
187
+                    ->localizeLabel('Percent Position')
188
+                    ->boolean($beforeNumber, $afterNumber, $selectPosition),
189
+            ])->columns();
190
+    }
191
+
192
+    protected function handleRecordUpdate(LocalizationModel $record, array $data): LocalizationModel
193
+    {
194
+        $record->fill($data);
195
+
196
+        $keysToWatch = [
197
+            'language',
198
+            'timezone',
199
+            'date_format',
200
+            'week_start',
201
+            'time_format',
202
+        ];
203
+
204
+        if ($record->isDirty($keysToWatch)) {
205
+            $this->dispatch('localizationUpdated');
206
+        }
207
+
208
+        $record->save();
209
+
210
+        return $record;
211
+    }
212
+
213
+    /**
214
+     * @return array<Action | ActionGroup>
215
+     */
216
+    protected function getFormActions(): array
217
+    {
218
+        return [
219
+            $this->getSaveFormAction(),
220
+        ];
221
+    }
222
+
223
+    protected function getSaveFormAction(): Action
224
+    {
225
+        return Action::make('save')
226
+            ->label(__('filament-panels::resources/pages/edit-record.form.actions.save.label'))
227
+            ->submit('save')
228
+            ->keyBindings(['mod+s']);
229
+    }
230
+
231
+    public static function canView(Model $record): bool
232
+    {
233
+        try {
234
+            return authorize('update', $record)->allowed();
235
+        } catch (AuthorizationException $exception) {
236
+            return $exception->toResponse()->allowed();
237
+        }
238
+    }
239
+}

+ 87
- 103
app/Filament/Company/Resources/Banking/AccountResource.php Переглянути файл

@@ -3,17 +3,21 @@
3 3
 namespace App\Filament\Company\Resources\Banking;
4 4
 
5 5
 use App\Actions\OptionAction\CreateCurrency;
6
+use App\Enums\AccountType;
7
+use App\Facades\Forex;
6 8
 use App\Filament\Company\Resources\Banking\AccountResource\Pages;
7 9
 use App\Models\Banking\Account;
8
-use App\Models\Setting\Currency;
9
-use App\Services\CurrencyService;
10
-use App\Utilities\CurrencyConverter;
10
+use App\Utilities\Currency\CurrencyAccessor;
11
+use App\Utilities\Currency\CurrencyConverter;
12
+use Filament\Forms;
11 13
 use Filament\Forms\Form;
12 14
 use Filament\Notifications\Notification;
13 15
 use Filament\Resources\Resource;
16
+use Filament\Support\Enums\FontWeight;
17
+use Filament\Tables;
14 18
 use Filament\Tables\Table;
15
-use Filament\{Forms, Tables};
16
-use Illuminate\Support\Facades\{Auth, DB};
19
+use Illuminate\Support\Facades\Auth;
20
+use Illuminate\Support\Facades\DB;
17 21
 use Illuminate\Validation\Rules\Unique;
18 22
 use Wallo\FilamentSelectify\Components\ToggleButton;
19 23
 
@@ -21,10 +25,19 @@ class AccountResource extends Resource
21 25
 {
22 26
     protected static ?string $model = Account::class;
23 27
 
28
+    protected static ?string $modelLabel = 'Account';
29
+
24 30
     protected static ?string $navigationIcon = 'heroicon-o-credit-card';
25 31
 
26 32
     protected static ?string $navigationGroup = 'Banking';
27 33
 
34
+    public static function getModelLabel(): string
35
+    {
36
+        $modelLabel = static::$modelLabel;
37
+
38
+        return translate($modelLabel);
39
+    }
40
+
28 41
     public static function form(Form $form): Form
29 42
     {
30 43
         return $form
@@ -34,18 +47,18 @@ class AccountResource extends Resource
34 47
                         Forms\Components\Section::make('Account Information')
35 48
                             ->schema([
36 49
                                 Forms\Components\Select::make('type')
37
-                                    ->label('Type')
38
-                                    ->options(Account::getAccountTypes())
50
+                                    ->options(AccountType::class)
51
+                                    ->localizeLabel()
39 52
                                     ->searchable()
40 53
                                     ->default('checking')
41 54
                                     ->live()
42 55
                                     ->required(),
43 56
                                 Forms\Components\TextInput::make('name')
44
-                                    ->label('Name')
45 57
                                     ->maxLength(100)
58
+                                    ->localizeLabel()
46 59
                                     ->required(),
47 60
                                 Forms\Components\TextInput::make('number')
48
-                                    ->label('Account Number')
61
+                                    ->localizeLabel('Account Number')
49 62
                                     ->unique(ignoreRecord: true, modifyRuleUsing: static function (Unique $rule, $state) {
50 63
                                         $companyId = Auth::user()->currentCompany->id;
51 64
 
@@ -55,18 +68,20 @@ class AccountResource extends Resource
55 68
                                     ->validationAttribute('account number')
56 69
                                     ->required(),
57 70
                                 ToggleButton::make('enabled')
58
-                                    ->label('Default Account')
59
-                                    ->hidden(static fn (Forms\Get $get) => $get('type') === 'credit_card')
60
-                                    ->offColor('danger')
61
-                                    ->onColor('primary'),
71
+                                    ->localizeLabel('Default')
72
+                                    ->onLabel(translate('Yes'))
73
+                                    ->offLabel(translate('No'))
74
+                                    ->hidden(static fn (Forms\Get $get) => $get('type') === 'credit_card'),
62 75
                             ])->columns(),
63 76
                         Forms\Components\Section::make('Currency & Balance')
64 77
                             ->schema([
65 78
                                 Forms\Components\Select::make('currency_code')
66
-                                    ->label('Currency')
79
+                                    ->localizeLabel('Currency')
67 80
                                     ->relationship('currency', 'name')
68
-                                    ->default(Currency::getDefaultCurrencyCode())
81
+                                    ->default(CurrencyAccessor::getDefaultCurrency())
69 82
                                     ->saveRelationshipsUsing(null)
83
+                                    ->disabledOn('edit')
84
+                                    ->dehydrated()
70 85
                                     ->preload()
71 86
                                     ->searchable()
72 87
                                     ->live()
@@ -80,9 +95,9 @@ class AccountResource extends Resource
80 95
                                     ->required()
81 96
                                     ->createOptionForm([
82 97
                                         Forms\Components\Select::make('currency.code')
83
-                                            ->label('Code')
98
+                                            ->localizeLabel()
84 99
                                             ->searchable()
85
-                                            ->options(Currency::getAvailableCurrencyCodes())
100
+                                            ->options(CurrencyAccessor::getAvailableCurrencies())
86 101
                                             ->live()
87 102
                                             ->afterStateUpdated(static function (callable $set, $state) {
88 103
                                                 if ($state === null) {
@@ -90,33 +105,30 @@ class AccountResource extends Resource
90 105
                                                 }
91 106
 
92 107
                                                 $currency_code = currency($state);
93
-                                                $currencyService = app(CurrencyService::class);
108
+                                                $defaultCurrencyCode = currency()->getCurrency();
109
+                                                $forexEnabled = Forex::isEnabled();
110
+                                                $exchangeRate = $forexEnabled ? Forex::getCachedExchangeRate($defaultCurrencyCode, $state) : null;
94 111
 
95
-                                                $defaultCurrencyCode = Currency::getDefaultCurrencyCode();
96
-                                                $rate = 1;
112
+                                                $set('currency.name', $currency_code->getName() ?? '');
97 113
 
98
-                                                if ($defaultCurrencyCode !== null) {
99
-                                                    $rate = $currencyService->getCachedExchangeRate($defaultCurrencyCode, $state);
114
+                                                if ($forexEnabled && $exchangeRate !== null) {
115
+                                                    $set('currency.rate', $exchangeRate);
100 116
                                                 }
101
-
102
-                                                $set('currency.name', $currency_code->getName() ?? '');
103
-                                                $set('currency.rate', $rate);
104 117
                                             })
105 118
                                             ->required(),
106 119
                                         Forms\Components\TextInput::make('currency.name')
107
-                                            ->label('Name')
120
+                                            ->localizeLabel()
108 121
                                             ->maxLength(100)
109 122
                                             ->required(),
110 123
                                         Forms\Components\TextInput::make('currency.rate')
111
-                                            ->label('Rate')
124
+                                            ->localizeLabel()
112 125
                                             ->numeric()
113 126
                                             ->required(),
114 127
                                     ])->createOptionAction(static function (Forms\Components\Actions\Action $action) {
115 128
                                         return $action
116 129
                                             ->label('Add Currency')
117
-                                            ->modalHeading('Add Currency')
118
-                                            ->modalSubmitActionLabel('Add')
119 130
                                             ->slideOver()
131
+                                            ->modalWidth('md')
120 132
                                             ->action(static function (array $data) {
121 133
                                                 return DB::transaction(static function () use ($data) {
122 134
                                                     $code = $data['currency']['code'];
@@ -128,9 +140,11 @@ class AccountResource extends Resource
128 140
                                             });
129 141
                                     }),
130 142
                                 Forms\Components\TextInput::make('opening_balance')
131
-                                    ->label('Opening Balance')
132 143
                                     ->required()
133
-                                    ->currency(static fn (Forms\Get $get) => $get('currency_code')),
144
+                                    ->localizeLabel()
145
+                                    ->disabledOn('edit')
146
+                                    ->dehydrated()
147
+                                    ->money(static fn (Forms\Get $get) => $get('currency_code')),
134 148
                             ])->columns(),
135 149
                         Forms\Components\Tabs::make('Account Specifications')
136 150
                             ->tabs([
@@ -138,24 +152,24 @@ class AccountResource extends Resource
138 152
                                     ->icon('heroicon-o-credit-card')
139 153
                                     ->schema([
140 154
                                         Forms\Components\TextInput::make('bank_name')
141
-                                            ->label('Bank Name')
155
+                                            ->localizeLabel()
142 156
                                             ->maxLength(100),
143 157
                                         Forms\Components\TextInput::make('bank_phone')
144
-                                            ->label('Bank Phone')
145 158
                                             ->tel()
159
+                                            ->localizeLabel()
146 160
                                             ->maxLength(20),
147 161
                                         Forms\Components\Textarea::make('bank_address')
148
-                                            ->label('Bank Address')
162
+                                            ->localizeLabel()
149 163
                                             ->columnSpanFull(),
150 164
                                     ])->columns(),
151 165
                                 Forms\Components\Tabs\Tab::make('Additional Information')
152 166
                                     ->icon('heroicon-o-information-circle')
153 167
                                     ->schema([
154 168
                                         Forms\Components\TextInput::make('description')
155
-                                            ->label('Description')
169
+                                            ->localizeLabel()
156 170
                                             ->maxLength(100),
157 171
                                         Forms\Components\SpatieTagsInput::make('tags')
158
-                                            ->label('Tags')
172
+                                            ->localizeLabel()
159 173
                                             ->placeholder('Enter tags...')
160 174
                                             ->type('statuses')
161 175
                                             ->suggestions([
@@ -164,7 +178,6 @@ class AccountResource extends Resource
164 178
                                                 'College Fund',
165 179
                                             ]),
166 180
                                         Forms\Components\MarkdownEditor::make('notes')
167
-                                            ->label('Notes')
168 181
                                             ->columnSpanFull(),
169 182
                                     ])->columns(),
170 183
                             ]),
@@ -175,21 +188,21 @@ class AccountResource extends Resource
175 188
                         Forms\Components\Section::make('Routing Information')
176 189
                             ->schema([
177 190
                                 Forms\Components\TextInput::make('aba_routing_number')
178
-                                    ->label('ABA Number')
191
+                                    ->localizeLabel('ABA Number')
179 192
                                     ->integer()
180 193
                                     ->length(9),
181 194
                                 Forms\Components\TextInput::make('ach_routing_number')
182
-                                    ->label('ACH Number')
195
+                                    ->localizeLabel('ACH Number')
183 196
                                     ->integer()
184 197
                                     ->length(9),
185 198
                             ]),
186 199
                         Forms\Components\Section::make('International Bank Information')
187 200
                             ->schema([
188 201
                                 Forms\Components\TextInput::make('bic_swift_code')
189
-                                    ->label('BIC/SWIFT Code')
202
+                                    ->localizeLabel('BIC/SWIFT Code')
190 203
                                     ->maxLength(11),
191 204
                                 Forms\Components\TextInput::make('iban')
192
-                                    ->label('IBAN')
205
+                                    ->localizeLabel('IBAN')
193 206
                                     ->maxLength(34),
194 207
                             ]),
195 208
                     ])->columnSpan(['lg' => 1]),
@@ -201,40 +214,26 @@ class AccountResource extends Resource
201 214
         return $table
202 215
             ->columns([
203 216
                 Tables\Columns\TextColumn::make('name')
204
-                    ->label('Account')
217
+                    ->localizeLabel('Account')
205 218
                     ->searchable()
206
-                    ->weight('semibold')
207
-                    ->icon(static fn (Account $record) => $record->enabled ? 'heroicon-o-lock-closed' : null)
208
-                    ->tooltip(static fn (Account $record) => $record->enabled ? 'Default Account' : null)
219
+                    ->weight(FontWeight::Medium)
220
+                    ->icon(static fn (Account $record) => $record->isEnabled() ? 'heroicon-o-lock-closed' : null)
221
+                    ->tooltip(static fn (Account $record) => $record->isEnabled() ? 'Default Account' : null)
209 222
                     ->iconPosition('after')
210 223
                     ->description(static fn (Account $record) => $record->number ?: 'N/A')
211 224
                     ->sortable(),
212 225
                 Tables\Columns\TextColumn::make('bank_name')
213
-                    ->label('Bank')
226
+                    ->localizeLabel('Bank')
214 227
                     ->placeholder('N/A')
215 228
                     ->description(static fn (Account $record) => $record->bank_phone ?: 'N/A')
216 229
                     ->searchable()
217 230
                     ->sortable(),
218 231
                 Tables\Columns\TextColumn::make('status')
219 232
                     ->badge()
220
-                    ->label('Status')
221
-                    ->colors([
222
-                        'primary' => 'open',
223
-                        'success' => 'active',
224
-                        'secondary' => 'dormant',
225
-                        'warning' => 'restricted',
226
-                        'danger' => 'closed',
227
-                    ])
228
-                    ->icons([
229
-                        'heroicon-o-currency-dollar' => 'open',
230
-                        'heroicon-o-clock' => 'active',
231
-                        'heroicon-o-status-offline' => 'dormant',
232
-                        'heroicon-o-exclamation' => 'restricted',
233
-                        'heroicon-o-x-circle' => 'closed',
234
-                    ])
233
+                    ->localizeLabel()
235 234
                     ->sortable(),
236
-                Tables\Columns\TextColumn::make('opening_balance')
237
-                    ->label('Current Balance')
235
+                Tables\Columns\TextColumn::make('balance')
236
+                    ->localizeLabel('Current Balance')
238 237
                     ->sortable()
239 238
                     ->currency(static fn (Account $record) => $record->currency_code, true),
240 239
             ])
@@ -244,68 +243,59 @@ class AccountResource extends Resource
244 243
             ->actions([
245 244
                 Tables\Actions\EditAction::make(),
246 245
                 Tables\Actions\Action::make('update_balance')
247
-                    ->hidden(static fn (Account $record) => $record->currency_code === Currency::getDefaultCurrencyCode())
246
+                    ->hidden(function (Account $record) {
247
+                        $usesDefaultCurrency = $record->currency->isEnabled();
248
+                        $forexDisabled = Forex::isDisabled();
249
+                        $sameExchangeRate = $record->currency->rate === $record->currency->live_rate;
250
+
251
+                        return $usesDefaultCurrency || $forexDisabled || $sameExchangeRate;
252
+                    })
248 253
                     ->label('Update Balance')
249 254
                     ->icon('heroicon-o-currency-dollar')
250 255
                     ->requiresConfirmation()
251 256
                     ->modalDescription('Are you sure you want to update the balance with the latest exchange rate?')
252 257
                     ->before(static function (Tables\Actions\Action $action, Account $record) {
253
-                        if ($record->currency_code !== Currency::getDefaultCurrencyCode()) {
254
-                            $currencyService = app(CurrencyService::class);
255
-                            $defaultCurrency = Currency::getDefaultCurrencyCode();
256
-                            $cachedExchangeRate = $currencyService->getCachedExchangeRate($defaultCurrency, $record->currency_code);
257
-                            $oldExchangeRate = $record->currency->rate;
258
-
259
-                            if ($cachedExchangeRate === null) {
258
+                        if ($record->currency->isDisabled()) {
259
+                            $defaultCurrency = CurrencyAccessor::getDefaultCurrency();
260
+                            $exchangeRate = Forex::getCachedExchangeRate($defaultCurrency, $record->currency_code);
261
+                            if ($exchangeRate === null) {
260 262
                                 Notification::make()
261 263
                                     ->warning()
262
-                                    ->title('Exchange Rate Unavailable')
264
+                                    ->title(__('Exchange Rate Unavailable'))
263 265
                                     ->body(__('The exchange rate for this account is currently unavailable. Please try again later.'))
264 266
                                     ->persistent()
265 267
                                     ->send();
266 268
 
267 269
                                 $action->cancel();
268 270
                             }
269
-
270
-                            if ($cachedExchangeRate === $oldExchangeRate) {
271
-                                Notification::make()
272
-                                    ->warning()
273
-                                    ->title('Balance Already Up to Date')
274
-                                    ->body(__('The :name account balance is already up to date.', ['name' => $record->name]))
275
-                                    ->persistent()
276
-                                    ->send();
277
-
278
-                                $action->cancel();
279
-                            }
280 271
                         }
281 272
                     })
282 273
                     ->action(static function (Account $record) {
283
-                        if ($record->currency_code !== Currency::getDefaultCurrencyCode()) {
284
-                            $currencyService = app(CurrencyService::class);
285
-                            $defaultCurrency = Currency::getDefaultCurrencyCode();
286
-                            $cachedExchangeRate = $currencyService->getCachedExchangeRate($defaultCurrency, $record->currency_code);
274
+                        if ($record->currency->isDisabled()) {
275
+                            $defaultCurrency = CurrencyAccessor::getDefaultCurrency();
276
+                            $exchangeRate = Forex::getCachedExchangeRate($defaultCurrency, $record->currency_code);
287 277
                             $oldExchangeRate = $record->currency->rate;
288 278
 
289
-                            if ($cachedExchangeRate !== $oldExchangeRate) {
279
+                            if ($exchangeRate !== null && $exchangeRate !== $oldExchangeRate) {
290 280
 
291 281
                                 $scale = 10 ** $record->currency->precision;
292 282
                                 $cleanedBalance = (int) filter_var($record->opening_balance, FILTER_SANITIZE_NUMBER_INT);
293 283
 
294
-                                $newBalance = ($cachedExchangeRate / $oldExchangeRate) * $cleanedBalance;
284
+                                $newBalance = ($exchangeRate / $oldExchangeRate) * $cleanedBalance;
295 285
                                 $newBalanceInt = (int) round($newBalance, $scale);
296 286
 
297 287
                                 $record->opening_balance = money($newBalanceInt, $record->currency_code)->getValue();
298
-                                $record->currency->rate = $cachedExchangeRate;
288
+                                $record->currency->rate = $exchangeRate;
299 289
 
300 290
                                 $record->currency->save();
301 291
                                 $record->save();
302
-                            }
303 292
 
304
-                            Notification::make()
305
-                                ->success()
306
-                                ->title('Balance Updated Successfully')
307
-                                ->body(__('The :name account balance has been updated to reflect the current exchange rate.', ['name' => $record->name]))
308
-                                ->send();
293
+                                Notification::make()
294
+                                    ->success()
295
+                                    ->title('Balance Updated Successfully')
296
+                                    ->body(__('The :name account balance has been updated to reflect the current exchange rate.', ['name' => $record->name]))
297
+                                    ->send();
298
+                            }
309 299
                         }
310 300
                     }),
311 301
             ])
@@ -314,18 +304,12 @@ class AccountResource extends Resource
314 304
                     Tables\Actions\DeleteBulkAction::make(),
315 305
                 ]),
316 306
             ])
307
+            ->checkIfRecordIsSelectableUsing(static fn (Account $record) => $record->isDisabled())
317 308
             ->emptyStateActions([
318 309
                 Tables\Actions\CreateAction::make(),
319 310
             ]);
320 311
     }
321 312
 
322
-    public static function getRelations(): array
323
-    {
324
-        return [
325
-            //
326
-        ];
327
-    }
328
-
329 313
     public static function getPages(): array
330 314
     {
331 315
         return [

+ 0
- 12
app/Filament/Company/Resources/Banking/AccountResource/Pages/EditAccount.php Переглянути файл

@@ -4,7 +4,6 @@ namespace App\Filament\Company\Resources\Banking\AccountResource\Pages;
4 4
 
5 5
 use App\Filament\Company\Resources\Banking\AccountResource;
6 6
 use App\Models\Banking\Account;
7
-use App\Models\Setting\Currency;
8 7
 use App\Traits\HandlesResourceRecordUpdate;
9 8
 use Filament\Actions;
10 9
 use Filament\Resources\Pages\EditRecord;
@@ -48,17 +47,6 @@ class EditAccount extends EditRecord
48 47
             throw new Halt('No authenticated user found.');
49 48
         }
50 49
 
51
-        $oldCurrency = $record->currency_code;
52
-        $newCurrency = $data['currency_code'];
53
-
54
-        if ($oldCurrency !== $newCurrency) {
55
-            $data['opening_balance'] = Currency::convertBalance(
56
-                $data['opening_balance'],
57
-                $oldCurrency,
58
-                $newCurrency
59
-            );
60
-        }
61
-
62 50
         return $this->handleRecordUpdateWithUniqueField($record, $data, $user);
63 51
     }
64 52
 }

+ 37
- 9
app/Filament/Company/Resources/Core/DepartmentResource.php Переглянути файл

@@ -3,22 +3,34 @@
3 3
 namespace App\Filament\Company\Resources\Core;
4 4
 
5 5
 use App\Filament\Company\Resources\Core\DepartmentResource\Pages;
6
+use App\Filament\Company\Resources\Core\DepartmentResource\RelationManagers\ChildrenRelationManager;
6 7
 use App\Models\Core\Department;
8
+use Filament\Forms;
7 9
 use Filament\Forms\Form;
8 10
 use Filament\Resources\Resource;
11
+use Filament\Tables;
9 12
 use Filament\Tables\Table;
10
-use Filament\{Forms, Tables};
13
+use Illuminate\Database\Eloquent\Builder;
11 14
 
12 15
 class DepartmentResource extends Resource
13 16
 {
14 17
     protected static ?string $model = Department::class;
15 18
 
19
+    protected static ?string $modelLabel = 'Department';
20
+
16 21
     protected static ?string $navigationIcon = 'heroicon-o-square-3-stack-3d';
17 22
 
18 23
     protected static ?string $navigationGroup = 'HR';
19 24
 
20 25
     protected static ?string $slug = 'hr/departments';
21 26
 
27
+    public static function getModelLabel(): string
28
+    {
29
+        $modelLabel = static::$modelLabel;
30
+
31
+        return translate($modelLabel);
32
+    }
33
+
22 34
     public static function form(Form $form): Form
23 35
     {
24 36
         return $form
@@ -28,26 +40,35 @@ class DepartmentResource extends Resource
28 40
                         Forms\Components\TextInput::make('name')
29 41
                             ->autofocus()
30 42
                             ->required()
43
+                            ->localizeLabel()
31 44
                             ->maxLength(100),
32 45
                         Forms\Components\Select::make('manager_id')
33
-                            ->label('Manager')
34
-                            ->relationship('manager', 'name')
46
+                            ->relationship(
47
+                                name: 'manager',
48
+                                titleAttribute: 'name',
49
+                                modifyQueryUsing: static function (Builder $query) {
50
+                                    $company = auth()->user()->currentCompany;
51
+                                    $companyUsers = $company->allUsers()->pluck('id')->toArray();
52
+
53
+                                    return $query->whereIn('id', $companyUsers);
54
+                                }
55
+                            )
56
+                            ->localizeLabel()
35 57
                             ->searchable()
36 58
                             ->preload()
37
-                            ->placeholder('Select a manager')
38 59
                             ->nullable(),
39 60
                         Forms\Components\Group::make()
40 61
                             ->schema([
41 62
                                 Forms\Components\Select::make('parent_id')
42
-                                    ->label('Parent Department')
63
+                                    ->localizeLabel('Parent Department')
43 64
                                     ->relationship('parent', 'name')
44 65
                                     ->preload()
45 66
                                     ->searchable()
46 67
                                     ->nullable(),
47 68
                                 Forms\Components\Textarea::make('description')
48
-                                    ->label('Description')
49 69
                                     ->autosize()
50
-                                    ->nullable(),
70
+                                    ->nullable()
71
+                                    ->localizeLabel(),
51 72
                             ])->columns(1),
52 73
                     ])->columns(),
53 74
             ]);
@@ -58,11 +79,18 @@ class DepartmentResource extends Resource
58 79
         return $table
59 80
             ->columns([
60 81
                 Tables\Columns\TextColumn::make('name')
82
+                    ->localizeLabel()
61 83
                     ->weight('semibold')
62 84
                     ->searchable()
63 85
                     ->sortable(),
64 86
                 Tables\Columns\TextColumn::make('manager.name')
65
-                    ->label('Manager')
87
+                    ->localizeLabel()
88
+                    ->searchable()
89
+                    ->sortable(),
90
+                Tables\Columns\TextColumn::make('children_count')
91
+                    ->localizeLabel('Children')
92
+                    ->badge()
93
+                    ->counts('children')
66 94
                     ->searchable()
67 95
                     ->sortable(),
68 96
             ])
@@ -82,7 +110,7 @@ class DepartmentResource extends Resource
82 110
     public static function getRelations(): array
83 111
     {
84 112
         return [
85
-            //
113
+            ChildrenRelationManager::class,
86 114
         ];
87 115
     }
88 116
 

+ 25
- 0
app/Filament/Company/Resources/Core/DepartmentResource/Pages/ListDepartments.php Переглянути файл

@@ -3,7 +3,9 @@
3 3
 namespace App\Filament\Company\Resources\Core\DepartmentResource\Pages;
4 4
 
5 5
 use App\Filament\Company\Resources\Core\DepartmentResource;
6
+use App\Models\Core\Department;
6 7
 use Filament\Actions;
8
+use Filament\Resources\Components\Tab;
7 9
 use Filament\Resources\Pages\ListRecords;
8 10
 
9 11
 class ListDepartments extends ListRecords
@@ -16,4 +18,27 @@ class ListDepartments extends ListRecords
16 18
             Actions\CreateAction::make(),
17 19
         ];
18 20
     }
21
+
22
+    public function getTabs(): array
23
+    {
24
+        return [
25
+            'all' => Tab::make('All')
26
+                ->badge(Department::query()->count()),
27
+            'main' => Tab::make('Main')
28
+                ->badge(Department::query()->whereParentId(null)->count())
29
+                ->modifyQueryUsing(static function ($query) {
30
+                    $query->whereParentId(null);
31
+                }),
32
+            'children' => Tab::make('Children')
33
+                ->badge(Department::query()->whereNotNull('parent_id')->count())
34
+                ->modifyQueryUsing(static function ($query) {
35
+                    $query->whereNotNull('parent_id');
36
+                }),
37
+        ];
38
+    }
39
+
40
+    public function getDefaultActiveTab(): string | int | null
41
+    {
42
+        return 'all';
43
+    }
19 44
 }

+ 90
- 0
app/Filament/Company/Resources/Core/DepartmentResource/RelationManagers/ChildrenRelationManager.php Переглянути файл

@@ -0,0 +1,90 @@
1
+<?php
2
+
3
+namespace App\Filament\Company\Resources\Core\DepartmentResource\RelationManagers;
4
+
5
+use Filament\Forms;
6
+use Filament\Forms\Form;
7
+use Filament\Resources\RelationManagers\RelationManager;
8
+use Filament\Tables;
9
+use Filament\Tables\Table;
10
+use Illuminate\Database\Eloquent\Builder;
11
+
12
+class ChildrenRelationManager extends RelationManager
13
+{
14
+    protected static string $relationship = 'children';
15
+
16
+    protected static ?string $recordTitleAttribute = 'name';
17
+
18
+    public function form(Form $form): Form
19
+    {
20
+        return $form
21
+            ->columns(1)
22
+            ->schema([
23
+                Forms\Components\TextInput::make('name')
24
+                    ->localizeLabel()
25
+                    ->autofocus()
26
+                    ->required()
27
+                    ->maxLength(100),
28
+                Forms\Components\Select::make('manager_id')
29
+                    ->localizeLabel()
30
+                    ->relationship(
31
+                        name: 'manager',
32
+                        titleAttribute: 'name',
33
+                        modifyQueryUsing: static function (Builder $query) {
34
+                            $company = auth()->user()->currentCompany;
35
+                            $companyUsers = $company->allUsers()->pluck('id')->toArray();
36
+
37
+                            return $query->whereIn('id', $companyUsers);
38
+                        }
39
+                    )
40
+                    ->searchable()
41
+                    ->preload()
42
+                    ->nullable(),
43
+                Forms\Components\Textarea::make('description')
44
+                    ->localizeLabel()
45
+                    ->autosize()
46
+                    ->nullable(),
47
+            ]);
48
+    }
49
+
50
+    public function table(Table $table): Table
51
+    {
52
+        return $table
53
+            ->modelLabel(translate('Department'))
54
+            ->inverseRelationship('parent')
55
+            ->columns([
56
+                Tables\Columns\TextColumn::make('name')
57
+                    ->localizeLabel()
58
+                    ->weight('semibold')
59
+                    ->searchable()
60
+                    ->sortable(),
61
+                Tables\Columns\TextColumn::make('manager.name')
62
+                    ->localizeLabel()
63
+                    ->searchable()
64
+                    ->sortable(),
65
+            ])
66
+            ->filters([
67
+                //
68
+            ])
69
+            ->headerActions([
70
+                Tables\Actions\CreateAction::make(),
71
+                Tables\Actions\AssociateAction::make()
72
+                    ->preloadRecordSelect()
73
+                    ->recordSelectOptionsQuery(function (Builder $query) {
74
+                        $existingChildren = $this->getRelationship()->pluck('id')->toArray();
75
+
76
+                        return $query->whereNotIn('id', $existingChildren)
77
+                            ->whereNotNull('parent_id');
78
+                    }),
79
+            ])
80
+            ->actions([
81
+                Tables\Actions\EditAction::make(),
82
+                Tables\Actions\DeleteAction::make(),
83
+            ])
84
+            ->bulkActions([
85
+                Tables\Actions\BulkActionGroup::make([
86
+                    Tables\Actions\DeleteBulkAction::make(),
87
+                ]),
88
+            ]);
89
+    }
90
+}

+ 49
- 59
app/Filament/Company/Resources/Setting/CategoryResource.php Переглянути файл

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

+ 4
- 1
app/Filament/Company/Resources/Setting/CategoryResource/Pages/CreateCategory.php Переглянути файл

@@ -2,6 +2,7 @@
2 2
 
3 3
 namespace App\Filament\Company\Resources\Setting\CategoryResource\Pages;
4 4
 
5
+use App\Enums\CategoryType;
5 6
 use App\Filament\Company\Resources\Setting\CategoryResource;
6 7
 use App\Models\Setting\Category;
7 8
 use App\Traits\HandlesResourceRecordCreation;
@@ -38,6 +39,8 @@ class CreateCategory extends CreateRecord
38 39
             throw new Halt('No authenticated user found');
39 40
         }
40 41
 
41
-        return $this->handleRecordCreationWithUniqueField($data, new Category(), $user, 'type');
42
+        $evaluatedTypes = [CategoryType::Income, CategoryType::Expense];
43
+
44
+        return $this->handleRecordCreationWithUniqueField($data, new Category(), $user, 'type', $evaluatedTypes);
42 45
     }
43 46
 }

+ 4
- 1
app/Filament/Company/Resources/Setting/CategoryResource/Pages/EditCategory.php Переглянути файл

@@ -2,6 +2,7 @@
2 2
 
3 3
 namespace App\Filament\Company\Resources\Setting\CategoryResource\Pages;
4 4
 
5
+use App\Enums\CategoryType;
5 6
 use App\Filament\Company\Resources\Setting\CategoryResource;
6 7
 use App\Traits\HandlesResourceRecordUpdate;
7 8
 use Filament\Actions;
@@ -45,6 +46,8 @@ class EditCategory extends EditRecord
45 46
             throw new Halt('No authenticated user found');
46 47
         }
47 48
 
48
-        return $this->handleRecordUpdateWithUniqueField($record, $data, $user, 'type');
49
+        $evaluatedTypes = [CategoryType::Income, CategoryType::Expense];
50
+
51
+        return $this->handleRecordUpdateWithUniqueField($record, $data, $user, 'type', $evaluatedTypes);
49 52
     }
50 53
 }

+ 90
- 102
app/Filament/Company/Resources/Setting/CurrencyResource.php Переглянути файл

@@ -2,25 +2,32 @@
2 2
 
3 3
 namespace App\Filament\Company\Resources\Setting;
4 4
 
5
+use App\Facades\Forex;
5 6
 use App\Filament\Company\Resources\Setting\CurrencyResource\Pages;
6 7
 use App\Models\Banking\Account;
7 8
 use App\Models\Setting\Currency;
8
-use App\Services\CurrencyService;
9
+use App\Models\Setting\Currency as CurrencyModel;
9 10
 use App\Traits\ChecksForeignKeyConstraints;
11
+use App\Traits\NotifiesOnDelete;
12
+use App\Utilities\Currency\CurrencyAccessor;
10 13
 use Closure;
14
+use Filament\Forms;
11 15
 use Filament\Forms\Form;
12
-use Filament\Notifications\Notification;
13 16
 use Filament\Resources\Resource;
17
+use Filament\Support\Enums\FontWeight;
18
+use Filament\Tables;
14 19
 use Filament\Tables\Table;
15
-use Filament\{Forms, Tables};
16 20
 use Illuminate\Database\Eloquent\Collection;
17 21
 use Wallo\FilamentSelectify\Components\ToggleButton;
18 22
 
19 23
 class CurrencyResource extends Resource
20 24
 {
21 25
     use ChecksForeignKeyConstraints;
26
+    use NotifiesOnDelete;
22 27
 
23
-    protected static ?string $model = Currency::class;
28
+    protected static ?string $model = CurrencyModel::class;
29
+
30
+    protected static ?string $modelLabel = 'Currency';
24 31
 
25 32
     protected static ?string $navigationIcon = 'heroicon-o-currency-dollar';
26 33
 
@@ -28,6 +35,13 @@ class CurrencyResource extends Resource
28 35
 
29 36
     protected static ?string $slug = 'settings/currencies';
30 37
 
38
+    public static function getModelLabel(): string
39
+    {
40
+        $modelLabel = static::$modelLabel;
41
+
42
+        return translate($modelLabel);
43
+    }
44
+
31 45
     public static function form(Form $form): Form
32 46
     {
33 47
         return $form
@@ -35,109 +49,111 @@ class CurrencyResource extends Resource
35 49
                 Forms\Components\Section::make('General')
36 50
                     ->schema([
37 51
                         Forms\Components\Select::make('code')
38
-                            ->label('Code')
39
-                            ->options(Currency::getAvailableCurrencyCodes())
52
+                            ->options(CurrencyAccessor::getAvailableCurrencies())
40 53
                             ->searchable()
41
-                            ->placeholder('Select a currency code...')
42 54
                             ->live()
43 55
                             ->required()
56
+                            ->localizeLabel()
44 57
                             ->hidden(static fn (Forms\Get $get, $state): bool => $get('enabled') && $state !== null)
45 58
                             ->afterStateUpdated(static function (Forms\Set $set, $state) {
46
-                                $fields = ['name', 'rate', 'precision', 'symbol', 'symbol_first', 'decimal_mark', 'thousands_separator'];
59
+                                $fields = ['name', 'precision', 'symbol', 'symbol_first', 'decimal_mark', 'thousands_separator'];
47 60
 
48 61
                                 if ($state === null) {
49
-                                    foreach ($fields as $field) {
50
-                                        $set($field, null);
51
-                                    }
62
+                                    array_walk($fields, static fn ($field) => $set($field, null));
63
+
52 64
                                     return;
53 65
                                 }
54 66
 
55
-                                $defaultCurrencyCode = Currency::getDefaultCurrencyCode();
56
-                                $currencyService = app(CurrencyService::class);
57
-
58
-                                $code = $state;
59
-                                $allCurrencies = Currency::getAllCurrencies();
60
-                                $selectedCurrencyCode = $allCurrencies[$code] ?? [];
61
-
62
-                                $rate = $defaultCurrencyCode ? $currencyService->getCachedExchangeRate($defaultCurrencyCode, $code) : 1;
67
+                                $currencyDetails = CurrencyAccessor::getAllCurrencies()[$state] ?? [];
68
+                                $defaultCurrencyCode = CurrencyAccessor::getDefaultCurrency();
69
+                                $exchangeRate = Forex::getCachedExchangeRate($defaultCurrencyCode, $state);
63 70
 
64
-                                foreach ($fields as $field) {
65
-                                    $set($field, $selectedCurrencyCode[$field] ?? ($field === 'rate' ? $rate : ''));
71
+                                if ($exchangeRate !== null) {
72
+                                    $set('rate', $exchangeRate);
66 73
                                 }
74
+
75
+                                array_walk($fields, static fn ($field) => $set($field, $currencyDetails[$field] ?? null));
67 76
                             }),
68 77
                         Forms\Components\TextInput::make('code')
69
-                            ->label('Code')
78
+                            ->localizeLabel()
70 79
                             ->hidden(static fn (Forms\Get $get): bool => ! ($get('enabled') && $get('code') !== null))
71 80
                             ->disabled(static fn (Forms\Get $get): bool => $get('enabled'))
72 81
                             ->dehydrated()
73 82
                             ->required(),
74 83
                         Forms\Components\TextInput::make('name')
75
-                            ->label('Name')
84
+                            ->localizeLabel()
76 85
                             ->maxLength(50)
77 86
                             ->required(),
78 87
                         Forms\Components\TextInput::make('rate')
79
-                            ->label('Rate')
80 88
                             ->numeric()
81 89
                             ->rule('gt:0')
82 90
                             ->live()
91
+                            ->localizeLabel()
92
+                            ->disabled(static fn (?CurrencyModel $record): bool => $record?->isEnabled() ?? false)
93
+                            ->dehydrated()
83 94
                             ->required(),
84 95
                         Forms\Components\Select::make('precision')
85
-                            ->label('Precision')
86
-                            ->native(false)
87
-                            ->selectablePlaceholder(false)
88
-                            ->placeholder('Select a precision...')
96
+                            ->localizeLabel()
89 97
                             ->options(['0', '1', '2', '3', '4'])
90 98
                             ->required(),
91 99
                         Forms\Components\TextInput::make('symbol')
92
-                            ->label('Symbol')
100
+                            ->localizeLabel()
93 101
                             ->maxLength(5)
94 102
                             ->required(),
95 103
                         Forms\Components\Select::make('symbol_first')
96
-                            ->label('Symbol Position')
97
-                            ->native(false)
98
-                            ->selectablePlaceholder(false)
99
-                            ->formatStateUsing(static fn ($state) => isset($state) ? (int) $state : null)
100
-                            ->boolean('Before Amount', 'After Amount', 'Select a symbol position...')
104
+                            ->localizeLabel('Symbol Position')
105
+                            ->boolean(translate('Before Amount'), translate('After Amount'), translate('Select a symbol position'))
101 106
                             ->required(),
102 107
                         Forms\Components\TextInput::make('decimal_mark')
103
-                            ->label('Decimal Separator')
108
+                            ->localizeLabel('Decimal Separator')
104 109
                             ->maxLength(1)
110
+                            ->rule(static function (Forms\Get $get): Closure {
111
+                                return static function ($attribute, $value, Closure $fail) use ($get) {
112
+                                    if ($value === $get('thousands_separator')) {
113
+                                        $fail(translate('Separators must be unique.'));
114
+                                    }
115
+                                };
116
+                            })
105 117
                             ->required(),
106 118
                         Forms\Components\TextInput::make('thousands_separator')
107
-                            ->label('Thousands Separator')
119
+                            ->localizeLabel()
108 120
                             ->maxLength(1)
109 121
                             ->rule(static function (Forms\Get $get): Closure {
110 122
                                 return static function ($attribute, $value, Closure $fail) use ($get) {
111
-                                    $decimalMark = $get('decimal_mark');
112
-
113
-                                    if ($value === $decimalMark) {
114
-                                        $fail('The thousands separator and decimal separator must be different.');
123
+                                    if ($value === $get('decimal_mark')) {
124
+                                        $fail(translate('Separators must be unique.'));
115 125
                                     }
116 126
                                 };
117 127
                             })
118 128
                             ->nullable(),
119 129
                         ToggleButton::make('enabled')
120
-                            ->label('Default Currency')
130
+                            ->localizeLabel('Default')
131
+                            ->onLabel(CurrencyModel::enabledLabel())
132
+                            ->offLabel(CurrencyModel::disabledLabel())
133
+                            ->disabled(static fn (?CurrencyModel $record): bool => $record?->isEnabled() ?? false)
134
+                            ->dehydrated()
121 135
                             ->live()
122
-                            ->offColor('danger')
123
-                            ->onColor('primary')
124 136
                             ->afterStateUpdated(static function (Forms\Set $set, Forms\Get $get, $state) {
125 137
                                 $enabledState = (bool) $state;
126 138
                                 $code = $get('code');
127 139
 
128
-                                $defaultCurrencyCode = Currency::getDefaultCurrencyCode();
129
-                                $currencyService = app(CurrencyService::class);
140
+                                if (! $code) {
141
+                                    return;
142
+                                }
130 143
 
131 144
                                 if ($enabledState) {
132 145
                                     $set('rate', 1);
133
-                                } else {
134
-                                    if ($code === null) {
135
-                                        return;
136
-                                    }
137 146
 
138
-                                    $rate = $currencyService->getCachedExchangeRate($defaultCurrencyCode, $code);
147
+                                    return;
148
+                                }
139 149
 
140
-                                    $set('rate', $rate ?? '');
150
+                                $forexEnabled = Forex::isEnabled();
151
+                                if ($forexEnabled) {
152
+                                    $defaultCurrencyCode = CurrencyAccessor::getDefaultCurrency();
153
+                                    $exchangeRate = Forex::getCachedExchangeRate($defaultCurrencyCode, $code);
154
+                                    if ($exchangeRate !== null) {
155
+                                        $set('rate', $exchangeRate);
156
+                                    }
141 157
                                 }
142 158
                             }),
143 159
                     ])->columns(),
@@ -149,23 +165,29 @@ class CurrencyResource extends Resource
149 165
         return $table
150 166
             ->columns([
151 167
                 Tables\Columns\TextColumn::make('name')
152
-                    ->label('Name')
153
-                    ->weight('semibold')
154
-                    ->icon(static fn (Currency $record) => $record->enabled ? 'heroicon-o-lock-closed' : null)
155
-                    ->tooltip(static fn (Currency $record) => $record->enabled ? 'Default Currency' : null)
168
+                    ->localizeLabel()
169
+                    ->weight(FontWeight::Medium)
170
+                    ->icon(static fn (CurrencyModel $record) => $record->isEnabled() ? 'heroicon-o-lock-closed' : null)
171
+                    ->tooltip(static function (CurrencyModel $record) {
172
+                        $tooltipMessage = translate('Default :Record', [
173
+                            'Record' => static::getModelLabel(),
174
+                        ]);
175
+
176
+                        return $record->isEnabled() ? $tooltipMessage : null;
177
+                    })
156 178
                     ->iconPosition('after')
157 179
                     ->searchable()
158 180
                     ->sortable(),
159 181
                 Tables\Columns\TextColumn::make('code')
160
-                    ->label('Code')
182
+                    ->localizeLabel()
161 183
                     ->searchable()
162 184
                     ->sortable(),
163 185
                 Tables\Columns\TextColumn::make('symbol')
164
-                    ->label('Symbol')
186
+                    ->localizeLabel()
165 187
                     ->searchable()
166 188
                     ->sortable(),
167 189
                 Tables\Columns\TextColumn::make('rate')
168
-                    ->label('Rate')
190
+                    ->localizeLabel()
169 191
                     ->searchable()
170 192
                     ->sortable(),
171 193
             ])
@@ -175,31 +197,16 @@ class CurrencyResource extends Resource
175 197
             ->actions([
176 198
                 Tables\Actions\EditAction::make(),
177 199
                 Tables\Actions\DeleteAction::make()
178
-                    ->before(static function (Tables\Actions\DeleteAction $action, Currency $record) {
179
-                        $defaultCurrency = $record->enabled;
200
+                    ->before(function (Tables\Actions\DeleteAction $action, Currency $record) {
180 201
                         $modelsToCheck = [
181 202
                             Account::class,
182 203
                         ];
183 204
 
184 205
                         $isUsed = self::isForeignKeyUsed('currency_code', $record->code, $modelsToCheck);
185 206
 
186
-                        if ($defaultCurrency) {
187
-                            Notification::make()
188
-                                ->danger()
189
-                                ->title('Action Denied')
190
-                                ->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]))
191
-                                ->persistent()
192
-                                ->send();
193
-
194
-                            $action->cancel();
195
-                        } elseif ($isUsed) {
196
-                            Notification::make()
197
-                                ->danger()
198
-                                ->title('Action Denied')
199
-                                ->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]))
200
-                                ->persistent()
201
-                                ->send();
202
-
207
+                        if ($isUsed) {
208
+                            $reason = 'in use';
209
+                            self::notifyBeforeDelete($record, $reason);
203 210
                             $action->cancel();
204 211
                         }
205 212
                     }),
@@ -209,48 +216,29 @@ class CurrencyResource extends Resource
209 216
                     Tables\Actions\DeleteBulkAction::make()
210 217
                         ->before(static function (Tables\Actions\DeleteBulkAction $action, Collection $records) {
211 218
                             foreach ($records as $record) {
212
-                                $defaultCurrency = $record->enabled;
213 219
                                 $modelsToCheck = [
214 220
                                     Account::class,
215 221
                                 ];
216 222
 
217 223
                                 $isUsed = self::isForeignKeyUsed('currency_code', $record->code, $modelsToCheck);
218 224
 
219
-                                if ($defaultCurrency) {
220
-                                    Notification::make()
221
-                                        ->danger()
222
-                                        ->title('Action Denied')
223
-                                        ->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]))
224
-                                        ->persistent()
225
-                                        ->send();
226
-
227
-                                    $action->cancel();
228
-                                } elseif ($isUsed) {
229
-                                    Notification::make()
230
-                                        ->danger()
231
-                                        ->title('Action Denied')
232
-                                        ->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]))
233
-                                        ->persistent()
234
-                                        ->send();
235
-
225
+                                if ($isUsed) {
226
+                                    $reason = 'in use';
227
+                                    self::notifyBeforeDelete($record, $reason);
236 228
                                     $action->cancel();
237 229
                                 }
238 230
                             }
239 231
                         }),
240 232
                 ]),
241 233
             ])
234
+            ->checkIfRecordIsSelectableUsing(static function (CurrencyModel $record) {
235
+                return $record->isDisabled();
236
+            })
242 237
             ->emptyStateActions([
243 238
                 Tables\Actions\CreateAction::make(),
244 239
             ]);
245 240
     }
246 241
 
247
-    public static function getRelations(): array
248
-    {
249
-        return [
250
-            //
251
-        ];
252
-    }
253
-
254 242
     public static function getPages(): array
255 243
     {
256 244
         return [

+ 0
- 5
app/Filament/Company/Resources/Setting/CurrencyResource/Pages/EditCurrency.php Переглянути файл

@@ -2,7 +2,6 @@
2 2
 
3 3
 namespace App\Filament\Company\Resources\Setting\CurrencyResource\Pages;
4 4
 
5
-use App\Events\DefaultCurrencyChanged;
6 5
 use App\Filament\Company\Resources\Setting\CurrencyResource;
7 6
 use App\Models\Setting\Currency;
8 7
 use App\Traits\HandlesResourceRecordUpdate;
@@ -48,10 +47,6 @@ class EditCurrency extends EditRecord
48 47
             throw new Halt('No authenticated user found');
49 48
         }
50 49
 
51
-        if ($data['enabled'] && ! $record->enabled) {
52
-            event(new DefaultCurrencyChanged($record));
53
-        }
54
-
55 50
         return $this->handleRecordUpdateWithUniqueField($record, $data, $user);
56 51
     }
57 52
 }

+ 75
- 49
app/Filament/Company/Resources/Setting/DiscountResource.php Переглянути файл

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

+ 4
- 1
app/Filament/Company/Resources/Setting/DiscountResource/Pages/CreateDiscount.php Переглянути файл

@@ -2,6 +2,7 @@
2 2
 
3 3
 namespace App\Filament\Company\Resources\Setting\DiscountResource\Pages;
4 4
 
5
+use App\Enums\DiscountType;
5 6
 use App\Filament\Company\Resources\Setting\DiscountResource;
6 7
 use App\Models\Setting\Discount;
7 8
 use App\Traits\HandlesResourceRecordCreation;
@@ -38,6 +39,8 @@ class CreateDiscount extends CreateRecord
38 39
             throw new Halt('No authenticated user found');
39 40
         }
40 41
 
41
-        return $this->handleRecordCreationWithUniqueField($data, new Discount(), $user, 'type');
42
+        $evaluatedTypes = [DiscountType::Sales, DiscountType::Purchase];
43
+
44
+        return $this->handleRecordCreationWithUniqueField($data, new Discount(), $user, 'type', $evaluatedTypes);
42 45
     }
43 46
 }

+ 4
- 1
app/Filament/Company/Resources/Setting/DiscountResource/Pages/EditDiscount.php Переглянути файл

@@ -2,6 +2,7 @@
2 2
 
3 3
 namespace App\Filament\Company\Resources\Setting\DiscountResource\Pages;
4 4
 
5
+use App\Enums\DiscountType;
5 6
 use App\Filament\Company\Resources\Setting\DiscountResource;
6 7
 use App\Traits\HandlesResourceRecordUpdate;
7 8
 use Filament\Actions;
@@ -45,6 +46,8 @@ class EditDiscount extends EditRecord
45 46
             throw new Halt('No authenticated user found');
46 47
         }
47 48
 
48
-        return $this->handleRecordUpdateWithUniqueField($record, $data, $user, 'type');
49
+        $evaluatedTypes = [DiscountType::Sales, DiscountType::Purchase];
50
+
51
+        return $this->handleRecordUpdateWithUniqueField($record, $data, $user, 'type', $evaluatedTypes);
49 52
     }
50 53
 }

+ 58
- 84
app/Filament/Company/Resources/Setting/TaxResource.php Переглянути файл

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

+ 4
- 1
app/Filament/Company/Resources/Setting/TaxResource/Pages/CreateTax.php Переглянути файл

@@ -2,6 +2,7 @@
2 2
 
3 3
 namespace App\Filament\Company\Resources\Setting\TaxResource\Pages;
4 4
 
5
+use App\Enums\TaxType;
5 6
 use App\Filament\Company\Resources\Setting\TaxResource;
6 7
 use App\Models\Setting\Tax;
7 8
 use App\Traits\HandlesResourceRecordCreation;
@@ -38,6 +39,8 @@ class CreateTax extends CreateRecord
38 39
             throw new Halt('No authenticated user found');
39 40
         }
40 41
 
41
-        return $this->handleRecordCreationWithUniqueField($data, new Tax(), $user, 'type');
42
+        $evaluatedTypes = [TaxType::Sales, TaxType::Purchase];
43
+
44
+        return $this->handleRecordCreationWithUniqueField($data, new Tax(), $user, 'type', $evaluatedTypes);
42 45
     }
43 46
 }

+ 4
- 1
app/Filament/Company/Resources/Setting/TaxResource/Pages/EditTax.php Переглянути файл

@@ -2,6 +2,7 @@
2 2
 
3 3
 namespace App\Filament\Company\Resources\Setting\TaxResource\Pages;
4 4
 
5
+use App\Enums\TaxType;
5 6
 use App\Filament\Company\Resources\Setting\TaxResource;
6 7
 use App\Traits\HandlesResourceRecordUpdate;
7 8
 use Filament\Actions;
@@ -45,6 +46,8 @@ class EditTax extends EditRecord
45 46
             throw new Halt('No authenticated user found');
46 47
         }
47 48
 
48
-        return $this->handleRecordUpdateWithUniqueField($record, $data, $user, 'type');
49
+        $evaluatedTypes = [TaxType::Sales, TaxType::Purchase];
50
+
51
+        return $this->handleRecordUpdateWithUniqueField($record, $data, $user, 'type', $evaluatedTypes);
49 52
     }
50 53
 }

+ 130
- 0
app/Helpers/format.php Переглянути файл

@@ -0,0 +1,130 @@
1
+<?php
2
+
3
+use App\Enums\NumberFormat;
4
+use App\Models\Setting\Localization;
5
+use Filament\Support\RawJs;
6
+
7
+if (! function_exists('generateJsCode')) {
8
+    function generateJsCode(string $precision, ?string $currency = null): string
9
+    {
10
+        $decimal_mark = currency($currency)->getDecimalMark();
11
+        $thousands_separator = currency($currency)->getThousandsSeparator();
12
+
13
+        return "\$money(\$input, '" . $decimal_mark . "', '" . $thousands_separator . "', " . $precision . ');';
14
+    }
15
+}
16
+
17
+if (! function_exists('generatePercentJsCode')) {
18
+    function generatePercentJsCode(string $format, int $precision): string
19
+    {
20
+        [$decimal_mark, $thousands_separator] = NumberFormat::from($format)->getFormattingParameters();
21
+
22
+        return "\$money(\$input, '" . $decimal_mark . "', '" . $thousands_separator . "', " . $precision . ');';
23
+    }
24
+}
25
+
26
+if (! function_exists('moneyMask')) {
27
+    function moneyMask(?string $currency = null): RawJs
28
+    {
29
+        $precision = currency($currency)->getPrecision();
30
+
31
+        return RawJs::make(generateJsCode($precision, $currency));
32
+    }
33
+}
34
+
35
+if (! function_exists('percentMask')) {
36
+    function percentMask(int $precision = 4): RawJs
37
+    {
38
+        $format = Localization::firstOrFail()->number_format->value;
39
+
40
+        return RawJs::make(generatePercentJsCode($format, $precision));
41
+    }
42
+}
43
+
44
+if (! function_exists('ratePrefix')) {
45
+    function ratePrefix($computation, ?string $currency = null): ?string
46
+    {
47
+        if ($computation instanceof BackedEnum) {
48
+            $computation = $computation->value;
49
+        }
50
+
51
+        if ($computation === 'fixed') {
52
+            return currency($currency)->getCodePrefix();
53
+        }
54
+
55
+        if ($computation === 'percentage' || $computation === 'compound') {
56
+            $percent_first = Localization::firstOrFail()->percent_first;
57
+
58
+            return $percent_first ? '%' : null;
59
+        }
60
+
61
+        return null;
62
+    }
63
+}
64
+
65
+if (! function_exists('rateSuffix')) {
66
+    function rateSuffix($computation, ?string $currency = null): ?string
67
+    {
68
+        if ($computation instanceof BackedEnum) {
69
+            $computation = $computation->value;
70
+        }
71
+
72
+        if ($computation === 'percentage' || $computation === 'compound') {
73
+            $percent_first = Localization::firstOrFail()->percent_first;
74
+
75
+            return $percent_first ? null : '%';
76
+        }
77
+
78
+        if ($computation === 'fixed') {
79
+            return currency($currency)->getCodeSuffix();
80
+        }
81
+
82
+        return null;
83
+    }
84
+}
85
+
86
+if (! function_exists('rateMask')) {
87
+    function rateMask($computation, ?string $currency = null): RawJs
88
+    {
89
+        if ($computation instanceof BackedEnum) {
90
+            $computation = $computation->value;
91
+        }
92
+
93
+        if ($computation === 'percentage' || $computation === 'compound') {
94
+            return percentMask(4);
95
+        }
96
+
97
+        $precision = currency($currency)->getPrecision();
98
+
99
+        return RawJs::make(generateJsCode($precision, $currency));
100
+    }
101
+}
102
+
103
+if (! function_exists('rateFormat')) {
104
+    function rateFormat($state, $computation, ?string $currency = null): ?string
105
+    {
106
+        if (blank($state)) {
107
+            return null;
108
+        }
109
+
110
+        if ($computation instanceof BackedEnum) {
111
+            $computation = $computation->value;
112
+        }
113
+
114
+        if ($computation === 'percentage' || $computation === 'compound') {
115
+            $percent_first = Localization::firstOrFail()->percent_first;
116
+
117
+            if ($percent_first) {
118
+                return '%' . $state;
119
+            }
120
+
121
+            return $state . '%';
122
+        }
123
+
124
+        if ($computation === 'fixed') {
125
+            return money($state, $currency, true)->formatWithCode();
126
+        }
127
+
128
+        return null;
129
+    }
130
+}

+ 0
- 13
app/Http/Controllers/Controller.php Переглянути файл

@@ -1,13 +0,0 @@
1
-<?php
2
-
3
-namespace App\Http\Controllers;
4
-
5
-use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
6
-use Illuminate\Foundation\Validation\ValidatesRequests;
7
-use Illuminate\Routing\Controller as BaseController;
8
-
9
-class Controller extends BaseController
10
-{
11
-    use AuthorizesRequests;
12
-    use ValidatesRequests;
13
-}

app/Http/Middleware/ApplyCurrentCompanyScope.php → app/Http/Middleware/ConfigureCurrentCompany.php Переглянути файл

@@ -2,11 +2,14 @@
2 2
 
3 3
 namespace App\Http\Middleware;
4 4
 
5
+use App\Events\CompanyConfigured;
6
+use App\Models\Company;
5 7
 use Closure;
8
+use Filament\Facades\Filament;
6 9
 use Illuminate\Http\Request;
7 10
 use Symfony\Component\HttpFoundation\Response;
8 11
 
9
-class ApplyCurrentCompanyScope
12
+class ConfigureCurrentCompany
10 13
 {
11 14
     /**
12 15
      * Handle an incoming request.
@@ -15,6 +18,13 @@ class ApplyCurrentCompanyScope
15 18
      */
16 19
     public function handle(Request $request, Closure $next): Response
17 20
     {
21
+        /** @var Company $company */
22
+        $company = Filament::getTenant();
23
+
24
+        if ($company) {
25
+            CompanyConfigured::dispatch($company);
26
+        }
27
+
18 28
         return $next($request);
19 29
     }
20 30
 }

+ 60
- 10
app/Listeners/ConfigureCompanyDefault.php Переглянути файл

@@ -2,11 +2,23 @@
2 2
 
3 3
 namespace App\Listeners;
4 4
 
5
-use App\Enums\{Font, MaxContentWidth, ModalWidth, PrimaryColor, RecordsPerPage, TableSortDirection};
6
-use App\Models\Company;
5
+use App\Enums\DateFormat;
6
+use App\Enums\Font;
7
+use App\Enums\MaxContentWidth;
8
+use App\Enums\ModalWidth;
9
+use App\Enums\PrimaryColor;
10
+use App\Enums\RecordsPerPage;
11
+use App\Enums\TableSortDirection;
12
+use App\Enums\WeekStart;
13
+use App\Events\CompanyConfigured;
14
+use App\Utilities\Currency\ConfigureCurrencies;
7 15
 use Filament\Actions\MountableAction;
8
-use Filament\Events\TenantSet;
9 16
 use Filament\Facades\Filament;
17
+use Filament\Forms\Components\DatePicker;
18
+use Filament\Forms\Components\Section;
19
+use Filament\Forms\Components\Tabs\Tab;
20
+use Filament\Navigation\NavigationGroup;
21
+use Filament\Resources\Components\Tab as ResourcesTab;
10 22
 use Filament\Support\Facades\FilamentColor;
11 23
 use Filament\Tables\Table;
12 24
 
@@ -15,10 +27,9 @@ class ConfigureCompanyDefault
15 27
     /**
16 28
      * Handle the event.
17 29
      */
18
-    public function handle(TenantSet $event): void
30
+    public function handle(CompanyConfigured $event): void
19 31
     {
20
-        /** @var Company $company */
21
-        $company = $event->getTenant();
32
+        $company = $event->company;
22 33
         $paginationPageOptions = RecordsPerPage::caseValues();
23 34
         $defaultPaginationPageOption = $company->appearance->records_per_page->value ?? RecordsPerPage::DEFAULT;
24 35
         $defaultSort = $company->appearance->table_sort_direction->value ?? TableSortDirection::DEFAULT;
@@ -28,8 +39,18 @@ class ConfigureCompanyDefault
28 39
         $maxContentWidth = $company->appearance->max_content_width->value ?? MaxContentWidth::DEFAULT;
29 40
         $defaultFont = $company->appearance->font->value ?? Font::DEFAULT;
30 41
         $hasTopNavigation = $company->appearance->has_top_navigation ?? false;
42
+        $default_language = $company->locale->language ?? config('transmatic.source_locale');
43
+        $defaultTimezone = $company->locale->timezone ?? config('app.timezone');
44
+        $dateFormat = $company->locale->date_format->value ?? DateFormat::DEFAULT;
45
+        $weekStart = $company->locale->week_start->value ?? WeekStart::DEFAULT;
46
+
47
+        app()->setLocale($default_language);
48
+        locale_set_default($default_language);
49
+        config(['app.timezone' => $defaultTimezone]);
50
+        date_default_timezone_set($defaultTimezone);
31 51
 
32 52
         Table::configureUsing(static function (Table $table) use ($paginationPageOptions, $defaultSort, $stripedTables, $defaultPaginationPageOption): void {
53
+
33 54
             $table
34 55
                 ->paginationPageOptions($paginationPageOptions)
35 56
                 ->defaultSort(column: 'id', direction: $defaultSort)
@@ -38,20 +59,49 @@ class ConfigureCompanyDefault
38 59
         }, isImportant: true);
39 60
 
40 61
         MountableAction::configureUsing(static function (MountableAction $action) use ($modalWidth): void {
41
-            $action->modalWidth($modalWidth);
62
+            $actionOperation = $action->getName();
63
+
64
+            if (in_array($actionOperation, ['delete', 'restore', 'forceDelete', 'detach'])) {
65
+                $action->modalWidth($modalWidth);
66
+            }
42 67
         }, isImportant: true);
43 68
 
44
-        $defaultColor = FilamentColor::register([
69
+        FilamentColor::register([
45 70
             'primary' => $defaultPrimaryColor->getColor(),
46 71
         ]);
47 72
 
48
-        FilamentColor::swap($defaultColor);
49
-
50 73
         Filament::getPanel('company')
51 74
             ->font($defaultFont)
52 75
             ->brandName($company->name)
53 76
             ->topNavigation($hasTopNavigation)
54 77
             ->sidebarCollapsibleOnDesktop(! $hasTopNavigation)
55 78
             ->maxContentWidth($maxContentWidth);
79
+
80
+        DatePicker::configureUsing(static function (DatePicker $component) use ($dateFormat, $weekStart) {
81
+            $component
82
+                ->displayFormat($dateFormat)
83
+                ->firstDayOfWeek($weekStart);
84
+        });
85
+
86
+        Tab::configureUsing(static function (Tab $tab) {
87
+            $label = $tab->getLabel();
88
+
89
+            $tab->label(ucwords(translate($label)));
90
+        }, isImportant: true);
91
+
92
+        Section::configureUsing(static function (Section $section): void {
93
+            $heading = $section->getHeading();
94
+            $section->heading(ucfirst(translate($heading)));
95
+        }, isImportant: true);
96
+
97
+        ResourcesTab::configureUsing(static function (ResourcesTab $tab): void {
98
+            $tab->localizeLabel();
99
+        }, isImportant: true);
100
+
101
+        NavigationGroup::configureUsing(static function (NavigationGroup $group): void {
102
+            $group->localizeLabel();
103
+        }, isImportant: true);
104
+
105
+        ConfigureCurrencies::syncCurrencies();
56 106
     }
57 107
 }

+ 3
- 4
app/Listeners/CreateCompanyDefaults.php Переглянути файл

@@ -3,7 +3,6 @@
3 3
 namespace App\Listeners;
4 4
 
5 5
 use App\Events\CompanyGenerated;
6
-use App\Models\Locale\Country;
7 6
 use App\Services\CompanyDefaultService;
8 7
 
9 8
 class CreateCompanyDefaults
@@ -23,12 +22,12 @@ class CreateCompanyDefaults
23 22
     {
24 23
         $company = $event->company;
25 24
         $countryCode = $event->country;
26
-
27
-        $currencyCode = Country::where('iso_code_2', $countryCode)->pluck('currency_code')->first();
25
+        $languageCode = $event->language;
26
+        $currency = $event->currency;
28 27
 
29 28
         $user = $company->owner;
30 29
 
31 30
         $companyDefaultService = new CompanyDefaultService();
32
-        $companyDefaultService->createCompanyDefaults($company, $user, $currencyCode);
31
+        $companyDefaultService->createCompanyDefaults($company, $user, $currency, $countryCode, $languageCode);
33 32
     }
34 33
 }

+ 15
- 1
app/Listeners/SyncWithCompanyDefaults.php Переглянути файл

@@ -2,7 +2,9 @@
2 2
 
3 3
 namespace App\Listeners;
4 4
 
5
-use App\Enums\{CategoryType, DiscountType, TaxType};
5
+use App\Enums\CategoryType;
6
+use App\Enums\DiscountType;
7
+use App\Enums\TaxType;
6 8
 use App\Events\CompanyDefaultEvent;
7 9
 use App\Models\Setting\CompanyDefault;
8 10
 use Illuminate\Support\Facades\DB;
@@ -67,6 +69,10 @@ class SyncWithCompanyDefaults
67 69
 
68 70
     private function handleDiscount($default, $type, $key): void
69 71
     {
72
+        if (! in_array($type, [DiscountType::Sales, DiscountType::Purchase], true)) {
73
+            return;
74
+        }
75
+
70 76
         match (true) {
71 77
             $type === DiscountType::Sales => $default->sales_discount_id = $key,
72 78
             $type === DiscountType::Purchase => $default->purchase_discount_id = $key,
@@ -75,6 +81,10 @@ class SyncWithCompanyDefaults
75 81
 
76 82
     private function handleTax($default, $type, $key): void
77 83
     {
84
+        if (! in_array($type, [TaxType::Sales, TaxType::Purchase], true)) {
85
+            return;
86
+        }
87
+
78 88
         match (true) {
79 89
             $type === TaxType::Sales => $default->sales_tax_id = $key,
80 90
             $type === TaxType::Purchase => $default->purchase_tax_id = $key,
@@ -83,6 +93,10 @@ class SyncWithCompanyDefaults
83 93
 
84 94
     private function handleCategory($default, $type, $key): void
85 95
     {
96
+        if (! in_array($type, [CategoryType::Income, CategoryType::Expense], true)) {
97
+            return;
98
+        }
99
+
86 100
         match (true) {
87 101
             $type === CategoryType::Income => $default->income_category_id = $key,
88 102
             $type === CategoryType::Expense => $default->expense_category_id = $key,

+ 48
- 0
app/Listeners/UpdateAccountBalances.php Переглянути файл

@@ -0,0 +1,48 @@
1
+<?php
2
+
3
+namespace App\Listeners;
4
+
5
+use App\Events\CurrencyRateChanged;
6
+use Illuminate\Support\Facades\DB;
7
+
8
+class UpdateAccountBalances
9
+{
10
+    /**
11
+     * Create the event listener.
12
+     */
13
+    public function __construct()
14
+    {
15
+        //
16
+    }
17
+
18
+    /**
19
+     * Handle the event.
20
+     */
21
+    public function handle(CurrencyRateChanged $event): void
22
+    {
23
+        DB::transaction(static function () use ($event) {
24
+            $accounts = $event->currency->accounts;
25
+
26
+            foreach ($accounts as $account) {
27
+                $initialHistory = $account->histories()->where('account_id', $account->id)
28
+                    ->orderBy('created_at')
29
+                    ->first();
30
+
31
+                if ($initialHistory) {
32
+                    $originalBalance = $initialHistory->balance;
33
+                    $originalBalance = money($originalBalance, $account->currency->code)->getAmount();
34
+                    $originalRate = $initialHistory->exchange_rate;
35
+                    $precision = $account->currency->precision;
36
+
37
+                    $newRate = $event->currency->rate;
38
+                    $newBalance = ($newRate / $originalRate) * $originalBalance;
39
+
40
+                    $newBalanceScaled = round($newBalance, $precision);
41
+
42
+                    $account->balance = $newBalanceScaled;
43
+                    $account->save();
44
+                }
45
+            }
46
+        });
47
+    }
48
+}

+ 24
- 8
app/Listeners/UpdateCurrencyRates.php Переглянути файл

@@ -2,16 +2,17 @@
2 2
 
3 3
 namespace App\Listeners;
4 4
 
5
+use App\Contracts\CurrencyHandler;
5 6
 use App\Events\DefaultCurrencyChanged;
6 7
 use App\Models\Setting\Currency;
7
-use App\Services\CurrencyService;
8
+use Illuminate\Support\Facades\DB;
8 9
 
9
-class UpdateCurrencyRates
10
+readonly class UpdateCurrencyRates
10 11
 {
11 12
     /**
12 13
      * Create the event listener.
13 14
      */
14
-    public function __construct()
15
+    public function __construct(private CurrencyHandler $currencyService)
15 16
     {
16 17
         //
17 18
     }
@@ -21,14 +22,29 @@ class UpdateCurrencyRates
21 22
      */
22 23
     public function handle(DefaultCurrencyChanged $event): void
23 24
     {
24
-        $currencyService = app(CurrencyService::class);
25
+        DB::transaction(function () use ($event) {
26
+            $defaultCurrency = $event->currency;
25 27
 
26
-        $currencies = Currency::where('code', '!=', $event->currency->code)->get();
28
+            if (bccomp((string) $defaultCurrency->rate, '1.0', 8) !== 0) {
29
+                $defaultCurrency->update(['rate' => 1]);
30
+            }
31
+
32
+            $this->updateOtherCurrencyRates($defaultCurrency);
33
+        });
34
+    }
35
+
36
+    private function updateOtherCurrencyRates(Currency $defaultCurrency): void
37
+    {
38
+        $targetCurrencies = Currency::where('code', '!=', $defaultCurrency->code)
39
+            ->pluck('code')
40
+            ->toArray();
41
+
42
+        $exchangeRates = $this->currencyService->getCachedExchangeRates($defaultCurrency->code, $targetCurrencies);
27 43
 
28
-        foreach ($currencies as $currency) {
29
-            $newRate = $currencyService->getCachedExchangeRate($event->currency->code, $currency->code);
44
+        foreach ($exchangeRates as $currencyCode => $newRate) {
45
+            $currency = Currency::where('code', $currencyCode)->first();
30 46
 
31
-            if ($newRate !== null) {
47
+            if ($currency && bccomp((string) $currency->rate, (string) $newRate, 8) !== 0) {
32 48
                 $currency->update(['rate' => $newRate]);
33 49
             }
34 50
         }

+ 134
- 0
app/Livewire/Company/Service/LiveCurrency/ListCompanyCurrencies.php Переглянути файл

@@ -0,0 +1,134 @@
1
+<?php
2
+
3
+namespace App\Livewire\Company\Service\LiveCurrency;
4
+
5
+use App\Models\Setting\Currency;
6
+use Filament\Forms\Concerns\InteractsWithForms;
7
+use Filament\Forms\Contracts\HasForms;
8
+use Filament\Notifications\Notification;
9
+use Filament\Support\Enums\FontWeight;
10
+use Filament\Support\Enums\IconPosition;
11
+use Filament\Tables;
12
+use Filament\Tables\Concerns\InteractsWithTable;
13
+use Filament\Tables\Contracts\HasTable;
14
+use Filament\Tables\Table;
15
+use Illuminate\Contracts\View\View;
16
+use Illuminate\Database\Eloquent\Collection;
17
+use Livewire\Component;
18
+
19
+class ListCompanyCurrencies extends Component implements HasForms, HasTable
20
+{
21
+    use InteractsWithForms;
22
+    use InteractsWithTable;
23
+
24
+    protected static ?string $tableModelLabel = 'Currency';
25
+
26
+    public function getTableModelLabel(): ?string
27
+    {
28
+        return static::$tableModelLabel;
29
+    }
30
+
31
+    public function table(Table $table): Table
32
+    {
33
+        return $table
34
+            ->query(Currency::query())
35
+            ->modelLabel($this->getTableModelLabel())
36
+            ->columns([
37
+                Tables\Columns\TextColumn::make('code')
38
+                    ->localizeLabel()
39
+                    ->weight(FontWeight::Medium)
40
+                    ->icon(static fn (Currency $record) => $record->isEnabled() ? 'heroicon-o-lock-closed' : null)
41
+                    ->tooltip(function (Currency $record) {
42
+                        $tooltipMessage = translate('Default :Record', [
43
+                            'Record' => $this->getTableModelLabel(),
44
+                        ]);
45
+
46
+                        return $record->isEnabled() ? $tooltipMessage : null;
47
+                    })
48
+                    ->iconPosition(IconPosition::After)
49
+                    ->sortable()
50
+                    ->searchable(),
51
+                Tables\Columns\TextColumn::make('name')
52
+                    ->localizeLabel()
53
+                    ->sortable()
54
+                    ->searchable(),
55
+                Tables\Columns\TextColumn::make('rate')
56
+                    ->localizeLabel()
57
+                    ->sortable()
58
+                    ->searchable(),
59
+                Tables\Columns\TextColumn::make('live_rate')
60
+                    ->localizeLabel()
61
+                    ->sortable()
62
+                    ->searchable(),
63
+            ])
64
+            ->filters([
65
+                //
66
+            ])
67
+            ->actions([
68
+                Tables\Actions\Action::make('update_rate')
69
+                    ->label('Update Rate')
70
+                    ->icon('heroicon-o-arrow-path')
71
+                    ->hidden(static fn (Currency $record): bool => $record->isEnabled() || ($record->rate === $record->live_rate))
72
+                    ->requiresConfirmation()
73
+                    ->action(static function (Currency $record): void {
74
+                        if (($record->rate !== $record->live_rate) && $record->isDisabled()) {
75
+                            $record->update([
76
+                                'rate' => $record->live_rate,
77
+                            ]);
78
+
79
+                            Notification::make()
80
+                                ->success()
81
+                                ->title('Exchange Rate Updated')
82
+                                ->body(__('The exchange rate for :currency has been updated to reflect the current market rate.', [
83
+                                    'currency' => $record->name,
84
+                                ]))
85
+                                ->send();
86
+                        }
87
+                    }),
88
+            ])
89
+            ->bulkActions([
90
+                Tables\Actions\BulkAction::make('update_rate')
91
+                    ->label('Update Rate')
92
+                    ->icon('heroicon-o-arrow-path')
93
+                    ->requiresConfirmation()
94
+                    ->deselectRecordsAfterCompletion()
95
+                    ->action(function (Collection $records): void {
96
+                        $updatedCurrencies = [];
97
+
98
+                        $records->each(function (Currency $record) use (&$updatedCurrencies): void {
99
+                            if (($record->rate !== $record->live_rate) && $record->isDisabled()) {
100
+                                $record->update([
101
+                                    'rate' => $record->live_rate,
102
+                                ]);
103
+
104
+                                $updatedCurrencies[] = $record->name;
105
+                            }
106
+                        });
107
+
108
+                        if (filled($updatedCurrencies)) {
109
+                            $currencyList = implode('<br>', array_map(static function ($currency) {
110
+                                return '&bull; ' . $currency;
111
+                            }, $updatedCurrencies));
112
+
113
+                            $message = __('The exchange rate for the following currencies has been updated to reflect the current market rate:') . '<br><br>';
114
+
115
+                            $message .= $currencyList;
116
+
117
+                            Notification::make()
118
+                                ->success()
119
+                                ->title('Exchange Rates Updated')
120
+                                ->body($message)
121
+                                ->send();
122
+                        }
123
+                    }),
124
+            ])
125
+            ->checkIfRecordIsSelectableUsing(static function (Currency $record): bool {
126
+                return ($record->rate !== $record->live_rate) && $record->isDisabled();
127
+            });
128
+    }
129
+
130
+    public function render(): View
131
+    {
132
+        return view('livewire.company.service.live-currency.list-company-currencies');
133
+    }
134
+}

+ 61
- 0
app/Livewire/Company/Service/LiveCurrency/ListCurrencies.php Переглянути файл

@@ -0,0 +1,61 @@
1
+<?php
2
+
3
+namespace App\Livewire\Company\Service\LiveCurrency;
4
+
5
+use App\Models\Service\CurrencyList;
6
+use Filament\Forms\Concerns\InteractsWithForms;
7
+use Filament\Forms\Contracts\HasForms;
8
+use Filament\Support\Enums\FontWeight;
9
+use Filament\Tables;
10
+use Filament\Tables\Concerns\InteractsWithTable;
11
+use Filament\Tables\Contracts\HasTable;
12
+use Filament\Tables\Table;
13
+use Illuminate\Contracts\View\View;
14
+use Livewire\Component;
15
+
16
+class ListCurrencies extends Component implements HasForms, HasTable
17
+{
18
+    use InteractsWithForms;
19
+    use InteractsWithTable;
20
+
21
+    public function table(Table $table): Table
22
+    {
23
+        return $table
24
+            ->query(CurrencyList::query())
25
+            ->columns([
26
+                Tables\Columns\TextColumn::make('code')
27
+                    ->localizeLabel()
28
+                    ->weight(FontWeight::Medium)
29
+                    ->sortable()
30
+                    ->searchable(),
31
+                Tables\Columns\TextColumn::make('name')
32
+                    ->localizeLabel()
33
+                    ->sortable()
34
+                    ->searchable(),
35
+                Tables\Columns\TextColumn::make('entity')
36
+                    ->localizeLabel()
37
+                    ->sortable()
38
+                    ->searchable(),
39
+                Tables\Columns\IconColumn::make('available')
40
+                    ->localizeLabel()
41
+                    ->boolean()
42
+                    ->sortable(),
43
+            ])
44
+            ->filters([
45
+                //
46
+            ])
47
+            ->actions([
48
+                //
49
+            ])
50
+            ->bulkActions([
51
+                Tables\Actions\BulkActionGroup::make([
52
+                    //
53
+                ]),
54
+            ]);
55
+    }
56
+
57
+    public function render(): View
58
+    {
59
+        return view('livewire.company.service.live-currency.list-currencies');
60
+    }
61
+}

+ 17
- 21
app/Models/Banking/Account.php Переглянути файл

@@ -3,12 +3,20 @@
3 3
 namespace App\Models\Banking;
4 4
 
5 5
 use App\Casts\MoneyCast;
6
+use App\Enums\AccountStatus;
7
+use App\Enums\AccountType;
8
+use App\Models\History\AccountHistory;
6 9
 use App\Models\Setting\Currency;
7
-use App\Traits\{Blamable, CompanyOwned, SyncsWithCompanyDefaults};
10
+use App\Traits\Blamable;
11
+use App\Traits\CompanyOwned;
12
+use App\Traits\HasDefault;
13
+use App\Traits\SyncsWithCompanyDefaults;
8 14
 use Database\Factories\Banking\AccountFactory;
9
-use Illuminate\Database\Eloquent\Factories\{Factory, HasFactory};
15
+use Illuminate\Database\Eloquent\Factories\Factory;
16
+use Illuminate\Database\Eloquent\Factories\HasFactory;
10 17
 use Illuminate\Database\Eloquent\Model;
11 18
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
19
+use Illuminate\Database\Eloquent\Relations\HasMany;
12 20
 use Spatie\Tags\HasTags;
13 21
 use Wallo\FilamentCompanies\FilamentCompanies;
14 22
 
@@ -16,6 +24,7 @@ class Account extends Model
16 24
 {
17 25
     use Blamable;
18 26
     use CompanyOwned;
27
+    use HasDefault;
19 28
     use HasFactory;
20 29
     use HasTags;
21 30
     use SyncsWithCompanyDefaults;
@@ -29,6 +38,7 @@ class Account extends Model
29 38
         'number',
30 39
         'currency_code',
31 40
         'opening_balance',
41
+        'balance',
32 42
         'description',
33 43
         'notes',
34 44
         'status',
@@ -46,8 +56,11 @@ class Account extends Model
46 56
     ];
47 57
 
48 58
     protected $casts = [
59
+        'type' => AccountType::class,
60
+        'status' => AccountStatus::class,
49 61
         'enabled' => 'boolean',
50 62
         'opening_balance' => MoneyCast::class,
63
+        'balance' => MoneyCast::class,
51 64
     ];
52 65
 
53 66
     public function company(): BelongsTo
@@ -70,26 +83,9 @@ class Account extends Model
70 83
         return $this->belongsTo(FilamentCompanies::userModel(), 'updated_by');
71 84
     }
72 85
 
73
-    public static function getAccountTypes(): array
86
+    public function histories(): HasMany
74 87
     {
75
-        return [
76
-            'checking' => 'Checking',
77
-            'savings' => 'Savings',
78
-            'money_market' => 'Money Market',
79
-            'certificate_of_deposit' => 'Certificate of Deposit',
80
-            'credit_card' => 'Credit Card',
81
-        ];
82
-    }
83
-
84
-    public static function getAccountStatuses(): array
85
-    {
86
-        return [
87
-            'open' => 'Open',
88
-            'active' => 'Active',
89
-            'dormant' => 'Dormant',
90
-            'restricted' => 'Restricted',
91
-            'closed' => 'Closed',
92
-        ];
88
+        return $this->hasMany(AccountHistory::class, 'account_id');
93 89
     }
94 90
 
95 91
     protected static function newFactory(): Factory

+ 6
- 9
app/Models/Common/Contact.php Переглянути файл

@@ -3,13 +3,15 @@
3 3
 namespace App\Models\Common;
4 4
 
5 5
 use App\Enums\ContactType;
6
-use App\Models\Core\Department;
7 6
 use App\Models\Setting\Currency;
8
-use App\Traits\{Blamable, CompanyOwned};
7
+use App\Traits\Blamable;
8
+use App\Traits\CompanyOwned;
9 9
 use Database\Factories\Common\ContactFactory;
10
-use Illuminate\Database\Eloquent\Factories\{Factory, HasFactory};
10
+use Illuminate\Database\Eloquent\Factories\Factory;
11
+use Illuminate\Database\Eloquent\Factories\HasFactory;
11 12
 use Illuminate\Database\Eloquent\Model;
12
-use Illuminate\Database\Eloquent\Relations\{BelongsTo, HasMany, HasOne};
13
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
14
+use Illuminate\Database\Eloquent\Relations\HasOne;
13 15
 use Wallo\FilamentCompanies\FilamentCompanies;
14 16
 
15 17
 class Contact extends Model
@@ -46,11 +48,6 @@ class Contact extends Model
46 48
         'type' => ContactType::class,
47 49
     ];
48 50
 
49
-    public function manager(): HasMany
50
-    {
51
-        return $this->hasMany(Department::class, 'manager_id');
52
-    }
53
-
54 51
     public function company(): BelongsTo
55 52
     {
56 53
         return $this->belongsTo(FilamentCompanies::companyModel(), 'company_id');

+ 27
- 5
app/Models/Company.php Переглянути файл

@@ -6,12 +6,24 @@ use App\Enums\DocumentType;
6 6
 use App\Models\Banking\Account;
7 7
 use App\Models\Common\Contact;
8 8
 use App\Models\Core\Department;
9
-use App\Models\Setting\{Appearance, Category, CompanyDefault, CompanyProfile, Currency, Discount, DocumentDefault, Tax};
9
+use App\Models\History\AccountHistory;
10
+use App\Models\Setting\Appearance;
11
+use App\Models\Setting\Category;
12
+use App\Models\Setting\CompanyDefault;
13
+use App\Models\Setting\CompanyProfile;
14
+use App\Models\Setting\Currency;
15
+use App\Models\Setting\Discount;
16
+use App\Models\Setting\DocumentDefault;
17
+use App\Models\Setting\Localization;
18
+use App\Models\Setting\Tax;
10 19
 use Filament\Models\Contracts\HasAvatar;
11 20
 use Illuminate\Database\Eloquent\Factories\HasFactory;
12
-use Illuminate\Database\Eloquent\Relations\{HasMany, HasOne};
21
+use Illuminate\Database\Eloquent\Relations\HasMany;
22
+use Illuminate\Database\Eloquent\Relations\HasOne;
13 23
 use Wallo\FilamentCompanies\Company as FilamentCompaniesCompany;
14
-use Wallo\FilamentCompanies\Events\{CompanyCreated, CompanyDeleted, CompanyUpdated};
24
+use Wallo\FilamentCompanies\Events\CompanyCreated;
25
+use Wallo\FilamentCompanies\Events\CompanyDeleted;
26
+use Wallo\FilamentCompanies\Events\CompanyUpdated;
15 27
 
16 28
 class Company extends FilamentCompaniesCompany implements HasAvatar
17 29
 {
@@ -47,9 +59,9 @@ class Company extends FilamentCompaniesCompany implements HasAvatar
47 59
         'deleted' => CompanyDeleted::class,
48 60
     ];
49 61
 
50
-    public function getFilamentAvatarUrl(): string
62
+    public function getFilamentAvatarUrl(): ?string
51 63
     {
52
-        return $this->owner->profile_photo_url;
64
+        return $this->profile->logo_url ?? $this->owner->profile_photo_url;
53 65
     }
54 66
 
55 67
     public function accounts(): HasMany
@@ -57,6 +69,11 @@ class Company extends FilamentCompaniesCompany implements HasAvatar
57 69
         return $this->hasMany(Account::class, 'company_id');
58 70
     }
59 71
 
72
+    public function accountHistories(): HasMany
73
+    {
74
+        return $this->hasMany(AccountHistory::class, 'company_id');
75
+    }
76
+
60 77
     public function appearance(): HasOne
61 78
     {
62 79
         return $this->hasOne(Appearance::class, 'company_id');
@@ -104,6 +121,11 @@ class Company extends FilamentCompaniesCompany implements HasAvatar
104 121
         return $this->hasMany(Discount::class, 'company_id');
105 122
     }
106 123
 
124
+    public function locale(): HasOne
125
+    {
126
+        return $this->hasOne(Localization::class, 'company_id');
127
+    }
128
+
107 129
     public function profile(): HasOne
108 130
     {
109 131
         return $this->hasOne(CompanyProfile::class, 'company_id');

+ 3
- 1
app/Models/ConnectedAccount.php Переглянути файл

@@ -4,7 +4,9 @@ namespace App\Models;
4 4
 
5 5
 use Illuminate\Database\Eloquent\Concerns\HasTimestamps;
6 6
 use Wallo\FilamentCompanies\ConnectedAccount as SocialiteConnectedAccount;
7
-use Wallo\FilamentCompanies\Events\{ConnectedAccountCreated, ConnectedAccountDeleted, ConnectedAccountUpdated};
7
+use Wallo\FilamentCompanies\Events\ConnectedAccountCreated;
8
+use Wallo\FilamentCompanies\Events\ConnectedAccountDeleted;
9
+use Wallo\FilamentCompanies\Events\ConnectedAccountUpdated;
8 10
 
9 11
 class ConnectedAccount extends SocialiteConnectedAccount
10 12
 {

+ 10
- 6
app/Models/Core/Department.php Переглянути файл

@@ -2,12 +2,15 @@
2 2
 
3 3
 namespace App\Models\Core;
4 4
 
5
-use App\Models\Common\Contact;
6
-use App\Traits\{Blamable, CompanyOwned};
5
+use App\Models\User;
6
+use App\Traits\Blamable;
7
+use App\Traits\CompanyOwned;
7 8
 use Database\Factories\Core\DepartmentFactory;
8
-use Illuminate\Database\Eloquent\Factories\{Factory, HasFactory};
9
+use Illuminate\Database\Eloquent\Factories\Factory;
10
+use Illuminate\Database\Eloquent\Factories\HasFactory;
9 11
 use Illuminate\Database\Eloquent\Model;
10
-use Illuminate\Database\Eloquent\Relations\{BelongsTo, HasMany};
12
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
13
+use Illuminate\Database\Eloquent\Relations\HasMany;
11 14
 use Wallo\FilamentCompanies\FilamentCompanies;
12 15
 
13 16
 class Department extends Model
@@ -35,12 +38,13 @@ class Department extends Model
35 38
 
36 39
     public function manager(): BelongsTo
37 40
     {
38
-        return $this->belongsTo(Contact::class, 'manager_id');
41
+        return $this->belongsTo(User::class, 'manager_id');
39 42
     }
40 43
 
41 44
     public function parent(): BelongsTo
42 45
     {
43
-        return $this->belongsTo(self::class, 'parent_id');
46
+        return $this->belongsTo(self::class, 'parent_id')
47
+            ->whereKeyNot($this->getKey());
44 48
     }
45 49
 
46 50
     public function children(): HasMany

+ 4
- 2
app/Models/Employeeship.php Переглянути файл

@@ -4,8 +4,10 @@ namespace App\Models;
4 4
 
5 5
 use App\Models\Common\Contact;
6 6
 use App\Models\Core\Department;
7
-use Illuminate\Database\Eloquent\Relations\{BelongsTo, HasMany};
8
-use Wallo\FilamentCompanies\{Employeeship as FilamentCompaniesEmployeeship, FilamentCompanies};
7
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
8
+use Illuminate\Database\Eloquent\Relations\HasMany;
9
+use Wallo\FilamentCompanies\Employeeship as FilamentCompaniesEmployeeship;
10
+use Wallo\FilamentCompanies\FilamentCompanies;
9 11
 
10 12
 class Employeeship extends FilamentCompaniesEmployeeship
11 13
 {

+ 64
- 0
app/Models/History/AccountHistory.php Переглянути файл

@@ -0,0 +1,64 @@
1
+<?php
2
+
3
+namespace App\Models\History;
4
+
5
+use App\Casts\CurrencyRateCast;
6
+use App\Casts\MoneyCast;
7
+use App\Models\Banking\Account;
8
+use App\Models\Setting\Currency;
9
+use Illuminate\Database\Eloquent\Factories\HasFactory;
10
+use Illuminate\Database\Eloquent\Model;
11
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
12
+use Wallo\FilamentCompanies\FilamentCompanies;
13
+
14
+class AccountHistory extends Model
15
+{
16
+    use HasFactory;
17
+
18
+    protected $table = 'account_histories';
19
+
20
+    protected $fillable = [
21
+        'company_id',
22
+        'account_id',
23
+        'type',
24
+        'name',
25
+        'number',
26
+        'currency_code',
27
+        'opening_balance',
28
+        'balance',
29
+        'exchange_rate',
30
+        'status',
31
+        'actions',
32
+        'description',
33
+        'enabled',
34
+        'changed_by',
35
+    ];
36
+
37
+    protected $casts = [
38
+        'enabled' => 'boolean',
39
+        'opening_balance' => MoneyCast::class,
40
+        'balance' => MoneyCast::class,
41
+        'exchange_rate' => CurrencyRateCast::class,
42
+        'actions' => 'array',
43
+    ];
44
+
45
+    public function company(): BelongsTo
46
+    {
47
+        return $this->belongsTo(FilamentCompanies::companyModel(), 'company_id');
48
+    }
49
+
50
+    public function account(): BelongsTo
51
+    {
52
+        return $this->belongsTo(Account::class, 'account_id');
53
+    }
54
+
55
+    public function currency(): BelongsTo
56
+    {
57
+        return $this->belongsTo(Currency::class, 'currency_code', 'code');
58
+    }
59
+
60
+    public function changedBy(): BelongsTo
61
+    {
62
+        return $this->belongsTo(FilamentCompanies::userModel(), 'changed_by');
63
+    }
64
+}

+ 3
- 5
app/Models/Locale/City.php Переглянути файл

@@ -11,8 +11,7 @@ use Squire\Model;
11 11
  * @property string $name
12 12
  * @property int $state_id
13 13
  * @property string $state_code
14
- * @property int $country_id
15
- * @property string $country_code
14
+ * @property string $country_id
16 15
  * @property float $latitude
17 16
  * @property float $longitude
18 17
  */
@@ -23,8 +22,7 @@ class City extends Model
23 22
         'name' => 'string',
24 23
         'state_id' => 'integer',
25 24
         'state_code' => 'string',
26
-        'country_id' => 'integer',
27
-        'country_code' => 'string',
25
+        'country_id' => 'string',
28 26
         'latitude' => 'float',
29 27
         'longitude' => 'float',
30 28
     ];
@@ -35,7 +33,7 @@ class City extends Model
35 33
             return collect();
36 34
         }
37 35
 
38
-        return self::query()->where('country_code', $countryCode)
36
+        return self::query()->where('country_id', $countryCode)
39 37
             ->where('state_id', $stateId)
40 38
             ->get();
41 39
     }

+ 43
- 8
app/Models/Locale/Country.php Переглянути файл

@@ -2,12 +2,17 @@
2 2
 
3 3
 namespace App\Models\Locale;
4 4
 
5
-use Illuminate\Database\Eloquent\Relations\{BelongsTo, HasMany};
5
+use App\Models\Setting\CompanyProfile;
6
+use Illuminate\Database\Eloquent\Casts\Attribute;
7
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
8
+use Illuminate\Database\Eloquent\Relations\HasMany;
6 9
 use Illuminate\Support\Collection;
7 10
 use Squire\Model;
11
+use Symfony\Component\Intl\Countries;
12
+use Symfony\Component\Intl\Locales;
8 13
 
9 14
 /**
10
- * @property int $id
15
+ * @property string $id
11 16
  * @property string $name
12 17
  * @property string $iso_code_3
13 18
  * @property string $iso_code_2
@@ -24,7 +29,7 @@ use Squire\Model;
24 29
 class Country extends Model
25 30
 {
26 31
     public static array $schema = [
27
-        'id' => 'integer',
32
+        'id' => 'string',
28 33
         'name' => 'string',
29 34
         'iso_code_3' => 'string',
30 35
         'iso_code_2' => 'string',
@@ -44,6 +49,11 @@ class Country extends Model
44 49
         return $this->belongsTo(Currency::class, 'currency_code', 'code');
45 50
     }
46 51
 
52
+    public function profiles(): HasMany
53
+    {
54
+        return $this->hasMany(CompanyProfile::class, 'country', 'id');
55
+    }
56
+
47 57
     public function states(): HasMany
48 58
     {
49 59
         return $this->hasMany(State::class, 'country_id', 'id');
@@ -54,25 +64,50 @@ class Country extends Model
54 64
         return $this->hasMany(City::class, 'country_id', 'id');
55 65
     }
56 66
 
57
-    public function timezones(): HasMany
67
+    protected function name(): Attribute
58 68
     {
59
-        return $this->hasMany(Timezone::class, 'country_id', 'id');
69
+        return Attribute::get(static function (mixed $value, array $attributes): string {
70
+            $exists = Countries::exists($attributes['id']);
71
+
72
+            return $exists ? Countries::getName($attributes['id']) : $value;
73
+        });
60 74
     }
61 75
 
62 76
     public static function findByIsoCode2(string $code): ?self
63 77
     {
64
-        return self::where('iso_code_2', $code)->first();
78
+        return self::where('id', $code)->first();
65 79
     }
66 80
 
67 81
     public static function getAllCountryCodes(): Collection
68 82
     {
69
-        return self::all()->pluck('iso_code_2');
83
+        return self::all()->pluck('id');
70 84
     }
71 85
 
72 86
     public static function getAvailableCountryOptions(): array
73 87
     {
74 88
         return self::all()->mapWithKeys(static function ($country): array {
75
-            return [$country->iso_code_2 => $country->name . ' ' . $country->flag];
89
+            return [$country->id => $country->name . ' ' . $country->flag];
76 90
         })->toArray();
77 91
     }
92
+
93
+    public static function getLanguagesByCountryCode(?string $code = null): array
94
+    {
95
+        if ($code === null) {
96
+            return Locales::getNames();
97
+        }
98
+
99
+        $locales = Locales::getNames();
100
+        $languages = [];
101
+
102
+        foreach (array_keys($locales) as $locale) {
103
+            $localeRegion = locale_get_region($locale);
104
+            $localeLanguage = locale_get_primary_language($locale);
105
+
106
+            if ($localeRegion === $code) {
107
+                $languages[$localeLanguage] = Locales::getName($localeLanguage);
108
+            }
109
+        }
110
+
111
+        return $languages;
112
+    }
78 113
 }

+ 0
- 0
app/Models/Locale/State.php Переглянути файл


Деякі файли не було показано, через те що забагато файлів було змінено

Завантаження…
Відмінити
Зберегти