Преглед изворни кода

Merge pull request #30 from andrewdwallo/development

Enhance application with new features
3.x
Andrew Wallo пре 1 година
родитељ
комит
4312276577
No account linked to committer's email address
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
 
75
 
76
     php artisan migrate:refresh
76
     php artisan migrate:refresh
77
 
77
 
78
-## Currency Exchange Rates
78
+## Live Currency
79
 
79
 
80
 ### Overview
80
 ### Overview
81
 
81
 
85
 
85
 
86
 Once you have your API key, you can enable the feature by setting the `CURRENCY_API_KEY` environment variable in your `.env` file.
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
 ### Configuration
104
 ### Configuration
89
 
105
 
90
 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:
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
 ],
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
 ### Important Information
121
 ### Important Information
123
 
122
 
125
 - Your API key is sensitive information and should be kept secret. Do not commit it to your repository or share it with anyone.
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
 - Note that API rate limits may apply depending on the service you choose. Make sure to review the terms for your chosen service.
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
 ## Dependencies
148
 ## Dependencies
129
 
149
 
130
 - [filamentphp/filament](https://github.com/filamentphp/filament) - A collection of beautiful full-stack components
150
 - [filamentphp/filament](https://github.com/filamentphp/filament) - A collection of beautiful full-stack components
131
 - [andrewdwallo/filament-companies](https://github.com/andrewdwallo/filament-companies) - A complete authentication system kit based on companies built for Filament
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
 - [akaunting/laravel-money](https://github.com/akaunting/laravel-money) - Currency formatting and conversion package for Laravel
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
 ***Note*** : It is recommended to read the documentation for all dependencies to get yourself familiar with how the application works.
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
 
2
 
3
 namespace App\Actions\FilamentCompanies;
3
 namespace App\Actions\FilamentCompanies;
4
 
4
 
5
-use App\Models\{Company, User};
5
+use App\Models\Company;
6
+use App\Models\User;
6
 use Closure;
7
 use Closure;
7
 use Illuminate\Auth\Access\AuthorizationException;
8
 use Illuminate\Auth\Access\AuthorizationException;
8
 use Illuminate\Contracts\Validation\Rule;
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
 use Wallo\FilamentCompanies\Contracts\AddsCompanyEmployees;
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
 use Wallo\FilamentCompanies\FilamentCompanies;
15
 use Wallo\FilamentCompanies\FilamentCompanies;
13
 use Wallo\FilamentCompanies\Rules\Role;
16
 use Wallo\FilamentCompanies\Rules\Role;
14
 
17
 

+ 4
- 2
app/Actions/FilamentCompanies/CreateCompany.php Прегледај датотеку

2
 
2
 
3
 namespace App\Actions\FilamentCompanies;
3
 namespace App\Actions\FilamentCompanies;
4
 
4
 
5
-use App\Models\{Company, User};
5
+use App\Models\Company;
6
+use App\Models\User;
6
 use Illuminate\Auth\Access\AuthorizationException;
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
 use Wallo\FilamentCompanies\Contracts\CreatesCompanies;
10
 use Wallo\FilamentCompanies\Contracts\CreatesCompanies;
9
 use Wallo\FilamentCompanies\Events\AddingCompany;
11
 use Wallo\FilamentCompanies\Events\AddingCompany;
10
 use Wallo\FilamentCompanies\FilamentCompanies;
12
 use Wallo\FilamentCompanies\FilamentCompanies;

+ 2
- 1
app/Actions/FilamentCompanies/CreateConnectedAccount.php Прегледај датотеку

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

+ 5
- 2
app/Actions/FilamentCompanies/CreateNewUser.php Прегледај датотеку

2
 
2
 
3
 namespace App\Actions\FilamentCompanies;
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
 use Wallo\FilamentCompanies\Contracts\CreatesNewUsers;
10
 use Wallo\FilamentCompanies\Contracts\CreatesNewUsers;
8
 use Wallo\FilamentCompanies\Features;
11
 use Wallo\FilamentCompanies\Features;
9
 
12
 

+ 6
- 3
app/Actions/FilamentCompanies/CreateUserFromProvider.php Прегледај датотеку

2
 
2
 
3
 namespace App\Actions\FilamentCompanies;
3
 namespace App\Actions\FilamentCompanies;
4
 
4
 
5
-use App\Models\{Company, User};
5
+use App\Models\Company;
6
+use App\Models\User;
6
 use Illuminate\Support\Facades\DB;
7
 use Illuminate\Support\Facades\DB;
7
 use Laravel\Socialite\Contracts\User as ProviderUserContract;
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
 class CreateUserFromProvider implements CreatesUserFromProvider
14
 class CreateUserFromProvider implements CreatesUserFromProvider
12
 {
15
 {

+ 4
- 2
app/Actions/FilamentCompanies/DeleteUser.php Прегледај датотеку

2
 
2
 
3
 namespace App\Actions\FilamentCompanies;
3
 namespace App\Actions\FilamentCompanies;
4
 
4
 
5
-use App\Models\{Company, User};
5
+use App\Models\Company;
6
+use App\Models\User;
6
 use Illuminate\Support\Facades\DB;
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
 class DeleteUser implements DeletesUsers
11
 class DeleteUser implements DeletesUsers
10
 {
12
 {

+ 5
- 2
app/Actions/FilamentCompanies/InviteCompanyEmployee.php Прегледај датотеку

2
 
2
 
3
 namespace App\Actions\FilamentCompanies;
3
 namespace App\Actions\FilamentCompanies;
4
 
4
 
5
-use App\Models\{Company, User};
5
+use App\Models\Company;
6
+use App\Models\User;
6
 use Closure;
7
 use Closure;
7
 use Illuminate\Auth\Access\AuthorizationException;
8
 use Illuminate\Auth\Access\AuthorizationException;
8
 use Illuminate\Database\Query\Builder;
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
 use Illuminate\Validation\Rule;
13
 use Illuminate\Validation\Rule;
11
 use Wallo\FilamentCompanies\Contracts\InvitesCompanyEmployees;
14
 use Wallo\FilamentCompanies\Contracts\InvitesCompanyEmployees;
12
 use Wallo\FilamentCompanies\Events\InvitingCompanyEmployee;
15
 use Wallo\FilamentCompanies\Events\InvitingCompanyEmployee;

+ 2
- 1
app/Actions/FilamentCompanies/RemoveCompanyEmployee.php Прегледај датотеку

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

+ 2
- 1
app/Actions/FilamentCompanies/SetUserPassword.php Прегледај датотеку

3
 namespace App\Actions\FilamentCompanies;
3
 namespace App\Actions\FilamentCompanies;
4
 
4
 
5
 use App\Models\User;
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
 use Wallo\FilamentCompanies\Contracts\SetsUserPasswords;
8
 use Wallo\FilamentCompanies\Contracts\SetsUserPasswords;
8
 
9
 
9
 class SetUserPassword implements SetsUserPasswords
10
 class SetUserPassword implements SetsUserPasswords

+ 4
- 2
app/Actions/FilamentCompanies/UpdateCompanyName.php Прегледај датотеку

2
 
2
 
3
 namespace App\Actions\FilamentCompanies;
3
 namespace App\Actions\FilamentCompanies;
4
 
4
 
5
-use App\Models\{Company, User};
5
+use App\Models\Company;
6
+use App\Models\User;
6
 use Illuminate\Auth\Access\AuthorizationException;
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
 use Wallo\FilamentCompanies\Contracts\UpdatesCompanyNames;
10
 use Wallo\FilamentCompanies\Contracts\UpdatesCompanyNames;
9
 
11
 
10
 class UpdateCompanyName implements UpdatesCompanyNames
12
 class UpdateCompanyName implements UpdatesCompanyNames

+ 2
- 1
app/Actions/FilamentCompanies/UpdateUserPassword.php Прегледај датотеку

3
 namespace App\Actions\FilamentCompanies;
3
 namespace App\Actions\FilamentCompanies;
4
 
4
 
5
 use App\Models\User;
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
 use Wallo\FilamentCompanies\Contracts\UpdatesUserPasswords;
8
 use Wallo\FilamentCompanies\Contracts\UpdatesUserPasswords;
8
 
9
 
9
 class UpdateUserPassword implements UpdatesUserPasswords
10
 class UpdateUserPassword implements UpdatesUserPasswords

+ 2
- 1
app/Actions/OptionAction/CreateCurrency.php Прегледај датотеку

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

+ 24
- 0
app/Casts/CurrencyRateCast.php Прегледај датотеку

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
 
2
 
3
 namespace App\Casts;
3
 namespace App\Casts;
4
 
4
 
5
-use App\Models\Setting\Currency;
5
+use App\Utilities\Currency\CurrencyAccessor;
6
 use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
6
 use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
7
+use Illuminate\Database\Eloquent\Model;
7
 use UnexpectedValueException;
8
 use UnexpectedValueException;
8
 
9
 
9
 class MoneyCast implements CastsAttributes
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
      * @throws UnexpectedValueException
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
         if (is_int($value)) {
24
         if (is_int($value)) {
24
             return $value;
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
         if (! $currency_code) {
30
         if (! $currency_code) {
30
             throw new UnexpectedValueException('Currency code is not set');
31
             throw new UnexpectedValueException('Currency code is not set');

+ 49
- 7
app/Casts/RateCast.php Прегледај датотеку

2
 
2
 
3
 namespace App\Casts;
3
 namespace App\Casts;
4
 
4
 
5
+use App\Enums\NumberFormat;
6
+use App\Models\Setting\Localization;
7
+use App\Utilities\Currency\CurrencyAccessor;
5
 use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
8
 use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
9
+use Illuminate\Database\Eloquent\Model;
6
 
10
 
7
 class RateCast implements CastsAttributes
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
-<?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
-<?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 Прегледај датотеку

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
-<?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 Прегледај датотеку

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
 <?php
1
 <?php
2
 
2
 
3
-namespace App\Interfaces\Utility;
3
+namespace App\Contracts;
4
 
4
 
5
 use Illuminate\Database\Eloquent\Model;
5
 use Illuminate\Database\Eloquent\Model;
6
 
6
 

+ 45
- 0
app/Enums/AccountStatus.php Прегледај датотеку

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 Прегледај датотеку

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
 
13
 
14
     public function getLabel(): ?string
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
 namespace App\Enums;
3
 namespace App\Enums;
4
 
4
 
5
 use Filament\Support\Colors\Color;
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
 enum ContactType: string implements HasColor, HasIcon, HasLabel
10
 enum ContactType: string implements HasColor, HasIcon, HasLabel
9
 {
11
 {

+ 41
- 0
app/Enums/DateFormat.php Прегледај датотеку

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

+ 0
- 19
app/Enums/DocumentAmountColumn.php Прегледај датотеку

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
-<?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
-<?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
 
2
 
3
 namespace App\Enums;
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
 enum DocumentType: string implements HasIcon, HasLabel
8
 enum DocumentType: string implements HasIcon, HasLabel
8
 {
9
 {

+ 0
- 19
app/Enums/DocumentUnitColumn.php Прегледај датотеку

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

+ 3
- 1
app/Enums/MaxContentWidth.php Прегледај датотеку

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

+ 3
- 1
app/Enums/ModalWidth.php Прегледај датотеку

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

+ 106
- 0
app/Enums/NumberFormat.php Прегледај датотеку

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
 
6
 
7
 enum PaymentTerms: string implements HasLabel
7
 enum PaymentTerms: string implements HasLabel
8
 {
8
 {
9
-    case DueOnReceipt = 'due_on_receipt';
9
+    case DueUponReceipt = 'due_upon_receipt';
10
     case Net7 = 'net_7';
10
     case Net7 = 'net_7';
11
     case Net10 = 'net_10';
11
     case Net10 = 'net_10';
12
     case Net15 = 'net_15';
12
     case Net15 = 'net_15';
14
     case Net60 = 'net_60';
14
     case Net60 = 'net_60';
15
     case Net90 = 'net_90';
15
     case Net90 = 'net_90';
16
 
16
 
17
-    public const DEFAULT = self::DueOnReceipt->value;
17
+    public const DEFAULT = self::DueUponReceipt->value;
18
 
18
 
19
     public function getLabel(): ?string
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
     public function getDays(): int
26
     public function getDays(): int
33
     {
27
     {
34
         return match ($this) {
28
         return match ($this) {
35
-            self::DueOnReceipt => 0,
29
+            self::DueUponReceipt => 0,
36
             self::Net7 => 7,
30
             self::Net7 => 7,
37
             self::Net10 => 10,
31
             self::Net10 => 10,
38
             self::Net15 => 15,
32
             self::Net15 => 15,
42
         };
36
         };
43
     }
37
     }
44
 
38
 
45
-    public function getDueDate(): string
39
+    public function getDueDate(string $format): string
46
     {
40
     {
47
         $days = $this->getDays() ?? 0;
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
 use App\Enums\Concerns\Utilities;
5
 use App\Enums\Concerns\Utilities;
6
 use Filament\Support\Colors\Color;
6
 use Filament\Support\Colors\Color;
7
 use Filament\Support\Contracts\HasColor;
7
 use Filament\Support\Contracts\HasColor;
8
+use Filament\Support\Contracts\HasLabel;
8
 use Spatie\Color\Rgb;
9
 use Spatie\Color\Rgb;
9
 use UnexpectedValueException;
10
 use UnexpectedValueException;
10
 
11
 
11
-enum PrimaryColor: string implements HasColor
12
+enum PrimaryColor: string implements HasColor, HasLabel
12
 {
13
 {
13
     use Utilities;
14
     use Utilities;
14
 
15
 
65
         };
66
         };
66
     }
67
     }
67
 
68
 
69
+    public function getLabel(): ?string
70
+    {
71
+        return ucfirst(translate($this->value));
72
+    }
73
+
68
     /**
74
     /**
69
      * @throws UnexpectedValueException
75
      * @throws UnexpectedValueException
70
      */
76
      */

+ 0
- 10
app/Enums/RecordsPerPage.php Прегледај датотеку

16
 
16
 
17
     public const DEFAULT = self::Ten->value;
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
     public function getLabel(): ?string
19
     public function getLabel(): ?string
30
     {
20
     {
31
         return (string) $this->value;
21
         return (string) $this->value;

+ 1
- 1
app/Enums/TableSortDirection.php Прегледај датотеку

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

+ 1
- 1
app/Enums/Template.php Прегледај датотеку

14
 
14
 
15
     public function getLabel(): ?string
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 Прегледај датотеку

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 Прегледај датотеку

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 Прегледај датотеку

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

+ 25
- 0
app/Events/CurrencyRateChanged.php Прегледај датотеку

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
-<?php
2
-
3
-namespace App\Events;
4
-
5
-class UpdateCompanyDefault
6
-{
7
-}

+ 30
- 0
app/Facades/Forex.php Прегледај датотеку

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 Прегледај датотеку

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
 {
9
 {
10
     public function phoneNumberForCountryCode(string $countryCode): string
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
         $filteredFormats = array_filter(
14
         $filteredFormats = array_filter(
15
             static::$e164Formats,
15
             static::$e164Formats,

+ 1
- 3
app/Faker/State.php Прегледај датотеку

9
 {
9
 {
10
     public function state(string $countryCode, string $column = 'id'): mixed
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
 
4
 
5
 use App\Enums\EntityType;
5
 use App\Enums\EntityType;
6
 use App\Events\CompanyGenerated;
6
 use App\Events\CompanyGenerated;
7
+use App\Models\Company;
7
 use App\Models\Locale\Country;
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
 use Filament\Forms\Form;
12
 use Filament\Forms\Form;
13
+use Filament\Forms\Get;
10
 use Illuminate\Database\Eloquent\Model;
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
 use Wallo\FilamentCompanies\Events\AddingCompany;
17
 use Wallo\FilamentCompanies\Events\AddingCompany;
13
 use Wallo\FilamentCompanies\FilamentCompanies;
18
 use Wallo\FilamentCompanies\FilamentCompanies;
14
 use Wallo\FilamentCompanies\Pages\Company\CreateCompany as FilamentCreateCompany;
19
 use Wallo\FilamentCompanies\Pages\Company\CreateCompany as FilamentCreateCompany;
30
                     ->required(),
35
                     ->required(),
31
                 Select::make('profile.entity_type')
36
                 Select::make('profile.entity_type')
32
                     ->label('Entity Type')
37
                     ->label('Entity Type')
33
-                    ->native(false)
34
                     ->options(EntityType::class)
38
                     ->options(EntityType::class)
35
                     ->required(),
39
                     ->required(),
36
                 Select::make('profile.country')
40
                 Select::make('profile.country')
37
                     ->label('Country')
41
                     ->label('Country')
38
-                    ->native(false)
42
+                    ->live()
39
                     ->searchable()
43
                     ->searchable()
40
                     ->options(Country::getAvailableCountryOptions())
44
                     ->options(Country::getAvailableCountryOptions())
41
                     ->required(),
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
             ->model(FilamentCompanies::companyModel())
70
             ->model(FilamentCompanies::companyModel())
44
             ->statePath('data');
71
             ->statePath('data');
54
 
81
 
55
         $personalCompany = $user?->personalCompany() === null;
82
         $personalCompany = $user?->personalCompany() === null;
56
 
83
 
84
+        /** @var Company $company */
57
         $company = $user?->ownedCompanies()->create([
85
         $company = $user?->ownedCompanies()->create([
58
             'name' => $data['name'],
86
             'name' => $data['name'],
59
             'personal_company' => $personalCompany,
87
             'personal_company' => $personalCompany,
69
 
97
 
70
         $name = $data['name'];
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
         $this->companyCreated($name);
102
         $this->companyCreated($name);
75
 
103
 

+ 69
- 0
app/Filament/Company/Pages/Service/LiveCurrency.php Прегледај датотеку

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
 
2
 
3
 namespace App\Filament\Company\Pages\Setting;
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
 use App\Models\Setting\Appearance as AppearanceModel;
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
 use Filament\Forms\Form;
17
 use Filament\Forms\Form;
10
 use Filament\Notifications\Notification;
18
 use Filament\Notifications\Notification;
11
 use Filament\Pages\Concerns\InteractsWithFormActions;
19
 use Filament\Pages\Concerns\InteractsWithFormActions;
12
 use Filament\Pages\Page;
20
 use Filament\Pages\Page;
13
 use Filament\Support\Exceptions\Halt;
21
 use Filament\Support\Exceptions\Halt;
14
 use Illuminate\Auth\Access\AuthorizationException;
22
 use Illuminate\Auth\Access\AuthorizationException;
23
+use Illuminate\Contracts\Support\Htmlable;
15
 use Illuminate\Database\Eloquent\Model;
24
 use Illuminate\Database\Eloquent\Model;
16
 use Livewire\Attributes\Locked;
25
 use Livewire\Attributes\Locked;
17
-use Wallo\FilamentSelectify\Components\{ButtonGroup, ToggleButton};
26
+use Wallo\FilamentSelectify\Components\ToggleButton;
18
 
27
 
19
 use function Filament\authorize;
28
 use function Filament\authorize;
20
 
29
 
27
 
36
 
28
     protected static ?string $navigationIcon = 'heroicon-o-paint-brush';
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
     protected static ?string $navigationGroup = 'Settings';
41
     protected static ?string $navigationGroup = 'Settings';
33
 
42
 
34
     protected static ?string $slug = 'settings/appearance';
43
     protected static ?string $slug = 'settings/appearance';
35
 
44
 
36
-    protected ?string $heading = 'Appearance';
37
-
38
     protected static string $view = 'filament.company.pages.setting.appearance';
45
     protected static string $view = 'filament.company.pages.setting.appearance';
39
 
46
 
40
     public ?array $data = [];
47
     public ?array $data = [];
42
     #[Locked]
49
     #[Locked]
43
     public ?AppearanceModel $record = null;
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
     public function mount(): void
62
     public function mount(): void
46
     {
63
     {
47
         $this->record = AppearanceModel::firstOrNew([
64
         $this->record = AppearanceModel::firstOrNew([
57
     {
74
     {
58
         $data = $this->record->attributesToArray();
75
         $data = $this->record->attributesToArray();
59
 
76
 
60
-        $data = $this->mutateFormDataBeforeFill($data);
61
-
62
         $this->form->fill($data);
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
     public function save(): void
80
     public function save(): void
76
     {
81
     {
77
         try {
82
         try {
78
             $data = $this->form->getState();
83
             $data = $this->form->getState();
79
 
84
 
80
-            $data = $this->mutateFormDataBeforeSave($data);
81
-
82
             $this->handleRecordUpdate($this->record, $data);
85
             $this->handleRecordUpdate($this->record, $data);
83
 
86
 
84
         } catch (Halt $exception) {
87
         } catch (Halt $exception) {
85
             return;
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
         return Notification::make()
96
         return Notification::make()
104
             ->success()
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
     public function form(Form $form): Form
101
     public function form(Form $form): Form
133
         return Section::make('General')
116
         return Section::make('General')
134
             ->schema([
117
             ->schema([
135
                 Select::make('primary_color')
118
                 Select::make('primary_color')
136
-                    ->label('Primary Color')
137
-                    ->native(false)
138
                     ->allowHtml()
119
                     ->allowHtml()
139
-                    ->selectablePlaceholder(false)
140
-                    ->rule('required')
120
+                    ->softRequired()
121
+                    ->localizeLabel()
141
                     ->options(
122
                     ->options(
142
                         collect(PrimaryColor::cases())
123
                         collect(PrimaryColor::cases())
143
                             ->mapWithKeys(static fn ($case) => [
124
                             ->mapWithKeys(static fn ($case) => [
144
                                 $case->value => "<span class='flex items-center gap-x-4'>
125
                                 $case->value => "<span class='flex items-center gap-x-4'>
145
                                 <span class='rounded-full w-4 h-4' style='background:rgb(" . $case->getColor()[600] . ")'></span>
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
                                 </span>',
128
                                 </span>',
148
                             ]),
129
                             ]),
149
                     ),
130
                     ),
150
                 Select::make('font')
131
                 Select::make('font')
151
-                    ->label('Font')
152
-                    ->native(false)
153
-                    ->selectablePlaceholder(false)
154
-                    ->rule('required')
155
                     ->allowHtml()
132
                     ->allowHtml()
133
+                    ->softRequired()
134
+                    ->localizeLabel()
156
                     ->options(
135
                     ->options(
157
                         collect(Font::cases())
136
                         collect(Font::cases())
158
                             ->mapWithKeys(static fn ($case) => [
137
                             ->mapWithKeys(static fn ($case) => [
167
         return Section::make('Layout')
146
         return Section::make('Layout')
168
             ->schema([
147
             ->schema([
169
                 Select::make('max_content_width')
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
                     ->options(MaxContentWidth::class),
151
                     ->options(MaxContentWidth::class),
175
                 Select::make('modal_width')
152
                 Select::make('modal_width')
176
-                    ->label('Modal Width')
177
-                    ->native(false)
178
-                    ->selectablePlaceholder(false)
179
-                    ->rule('required')
153
+                    ->softRequired()
154
+                    ->localizeLabel()
180
                     ->options(ModalWidth::class),
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
                 ToggleButton::make('is_table_striped')
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
             ])->columns();
164
             ])->columns();
191
     }
165
     }
192
 
166
 
195
         return Section::make('Data Presentation')
169
         return Section::make('Data Presentation')
196
             ->schema([
170
             ->schema([
197
                 Select::make('table_sort_direction')
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
                     ->options(TableSortDirection::class),
174
                     ->options(TableSortDirection::class),
203
                 Select::make('records_per_page')
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
                     ->options(RecordsPerPage::class),
178
                     ->options(RecordsPerPage::class),
209
             ])->columns();
179
             ])->columns();
210
     }
180
     }
211
 
181
 
212
     protected function handleRecordUpdate(AppearanceModel $record, array $data): AppearanceModel
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
         $keysToWatch = [
186
         $keysToWatch = [
219
             'primary_color',
187
             'primary_color',
222
             'font',
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
         return $record;
199
         return $record;
234
     }
200
     }
246
     protected function getSaveFormAction(): Action
212
     protected function getSaveFormAction(): Action
247
     {
213
     {
248
         return Action::make('save')
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
             ->submit('save')
216
             ->submit('save')
251
             ->keyBindings(['mod+s']);
217
             ->keyBindings(['mod+s']);
252
     }
218
     }

+ 103
- 76
app/Filament/Company/Pages/Setting/CompanyDefault.php Прегледај датотеку

3
 namespace App\Filament\Company\Pages\Setting;
3
 namespace App\Filament\Company\Pages\Setting;
4
 
4
 
5
 use App\Events\CompanyDefaultUpdated;
5
 use App\Events\CompanyDefaultUpdated;
6
+use App\Models\Banking\Account;
6
 use App\Models\Setting\CompanyDefault as CompanyDefaultModel;
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
 use Filament\Forms\Form;
16
 use Filament\Forms\Form;
10
 use Filament\Notifications\Notification;
17
 use Filament\Notifications\Notification;
11
 use Filament\Pages\Concerns\InteractsWithFormActions;
18
 use Filament\Pages\Concerns\InteractsWithFormActions;
12
 use Filament\Pages\Page;
19
 use Filament\Pages\Page;
13
 use Filament\Support\Exceptions\Halt;
20
 use Filament\Support\Exceptions\Halt;
14
 use Illuminate\Auth\Access\AuthorizationException;
21
 use Illuminate\Auth\Access\AuthorizationException;
22
+use Illuminate\Contracts\Support\Htmlable;
15
 use Illuminate\Database\Eloquent\Model;
23
 use Illuminate\Database\Eloquent\Model;
24
+use Illuminate\Support\Facades\Blade;
16
 use Livewire\Attributes\Locked;
25
 use Livewire\Attributes\Locked;
17
 
26
 
18
 use function Filament\authorize;
27
 use function Filament\authorize;
26
 
35
 
27
     protected static ?string $navigationIcon = 'heroicon-o-adjustments-vertical';
36
     protected static ?string $navigationIcon = 'heroicon-o-adjustments-vertical';
28
 
37
 
29
-    protected static ?string $navigationLabel = 'Default';
30
-
31
     protected static ?string $navigationGroup = 'Settings';
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
     protected static string $view = 'filament.company.pages.setting.company-default';
44
     protected static string $view = 'filament.company.pages.setting.company-default';
38
 
45
 
46
+    /**
47
+     * @var array<string, mixed> | null
48
+     */
39
     public ?array $data = [];
49
     public ?array $data = [];
40
 
50
 
41
     #[Locked]
51
     #[Locked]
42
     public ?CompanyDefaultModel $record = null;
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
     public function mount(): void
64
     public function mount(): void
45
     {
65
     {
46
         $this->record = CompanyDefaultModel::firstOrNew([
66
         $this->record = CompanyDefaultModel::firstOrNew([
56
     {
76
     {
57
         $data = $this->record->attributesToArray();
77
         $data = $this->record->attributesToArray();
58
 
78
 
59
-        $data = $this->mutateFormDataBeforeFill($data);
60
-
61
         $this->form->fill($data);
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
     public function save(): void
82
     public function save(): void
75
     {
83
     {
76
         try {
84
         try {
77
             $data = $this->form->getState();
85
             $data = $this->form->getState();
78
 
86
 
79
-            $data = $this->mutateFormDataBeforeSave($data);
80
-
81
             $this->handleRecordUpdate($this->record, $data);
87
             $this->handleRecordUpdate($this->record, $data);
82
 
88
 
83
         } catch (Halt $exception) {
89
         } catch (Halt $exception) {
84
             return;
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
         return Notification::make()
98
         return Notification::make()
103
             ->success()
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
     public function form(Form $form): Form
103
     public function form(Form $form): Form
132
         return Section::make('General')
118
         return Section::make('General')
133
             ->schema([
119
             ->schema([
134
                 Select::make('account_id')
120
                 Select::make('account_id')
135
-                    ->label('Account')
121
+                    ->localizeLabel()
136
                     ->relationship('account', 'name')
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
                     ->saveRelationshipsUsing(null)
130
                     ->saveRelationshipsUsing(null)
138
                     ->selectablePlaceholder(false)
131
                     ->selectablePlaceholder(false)
139
                     ->searchable()
132
                     ->searchable()
140
                     ->preload(),
133
                     ->preload(),
141
                 Select::make('currency_code')
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
                     ->saveRelationshipsUsing(null)
139
                     ->saveRelationshipsUsing(null)
145
-                    ->selectablePlaceholder(false)
146
-                    ->rule('required')
147
                     ->searchable()
140
                     ->searchable()
148
                     ->preload(),
141
                     ->preload(),
149
             ])->columns();
142
             ])->columns();
154
         return Section::make('Taxes & Discounts')
147
         return Section::make('Taxes & Discounts')
155
             ->schema([
148
             ->schema([
156
                 Select::make('sales_tax_id')
149
                 Select::make('sales_tax_id')
157
-                    ->label('Sales Tax')
150
+                    ->softRequired()
151
+                    ->localizeLabel()
158
                     ->relationship('salesTax', 'name')
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
                     ->saveRelationshipsUsing(null)
163
                     ->saveRelationshipsUsing(null)
160
-                    ->selectablePlaceholder(false)
161
-                    ->rule('required')
162
-                    ->searchable()
163
-                    ->preload(),
164
+                    ->searchable(),
164
                 Select::make('purchase_tax_id')
165
                 Select::make('purchase_tax_id')
165
-                    ->label('Purchase Tax')
166
+                    ->softRequired()
167
+                    ->localizeLabel()
166
                     ->relationship('purchaseTax', 'name')
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
                     ->saveRelationshipsUsing(null)
179
                     ->saveRelationshipsUsing(null)
168
-                    ->selectablePlaceholder(false)
169
-                    ->rule('required')
170
-                    ->searchable()
171
-                    ->preload(),
180
+                    ->searchable(),
172
                 Select::make('sales_discount_id')
181
                 Select::make('sales_discount_id')
173
-                    ->label('Sales Discount')
182
+                    ->softRequired()
183
+                    ->localizeLabel()
174
                     ->relationship('salesDiscount', 'name')
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
                     ->saveRelationshipsUsing(null)
194
                     ->saveRelationshipsUsing(null)
176
-                    ->selectablePlaceholder(false)
177
-                    ->rule('required')
178
-                    ->searchable()
179
-                    ->preload(),
195
+                    ->allowHtml()
196
+                    ->searchable(),
180
                 Select::make('purchase_discount_id')
197
                 Select::make('purchase_discount_id')
181
-                    ->label('Purchase Discount')
198
+                    ->softRequired()
199
+                    ->localizeLabel()
182
                     ->relationship('purchaseDiscount', 'name')
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
                     ->saveRelationshipsUsing(null)
210
                     ->saveRelationshipsUsing(null)
184
-                    ->selectablePlaceholder(false)
185
-                    ->rule('required')
186
-                    ->searchable()
187
-                    ->preload(),
211
+                    ->searchable(),
188
             ])->columns();
212
             ])->columns();
189
     }
213
     }
190
 
214
 
193
         return Section::make('Categories')
217
         return Section::make('Categories')
194
             ->schema([
218
             ->schema([
195
                 Select::make('income_category_id')
219
                 Select::make('income_category_id')
196
-                    ->label('Income Category')
220
+                    ->softRequired()
221
+                    ->localizeLabel()
197
                     ->relationship('incomeCategory', 'name')
222
                     ->relationship('incomeCategory', 'name')
198
                     ->saveRelationshipsUsing(null)
223
                     ->saveRelationshipsUsing(null)
199
-                    ->selectablePlaceholder(false)
200
-                    ->rule('required')
201
-                    ->searchable()
224
+                    ->required()
202
                     ->preload(),
225
                     ->preload(),
203
                 Select::make('expense_category_id')
226
                 Select::make('expense_category_id')
204
-                    ->label('Expense Category')
227
+                    ->softRequired()
228
+                    ->localizeLabel()
205
                     ->relationship('expenseCategory', 'name')
229
                     ->relationship('expenseCategory', 'name')
206
                     ->saveRelationshipsUsing(null)
230
                     ->saveRelationshipsUsing(null)
207
-                    ->selectablePlaceholder(false)
208
-                    ->rule('required')
209
                     ->searchable()
231
                     ->searchable()
210
                     ->preload(),
232
                     ->preload(),
211
             ])->columns();
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
     protected function handleRecordUpdate(CompanyDefaultModel $record, array $data): CompanyDefaultModel
241
     protected function handleRecordUpdate(CompanyDefaultModel $record, array $data): CompanyDefaultModel
215
     {
242
     {
216
         CompanyDefaultUpdated::dispatch($record, $data);
243
         CompanyDefaultUpdated::dispatch($record, $data);
233
     protected function getSaveFormAction(): Action
260
     protected function getSaveFormAction(): Action
234
     {
261
     {
235
         return Action::make('save')
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
             ->submit('save')
264
             ->submit('save')
238
             ->keyBindings(['mod+s']);
265
             ->keyBindings(['mod+s']);
239
     }
266
     }

+ 72
- 103
app/Filament/Company/Pages/Setting/CompanyProfile.php Прегледај датотеку

3
 namespace App\Filament\Company\Pages\Setting;
3
 namespace App\Filament\Company\Pages\Setting;
4
 
4
 
5
 use App\Enums\EntityType;
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
 use App\Models\Setting\CompanyProfile as CompanyProfileModel;
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
 use Filament\Notifications\Notification;
21
 use Filament\Notifications\Notification;
12
 use Filament\Pages\Concerns\InteractsWithFormActions;
22
 use Filament\Pages\Concerns\InteractsWithFormActions;
13
 use Filament\Pages\Page;
23
 use Filament\Pages\Page;
14
 use Filament\Support\Exceptions\Halt;
24
 use Filament\Support\Exceptions\Halt;
15
 use Illuminate\Auth\Access\AuthorizationException;
25
 use Illuminate\Auth\Access\AuthorizationException;
26
+use Illuminate\Contracts\Support\Htmlable;
16
 use Illuminate\Database\Eloquent\Model;
27
 use Illuminate\Database\Eloquent\Model;
17
 use Illuminate\Support\Facades\Auth;
28
 use Illuminate\Support\Facades\Auth;
18
 use Livewire\Attributes\Locked;
29
 use Livewire\Attributes\Locked;
29
 
40
 
30
     protected static ?string $navigationIcon = 'heroicon-o-building-office-2';
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
     protected static ?string $navigationGroup = 'Settings';
45
     protected static ?string $navigationGroup = 'Settings';
35
 
46
 
36
     protected static ?string $slug = 'settings/company-profile';
47
     protected static ?string $slug = 'settings/company-profile';
37
 
48
 
38
-    protected ?string $heading = 'Company Profile';
39
-
40
     protected static string $view = 'filament.company.pages.setting.company-profile';
49
     protected static string $view = 'filament.company.pages.setting.company-profile';
41
 
50
 
42
     public ?array $data = [];
51
     public ?array $data = [];
44
     #[Locked]
53
     #[Locked]
45
     public ?CompanyProfileModel $record = null;
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
     public function mount(): void
66
     public function mount(): void
48
     {
67
     {
49
         $this->record = CompanyProfileModel::firstOrNew([
68
         $this->record = CompanyProfileModel::firstOrNew([
59
     {
78
     {
60
         $data = $this->record->attributesToArray();
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
         $this->form->fill($data);
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
     public function save(): void
84
     public function save(): void
81
     {
85
     {
82
         try {
86
         try {
83
             $data = $this->form->getState();
87
             $data = $this->form->getState();
84
 
88
 
85
-            $data = $this->mutateFormDataBeforeSave($data);
86
-
87
             $this->handleRecordUpdate($this->record, $data);
89
             $this->handleRecordUpdate($this->record, $data);
88
 
90
 
89
         } catch (Halt $exception) {
91
         } catch (Halt $exception) {
90
             return;
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
         return Notification::make()
100
         return Notification::make()
109
             ->success()
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
     public function form(Form $form): Form
105
     public function form(Form $form): Form
127
                 $this->getIdentificationSection(),
109
                 $this->getIdentificationSection(),
128
                 $this->getLocationDetailsSection(),
110
                 $this->getLocationDetailsSection(),
129
                 $this->getLegalAndComplianceSection(),
111
                 $this->getLegalAndComplianceSection(),
130
-                $this->getFiscalYearSection(),
131
             ])
112
             ])
132
             ->model($this->record)
113
             ->model($this->record)
133
             ->statePath('data')
114
             ->statePath('data')
138
     {
119
     {
139
         return Section::make('Identification')
120
         return Section::make('Identification')
140
             ->schema([
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
                 Group::make()
122
                 Group::make()
158
                     ->schema([
123
                     ->schema([
159
                         TextInput::make('email')
124
                         TextInput::make('email')
160
-                            ->label('Email')
161
                             ->email()
125
                             ->email()
126
+                            ->localizeLabel()
162
                             ->maxLength(255)
127
                             ->maxLength(255)
163
                             ->required(),
128
                             ->required(),
164
                         TextInput::make('phone_number')
129
                         TextInput::make('phone_number')
165
-                            ->label('Phone Number')
166
                             ->tel()
130
                             ->tel()
167
-                            ->nullable(),
131
+                            ->nullable()
132
+                            ->localizeLabel(),
168
                     ])->columns(1),
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
             ])->columns();
154
             ])->columns();
170
     }
155
     }
171
 
156
 
174
         return Section::make('Location Details')
159
         return Section::make('Location Details')
175
             ->schema([
160
             ->schema([
176
                 Select::make('country')
161
                 Select::make('country')
177
-                    ->label('Country')
178
-                    ->native(false)
179
-                    ->live()
180
                     ->searchable()
162
                     ->searchable()
163
+                    ->localizeLabel()
164
+                    ->live()
181
                     ->options(Country::getAvailableCountryOptions())
165
                     ->options(Country::getAvailableCountryOptions())
182
                     ->afterStateUpdated(static function (Set $set) {
166
                     ->afterStateUpdated(static function (Set $set) {
183
                         $set('state_id', null);
167
                         $set('state_id', null);
184
-                        $set('timezone', null);
185
                         $set('city_id', null);
168
                         $set('city_id', null);
186
                     })
169
                     })
187
                     ->required(),
170
                     ->required(),
188
                 Select::make('state_id')
171
                 Select::make('state_id')
189
-                    ->label('State / Province')
172
+                    ->localizeLabel('State / Province')
190
                     ->searchable()
173
                     ->searchable()
191
                     ->live()
174
                     ->live()
192
                     ->options(static fn (Get $get) => State::getStateOptions($get('country')))
175
                     ->options(static fn (Get $get) => State::getStateOptions($get('country')))
193
                     ->nullable(),
176
                     ->nullable(),
194
-                Select::make('timezone')
195
-                    ->label('Timezone')
196
-                    ->searchable()
197
-                    ->options(static fn (Get $get) => Timezone::getTimezoneOptions($get('country')))
198
-                    ->nullable(),
199
                 TextInput::make('address')
177
                 TextInput::make('address')
200
-                    ->label('Street Address')
178
+                    ->localizeLabel('Street Address')
201
                     ->maxLength(255)
179
                     ->maxLength(255)
202
                     ->nullable(),
180
                     ->nullable(),
203
                 Select::make('city_id')
181
                 Select::make('city_id')
204
-                    ->label('City / Town')
182
+                    ->localizeLabel('City / Town')
205
                     ->searchable()
183
                     ->searchable()
206
                     ->options(static fn (Get $get) => City::getCityOptions($get('country'), $get('state_id')))
184
                     ->options(static fn (Get $get) => City::getCityOptions($get('country'), $get('state_id')))
207
                     ->nullable(),
185
                     ->nullable(),
208
                 TextInput::make('zip_code')
186
                 TextInput::make('zip_code')
209
-                    ->label('Zip Code')
187
+                    ->localizeLabel('Zip / Postal Code')
210
                     ->maxLength(20)
188
                     ->maxLength(20)
211
                     ->nullable(),
189
                     ->nullable(),
212
             ])->columns();
190
             ])->columns();
217
         return Section::make('Legal & Compliance')
195
         return Section::make('Legal & Compliance')
218
             ->schema([
196
             ->schema([
219
                 Select::make('entity_type')
197
                 Select::make('entity_type')
220
-                    ->label('Entity Type')
221
-                    ->native(false)
198
+                    ->localizeLabel()
222
                     ->options(EntityType::class)
199
                     ->options(EntityType::class)
223
                     ->required(),
200
                     ->required(),
224
                 TextInput::make('tax_id')
201
                 TextInput::make('tax_id')
225
-                    ->label('Tax ID')
202
+                    ->localizeLabel('Tax ID')
226
                     ->maxLength(50)
203
                     ->maxLength(50)
227
                     ->nullable(),
204
                     ->nullable(),
228
             ])->columns();
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
     protected function handleRecordUpdate(CompanyProfileModel $record, array $data): CompanyProfileModel
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
         return $record;
222
         return $record;
254
     }
223
     }
266
     protected function getSaveFormAction(): Action
235
     protected function getSaveFormAction(): Action
267
     {
236
     {
268
         return Action::make('save')
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
             ->submit('save')
239
             ->submit('save')
271
             ->keyBindings(['mod+s']);
240
             ->keyBindings(['mod+s']);
272
     }
241
     }

+ 86
- 108
app/Filament/Company/Pages/Setting/Invoice.php Прегледај датотеку

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

+ 239
- 0
app/Filament/Company/Pages/Setting/Localization.php Прегледај датотеку

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

+ 0
- 12
app/Filament/Company/Resources/Banking/AccountResource/Pages/EditAccount.php Прегледај датотеку

4
 
4
 
5
 use App\Filament\Company\Resources\Banking\AccountResource;
5
 use App\Filament\Company\Resources\Banking\AccountResource;
6
 use App\Models\Banking\Account;
6
 use App\Models\Banking\Account;
7
-use App\Models\Setting\Currency;
8
 use App\Traits\HandlesResourceRecordUpdate;
7
 use App\Traits\HandlesResourceRecordUpdate;
9
 use Filament\Actions;
8
 use Filament\Actions;
10
 use Filament\Resources\Pages\EditRecord;
9
 use Filament\Resources\Pages\EditRecord;
48
             throw new Halt('No authenticated user found.');
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
         return $this->handleRecordUpdateWithUniqueField($record, $data, $user);
50
         return $this->handleRecordUpdateWithUniqueField($record, $data, $user);
63
     }
51
     }
64
 }
52
 }

+ 37
- 9
app/Filament/Company/Resources/Core/DepartmentResource.php Прегледај датотеку

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

+ 25
- 0
app/Filament/Company/Resources/Core/DepartmentResource/Pages/ListDepartments.php Прегледај датотеку

3
 namespace App\Filament\Company\Resources\Core\DepartmentResource\Pages;
3
 namespace App\Filament\Company\Resources\Core\DepartmentResource\Pages;
4
 
4
 
5
 use App\Filament\Company\Resources\Core\DepartmentResource;
5
 use App\Filament\Company\Resources\Core\DepartmentResource;
6
+use App\Models\Core\Department;
6
 use Filament\Actions;
7
 use Filament\Actions;
8
+use Filament\Resources\Components\Tab;
7
 use Filament\Resources\Pages\ListRecords;
9
 use Filament\Resources\Pages\ListRecords;
8
 
10
 
9
 class ListDepartments extends ListRecords
11
 class ListDepartments extends ListRecords
16
             Actions\CreateAction::make(),
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 Прегледај датотеку

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
 use App\Enums\CategoryType;
5
 use App\Enums\CategoryType;
6
 use App\Filament\Company\Resources\Setting\CategoryResource\Pages;
6
 use App\Filament\Company\Resources\Setting\CategoryResource\Pages;
7
 use App\Models\Setting\Category;
7
 use App\Models\Setting\Category;
8
+use App\Traits\NotifiesOnDelete;
8
 use Closure;
9
 use Closure;
9
 use Exception;
10
 use Exception;
11
+use Filament\Forms;
10
 use Filament\Forms\Form;
12
 use Filament\Forms\Form;
11
-use Filament\Notifications\Notification;
12
 use Filament\Resources\Resource;
13
 use Filament\Resources\Resource;
14
+use Filament\Support\Enums\FontWeight;
15
+use Filament\Tables;
13
 use Filament\Tables\Table;
16
 use Filament\Tables\Table;
14
-use Filament\{Forms, Tables};
15
-use Illuminate\Database\Eloquent\Collection;
16
 use Wallo\FilamentSelectify\Components\ToggleButton;
17
 use Wallo\FilamentSelectify\Components\ToggleButton;
17
 
18
 
18
 class CategoryResource extends Resource
19
 class CategoryResource extends Resource
19
 {
20
 {
21
+    use NotifiesOnDelete;
22
+
20
     protected static ?string $model = Category::class;
23
     protected static ?string $model = Category::class;
21
 
24
 
25
+    protected static ?string $modelLabel = 'Category';
26
+
22
     protected static ?string $navigationIcon = 'heroicon-o-folder';
27
     protected static ?string $navigationIcon = 'heroicon-o-folder';
23
 
28
 
24
     protected static ?string $navigationGroup = 'Settings';
29
     protected static ?string $navigationGroup = 'Settings';
25
 
30
 
26
     protected static ?string $slug = 'settings/categories';
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
     public static function form(Form $form): Form
40
     public static function form(Form $form): Form
29
     {
41
     {
30
         return $form
42
         return $form
32
                 Forms\Components\Section::make('General')
44
                 Forms\Components\Section::make('General')
33
                     ->schema([
45
                     ->schema([
34
                         Forms\Components\TextInput::make('name')
46
                         Forms\Components\TextInput::make('name')
35
-                            ->label('Name')
47
+                            ->localizeLabel()
36
                             ->autofocus()
48
                             ->autofocus()
37
                             ->required()
49
                             ->required()
38
                             ->maxLength(255)
50
                             ->maxLength(255)
44
                                         ->first();
56
                                         ->first();
45
 
57
 
46
                                     if ($existingCategory && $existingCategory->getKey() !== $component->getRecord()?->getKey()) {
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
                         Forms\Components\Select::make('type')
69
                         Forms\Components\Select::make('type')
70
+                            ->localizeLabel()
53
                             ->options(CategoryType::class)
71
                             ->options(CategoryType::class)
54
-                            ->required()
55
-                            ->native(false)
56
-                            ->label('Type'),
72
+                            ->required(),
57
                         Forms\Components\ColorPicker::make('color')
73
                         Forms\Components\ColorPicker::make('color')
58
-                            ->required()
59
-                            ->label('Color'),
74
+                            ->localizeLabel()
75
+                            ->required(),
60
                         ToggleButton::make('enabled')
76
                         ToggleButton::make('enabled')
61
-                            ->label('Default'),
77
+                            ->localizeLabel('Default')
78
+                            ->onLabel(Category::enabledLabel())
79
+                            ->offLabel(Category::disabledLabel()),
62
                     ])->columns(),
80
                     ])->columns(),
63
             ]);
81
             ]);
64
     }
82
     }
71
         return $table
89
         return $table
72
             ->columns([
90
             ->columns([
73
                 Tables\Columns\TextColumn::make('name')
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
                     ->iconPosition('after')
103
                     ->iconPosition('after')
79
                     ->searchable()
104
                     ->searchable()
80
                     ->sortable(),
105
                     ->sortable(),
81
                 Tables\Columns\TextColumn::make('type')
106
                 Tables\Columns\TextColumn::make('type')
82
-                    ->label('Type')
107
+                    ->localizeLabel()
83
                     ->sortable()
108
                     ->sortable()
84
                     ->searchable(),
109
                     ->searchable(),
85
                 Tables\Columns\ColorColumn::make('color')
110
                 Tables\Columns\ColorColumn::make('color')
86
-                    ->label('Color')
87
-                    ->copyable()
88
-                    ->copyMessage('Color code copied'),
111
+                    ->localizeLabel()
112
+                    ->copyable(),
89
             ])
113
             ])
90
             ->filters([
114
             ->filters([
91
                 Tables\Filters\SelectFilter::make('type')
115
                 Tables\Filters\SelectFilter::make('type')
95
             ])
119
             ])
96
             ->actions([
120
             ->actions([
97
                 Tables\Actions\EditAction::make(),
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
             ->bulkActions([
124
             ->bulkActions([
113
                 Tables\Actions\BulkActionGroup::make([
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
             ->emptyStateActions([
132
             ->emptyStateActions([
143
                 Tables\Actions\CreateAction::make(),
133
                 Tables\Actions\CreateAction::make(),
144
             ]);
134
             ]);

+ 4
- 1
app/Filament/Company/Resources/Setting/CategoryResource/Pages/CreateCategory.php Прегледај датотеку

2
 
2
 
3
 namespace App\Filament\Company\Resources\Setting\CategoryResource\Pages;
3
 namespace App\Filament\Company\Resources\Setting\CategoryResource\Pages;
4
 
4
 
5
+use App\Enums\CategoryType;
5
 use App\Filament\Company\Resources\Setting\CategoryResource;
6
 use App\Filament\Company\Resources\Setting\CategoryResource;
6
 use App\Models\Setting\Category;
7
 use App\Models\Setting\Category;
7
 use App\Traits\HandlesResourceRecordCreation;
8
 use App\Traits\HandlesResourceRecordCreation;
38
             throw new Halt('No authenticated user found');
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
 
2
 
3
 namespace App\Filament\Company\Resources\Setting\CategoryResource\Pages;
3
 namespace App\Filament\Company\Resources\Setting\CategoryResource\Pages;
4
 
4
 
5
+use App\Enums\CategoryType;
5
 use App\Filament\Company\Resources\Setting\CategoryResource;
6
 use App\Filament\Company\Resources\Setting\CategoryResource;
6
 use App\Traits\HandlesResourceRecordUpdate;
7
 use App\Traits\HandlesResourceRecordUpdate;
7
 use Filament\Actions;
8
 use Filament\Actions;
45
             throw new Halt('No authenticated user found');
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
 
2
 
3
 namespace App\Filament\Company\Resources\Setting;
3
 namespace App\Filament\Company\Resources\Setting;
4
 
4
 
5
+use App\Facades\Forex;
5
 use App\Filament\Company\Resources\Setting\CurrencyResource\Pages;
6
 use App\Filament\Company\Resources\Setting\CurrencyResource\Pages;
6
 use App\Models\Banking\Account;
7
 use App\Models\Banking\Account;
7
 use App\Models\Setting\Currency;
8
 use App\Models\Setting\Currency;
8
-use App\Services\CurrencyService;
9
+use App\Models\Setting\Currency as CurrencyModel;
9
 use App\Traits\ChecksForeignKeyConstraints;
10
 use App\Traits\ChecksForeignKeyConstraints;
11
+use App\Traits\NotifiesOnDelete;
12
+use App\Utilities\Currency\CurrencyAccessor;
10
 use Closure;
13
 use Closure;
14
+use Filament\Forms;
11
 use Filament\Forms\Form;
15
 use Filament\Forms\Form;
12
-use Filament\Notifications\Notification;
13
 use Filament\Resources\Resource;
16
 use Filament\Resources\Resource;
17
+use Filament\Support\Enums\FontWeight;
18
+use Filament\Tables;
14
 use Filament\Tables\Table;
19
 use Filament\Tables\Table;
15
-use Filament\{Forms, Tables};
16
 use Illuminate\Database\Eloquent\Collection;
20
 use Illuminate\Database\Eloquent\Collection;
17
 use Wallo\FilamentSelectify\Components\ToggleButton;
21
 use Wallo\FilamentSelectify\Components\ToggleButton;
18
 
22
 
19
 class CurrencyResource extends Resource
23
 class CurrencyResource extends Resource
20
 {
24
 {
21
     use ChecksForeignKeyConstraints;
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
     protected static ?string $navigationIcon = 'heroicon-o-currency-dollar';
32
     protected static ?string $navigationIcon = 'heroicon-o-currency-dollar';
26
 
33
 
28
 
35
 
29
     protected static ?string $slug = 'settings/currencies';
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
     public static function form(Form $form): Form
45
     public static function form(Form $form): Form
32
     {
46
     {
33
         return $form
47
         return $form
35
                 Forms\Components\Section::make('General')
49
                 Forms\Components\Section::make('General')
36
                     ->schema([
50
                     ->schema([
37
                         Forms\Components\Select::make('code')
51
                         Forms\Components\Select::make('code')
38
-                            ->label('Code')
39
-                            ->options(Currency::getAvailableCurrencyCodes())
52
+                            ->options(CurrencyAccessor::getAvailableCurrencies())
40
                             ->searchable()
53
                             ->searchable()
41
-                            ->placeholder('Select a currency code...')
42
                             ->live()
54
                             ->live()
43
                             ->required()
55
                             ->required()
56
+                            ->localizeLabel()
44
                             ->hidden(static fn (Forms\Get $get, $state): bool => $get('enabled') && $state !== null)
57
                             ->hidden(static fn (Forms\Get $get, $state): bool => $get('enabled') && $state !== null)
45
                             ->afterStateUpdated(static function (Forms\Set $set, $state) {
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
                                 if ($state === null) {
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
                                     return;
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
                         Forms\Components\TextInput::make('code')
77
                         Forms\Components\TextInput::make('code')
69
-                            ->label('Code')
78
+                            ->localizeLabel()
70
                             ->hidden(static fn (Forms\Get $get): bool => ! ($get('enabled') && $get('code') !== null))
79
                             ->hidden(static fn (Forms\Get $get): bool => ! ($get('enabled') && $get('code') !== null))
71
                             ->disabled(static fn (Forms\Get $get): bool => $get('enabled'))
80
                             ->disabled(static fn (Forms\Get $get): bool => $get('enabled'))
72
                             ->dehydrated()
81
                             ->dehydrated()
73
                             ->required(),
82
                             ->required(),
74
                         Forms\Components\TextInput::make('name')
83
                         Forms\Components\TextInput::make('name')
75
-                            ->label('Name')
84
+                            ->localizeLabel()
76
                             ->maxLength(50)
85
                             ->maxLength(50)
77
                             ->required(),
86
                             ->required(),
78
                         Forms\Components\TextInput::make('rate')
87
                         Forms\Components\TextInput::make('rate')
79
-                            ->label('Rate')
80
                             ->numeric()
88
                             ->numeric()
81
                             ->rule('gt:0')
89
                             ->rule('gt:0')
82
                             ->live()
90
                             ->live()
91
+                            ->localizeLabel()
92
+                            ->disabled(static fn (?CurrencyModel $record): bool => $record?->isEnabled() ?? false)
93
+                            ->dehydrated()
83
                             ->required(),
94
                             ->required(),
84
                         Forms\Components\Select::make('precision')
95
                         Forms\Components\Select::make('precision')
85
-                            ->label('Precision')
86
-                            ->native(false)
87
-                            ->selectablePlaceholder(false)
88
-                            ->placeholder('Select a precision...')
96
+                            ->localizeLabel()
89
                             ->options(['0', '1', '2', '3', '4'])
97
                             ->options(['0', '1', '2', '3', '4'])
90
                             ->required(),
98
                             ->required(),
91
                         Forms\Components\TextInput::make('symbol')
99
                         Forms\Components\TextInput::make('symbol')
92
-                            ->label('Symbol')
100
+                            ->localizeLabel()
93
                             ->maxLength(5)
101
                             ->maxLength(5)
94
                             ->required(),
102
                             ->required(),
95
                         Forms\Components\Select::make('symbol_first')
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
                             ->required(),
106
                             ->required(),
102
                         Forms\Components\TextInput::make('decimal_mark')
107
                         Forms\Components\TextInput::make('decimal_mark')
103
-                            ->label('Decimal Separator')
108
+                            ->localizeLabel('Decimal Separator')
104
                             ->maxLength(1)
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
                             ->required(),
117
                             ->required(),
106
                         Forms\Components\TextInput::make('thousands_separator')
118
                         Forms\Components\TextInput::make('thousands_separator')
107
-                            ->label('Thousands Separator')
119
+                            ->localizeLabel()
108
                             ->maxLength(1)
120
                             ->maxLength(1)
109
                             ->rule(static function (Forms\Get $get): Closure {
121
                             ->rule(static function (Forms\Get $get): Closure {
110
                                 return static function ($attribute, $value, Closure $fail) use ($get) {
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
                             ->nullable(),
128
                             ->nullable(),
119
                         ToggleButton::make('enabled')
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
                             ->live()
135
                             ->live()
122
-                            ->offColor('danger')
123
-                            ->onColor('primary')
124
                             ->afterStateUpdated(static function (Forms\Set $set, Forms\Get $get, $state) {
136
                             ->afterStateUpdated(static function (Forms\Set $set, Forms\Get $get, $state) {
125
                                 $enabledState = (bool) $state;
137
                                 $enabledState = (bool) $state;
126
                                 $code = $get('code');
138
                                 $code = $get('code');
127
 
139
 
128
-                                $defaultCurrencyCode = Currency::getDefaultCurrencyCode();
129
-                                $currencyService = app(CurrencyService::class);
140
+                                if (! $code) {
141
+                                    return;
142
+                                }
130
 
143
 
131
                                 if ($enabledState) {
144
                                 if ($enabledState) {
132
                                     $set('rate', 1);
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
                     ])->columns(),
159
                     ])->columns(),
149
         return $table
165
         return $table
150
             ->columns([
166
             ->columns([
151
                 Tables\Columns\TextColumn::make('name')
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
                     ->iconPosition('after')
178
                     ->iconPosition('after')
157
                     ->searchable()
179
                     ->searchable()
158
                     ->sortable(),
180
                     ->sortable(),
159
                 Tables\Columns\TextColumn::make('code')
181
                 Tables\Columns\TextColumn::make('code')
160
-                    ->label('Code')
182
+                    ->localizeLabel()
161
                     ->searchable()
183
                     ->searchable()
162
                     ->sortable(),
184
                     ->sortable(),
163
                 Tables\Columns\TextColumn::make('symbol')
185
                 Tables\Columns\TextColumn::make('symbol')
164
-                    ->label('Symbol')
186
+                    ->localizeLabel()
165
                     ->searchable()
187
                     ->searchable()
166
                     ->sortable(),
188
                     ->sortable(),
167
                 Tables\Columns\TextColumn::make('rate')
189
                 Tables\Columns\TextColumn::make('rate')
168
-                    ->label('Rate')
190
+                    ->localizeLabel()
169
                     ->searchable()
191
                     ->searchable()
170
                     ->sortable(),
192
                     ->sortable(),
171
             ])
193
             ])
175
             ->actions([
197
             ->actions([
176
                 Tables\Actions\EditAction::make(),
198
                 Tables\Actions\EditAction::make(),
177
                 Tables\Actions\DeleteAction::make()
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
                         $modelsToCheck = [
201
                         $modelsToCheck = [
181
                             Account::class,
202
                             Account::class,
182
                         ];
203
                         ];
183
 
204
 
184
                         $isUsed = self::isForeignKeyUsed('currency_code', $record->code, $modelsToCheck);
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
                             $action->cancel();
210
                             $action->cancel();
204
                         }
211
                         }
205
                     }),
212
                     }),
209
                     Tables\Actions\DeleteBulkAction::make()
216
                     Tables\Actions\DeleteBulkAction::make()
210
                         ->before(static function (Tables\Actions\DeleteBulkAction $action, Collection $records) {
217
                         ->before(static function (Tables\Actions\DeleteBulkAction $action, Collection $records) {
211
                             foreach ($records as $record) {
218
                             foreach ($records as $record) {
212
-                                $defaultCurrency = $record->enabled;
213
                                 $modelsToCheck = [
219
                                 $modelsToCheck = [
214
                                     Account::class,
220
                                     Account::class,
215
                                 ];
221
                                 ];
216
 
222
 
217
                                 $isUsed = self::isForeignKeyUsed('currency_code', $record->code, $modelsToCheck);
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
                                     $action->cancel();
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
             ->emptyStateActions([
237
             ->emptyStateActions([
243
                 Tables\Actions\CreateAction::make(),
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
     public static function getPages(): array
242
     public static function getPages(): array
255
     {
243
     {
256
         return [
244
         return [

+ 0
- 5
app/Filament/Company/Resources/Setting/CurrencyResource/Pages/EditCurrency.php Прегледај датотеку

2
 
2
 
3
 namespace App\Filament\Company\Resources\Setting\CurrencyResource\Pages;
3
 namespace App\Filament\Company\Resources\Setting\CurrencyResource\Pages;
4
 
4
 
5
-use App\Events\DefaultCurrencyChanged;
6
 use App\Filament\Company\Resources\Setting\CurrencyResource;
5
 use App\Filament\Company\Resources\Setting\CurrencyResource;
7
 use App\Models\Setting\Currency;
6
 use App\Models\Setting\Currency;
8
 use App\Traits\HandlesResourceRecordUpdate;
7
 use App\Traits\HandlesResourceRecordUpdate;
48
             throw new Halt('No authenticated user found');
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
         return $this->handleRecordUpdateWithUniqueField($record, $data, $user);
50
         return $this->handleRecordUpdateWithUniqueField($record, $data, $user);
56
     }
51
     }
57
 }
52
 }

+ 75
- 49
app/Filament/Company/Resources/Setting/DiscountResource.php Прегледај датотеку

2
 
2
 
3
 namespace App\Filament\Company\Resources\Setting;
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
 use App\Filament\Company\Resources\Setting\DiscountResource\Pages;
10
 use App\Filament\Company\Resources\Setting\DiscountResource\Pages;
7
 use App\Models\Setting\Discount;
11
 use App\Models\Setting\Discount;
12
+use App\Models\Setting\Localization;
13
+use App\Traits\NotifiesOnDelete;
8
 use Closure;
14
 use Closure;
15
+use Filament\Forms;
9
 use Filament\Forms\Form;
16
 use Filament\Forms\Form;
10
 use Filament\Resources\Resource;
17
 use Filament\Resources\Resource;
18
+use Filament\Support\Enums\FontWeight;
19
+use Filament\Tables;
11
 use Filament\Tables\Table;
20
 use Filament\Tables\Table;
12
-use Filament\{Forms, Tables};
13
 use Wallo\FilamentSelectify\Components\ToggleButton;
21
 use Wallo\FilamentSelectify\Components\ToggleButton;
14
 
22
 
15
 class DiscountResource extends Resource
23
 class DiscountResource extends Resource
16
 {
24
 {
25
+    use NotifiesOnDelete;
26
+
17
     protected static ?string $model = Discount::class;
27
     protected static ?string $model = Discount::class;
18
 
28
 
29
+    protected static ?string $modelLabel = 'Discount';
30
+
19
     protected static ?string $navigationIcon = 'heroicon-o-tag';
31
     protected static ?string $navigationIcon = 'heroicon-o-tag';
20
 
32
 
21
     protected static ?string $navigationGroup = 'Settings';
33
     protected static ?string $navigationGroup = 'Settings';
22
 
34
 
23
     protected static ?string $slug = 'settings/discounts';
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
     public static function form(Form $form): Form
44
     public static function form(Form $form): Form
26
     {
45
     {
27
         return $form
46
         return $form
29
                 Forms\Components\Section::make('General')
48
                 Forms\Components\Section::make('General')
30
                     ->schema([
49
                     ->schema([
31
                         Forms\Components\TextInput::make('name')
50
                         Forms\Components\TextInput::make('name')
32
-                            ->label('Name')
33
                             ->autofocus()
51
                             ->autofocus()
34
                             ->required()
52
                             ->required()
53
+                            ->localizeLabel()
35
                             ->maxLength(255)
54
                             ->maxLength(255)
36
                             ->rule(static function (Forms\Get $get, Forms\Components\Component $component): Closure {
55
                             ->rule(static function (Forms\Get $get, Forms\Components\Component $component): Closure {
37
                                 return static function (string $attribute, $value, Closure $fail) use ($get, $component) {
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
                                         ->where('name', $value)
58
                                         ->where('name', $value)
40
                                         ->where('type', $get('type'))
59
                                         ->where('type', $get('type'))
41
                                         ->first();
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
                         Forms\Components\TextInput::make('description')
73
                         Forms\Components\TextInput::make('description')
50
-                            ->label('Description'),
74
+                            ->localizeLabel(),
51
                         Forms\Components\Select::make('computation')
75
                         Forms\Components\Select::make('computation')
52
-                            ->label('Computation')
76
+                            ->localizeLabel()
53
                             ->options(DiscountComputation::class)
77
                             ->options(DiscountComputation::class)
54
                             ->default(DiscountComputation::Percentage)
78
                             ->default(DiscountComputation::Percentage)
55
                             ->live()
79
                             ->live()
56
-                            ->native(false)
57
                             ->required(),
80
                             ->required(),
58
                         Forms\Components\TextInput::make('rate')
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
                             ->required(),
84
                             ->required(),
71
                         Forms\Components\Select::make('type')
85
                         Forms\Components\Select::make('type')
72
-                            ->label('Type')
86
+                            ->localizeLabel()
73
                             ->options(DiscountType::class)
87
                             ->options(DiscountType::class)
74
                             ->default(DiscountType::Sales)
88
                             ->default(DiscountType::Sales)
75
-                            ->native(false)
76
                             ->required(),
89
                             ->required(),
77
                         Forms\Components\Select::make('scope')
90
                         Forms\Components\Select::make('scope')
78
-                            ->label('Scope')
91
+                            ->localizeLabel()
79
                             ->options(DiscountScope::class)
92
                             ->options(DiscountScope::class)
80
-                            ->native(false),
93
+                            ->nullable(),
81
                         Forms\Components\DateTimePicker::make('start_date')
94
                         Forms\Components\DateTimePicker::make('start_date')
82
-                            ->label('Start Date')
83
-                            ->native(false)
95
+                            ->localizeLabel()
84
                             ->minDate(static function ($context, ?Discount $record = null) {
96
                             ->minDate(static function ($context, ?Discount $record = null) {
85
                                 if ($context === 'create') {
97
                                 if ($context === 'create') {
86
                                     return today()->addDay();
98
                                     return today()->addDay();
100
                             ->disabled(static fn ($context, ?Discount $record = null) => $context === 'edit' && $record?->start_date?->isPast() ?? false)
112
                             ->disabled(static fn ($context, ?Discount $record = null) => $context === 'edit' && $record?->start_date?->isPast() ?? false)
101
                             ->helperText(static fn (Forms\Components\DateTimePicker $component) => $component->isDisabled() ? 'Start date cannot be changed after the discount has begun.' : null),
113
                             ->helperText(static fn (Forms\Components\DateTimePicker $component) => $component->isDisabled() ? 'Start date cannot be changed after the discount has begun.' : null),
102
                         Forms\Components\DateTimePicker::make('end_date')
114
                         Forms\Components\DateTimePicker::make('end_date')
103
-                            ->label('End Date')
104
-                            ->native(false)
105
                             ->live()
115
                             ->live()
116
+                            ->localizeLabel()
106
                             ->minDate(static function (callable $get, ?Discount $record = null) {
117
                             ->minDate(static function (callable $get, ?Discount $record = null) {
107
                                 $start_date = $get('start_date') ?? $record?->start_date;
118
                                 $start_date = $get('start_date') ?? $record?->start_date;
108
 
119
 
113
                             ->displayFormat('F d, Y H:i')
124
                             ->displayFormat('F d, Y H:i')
114
                             ->seconds(false),
125
                             ->seconds(false),
115
                         ToggleButton::make('enabled')
126
                         ToggleButton::make('enabled')
116
-                            ->label('Default'),
127
+                            ->localizeLabel('Default')
128
+                            ->onLabel(Discount::enabledLabel())
129
+                            ->offLabel(Discount::disabledLabel()),
117
                     ])->columns(),
130
                     ])->columns(),
118
             ]);
131
             ]);
119
     }
132
     }
123
         return $table
136
         return $table
124
             ->columns([
137
             ->columns([
125
                 Tables\Columns\TextColumn::make('name')
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
                     ->iconPosition('after')
150
                     ->iconPosition('after')
131
                     ->searchable()
151
                     ->searchable()
132
                     ->sortable(),
152
                     ->sortable(),
133
                 Tables\Columns\TextColumn::make('computation')
153
                 Tables\Columns\TextColumn::make('computation')
134
-                    ->label('Computation')
154
+                    ->localizeLabel()
135
                     ->searchable()
155
                     ->searchable()
136
                     ->sortable(),
156
                     ->sortable(),
137
                 Tables\Columns\TextColumn::make('rate')
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
                     ->searchable()
160
                     ->searchable()
141
                     ->sortable(),
161
                     ->sortable(),
142
                 Tables\Columns\TextColumn::make('type')
162
                 Tables\Columns\TextColumn::make('type')
143
-                    ->label('Type')
163
+                    ->localizeLabel()
144
                     ->badge()
164
                     ->badge()
145
                     ->searchable()
165
                     ->searchable()
146
                     ->sortable(),
166
                     ->sortable(),
147
                 Tables\Columns\TextColumn::make('start_date')
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
                     ->searchable()
175
                     ->searchable()
151
                     ->sortable(),
176
                     ->sortable(),
152
                 Tables\Columns\TextColumn::make('end_date')
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
                     ->color(static fn (Discount $record) => $record->end_date?->isPast() ? 'danger' : null)
185
                     ->color(static fn (Discount $record) => $record->end_date?->isPast() ? 'danger' : null)
156
                     ->searchable()
186
                     ->searchable()
157
                     ->sortable(),
187
                     ->sortable(),
167
                     Tables\Actions\DeleteBulkAction::make(),
197
                     Tables\Actions\DeleteBulkAction::make(),
168
                 ]),
198
                 ]),
169
             ])
199
             ])
200
+            ->checkIfRecordIsSelectableUsing(static function (Discount $record) {
201
+                return $record->isDisabled();
202
+            })
170
             ->emptyStateActions([
203
             ->emptyStateActions([
171
                 Tables\Actions\CreateAction::make(),
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
     public static function getPages(): array
208
     public static function getPages(): array
183
     {
209
     {
184
         return [
210
         return [

+ 4
- 1
app/Filament/Company/Resources/Setting/DiscountResource/Pages/CreateDiscount.php Прегледај датотеку

2
 
2
 
3
 namespace App\Filament\Company\Resources\Setting\DiscountResource\Pages;
3
 namespace App\Filament\Company\Resources\Setting\DiscountResource\Pages;
4
 
4
 
5
+use App\Enums\DiscountType;
5
 use App\Filament\Company\Resources\Setting\DiscountResource;
6
 use App\Filament\Company\Resources\Setting\DiscountResource;
6
 use App\Models\Setting\Discount;
7
 use App\Models\Setting\Discount;
7
 use App\Traits\HandlesResourceRecordCreation;
8
 use App\Traits\HandlesResourceRecordCreation;
38
             throw new Halt('No authenticated user found');
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
 
2
 
3
 namespace App\Filament\Company\Resources\Setting\DiscountResource\Pages;
3
 namespace App\Filament\Company\Resources\Setting\DiscountResource\Pages;
4
 
4
 
5
+use App\Enums\DiscountType;
5
 use App\Filament\Company\Resources\Setting\DiscountResource;
6
 use App\Filament\Company\Resources\Setting\DiscountResource;
6
 use App\Traits\HandlesResourceRecordUpdate;
7
 use App\Traits\HandlesResourceRecordUpdate;
7
 use Filament\Actions;
8
 use Filament\Actions;
45
             throw new Halt('No authenticated user found');
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
 
2
 
3
 namespace App\Filament\Company\Resources\Setting;
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
 use App\Filament\Company\Resources\Setting\TaxResource\Pages;
8
 use App\Filament\Company\Resources\Setting\TaxResource\Pages;
7
 use App\Models\Setting\Tax;
9
 use App\Models\Setting\Tax;
10
+use App\Traits\NotifiesOnDelete;
8
 use Closure;
11
 use Closure;
12
+use Filament\Forms;
9
 use Filament\Forms\Form;
13
 use Filament\Forms\Form;
10
-use Filament\Notifications\Notification;
11
 use Filament\Resources\Resource;
14
 use Filament\Resources\Resource;
15
+use Filament\Support\Enums\FontWeight;
16
+use Filament\Tables;
12
 use Filament\Tables\Table;
17
 use Filament\Tables\Table;
13
-use Filament\{Forms, Tables};
14
-use Illuminate\Database\Eloquent\Collection;
15
 use Wallo\FilamentSelectify\Components\ToggleButton;
18
 use Wallo\FilamentSelectify\Components\ToggleButton;
16
 
19
 
17
 class TaxResource extends Resource
20
 class TaxResource extends Resource
18
 {
21
 {
22
+    use NotifiesOnDelete;
23
+
19
     protected static ?string $model = Tax::class;
24
     protected static ?string $model = Tax::class;
20
 
25
 
26
+    protected static ?string $modelLabel = 'Tax';
27
+
21
     protected static ?string $navigationIcon = 'heroicon-o-receipt-percent';
28
     protected static ?string $navigationIcon = 'heroicon-o-receipt-percent';
22
 
29
 
23
     protected static ?string $navigationGroup = 'Settings';
30
     protected static ?string $navigationGroup = 'Settings';
24
 
31
 
25
     protected static ?string $slug = 'settings/taxes';
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
     public static function form(Form $form): Form
41
     public static function form(Form $form): Form
28
     {
42
     {
29
         return $form
43
         return $form
31
                 Forms\Components\Section::make('General')
45
                 Forms\Components\Section::make('General')
32
                     ->schema([
46
                     ->schema([
33
                         Forms\Components\TextInput::make('name')
47
                         Forms\Components\TextInput::make('name')
34
-                            ->label('Name')
35
                             ->autofocus()
48
                             ->autofocus()
36
                             ->required()
49
                             ->required()
50
+                            ->localizeLabel()
37
                             ->maxLength(255)
51
                             ->maxLength(255)
38
                             ->rule(static function (Forms\Get $get, Forms\Components\Component $component): Closure {
52
                             ->rule(static function (Forms\Get $get, Forms\Components\Component $component): Closure {
39
                                 return static function (string $attribute, $value, Closure $fail) use ($get, $component) {
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
                                         ->where('name', $value)
55
                                         ->where('name', $value)
42
                                         ->where('type', $get('type'))
56
                                         ->where('type', $get('type'))
43
                                         ->first();
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
                         Forms\Components\Select::make('computation')
71
                         Forms\Components\Select::make('computation')
54
-                            ->label('Computation')
72
+                            ->localizeLabel()
55
                             ->options(TaxComputation::class)
73
                             ->options(TaxComputation::class)
56
                             ->default(TaxComputation::Percentage)
74
                             ->default(TaxComputation::Percentage)
57
                             ->live()
75
                             ->live()
58
-                            ->native(false)
59
                             ->required(),
76
                             ->required(),
60
                         Forms\Components\TextInput::make('rate')
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
                             ->required(),
80
                             ->required(),
73
                         Forms\Components\Select::make('type')
81
                         Forms\Components\Select::make('type')
74
-                            ->label('Type')
82
+                            ->localizeLabel()
75
                             ->options(TaxType::class)
83
                             ->options(TaxType::class)
76
                             ->default(TaxType::Sales)
84
                             ->default(TaxType::Sales)
77
-                            ->native(false)
78
                             ->required(),
85
                             ->required(),
79
                         Forms\Components\Select::make('scope')
86
                         Forms\Components\Select::make('scope')
80
-                            ->label('Scope')
81
-                            ->options(TaxScope::class)
82
-                            ->native(false),
87
+                            ->localizeLabel()
88
+                            ->options(TaxScope::class),
83
                         ToggleButton::make('enabled')
89
                         ToggleButton::make('enabled')
84
-                            ->label('Enabled'),
90
+                            ->localizeLabel('Default')
91
+                            ->onLabel(Tax::enabledLabel())
92
+                            ->offLabel(Tax::disabledLabel()),
85
                     ])->columns(),
93
                     ])->columns(),
86
             ]);
94
             ]);
87
     }
95
     }
91
         return $table
99
         return $table
92
             ->columns([
100
             ->columns([
93
                 Tables\Columns\TextColumn::make('name')
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
                     ->iconPosition('after')
113
                     ->iconPosition('after')
99
                     ->searchable()
114
                     ->searchable()
100
                     ->sortable(),
115
                     ->sortable(),
101
                 Tables\Columns\TextColumn::make('computation')
116
                 Tables\Columns\TextColumn::make('computation')
102
-                    ->label('Computation')
117
+                    ->localizeLabel()
103
                     ->searchable()
118
                     ->searchable()
104
                     ->sortable(),
119
                     ->sortable(),
105
                 Tables\Columns\TextColumn::make('rate')
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
                     ->searchable()
123
                     ->searchable()
109
                     ->sortable(),
124
                     ->sortable(),
110
                 Tables\Columns\TextColumn::make('type')
125
                 Tables\Columns\TextColumn::make('type')
111
-                    ->label('Type')
126
+                    ->localizeLabel()
112
                     ->badge()
127
                     ->badge()
113
                     ->searchable()
128
                     ->searchable()
114
                     ->sortable(),
129
                     ->sortable(),
118
             ])
133
             ])
119
             ->actions([
134
             ->actions([
120
                 Tables\Actions\EditAction::make(),
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
             ->bulkActions([
138
             ->bulkActions([
136
                 Tables\Actions\BulkActionGroup::make([
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
             ->emptyStateActions([
146
             ->emptyStateActions([
166
                 Tables\Actions\CreateAction::make(),
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
     public static function getPages(): array
151
     public static function getPages(): array
178
     {
152
     {
179
         return [
153
         return [

+ 4
- 1
app/Filament/Company/Resources/Setting/TaxResource/Pages/CreateTax.php Прегледај датотеку

2
 
2
 
3
 namespace App\Filament\Company\Resources\Setting\TaxResource\Pages;
3
 namespace App\Filament\Company\Resources\Setting\TaxResource\Pages;
4
 
4
 
5
+use App\Enums\TaxType;
5
 use App\Filament\Company\Resources\Setting\TaxResource;
6
 use App\Filament\Company\Resources\Setting\TaxResource;
6
 use App\Models\Setting\Tax;
7
 use App\Models\Setting\Tax;
7
 use App\Traits\HandlesResourceRecordCreation;
8
 use App\Traits\HandlesResourceRecordCreation;
38
             throw new Halt('No authenticated user found');
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
 
2
 
3
 namespace App\Filament\Company\Resources\Setting\TaxResource\Pages;
3
 namespace App\Filament\Company\Resources\Setting\TaxResource\Pages;
4
 
4
 
5
+use App\Enums\TaxType;
5
 use App\Filament\Company\Resources\Setting\TaxResource;
6
 use App\Filament\Company\Resources\Setting\TaxResource;
6
 use App\Traits\HandlesResourceRecordUpdate;
7
 use App\Traits\HandlesResourceRecordUpdate;
7
 use Filament\Actions;
8
 use Filament\Actions;
45
             throw new Halt('No authenticated user found');
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 Прегледај датотеку

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
-<?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
 
2
 
3
 namespace App\Http\Middleware;
3
 namespace App\Http\Middleware;
4
 
4
 
5
+use App\Events\CompanyConfigured;
6
+use App\Models\Company;
5
 use Closure;
7
 use Closure;
8
+use Filament\Facades\Filament;
6
 use Illuminate\Http\Request;
9
 use Illuminate\Http\Request;
7
 use Symfony\Component\HttpFoundation\Response;
10
 use Symfony\Component\HttpFoundation\Response;
8
 
11
 
9
-class ApplyCurrentCompanyScope
12
+class ConfigureCurrentCompany
10
 {
13
 {
11
     /**
14
     /**
12
      * Handle an incoming request.
15
      * Handle an incoming request.
15
      */
18
      */
16
     public function handle(Request $request, Closure $next): Response
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
         return $next($request);
28
         return $next($request);
19
     }
29
     }
20
 }
30
 }

+ 60
- 10
app/Listeners/ConfigureCompanyDefault.php Прегледај датотеку

2
 
2
 
3
 namespace App\Listeners;
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
 use Filament\Actions\MountableAction;
15
 use Filament\Actions\MountableAction;
8
-use Filament\Events\TenantSet;
9
 use Filament\Facades\Filament;
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
 use Filament\Support\Facades\FilamentColor;
22
 use Filament\Support\Facades\FilamentColor;
11
 use Filament\Tables\Table;
23
 use Filament\Tables\Table;
12
 
24
 
15
     /**
27
     /**
16
      * Handle the event.
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
         $paginationPageOptions = RecordsPerPage::caseValues();
33
         $paginationPageOptions = RecordsPerPage::caseValues();
23
         $defaultPaginationPageOption = $company->appearance->records_per_page->value ?? RecordsPerPage::DEFAULT;
34
         $defaultPaginationPageOption = $company->appearance->records_per_page->value ?? RecordsPerPage::DEFAULT;
24
         $defaultSort = $company->appearance->table_sort_direction->value ?? TableSortDirection::DEFAULT;
35
         $defaultSort = $company->appearance->table_sort_direction->value ?? TableSortDirection::DEFAULT;
28
         $maxContentWidth = $company->appearance->max_content_width->value ?? MaxContentWidth::DEFAULT;
39
         $maxContentWidth = $company->appearance->max_content_width->value ?? MaxContentWidth::DEFAULT;
29
         $defaultFont = $company->appearance->font->value ?? Font::DEFAULT;
40
         $defaultFont = $company->appearance->font->value ?? Font::DEFAULT;
30
         $hasTopNavigation = $company->appearance->has_top_navigation ?? false;
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
         Table::configureUsing(static function (Table $table) use ($paginationPageOptions, $defaultSort, $stripedTables, $defaultPaginationPageOption): void {
52
         Table::configureUsing(static function (Table $table) use ($paginationPageOptions, $defaultSort, $stripedTables, $defaultPaginationPageOption): void {
53
+
33
             $table
54
             $table
34
                 ->paginationPageOptions($paginationPageOptions)
55
                 ->paginationPageOptions($paginationPageOptions)
35
                 ->defaultSort(column: 'id', direction: $defaultSort)
56
                 ->defaultSort(column: 'id', direction: $defaultSort)
38
         }, isImportant: true);
59
         }, isImportant: true);
39
 
60
 
40
         MountableAction::configureUsing(static function (MountableAction $action) use ($modalWidth): void {
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
         }, isImportant: true);
67
         }, isImportant: true);
43
 
68
 
44
-        $defaultColor = FilamentColor::register([
69
+        FilamentColor::register([
45
             'primary' => $defaultPrimaryColor->getColor(),
70
             'primary' => $defaultPrimaryColor->getColor(),
46
         ]);
71
         ]);
47
 
72
 
48
-        FilamentColor::swap($defaultColor);
49
-
50
         Filament::getPanel('company')
73
         Filament::getPanel('company')
51
             ->font($defaultFont)
74
             ->font($defaultFont)
52
             ->brandName($company->name)
75
             ->brandName($company->name)
53
             ->topNavigation($hasTopNavigation)
76
             ->topNavigation($hasTopNavigation)
54
             ->sidebarCollapsibleOnDesktop(! $hasTopNavigation)
77
             ->sidebarCollapsibleOnDesktop(! $hasTopNavigation)
55
             ->maxContentWidth($maxContentWidth);
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
 namespace App\Listeners;
3
 namespace App\Listeners;
4
 
4
 
5
 use App\Events\CompanyGenerated;
5
 use App\Events\CompanyGenerated;
6
-use App\Models\Locale\Country;
7
 use App\Services\CompanyDefaultService;
6
 use App\Services\CompanyDefaultService;
8
 
7
 
9
 class CreateCompanyDefaults
8
 class CreateCompanyDefaults
23
     {
22
     {
24
         $company = $event->company;
23
         $company = $event->company;
25
         $countryCode = $event->country;
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
         $user = $company->owner;
28
         $user = $company->owner;
30
 
29
 
31
         $companyDefaultService = new CompanyDefaultService();
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
 
2
 
3
 namespace App\Listeners;
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
 use App\Events\CompanyDefaultEvent;
8
 use App\Events\CompanyDefaultEvent;
7
 use App\Models\Setting\CompanyDefault;
9
 use App\Models\Setting\CompanyDefault;
8
 use Illuminate\Support\Facades\DB;
10
 use Illuminate\Support\Facades\DB;
67
 
69
 
68
     private function handleDiscount($default, $type, $key): void
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
         match (true) {
76
         match (true) {
71
             $type === DiscountType::Sales => $default->sales_discount_id = $key,
77
             $type === DiscountType::Sales => $default->sales_discount_id = $key,
72
             $type === DiscountType::Purchase => $default->purchase_discount_id = $key,
78
             $type === DiscountType::Purchase => $default->purchase_discount_id = $key,
75
 
81
 
76
     private function handleTax($default, $type, $key): void
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
         match (true) {
88
         match (true) {
79
             $type === TaxType::Sales => $default->sales_tax_id = $key,
89
             $type === TaxType::Sales => $default->sales_tax_id = $key,
80
             $type === TaxType::Purchase => $default->purchase_tax_id = $key,
90
             $type === TaxType::Purchase => $default->purchase_tax_id = $key,
83
 
93
 
84
     private function handleCategory($default, $type, $key): void
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
         match (true) {
100
         match (true) {
87
             $type === CategoryType::Income => $default->income_category_id = $key,
101
             $type === CategoryType::Income => $default->income_category_id = $key,
88
             $type === CategoryType::Expense => $default->expense_category_id = $key,
102
             $type === CategoryType::Expense => $default->expense_category_id = $key,

+ 48
- 0
app/Listeners/UpdateAccountBalances.php Прегледај датотеку

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
 
2
 
3
 namespace App\Listeners;
3
 namespace App\Listeners;
4
 
4
 
5
+use App\Contracts\CurrencyHandler;
5
 use App\Events\DefaultCurrencyChanged;
6
 use App\Events\DefaultCurrencyChanged;
6
 use App\Models\Setting\Currency;
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
      * Create the event listener.
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
      */
22
      */
22
     public function handle(DefaultCurrencyChanged $event): void
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
                 $currency->update(['rate' => $newRate]);
48
                 $currency->update(['rate' => $newRate]);
33
             }
49
             }
34
         }
50
         }

+ 134
- 0
app/Livewire/Company/Service/LiveCurrency/ListCompanyCurrencies.php Прегледај датотеку

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 Прегледај датотеку

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
 namespace App\Models\Banking;
3
 namespace App\Models\Banking;
4
 
4
 
5
 use App\Casts\MoneyCast;
5
 use App\Casts\MoneyCast;
6
+use App\Enums\AccountStatus;
7
+use App\Enums\AccountType;
8
+use App\Models\History\AccountHistory;
6
 use App\Models\Setting\Currency;
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
 use Database\Factories\Banking\AccountFactory;
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
 use Illuminate\Database\Eloquent\Model;
17
 use Illuminate\Database\Eloquent\Model;
11
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
18
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
19
+use Illuminate\Database\Eloquent\Relations\HasMany;
12
 use Spatie\Tags\HasTags;
20
 use Spatie\Tags\HasTags;
13
 use Wallo\FilamentCompanies\FilamentCompanies;
21
 use Wallo\FilamentCompanies\FilamentCompanies;
14
 
22
 
16
 {
24
 {
17
     use Blamable;
25
     use Blamable;
18
     use CompanyOwned;
26
     use CompanyOwned;
27
+    use HasDefault;
19
     use HasFactory;
28
     use HasFactory;
20
     use HasTags;
29
     use HasTags;
21
     use SyncsWithCompanyDefaults;
30
     use SyncsWithCompanyDefaults;
29
         'number',
38
         'number',
30
         'currency_code',
39
         'currency_code',
31
         'opening_balance',
40
         'opening_balance',
41
+        'balance',
32
         'description',
42
         'description',
33
         'notes',
43
         'notes',
34
         'status',
44
         'status',
46
     ];
56
     ];
47
 
57
 
48
     protected $casts = [
58
     protected $casts = [
59
+        'type' => AccountType::class,
60
+        'status' => AccountStatus::class,
49
         'enabled' => 'boolean',
61
         'enabled' => 'boolean',
50
         'opening_balance' => MoneyCast::class,
62
         'opening_balance' => MoneyCast::class,
63
+        'balance' => MoneyCast::class,
51
     ];
64
     ];
52
 
65
 
53
     public function company(): BelongsTo
66
     public function company(): BelongsTo
70
         return $this->belongsTo(FilamentCompanies::userModel(), 'updated_by');
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
     protected static function newFactory(): Factory
91
     protected static function newFactory(): Factory

+ 6
- 9
app/Models/Common/Contact.php Прегледај датотеку

3
 namespace App\Models\Common;
3
 namespace App\Models\Common;
4
 
4
 
5
 use App\Enums\ContactType;
5
 use App\Enums\ContactType;
6
-use App\Models\Core\Department;
7
 use App\Models\Setting\Currency;
6
 use App\Models\Setting\Currency;
8
-use App\Traits\{Blamable, CompanyOwned};
7
+use App\Traits\Blamable;
8
+use App\Traits\CompanyOwned;
9
 use Database\Factories\Common\ContactFactory;
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
 use Illuminate\Database\Eloquent\Model;
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
 use Wallo\FilamentCompanies\FilamentCompanies;
15
 use Wallo\FilamentCompanies\FilamentCompanies;
14
 
16
 
15
 class Contact extends Model
17
 class Contact extends Model
46
         'type' => ContactType::class,
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
     public function company(): BelongsTo
51
     public function company(): BelongsTo
55
     {
52
     {
56
         return $this->belongsTo(FilamentCompanies::companyModel(), 'company_id');
53
         return $this->belongsTo(FilamentCompanies::companyModel(), 'company_id');

+ 27
- 5
app/Models/Company.php Прегледај датотеку

6
 use App\Models\Banking\Account;
6
 use App\Models\Banking\Account;
7
 use App\Models\Common\Contact;
7
 use App\Models\Common\Contact;
8
 use App\Models\Core\Department;
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
 use Filament\Models\Contracts\HasAvatar;
19
 use Filament\Models\Contracts\HasAvatar;
11
 use Illuminate\Database\Eloquent\Factories\HasFactory;
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
 use Wallo\FilamentCompanies\Company as FilamentCompaniesCompany;
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
 class Company extends FilamentCompaniesCompany implements HasAvatar
28
 class Company extends FilamentCompaniesCompany implements HasAvatar
17
 {
29
 {
47
         'deleted' => CompanyDeleted::class,
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
     public function accounts(): HasMany
67
     public function accounts(): HasMany
57
         return $this->hasMany(Account::class, 'company_id');
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
     public function appearance(): HasOne
77
     public function appearance(): HasOne
61
     {
78
     {
62
         return $this->hasOne(Appearance::class, 'company_id');
79
         return $this->hasOne(Appearance::class, 'company_id');
104
         return $this->hasMany(Discount::class, 'company_id');
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
     public function profile(): HasOne
129
     public function profile(): HasOne
108
     {
130
     {
109
         return $this->hasOne(CompanyProfile::class, 'company_id');
131
         return $this->hasOne(CompanyProfile::class, 'company_id');

+ 3
- 1
app/Models/ConnectedAccount.php Прегледај датотеку

4
 
4
 
5
 use Illuminate\Database\Eloquent\Concerns\HasTimestamps;
5
 use Illuminate\Database\Eloquent\Concerns\HasTimestamps;
6
 use Wallo\FilamentCompanies\ConnectedAccount as SocialiteConnectedAccount;
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
 class ConnectedAccount extends SocialiteConnectedAccount
11
 class ConnectedAccount extends SocialiteConnectedAccount
10
 {
12
 {

+ 10
- 6
app/Models/Core/Department.php Прегледај датотеку

2
 
2
 
3
 namespace App\Models\Core;
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
 use Database\Factories\Core\DepartmentFactory;
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
 use Illuminate\Database\Eloquent\Model;
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
 use Wallo\FilamentCompanies\FilamentCompanies;
14
 use Wallo\FilamentCompanies\FilamentCompanies;
12
 
15
 
13
 class Department extends Model
16
 class Department extends Model
35
 
38
 
36
     public function manager(): BelongsTo
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
     public function parent(): BelongsTo
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
     public function children(): HasMany
50
     public function children(): HasMany

+ 4
- 2
app/Models/Employeeship.php Прегледај датотеку

4
 
4
 
5
 use App\Models\Common\Contact;
5
 use App\Models\Common\Contact;
6
 use App\Models\Core\Department;
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
 class Employeeship extends FilamentCompaniesEmployeeship
12
 class Employeeship extends FilamentCompaniesEmployeeship
11
 {
13
 {

+ 64
- 0
app/Models/History/AccountHistory.php Прегледај датотеку

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

+ 43
- 8
app/Models/Locale/Country.php Прегледај датотеку

2
 
2
 
3
 namespace App\Models\Locale;
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
 use Illuminate\Support\Collection;
9
 use Illuminate\Support\Collection;
7
 use Squire\Model;
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
  * @property string $name
16
  * @property string $name
12
  * @property string $iso_code_3
17
  * @property string $iso_code_3
13
  * @property string $iso_code_2
18
  * @property string $iso_code_2
24
 class Country extends Model
29
 class Country extends Model
25
 {
30
 {
26
     public static array $schema = [
31
     public static array $schema = [
27
-        'id' => 'integer',
32
+        'id' => 'string',
28
         'name' => 'string',
33
         'name' => 'string',
29
         'iso_code_3' => 'string',
34
         'iso_code_3' => 'string',
30
         'iso_code_2' => 'string',
35
         'iso_code_2' => 'string',
44
         return $this->belongsTo(Currency::class, 'currency_code', 'code');
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
     public function states(): HasMany
57
     public function states(): HasMany
48
     {
58
     {
49
         return $this->hasMany(State::class, 'country_id', 'id');
59
         return $this->hasMany(State::class, 'country_id', 'id');
54
         return $this->hasMany(City::class, 'country_id', 'id');
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
     public static function findByIsoCode2(string $code): ?self
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
     public static function getAllCountryCodes(): Collection
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
     public static function getAvailableCountryOptions(): array
86
     public static function getAvailableCountryOptions(): array
73
     {
87
     {
74
         return self::all()->mapWithKeys(static function ($country): array {
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
         })->toArray();
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 Прегледај датотеку


Неке датотеке нису приказане због велике количине промена

Loading…
Откажи
Сачувај