diff --git a/.env.example b/.env.example index b4d8f7a..0499d29 100644 --- a/.env.example +++ b/.env.example @@ -54,3 +54,5 @@ DOCKER_INSTALL_XDEBUG=false GOOGLE_CLIENT_ID= GOOGLE_CLIENT_SECRET= GOOGLE_REDIRECT= + +LOCAL_EMAIL_FOR_LOGIN_VIA_GOOGLE= diff --git a/.eslintrc.js b/.eslintrc.js index 898a3a0..5245fdc 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -12,5 +12,6 @@ module.exports = { indent: ['error', 4], 'vue/html-indent': ['error', 4], 'vue/multi-word-component-names': 'off', - } + 'comma-dangle': ['error', 'always-multiline'], + }, }; diff --git a/app/Console/Commands/CreateUserCommand.php b/app/Console/Commands/CreateUserCommand.php new file mode 100644 index 0000000..34c6dec --- /dev/null +++ b/app/Console/Commands/CreateUserCommand.php @@ -0,0 +1,26 @@ +argument("email"); + + User::factory([ + "email" => $email, + ])->create(); + + $this->info("The user has been created"); + } +} diff --git a/app/Enums/EmploymentForm.php b/app/Enums/EmploymentForm.php new file mode 100644 index 0000000..27c1bb7 --- /dev/null +++ b/app/Enums/EmploymentForm.php @@ -0,0 +1,30 @@ +value); + } + + public static function casesToSelect(): array + { + $cases = collect(EmploymentForm::cases()); + + return $cases->map( + fn(EmploymentForm $enum) => [ + "label" => $enum->label(), + "value" => $enum->value, + ], + )->toArray(); + } +} diff --git a/app/Helpers/UserAvatarGenerator.php b/app/Helpers/UserAvatarGenerator.php new file mode 100644 index 0000000..beb39c7 --- /dev/null +++ b/app/Helpers/UserAvatarGenerator.php @@ -0,0 +1,50 @@ +generateUuid()}.svg"; + + Storage::put($path, $this->generate($user)); + + return $path; + } + + protected function generate(User $user): SVG + { + return $this->generator->rounded() + ->background($this->getColor($user->name)) + ->color("#F4F8FD") + ->smooth() + ->fontSize(0.33) + ->generateSvg($user->name); + } + + protected function getColor(string $name): string + { + $colors = config("colors"); + + return $colors[strlen($name) % count($colors)]; + } + + protected function generateUuid(): string + { + return Str::uuid()->toString(); + } +} diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php new file mode 100644 index 0000000..a390687 --- /dev/null +++ b/app/Http/Controllers/UserController.php @@ -0,0 +1,83 @@ +withTrashed() + ->search($request->query("search")) + ->latest() + ->paginate() + ->withQueryString(); + + return inertia("Users/Index", [ + "users" => UserResource::collection($users), + "filters" => $request->only("search"), + ]); + } + + public function create(): Response + { + return inertia("Users/Create", [ + "employmentForms" => EmploymentForm::casesToSelect(), + ]); + } + + public function store(UserRequest $request): RedirectResponse + { + User::query()->create($request->data()); + + return redirect() + ->route("users.index") + ->with("success", __("User has been created")); + } + + public function edit(User $user): Response + { + return inertia("Users/Edit", [ + "user" => new UserFormDataResource($user), + "employmentForms" => EmploymentForm::casesToSelect(), + ]); + } + + public function update(UserRequest $request, User $user): RedirectResponse + { + $user->update($request->data()); + + return redirect() + ->route("users.index") + ->with("success", __("User has been updated")); + } + + public function destroy(User $user): RedirectResponse + { + $user->delete(); + + return redirect() + ->route("users.index") + ->with("success", __("User has been deleted")); + } + + public function restore(User $user): RedirectResponse + { + $user->restore(); + + return redirect() + ->route("users.index") + ->with("success", __("User has been restored")); + } +} diff --git a/app/Http/Requests/UserRequest.php b/app/Http/Requests/UserRequest.php new file mode 100644 index 0000000..b1a92a0 --- /dev/null +++ b/app/Http/Requests/UserRequest.php @@ -0,0 +1,33 @@ + ["required", "min:3", "max: 150"], + "email" => ["required", "email", Rule::unique("users", "email")->ignore($this->user)], + "employmentForm" => ["required", new Enum(EmploymentForm::class)], + "employmentDate" => ["required", "date"], + ]; + } + + public function data(): array + { + return [ + "name" => $this->get("name"), + "email" => $this->get("email"), + "employment_form" => $this->get("employmentForm"), + "employment_date" => $this->get("employmentDate"), + ]; + } +} diff --git a/app/Http/Resources/UserFormDataResource.php b/app/Http/Resources/UserFormDataResource.php new file mode 100644 index 0000000..df7da1f --- /dev/null +++ b/app/Http/Resources/UserFormDataResource.php @@ -0,0 +1,23 @@ + $this->id, + "name" => $this->name, + "email" => $this->email, + "employmentForm" => $this->employment_form, + "employmentDate" => $this->employment_date, + ]; + } +} diff --git a/app/Http/Resources/UserResource.php b/app/Http/Resources/UserResource.php index b3e03f0..c9217a6 100644 --- a/app/Http/Resources/UserResource.php +++ b/app/Http/Resources/UserResource.php @@ -8,6 +8,8 @@ use Illuminate\Http\Resources\Json\JsonResource; class UserResource extends JsonResource { + public static $wrap = false; + public function toArray($request): array { return [ @@ -15,7 +17,10 @@ class UserResource extends JsonResource "name" => $this->name, "email" => $this->email, "role" => "Human Resources Manager", - "avatar" => "https://images.unsplash.com/photo-1550525811-e5869dd03032?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80", + "avatar" => asset($this->avatar), + "deleted" => $this->trashed(), + "employmentForm" => $this->employment_form->label(), + "employmentDate" => $this->employment_date->toDisplayString(), ]; } } diff --git a/app/Models/User.php b/app/Models/User.php index 660b8f7..aa76021 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -4,26 +4,53 @@ declare(strict_types=1); namespace Toby\Models; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; +use Illuminate\Support\Carbon; +use Toby\Enums\EmploymentForm; /** * @property int $id * @property string $name * @property string $email + * @property string $avatar + * @property EmploymentForm $employment_form + * @property Carbon $employment_date */ class User extends Authenticatable { use HasFactory; use Notifiable; + use SoftDeletes; protected $fillable = [ "name", "email", + "avatar", + "employment_form", + "employment_date", + ]; + + protected $casts = [ + "employment_form" => EmploymentForm::class, + "employment_date" => "datetime", ]; protected $hidden = [ "remember_token", ]; + + public function scopeSearch(Builder $query, ?string $text): Builder + { + if ($text === null) { + return $query; + } + + return $query + ->where("name", "LIKE", "%{$text}%") + ->orWhere("email", "LIKE", "%{$text}%"); + } } diff --git a/app/Observers/UserObserver.php b/app/Observers/UserObserver.php new file mode 100644 index 0000000..32f6f9c --- /dev/null +++ b/app/Observers/UserObserver.php @@ -0,0 +1,37 @@ +avatar = $this->generator->generateFor($user); + + $user->save(); + } + + public function updating(User $user): void + { + if ($user->isDirty("name")) { + Storage::delete($user->avatar); + $user->avatar = $this->generator->generateFor($user); + } + } + + public function forceDeleted(User $user): void + { + Storage::delete($user->avatar); + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 08b69d4..db3deaa 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -4,8 +4,17 @@ declare(strict_types=1); namespace Toby\Providers; +use Illuminate\Support\Carbon; use Illuminate\Support\ServiceProvider; +use Toby\Models\User; +use Toby\Observers\UserObserver; class AppServiceProvider extends ServiceProvider { + public function boot(): void + { + User::observe(UserObserver::class); + + Carbon::macro("toDisplayString", fn() => $this->translatedFormat("j F Y")); + } } diff --git a/composer.json b/composer.json index d597c15..3d13d3f 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,8 @@ "laravel/framework": "^8.75", "laravel/socialite": "^5.2", "laravel/telescope": "^4.6", - "laravel/tinker": "^2.5" + "laravel/tinker": "^2.5", + "lasserafn/php-initial-avatar-generator": "^4.2" }, "require-dev": { "blumilksoftware/codestyle": "^0.9.0", diff --git a/composer.lock b/composer.lock index f58b160..bc06f33 100644 --- a/composer.lock +++ b/composer.lock @@ -4,35 +4,35 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f5d5c19c36f856c3d336c36a43aa23e4", + "content-hash": "3412dd2a403927b829237ae4db36351a", "packages": [ { "name": "asm89/stack-cors", - "version": "v2.0.5", + "version": "v2.1.1", "source": { "type": "git", "url": "https://github.com/asm89/stack-cors.git", - "reference": "7a198ec737e926eab15d29368fc6fff66772b0e2" + "reference": "73e5b88775c64ccc0b84fb60836b30dc9d92ac4a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/asm89/stack-cors/zipball/7a198ec737e926eab15d29368fc6fff66772b0e2", - "reference": "7a198ec737e926eab15d29368fc6fff66772b0e2", + "url": "https://api.github.com/repos/asm89/stack-cors/zipball/73e5b88775c64ccc0b84fb60836b30dc9d92ac4a", + "reference": "73e5b88775c64ccc0b84fb60836b30dc9d92ac4a", "shasum": "" }, "require": { - "php": "^7.0|^8.0", - "symfony/http-foundation": "~2.7|~3.0|~4.0|~5.0|~6.0", - "symfony/http-kernel": "~2.7|~3.0|~4.0|~5.0|~6.0" + "php": "^7.2|^8.0", + "symfony/http-foundation": "^4|^5|^6", + "symfony/http-kernel": "^4|^5|^6" }, "require-dev": { - "phpunit/phpunit": "^6|^7|^8|^9", + "phpunit/phpunit": "^7|^9", "squizlabs/php_codesniffer": "^3.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "2.1-dev" } }, "autoload": { @@ -58,9 +58,9 @@ ], "support": { "issues": "https://github.com/asm89/stack-cors/issues", - "source": "https://github.com/asm89/stack-cors/tree/v2.0.5" + "source": "https://github.com/asm89/stack-cors/tree/v2.1.1" }, - "time": "2022-01-03T15:27:13+00:00" + "time": "2022-01-18T09:12:03+00:00" }, { "name": "brick/math", @@ -366,29 +366,29 @@ }, { "name": "dragonmantank/cron-expression", - "version": "v3.2.4", + "version": "v3.3.0", "source": { "type": "git", "url": "https://github.com/dragonmantank/cron-expression.git", - "reference": "9545dea2a1d92b60c8b3d06f02025c83e999bde0" + "reference": "63f2a76a045bac6ec93cc2daf2b534b412aa0313" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/9545dea2a1d92b60c8b3d06f02025c83e999bde0", - "reference": "9545dea2a1d92b60c8b3d06f02025c83e999bde0", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/63f2a76a045bac6ec93cc2daf2b534b412aa0313", + "reference": "63f2a76a045bac6ec93cc2daf2b534b412aa0313", "shasum": "" }, "require": { "php": "^7.2|^8.0", - "webmozart/assert": "^1.7.0" + "webmozart/assert": "^1.0" }, "replace": { "mtdowling/cron-expression": "^1.0" }, "require-dev": { "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^0.12", - "phpstan/phpstan-webmozart-assert": "^0.12.7", + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-webmozart-assert": "^1.0", "phpunit/phpunit": "^7.0|^8.0|^9.0" }, "type": "library", @@ -415,7 +415,7 @@ ], "support": { "issues": "https://github.com/dragonmantank/cron-expression/issues", - "source": "https://github.com/dragonmantank/cron-expression/tree/v3.2.4" + "source": "https://github.com/dragonmantank/cron-expression/tree/v3.3.0" }, "funding": [ { @@ -423,7 +423,7 @@ "type": "github" } ], - "time": "2022-01-13T04:09:37+00:00" + "time": "2022-01-14T16:02:05+00:00" }, { "name": "egulias/email-validator", @@ -961,16 +961,16 @@ }, { "name": "inertiajs/inertia-laravel", - "version": "v0.5.2", + "version": "v0.5.4", "source": { "type": "git", "url": "https://github.com/inertiajs/inertia-laravel.git", - "reference": "9c8c4201435aa0c11cb832242cf4c1b01bd8ef32" + "reference": "6a050ce04a710ac4809161558ac09fe49f13075e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/inertiajs/inertia-laravel/zipball/9c8c4201435aa0c11cb832242cf4c1b01bd8ef32", - "reference": "9c8c4201435aa0c11cb832242cf4c1b01bd8ef32", + "url": "https://api.github.com/repos/inertiajs/inertia-laravel/zipball/6a050ce04a710ac4809161558ac09fe49f13075e", + "reference": "6a050ce04a710ac4809161558ac09fe49f13075e", "shasum": "" }, "require": { @@ -1018,7 +1018,7 @@ ], "support": { "issues": "https://github.com/inertiajs/inertia-laravel/issues", - "source": "https://github.com/inertiajs/inertia-laravel/tree/v0.5.2" + "source": "https://github.com/inertiajs/inertia-laravel/tree/v0.5.4" }, "funding": [ { @@ -1026,7 +1026,91 @@ "type": "github" } ], - "time": "2022-01-12T16:18:26+00:00" + "time": "2022-01-18T10:59:08+00:00" + }, + { + "name": "intervention/image", + "version": "2.7.1", + "source": { + "type": "git", + "url": "https://github.com/Intervention/image.git", + "reference": "744ebba495319501b873a4e48787759c72e3fb8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Intervention/image/zipball/744ebba495319501b873a4e48787759c72e3fb8c", + "reference": "744ebba495319501b873a4e48787759c72e3fb8c", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "guzzlehttp/psr7": "~1.1 || ^2.0", + "php": ">=5.4.0" + }, + "require-dev": { + "mockery/mockery": "~0.9.2", + "phpunit/phpunit": "^4.8 || ^5.7 || ^7.5.15" + }, + "suggest": { + "ext-gd": "to use GD library based image processing.", + "ext-imagick": "to use Imagick based image processing.", + "intervention/imagecache": "Caching extension for the Intervention Image library" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.4-dev" + }, + "laravel": { + "providers": [ + "Intervention\\Image\\ImageServiceProvider" + ], + "aliases": { + "Image": "Intervention\\Image\\Facades\\Image" + } + } + }, + "autoload": { + "psr-4": { + "Intervention\\Image\\": "src/Intervention/Image" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Oliver Vogel", + "email": "oliver@olivervogel.com", + "homepage": "http://olivervogel.com/" + } + ], + "description": "Image handling and manipulation library with support for Laravel integration", + "homepage": "http://image.intervention.io/", + "keywords": [ + "gd", + "image", + "imagick", + "laravel", + "thumbnail", + "watermark" + ], + "support": { + "issues": "https://github.com/Intervention/image/issues", + "source": "https://github.com/Intervention/image/tree/2.7.1" + }, + "funding": [ + { + "url": "https://www.paypal.me/interventionphp", + "type": "custom" + }, + { + "url": "https://github.com/Intervention", + "type": "github" + } + ], + "time": "2021-12-16T16:49:26+00:00" }, { "name": "laravel/framework", @@ -1464,6 +1548,169 @@ }, "time": "2022-01-10T08:52:49+00:00" }, + { + "name": "lasserafn/php-initial-avatar-generator", + "version": "4.2.1", + "source": { + "type": "git", + "url": "https://github.com/LasseRafn/php-initial-avatar-generator.git", + "reference": "49d0b10cc8819af831e0f6fb1056a7d5ed9512d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/LasseRafn/php-initial-avatar-generator/zipball/49d0b10cc8819af831e0f6fb1056a7d5ed9512d0", + "reference": "49d0b10cc8819af831e0f6fb1056a7d5ed9512d0", + "shasum": "" + }, + "require": { + "ext-json": "*", + "intervention/image": "^2.3", + "lasserafn/php-initials": "^3.0", + "lasserafn/php-string-script-language": "^0.3.0", + "meyfa/php-svg": "^0.9.0", + "overtrue/pinyin": "^4.0", + "php": "^7.0|^7.1|^7.2|^7.3|^7.4|^8.0" + }, + "require-dev": { + "doctrine/instantiator": "1.0.*", + "phpunit/phpunit": "^6.5", + "satooshi/php-coveralls": "^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "LasseRafn\\InitialAvatarGenerator\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lasse Rafn", + "email": "lasserafn@gmail.com" + } + ], + "description": "A package to generate avatars with initials for PHP", + "keywords": [ + "Initials", + "avatar", + "image", + "svg" + ], + "support": { + "issues": "https://github.com/LasseRafn/php-initial-avatar-generator/issues", + "source": "https://github.com/LasseRafn/php-initial-avatar-generator/tree/4.2.1" + }, + "funding": [ + { + "url": "https://opencollective.com/ui-avatars", + "type": "open_collective" + } + ], + "time": "2020-12-24T13:12:12+00:00" + }, + { + "name": "lasserafn/php-initials", + "version": "3.1", + "source": { + "type": "git", + "url": "https://github.com/LasseRafn/php-initials.git", + "reference": "d287e1542687390eb68de779949bc0adc49e2d52" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/LasseRafn/php-initials/zipball/d287e1542687390eb68de779949bc0adc49e2d52", + "reference": "d287e1542687390eb68de779949bc0adc49e2d52", + "shasum": "" + }, + "require": { + "php": "^5.6|^7.0|^7.1|^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7", + "satooshi/php-coveralls": "^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "LasseRafn\\Initials\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lasse Rafn", + "email": "lasserafn@gmail.com" + } + ], + "description": "A package to generate initials in PHP", + "keywords": [ + "Initials", + "php" + ], + "support": { + "issues": "https://github.com/LasseRafn/php-initials/issues", + "source": "https://github.com/LasseRafn/php-initials/tree/3.1" + }, + "time": "2020-12-24T12:25:51+00:00" + }, + { + "name": "lasserafn/php-string-script-language", + "version": "0.3", + "source": { + "type": "git", + "url": "https://github.com/LasseRafn/php-string-script-language.git", + "reference": "49a09d4a5e38c1e59a2656ac05b601d615c7cddb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/LasseRafn/php-string-script-language/zipball/49a09d4a5e38c1e59a2656ac05b601d615c7cddb", + "reference": "49a09d4a5e38c1e59a2656ac05b601d615c7cddb", + "shasum": "" + }, + "require": { + "php": "^5.6|^7.0|^7.1|^8.0" + }, + "require-dev": { + "doctrine/instantiator": "1.0.5", + "phpunit/phpunit": "^5.6", + "phpunit/phpunit-mock-objects": "3.2.4", + "satooshi/php-coveralls": "^1.0", + "sebastian/exporter": "^1.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "LasseRafn\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lasse Rafn", + "email": "lasserafn@gmail.com" + } + ], + "description": "Detect language/encoding of a string in PHP", + "keywords": [ + "language", + "php", + "string" + ], + "support": { + "issues": "https://github.com/LasseRafn/php-string-script-language/issues", + "source": "https://github.com/LasseRafn/php-string-script-language/tree/0.3" + }, + "time": "2020-12-24T12:43:59+00:00" + }, { "name": "league/commonmark", "version": "2.1.1", @@ -1875,6 +2122,56 @@ }, "time": "2021-08-15T23:05:49+00:00" }, + { + "name": "meyfa/php-svg", + "version": "v0.9.1", + "source": { + "type": "git", + "url": "https://github.com/meyfa/php-svg.git", + "reference": "34401edef1f724898f468f71b85505fbcc8351bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/meyfa/php-svg/zipball/34401edef1f724898f468f71b85505fbcc8351bb", + "reference": "34401edef1f724898f468f71b85505fbcc8351bb", + "shasum": "" + }, + "require": { + "ext-gd": "*", + "ext-simplexml": "*", + "php": ">=5.3.3" + }, + "require-dev": { + "meyfa/phpunit-assert-gd": "^1.1", + "phpunit/phpunit": "^4.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "SVG\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabian Meyer", + "homepage": "http://meyfa.net" + } + ], + "description": "Read, edit, write, and render SVG files with PHP", + "homepage": "https://github.com/meyfa/php-svg", + "keywords": [ + "svg" + ], + "support": { + "issues": "https://github.com/meyfa/php-svg/issues", + "source": "https://github.com/meyfa/php-svg/tree/v0.9.1" + }, + "time": "2019-07-30T18:41:25+00:00" + }, { "name": "monolog/monolog", "version": "2.3.5", @@ -2338,6 +2635,79 @@ }, "time": "2021-04-09T13:42:10+00:00" }, + { + "name": "overtrue/pinyin", + "version": "4.0.8", + "source": { + "type": "git", + "url": "https://github.com/overtrue/pinyin.git", + "reference": "04bdb4d33d50e8fb1aa5a824064c5151c4b15dc2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/overtrue/pinyin/zipball/04bdb4d33d50e8fb1aa5a824064c5151c4b15dc2", + "reference": "04bdb4d33d50e8fb1aa5a824064c5151c4b15dc2", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "brainmaestro/composer-git-hooks": "^2.7", + "friendsofphp/php-cs-fixer": "^2.16", + "phpunit/phpunit": "~8.0" + }, + "type": "library", + "extra": { + "hooks": { + "pre-commit": [ + "composer test", + "composer fix-style" + ], + "pre-push": [ + "composer test", + "composer check-style" + ] + } + }, + "autoload": { + "psr-4": { + "Overtrue\\Pinyin\\": "src/" + }, + "files": [ + "src/const.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "overtrue", + "email": "anzhengchao@gmail.com", + "homepage": "http://github.com/overtrue" + } + ], + "description": "Chinese to pinyin translator.", + "homepage": "https://github.com/overtrue/pinyin", + "keywords": [ + "Chinese", + "Pinyin", + "cn2pinyin" + ], + "support": { + "issues": "https://github.com/overtrue/pinyin/issues", + "source": "https://github.com/overtrue/pinyin/tree/4.0.8" + }, + "funding": [ + { + "url": "https://www.patreon.com/overtrue", + "type": "patreon" + } + ], + "time": "2021-07-19T03:43:32+00:00" + }, { "name": "phpoption/phpoption", "version": "1.8.1", diff --git a/config/colors.php b/config/colors.php new file mode 100644 index 0000000..0b8a3ad --- /dev/null +++ b/config/colors.php @@ -0,0 +1,21 @@ + [ public_path("storage") => storage_path("app/public"), + public_path("avatars") => storage_path("app/avatars"), ], ]; diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index 4ddc304..b6cf77f 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -6,14 +6,17 @@ namespace Database\Factories; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Str; +use Toby\Enums\EmploymentForm; class UserFactory extends Factory { public function definition(): array { return [ - "name" => $this->faker->name(), + "name" => "{$this->faker->firstName} {$this->faker->lastName}", "email" => $this->faker->unique()->safeEmail(), + "employment_form" => $this->faker->randomElement(EmploymentForm::cases()), + "employment_date" => $this->faker->dateTimeBetween("2020-10-27"), "remember_token" => Str::random(10), ]; } diff --git a/database/migrations/2014_10_12_000000_create_users_table.php b/database/migrations/2014_10_12_000000_create_users_table.php index d1943d9..e7717b5 100644 --- a/database/migrations/2014_10_12_000000_create_users_table.php +++ b/database/migrations/2014_10_12_000000_create_users_table.php @@ -13,7 +13,11 @@ return new class() extends Migration { $table->id(); $table->string("name"); $table->string("email")->unique(); + $table->string("avatar")->nullable(); + $table->string("employment_form"); + $table->dateTime("employment_date"); $table->rememberToken(); + $table->softDeletes(); $table->timestamps(); }); } diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 1649f4a..f11b4c8 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -11,6 +11,9 @@ class DatabaseSeeder extends Seeder { public function run(): void { - User::factory(10)->create(); + User::factory(35)->create(); + User::factory([ + "email" => env("LOCAL_EMAIL_FOR_LOGIN_VIA_GOOGLE"), + ])->create(); } } diff --git a/package-lock.json b/package-lock.json index 8cb4e0f..c24807a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,10 +15,13 @@ "@tailwindcss/typography": "^0.5.0", "@vue/compiler-sfc": "^3.2.26", "autoprefixer": "^10.4.2", + "flatpickr": "^4.6.9", "laravel-mix": "^6.0.6", + "lodash": "^4.17.21", "postcss": "^8.4.5", "tailwindcss": "^3.0.13", "vue": "^3.2.26", + "vue-flatpickr-component": "^9.0.5", "vue-loader": "^17.0.0" }, "devDependencies": { @@ -4722,6 +4725,11 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/flatpickr": { + "version": "4.6.9", + "resolved": "https://registry.npmjs.org/flatpickr/-/flatpickr-4.6.9.tgz", + "integrity": "sha512-F0azNNi8foVWKSF+8X+ZJzz8r9sE1G4hl06RyceIaLvyltKvDl6vqk9Lm/6AUUCi5HWaIjiUbk7UpeE/fOXOpw==" + }, "node_modules/flatted": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.4.tgz", @@ -8892,6 +8900,20 @@ "node": ">=4.0" } }, + "node_modules/vue-flatpickr-component": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/vue-flatpickr-component/-/vue-flatpickr-component-9.0.5.tgz", + "integrity": "sha512-fKuz/D4ePQKi+jPo4xjYRgBCLTWrTsCoKbx8nam63x4kTDtSqvFOjNwLvy0QgwC0lC+aFpUWa1dNYTH0hgUcCA==", + "dependencies": { + "flatpickr": "^4.6.9" + }, + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, "node_modules/vue-loader": { "version": "17.0.0", "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-17.0.0.tgz", @@ -12982,6 +13004,11 @@ "rimraf": "^3.0.2" } }, + "flatpickr": { + "version": "4.6.9", + "resolved": "https://registry.npmjs.org/flatpickr/-/flatpickr-4.6.9.tgz", + "integrity": "sha512-F0azNNi8foVWKSF+8X+ZJzz8r9sE1G4hl06RyceIaLvyltKvDl6vqk9Lm/6AUUCi5HWaIjiUbk7UpeE/fOXOpw==" + }, "flatted": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.4.tgz", @@ -15970,6 +15997,14 @@ } } }, + "vue-flatpickr-component": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/vue-flatpickr-component/-/vue-flatpickr-component-9.0.5.tgz", + "integrity": "sha512-fKuz/D4ePQKi+jPo4xjYRgBCLTWrTsCoKbx8nam63x4kTDtSqvFOjNwLvy0QgwC0lC+aFpUWa1dNYTH0hgUcCA==", + "requires": { + "flatpickr": "^4.6.9" + } + }, "vue-loader": { "version": "17.0.0", "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-17.0.0.tgz", diff --git a/package.json b/package.json index 4ac8b9c..f0212fa 100644 --- a/package.json +++ b/package.json @@ -22,10 +22,13 @@ "@tailwindcss/typography": "^0.5.0", "@vue/compiler-sfc": "^3.2.26", "autoprefixer": "^10.4.2", + "flatpickr": "^4.6.9", "laravel-mix": "^6.0.6", + "lodash": "^4.17.21", "postcss": "^8.4.5", "tailwindcss": "^3.0.13", "vue": "^3.2.26", + "vue-flatpickr-component": "^9.0.5", "vue-loader": "^17.0.0" }, "devDependencies": { diff --git a/public/avatars b/public/avatars new file mode 120000 index 0000000..c6a773f --- /dev/null +++ b/public/avatars @@ -0,0 +1 @@ +/application/storage/app/avatars \ No newline at end of file diff --git a/resources/css/app.css b/resources/css/app.css index b5c61c9..0bee166 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -1,3 +1,49 @@ +@import 'flatpickr/dist/themes/light.css'; + @tailwind base; @tailwind components; @tailwind utilities; + + +.flatpickr-months .flatpickr-prev-month:hover svg, +.flatpickr-months .flatpickr-next-month:hover svg { + fill: #4F46E5; +} + +.flatpickr-day.selected, +.flatpickr-day.startRange, +.flatpickr-day.endRange, +.flatpickr-day.selected.inRange, +.flatpickr-day.startRange.inRange, +.flatpickr-day.endRange.inRange, +.flatpickr-day.selected:focus, +.flatpickr-day.startRange:focus, +.flatpickr-day.endRange:focus, +.flatpickr-day.selected:hover, +.flatpickr-day.startRange:hover, +.flatpickr-day.endRange:hover, +.flatpickr-day.selected.prevMonthDay, +.flatpickr-day.startRange.prevMonthDay, +.flatpickr-day.endRange.prevMonthDay, +.flatpickr-day.selected.nextMonthDay, +.flatpickr-day.startRange.nextMonthDay, +.flatpickr-day.endRange.nextMonthDay { + background: #527ABA; + -webkit-box-shadow: none; + box-shadow: none; + color: #fff; + border-color: #527ABA; +} + +.flatpickr-day.selected.startRange + .endRange:not(:nth-child(7n+1)), +.flatpickr-day.startRange.startRange + .endRange:not(:nth-child(7n+1)), +.flatpickr-day.endRange.startRange + .endRange:not(:nth-child(7n+1)) { + -webkit-box-shadow: -10px 0 0 #527ABA; + box-shadow: -10px 0 0 #527ABA; +} + +.flatpickr-day.week.selected { + border-radius: 0; + -webkit-box-shadow: -5px 0 0 #527ABA, 5px 0 0 #527ABA; + box-shadow: -5px 0 0 #527ABA, 5px 0 0 #527ABA; +} diff --git a/resources/js/Pages/Dashboard.vue b/resources/js/Pages/Dashboard.vue index 605d48d..4eb47d9 100644 --- a/resources/js/Pages/Dashboard.vue +++ b/resources/js/Pages/Dashboard.vue @@ -291,7 +291,7 @@ export default { name: 'Payroll', href: '#', iconForeground: 'text-yellow-700', - iconBackground: 'bg-yellow-50' + iconBackground: 'bg-yellow-50', }, { icon: ReceiptRefundIcon, diff --git a/resources/js/Pages/Login.vue b/resources/js/Pages/Login.vue index e3ba099..4b52501 100644 --- a/resources/js/Pages/Login.vue +++ b/resources/js/Pages/Login.vue @@ -80,7 +80,7 @@ export default { errors: { type: Object, default: () => ({oauth: null}), - } + }, }, }; diff --git a/resources/js/Pages/Users/Create.vue b/resources/js/Pages/Users/Create.vue new file mode 100644 index 0000000..3402b70 --- /dev/null +++ b/resources/js/Pages/Users/Create.vue @@ -0,0 +1,206 @@ + + + diff --git a/resources/js/Pages/Users/Edit.vue b/resources/js/Pages/Users/Edit.vue new file mode 100644 index 0000000..ca4afef --- /dev/null +++ b/resources/js/Pages/Users/Edit.vue @@ -0,0 +1,210 @@ + + + diff --git a/resources/js/Pages/Users/Index.vue b/resources/js/Pages/Users/Index.vue new file mode 100644 index 0000000..b566a7d --- /dev/null +++ b/resources/js/Pages/Users/Index.vue @@ -0,0 +1,300 @@ + + + diff --git a/resources/js/Shared/MainMenu.vue b/resources/js/Shared/MainMenu.vue index e5644f6..7373e89 100644 --- a/resources/js/Shared/MainMenu.vue +++ b/resources/js/Shared/MainMenu.vue @@ -37,10 +37,10 @@ >
- Open user menu + {{ user.avatar }} Workflow
@@ -186,7 +186,7 @@
@@ -241,7 +241,7 @@ import { PopoverOverlay, PopoverPanel, TransitionChild, - TransitionRoot + TransitionRoot, } from '@headlessui/vue'; import {BellIcon, MenuIcon, XIcon} from '@heroicons/vue/outline'; import {computed} from 'vue'; @@ -267,8 +267,8 @@ export default { setup() { const user = computed(() => usePage().props.value.auth.user); const navigation = [ - {name: 'Home', href: '/', current: true}, - {name: 'Profile', href: '#', current: false}, + {name: 'Strona główna', href: '/', current: true}, + {name: 'Użytkownicy', href: '/users', current: false}, {name: 'Resources', href: '#', current: false}, {name: 'Company Directory', href: '#', current: false}, {name: 'Openings', href: '#', current: false}, @@ -276,7 +276,7 @@ export default { const userNavigation = [ {name: 'Your Profile', href: '#'}, {name: 'Settings', href: '#'}, - {name: 'Sign out', href: '/logout', method: 'post', as: 'button'}, + {name: 'Wyloguj się', href: '/logout', method: 'post', as: 'button'}, ]; return { @@ -284,7 +284,7 @@ export default { navigation, userNavigation, }; - } + }, }; diff --git a/resources/js/app.js b/resources/js/app.js index 95cc0c5..64f1131 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -2,6 +2,8 @@ import {createApp, h} from 'vue'; import {createInertiaApp, Head, Link} from '@inertiajs/inertia-vue3'; import {InertiaProgress} from '@inertiajs/progress'; import AppLayout from '@/Shared/Layout/AppLayout'; +import Flatpickr from 'flatpickr'; +import { Polish } from 'flatpickr/dist/l10n/pl.js'; createInertiaApp({ resolve: name => { @@ -21,4 +23,16 @@ createInertiaApp({ title: title => `${title} - Toby`, }); -InertiaProgress.init(); +InertiaProgress.init({ + delay: 0, + color: 'red', +}); + +Flatpickr.localize(Polish); +Flatpickr.setDefaults({ + dateFormat: 'Y-m-d', + enableTime: false, + altFormat: 'j F Y', + altInput: true, +}); + diff --git a/resources/lang/en/pagination.php b/resources/lang/en/pagination.php index d654f2e..5f11429 100644 --- a/resources/lang/en/pagination.php +++ b/resources/lang/en/pagination.php @@ -3,6 +3,6 @@ declare(strict_types=1); return [ - "previous" => "« Previous", - "next" => "Next »", + "previous" => "Previous", + "next" => "Next", ]; diff --git a/resources/lang/pl.json b/resources/lang/pl.json index b6ae679..2869cfa 100644 --- a/resources/lang/pl.json +++ b/resources/lang/pl.json @@ -1,3 +1,7 @@ { - "User does not exist.": "Użytkownik nie istnieje." + "User does not exist.": "Użytkownik nie istnieje.", + "employment_contract": "Umowa o pracę", + "commission_contract": "Umowa zlecenie", + "b2b_contract": "Kontrakt B2B", + "board_member_contract": "Członek zarządu" } diff --git a/resources/lang/pl/auth.php b/resources/lang/pl/auth.php new file mode 100644 index 0000000..b8255a7 --- /dev/null +++ b/resources/lang/pl/auth.php @@ -0,0 +1,9 @@ + "Błędny login lub hasło.", + "password" => "Podane hasło jest nieprawidłowe.", + "throttle" => "Za dużo nieudanych prób logowania. Proszę spróbować za :seconds sekund.", +]; diff --git a/resources/lang/pl/pagination.php b/resources/lang/pl/pagination.php new file mode 100644 index 0000000..49e8df0 --- /dev/null +++ b/resources/lang/pl/pagination.php @@ -0,0 +1,8 @@ + "Następna", + "previous" => "Poprzednia", +]; diff --git a/resources/lang/pl/passwords.php b/resources/lang/pl/passwords.php new file mode 100644 index 0000000..2a4dd05 --- /dev/null +++ b/resources/lang/pl/passwords.php @@ -0,0 +1,11 @@ + "Hasło zostało zresetowane!", + "sent" => "Przypomnienie hasła zostało wysłane!", + "throttled" => "Proszę zaczekać zanim spróbujesz ponownie.", + "token" => "Token resetowania hasła jest nieprawidłowy.", + "user" => "Nie znaleziono użytkownika z takim adresem e-mail.", +]; diff --git a/resources/lang/pl/validation.php b/resources/lang/pl/validation.php new file mode 100644 index 0000000..09fdc6d --- /dev/null +++ b/resources/lang/pl/validation.php @@ -0,0 +1,117 @@ + "Pole :attribute musi zostać zaakceptowane.", + "active_url" => "Pole :attribute jest nieprawidłowym adresem URL.", + "after" => "Pole :attribute musi być datą późniejszą od :date.", + "after_or_equal" => "Pole :attribute musi być datą nie wcześniejszą niż :date.", + "alpha" => "Pole :attribute może zawierać jedynie litery.", + "alpha_dash" => "Pole :attribute może zawierać jedynie litery, cyfry i myślniki.", + "alpha_num" => "Pole :attribute może zawierać jedynie litery i cyfry.", + "array" => "Pole :attribute musi być tablicą.", + "attached" => "Ten :attribute jest już dołączony.", + "before" => "Pole :attribute musi być datą wcześniejszą od :date.", + "before_or_equal" => "Pole :attribute musi być datą nie późniejszą niż :date.", + "between" => [ + "array" => "Pole :attribute musi składać się z :min - :max elementów.", + "file" => "Pole :attribute musi zawierać się w granicach :min - :max kilobajtów.", + "numeric" => "Pole :attribute musi zawierać się w granicach :min - :max.", + "string" => "Pole :attribute musi zawierać się w granicach :min - :max znaków.", + ], + "boolean" => "Pole :attribute musi mieć wartość logiczną prawda albo fałsz.", + "confirmed" => "Potwierdzenie pola :attribute nie zgadza się.", + "current_password" => "Hasło jest nieprawidłowe.", + "date" => "Pole :attribute nie jest prawidłową datą.", + "date_equals" => "Pole :attribute musi być datą równą :date.", + "date_format" => "Pole :attribute nie jest w formacie :format.", + "different" => "Pole :attribute oraz :other muszą się różnić.", + "digits" => "Pole :attribute musi składać się z :digits cyfr.", + "digits_between" => "Pole :attribute musi mieć od :min do :max cyfr.", + "dimensions" => "Pole :attribute ma niepoprawne wymiary.", + "distinct" => "Pole :attribute ma zduplikowane wartości.", + "email" => "Pole :attribute nie jest poprawnym adresem e-mail.", + "ends_with" => "Pole :attribute musi kończyć się jedną z następujących wartości: :values.", + "exists" => "Zaznaczone pole :attribute jest nieprawidłowe.", + "file" => "Pole :attribute musi być plikiem.", + "filled" => "Pole :attribute nie może być puste.", + "gt" => [ + "array" => "Pole :attribute musi mieć więcej niż :value elementów.", + "file" => "Pole :attribute musi być większe niż :value kilobajtów.", + "numeric" => "Pole :attribute musi być większe niż :value.", + "string" => "Pole :attribute musi być dłuższe niż :value znaków.", + ], + "gte" => [ + "array" => "Pole :attribute musi mieć :value lub więcej elementów.", + "file" => "Pole :attribute musi być większe lub równe :value kilobajtów.", + "numeric" => "Pole :attribute musi być większe lub równe :value.", + "string" => "Pole :attribute musi być dłuższe lub równe :value znaków.", + ], + "image" => "Pole :attribute musi być obrazkiem.", + "in" => "Zaznaczony element :attribute jest nieprawidłowy.", + "in_array" => "Pole :attribute nie znajduje się w :other.", + "integer" => "Pole :attribute musi być liczbą całkowitą.", + "ip" => "Pole :attribute musi być prawidłowym adresem IP.", + "ipv4" => "Pole :attribute musi być prawidłowym adresem IPv4.", + "ipv6" => "Pole :attribute musi być prawidłowym adresem IPv6.", + "json" => "Pole :attribute musi być poprawnym ciągiem znaków JSON.", + "lt" => [ + "array" => "Pole :attribute musi mieć mniej niż :value elementów.", + "file" => "Pole :attribute musi być mniejsze niż :value kilobajtów.", + "numeric" => "Pole :attribute musi być mniejsze niż :value.", + "string" => "Pole :attribute musi być krótsze niż :value znaków.", + ], + "lte" => [ + "array" => "Pole :attribute musi mieć :value lub mniej elementów.", + "file" => "Pole :attribute musi być mniejsze lub równe :value kilobajtów.", + "numeric" => "Pole :attribute musi być mniejsze lub równe :value.", + "string" => "Pole :attribute musi być krótsze lub równe :value znaków.", + ], + "max" => [ + "array" => "Pole :attribute nie może mieć więcej niż :max elementów.", + "file" => "Pole :attribute nie może być większe niż :max kilobajtów.", + "numeric" => "Pole :attribute nie może być większe niż :max.", + "string" => "Pole :attribute nie może być dłuższe niż :max znaków.", + ], + "mimes" => "Pole :attribute musi być plikiem typu :values.", + "mimetypes" => "Pole :attribute musi być plikiem typu :values.", + "min" => [ + "array" => "Pole :attribute musi mieć przynajmniej :min elementów.", + "file" => "Pole :attribute musi mieć przynajmniej :min kilobajtów.", + "numeric" => "Pole :attribute musi być nie mniejsze od :min.", + "string" => "Pole :attribute musi mieć przynajmniej :min znaków.", + ], + "multiple_of" => "Pole :attribute musi być wielokrotnością wartości :value", + "not_in" => "Zaznaczony :attribute jest nieprawidłowy.", + "not_regex" => "Format pola :attribute jest nieprawidłowy.", + "numeric" => "Pole :attribute musi być liczbą.", + "password" => "Hasło jest nieprawidłowe.", + "present" => "Pole :attribute musi być obecne.", + "prohibited" => "Pole :attribute jest zabronione.", + "prohibited_if" => "Pole :attribute jest zabronione, gdy :other to :value.", + "prohibited_unless" => "Pole :attribute jest zabronione, chyba że :other jest w :values.", + "regex" => "Format pola :attribute jest nieprawidłowy.", + "relatable" => "Ten :attribute może nie być powiązany z tym zasobem.", + "required" => "Pole :attribute jest wymagane.", + "required_if" => "Pole :attribute jest wymagane gdy :other ma wartość :value.", + "required_unless" => "Pole :attribute jest wymagane jeżeli :other nie znajduje się w :values.", + "required_with" => "Pole :attribute jest wymagane gdy :values jest obecny.", + "required_with_all" => "Pole :attribute jest wymagane gdy wszystkie :values są obecne.", + "required_without" => "Pole :attribute jest wymagane gdy :values nie jest obecny.", + "required_without_all" => "Pole :attribute jest wymagane gdy żadne z :values nie są obecne.", + "same" => "Pole :attribute i :other muszą być takie same.", + "size" => [ + "array" => "Pole :attribute musi zawierać :size elementów.", + "file" => "Pole :attribute musi mieć :size kilobajtów.", + "numeric" => "Pole :attribute musi mieć :size.", + "string" => "Pole :attribute musi mieć :size znaków.", + ], + "starts_with" => "Pole :attribute musi zaczynać się jedną z następujących wartości: :values.", + "string" => "Pole :attribute musi być ciągiem znaków.", + "timezone" => "Pole :attribute musi być prawidłową strefą czasową.", + "unique" => "Taki :attribute już występuje.", + "uploaded" => "Nie udało się wgrać pliku :attribute.", + "url" => "Format pola :attribute jest nieprawidłowy.", + "uuid" => "Pole :attribute musi być poprawnym identyfikatorem UUID.", +]; diff --git a/resources/views/app.blade.php b/resources/views/app.blade.php index 0c09d2c..6579091 100644 --- a/resources/views/app.blade.php +++ b/resources/views/app.blade.php @@ -1,5 +1,5 @@ - + diff --git a/routes/web.php b/routes/web.php index f4434c3..5479b3e 100644 --- a/routes/web.php +++ b/routes/web.php @@ -5,10 +5,14 @@ declare(strict_types=1); use Illuminate\Support\Facades\Route; use Toby\Http\Controllers\GoogleController; use Toby\Http\Controllers\LogoutController; +use Toby\Http\Controllers\UserController; Route::middleware("auth")->group(function (): void { Route::get("/", fn() => inertia("Dashboard"))->name("dashboard"); Route::post("/logout", LogoutController::class); + + Route::resource("users", UserController::class); + Route::post("users/{user}/restore", [UserController::class, "restore"])->withTrashed(); }); Route::middleware("guest")->group(function (): void { diff --git a/tests/Feature/UserTest.php b/tests/Feature/UserTest.php new file mode 100644 index 0000000..8dcfae4 --- /dev/null +++ b/tests/Feature/UserTest.php @@ -0,0 +1,163 @@ +count(10)->create(); + $admin = User::factory()->create(); + + $this->assertDatabaseCount("users", 11); + + $this->actingAs($admin) + ->get("/users") + ->assertInertia( + fn(Assert $page) => $page + ->component("Users/Index") + ->has("users.data", 11), + ); + } + + public function testAdminCanSearchUsersList(): void + { + User::factory([ + "name" => "Test User1", + ])->create(); + User::factory([ + "name" => "Test User2", + ])->create(); + User::factory([ + "name" => "Test User3", + ])->create(); + $admin = User::factory([ + "name" => "John Doe", + ])->create(); + + $this->assertDatabaseCount("users", 4); + + $this->actingAs($admin) + ->get("/users?search=test") + ->assertInertia( + fn(Assert $page) => $page + ->component("Users/Index") + ->has("users.data", 3), + ); + } + + public function testUserListIsPaginated(): void + { + User::factory()->count(15)->create(); + $admin = User::factory()->create(); + + $this->assertDatabaseCount("users", 16); + + $this->actingAs($admin) + ->get("/users?page=2") + ->assertInertia( + fn(Assert $page) => $page + ->component("Users/Index") + ->has("users.data", 1), + ); + } + + public function testAdminCanCreateUser(): void + { + $admin = User::factory()->create(); + Carbon::setTestNow(Carbon::now()); + + $this->actingAs($admin) + ->post("/users", [ + "name" => "John Doe", + "email" => "john.doe@example.com", + "employmentForm" => EmploymentForm::B2B_CONTRACT->value, + "employmentDate" => Carbon::now()->toDateTimeString(), + ]) + ->assertSessionHasNoErrors(); + + $this->assertDatabaseHas("users", [ + "name" => "John Doe", + "email" => "john.doe@example.com", + "employment_form" => EmploymentForm::B2B_CONTRACT->value, + "employment_date" => Carbon::now()->toDateTimeString(), + ]); + } + + public function testAdminCanEditUser(): void + { + $admin = User::factory()->create(); + $user = User::factory()->create(); + + Carbon::setTestNow(); + + $this->assertDatabaseHas("users", [ + "name" => $user->name, + "email" => $user->email, + "employment_form" => $user->employment_form->value, + "employment_date" => $user->employment_date->toDateTimeString(), + ]); + + $this->actingAs($admin) + ->put("/users/{$user->id}", [ + "name" => "John Doe", + "email" => "john.doe@example.com", + "employmentForm" => EmploymentForm::B2B_CONTRACT->value, + "employmentDate" => Carbon::now()->toDateTimeString(), + ]) + ->assertSessionHasNoErrors(); + + $this->assertDatabaseHas("users", [ + "name" => "John Doe", + "email" => "john.doe@example.com", + "employment_form" => EmploymentForm::B2B_CONTRACT->value, + "employment_date" => Carbon::now()->toDateTimeString(), + ]); + } + + public function testAdminCanDeleteUser(): void + { + $admin = User::factory()->create(); + $user = User::factory()->create(); + + $this->actingAs($admin) + ->delete("/users/{$user->id}") + ->assertSessionHasNoErrors(); + + $this->assertSoftDeleted($user); + } + + public function testAdminCanRestoreUser(): void + { + $admin = User::factory()->create(); + $user = User::factory()->create(); + $user->delete(); + + $this->assertSoftDeleted($user); + + $this->actingAs($admin) + ->post("/users/{$user->id}/restore") + ->assertSessionHasNoErrors(); + + $this->assertNotSoftDeleted($user); + } +} diff --git a/tests/Unit/AvatarTest.php b/tests/Unit/AvatarTest.php new file mode 100644 index 0000000..2da1550 --- /dev/null +++ b/tests/Unit/AvatarTest.php @@ -0,0 +1,69 @@ +create(); + + Storage::assertExists($user->avatar); + } + + public function testAvatarIsDeletedWhenUserIsForceDeleted(): void + { + $user = User::factory()->create(); + + Storage::assertExists($user->avatar); + + $user->forceDelete(); + + Storage::assertMissing($user->avatar); + } + + public function testAvatarIsReplacedWhenUserChangedTheirName(): void + { + $user = User::factory()->create(); + $oldAvatar = $user->avatar; + + Storage::assertExists($oldAvatar); + + $user->update([ + "name" => "John Doe", + ]); + + Storage::assertMissing($oldAvatar); + Storage::assertExists($user->avatar); + } + + public function testAvatarIsNotReplacedWhenUserChangedOtherData(): void + { + $user = User::factory()->create(); + $avatar = $user->avatar; + + Storage::assertExists($avatar); + + $user->update([ + "email" => "john.doe@example.com", + ]); + + Storage::assertExists($avatar); + } +} diff --git a/tests/Unit/ExampleTest.php b/tests/Unit/ExampleTest.php deleted file mode 100644 index b98aaab..0000000 --- a/tests/Unit/ExampleTest.php +++ /dev/null @@ -1,15 +0,0 @@ -assertTrue(true); - } -}