From 37ce3262ac6f013be88e68993c144728dec89997 Mon Sep 17 00:00:00 2001 From: Adrian Hopek Date: Mon, 9 May 2022 15:03:09 +0200 Subject: [PATCH] wip --- app/Architecture/ExceptionHandler.php | 1 - ...ionRequestWaitsForApprovalNotification.php | 1 - .../Validation/Rules/VacationRequestRule.php | 1 + app/Eloquent/Models/Holiday.php | 1 - app/Eloquent/Models/Profile.php | 2 - app/Eloquent/Models/Resume.php | 44 + app/Eloquent/Models/Technology.php | 23 + app/Eloquent/Models/User.php | 3 - app/Eloquent/Models/VacationRequest.php | 1 - .../Models/VacationRequestActivity.php | 1 - .../Http/Controllers/ResumeController.php | 104 +- app/Infrastructure/Http/Kernel.php | 2 - .../Http/Requests/ResumeRequest.php | 77 ++ .../Http/Resources/ResumeFormResource.php | 26 + .../Http/Resources/ResumeResource.php | 24 + database/factories/ResumeFactory.php | 90 ++ database/factories/TechnologyFactory.php | 20 + ...2022_05_09_103339_create_resumes_table.php | 30 + ...05_09_105940_create_technologies_table.php | 23 + database/seeders/DemoSeeder.php | 32 + resources/js/Pages/Resumes/Create.vue | 1216 +++++++---------- resources/js/Pages/Resumes/Edit.vue | 637 +++++++++ resources/js/Pages/Resumes/Index.vue | 171 ++- resources/js/Shared/Forms/Combobox.vue | 5 +- resources/js/Shared/Forms/DynamicSection.vue | 114 ++ 25 files changed, 1843 insertions(+), 806 deletions(-) create mode 100644 app/Eloquent/Models/Resume.php create mode 100644 app/Eloquent/Models/Technology.php create mode 100644 app/Infrastructure/Http/Requests/ResumeRequest.php create mode 100644 app/Infrastructure/Http/Resources/ResumeFormResource.php create mode 100644 app/Infrastructure/Http/Resources/ResumeResource.php create mode 100644 database/factories/ResumeFactory.php create mode 100644 database/factories/TechnologyFactory.php create mode 100644 database/migrations/2022_05_09_103339_create_resumes_table.php create mode 100644 database/migrations/2022_05_09_105940_create_technologies_table.php create mode 100644 resources/js/Pages/Resumes/Edit.vue create mode 100644 resources/js/Shared/Forms/DynamicSection.vue diff --git a/app/Architecture/ExceptionHandler.php b/app/Architecture/ExceptionHandler.php index b286bd1..a43fcab 100644 --- a/app/Architecture/ExceptionHandler.php +++ b/app/Architecture/ExceptionHandler.php @@ -16,7 +16,6 @@ class ExceptionHandler extends Handler "password", "password_confirmation", ]; - protected array $handleByInertia = [ Response::HTTP_INTERNAL_SERVER_ERROR, Response::HTTP_SERVICE_UNAVAILABLE, diff --git a/app/Domain/Notifications/VacationRequestWaitsForApprovalNotification.php b/app/Domain/Notifications/VacationRequestWaitsForApprovalNotification.php index 109eef9..90b5507 100644 --- a/app/Domain/Notifications/VacationRequestWaitsForApprovalNotification.php +++ b/app/Domain/Notifications/VacationRequestWaitsForApprovalNotification.php @@ -99,4 +99,3 @@ class VacationRequestWaitsForApprovalNotification extends Notification ]); } } - diff --git a/app/Domain/Validation/Rules/VacationRequestRule.php b/app/Domain/Validation/Rules/VacationRequestRule.php index 07af8d2..f7e3c6d 100644 --- a/app/Domain/Validation/Rules/VacationRequestRule.php +++ b/app/Domain/Validation/Rules/VacationRequestRule.php @@ -9,5 +9,6 @@ use Toby\Eloquent\Models\VacationRequest; interface VacationRequestRule { public function check(VacationRequest $vacationRequest): bool; + public function errorMessage(): string; } diff --git a/app/Eloquent/Models/Holiday.php b/app/Eloquent/Models/Holiday.php index be8ae49..466065c 100644 --- a/app/Eloquent/Models/Holiday.php +++ b/app/Eloquent/Models/Holiday.php @@ -21,7 +21,6 @@ class Holiday extends Model use HasFactory; protected $guarded = []; - protected $casts = [ "date" => "date", ]; diff --git a/app/Eloquent/Models/Profile.php b/app/Eloquent/Models/Profile.php index df237e9..b8b3dc4 100644 --- a/app/Eloquent/Models/Profile.php +++ b/app/Eloquent/Models/Profile.php @@ -26,9 +26,7 @@ class Profile extends Model use HasAvatar; protected $primaryKey = "user_id"; - protected $guarded = []; - protected $casts = [ "employment_form" => EmploymentForm::class, "employment_date" => "date", diff --git a/app/Eloquent/Models/Resume.php b/app/Eloquent/Models/Resume.php new file mode 100644 index 0000000..d809d76 --- /dev/null +++ b/app/Eloquent/Models/Resume.php @@ -0,0 +1,44 @@ + AsCollection::class, + "languages" => AsCollection::class, + "technologies" => AsCollection::class, + "projects" => AsCollection::class, + ]; + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + protected static function newFactory(): ResumeFactory + { + return ResumeFactory::new(); + } +} diff --git a/app/Eloquent/Models/Technology.php b/app/Eloquent/Models/Technology.php new file mode 100644 index 0000000..218d1d8 --- /dev/null +++ b/app/Eloquent/Models/Technology.php @@ -0,0 +1,23 @@ + Role::class, "last_active_at" => "datetime", "employment_form" => EmploymentForm::class, "employment_date" => "date", ]; - protected $hidden = [ "remember_token", ]; - protected $with = [ "profile", ]; diff --git a/app/Eloquent/Models/VacationRequest.php b/app/Eloquent/Models/VacationRequest.php index 46bd084..97f94b7 100644 --- a/app/Eloquent/Models/VacationRequest.php +++ b/app/Eloquent/Models/VacationRequest.php @@ -41,7 +41,6 @@ class VacationRequest extends Model use HasStates; protected $guarded = []; - protected $casts = [ "type" => VacationType::class, "state" => VacationRequestState::class, diff --git a/app/Eloquent/Models/VacationRequestActivity.php b/app/Eloquent/Models/VacationRequestActivity.php index 942bb96..2064e9e 100644 --- a/app/Eloquent/Models/VacationRequestActivity.php +++ b/app/Eloquent/Models/VacationRequestActivity.php @@ -22,7 +22,6 @@ class VacationRequestActivity extends Model use HasFactory; protected $guarded = []; - protected $casts = [ "from" => VacationRequestState::class, "to" => VacationRequestState::class, diff --git a/app/Infrastructure/Http/Controllers/ResumeController.php b/app/Infrastructure/Http/Controllers/ResumeController.php index 6426eb0..9624f4b 100644 --- a/app/Infrastructure/Http/Controllers/ResumeController.php +++ b/app/Infrastructure/Http/Controllers/ResumeController.php @@ -4,26 +4,108 @@ declare(strict_types=1); namespace Toby\Infrastructure\Http\Controllers; -use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Http\RedirectResponse; -use Illuminate\Http\Request; use Inertia\Response; -use Toby\Eloquent\Helpers\YearPeriodRetriever; -use Toby\Eloquent\Models\Holiday; -use Toby\Infrastructure\Http\Requests\HolidayRequest; -use Toby\Infrastructure\Http\Resources\HolidayFormDataResource; -use Toby\Infrastructure\Http\Resources\HolidayResource; +use Toby\Eloquent\Models\Resume; +use Toby\Eloquent\Models\Technology; +use Toby\Eloquent\Models\User; +use Toby\Infrastructure\Http\Requests\ResumeRequest; +use Toby\Infrastructure\Http\Resources\ResumeFormResource; +use Toby\Infrastructure\Http\Resources\ResumeResource; +use Toby\Infrastructure\Http\Resources\SimpleUserResource; class ResumeController extends Controller { - public function index(Request $request): Response + public function index(): Response { - return inertia("Resumes/Index"); + $resumes = Resume::query() + ->paginate(); + + return inertia("Resumes/Index", [ + "resumes" => ResumeResource::collection($resumes), + ]); } public function create(): Response { - return inertia("Resumes/Create",[ - ]); + $users = User::query() + ->orderByProfileField("last_name") + ->orderByProfileField("first_name") + ->get(); + + return inertia("Resumes/Create", [ + "users" => SimpleUserResource::collection($users), + "technologies" => Technology::all()->pluck("name"), + ]); + } + + public function store(ResumeRequest $request): RedirectResponse + { + $resume = new Resume(); + + if ($request->hasEmployee()) { + $resume->user()->associate($request->getEmployee()); + } else { + $resume->name = $request->getName(); + } + + $resume->fill([ + "education" => $request->getEducation(), + "languages" => $request->getLanguageLevels(), + "technologies" => $request->getTechnologyLevels(), + "projects" => $request->getProjects(), + ]); + + $resume->save(); + + return redirect() + ->route("resumes.index") + ->with("success", __("Resume has been created.")); + } + + public function edit(Resume $resume): Response + { + $users = User::query() + ->orderByProfileField("last_name") + ->orderByProfileField("first_name") + ->get(); + + return inertia("Resumes/Edit", [ + "resume" => new ResumeFormResource($resume), + "users" => SimpleUserResource::collection($users), + "technologies" => Technology::all()->pluck("name"), + ]); + } + + public function update(Resume $resume, ResumeRequest $request): RedirectResponse + { + if ($request->hasEmployee()) { + $resume->user()->associate($request->getEmployee()); + } else { + $resume->user()->dissociate(); + $resume->name = $request->getName(); + } + + $resume->fill([ + "education" => $request->getEducation(), + "languages" => $request->getLanguageLevels(), + "technologies" => $request->getTechnologyLevels(), + "projects" => $request->getProjects(), + ]); + + $resume->save(); + + return redirect() + ->route("resumes.index") + ->with("success", __("Resume has been updated.")); + } + + public function destroy(Resume $resume): RedirectResponse + { + $resume->delete(); + + return redirect() + ->route("resumes.index") + ->with("success", __("Resume has been deleted.")); } } diff --git a/app/Infrastructure/Http/Kernel.php b/app/Infrastructure/Http/Kernel.php index 5c6a238..5b1e42f 100644 --- a/app/Infrastructure/Http/Kernel.php +++ b/app/Infrastructure/Http/Kernel.php @@ -40,7 +40,6 @@ class Kernel extends HttpKernel TrimStrings::class, ConvertEmptyStringsToNull::class, ]; - protected $middlewareGroups = [ "web" => [ EncryptCookies::class, @@ -58,7 +57,6 @@ class Kernel extends HttpKernel SubstituteBindings::class, ], ]; - protected $routeMiddleware = [ "auth" => Authenticate::class, "auth.basic" => AuthenticateWithBasicAuth::class, diff --git a/app/Infrastructure/Http/Requests/ResumeRequest.php b/app/Infrastructure/Http/Requests/ResumeRequest.php new file mode 100644 index 0000000..4ae5920 --- /dev/null +++ b/app/Infrastructure/Http/Requests/ResumeRequest.php @@ -0,0 +1,77 @@ + ["nullable", "exists:users,id"], + "name" => ["required_without:user"], + + "education.*.school" => ["required"], + "education.*.degree" => ["required"], + "education.*.fieldOfStudy" => ["required"], + "education.*.startDate" => ["required", "date_format:Y-m-d"], + "education.*.endDate" => ["required", "date_format:Y-m-d", "after:startDate"], + + "languages.*.name" => ["required", "distinct"], + "languages.*.level" => ["required", Rule::in(1, 2, 3, 4, 5, 6)], + + "technologies.*.name" => ["required", "exists:technologies,name", "distinct"], + "technologies.*.level" => ["required", Rule::in(1, 2, 3, 4, 5)], + + "projects.*.description" => ["required"], + "projects.*.technologies" => ["required"], + "projects.*.startDate" => ["required", "date_format:Y-m-d"], + "projects.*.endDate" => ["required", "date_format:Y-m-d", "after:startDate"], + "projects.*.tasks" => ["required"], + ]; + } + + public function hasEmployee(): bool + { + return $this->has("user"); + } + + public function getEmployee(): User + { + /** @var User $user */ + $user = User::query()->find($this->get("user")); + + return $user; + } + + public function getName(): string + { + return $this->get("name"); + } + + public function getLanguageLevels(): Collection + { + return $this->collect("languages"); + } + + public function getTechnologyLevels(): Collection + { + return $this->collect("technologies"); + } + + public function getEducation(): Collection + { + return $this->collect("education"); + } + + public function getProjects(): Collection + { + return $this->collect("projects"); + } +} diff --git a/app/Infrastructure/Http/Resources/ResumeFormResource.php b/app/Infrastructure/Http/Resources/ResumeFormResource.php new file mode 100644 index 0000000..0b3133d --- /dev/null +++ b/app/Infrastructure/Http/Resources/ResumeFormResource.php @@ -0,0 +1,26 @@ + $this->id, + "user" => $this->user_id, + "name" => $this->name, + "description" => $this->description, + "education" => $this->education, + "languages" => $this->languages, + "technologies" => $this->technologies, + "projects" => $this->projects, + ]; + } +} diff --git a/app/Infrastructure/Http/Resources/ResumeResource.php b/app/Infrastructure/Http/Resources/ResumeResource.php new file mode 100644 index 0000000..b126124 --- /dev/null +++ b/app/Infrastructure/Http/Resources/ResumeResource.php @@ -0,0 +1,24 @@ + $this->id, + "user" => new SimpleUserResource($this->user), + "name" => $this->name, + "description" => $this->description, + "createdAt" => $this->created_at->toDisplayString(), + "updatedAt" => $this->updated_at->toDisplayString(), + ]; + } +} diff --git a/database/factories/ResumeFactory.php b/database/factories/ResumeFactory.php new file mode 100644 index 0000000..a0e6208 --- /dev/null +++ b/database/factories/ResumeFactory.php @@ -0,0 +1,90 @@ + fn(array $attr) => empty($attr["user_id"]) ? $this->faker->name : null, + "description" => $this->faker->boolean(30) ? $this->faker->sentence : null, + "education" => $this->generateEducation(), + "languages" => $this->generateLanguages(), + "technologies" => $this->generateTechnologies(), + "projects" => $this->generateProjects(), + ]; + } + + protected function generateEducation(): array + { + $items = []; + + for ($i = 0; $i < $this->faker->numberBetween(1, 2); $i++) { + $items[] = [ + "school" => $this->faker->sentence, + "degree" => $this->faker->sentence, + "fieldOfStudy" => $this->faker->sentence, + "startDate" => $this->faker->date, + "endDate" => $this->faker->date, + ]; + } + + return $items; + } + + protected function generateLanguages(): array + { + $languages = new Collection(["english", "polish", "germany"]); + $number = $this->faker->numberBetween(1, $languages->count()); + + return $languages->random($number) + ->map(fn(string $language): array => [ + "language" => $language, + "level" => $this->faker->numberBetween(1, 6), + ]) + ->all(); + } + + protected function generateTechnologies(): array + { + $technologies = Technology::all(); + $number = $this->faker->numberBetween(2, $technologies->count()); + + return $technologies->random($number) + ->map(fn(string $technology): array => [ + "technology" => $technology, + "level" => $this->faker->numberBetween(1, 5), + ]) + ->all(); + } + + protected function generateProjects(): array + { + $items = []; + $technologies = Technology::all(); + + for ($i = 0; $i < $this->faker->numberBetween(1, 3); $i++) { + $number = $this->faker->numberBetween(2, $technologies->count()); + + $items[] = [ + "description" => $this->faker->text, + "technologies" => $technologies->random($number)->all(), + "startDate" => $this->faker->date, + "endDate" => $this->faker->date, + "tasks" => $this->faker->text, + ]; + } + + return $items; + } +} diff --git a/database/factories/TechnologyFactory.php b/database/factories/TechnologyFactory.php new file mode 100644 index 0000000..f9cd1d5 --- /dev/null +++ b/database/factories/TechnologyFactory.php @@ -0,0 +1,20 @@ + $this->faker->unique()->word, + ]; + } +} diff --git a/database/migrations/2022_05_09_103339_create_resumes_table.php b/database/migrations/2022_05_09_103339_create_resumes_table.php new file mode 100644 index 0000000..88f3869 --- /dev/null +++ b/database/migrations/2022_05_09_103339_create_resumes_table.php @@ -0,0 +1,30 @@ +id(); + $table->foreignIdFor(User::class)->nullable()->constrained()->cascadeOnDelete(); + $table->string("name")->nullable(); + $table->string("description")->nullable(); + $table->json("education"); + $table->json("languages"); + $table->json("technologies"); + $table->json("projects"); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists("resumes"); + } +}; diff --git a/database/migrations/2022_05_09_105940_create_technologies_table.php b/database/migrations/2022_05_09_105940_create_technologies_table.php new file mode 100644 index 0000000..4adefc7 --- /dev/null +++ b/database/migrations/2022_05_09_105940_create_technologies_table.php @@ -0,0 +1,23 @@ +id(); + $table->string("name")->unique(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists("technologies"); + } +}; diff --git a/database/seeders/DemoSeeder.php b/database/seeders/DemoSeeder.php index 41470ba..abe214e 100644 --- a/database/seeders/DemoSeeder.php +++ b/database/seeders/DemoSeeder.php @@ -20,6 +20,8 @@ use Toby\Domain\States\VacationRequest\WaitingForAdministrative; use Toby\Domain\States\VacationRequest\WaitingForTechnical; use Toby\Domain\VacationDaysCalculator; use Toby\Eloquent\Models\Key; +use Toby\Eloquent\Models\Resume; +use Toby\Eloquent\Models\Technology; use Toby\Eloquent\Models\User; use Toby\Eloquent\Models\VacationLimit; use Toby\Eloquent\Models\VacationRequest; @@ -335,5 +337,35 @@ class DemoSeeder extends Seeder ->for($user) ->create(); } + + Technology::factory()->createMany([ + ["name" => "Laravel"], + ["name" => "Symfony"], + ["name" => "CakePHP"], + ["name" => "PHP"], + ["name" => "Livewire"], + ["name" => "Inertia"], + ["name" => "Vue"], + ["name" => "Javascript"], + ["name" => "Redis"], + ["name" => "AWS"], + ["name" => "Tailwind"], + ["name" => "CSS"], + ["name" => "PHPUnit"], + ["name" => "Cypress"], + ["name" => "Behat"], + ["name" => "Pest"], + ["name" => "Golang"], + ]); + + foreach ($users as $user) { + Resume::factory() + ->for($user) + ->create(); + } + + Resume::factory() + ->count(3) + ->create(); } } diff --git a/resources/js/Pages/Resumes/Create.vue b/resources/js/Pages/Resumes/Create.vue index fab65c5..8d59992 100644 --- a/resources/js/Pages/Resumes/Create.vue +++ b/resources/js/Pages/Resumes/Create.vue @@ -6,696 +6,445 @@ Dodaj CV -
+

Dane podstawowe

-
-
- -
- -

- {{ form.errors.firstName }} -

-
-
-
- -
- -

- {{ form.errors.lastName }} -

-
-
-
-
-
-

- Edukacja -

- - - -
- -
-
-
-

- Języki -

- - - -
- -
-
-
-

- Technologie -

- - - -
- -
-
-
-

- Projekty -

- -