#134 - fill users data for resume #144
| @@ -1,5 +1,4 @@ | ||||
| module.exports = { | ||||
|   plugins: ['tailwindcss'], | ||||
|   env: { | ||||
|     node: true, | ||||
|     'vue/setup-compiler-macros': true, | ||||
| @@ -16,11 +15,6 @@ module.exports = { | ||||
|     'comma-dangle': ['error', 'always-multiline'], | ||||
|     'object-curly-spacing': ['error', 'always'], | ||||
|     'vue/require-default-prop': 0, | ||||
|     'tailwindcss/classnames-order': 'error', | ||||
|     'tailwindcss/enforces-negative-arbitrary-values': 'error', | ||||
|     'tailwindcss/enforces-shorthand': 'error', | ||||
|     'tailwindcss/no-arbitrary-value': 'error', | ||||
|     'tailwindcss/no-contradicting-classname': 'error', | ||||
|     'vue/multi-word-component-names': 0, | ||||
|   }, | ||||
| } | ||||
|   | ||||
| @@ -35,5 +35,6 @@ class AuthServiceProvider extends ServiceProvider | ||||
|         Gate::define("manageVacationLimits", fn(User $user): bool => $user->role === Role::AdministrativeApprover); | ||||
|         Gate::define("generateTimesheet", fn(User $user): bool => $user->role === Role::AdministrativeApprover); | ||||
|         Gate::define("listMonthlyUsage", fn(User $user): bool => $user->role === Role::AdministrativeApprover); | ||||
|         Gate::define("manageResumes", fn(User $user): bool => $user->role === Role::TechnicalApprover); | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										113
									
								
								app/Domain/ResumeGenerator.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								app/Domain/ResumeGenerator.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,113 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain; | ||||
|  | ||||
| use Illuminate\Support\Carbon; | ||||
| use Illuminate\Support\Collection; | ||||
| use Illuminate\Support\Str; | ||||
| use PhpOffice\PhpWord\TemplateProcessor; | ||||
| use Toby\Eloquent\Models\Resume; | ||||
|  | ||||
| class ResumeGenerator | ||||
| { | ||||
|     public function generate(Resume $resume): string | ||||
|     { | ||||
|         $processor = new TemplateProcessor($this->getTemplate()); | ||||
|  | ||||
|         $processor->setValue("id", $resume->id); | ||||
|         $processor->setValue("name", $resume->user ? $resume->user->profile->full_name : $resume->name); | ||||
|  | ||||
|         $this->fillTechnologies($processor, $resume); | ||||
|         $this->fillLanguages($processor, $resume); | ||||
|         $this->fillEducation($processor, $resume); | ||||
|         $this->fillProjects($processor, $resume); | ||||
|  | ||||
|         return $processor->save(); | ||||
|     } | ||||
|  | ||||
|     public function getTemplate(): string | ||||
|     { | ||||
|         return resource_path("views/docx/resume_eng.docx"); | ||||
|     } | ||||
|  | ||||
|     protected function fillTechnologies(TemplateProcessor $processor, Resume $resume): void | ||||
|     { | ||||
|         $processor->cloneBlock("technologies", 0, true, false, $this->getTechnologies($resume)); | ||||
|     } | ||||
|  | ||||
|     protected function fillLanguages(TemplateProcessor $processor, Resume $resume): void | ||||
|     { | ||||
|         $processor->cloneBlock("languages", 0, true, false, $this->getLanguages($resume)); | ||||
|     } | ||||
|  | ||||
|     protected function fillEducation(TemplateProcessor $processor, Resume $resume): void | ||||
|     { | ||||
|         $processor->cloneBlock("education", 0, true, false, $this->getEducation($resume)); | ||||
|     } | ||||
|  | ||||
|     protected function fillProjects(TemplateProcessor $processor, Resume $resume): void | ||||
|     { | ||||
|         $processor->cloneBlock("projects", $resume->projects->count(), true, true); | ||||
|  | ||||
|         foreach ($resume->projects as $index => $project) { | ||||
|             ++$index; | ||||
|             $processor->setValues($this->getProject($project, $index)); | ||||
|  | ||||
|             $processor->cloneBlock("project_technologies#{$index}", 0, true, false, $this->getProjectTechnologies($project, $index)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     protected function getProject(array $project, int $index): array | ||||
|     { | ||||
|         return [ | ||||
|             "index#{$index}" => $index, | ||||
|             "start_date#{$index}" => Carbon::createFromFormat("m/Y", $project["startDate"])->format("n.Y"), | ||||
|             "end_date#{$index}" => $project["current"] ? "present" : Carbon::createFromFormat("m/Y", $project["endDate"])->format("n.Y"), | ||||
|             "description#{$index}" => $project["description"], | ||||
|             "tasks#{$index}" => $this->withNewLines($project["tasks"]), | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     protected function withNewLines(string $text): string | ||||
|     { | ||||
|         return Str::replace("\n", "</w:t><w:br/><w:t>", $text); | ||||
|     } | ||||
|  | ||||
|     protected function getProjectTechnologies(array $project, int $index): array | ||||
|     { | ||||
|         $technologies = new Collection($project["technologies"] ?? []); | ||||
|  | ||||
|         return $technologies->map(fn(string $name) => [ | ||||
|             "technology#{$index}" => $name, | ||||
|         ])->all(); | ||||
|     } | ||||
|  | ||||
|     protected function getTechnologies(Resume $resume): array | ||||
|     { | ||||
|         return $resume->technologies->map(fn(array $technology): array => [ | ||||
|             "technology_name" => $technology["name"], | ||||
|             "technology_level" => __("resume.technology_levels.{$technology["level"]}"), | ||||
|         ])->all(); | ||||
|     } | ||||
|  | ||||
|     protected function getLanguages(Resume $resume): array | ||||
|     { | ||||
|         return $resume->languages->map(fn(array $language): array => [ | ||||
|             "language_name" => $language["name"], | ||||
|             "language_level" => __("resume.language_levels.{$language["level"]}"), | ||||
|         ])->all(); | ||||
|     } | ||||
|  | ||||
|     protected function getEducation(Resume $resume): array | ||||
|     { | ||||
|         return $resume->education->map(fn(array $project, int $index): array => [ | ||||
|             "start_date" => Carbon::createFromFormat("m/Y", $project["startDate"])->format("n.Y"), | ||||
|             "end_date" => $project["current"] ? "present" : Carbon::createFromFormat("m/Y", $project["endDate"])->format("n.Y"), | ||||
|             "school" => $project["school"], | ||||
|             "field_of_study" => $project["fieldOfStudy"], | ||||
|             "degree" => $project["degree"], | ||||
|         ])->all(); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										44
									
								
								app/Eloquent/Models/Resume.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								app/Eloquent/Models/Resume.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Eloquent\Models; | ||||
|  | ||||
| use Database\Factories\ResumeFactory; | ||||
| use Illuminate\Database\Eloquent\Casts\AsCollection; | ||||
| use Illuminate\Database\Eloquent\Factories\HasFactory; | ||||
| use Illuminate\Database\Eloquent\Model; | ||||
| use Illuminate\Database\Eloquent\Relations\BelongsTo; | ||||
| use Illuminate\Support\Collection; | ||||
|  | ||||
| /** | ||||
|  * @property int $id | ||||
|  * @property ?User $user | ||||
|  * @property string $name | ||||
|  * @property Collection $education | ||||
|  * @property Collection $languages | ||||
|  * @property Collection $technologies | ||||
|  * @property Collection $projects | ||||
|  */ | ||||
| class Resume extends Model | ||||
| { | ||||
|     use HasFactory; | ||||
|  | ||||
|     protected $guarded = []; | ||||
|     protected $casts = [ | ||||
|         "education" => 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(); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										25
									
								
								app/Eloquent/Models/Technology.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								app/Eloquent/Models/Technology.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Eloquent\Models; | ||||
|  | ||||
| use Database\Factories\TechnologyFactory; | ||||
| use Illuminate\Database\Eloquent\Factories\HasFactory; | ||||
| use Illuminate\Database\Eloquent\Model; | ||||
|  | ||||
| /** | ||||
|  * @property int $id | ||||
|  * @property string $name | ||||
|  */ | ||||
| class Technology extends Model | ||||
| { | ||||
|     use HasFactory; | ||||
|  | ||||
|     protected $guarded = []; | ||||
|  | ||||
|     protected static function newFactory(): TechnologyFactory | ||||
|     { | ||||
|         return TechnologyFactory::new(); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										137
									
								
								app/Infrastructure/Http/Controllers/ResumeController.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								app/Infrastructure/Http/Controllers/ResumeController.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,137 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Infrastructure\Http\Controllers; | ||||
|  | ||||
| use Illuminate\Http\RedirectResponse; | ||||
| use Inertia\Response; | ||||
| use Symfony\Component\HttpFoundation\BinaryFileResponse as BinaryFileResponseAlias; | ||||
| use Toby\Domain\ResumeGenerator; | ||||
| 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(): Response | ||||
|     { | ||||
|         $this->authorize("manageResumes"); | ||||
|  | ||||
|         $resumes = Resume::query() | ||||
|             ->latest("updated_at") | ||||
|             ->paginate(); | ||||
|  | ||||
|         return inertia("Resumes/Index", [ | ||||
|             "resumes" => ResumeResource::collection($resumes), | ||||
|         ]); | ||||
|     } | ||||
|  | ||||
|     public function create(): Response | ||||
|     { | ||||
|         $this->authorize("manageResumes"); | ||||
|  | ||||
|         $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 show(Resume $resume, ResumeGenerator $generator): BinaryFileResponseAlias | ||||
|     { | ||||
|         $this->authorize("manageResumes"); | ||||
|  | ||||
|         $path = $generator->generate($resume); | ||||
|  | ||||
|         return response() | ||||
|             ->download($path, "resume-{$resume->id}.docx") | ||||
|             ->deleteFileAfterSend(); | ||||
|     } | ||||
|  | ||||
|     public function store(ResumeRequest $request): RedirectResponse | ||||
|     { | ||||
|         $this->authorize("manageResumes"); | ||||
|  | ||||
|         $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 | ||||
|     { | ||||
|         $this->authorize("manageResumes"); | ||||
|  | ||||
|         $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 | ||||
|     { | ||||
|         $this->authorize("manageResumes"); | ||||
|  | ||||
|         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 | ||||
|     { | ||||
|         $this->authorize("manageResumes"); | ||||
|  | ||||
|         $resume->delete(); | ||||
|  | ||||
|         return redirect() | ||||
|             ->route("resumes.index") | ||||
|             ->with("success", __("Resume has been deleted.")); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										57
									
								
								app/Infrastructure/Http/Controllers/TechnologyController.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								app/Infrastructure/Http/Controllers/TechnologyController.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Infrastructure\Http\Controllers; | ||||
|  | ||||
| use Illuminate\Auth\Access\AuthorizationException; | ||||
| use Inertia\Response; | ||||
| use Symfony\Component\HttpFoundation\RedirectResponse; | ||||
| use Toby\Eloquent\Models\Technology; | ||||
| use Toby\Infrastructure\Http\Requests\TechnologyRequest; | ||||
| use Toby\Infrastructure\Http\Resources\TechnologyResource; | ||||
|  | ||||
| class TechnologyController extends Controller | ||||
| { | ||||
|     public function index(): Response | ||||
|     { | ||||
|         $this->authorize("manageResumes"); | ||||
|  | ||||
|         $technologies = Technology::query() | ||||
|             ->orderBy("name") | ||||
|             ->get(); | ||||
|  | ||||
|         return inertia("Technologies", [ | ||||
|             "technologies" => TechnologyResource::collection($technologies), | ||||
|         ]); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @throws AuthorizationException | ||||
|      */ | ||||
|     public function store(TechnologyRequest $request): RedirectResponse | ||||
|     { | ||||
|         $this->authorize("manageResumes"); | ||||
|  | ||||
|         $technology = Technology::query()->create($request->data()); | ||||
|  | ||||
|         return redirect() | ||||
|             ->back() | ||||
|             ->with("success", __("Technology :name has been created.", [ | ||||
|                 "name" => $technology->name, | ||||
|             ])); | ||||
|     } | ||||
|  | ||||
|     public function destroy(Technology $technology): RedirectResponse | ||||
|     { | ||||
|         $this->authorize("manageResumes"); | ||||
|  | ||||
|         $technology->delete(); | ||||
|  | ||||
|         return redirect() | ||||
|             ->back() | ||||
|             ->with("success", __("Technology :name has been deleted.", [ | ||||
|                 "name" => $technology->name, | ||||
|             ])); | ||||
|     } | ||||
| } | ||||
| @@ -39,6 +39,7 @@ class HandleInertiaRequests extends Middleware | ||||
|                 "manageUsers" => $user ? $user->can("manageUsers") : false, | ||||
|                 "listAllVacationRequests" => $user ? $user->can("listAll", VacationRequest::class) : false, | ||||
|                 "listMonthlyUsage" => $user ? $user->can("listMonthlyUsage") : false, | ||||
|                 "manageResumes" => $user ? $user->can("manageResumes") : false, | ||||
|             ], | ||||
|         ]; | ||||
|     } | ||||
|   | ||||
							
								
								
									
										79
									
								
								app/Infrastructure/Http/Requests/ResumeRequest.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								app/Infrastructure/Http/Requests/ResumeRequest.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,79 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Infrastructure\Http\Requests; | ||||
|  | ||||
| use Illuminate\Foundation\Http\FormRequest; | ||||
| use Illuminate\Support\Collection; | ||||
| use Illuminate\Validation\Rule; | ||||
| use Toby\Eloquent\Models\User; | ||||
|  | ||||
| class ResumeRequest extends FormRequest | ||||
| { | ||||
|     public function rules(): array | ||||
|     { | ||||
|         return [ | ||||
|             "user" => ["nullable", "exists:users,id"], | ||||
|             "name" => ["required_without:user"], | ||||
|  | ||||
|             "education.*.school" => ["required"], | ||||
|             "education.*.degree" => ["required"], | ||||
|             "education.*.fieldOfStudy" => ["required"], | ||||
|             "education.*.startDate" => ["required", "date_format:m/Y"], | ||||
|             "education.*.current" => ["required", "boolean"], | ||||
|             "education.*.endDate" => ["required_if:education.*.current,false", "nullable", "date_format:m/Y", "after:education.*.startDate"], | ||||
|  | ||||
|             "languages.*.name" => ["required", "distinct"], | ||||
|             "languages.*.level" => ["required", Rule::in(1, 2, 3, 4, 5, 6)], | ||||
|  | ||||
|             "technologies.*.name" => ["required", "distinct"], | ||||
|             "technologies.*.level" => ["required", Rule::in(1, 2, 3, 4, 5)], | ||||
|  | ||||
|             "projects.*.description" => ["required"], | ||||
|             "projects.*.technologies" => ["array", "min:1", "distinct"], | ||||
|             "projects.*.startDate" => ["required", "date_format:m/Y"], | ||||
|             "projects.*.current" => ["required", "boolean"], | ||||
|             "projects.*.endDate" => ["required_if:projects.*.current,false", "nullable", "date_format:m/Y", "after:projects.*.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"); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										29
									
								
								app/Infrastructure/Http/Requests/TechnologyRequest.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								app/Infrastructure/Http/Requests/TechnologyRequest.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Infrastructure\Http\Requests; | ||||
|  | ||||
| use Illuminate\Foundation\Http\FormRequest; | ||||
| use Illuminate\Validation\Rule; | ||||
|  | ||||
| class TechnologyRequest extends FormRequest | ||||
| { | ||||
|     public function rules(): array | ||||
|     { | ||||
|         return [ | ||||
|             "name" => [ | ||||
|                 "required", | ||||
|                 Rule::unique("technologies", "name")->ignore($this->technology), | ||||
|                 "max:255", | ||||
|             ], | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     public function data(): array | ||||
|     { | ||||
|         return [ | ||||
|             "name" => $this->get("name"), | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										26
									
								
								app/Infrastructure/Http/Resources/ResumeFormResource.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								app/Infrastructure/Http/Resources/ResumeFormResource.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Infrastructure\Http\Resources; | ||||
|  | ||||
| use Illuminate\Http\Resources\Json\JsonResource; | ||||
|  | ||||
| class ResumeFormResource extends JsonResource | ||||
| { | ||||
|     public static $wrap = null; | ||||
|  | ||||
|     public function toArray($request): array | ||||
|     { | ||||
|         return [ | ||||
|             "id" => $this->id, | ||||
|             "user" => $this->user_id, | ||||
|             "name" => $this->name, | ||||
|             "description" => $this->description, | ||||
|             "education" => $this->education, | ||||
|             "languages" => $this->languages, | ||||
|             "technologies" => $this->technologies, | ||||
|             "projects" => $this->projects, | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										28
									
								
								app/Infrastructure/Http/Resources/ResumeResource.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								app/Infrastructure/Http/Resources/ResumeResource.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Infrastructure\Http\Resources; | ||||
|  | ||||
| use Illuminate\Http\Resources\Json\JsonResource; | ||||
|  | ||||
| class ResumeResource extends JsonResource | ||||
| { | ||||
|     public static $wrap = null; | ||||
|  | ||||
|     public function toArray($request): array | ||||
|     { | ||||
|         return [ | ||||
|             "id" => $this->id, | ||||
|             "user" => new SimpleUserResource($this->user), | ||||
|             "name" => $this->name, | ||||
|             "description" => $this->description, | ||||
|             "educationCount" => $this->education->count(), | ||||
|             "languageCount" => $this->languages->count(), | ||||
|             "technologyCount" => $this->technologies->count(), | ||||
|             "projectCount" => $this->projects->count(), | ||||
|             "createdAt" => $this->created_at->toDisplayString(), | ||||
|             "updatedAt" => $this->updated_at->toDisplayString(), | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										20
									
								
								app/Infrastructure/Http/Resources/TechnologyResource.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								app/Infrastructure/Http/Resources/TechnologyResource.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Infrastructure\Http\Resources; | ||||
|  | ||||
| use Illuminate\Http\Resources\Json\JsonResource; | ||||
|  | ||||
| class TechnologyResource extends JsonResource | ||||
| { | ||||
|     public static $wrap = null; | ||||
|  | ||||
|     public function toArray($request): array | ||||
|     { | ||||
|         return [ | ||||
|             "id" => $this->id, | ||||
|             "name" => $this->name, | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
| @@ -19,6 +19,7 @@ | ||||
|         "laravel/telescope": "^4.6", | ||||
|         "laravel/tinker": "^2.5", | ||||
|         "maatwebsite/excel": "^3.1", | ||||
|         "phpoffice/phpword": "^0.18.3", | ||||
|         "rackbeat/laravel-ui-avatars": "^1.0", | ||||
|         "spatie/laravel-google-calendar": "^3.5", | ||||
|         "spatie/laravel-model-states": "^2.1", | ||||
|   | ||||
							
								
								
									
										293
									
								
								composer.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										293
									
								
								composer.lock
									
									
									
										generated
									
									
									
								
							| @@ -4,7 +4,7 @@ | ||||
|         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", | ||||
|         "This file is @generated automatically" | ||||
|     ], | ||||
|     "content-hash": "c23bb050dbab6338570f0f73adb83cf8", | ||||
|     "content-hash": "4eda92fdbff36c6073984536a46dde77", | ||||
|     "packages": [ | ||||
|         { | ||||
|             "name": "azuyalabs/yasumi", | ||||
| @@ -1039,16 +1039,16 @@ | ||||
|         }, | ||||
|         { | ||||
|             "name": "google/apiclient-services", | ||||
|             "version": "v0.247.0", | ||||
|             "version": "v0.248.0", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/googleapis/google-api-php-client-services.git", | ||||
|                 "reference": "1fc6eab7512b4e2dd4b6c96b2697f9b6bfbaddc2" | ||||
|                 "reference": "5967583706b1b09c49d04e3c1c3c05e459b0767e" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/googleapis/google-api-php-client-services/zipball/1fc6eab7512b4e2dd4b6c96b2697f9b6bfbaddc2", | ||||
|                 "reference": "1fc6eab7512b4e2dd4b6c96b2697f9b6bfbaddc2", | ||||
|                 "url": "https://api.github.com/repos/googleapis/google-api-php-client-services/zipball/5967583706b1b09c49d04e3c1c3c05e459b0767e", | ||||
|                 "reference": "5967583706b1b09c49d04e3c1c3c05e459b0767e", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
| @@ -1077,9 +1077,9 @@ | ||||
|             ], | ||||
|             "support": { | ||||
|                 "issues": "https://github.com/googleapis/google-api-php-client-services/issues", | ||||
|                 "source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.247.0" | ||||
|                 "source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.248.0" | ||||
|             }, | ||||
|             "time": "2022-05-02T01:12:12+00:00" | ||||
|             "time": "2022-05-09T01:06:12+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "google/auth", | ||||
| @@ -1678,17 +1678,79 @@ | ||||
|             "time": "2021-12-16T16:49:26+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "laravel/framework", | ||||
|             "version": "v9.11.0", | ||||
|             "name": "laminas/laminas-escaper", | ||||
|             "version": "2.10.0", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/laravel/framework.git", | ||||
|                 "reference": "598a8c84d452a66b90a3213b1d67189cc726c728" | ||||
|                 "url": "https://github.com/laminas/laminas-escaper.git", | ||||
|                 "reference": "58af67282db37d24e584a837a94ee55b9c7552be" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/laravel/framework/zipball/598a8c84d452a66b90a3213b1d67189cc726c728", | ||||
|                 "reference": "598a8c84d452a66b90a3213b1d67189cc726c728", | ||||
|                 "url": "https://api.github.com/repos/laminas/laminas-escaper/zipball/58af67282db37d24e584a837a94ee55b9c7552be", | ||||
|                 "reference": "58af67282db37d24e584a837a94ee55b9c7552be", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
|                 "ext-ctype": "*", | ||||
|                 "ext-mbstring": "*", | ||||
|                 "php": "^7.4 || ~8.0.0 || ~8.1.0" | ||||
|             }, | ||||
|             "conflict": { | ||||
|                 "zendframework/zend-escaper": "*" | ||||
|             }, | ||||
|             "require-dev": { | ||||
|                 "infection/infection": "^0.26.6", | ||||
|                 "laminas/laminas-coding-standard": "~2.3.0", | ||||
|                 "maglnet/composer-require-checker": "^3.8.0", | ||||
|                 "phpunit/phpunit": "^9.5.18", | ||||
|                 "psalm/plugin-phpunit": "^0.16.1", | ||||
|                 "vimeo/psalm": "^4.22.0" | ||||
|             }, | ||||
|             "type": "library", | ||||
|             "autoload": { | ||||
|                 "psr-4": { | ||||
|                     "Laminas\\Escaper\\": "src/" | ||||
|                 } | ||||
|             }, | ||||
|             "notification-url": "https://packagist.org/downloads/", | ||||
|             "license": [ | ||||
|                 "BSD-3-Clause" | ||||
|             ], | ||||
|             "description": "Securely and safely escape HTML, HTML attributes, JavaScript, CSS, and URLs", | ||||
|             "homepage": "https://laminas.dev", | ||||
|             "keywords": [ | ||||
|                 "escaper", | ||||
|                 "laminas" | ||||
|             ], | ||||
|             "support": { | ||||
|                 "chat": "https://laminas.dev/chat", | ||||
|                 "docs": "https://docs.laminas.dev/laminas-escaper/", | ||||
|                 "forum": "https://discourse.laminas.dev", | ||||
|                 "issues": "https://github.com/laminas/laminas-escaper/issues", | ||||
|                 "rss": "https://github.com/laminas/laminas-escaper/releases.atom", | ||||
|                 "source": "https://github.com/laminas/laminas-escaper" | ||||
|             }, | ||||
|             "funding": [ | ||||
|                 { | ||||
|                     "url": "https://funding.communitybridge.org/projects/laminas-project", | ||||
|                     "type": "community_bridge" | ||||
|                 } | ||||
|             ], | ||||
|             "time": "2022-03-08T20:15:36+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "laravel/framework", | ||||
|             "version": "v9.12.1", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/laravel/framework.git", | ||||
|                 "reference": "7c8bf052ef4919b8ffa7f25ec6f8648c4a36fc57" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/laravel/framework/zipball/7c8bf052ef4919b8ffa7f25ec6f8648c4a36fc57", | ||||
|                 "reference": "7c8bf052ef4919b8ffa7f25ec6f8648c4a36fc57", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
| @@ -1854,7 +1916,7 @@ | ||||
|                 "issues": "https://github.com/laravel/framework/issues", | ||||
|                 "source": "https://github.com/laravel/framework" | ||||
|             }, | ||||
|             "time": "2022-05-03T14:47:20+00:00" | ||||
|             "time": "2022-05-10T19:32:47+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "laravel/helpers", | ||||
| @@ -3122,16 +3184,16 @@ | ||||
|         }, | ||||
|         { | ||||
|             "name": "monolog/monolog", | ||||
|             "version": "2.5.0", | ||||
|             "version": "2.6.0", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/Seldaek/monolog.git", | ||||
|                 "reference": "4192345e260f1d51b365536199744b987e160edc" | ||||
|                 "reference": "247918972acd74356b0a91dfaa5adcaec069b6c0" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/Seldaek/monolog/zipball/4192345e260f1d51b365536199744b987e160edc", | ||||
|                 "reference": "4192345e260f1d51b365536199744b987e160edc", | ||||
|                 "url": "https://api.github.com/repos/Seldaek/monolog/zipball/247918972acd74356b0a91dfaa5adcaec069b6c0", | ||||
|                 "reference": "247918972acd74356b0a91dfaa5adcaec069b6c0", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
| @@ -3144,18 +3206,23 @@ | ||||
|             "require-dev": { | ||||
|                 "aws/aws-sdk-php": "^2.4.9 || ^3.0", | ||||
|                 "doctrine/couchdb": "~1.0@dev", | ||||
|                 "elasticsearch/elasticsearch": "^7", | ||||
|                 "elasticsearch/elasticsearch": "^7 || ^8", | ||||
|                 "ext-json": "*", | ||||
|                 "graylog2/gelf-php": "^1.4.2", | ||||
|                 "guzzlehttp/guzzle": "^7.4", | ||||
|                 "guzzlehttp/psr7": "^2.2", | ||||
|                 "mongodb/mongodb": "^1.8", | ||||
|                 "php-amqplib/php-amqplib": "~2.4 || ^3", | ||||
|                 "php-console/php-console": "^3.1.3", | ||||
|                 "phpspec/prophecy": "^1.6.1", | ||||
|                 "phpspec/prophecy": "^1.15", | ||||
|                 "phpstan/phpstan": "^0.12.91", | ||||
|                 "phpunit/phpunit": "^8.5", | ||||
|                 "phpunit/phpunit": "^8.5.14", | ||||
|                 "predis/predis": "^1.1", | ||||
|                 "rollbar/rollbar": "^1.3 || ^2 || ^3", | ||||
|                 "ruflin/elastica": ">=0.90@dev", | ||||
|                 "swiftmailer/swiftmailer": "^5.3|^6.0" | ||||
|                 "ruflin/elastica": "^7", | ||||
|                 "swiftmailer/swiftmailer": "^5.3|^6.0", | ||||
|                 "symfony/mailer": "^5.4 || ^6", | ||||
|                 "symfony/mime": "^5.4 || ^6" | ||||
|             }, | ||||
|             "suggest": { | ||||
|                 "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", | ||||
| @@ -3205,7 +3272,7 @@ | ||||
|             ], | ||||
|             "support": { | ||||
|                 "issues": "https://github.com/Seldaek/monolog/issues", | ||||
|                 "source": "https://github.com/Seldaek/monolog/tree/2.5.0" | ||||
|                 "source": "https://github.com/Seldaek/monolog/tree/2.6.0" | ||||
|             }, | ||||
|             "funding": [ | ||||
|                 { | ||||
| @@ -3217,7 +3284,7 @@ | ||||
|                     "type": "tidelift" | ||||
|                 } | ||||
|             ], | ||||
|             "time": "2022-04-08T15:43:54+00:00" | ||||
|             "time": "2022-05-10T09:36:00+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "myclabs/php-enum", | ||||
| @@ -3963,6 +4030,118 @@ | ||||
|             }, | ||||
|             "time": "2022-04-24T13:53:10+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "phpoffice/phpword", | ||||
|             "version": "0.18.3", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/PHPOffice/PHPWord.git", | ||||
|                 "reference": "be0190cd5d8f95b4be08d5853b107aa4e352759a" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/PHPOffice/PHPWord/zipball/be0190cd5d8f95b4be08d5853b107aa4e352759a", | ||||
|                 "reference": "be0190cd5d8f95b4be08d5853b107aa4e352759a", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
|                 "ext-xml": "*", | ||||
|                 "laminas/laminas-escaper": "^2.2", | ||||
|                 "php": "^5.3.3 || ^7.0 || ^8.0" | ||||
|             }, | ||||
|             "require-dev": { | ||||
|                 "dompdf/dompdf": "0.8.* || 1.0.*", | ||||
|                 "ext-gd": "*", | ||||
|                 "ext-zip": "*", | ||||
|                 "friendsofphp/php-cs-fixer": "^2.2", | ||||
|                 "mpdf/mpdf": "5.7.4 || 6.* || 7.* || 8.*", | ||||
|                 "php-coveralls/php-coveralls": "1.1.0 || ^2.0", | ||||
|                 "phploc/phploc": "2.* || 3.* || 4.* || 5.* || 6.* || 7.*", | ||||
|                 "phpmd/phpmd": "2.*", | ||||
|                 "phpunit/phpunit": "^4.8.36 || ^7.0", | ||||
|                 "squizlabs/php_codesniffer": "^2.9 || ^3.5", | ||||
|                 "tecnickcom/tcpdf": "6.*" | ||||
|             }, | ||||
|             "suggest": { | ||||
|                 "dompdf/dompdf": "Allows writing PDF", | ||||
|                 "ext-gd2": "Allows adding images", | ||||
|                 "ext-xmlwriter": "Allows writing OOXML and ODF", | ||||
|                 "ext-xsl": "Allows applying XSL style sheet to headers, to main document part, and to footers of an OOXML template", | ||||
|                 "ext-zip": "Allows writing OOXML and ODF" | ||||
|             }, | ||||
|             "type": "library", | ||||
|             "extra": { | ||||
|                 "branch-alias": { | ||||
|                     "dev-develop": "0.19-dev" | ||||
|                 } | ||||
|             }, | ||||
|             "autoload": { | ||||
|                 "psr-4": { | ||||
|                     "PhpOffice\\PhpWord\\": "src/PhpWord" | ||||
|                 } | ||||
|             }, | ||||
|             "notification-url": "https://packagist.org/downloads/", | ||||
|             "license": [ | ||||
|                 "LGPL-3.0" | ||||
|             ], | ||||
|             "authors": [ | ||||
|                 { | ||||
|                     "name": "Mark Baker" | ||||
|                 }, | ||||
|                 { | ||||
|                     "name": "Gabriel Bull", | ||||
|                     "email": "me@gabrielbull.com", | ||||
|                     "homepage": "http://gabrielbull.com/" | ||||
|                 }, | ||||
|                 { | ||||
|                     "name": "Franck Lefevre", | ||||
|                     "homepage": "https://rootslabs.net/blog/" | ||||
|                 }, | ||||
|                 { | ||||
|                     "name": "Ivan Lanin", | ||||
|                     "homepage": "http://ivan.lanin.org" | ||||
|                 }, | ||||
|                 { | ||||
|                     "name": "Roman Syroeshko", | ||||
|                     "homepage": "http://ru.linkedin.com/pub/roman-syroeshko/34/a53/994/" | ||||
|                 }, | ||||
|                 { | ||||
|                     "name": "Antoine de Troostembergh" | ||||
|                 } | ||||
|             ], | ||||
|             "description": "PHPWord - A pure PHP library for reading and writing word processing documents (OOXML, ODF, RTF, HTML, PDF)", | ||||
|             "homepage": "http://phpoffice.github.io", | ||||
|             "keywords": [ | ||||
|                 "ISO IEC 29500", | ||||
|                 "OOXML", | ||||
|                 "Office Open XML", | ||||
|                 "OpenDocument", | ||||
|                 "OpenXML", | ||||
|                 "PhpOffice", | ||||
|                 "PhpWord", | ||||
|                 "Rich Text Format", | ||||
|                 "WordprocessingML", | ||||
|                 "doc", | ||||
|                 "docx", | ||||
|                 "html", | ||||
|                 "odf", | ||||
|                 "odt", | ||||
|                 "office", | ||||
|                 "pdf", | ||||
|                 "php", | ||||
|                 "reader", | ||||
|                 "rtf", | ||||
|                 "template", | ||||
|                 "template processor", | ||||
|                 "word", | ||||
|                 "writer" | ||||
|             ], | ||||
|             "support": { | ||||
|                 "issues": "https://github.com/PHPOffice/PHPWord/issues", | ||||
|                 "source": "https://github.com/PHPOffice/PHPWord/tree/0.18.3" | ||||
|             }, | ||||
|             "time": "2022-02-17T15:40:03+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "phpoption/phpoption", | ||||
|             "version": "1.8.1", | ||||
| @@ -4558,16 +4737,16 @@ | ||||
|         }, | ||||
|         { | ||||
|             "name": "psy/psysh", | ||||
|             "version": "v0.11.2", | ||||
|             "version": "v0.11.4", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/bobthecow/psysh.git", | ||||
|                 "reference": "7f7da640d68b9c9fec819caae7c744a213df6514" | ||||
|                 "reference": "05c544b339b112226ad14803e1e5b09a61957454" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/bobthecow/psysh/zipball/7f7da640d68b9c9fec819caae7c744a213df6514", | ||||
|                 "reference": "7f7da640d68b9c9fec819caae7c744a213df6514", | ||||
|                 "url": "https://api.github.com/repos/bobthecow/psysh/zipball/05c544b339b112226ad14803e1e5b09a61957454", | ||||
|                 "reference": "05c544b339b112226ad14803e1e5b09a61957454", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
| @@ -4582,15 +4761,13 @@ | ||||
|                 "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4" | ||||
|             }, | ||||
|             "require-dev": { | ||||
|                 "bamarni/composer-bin-plugin": "^1.2", | ||||
|                 "hoa/console": "3.17.05.02" | ||||
|                 "bamarni/composer-bin-plugin": "^1.2" | ||||
|             }, | ||||
|             "suggest": { | ||||
|                 "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", | ||||
|                 "ext-pdo-sqlite": "The doc command requires SQLite to work.", | ||||
|                 "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well.", | ||||
|                 "ext-readline": "Enables support for arrow-key history navigation, and showing and manipulating command history.", | ||||
|                 "hoa/console": "A pure PHP readline implementation. You'll want this if your PHP install doesn't already support readline or libedit." | ||||
|                 "ext-readline": "Enables support for arrow-key history navigation, and showing and manipulating command history." | ||||
|             }, | ||||
|             "bin": [ | ||||
|                 "bin/psysh" | ||||
| @@ -4630,9 +4807,9 @@ | ||||
|             ], | ||||
|             "support": { | ||||
|                 "issues": "https://github.com/bobthecow/psysh/issues", | ||||
|                 "source": "https://github.com/bobthecow/psysh/tree/v0.11.2" | ||||
|                 "source": "https://github.com/bobthecow/psysh/tree/v0.11.4" | ||||
|             }, | ||||
|             "time": "2022-02-28T15:28:54+00:00" | ||||
|             "time": "2022-05-06T12:49:14+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "rackbeat/laravel-ui-avatars", | ||||
| @@ -7599,16 +7776,16 @@ | ||||
|     "packages-dev": [ | ||||
|         { | ||||
|             "name": "blumilksoftware/codestyle", | ||||
|             "version": "v1.2.0", | ||||
|             "version": "v1.3.0", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/blumilksoftware/codestyle.git", | ||||
|                 "reference": "124c55f0374d8f6952675011e359cb54f40f4090" | ||||
|                 "reference": "bf694da19f2cd5d0575b8fad585d3570d6785c23" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/blumilksoftware/codestyle/zipball/124c55f0374d8f6952675011e359cb54f40f4090", | ||||
|                 "reference": "124c55f0374d8f6952675011e359cb54f40f4090", | ||||
|                 "url": "https://api.github.com/repos/blumilksoftware/codestyle/zipball/bf694da19f2cd5d0575b8fad585d3570d6785c23", | ||||
|                 "reference": "bf694da19f2cd5d0575b8fad585d3570d6785c23", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
| @@ -7643,9 +7820,9 @@ | ||||
|             "description": "Blumilk codestyle configurator", | ||||
|             "support": { | ||||
|                 "issues": "https://github.com/blumilksoftware/codestyle/issues", | ||||
|                 "source": "https://github.com/blumilksoftware/codestyle/tree/v1.2.0" | ||||
|                 "source": "https://github.com/blumilksoftware/codestyle/tree/v1.3.0" | ||||
|             }, | ||||
|             "time": "2022-04-27T10:13:34+00:00" | ||||
|             "time": "2022-05-10T09:41:53+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "composer/pcre", | ||||
| @@ -8335,16 +8512,16 @@ | ||||
|         }, | ||||
|         { | ||||
|             "name": "laravel/dusk", | ||||
|             "version": "v6.23.1", | ||||
|             "version": "v6.24.0", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/laravel/dusk.git", | ||||
|                 "reference": "41f6deb42ae42b9b7dae1c32c03cb35d365d3118" | ||||
|                 "reference": "7fed3695741787d9998c5f04c94adfd62d70e766" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/laravel/dusk/zipball/41f6deb42ae42b9b7dae1c32c03cb35d365d3118", | ||||
|                 "reference": "41f6deb42ae42b9b7dae1c32c03cb35d365d3118", | ||||
|                 "url": "https://api.github.com/repos/laravel/dusk/zipball/7fed3695741787d9998c5f04c94adfd62d70e766", | ||||
|                 "reference": "7fed3695741787d9998c5f04c94adfd62d70e766", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
| @@ -8402,9 +8579,9 @@ | ||||
|             ], | ||||
|             "support": { | ||||
|                 "issues": "https://github.com/laravel/dusk/issues", | ||||
|                 "source": "https://github.com/laravel/dusk/tree/v6.23.1" | ||||
|                 "source": "https://github.com/laravel/dusk/tree/v6.24.0" | ||||
|             }, | ||||
|             "time": "2022-05-02T14:01:47+00:00" | ||||
|             "time": "2022-05-09T13:43:52+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "mockery/mockery", | ||||
| @@ -10592,16 +10769,16 @@ | ||||
|         }, | ||||
|         { | ||||
|             "name": "spatie/ignition", | ||||
|             "version": "1.2.9", | ||||
|             "version": "1.2.10", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/spatie/ignition.git", | ||||
|                 "reference": "db25202fab2d5c14613b8914a1bb374998bbf870" | ||||
|                 "reference": "dd8c3a21170b1d0f4d15048b2f4fa4a1a3a92a64" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/spatie/ignition/zipball/db25202fab2d5c14613b8914a1bb374998bbf870", | ||||
|                 "reference": "db25202fab2d5c14613b8914a1bb374998bbf870", | ||||
|                 "url": "https://api.github.com/repos/spatie/ignition/zipball/dd8c3a21170b1d0f4d15048b2f4fa4a1a3a92a64", | ||||
|                 "reference": "dd8c3a21170b1d0f4d15048b2f4fa4a1a3a92a64", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
| @@ -10658,20 +10835,20 @@ | ||||
|                     "type": "github" | ||||
|                 } | ||||
|             ], | ||||
|             "time": "2022-04-23T20:37:21+00:00" | ||||
|             "time": "2022-05-10T12:21:27+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "spatie/laravel-ignition", | ||||
|             "version": "1.2.2", | ||||
|             "version": "1.2.3", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/spatie/laravel-ignition.git", | ||||
|                 "reference": "924d1ae878874ad0bb49f63b69a9af759a34ee78" | ||||
|                 "reference": "51e5daaa7e43c154fe57f1ddfbba862f9fe57646" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/spatie/laravel-ignition/zipball/924d1ae878874ad0bb49f63b69a9af759a34ee78", | ||||
|                 "reference": "924d1ae878874ad0bb49f63b69a9af759a34ee78", | ||||
|                 "url": "https://api.github.com/repos/spatie/laravel-ignition/zipball/51e5daaa7e43c154fe57f1ddfbba862f9fe57646", | ||||
|                 "reference": "51e5daaa7e43c154fe57f1ddfbba862f9fe57646", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
| @@ -10748,7 +10925,7 @@ | ||||
|                     "type": "github" | ||||
|                 } | ||||
|             ], | ||||
|             "time": "2022-04-14T18:04:51+00:00" | ||||
|             "time": "2022-05-05T15:53:24+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "symfony/filesystem", | ||||
| @@ -11004,5 +11181,5 @@ | ||||
|         "ext-redis": "*" | ||||
|     }, | ||||
|     "platform-dev": [], | ||||
|     "plugin-api-version": "2.3.0" | ||||
|     "plugin-api-version": "2.1.0" | ||||
| } | ||||
|   | ||||
| @@ -109,7 +109,7 @@ return [ | ||||
|         VacationTypeConfigRetriever::KEY_IS_VACATION => true, | ||||
|     ], | ||||
|     VacationType::Absence->value => [ | ||||
|         VacationTypeConfigRetriever::KEY_TECHNICAL_APPROVAL => true, | ||||
|         VacationTypeConfigRetriever::KEY_TECHNICAL_APPROVAL => false, | ||||
|         VacationTypeConfigRetriever::KEY_ADMINISTRATIVE_APPROVAL => false, | ||||
|         VacationTypeConfigRetriever::KEY_BILLABLE => false, | ||||
|         VacationTypeConfigRetriever::KEY_HAS_LIMIT => false, | ||||
|   | ||||
							
								
								
									
										92
									
								
								database/factories/ResumeFactory.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								database/factories/ResumeFactory.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,92 @@ | ||||
|   
				
					
						
						krzysztofrewak
					
					
						commented  
						Review
						 ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
```   
				
					
						
						krzysztofrewak
					
					
						commented  
						Review
						 ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
| <?php | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|  | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
| declare(strict_types=1); | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|  | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
| namespace Database\Factories; | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|  | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
| use Illuminate\Database\Eloquent\Factories\Factory; | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
| use Illuminate\Support\Carbon; | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
| use Illuminate\Support\Collection; | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
| use Toby\Eloquent\Models\Resume; | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
| use Toby\Eloquent\Models\Technology; | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|  | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
| class ResumeFactory extends Factory | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
| { | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|     protected $model = Resume::class; | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|  | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|     public function definition(): array | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|     { | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|         return [ | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|             "name" => fn(array $attributes): ?string => empty($attributes["user_id"]) ? $this->faker->name : null, | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|             "education" => $this->generateEducation(), | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|             "languages" => $this->generateLanguages(), | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|             "technologies" => $this->generateTechnologies(), | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|             "projects" => $this->generateProjects(), | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|         ]; | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|     } | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|  | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|     protected function generateEducation(): array | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|     { | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|         $items = []; | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|  | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|         for ($i = 0; $i < $this->faker->numberBetween(1, 2); $i++) { | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|             $items[] = [ | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|                 "school" => $this->faker->sentence, | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|                 "degree" => $this->faker->sentence, | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|                 "fieldOfStudy" => $this->faker->sentence, | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|                 "current" => false, | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|                 "startDate" => Carbon::create($this->faker->date)->format("m/Y"), | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|                 "endDate" => Carbon::create($this->faker->date)->format("m/Y"), | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|             ]; | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|         } | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|  | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|         return $items; | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|     } | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|  | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|     protected function generateLanguages(): array | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|     { | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|         $languages = new Collection(["English", "Polish", "German"]); | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|         $number = $this->faker->numberBetween(1, $languages->count()); | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|  | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|         return $languages->random($number) | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|             ->map(fn(string $language): array => [ | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|                 "name" => $language, | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|                 "level" => $this->faker->numberBetween(1, 6), | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|             ]) | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|             ->all(); | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|     } | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|  | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|     protected function generateTechnologies(): array | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|     { | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|         $technologies = Technology::all()->pluck("name"); | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|         $number = $this->faker->numberBetween(2, $technologies->count()); | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|  | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|         return $technologies->random($number) | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|             ->map(fn(string $technology): array => [ | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|                 "name" => $technology, | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|                 "level" => $this->faker->numberBetween(1, 5), | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|             ]) | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|             ->all(); | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|     } | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|  | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|     protected function generateProjects(): array | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|     { | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|         $items = []; | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|         $technologies = Technology::all()->pluck("name"); | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|  | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|         for ($i = 0; $i < $this->faker->numberBetween(1, 3); $i++) { | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|             $number = $this->faker->numberBetween(2, $technologies->count()); | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|  | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|             $items[] = [ | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|                 "description" => $this->faker->text, | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|                 "technologies" => $technologies->random($number)->all(), | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|                 "current" => false, | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|                 "startDate" => Carbon::create($this->faker->date)->format("m/Y"), | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|                 "endDate" => Carbon::create($this->faker->date)->format("m/Y"), | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|                 "tasks" => $this->faker->text, | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|             ]; | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|         } | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|  | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|         return $items; | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
|     } | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
| } | ||||
|   ```suggestion
            "name" => fn(array $attributes): bool => empty($attributes["user_id"]) ? $this->faker->name : null,
``` | ||||
							
								
								
									
										20
									
								
								database/factories/TechnologyFactory.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								database/factories/TechnologyFactory.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Database\Factories; | ||||
|  | ||||
| use Illuminate\Database\Eloquent\Factories\Factory; | ||||
| use Toby\Eloquent\Models\Technology; | ||||
|  | ||||
| class TechnologyFactory extends Factory | ||||
| { | ||||
|     protected $model = Technology::class; | ||||
|  | ||||
|     public function definition(): array | ||||
|     { | ||||
|         return [ | ||||
|             "name" => $this->faker->unique()->word, | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,29 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| use Illuminate\Database\Migrations\Migration; | ||||
| use Illuminate\Database\Schema\Blueprint; | ||||
| use Illuminate\Support\Facades\Schema; | ||||
| use Toby\Eloquent\Models\User; | ||||
|  | ||||
| return new class() extends Migration { | ||||
|     public function up(): void | ||||
|     { | ||||
|         Schema::create("resumes", function (Blueprint $table): void { | ||||
|             $table->id(); | ||||
|             $table->foreignIdFor(User::class)->nullable()->constrained()->cascadeOnDelete(); | ||||
|             $table->string("name")->nullable(); | ||||
|             $table->json("education"); | ||||
|             $table->json("languages"); | ||||
|             $table->json("technologies"); | ||||
|             $table->json("projects"); | ||||
|             $table->timestamps(); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     public function down(): void | ||||
|     { | ||||
|         Schema::dropIfExists("resumes"); | ||||
|     } | ||||
| }; | ||||
| @@ -0,0 +1,23 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| use Illuminate\Database\Migrations\Migration; | ||||
| use Illuminate\Database\Schema\Blueprint; | ||||
| use Illuminate\Support\Facades\Schema; | ||||
|  | ||||
| return new class() extends Migration { | ||||
|     public function up(): void | ||||
|     { | ||||
|         Schema::create("technologies", function (Blueprint $table): void { | ||||
|             $table->id(); | ||||
|             $table->string("name")->unique(); | ||||
|             $table->timestamps(); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     public function down(): void | ||||
|     { | ||||
|         Schema::dropIfExists("technologies"); | ||||
|     } | ||||
| }; | ||||
| @@ -20,6 +20,8 @@ use Toby\Domain\States\VacationRequest\WaitingForAdministrative; | ||||
| use Toby\Domain\States\VacationRequest\WaitingForTechnical; | ||||
| use Toby\Domain\WorkDaysCalculator; | ||||
| 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; | ||||
| @@ -332,5 +334,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(); | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										2097
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2097
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										11
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								package.json
									
									
									
									
									
								
							| @@ -9,8 +9,7 @@ | ||||
|         "prod": "npm run production", | ||||
|         "production": "mix --production", | ||||
|         "lint": "./node_modules/.bin/eslint resources/js --ext .js,.vue", | ||||
|         "lintf": "./node_modules/.bin/eslint resources/js --ext .js,.vue --fix", | ||||
|         "postinstall": "npm run prod" | ||||
|         "lintf": "./node_modules/.bin/eslint resources/js --ext .js,.vue --fix" | ||||
|     }, | ||||
|     "dependencies": { | ||||
|         "@headlessui/vue": "^1.6.1", | ||||
| @@ -21,7 +20,7 @@ | ||||
|         "@tailwindcss/forms": "^0.5.1", | ||||
|         "@tailwindcss/line-clamp": "^0.4.0", | ||||
|         "@tailwindcss/typography": "^0.5.2", | ||||
|         "@vue/compiler-sfc": "^3.2.33", | ||||
|         "@vue/compiler-sfc": "^3.2.31", | ||||
|         "autoprefixer": "^10.4.7", | ||||
|         "axios": "^0.27.2", | ||||
|         "echarts": "^5.3.2", | ||||
| @@ -32,17 +31,17 @@ | ||||
|         "luxon": "^2.3.2", | ||||
|         "postcss": "^8.4.13", | ||||
|         "tailwindcss": "^3.0.24", | ||||
|         "vue": "^3.2.33", | ||||
|         "vue": "^3.2.21", | ||||
|         "vue-echarts": "^6.0.2", | ||||
|         "vue-flatpickr-component": "^9.0.6", | ||||
|         "vue-loader": "^17.0.0", | ||||
|         "vue-material-design-icons": "^5.0.0", | ||||
|         "vue-toastification": "^2.0.0-rc.5", | ||||
|         "vue3-popper": "^1.4.2" | ||||
|         "vue3-popper": "^1.4.2", | ||||
|         "vuedraggable": "^4.1.0" | ||||
|     }, | ||||
|     "devDependencies": { | ||||
|         "eslint": "^8.14.0", | ||||
|         "eslint-plugin-tailwindcss": "^3.5.0", | ||||
|         "eslint-plugin-vue": "^8.7.1" | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -19,6 +19,7 @@ | ||||
|     </coverage> | ||||
|     <php> | ||||
|         <server name="APP_ENV" value="testing"/> | ||||
|         <env name="APP_URL" value="http://localhost"/> | ||||
|         <env name="APP_KEY" value="base64:SKEJSy9oF9chQBCMbxqgj5zhtAvug9kwZ+cDiP1Y8A8="/> | ||||
|         <env name="BCRYPT_ROUNDS" value="4"/> | ||||
|         <env name="CACHE_DRIVER" value="array"/> | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| @import 'flatpickr/dist/themes/light.css'; | ||||
| @import 'flatpickr/dist/plugins/monthSelect/style.css'; | ||||
| @import 'vue-toastification/dist/index.css'; | ||||
|  | ||||
| @tailwind base; | ||||
| @@ -28,7 +29,8 @@ | ||||
| .flatpickr-day.endRange.prevMonthDay, | ||||
| .flatpickr-day.selected.nextMonthDay, | ||||
| .flatpickr-day.startRange.nextMonthDay, | ||||
| .flatpickr-day.endRange.nextMonthDay { | ||||
| .flatpickr-day.endRange.nextMonthDay, | ||||
| .flatpickr-monthSelect-month.selected { | ||||
|     background: #527ABA; | ||||
|     -webkit-box-shadow: none; | ||||
|     box-shadow: none; | ||||
|   | ||||
							
								
								
									
										89
									
								
								resources/js/Composables/useLevels.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								resources/js/Composables/useLevels.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,89 @@ | ||||
| const technologyLevels = [ | ||||
|   { | ||||
|     level: 1, | ||||
|     name: 'Beginner', | ||||
|     activeColor: 'bg-rose-400', | ||||
|     backgroundColor: 'bg-rose-100', | ||||
|     textColor: 'text-rose-400', | ||||
|   }, | ||||
|   { | ||||
|     level: 2, | ||||
|     name: 'Junior', | ||||
|     activeColor: 'bg-orange-400', | ||||
|     backgroundColor: 'bg-orange-100', | ||||
|     textColor: 'text-orange-400', | ||||
|   }, | ||||
|   { | ||||
|     level: 3, | ||||
|     name: 'Regular', | ||||
|     activeColor: 'bg-amber-400', | ||||
|     backgroundColor: 'bg-amber-100', | ||||
|     textColor: 'text-yellow-500', | ||||
|   }, | ||||
|   { | ||||
|     level: 4, | ||||
|     name: 'Advanced', | ||||
|     activeColor: 'bg-emerald-400', | ||||
|     backgroundColor: 'bg-emerald-100', | ||||
|     textColor: 'text-emerald-400', | ||||
|   }, | ||||
|   { | ||||
|     level: 5, | ||||
|     name: 'Expert', | ||||
|     activeColor: 'bg-blumilk-400', | ||||
|     backgroundColor: 'bg-blumilk-100', | ||||
|     textColor: 'text-blumilk-400', | ||||
|   }, | ||||
| ] | ||||
|  | ||||
| const languageLevels = [ | ||||
|   { | ||||
|     level: 1, | ||||
|     name: 'A1', | ||||
|     activeColor: 'bg-rose-400', | ||||
|     backgroundColor: 'bg-rose-100', | ||||
|     textColor: 'text-rose-400', | ||||
|   }, | ||||
|   { | ||||
|     level: 2, | ||||
|     name: 'A2', | ||||
|     activeColor: 'bg-orange-400', | ||||
|     backgroundColor: 'bg-orange-100', | ||||
|     textColor: 'text-orange-400', | ||||
|   }, | ||||
|   { | ||||
|     level: 3, | ||||
|     name: 'B1', | ||||
|     activeColor: 'bg-amber-400', | ||||
|     backgroundColor: 'bg-amber-100', | ||||
|     textColor: 'text-yellow-500', | ||||
|   }, | ||||
|   { | ||||
|     level: 4, | ||||
|     name: 'B2', | ||||
|     activeColor: 'bg-emerald-400', | ||||
|     backgroundColor: 'bg-emerald-100', | ||||
|     textColor: 'text-emerald-400', | ||||
|   }, | ||||
|   { | ||||
|     level: 5, | ||||
|     name: 'C1', | ||||
|     activeColor: 'bg-blumilk-400', | ||||
|     backgroundColor: 'bg-blumilk-100', | ||||
|     textColor: 'text-blumilk-400', | ||||
|   }, | ||||
|   { | ||||
|     level: 6, | ||||
|     name: 'C2', | ||||
|     activeColor: 'bg-gray-700', | ||||
|     backgroundColor: 'bg-gray-200', | ||||
|     textColor: 'text-gray-700', | ||||
|   }, | ||||
| ] | ||||
|  | ||||
| export default function () { | ||||
|   return { | ||||
|     technologyLevels, | ||||
|     languageLevels, | ||||
|   } | ||||
| } | ||||
							
								
								
									
										647
									
								
								resources/js/Pages/Resumes/Create.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										647
									
								
								resources/js/Pages/Resumes/Create.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,647 @@ | ||||
| <template> | ||||
|   <InertiaHead title="Dodawanie CV" /> | ||||
|   <div class="mx-auto w-full max-w-7xl bg-white shadow-md"> | ||||
|     <div class="p-4 sm:px-6"> | ||||
|       <h2 class="text-lg font-medium leading-6 text-gray-900"> | ||||
|         Dodaj CV | ||||
|       </h2> | ||||
|     </div> | ||||
|     <form | ||||
|       class="flex flex-col justify-center py-8 px-6 space-y-8 border-t border-gray-200" | ||||
|       @submit.prevent="submitResume" | ||||
|     > | ||||
|       <div class="space-y-8 sm:space-y-5"> | ||||
|         <div> | ||||
|           <h3 class="text-lg font-medium leading-6 text-gray-900"> | ||||
|             Dane podstawowe | ||||
|           </h3> | ||||
|           <div class="grid grid-cols-2 gap-8 py-4"> | ||||
|             <Listbox | ||||
|               v-model="form.user" | ||||
|               as="div" | ||||
|             > | ||||
|               <ListboxLabel class="block text-sm font-medium text-gray-700"> | ||||
|                 Użytkownik | ||||
|               </ListboxLabel> | ||||
|               <div class="relative mt-2"> | ||||
|                 <ListboxButton | ||||
|                   class="relative py-2 pr-10 pl-3 w-full max-w-md h-10 text-left bg-white rounded-md border border-gray-300 focus:border-blumilk-500 focus:outline-none focus:ring-1 focus:ring-blumilk-500 shadow-sm cursor-default sm:text-sm" | ||||
|                 > | ||||
|                   <span v-if="form.user === null"> | ||||
|                     Nie istnieje w bazie | ||||
|                   </span> | ||||
|                   <span | ||||
|                     v-else | ||||
|                     class="flex items-center" | ||||
|                   > | ||||
|                     <img | ||||
|                       :src="form.user.avatar" | ||||
|                       class="shrink-0 w-6 h-6 rounded-full" | ||||
|                     > | ||||
|                     <span class="block ml-3 truncate">{{ form.user.name }}</span> | ||||
|                   </span> | ||||
|                   <span class="flex absolute inset-y-0 right-0 items-center pr-2 pointer-events-none"> | ||||
|                     <SelectorIcon class="w-5 h-5 text-gray-400" /> | ||||
|                   </span> | ||||
|                 </ListboxButton> | ||||
|  | ||||
|                 <transition | ||||
|                   leave-active-class="transition ease-in duration-100" | ||||
|                   leave-from-class="opacity-100" | ||||
|                   leave-to-class="opacity-0" | ||||
|                 > | ||||
|                   <ListboxOptions | ||||
|                     class="overflow-auto absolute z-10 py-1 mt-1 w-full max-w-lg max-h-60 text-base bg-white rounded-md focus:outline-none ring-1 ring-black ring-opacity-5 shadow-lg sm:text-sm" | ||||
|                   > | ||||
|                     <ListboxOption | ||||
|                       v-slot="{ active }" | ||||
|                       as="template" | ||||
|                       :value="null" | ||||
|                     > | ||||
|                       <li | ||||
|                         :class="[active ? 'bg-gray-100' : 'text-gray-900', 'cursor-default select-none relative py-2 pl-3 pr-9']" | ||||
|                       > | ||||
|                         <div class="flex items-center"> | ||||
|                           Nie istnieje w bazie | ||||
|                         </div> | ||||
|  | ||||
|                         <span | ||||
|                           v-if="form.user === null" | ||||
|                           :class="['text-blumilk-600 absolute inset-y-0 right-0 flex items-center pr-4']" | ||||
|                         > | ||||
|                           <CheckIcon class="w-5 h-5" /> | ||||
|                         </span> | ||||
|                       </li> | ||||
|                     </ListboxOption> | ||||
|                     <ListboxOption | ||||
|                       v-for="user in users.data" | ||||
|                       :key="user.id" | ||||
|                       v-slot="{ active }" | ||||
|                       as="template" | ||||
|                       :value="user" | ||||
|                     > | ||||
|                       <li | ||||
|                         :class="[active ? 'bg-gray-100' : 'text-gray-900', 'cursor-default select-none relative py-2 pl-3 pr-9']" | ||||
|                       > | ||||
|                         <div class="flex items-center"> | ||||
|                           <img | ||||
|                             :src="user.avatar" | ||||
|                             class="shrink-0 w-6 h-6 rounded-full" | ||||
|                           > | ||||
|                           <span | ||||
|                             :class="[form.user?.id === user.id ? 'font-semibold' : 'font-normal', 'ml-3 block truncate']" | ||||
|                           > | ||||
|                             {{ user.name }} | ||||
|                           </span> | ||||
|                         </div> | ||||
|                         <span | ||||
|                           v-if="form.user?.id === user.id" | ||||
|                           :class="['text-blumilk-600 absolute inset-y-0 right-0 flex items-center pr-4']" | ||||
|                         > | ||||
|                           <CheckIcon class="w-5 h-5" /> | ||||
|                         </span> | ||||
|                       </li> | ||||
|                     </ListboxOption> | ||||
|                   </ListboxOptions> | ||||
|                 </transition> | ||||
|               </div> | ||||
|             </Listbox> | ||||
|             <div v-if="form.user === null"> | ||||
|               <label | ||||
|                 for="name" | ||||
|                 class="block text-sm font-medium text-gray-700 sm:mt-px" | ||||
|               > | ||||
|                 Imię i nazwisko | ||||
|               </label> | ||||
|               <div class="mt-2"> | ||||
|                 <input | ||||
|                   id="name" | ||||
|                   v-model="form.name" | ||||
|                   type="text" | ||||
|                   class="block w-full max-w-md rounded-md shadow-sm sm:text-sm" | ||||
|                   :class="{ 'border-red-300 text-red-900 focus:outline-none focus:ring-red-500 focus:border-red-500': form.errors.name, 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors.name }" | ||||
|                 > | ||||
|                 <p | ||||
|                   v-if="form.errors.name" | ||||
|                   class="mt-2 text-sm text-red-600" | ||||
|                 > | ||||
|                   {{ form.errors.name }} | ||||
|                 </p> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|         <DynamicSection | ||||
|           v-model="form.educations" | ||||
|           header="Edukacja" | ||||
|           add-label="Dodaj szkołę" | ||||
|           @add-item="addEducation" | ||||
|           @remove-item="(index) => form.educations.splice(index, 1)" | ||||
|         > | ||||
|           <template #itemHeader="{ element, index }"> | ||||
|             <template v-if="hasAnyErrorInSection('education', index)"> | ||||
|               <ExclamationCircleIcon class="h-6 w-6 mr-2 text-red-600 inline-block" /> | ||||
|             </template> | ||||
|             {{ element.school ? element.school : '(Nieokreślony)' }} | ||||
|           </template> | ||||
|           <template #form="{ element, index }"> | ||||
|             <div class="items-center py-4 sm:grid sm:grid-cols-2"> | ||||
|               <label class="block text-sm font-medium text-gray-700 sm:mt-px"> | ||||
|                 Szkoła | ||||
|               </label> | ||||
|               <div class="mt-1 sm:mt-0"> | ||||
|                 <input | ||||
|                   v-model="element.school" | ||||
|                   type="text" | ||||
|                   class="block w-full rounded-md shadow-sm sm:text-sm" | ||||
|                   :class="{ 'border-red-300 text-red-900 focus:outline-none focus:ring-red-500 focus:border-red-500': form.errors[`education.${index}.school`], 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors[`education.${index}.school`] }" | ||||
|                 > | ||||
|                 <p | ||||
|                   v-if="form.errors[`education.${index}.school`]" | ||||
|                   class="mt-2 text-sm text-red-600" | ||||
|                 > | ||||
|                   {{ form.errors[`education.${index}.school`] }} | ||||
|                 </p> | ||||
|               </div> | ||||
|             </div> | ||||
|             <div class="items-center py-4 sm:grid sm:grid-cols-2"> | ||||
|               <label class="block text-sm font-medium text-gray-700 sm:mt-px"> | ||||
|                 Stopień | ||||
|               </label> | ||||
|               <div class="mt-1 sm:mt-0"> | ||||
|                 <input | ||||
|                   v-model="element.degree" | ||||
|                   type="text" | ||||
|                   class="block w-full rounded-md shadow-sm sm:text-sm" | ||||
|                   :class="{ 'border-red-300 text-red-900 focus:outline-none focus:ring-red-500 focus:border-red-500': form.errors[`education.${index}.degree`], 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors[`education.${index}.degree`] }" | ||||
|                 > | ||||
|                 <p | ||||
|                   v-if="form.errors[`education.${index}.degree`]" | ||||
|                   class="mt-2 text-sm text-red-600" | ||||
|                 > | ||||
|                   {{ form.errors[`education.${index}.degree`] }} | ||||
|                 </p> | ||||
|               </div> | ||||
|             </div> | ||||
|             <div class="items-center py-4 sm:grid sm:grid-cols-2"> | ||||
|               <label class="block text-sm font-medium text-gray-700 sm:mt-px"> | ||||
|                 Kierunek/Specjalizacja | ||||
|               </label> | ||||
|               <div class="mt-1 sm:mt-0"> | ||||
|                 <input | ||||
|                   v-model="element.fieldOfStudy" | ||||
|                   type="text" | ||||
|                   class="block w-full rounded-md shadow-sm sm:text-sm" | ||||
|                   :class="{ 'border-red-300 text-red-900 focus:outline-none focus:ring-red-500 focus:border-red-500': form.errors[`education.${index}.fieldOfStudy`], 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors[`education.${index}.fieldOfStudy`] }" | ||||
|                 > | ||||
|                 <p | ||||
|                   v-if="form.errors[`education.${index}.fieldOfStudy`]" | ||||
|                   class="mt-2 text-sm text-red-600" | ||||
|                 > | ||||
|                   {{ form.errors[`education.${index}.fieldOfStudy`] }} | ||||
|                 </p> | ||||
|               </div> | ||||
|             </div> | ||||
|             <div class="items-center py-4 sm:grid sm:grid-cols-2"> | ||||
|               <label class="block text-sm font-medium text-gray-700 sm:mt-px"> | ||||
|                 Data rozpoczęcia | ||||
|               </label> | ||||
|               <div class="mt-1 sm:mt-0"> | ||||
|                 <MonthPicker | ||||
|                   v-model="element.startDate" | ||||
|                   placeholder="Wybierz datę" | ||||
|                   class="block w-full rounded-md shadow-sm sm:text-sm" | ||||
|                   :class="{ 'border-red-300 text-red-900 focus:outline-none focus:ring-red-500 focus:border-red-500': form.errors[`education.${index}.startDate`], 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors[`education.${index}.startDate`] }" | ||||
|                 /> | ||||
|                 <p | ||||
|                   v-if="form.errors[`education.${index}.startDate`]" | ||||
|                   class="mt-2 text-sm text-red-600" | ||||
|                 > | ||||
|                   {{ form.errors[`education.${index}.startDate`] }} | ||||
|                 </p> | ||||
|               </div> | ||||
|             </div> | ||||
|             <div class="items-center py-4 sm:grid sm:grid-cols-2"> | ||||
|               <label class="block text-sm font-medium text-gray-700 sm:mt-px"> | ||||
|                 Data zakończenia | ||||
|               </label> | ||||
|               <div class="mt-1 sm:mt-0"> | ||||
|                 <div class="space-y-2"> | ||||
|                   <label class="block text-sm font-medium text-gray-700 sm:mt-px"> | ||||
|                     <input | ||||
|                       v-model="element.current" | ||||
|                       type="checkbox" | ||||
|                       class="focus:ring-blumilk-500 h-4 w-4 text-blumilk-600 border-gray-300 rounded mr-1" | ||||
|                     > | ||||
|                     W trakcie | ||||
|                   </label> | ||||
|                   <MonthPicker | ||||
|                     v-model="element.endDate" | ||||
|                     placeholder="Wybierz datę" | ||||
|                     :disabled="element.current" | ||||
|                     class="block w-full rounded-md shadow-sm sm:text-sm disabled:bg-gray-100" | ||||
|                     :class="{ 'border-red-300 text-red-900 focus:outline-none focus:ring-red-500 focus:border-red-500': form.errors[`education.${index}.endDate`], 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors[`education.${index}.endDate`] }" | ||||
|                   /> | ||||
|                 </div> | ||||
|                 <p | ||||
|                   v-if="form.errors[`education.${index}.endDate`]" | ||||
|                   class="mt-2 text-sm text-red-600" | ||||
|                 > | ||||
|                   {{ form.errors[`education.${index}.endDate`] }} | ||||
|                 </p> | ||||
|               </div> | ||||
|             </div> | ||||
|           </template> | ||||
|         </DynamicSection> | ||||
|         <DynamicSection | ||||
|           v-model="form.languages" | ||||
|           header="Języki" | ||||
|           add-label="Dodaj język" | ||||
|           @add-item="addLanguage" | ||||
|           @remove-item="(index) => form.languages.splice(index, 1)" | ||||
|         > | ||||
|           <template #itemHeader="{ element, index }"> | ||||
|             <template v-if="hasAnyErrorInSection('languages', index)"> | ||||
|               <ExclamationCircleIcon class="h-6 w-6 mr-2 text-red-600 inline-block" /> | ||||
|             </template> | ||||
|             <template v-if="element.name"> | ||||
|               {{ element.name }} - <span :class="element.level.textColor">{{ element.level.name }}</span> | ||||
|             </template> | ||||
|             <template v-else> | ||||
|               (Nieokreślony) | ||||
|             </template> | ||||
|           </template> | ||||
|           <template #form="{ element, index }"> | ||||
|             <div class="gap-4 md:grid md:grid-cols-2 "> | ||||
|               <div class="py-4"> | ||||
|                 <label | ||||
|                   :for="`language-${index}-level`" | ||||
|                   class="block text-sm font-medium text-gray-700" | ||||
|                 > | ||||
|                   Język | ||||
|                 </label> | ||||
|                 <div class="mt-2"> | ||||
|                   <Combobox | ||||
|                     :id="`language-${index}-level`" | ||||
|                     v-model="element.name" | ||||
|                     :items="languages" | ||||
|                   /> | ||||
|                   <p | ||||
|                     v-if="form.errors[`languages.${index}.name`]" | ||||
|                     class="mt-2 text-sm text-red-600" | ||||
|                   > | ||||
|                     {{ form.errors[`languages.${index}.name`] }} | ||||
|                   </p> | ||||
|                 </div> | ||||
|               </div> | ||||
|               <div class="py-4"> | ||||
|                 <label | ||||
|                   :for="`language-level-${index}`" | ||||
|                   class="block text-sm font-medium text-gray-700" | ||||
|                 > | ||||
|                   Poziom - <span :class="element.level.textColor">{{ element.level.name }}</span> | ||||
|                 </label> | ||||
|                 <div class="mt-2"> | ||||
|                   <LevelPicker | ||||
|                     v-model.number="element.level" | ||||
|                     :levels="languageLevels" | ||||
|                   /> | ||||
|                   <p | ||||
|                     v-if="form.errors[`languages.${index}.level`]" | ||||
|                     class="mt-2 text-sm text-red-600" | ||||
|                   > | ||||
|                     {{ form.errors[`languages.${index}.level`] }} | ||||
|                   </p> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </template> | ||||
|         </DynamicSection> | ||||
|         <DynamicSection | ||||
|           v-model="form.technologies" | ||||
|           header="Technologie" | ||||
|           add-label="Dodaj technologię" | ||||
|           @add-item="addTechnology" | ||||
|           @remove-item="(index) => form.technologies.splice(index, 1)" | ||||
|         > | ||||
|           <template #itemHeader="{ element, index }"> | ||||
|             <template v-if="hasAnyErrorInSection('technologies', index)"> | ||||
|               <ExclamationCircleIcon class="h-6 w-6 mr-2 text-red-600 inline-block" /> | ||||
|             </template> | ||||
|             <template v-if="element.name"> | ||||
|               {{ element.name }} - <span :class="element.level.textColor">{{ element.level.name }}</span> | ||||
|             </template> | ||||
|             <template v-else> | ||||
|               (Nieokreślony) | ||||
|             </template> | ||||
|           </template> | ||||
|           <template #form="{ element, index }"> | ||||
|             <div class="gap-4 md:grid md:grid-cols-2 "> | ||||
|               <div class="py-4"> | ||||
|                 <label | ||||
|                   :for="`technology-${index}-level`" | ||||
|                   class="block text-sm font-medium text-gray-700" | ||||
|                 > | ||||
|                   Technologia | ||||
|                 </label> | ||||
|                 <div class="mt-2"> | ||||
|                   <Combobox | ||||
|                     :id="`technology-${index}-level`" | ||||
|                     v-model="element.name" | ||||
|                     :items="technologies" | ||||
|                   /> | ||||
|                   <p | ||||
|                     v-if="form.errors[`technologies.${index}.name`]" | ||||
|                     class="mt-2 text-sm text-red-600" | ||||
|                   > | ||||
|                     {{ form.errors[`technologies.${index}.name`] }} | ||||
|                   </p> | ||||
|                 </div> | ||||
|               </div> | ||||
|               <div class="py-4"> | ||||
|                 <label | ||||
|                   :for="`technology-level-${index}`" | ||||
|                   class="block text-sm font-medium text-gray-700" | ||||
|                 > | ||||
|                   Poziom - <span :class="element.level.textColor">{{ element.level.name }}</span> | ||||
|                 </label> | ||||
|                 <div class="mt-2"> | ||||
|                   <LevelPicker | ||||
|                     v-model.number="element.level" | ||||
|                     :levels="technologyLevels" | ||||
|                   /> | ||||
|                   <p | ||||
|                     v-if="form.errors[`technologies.${index}.level`]" | ||||
|                     class="mt-2 text-sm text-red-600" | ||||
|                   > | ||||
|                     {{ form.errors[`technologies.${index}.level`] }} | ||||
|                   </p> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </template> | ||||
|         </DynamicSection> | ||||
|         <DynamicSection | ||||
|           v-model="form.projects" | ||||
|           header="Projekty" | ||||
|           add-label="Dodaj projekt" | ||||
|           @add-item="addProject" | ||||
|           @remove-item="(index) => form.projects.splice(index, 1)" | ||||
|         > | ||||
|           <template #itemHeader="{ element, index }"> | ||||
|             <template v-if="hasAnyErrorInSection('projects', index)"> | ||||
|               <ExclamationCircleIcon class="h-6 w-6 mr-2 text-red-600 inline-block" /> | ||||
|             </template> | ||||
|             {{ element.description ? element.description : '(Nieokreślony)' }} | ||||
|           </template> | ||||
|           <template #form="{ element, index }"> | ||||
|             <div class="items-center py-4 sm:grid sm:grid-cols-2"> | ||||
|               <label | ||||
|                 :for="`project-description-${index}`" | ||||
|                 class="block text-sm font-medium text-gray-700 sm:mt-px" | ||||
|               > | ||||
|                 Opis projektu | ||||
|               </label> | ||||
|               <div class="mt-1 sm:mt-0"> | ||||
|                 <textarea | ||||
|                   :id="`project-description-${index}`" | ||||
|                   v-model="element.description" | ||||
|                   rows="5" | ||||
|                   class="block w-full rounded-md shadow-sm sm:text-sm" | ||||
|                   :class="{ 'border-red-300 text-red-900 focus:outline-none focus:ring-red-500 focus:border-red-500': form.errors[`projects.${index}.description`], 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors[`projects.${index}.description`] }" | ||||
|                 /> | ||||
|                 <p | ||||
|                   v-if="form.errors[`projects.${index}.description`]" | ||||
|                   class="mt-2 text-sm text-red-600" | ||||
|                 > | ||||
|                   {{ form.errors[`projects.${index}.description`] }} | ||||
|                 </p> | ||||
|               </div> | ||||
|             </div> | ||||
|             <div class="items-center py-4 sm:grid sm:grid-cols-2"> | ||||
|               <label | ||||
|                 :for="`project-technologies-${index}`" | ||||
|                 class="block text-sm font-medium text-gray-700 sm:mt-px" | ||||
|               > | ||||
|                 Technologie | ||||
|               </label> | ||||
|               <div class="mt-1 sm:mt-0"> | ||||
|                 <MultipleCombobox | ||||
|                   :id="`project-technologies-${index}`" | ||||
|                   v-model="element.technologies" | ||||
|                   :items="technologies" | ||||
|                 /> | ||||
|                 <p | ||||
|                   v-if="form.errors[`projects.${index}.technologies`]" | ||||
|                   class="mt-2 text-sm text-red-600" | ||||
|                 > | ||||
|                   {{ form.errors[`projects.${index}.technologies`] }} | ||||
|                 </p> | ||||
|               </div> | ||||
|             </div> | ||||
|             <div class="items-center py-4 sm:grid sm:grid-cols-2"> | ||||
|               <label | ||||
|                 :for="`project-startDate-${index}`" | ||||
|                 class="block text-sm font-medium text-gray-700 sm:mt-px" | ||||
|               > | ||||
|                 Data rozpoczęcia | ||||
|               </label> | ||||
|               <div class="mt-1 sm:mt-0"> | ||||
|                 <MonthPicker | ||||
|                   :id="`project-startDate-${index}`" | ||||
|                   v-model="element.startDate" | ||||
|                   placeholder="Wybierz datę" | ||||
|                   class="block w-full rounded-md shadow-sm sm:text-sm" | ||||
|                   :class="{ 'border-red-300 text-red-900 focus:outline-none focus:ring-red-500 focus:border-red-500': form.errors[`projects.${index}.startDate`], 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors[`projects.${index}.startDate`] }" | ||||
|                 /> | ||||
|                 <p | ||||
|                   v-if="form.errors[`projects.${index}.startDate`]" | ||||
|                   class="mt-2 text-sm text-red-600" | ||||
|                 > | ||||
|                   {{ form.errors[`projects.${index}.startDate`] }} | ||||
|                 </p> | ||||
|               </div> | ||||
|             </div> | ||||
|             <div class="items-center py-4 sm:grid sm:grid-cols-2"> | ||||
|               <label | ||||
|                 :for="`project-endDate-${index}`" | ||||
|                 class="block text-sm font-medium text-gray-700 sm:mt-px" | ||||
|               > | ||||
|                 Data zakończenia | ||||
|               </label> | ||||
|               <div class="mt-1 sm:mt-0"> | ||||
|                 <div class="space-y-2"> | ||||
|                   <label class="block text-sm font-medium text-gray-700 sm:mt-px"> | ||||
|                     <input | ||||
|                       v-model="element.current" | ||||
|                       type="checkbox" | ||||
|                       class="focus:ring-blumilk-500 h-4 w-4 text-blumilk-600 border-gray-300 rounded mr-1" | ||||
|                     > | ||||
|                     W trakcie | ||||
|                   </label> | ||||
|                   <MonthPicker | ||||
|                     :id="`project-endDate-${index}`" | ||||
|                     v-model="element.endDate" | ||||
|                     placeholder="Wybierz datę" | ||||
|                     :disabled="element.current" | ||||
|                     class="block w-full rounded-md shadow-sm sm:text-sm disabled:bg-gray-100" | ||||
|                     :class="{ 'border-red-300 text-red-900 focus:outline-none focus:ring-red-500 focus:border-red-500': form.errors[`projects.${index}.endDate`], 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors[`projects.${index}.endDate`] }" | ||||
|                   /> | ||||
|                 </div> | ||||
|                 <p | ||||
|                   v-if="form.errors[`projects.${index}.endDate`]" | ||||
|                   class="mt-2 text-sm text-red-600" | ||||
|                 > | ||||
|                   {{ form.errors[`projects.${index}.endDate`] }} | ||||
|                 </p> | ||||
|               </div> | ||||
|             </div> | ||||
|             <div class="items-center py-4 sm:grid sm:grid-cols-2"> | ||||
|               <label | ||||
|                 :for="`project-tasks-${index}`" | ||||
|                 class="block text-sm font-medium text-gray-700 sm:mt-px" | ||||
|               > | ||||
|                 Zadania | ||||
|               </label> | ||||
|               <div class="mt-1 sm:mt-0"> | ||||
|                 <textarea | ||||
|                   :id="`project-tasks-${index}`" | ||||
|                   v-model="element.tasks" | ||||
|                   rows="5" | ||||
|                   class="block w-full rounded-md shadow-sm sm:text-sm" | ||||
|                   :class="{ 'border-red-300 text-red-900 focus:outline-none focus:ring-red-500 focus:border-red-500': form.errors[`projects.${index}.tasks`], 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors[`projects.${index}.tasks`] }" | ||||
|                 /> | ||||
|                 <p | ||||
|                   v-if="form.errors[`projects.${index}.tasks`]" | ||||
|                   class="mt-2 text-sm text-red-600" | ||||
|                 > | ||||
|                   {{ form.errors[`projects.${index}.tasks`] }} | ||||
|                 </p> | ||||
|               </div> | ||||
|             </div> | ||||
|           </template> | ||||
|         </DynamicSection> | ||||
|         <div class="pt-5"> | ||||
|           <div class="flex justify-end"> | ||||
|             <InertiaLink | ||||
|               href="/resumes" | ||||
|               class="py-2 px-4 text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blumilk-500 focus:ring-offset-2 shadow-sm" | ||||
|             > | ||||
|               Anuluj | ||||
|             </InertiaLink> | ||||
|             <button | ||||
|               type="submit" | ||||
|               class="inline-flex justify-center py-2 px-4 ml-3 text-sm font-medium text-white bg-blumilk-600 hover:bg-blumilk-700 rounded-md border border-transparent focus:outline-none focus:ring-2 focus:ring-blumilk-500 focus:ring-offset-2 shadow-sm" | ||||
|             > | ||||
|               Zapisz | ||||
|             </button> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </form> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup> | ||||
| import { Listbox, ListboxOption, ListboxOptions, ListboxLabel, ListboxButton } from '@headlessui/vue' | ||||
| import { SelectorIcon, CheckIcon } from '@heroicons/vue/outline' | ||||
| import { ExclamationCircleIcon } from '@heroicons/vue/solid' | ||||
| import { useForm } from '@inertiajs/inertia-vue3' | ||||
| import MonthPicker from '@/Shared/Forms/MonthPicker' | ||||
| import DynamicSection from '@/Shared/Forms/DynamicSection' | ||||
| import Combobox from '@/Shared/Forms/Combobox' | ||||
| import MultipleCombobox from '@/Shared/Forms/MultipleCombobox' | ||||
| import LevelPicker from '@/Shared/Forms/LevelPicker' | ||||
| import useLevels from '@/Composables/useLevels' | ||||
|  | ||||
| const props = defineProps({ | ||||
|   users: Object, | ||||
|   technologies: Array, | ||||
| }) | ||||
|  | ||||
| const { technologyLevels, languageLevels } = useLevels() | ||||
|  | ||||
| const languages = [ | ||||
|   'Polish', | ||||
|   'English', | ||||
|   'German', | ||||
| ] | ||||
|  | ||||
| const form = useForm('createResume',{ | ||||
|   user: props.users.data[0], | ||||
|   name: null, | ||||
|   educations: [], | ||||
|   projects: [], | ||||
|   technologies: [], | ||||
|   languages: [], | ||||
| }) | ||||
|  | ||||
| function addProject() { | ||||
|   form.projects.push({ | ||||
|     description: null, | ||||
|     technologies: [], | ||||
|     tasks: null, | ||||
|     startDate: null, | ||||
|     endDate: null, | ||||
|     current: false, | ||||
|   }) | ||||
| } | ||||
|  | ||||
| function addTechnology() { | ||||
|   form.technologies.push({ | ||||
|     name: null, | ||||
|     level: technologyLevels[0], | ||||
|   }) | ||||
| } | ||||
|  | ||||
| function addEducation() { | ||||
|   form.educations.push({ | ||||
|     school: null, | ||||
|     degree: null, | ||||
|     fieldOfStudy: null, | ||||
|     startDate: null, | ||||
|     endDate: null, | ||||
|     current: false, | ||||
|   }) | ||||
| } | ||||
|  | ||||
| function addLanguage() { | ||||
|   form.languages.push({ | ||||
|     name: null, | ||||
|     level: languageLevels[0], | ||||
|   }) | ||||
| } | ||||
|  | ||||
| function hasAnyErrorInSection(section, index) { | ||||
|   return Object | ||||
|     .keys(form.errors) | ||||
|     .some((error) => error.startsWith(`${section}.${index}.`)) | ||||
| } | ||||
|  | ||||
| function submitResume() { | ||||
|   form | ||||
|     .transform((data) => ({ | ||||
|       user: data.user?.id, | ||||
|       name: data.name, | ||||
|       education: data.educations.map(education => ({ | ||||
|         ...education, | ||||
|         current: !!education.current, | ||||
|         endDate: education.current ? null: education.endDate, | ||||
|       })), | ||||
|       languages: data.languages.map(language => ({ | ||||
|         name: language.name, | ||||
|         level: language.level.level, | ||||
|       })), | ||||
|       technologies: data.technologies.map(technology => ({ | ||||
|         name: technology.name, | ||||
|         level: technology.level.level, | ||||
|       })), | ||||
|       projects: data.projects.map(project => ({ | ||||
|         ...project, | ||||
|         current: !!project.current, | ||||
|         endDate: project.current ? null : project.endDate, | ||||
|       })), | ||||
|     })) | ||||
|     .post('/resumes') | ||||
| } | ||||
| </script> | ||||
							
								
								
									
										654
									
								
								resources/js/Pages/Resumes/Edit.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										654
									
								
								resources/js/Pages/Resumes/Edit.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,654 @@ | ||||
| <template> | ||||
|   <InertiaHead title="Edycja CV" /> | ||||
|   <div class="mx-auto w-full max-w-7xl bg-white shadow-md"> | ||||
|     <div class="p-4 sm:px-6"> | ||||
|       <h2 class="text-lg font-medium leading-6 text-gray-900"> | ||||
|         Edytuj CV | ||||
|       </h2> | ||||
|     </div> | ||||
|     <form | ||||
|       class="flex flex-col justify-center py-8 px-6 space-y-8 border-t border-gray-200" | ||||
|       @submit.prevent="submitResume" | ||||
|     > | ||||
|       <div class="space-y-8 sm:space-y-5"> | ||||
|         <div> | ||||
|           <h3 class="text-lg font-medium leading-6 text-gray-900"> | ||||
|             Dane podstawowe | ||||
|           </h3> | ||||
|           <div class="grid grid-cols-2 gap-8 py-4"> | ||||
|             <Listbox | ||||
|               v-model="form.user" | ||||
|               as="div" | ||||
|             > | ||||
|               <ListboxLabel class="block text-sm font-medium text-gray-700"> | ||||
|                 Użytkownik | ||||
|               </ListboxLabel> | ||||
|               <div class="relative mt-2"> | ||||
|                 <ListboxButton | ||||
|                   class="relative py-2 pr-10 pl-3 w-full max-w-md h-10 text-left bg-white rounded-md border border-gray-300 focus:border-blumilk-500 focus:outline-none focus:ring-1 focus:ring-blumilk-500 shadow-sm cursor-default sm:text-sm" | ||||
|                 > | ||||
|                   <span v-if="form.user === null"> | ||||
|                     Nie istnieje w bazie | ||||
|                   </span> | ||||
|                   <span | ||||
|                     v-else | ||||
|                     class="flex items-center" | ||||
|                   > | ||||
|                     <img | ||||
|                       :src="form.user.avatar" | ||||
|                       class="shrink-0 w-6 h-6 rounded-full" | ||||
|                     > | ||||
|                     <span class="block ml-3 truncate">{{ form.user.name }}</span> | ||||
|                   </span> | ||||
|                   <span class="flex absolute inset-y-0 right-0 items-center pr-2 pointer-events-none"> | ||||
|                     <SelectorIcon class="w-5 h-5 text-gray-400" /> | ||||
|                   </span> | ||||
|                 </ListboxButton> | ||||
|  | ||||
|                 <transition | ||||
|                   leave-active-class="transition ease-in duration-100" | ||||
|                   leave-from-class="opacity-100" | ||||
|                   leave-to-class="opacity-0" | ||||
|                 > | ||||
|                   <ListboxOptions | ||||
|                     class="overflow-auto absolute z-10 py-1 mt-1 w-full max-w-lg max-h-60 text-base bg-white rounded-md focus:outline-none ring-1 ring-black ring-opacity-5 shadow-lg sm:text-sm" | ||||
|                   > | ||||
|                     <ListboxOption | ||||
|                       v-slot="{ active }" | ||||
|                       as="template" | ||||
|                       :value="null" | ||||
|                     > | ||||
|                       <li | ||||
|                         :class="[active ? 'bg-gray-100' : 'text-gray-900', 'cursor-default select-none relative py-2 pl-3 pr-9']" | ||||
|                       > | ||||
|                         <div class="flex items-center"> | ||||
|                           Nie istnieje w bazie | ||||
|                         </div> | ||||
|  | ||||
|                         <span | ||||
|                           v-if="form.user === null" | ||||
|                           :class="['text-blumilk-600 absolute inset-y-0 right-0 flex items-center pr-4']" | ||||
|                         > | ||||
|                           <CheckIcon class="w-5 h-5" /> | ||||
|                         </span> | ||||
|                       </li> | ||||
|                     </ListboxOption> | ||||
|                     <ListboxOption | ||||
|                       v-for="user in users.data" | ||||
|                       :key="user.id" | ||||
|                       v-slot="{ active }" | ||||
|                       as="template" | ||||
|                       :value="user" | ||||
|                     > | ||||
|                       <li | ||||
|                         :class="[active ? 'bg-gray-100' : 'text-gray-900', 'cursor-default select-none relative py-2 pl-3 pr-9']" | ||||
|                       > | ||||
|                         <div class="flex items-center"> | ||||
|                           <img | ||||
|                             :src="user.avatar" | ||||
|                             class="shrink-0 w-6 h-6 rounded-full" | ||||
|                           > | ||||
|                           <span | ||||
|                             :class="[form.user?.id === user.id ? 'font-semibold' : 'font-normal', 'ml-3 block truncate']" | ||||
|                           > | ||||
|                             {{ user.name }} | ||||
|                           </span> | ||||
|                         </div> | ||||
|                         <span | ||||
|                           v-if="form.user?.id === user.id" | ||||
|                           :class="['text-blumilk-600 absolute inset-y-0 right-0 flex items-center pr-4']" | ||||
|                         > | ||||
|                           <CheckIcon class="w-5 h-5" /> | ||||
|                         </span> | ||||
|                       </li> | ||||
|                     </ListboxOption> | ||||
|                   </ListboxOptions> | ||||
|                 </transition> | ||||
|               </div> | ||||
|             </Listbox> | ||||
|             <div v-if="form.user === null"> | ||||
|               <label | ||||
|                 for="name" | ||||
|                 class="block text-sm font-medium text-gray-700 sm:mt-px" | ||||
|               > | ||||
|                 Imię i nazwisko | ||||
|               </label> | ||||
|               <div class="mt-2"> | ||||
|                 <input | ||||
|                   id="name" | ||||
|                   v-model="form.name" | ||||
|                   type="text" | ||||
|                   class="block w-full max-w-md rounded-md shadow-sm sm:text-sm" | ||||
|                   :class="{ 'border-red-300 text-red-900 focus:outline-none focus:ring-red-500 focus:border-red-500': form.errors.name, 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors.name }" | ||||
|                 > | ||||
|                 <p | ||||
|                   v-if="form.errors.name" | ||||
|                   class="mt-2 text-sm text-red-600" | ||||
|                 > | ||||
|                   {{ form.errors.name }} | ||||
|                 </p> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|         <DynamicSection | ||||
|           v-model="form.educations" | ||||
|           header="Edukacja" | ||||
|           add-label="Dodaj szkołę" | ||||
|           @add-item="addEducation" | ||||
|           @remove-item="(index) => form.educations.splice(index, 1)" | ||||
|         > | ||||
|           <template #itemHeader="{ element, index }"> | ||||
|             <template v-if="hasAnyErrorInSection('education', index)"> | ||||
|               <ExclamationCircleIcon class="h-6 w-6 mr-2 text-red-600 inline-block" /> | ||||
|             </template> | ||||
|             {{ element.school ? element.school : '(Nieokreślony)' }} | ||||
|           </template> | ||||
|           <template #form="{ element, index }"> | ||||
|             <div class="items-center py-4 sm:grid sm:grid-cols-2"> | ||||
|               <label class="block text-sm font-medium text-gray-700 sm:mt-px"> | ||||
|                 Szkoła | ||||
|               </label> | ||||
|               <div class="mt-1 sm:mt-0"> | ||||
|                 <input | ||||
|                   v-model="element.school" | ||||
|                   type="text" | ||||
|                   class="block w-full rounded-md shadow-sm sm:text-sm" | ||||
|                   :class="{ 'border-red-300 text-red-900 focus:outline-none focus:ring-red-500 focus:border-red-500': form.errors[`education.${index}.school`], 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors[`education.${index}.school`] }" | ||||
|                 > | ||||
|                 <p | ||||
|                   v-if="form.errors[`education.${index}.school`]" | ||||
|                   class="mt-2 text-sm text-red-600" | ||||
|                 > | ||||
|                   {{ form.errors[`education.${index}.school`] }} | ||||
|                 </p> | ||||
|               </div> | ||||
|             </div> | ||||
|             <div class="items-center py-4 sm:grid sm:grid-cols-2"> | ||||
|               <label class="block text-sm font-medium text-gray-700 sm:mt-px"> | ||||
|                 Stopień | ||||
|               </label> | ||||
|               <div class="mt-1 sm:mt-0"> | ||||
|                 <input | ||||
|                   v-model="element.degree" | ||||
|                   type="text" | ||||
|                   class="block w-full rounded-md shadow-sm sm:text-sm" | ||||
|                   :class="{ 'border-red-300 text-red-900 focus:outline-none focus:ring-red-500 focus:border-red-500': form.errors[`education.${index}.degree`], 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors[`education.${index}.degree`] }" | ||||
|                 > | ||||
|                 <p | ||||
|                   v-if="form.errors[`education.${index}.degree`]" | ||||
|                   class="mt-2 text-sm text-red-600" | ||||
|                 > | ||||
|                   {{ form.errors[`education.${index}.degree`] }} | ||||
|                 </p> | ||||
|               </div> | ||||
|             </div> | ||||
|             <div class="items-center py-4 sm:grid sm:grid-cols-2"> | ||||
|               <label class="block text-sm font-medium text-gray-700 sm:mt-px"> | ||||
|                 Kierunek/Specjalizacja | ||||
|               </label> | ||||
|               <div class="mt-1 sm:mt-0"> | ||||
|                 <input | ||||
|                   v-model="element.fieldOfStudy" | ||||
|                   type="text" | ||||
|                   class="block w-full rounded-md shadow-sm sm:text-sm" | ||||
|                   :class="{ 'border-red-300 text-red-900 focus:outline-none focus:ring-red-500 focus:border-red-500': form.errors[`education.${index}.fieldOfStudy`], 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors[`education.${index}.fieldOfStudy`] }" | ||||
|                 > | ||||
|                 <p | ||||
|                   v-if="form.errors[`education.${index}.fieldOfStudy`]" | ||||
|                   class="mt-2 text-sm text-red-600" | ||||
|                 > | ||||
|                   {{ form.errors[`education.${index}.fieldOfStudy`] }} | ||||
|                 </p> | ||||
|               </div> | ||||
|             </div> | ||||
|             <div class="items-center py-4 sm:grid sm:grid-cols-2"> | ||||
|               <label class="block text-sm font-medium text-gray-700 sm:mt-px"> | ||||
|                 Data rozpoczęcia | ||||
|               </label> | ||||
|               <div class="mt-1 sm:mt-0"> | ||||
|                 <MonthPicker | ||||
|                   v-model="element.startDate" | ||||
|                   placeholder="Wybierz datę" | ||||
|                   class="block w-full rounded-md shadow-sm sm:text-sm" | ||||
|                   :class="{ 'border-red-300 text-red-900 focus:outline-none focus:ring-red-500 focus:border-red-500': form.errors[`education.${index}.startDate`], 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors[`education.${index}.startDate`] }" | ||||
|                 /> | ||||
|                 <p | ||||
|                   v-if="form.errors[`education.${index}.startDate`]" | ||||
|                   class="mt-2 text-sm text-red-600" | ||||
|                 > | ||||
|                   {{ form.errors[`education.${index}.startDate`] }} | ||||
|                 </p> | ||||
|               </div> | ||||
|             </div> | ||||
|             <div class="items-center py-4 sm:grid sm:grid-cols-2"> | ||||
|               <label class="block text-sm font-medium text-gray-700 sm:mt-px"> | ||||
|                 Data zakończenia | ||||
|               </label> | ||||
|               <div class="mt-1 sm:mt-0"> | ||||
|                 <div class="space-y-2"> | ||||
|                   <label class="block text-sm font-medium text-gray-700 sm:mt-px"> | ||||
|                     <input | ||||
|                       v-model="element.current" | ||||
|                       type="checkbox" | ||||
|                       class="focus:ring-blumilk-500 h-4 w-4 text-blumilk-600 border-gray-300 rounded mr-1" | ||||
|                     > | ||||
|                     W trakcie | ||||
|                   </label> | ||||
|                   <MonthPicker | ||||
|                     v-model="element.endDate" | ||||
|                     placeholder="Wybierz datę" | ||||
|                     :disabled="element.current" | ||||
|                     class="block w-full rounded-md shadow-sm sm:text-sm disabled:bg-gray-100" | ||||
|                     :class="{ 'border-red-300 text-red-900 focus:outline-none focus:ring-red-500 focus:border-red-500': form.errors[`education.${index}.endDate`], 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors[`education.${index}.endDate`] }" | ||||
|                   /> | ||||
|                 </div> | ||||
|                 <p | ||||
|                   v-if="form.errors[`education.${index}.endDate`]" | ||||
|                   class="mt-2 text-sm text-red-600" | ||||
|                 > | ||||
|                   {{ form.errors[`education.${index}.endDate`] }} | ||||
|                 </p> | ||||
|               </div> | ||||
|             </div> | ||||
|           </template> | ||||
|         </DynamicSection> | ||||
|         <DynamicSection | ||||
|           v-model="form.languages" | ||||
|           header="Języki" | ||||
|           add-label="Dodaj język" | ||||
|           @add-item="addLanguage" | ||||
|           @remove-item="(index) => form.languages.splice(index, 1)" | ||||
|         > | ||||
|           <template #itemHeader="{ element, index }"> | ||||
|             <template v-if="hasAnyErrorInSection('languages', index)"> | ||||
|               <ExclamationCircleIcon class="h-6 w-6 mr-2 text-red-600 inline-block" /> | ||||
|             </template> | ||||
|             <template v-if="element.name"> | ||||
|               {{ element.name }} - <span :class="element.level.textColor">{{ element.level.name }}</span> | ||||
|             </template> | ||||
|             <template v-else> | ||||
|               (Nieokreślony) | ||||
|             </template> | ||||
|           </template> | ||||
|           <template #form="{ element, index }"> | ||||
|             <div class="gap-4 md:grid md:grid-cols-2 "> | ||||
|               <div class="py-4"> | ||||
|                 <label | ||||
|                   :for="`language-${index}-level`" | ||||
|                   class="block text-sm font-medium text-gray-700" | ||||
|                 > | ||||
|                   Język | ||||
|                 </label> | ||||
|                 <div class="mt-2"> | ||||
|                   <Combobox | ||||
|                     :id="`language-${index}-level`" | ||||
|                     v-model="element.name" | ||||
|                     :items="languages" | ||||
|                   /> | ||||
|                   <p | ||||
|                     v-if="form.errors[`languages.${index}.name`]" | ||||
|                     class="mt-2 text-sm text-red-600" | ||||
|                   > | ||||
|                     {{ form.errors[`languages.${index}.name`] }} | ||||
|                   </p> | ||||
|                 </div> | ||||
|               </div> | ||||
|               <div class="py-4"> | ||||
|                 <label | ||||
|                   :for="`language-level-${index}`" | ||||
|                   class="block text-sm font-medium text-gray-700" | ||||
|                 > | ||||
|                   Poziom - <span :class="element.level.textColor">{{ element.level.name }}</span> | ||||
|                 </label> | ||||
|                 <div class="mt-2"> | ||||
|                   <LevelPicker | ||||
|                     v-model.number="element.level" | ||||
|                     :levels="languageLevels" | ||||
|                   /> | ||||
|                   <p | ||||
|                     v-if="form.errors[`languages.${index}.level`]" | ||||
|                     class="mt-2 text-sm text-red-600" | ||||
|                   > | ||||
|                     {{ form.errors[`languages.${index}.level`] }} | ||||
|                   </p> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </template> | ||||
|         </DynamicSection> | ||||
|         <DynamicSection | ||||
|           v-model="form.technologies" | ||||
|           header="Technologie" | ||||
|           add-label="Dodaj technologię" | ||||
|           @add-item="addTechnology" | ||||
|           @remove-item="(index) => form.technologies.splice(index, 1)" | ||||
|         > | ||||
|           <template #itemHeader="{ element, index }"> | ||||
|             <template v-if="hasAnyErrorInSection('technologies', index)"> | ||||
|               <ExclamationCircleIcon class="h-6 w-6 mr-2 text-red-600 inline-block" /> | ||||
|             </template> | ||||
|             <template v-if="element.name"> | ||||
|               {{ element.name }} - <span :class="element.level.textColor">{{ element.level.name }}</span> | ||||
|             </template> | ||||
|             <template v-else> | ||||
|               (Nieokreślony) | ||||
|             </template> | ||||
|           </template> | ||||
|           <template #form="{ element, index }"> | ||||
|             <div class="gap-4 md:grid md:grid-cols-2 "> | ||||
|               <div class="py-4"> | ||||
|                 <label | ||||
|                   :for="`technology-${index}-level`" | ||||
|                   class="block text-sm font-medium text-gray-700" | ||||
|                 > | ||||
|                   Technologia | ||||
|                 </label> | ||||
|                 <div class="mt-2"> | ||||
|                   <Combobox | ||||
|                     :id="`technology-${index}-level`" | ||||
|                     v-model="element.name" | ||||
|                     :items="technologies" | ||||
|                   /> | ||||
|                   <p | ||||
|                     v-if="form.errors[`technologies.${index}.name`]" | ||||
|                     class="mt-2 text-sm text-red-600" | ||||
|                   > | ||||
|                     {{ form.errors[`technologies.${index}.name`] }} | ||||
|                   </p> | ||||
|                 </div> | ||||
|               </div> | ||||
|               <div class="py-4"> | ||||
|                 <label | ||||
|                   :for="`technology-level-${index}`" | ||||
|                   class="block text-sm font-medium text-gray-700" | ||||
|                 > | ||||
|                   Poziom - <span :class="element.level.textColor">{{ element.level.name }}</span> | ||||
|                 </label> | ||||
|                 <div class="mt-2"> | ||||
|                   <LevelPicker | ||||
|                     v-model.number="element.level" | ||||
|                     :levels="technologyLevels" | ||||
|                   /> | ||||
|                   <p | ||||
|                     v-if="form.errors[`technologies.${index}.level`]" | ||||
|                     class="mt-2 text-sm text-red-600" | ||||
|                   > | ||||
|                     {{ form.errors[`technologies.${index}.level`] }} | ||||
|                   </p> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </template> | ||||
|         </DynamicSection> | ||||
|         <DynamicSection | ||||
|           v-model="form.projects" | ||||
|           header="Projekty" | ||||
|           add-label="Dodaj projekt" | ||||
|           @add-item="addProject" | ||||
|           @remove-item="(index) => form.projects.splice(index, 1)" | ||||
|         > | ||||
|           <template #itemHeader="{ element, index }"> | ||||
|             <template v-if="hasAnyErrorInSection('projects', index)"> | ||||
|               <ExclamationCircleIcon class="h-6 w-6 mr-2 text-red-600 inline-block" /> | ||||
|             </template> | ||||
|             {{ element.description ? element.description : '(Nieokreślony)' }} | ||||
|           </template> | ||||
|           <template #form="{ element, index }"> | ||||
|             <div class="items-center py-4 sm:grid sm:grid-cols-2"> | ||||
|               <label | ||||
|                 :for="`project-description-${index}`" | ||||
|                 class="block text-sm font-medium text-gray-700 sm:mt-px" | ||||
|               > | ||||
|                 Opis projektu | ||||
|               </label> | ||||
|               <div class="mt-1 sm:mt-0"> | ||||
|                 <textarea | ||||
|                   :id="`project-description-${index}`" | ||||
|                   v-model="element.description" | ||||
|                   rows="5" | ||||
|                   class="block w-full rounded-md shadow-sm sm:text-sm" | ||||
|                   :class="{ 'border-red-300 text-red-900 focus:outline-none focus:ring-red-500 focus:border-red-500': form.errors[`projects.${index}.description`], 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors[`projects.${index}.description`] }" | ||||
|                 /> | ||||
|                 <p | ||||
|                   v-if="form.errors[`projects.${index}.description`]" | ||||
|                   class="mt-2 text-sm text-red-600" | ||||
|                 > | ||||
|                   {{ form.errors[`projects.${index}.description`] }} | ||||
|                 </p> | ||||
|               </div> | ||||
|             </div> | ||||
|             <div class="items-center py-4 sm:grid sm:grid-cols-2"> | ||||
|               <label | ||||
|                 :for="`project-technologies-${index}`" | ||||
|                 class="block text-sm font-medium text-gray-700 sm:mt-px" | ||||
|               > | ||||
|                 Technologie | ||||
|               </label> | ||||
|               <div class="mt-1 sm:mt-0"> | ||||
|                 <MultipleCombobox | ||||
|                   :id="`project-technologies-${index}`" | ||||
|                   v-model="element.technologies" | ||||
|                   :items="technologies" | ||||
|                 /> | ||||
|                 <p | ||||
|                   v-if="form.errors[`projects.${index}.technologies`]" | ||||
|                   class="mt-2 text-sm text-red-600" | ||||
|                 > | ||||
|                   {{ form.errors[`projects.${index}.technologies`] }} | ||||
|                 </p> | ||||
|               </div> | ||||
|             </div> | ||||
|             <div class="items-center py-4 sm:grid sm:grid-cols-2"> | ||||
|               <label | ||||
|                 :for="`project-startDate-${index}`" | ||||
|                 class="block text-sm font-medium text-gray-700 sm:mt-px" | ||||
|               > | ||||
|                 Data rozpoczęcia | ||||
|               </label> | ||||
|               <div class="mt-1 sm:mt-0"> | ||||
|                 <MonthPicker | ||||
|                   :id="`project-startDate-${index}`" | ||||
|                   v-model="element.startDate" | ||||
|                   placeholder="Wybierz datę" | ||||
|                   class="block w-full rounded-md shadow-sm sm:text-sm" | ||||
|                   :class="{ 'border-red-300 text-red-900 focus:outline-none focus:ring-red-500 focus:border-red-500': form.errors[`projects.${index}.startDate`], 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors[`projects.${index}.startDate`] }" | ||||
|                 /> | ||||
|                 <p | ||||
|                   v-if="form.errors[`projects.${index}.startDate`]" | ||||
|                   class="mt-2 text-sm text-red-600" | ||||
|                 > | ||||
|                   {{ form.errors[`projects.${index}.startDate`] }} | ||||
|                 </p> | ||||
|               </div> | ||||
|             </div> | ||||
|             <div class="items-center py-4 sm:grid sm:grid-cols-2"> | ||||
|               <label | ||||
|                 :for="`project-endDate-${index}`" | ||||
|                 class="block text-sm font-medium text-gray-700 sm:mt-px" | ||||
|               > | ||||
|                 Data zakończenia | ||||
|               </label> | ||||
|               <div class="mt-1 sm:mt-0"> | ||||
|                 <div class="space-y-2"> | ||||
|                   <label class="block text-sm font-medium text-gray-700 sm:mt-px"> | ||||
|                     <input | ||||
|                       v-model="element.current" | ||||
|                       type="checkbox" | ||||
|                       class="focus:ring-blumilk-500 h-4 w-4 text-blumilk-600 border-gray-300 rounded mr-1" | ||||
|                     > | ||||
|                     W trakcie | ||||
|                   </label> | ||||
|                   <MonthPicker | ||||
|                     :id="`project-endDate-${index}`" | ||||
|                     v-model="element.endDate" | ||||
|                     placeholder="Wybierz datę" | ||||
|                     :disabled="element.current" | ||||
|                     class="block w-full rounded-md shadow-sm sm:text-sm disabled:bg-gray-100" | ||||
|                     :class="{ 'border-red-300 text-red-900 focus:outline-none focus:ring-red-500 focus:border-red-500': form.errors[`projects.${index}.endDate`], 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors[`projects.${index}.endDate`] }" | ||||
|                   /> | ||||
|                 </div> | ||||
|                 <p | ||||
|                   v-if="form.errors[`projects.${index}.endDate`]" | ||||
|                   class="mt-2 text-sm text-red-600" | ||||
|                 > | ||||
|                   {{ form.errors[`projects.${index}.endDate`] }} | ||||
|                 </p> | ||||
|               </div> | ||||
|             </div> | ||||
|             <div class="items-center py-4 sm:grid sm:grid-cols-2"> | ||||
|               <label | ||||
|                 :for="`project-tasks-${index}`" | ||||
|                 class="block text-sm font-medium text-gray-700 sm:mt-px" | ||||
|               > | ||||
|                 Zadania | ||||
|               </label> | ||||
|               <div class="mt-1 sm:mt-0"> | ||||
|                 <textarea | ||||
|                   :id="`project-tasks-${index}`" | ||||
|                   v-model="element.tasks" | ||||
|                   rows="5" | ||||
|                   class="block w-full rounded-md shadow-sm sm:text-sm" | ||||
|                   :class="{ 'border-red-300 text-red-900 focus:outline-none focus:ring-red-500 focus:border-red-500': form.errors[`projects.${index}.tasks`], 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors[`projects.${index}.tasks`] }" | ||||
|                 /> | ||||
|                 <p | ||||
|                   v-if="form.errors[`projects.${index}.tasks`]" | ||||
|                   class="mt-2 text-sm text-red-600" | ||||
|                 > | ||||
|                   {{ form.errors[`projects.${index}.tasks`] }} | ||||
|                 </p> | ||||
|               </div> | ||||
|             </div> | ||||
|           </template> | ||||
|         </DynamicSection> | ||||
|         <div class="pt-5"> | ||||
|           <div class="flex justify-end"> | ||||
|             <InertiaLink | ||||
|               href="/resumes" | ||||
|               class="py-2 px-4 text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blumilk-500 focus:ring-offset-2 shadow-sm" | ||||
|             > | ||||
|               Anuluj | ||||
|             </InertiaLink> | ||||
|             <button | ||||
|               type="submit" | ||||
|               class="inline-flex justify-center py-2 px-4 ml-3 text-sm font-medium text-white bg-blumilk-600 hover:bg-blumilk-700 rounded-md border border-transparent focus:outline-none focus:ring-2 focus:ring-blumilk-500 focus:ring-offset-2 shadow-sm" | ||||
|             > | ||||
|               Zapisz | ||||
|             </button> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </form> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup> | ||||
| import { Listbox, ListboxOption, ListboxOptions, ListboxLabel, ListboxButton } from '@headlessui/vue' | ||||
| import { SelectorIcon, CheckIcon } from '@heroicons/vue/outline' | ||||
| import { ExclamationCircleIcon } from '@heroicons/vue/solid' | ||||
| import { useForm } from '@inertiajs/inertia-vue3' | ||||
| import MonthPicker from '@/Shared/Forms/MonthPicker' | ||||
| import DynamicSection from '@/Shared/Forms/DynamicSection' | ||||
| import Combobox from '@/Shared/Forms/Combobox' | ||||
| import MultipleCombobox from '@/Shared/Forms/MultipleCombobox' | ||||
| import LevelPicker from '@/Shared/Forms/LevelPicker' | ||||
| import useLevels from '@/Composables/useLevels' | ||||
|  | ||||
| const props = defineProps({ | ||||
|   users: Object, | ||||
|   technologies: Array, | ||||
|   resume: Object, | ||||
| }) | ||||
|  | ||||
| const { technologyLevels, languageLevels } = useLevels() | ||||
|  | ||||
| const languages = [ | ||||
|   'Polish', | ||||
|   'English', | ||||
|   'German', | ||||
| ] | ||||
|  | ||||
| const form = useForm(`EditResume:${props.resume.id}`,{ | ||||
|   user: props.users.data.find((user) => user.id === props.resume.user) ?? null, | ||||
|   name: props.resume.name ?? null , | ||||
|   educations: props.resume.education ?? [], | ||||
|   projects: props.resume.projects ?? [], | ||||
|   technologies: props.resume.technologies.map((technology) => ({ | ||||
|     name: technology.name, | ||||
|     level: technologyLevels.find((level) => level.level === technology.level), | ||||
|   })) ?? [], | ||||
|   languages: props.resume.languages.map((language) => ({ | ||||
|     name: language.name, | ||||
|     level: languageLevels.find((level) => level.level === language.level), | ||||
|   })) ?? [], | ||||
| }) | ||||
|  | ||||
| function addProject() { | ||||
|   form.projects.push({ | ||||
|     description: null, | ||||
|     technologies: [], | ||||
|     tasks: null, | ||||
|     startDate: null, | ||||
|     endDate: null, | ||||
|     current: false, | ||||
|   }) | ||||
| } | ||||
|  | ||||
| function addTechnology() { | ||||
|   form.technologies.push({ | ||||
|     name: null, | ||||
|     level: technologyLevels[0], | ||||
|   }) | ||||
| } | ||||
|  | ||||
| function addEducation() { | ||||
|   form.educations.push({ | ||||
|     school: null, | ||||
|     degree: null, | ||||
|     fieldOfStudy: null, | ||||
|     startDate: null, | ||||
|     endDate: null, | ||||
|     current: false, | ||||
|   }) | ||||
| } | ||||
|  | ||||
| function addLanguage() { | ||||
|   form.languages.push({ | ||||
|     name: null, | ||||
|     level: languageLevels[0], | ||||
|   }) | ||||
| } | ||||
|  | ||||
| function hasAnyErrorInSection(section, index) { | ||||
|   return Object | ||||
|     .keys(form.errors) | ||||
|     .some((error) => error.startsWith(`${section}.${index}.`)) | ||||
| } | ||||
|  | ||||
| function submitResume() { | ||||
|   form | ||||
|     .transform((data) => ({ | ||||
|       user: data.user?.id, | ||||
|       name: data.name, | ||||
|       education: data.educations.map(education => ({ | ||||
|         ...education, | ||||
|         current: !!education.current, | ||||
|         endDate: education.current ? null: education.endDate, | ||||
|       })), | ||||
|       languages: data.languages.map(language => ({ | ||||
|         name: language.name, | ||||
|         level: language.level.level, | ||||
|       })), | ||||
|       technologies: data.technologies.map(technology => ({ | ||||
|         name: technology.name, | ||||
|         level: technology.level.level, | ||||
|       })), | ||||
|       projects: data.projects.map(project => ({ | ||||
|         ...project, | ||||
|         current: !!project.current, | ||||
|         endDate: project.current ? null : project.endDate, | ||||
|       })), | ||||
|     })) | ||||
|     .put(`/resumes/${props.resume.id}`) | ||||
| } | ||||
| </script> | ||||
							
								
								
									
										199
									
								
								resources/js/Pages/Resumes/Index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										199
									
								
								resources/js/Pages/Resumes/Index.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,199 @@ | ||||
| <template> | ||||
|   <InertiaHead title="CV" /> | ||||
|   <div class="bg-white shadow-md"> | ||||
|     <div class="flex justify-between items-center p-4 sm:px-6"> | ||||
|       <h2 class="text-lg font-medium leading-6 text-gray-900"> | ||||
|         Lista CV | ||||
|       </h2> | ||||
|       <div> | ||||
|         <InertiaLink | ||||
|           href="resumes/create" | ||||
|           class="inline-flex items-center py-3 px-4 text-sm font-medium leading-4 text-white bg-blumilk-600 hover:bg-blumilk-700 rounded-md border border-transparent focus:outline-none focus:ring-2 focus:ring-blumilk-500 focus:ring-offset-2 shadow-sm" | ||||
|         > | ||||
|           Dodaj CV | ||||
|         </InertiaLink> | ||||
|       </div> | ||||
|     </div> | ||||
|     <div class="border-t border-gray-200"> | ||||
|       <div class="overflow-auto xl:overflow-visible"> | ||||
|         <table class="min-w-full divide-y divide-gray-200"> | ||||
|           <thead class="bg-gray-50"> | ||||
|             <tr> | ||||
|               <th | ||||
|                 scope="col" | ||||
|                 class="py-3 px-4 text-xs font-semibold tracking-wider text-left text-gray-500 uppercase whitespace-nowrap" | ||||
|               > | ||||
|                 Użytkownik | ||||
|               </th> | ||||
|               <th | ||||
|                 scope="col" | ||||
|                 class="py-3 px-4 text-xs font-semibold tracking-wider text-left text-gray-500 uppercase whitespace-nowrap" | ||||
|               > | ||||
|                 Data utworzenia | ||||
|               </th> | ||||
|               <th | ||||
|                 scope="col" | ||||
|                 class="py-3 px-4 text-xs font-semibold tracking-wider text-left text-gray-500 uppercase whitespace-nowrap" | ||||
|               > | ||||
|                 Data aktualizacji | ||||
|               </th> | ||||
|               <th | ||||
|                 scope="col" | ||||
|                 class="py-3 px-4 text-xs font-semibold tracking-wider text-left text-gray-500 uppercase whitespace-nowrap" | ||||
|               > | ||||
|                 Szkoły | ||||
|               </th> | ||||
|               <th | ||||
|                 scope="col" | ||||
|                 class="py-3 px-4 text-xs font-semibold tracking-wider text-left text-gray-500 uppercase whitespace-nowrap" | ||||
|               > | ||||
|                 Języki | ||||
|               </th> | ||||
|               <th | ||||
|                 scope="col" | ||||
|                 class="py-3 px-4 text-xs font-semibold tracking-wider text-left text-gray-500 uppercase whitespace-nowrap" | ||||
|               > | ||||
|                 Technologie | ||||
|               </th> | ||||
|               <th | ||||
|                 scope="col" | ||||
|                 class="py-3 px-4 text-xs font-semibold tracking-wider text-left text-gray-500 uppercase whitespace-nowrap" | ||||
|               > | ||||
|                 Projekty | ||||
|               </th> | ||||
|               <th | ||||
|                 scope="col" | ||||
|                 class="py-3 px-4 text-xs font-semibold tracking-wider text-left text-gray-500 uppercase whitespace-nowrap" | ||||
|               /> | ||||
|             </tr> | ||||
|           </thead> | ||||
|           <tbody class="bg-white divide-y divide-gray-100"> | ||||
|             <tr | ||||
|               v-for="resume in resumes.data" | ||||
|               :key="resume.id" | ||||
|               class="hover:bg-blumilk-25" | ||||
|             > | ||||
|               <td class="p-4 text-sm text-gray-500 whitespace-nowrap"> | ||||
|                 <div | ||||
|                   v-if="resume.user" | ||||
|                   class="flex" | ||||
|                 > | ||||
|                   <span class="inline-flex justify-center items-center w-10 h-10 rounded-full"> | ||||
|                     <img | ||||
|                       class="w-10 h-10 rounded-full" | ||||
|                       :src="resume.user.avatar" | ||||
|                     > | ||||
|                   </span> | ||||
|                   <div class="ml-3"> | ||||
|                     <p class="text-sm font-medium text-gray-900 break-all"> | ||||
|                       {{ resume.user.name }} | ||||
|                     </p> | ||||
|                     <p class="text-sm text-gray-500 break-all"> | ||||
|                       {{ resume.user.email }} | ||||
|                     </p> | ||||
|                   </div> | ||||
|                 </div> | ||||
|                 <template v-else> | ||||
|                   <span class="text-sm font-medium text-gray-900 break-all">{{ resume.name }}</span> | ||||
|                 </template> | ||||
|               </td> | ||||
|               <td class="p-4 text-sm text-gray-500 whitespace-nowrap"> | ||||
|                 {{ resume.createdAt }} | ||||
|               </td> | ||||
|               <td class="p-4 text-sm text-gray-500 whitespace-nowrap"> | ||||
|                 {{ resume.updatedAt }} | ||||
|               </td> | ||||
|               <td class="p-4 text-sm text-gray-500 whitespace-nowrap"> | ||||
|                 {{ resume.educationCount }} | ||||
|               </td> | ||||
|               <td class="p-4 text-sm text-gray-500 whitespace-nowrap"> | ||||
|                 {{ resume.languageCount }} | ||||
|               </td> | ||||
|               <td class="p-4 text-sm text-gray-500 whitespace-nowrap"> | ||||
|                 {{ resume.technologyCount }} | ||||
|               </td> | ||||
|               <td class="p-4 text-sm text-gray-500 whitespace-nowrap"> | ||||
|                 {{ resume.projectCount }} | ||||
|               </td> | ||||
|               <td class="p-4 text-sm text-right text-gray-500 whitespace-nowrap"> | ||||
|                 <Menu | ||||
|                   as="div" | ||||
|                   class="inline-block relative text-left" | ||||
|                 > | ||||
|                   <MenuButton class="flex items-center text-gray-400 hover:text-gray-600 rounded-full focus:outline-none focus:ring-2 focus:ring-blumilk-500 focus:ring-offset-2 focus:ring-offset-gray-100"> | ||||
|                     <DotsVerticalIcon class="w-5 h-5" /> | ||||
|                   </MenuButton> | ||||
|  | ||||
|                   <transition | ||||
|                     enter-active-class="transition ease-out duration-100" | ||||
|                     enter-from-class="transform opacity-0 scale-95" | ||||
|                     enter-to-class="transform opacity-100 scale-100" | ||||
|                     leave-active-class="transition ease-in duration-75" | ||||
|                     leave-from-class="transform opacity-100 scale-100" | ||||
|                     leave-to-class="transform opacity-0 scale-95" | ||||
|                   > | ||||
|                     <MenuItems class="absolute right-0 z-10 mt-2 w-56 bg-white rounded-md focus:outline-none ring-1 ring-black ring-opacity-5 shadow-lg origin-top-right"> | ||||
|                       <div class="py-1"> | ||||
|                         <MenuItem v-slot="{ active }"> | ||||
|                           <InertiaLink | ||||
|                             :href="`/resumes/${resume.id}/edit`" | ||||
|                             :class="[active ? 'bg-gray-100 text-gray-900' : 'text-gray-700', 'font-medium block px-4 py-2 flex text-sm']" | ||||
|                           > | ||||
|                             <PencilIcon class="mr-2 w-5 h-5 text-blue-500" /> Edytuj | ||||
|                           </InertiaLink> | ||||
|                         </MenuItem> | ||||
|                         <MenuItem v-slot="{ active }"> | ||||
|                           <a | ||||
|                             :href="`/resumes/${resume.id}`" | ||||
|                             :class="[active ? 'bg-gray-100 text-gray-900' : 'text-gray-700', 'block w-full text-left font-medium px-4 py-2 flex text-sm']" | ||||
|                           > | ||||
|                             <DownloadIcon class="mr-2 w-5 h-5 text-blumilk-500" /> Pobierz | ||||
|                           </a> | ||||
|                         </MenuItem> | ||||
|                         <MenuItem | ||||
|                           v-slot="{ active }" | ||||
|                           class="flex" | ||||
|                         > | ||||
|                           <InertiaLink | ||||
|                             as="button" | ||||
|                             method="delete" | ||||
|                             :preserve-scroll="true" | ||||
|                             :href="`/resumes/${resume.id}`" | ||||
|                             :class="[active ? 'bg-gray-100 text-gray-900' : 'text-gray-700', 'block w-full text-left font-medium px-4 py-2 text-sm']" | ||||
|                           > | ||||
|                             <TrashIcon class="mr-2 w-5 h-5 text-red-500" /> Usuń | ||||
|                           </InertiaLink> | ||||
|                         </MenuItem> | ||||
|                       </div> | ||||
|                     </MenuItems> | ||||
|                   </transition> | ||||
|                 </Menu> | ||||
|               </td> | ||||
|             </tr> | ||||
|             <tr v-if="!resumes.data.length"> | ||||
|               <td | ||||
|                 colspan="100%" | ||||
|                 class="py-4 text-xl leading-5 text-center text-gray-700" | ||||
|               > | ||||
|                 Brak danych | ||||
|               </td> | ||||
|             </tr> | ||||
|           </tbody> | ||||
|         </table> | ||||
|       </div> | ||||
|       <Pagination :pagination="resumes.meta" /> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup> | ||||
| import { DotsVerticalIcon } from '@heroicons/vue/outline' | ||||
| import { DownloadIcon, PencilIcon, TrashIcon } from '@heroicons/vue/solid' | ||||
| import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue' | ||||
| import Pagination from '@/Shared/Pagination' | ||||
|  | ||||
| defineProps({ | ||||
|   resumes: Object, | ||||
|   can: Object, | ||||
| }) | ||||
| </script> | ||||
							
								
								
									
										222
									
								
								resources/js/Pages/Technologies.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										222
									
								
								resources/js/Pages/Technologies.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,222 @@ | ||||
| <template> | ||||
|   <InertiaHead title="Klucze" /> | ||||
|   <div class="bg-white shadow-md"> | ||||
|     <div class="flex justify-between items-center p-4 sm:px-6"> | ||||
|       <div> | ||||
|         <h2 class="text-lg font-medium leading-6 text-gray-900"> | ||||
|           Technologie | ||||
|         </h2> | ||||
|       </div> | ||||
|       <div> | ||||
|         <button | ||||
|           type="button" | ||||
|           class="inline-flex items-center py-3 px-4 text-sm font-medium leading-4 text-white bg-blumilk-600 hover:bg-blumilk-700 rounded-md border border-transparent focus:outline-none focus:ring-2 focus:ring-blumilk-500 focus:ring-offset-2 shadow-sm" | ||||
|           @click="creating = true" | ||||
|         > | ||||
|           Dodaj technologię | ||||
|         </button> | ||||
|       </div> | ||||
|     </div> | ||||
|     <div class="border-t border-gray-200"> | ||||
|       <div class="overflow-auto xl:overflow-visible"> | ||||
|         <table class="min-w-full divide-y divide-gray-200"> | ||||
|           <thead class="bg-gray-50"> | ||||
|             <tr> | ||||
|               <th | ||||
|                 scope="col" | ||||
|                 class="py-3 px-4 text-xs font-semibold tracking-wider text-left text-gray-500 uppercase whitespace-nowrap" | ||||
|               > | ||||
|                 Technologia | ||||
|               </th> | ||||
|               <th | ||||
|                 scope="col" | ||||
|                 class="py-3 px-4 text-xs font-semibold tracking-wider text-left text-gray-500 uppercase whitespace-nowrap" | ||||
|               /> | ||||
|             </tr> | ||||
|           </thead> | ||||
|           <tbody class="bg-white divide-y divide-gray-100"> | ||||
|             <tr | ||||
|               v-for="technology in technologies.data" | ||||
|               :key="technology.id" | ||||
|               class="hover:bg-blumilk-25" | ||||
|             > | ||||
|               <td class="px-4 py-2 text-sm text-gray-500 whitespace-nowrap"> | ||||
|                 {{ technology.name }} | ||||
|               </td> | ||||
|               <td class="px-4 py-2 text-sm text-right text-gray-500 whitespace-nowrap"> | ||||
|                 <Menu | ||||
|                   as="div" | ||||
|                   class="inline-block relative text-left" | ||||
|                 > | ||||
|                   <MenuButton | ||||
|                     class="flex items-center text-gray-400 hover:text-gray-600 rounded-full focus:outline-none focus:ring-2 focus:ring-blumilk-500 focus:ring-offset-2 focus:ring-offset-gray-100" | ||||
|                   > | ||||
|                     <DotsVerticalIcon class="w-5 h-5" /> | ||||
|                   </MenuButton> | ||||
|  | ||||
|                   <transition | ||||
|                     enter-active-class="transition ease-out duration-100" | ||||
|                     enter-from-class="transform opacity-0 scale-95" | ||||
|                     enter-to-class="transform opacity-100 scale-100" | ||||
|                     leave-active-class="transition ease-in duration-75" | ||||
|                     leave-from-class="transform opacity-100 scale-100" | ||||
|                     leave-to-class="transform opacity-0 scale-95" | ||||
|                   > | ||||
|                     <MenuItems | ||||
|                       class="absolute right-0 z-10 mt-2 w-56 bg-white rounded-md focus:outline-none ring-1 ring-black ring-opacity-5 shadow-lg origin-top-right" | ||||
|                     > | ||||
|                       <div class="py-1"> | ||||
|                         <MenuItem | ||||
|                           v-slot="{ active }" | ||||
|                           class="flex" | ||||
|                         > | ||||
|                           <InertiaLink | ||||
|                             as="button" | ||||
|                             method="delete" | ||||
|                             preserve-scroll | ||||
|                             :href="`/technologies/${technology.id}`" | ||||
|                             :class="[active ? 'bg-gray-100 text-gray-900' : 'text-gray-700', 'block w-full text-left font-medium px-4 py-2 text-sm']" | ||||
|                           > | ||||
|                             <TrashIcon class="mr-2 w-5 h-5 text-red-500" /> | ||||
|                             Usuń | ||||
|                           </InertiaLink> | ||||
|                         </MenuItem> | ||||
|                       </div> | ||||
|                     </MenuItems> | ||||
|                   </transition> | ||||
|                 </Menu> | ||||
|               </td> | ||||
|             </tr> | ||||
|             <tr v-if="!technologies.data.length"> | ||||
|               <td | ||||
|                 colspan="100%" | ||||
|                 class="py-4 text-xl leading-5 text-center text-gray-700" | ||||
|               > | ||||
|                 Brak danych | ||||
|               </td> | ||||
|             </tr> | ||||
|           </tbody> | ||||
|         </table> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
|   <TransitionRoot | ||||
|     as="template" | ||||
|     :show="creating" | ||||
|   > | ||||
|     <Dialog | ||||
|       is="div" | ||||
|       class="overflow-y-auto fixed inset-0 z-10" | ||||
|       @close="creating = false" | ||||
|     > | ||||
|       <div class="flex justify-center items-end px-4 pt-4 pb-20 min-h-screen text-center sm:block sm:p-0"> | ||||
|         <TransitionChild | ||||
|           as="template" | ||||
|           enter="ease-out duration-300" | ||||
|           enter-from="opacity-0" | ||||
|           enter-to="opacity-100" | ||||
|           leave="ease-in duration-200" | ||||
|           leave-from="opacity-100" | ||||
|           leave-to="opacity-0" | ||||
|         > | ||||
|           <DialogOverlay class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" /> | ||||
|         </TransitionChild> | ||||
|  | ||||
|         <span class="hidden sm:inline-block sm:h-screen sm:align-middle">​</span> | ||||
|         <TransitionChild | ||||
|           as="template" | ||||
|           enter="ease-out duration-300" | ||||
|           enter-from="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" | ||||
|           enter-to="opacity-100 translate-y-0 sm:scale-100" | ||||
|           leave="ease-in duration-200" | ||||
|           leave-from="opacity-100 translate-y-0 sm:scale-100" | ||||
|           leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" | ||||
|         > | ||||
|           <form | ||||
|             class="inline-block relative px-4 pt-5 pb-4 text-left align-bottom bg-white rounded-lg shadow-xl transition-all transform sm:p-6 sm:my-8 sm:w-full sm:max-w-sm sm:align-middle" | ||||
|             @submit.prevent="submitCreateTechnology" | ||||
|           > | ||||
|             <div> | ||||
|               <div> | ||||
|                 <DialogTitle | ||||
|                   as="h3" | ||||
|                   class="text-lg font-medium leading-6 text-center text-gray-900 font-sembiold" | ||||
|                 > | ||||
|                   Dodaj technologię | ||||
|                 </DialogTitle> | ||||
|                 <div class="mt-5"> | ||||
|                   <label | ||||
|                     for="name" | ||||
|                     class="block text-sm font-medium text-gray-700 sm:mt-px" | ||||
|                   > | ||||
|                     Nazwa | ||||
|                   </label> | ||||
|                   <div class="mt-2"> | ||||
|                     <input | ||||
|                       id="name" | ||||
|                       v-model="form.name" | ||||
|                       type="text" | ||||
|                       class="block w-full max-w-lg rounded-md shadow-sm sm:text-sm" | ||||
|                       :class="{ 'border-red-300 text-red-900 focus:outline-none focus:ring-red-500 focus:border-red-500': form.errors.name, 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors.name }" | ||||
|                     > | ||||
|                     <p | ||||
|                       v-if="form.errors.name" | ||||
|                       class="mt-2 text-sm text-red-600" | ||||
|                     > | ||||
|                       {{ form.errors.name }} | ||||
|                     </p> | ||||
|                   </div> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|             <div class="mt-5 sm:mt-6"> | ||||
|               <div class="flex justify-end space-x-3"> | ||||
|                 <button | ||||
|                   type="button" | ||||
|                   class="py-2 px-4 text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blumilk-500 focus:ring-offset-2 shadow-sm" | ||||
|                   @click="creating = false" | ||||
|                 > | ||||
|                   Anuluj | ||||
|                 </button> | ||||
|                 <button | ||||
|                   type="submit" | ||||
|                   :disabled="form.processing" | ||||
|                   class="inline-flex justify-center py-2 px-4 text-base font-medium text-white bg-blumilk-600 hover:bg-blumilk-700 rounded-md border border-transparent focus:outline-none focus:ring-2 focus:ring-blumilk-500 focus:ring-offset-2 shadow-sm sm:text-sm" | ||||
|                 > | ||||
|                   Dodaj | ||||
|                 </button> | ||||
|               </div> | ||||
|             </div> | ||||
|           </form> | ||||
|         </TransitionChild> | ||||
|       </div> | ||||
|     </Dialog> | ||||
|   </TransitionRoot> | ||||
| </template> | ||||
|  | ||||
| <script setup> | ||||
| import { DotsVerticalIcon, TrashIcon } from '@heroicons/vue/solid' | ||||
| import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue' | ||||
| import { ref } from 'vue' | ||||
| import { Dialog, DialogOverlay, DialogTitle, TransitionChild, TransitionRoot } from '@headlessui/vue' | ||||
| import { useForm } from '@inertiajs/inertia-vue3' | ||||
|  | ||||
| defineProps({ | ||||
|   technologies: Object, | ||||
| }) | ||||
|  | ||||
| const creating = ref(false) | ||||
|  | ||||
| const form = useForm({ | ||||
|   name: null, | ||||
| }) | ||||
|  | ||||
| function submitCreateTechnology() { | ||||
|   form.post('technologies', { | ||||
|     preserveState: (page) => Object.keys(page.props.errors).length, | ||||
|     preserveScroll: true, | ||||
|     onSuccess: () => form.reset(), | ||||
|   }) | ||||
| } | ||||
|  | ||||
| </script> | ||||
							
								
								
									
										68
									
								
								resources/js/Shared/Forms/Combobox.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								resources/js/Shared/Forms/Combobox.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,68 @@ | ||||
| <template> | ||||
|   <Combobox | ||||
|     as="div" | ||||
|     nullable | ||||
|   > | ||||
|     <div class="relative"> | ||||
|       <ComboboxInput | ||||
|         :id="id" | ||||
|         class="w-full h-12 rounded-md border border-gray-300 bg-white py-2 pl-3 pr-10 shadow-sm focus:border-blumilk-500 focus:outline-none focus:ring-1 focus:ring-blumilk-500 sm:text-sm" | ||||
|         @change="query = $event.target.value" | ||||
|       /> | ||||
|       <ComboboxButton class="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none"> | ||||
|         <SelectorIcon class="h-5 w-5 text-gray-400" /> | ||||
|       </ComboboxButton> | ||||
|  | ||||
|       <ComboboxOptions | ||||
|         v-if="filteredItems.length" | ||||
|         class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm" | ||||
|       > | ||||
|         <ComboboxOption | ||||
|           v-for="item in filteredItems" | ||||
|           :key="item.id" | ||||
|           v-slot="{ active, selected }" | ||||
|           :value="item" | ||||
|           as="template" | ||||
|         > | ||||
|           <li :class="['relative cursor-default select-none py-2 pl-3 pr-9', active ? 'bg-blumilk-600 text-white' : 'text-gray-900']"> | ||||
|             <span :class="['block truncate', selected && 'font-semibold']"> | ||||
|               {{ item }} | ||||
|             </span> | ||||
|  | ||||
|             <span | ||||
|               v-if="selected" | ||||
|               :class="['absolute inset-y-0 right-0 flex items-center pr-4', active ? 'text-white' : 'text-blumilk-600']" | ||||
|             > | ||||
|               <CheckIcon class="h-5 w-5" /> | ||||
|             </span> | ||||
|           </li> | ||||
|         </ComboboxOption> | ||||
|       </ComboboxOptions> | ||||
|     </div> | ||||
|   </Combobox> | ||||
| </template> | ||||
|  | ||||
| <script setup> | ||||
| import { computed, ref } from 'vue' | ||||
| import { CheckIcon, SelectorIcon } from '@heroicons/vue/solid' | ||||
| import { | ||||
|   Combobox, | ||||
|   ComboboxButton, | ||||
|   ComboboxInput, | ||||
|   ComboboxOption, | ||||
|   ComboboxOptions, | ||||
| } from '@headlessui/vue' | ||||
|  | ||||
| const props = defineProps({ | ||||
|   items: Array, | ||||
|   id: String, | ||||
| }) | ||||
|  | ||||
| const query = ref('') | ||||
|  | ||||
| const filteredItems = computed(() => | ||||
|   query.value === '' | ||||
|     ? props.items | ||||
|     : props.items.filter((item) => item.toLowerCase().includes(query.value.toLowerCase())), | ||||
| ) | ||||
| </script> | ||||
							
								
								
									
										113
									
								
								resources/js/Shared/Forms/DynamicSection.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								resources/js/Shared/Forms/DynamicSection.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,113 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <h3 class="text-lg font-medium leading-6 text-gray-900"> | ||||
|       {{ header }} | ||||
|     </h3> | ||||
|     <Draggable | ||||
|       v-model="items" | ||||
|       class="pt-4 space-y-4" | ||||
|       tag="transition-group" | ||||
|       ghost-class="opacity-50" | ||||
|       handle=".handle" | ||||
|       :animation="200" | ||||
|       :component-data="{tag: 'div', type: 'transition-group'}" | ||||
|       :item-key="((item) => items.indexOf(item))" | ||||
|     > | ||||
|       <template #item="{ element, index }"> | ||||
|         <div class="group flex items-start space-x-3"> | ||||
|           <button | ||||
|             class="py-4 text-red-500 hover:text-gray-600 opacity-100 group-hover:opacity-100 transition-opacity hover:scale-110 lg:opacity-0 handle" | ||||
|             type="button" | ||||
|           > | ||||
|             <ViewGridIcon class="w-5 h-5 text-gray-500" /> | ||||
|           </button> | ||||
|           <Disclosure | ||||
|             v-slot="{ open }" | ||||
|             as="div" | ||||
|             class="flex-1 border border-gray-200" | ||||
|           > | ||||
|             <div class="flex"> | ||||
|               <DisclosureButton class="transition transition-colors rounded-md group w-full max-w-full overflow-hidden flex items-center justify-between p-4 font-semibold text-gray-500 hover:text-blumilk-500 transition transition-colors rounded-md focus:outline-none"> | ||||
|                 <div class="break-all line-clamp-1 text-md"> | ||||
|                   <slot | ||||
|                     name="itemHeader" | ||||
|                     :element="element" | ||||
|                     :index="index" | ||||
|                   /> | ||||
|                 </div> | ||||
|                 <div class="ml-2"> | ||||
|                   <svg | ||||
|                     :class="[open ? '-rotate-90' : 'rotate-90', 'h-6 w-6 transform transition-transform ease-in-out duration-150']" | ||||
|                     viewBox="0 0 20 20" | ||||
|                   > | ||||
|                     <path | ||||
|                       d="M6 6L14 10L6 14V6Z" | ||||
|                       fill="currentColor" | ||||
|                     /> | ||||
|                   </svg> | ||||
|                 </div> | ||||
|               </DisclosureButton> | ||||
|             </div> | ||||
|             <DisclosurePanel | ||||
|               as="div" | ||||
|               class="py-2 px-4 border-t border-gray-200" | ||||
|             > | ||||
|               <slot | ||||
|                 name="form" | ||||
|                 :element="element" | ||||
|                 :index="index" | ||||
|               /> | ||||
|             </DisclosurePanel> | ||||
|           </Disclosure> | ||||
|           <button | ||||
|             class="py-4 text-red-500 hover:text-red-600 opacity-100 group-hover:opacity-100 transition-opacity hover:scale-110 lg:opacity-0" | ||||
|             type="button" | ||||
|             @click="removeItem(index)" | ||||
|           > | ||||
|             <TrashIcon class="w-5 h-5 text-red-500" /> | ||||
|           </button> | ||||
|         </div> | ||||
|       </template> | ||||
|     </Draggable> | ||||
|     <div class="px-8"> | ||||
|       <button | ||||
|         type="button" | ||||
|         class="p-4 mx-auto mt-4 w-full font-semibold text-center text-blumilk-600 hover:bg-blumilk-25 focus:outline-none transition-colors" | ||||
|         @click="addItem()" | ||||
|       > | ||||
|         {{ addLabel }} | ||||
|       </button> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup> | ||||
| import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue' | ||||
| import { TrashIcon, ViewGridIcon } from '@heroicons/vue/outline' | ||||
| import Draggable from 'vuedraggable' | ||||
| import { computed } from 'vue' | ||||
|  | ||||
| const props = defineProps({ | ||||
|   header: String, | ||||
|   addLabel: String, | ||||
|   modelValue: Object, | ||||
|   itemHeader: [Function, String], | ||||
| }) | ||||
|  | ||||
| const emit = defineEmits(['update:modelValue', 'addItem', 'removeItem']) | ||||
|  | ||||
| const items = computed({ | ||||
|   get: () => props.modelValue, | ||||
|   set: (value) => { | ||||
|     emit('update:modelValue', value) | ||||
|   }, | ||||
| }) | ||||
|  | ||||
| function addItem() { | ||||
|   emit('addItem') | ||||
| } | ||||
|  | ||||
| function removeItem(index) { | ||||
|   emit('removeItem', index) | ||||
| } | ||||
| </script> | ||||
							
								
								
									
										46
									
								
								resources/js/Shared/Forms/LevelPicker.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								resources/js/Shared/Forms/LevelPicker.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| <template> | ||||
|   <RadioGroup v-model="selectedValue"> | ||||
|     <div | ||||
|       :class="`relative overflow-hidden flex h-12 rounded-l-md rounded-r-md space-x-px ${selectedValue.activeColor} transition-colors duration-200 easy-in-out`" | ||||
|     > | ||||
|       <RadioGroupOption | ||||
|         v-for="(level, index) in levels" | ||||
|         :key="index" | ||||
|         as="template" | ||||
|         :value="level" | ||||
|       > | ||||
|         <div | ||||
|           :class="`${selectedValue.backgroundColor} hover:opacity-80 cursor-pointer transition-colors duration-200 easy-in-out focus:outline-none flex-1`" | ||||
|         /> | ||||
|       </RadioGroupOption> | ||||
|       <div | ||||
|         :class="`absolute transform transition-transform  duration-200 easy-in-out`" | ||||
|         :style="`width: ${100/levels.length}%; transform: translateX(calc(${100 * currentIndex}% - 1px))`" | ||||
|       > | ||||
|         <div :class="`h-12 ${selectedValue.activeColor} transition-colors duration-300 easy-in-out`" /> | ||||
|       </div> | ||||
|     </div> | ||||
|   </RadioGroup> | ||||
| </template> | ||||
|  | ||||
| <script setup> | ||||
| import { RadioGroup, RadioGroupOption } from '@headlessui/vue' | ||||
| import { computed } from 'vue' | ||||
|  | ||||
| const emit = defineEmits(['update:modelValue']) | ||||
|  | ||||
| const props = defineProps({ | ||||
|   levels: Array, | ||||
|   modelValue: Object, | ||||
| }) | ||||
|  | ||||
| const selectedValue = computed({ | ||||
|   get: () => props.modelValue, | ||||
|   set: (value) => { | ||||
|     emit('update:modelValue', value) | ||||
|   }, | ||||
| }) | ||||
|  | ||||
| const currentIndex = computed(() => props.levels.findIndex((level) => level.level === selectedValue.value.level)) | ||||
|  | ||||
| </script> | ||||
							
								
								
									
										18
									
								
								resources/js/Shared/Forms/MonthPicker.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								resources/js/Shared/Forms/MonthPicker.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| <template> | ||||
|   <FlatPickr :config="config" /> | ||||
| </template> | ||||
|  | ||||
| <script setup> | ||||
| import FlatPickr from 'vue-flatpickr-component' | ||||
| import monthSelectPlugin from 'flatpickr/dist/plugins/monthSelect' | ||||
|  | ||||
| const config = { | ||||
|   plugins: [ | ||||
|     new monthSelectPlugin({ | ||||
|       shorthand: true, | ||||
|       dateFormat: 'm/Y', | ||||
|     }), | ||||
|   ], | ||||
| } | ||||
| </script> | ||||
|  | ||||
							
								
								
									
										108
									
								
								resources/js/Shared/Forms/MultipleCombobox.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								resources/js/Shared/Forms/MultipleCombobox.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,108 @@ | ||||
| <template> | ||||
|   <Combobox | ||||
|     v-model="selectedItems" | ||||
|     as="div" | ||||
|     nullable | ||||
|     multiple | ||||
|   > | ||||
|     <div class="flex flex-wrap gap-3"> | ||||
|       <span | ||||
|         v-for="(item, index) in selectedItems" | ||||
|         :key="index" | ||||
|         class="inline-flex items-center py-1.5 pl-3 pr-1.5 rounded-lg text-sm font-medium bg-blumilk-500 text-white" | ||||
|       > | ||||
|         {{ item }} | ||||
|         <button | ||||
|           type="button" | ||||
|           class="flex-shrink-0 ml-0.5 h-5 w-5 rounded-full inline-flex items-center justify-center text-white hover:bg-blumilk-600 focus:outline-none" | ||||
|           @click="selectedItems.splice(index, 1)" | ||||
|         > | ||||
|           <svg | ||||
|             class="h-2 w-2" | ||||
|             stroke="currentColor" | ||||
|             fill="none" | ||||
|             viewBox="0 0 8 8" | ||||
|           > | ||||
|             <path | ||||
|               stroke-linecap="round" | ||||
|               stroke-width="1.5" | ||||
|               d="M1 1l6 6m0-6L1 7" | ||||
|             /> | ||||
|           </svg> | ||||
|         </button> | ||||
|       </span> | ||||
|     </div> | ||||
|     <div class="relative mt-2"> | ||||
|       <ComboboxInput | ||||
|         :id="id" | ||||
|         class="w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-10 shadow-sm focus:border-blumilk-500 focus:outline-none focus:ring-1 focus:ring-blumilk-500 sm:text-sm" | ||||
|         @change="query = $event.target.value" | ||||
|       /> | ||||
|       <ComboboxButton class="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none"> | ||||
|         <SelectorIcon class="h-5 w-5 text-gray-400" /> | ||||
|       </ComboboxButton> | ||||
|  | ||||
|       <ComboboxOptions | ||||
|         v-if="filteredItems.length" | ||||
|         class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm" | ||||
|       > | ||||
|         <ComboboxOption | ||||
|           v-for="item in filteredItems" | ||||
|           :key="item.id" | ||||
|           v-slot="{ active, selected }" | ||||
|           :value="item" | ||||
|           as="template" | ||||
|         > | ||||
|           <li :class="['relative cursor-default select-none py-2 pl-3 pr-9', active ? 'bg-blumilk-600 text-white' : 'text-gray-900']"> | ||||
|             <span :class="['block truncate', selected && 'font-semibold']"> | ||||
|               {{ item }} | ||||
|             </span> | ||||
|  | ||||
|             <span | ||||
|               v-if="selected" | ||||
|               :class="['absolute inset-y-0 right-0 flex items-center pr-4', active ? 'text-white' : 'text-blumilk-600']" | ||||
|             > | ||||
|               <CheckIcon class="h-5 w-5" /> | ||||
|             </span> | ||||
|           </li> | ||||
|         </ComboboxOption> | ||||
|       </ComboboxOptions> | ||||
|     </div> | ||||
|   </Combobox> | ||||
| </template> | ||||
|  | ||||
| <script setup> | ||||
| import { computed, ref } from 'vue' | ||||
| import { CheckIcon, SelectorIcon } from '@heroicons/vue/solid' | ||||
| import { | ||||
|   Combobox, | ||||
|   ComboboxButton, | ||||
|   ComboboxInput, | ||||
|   ComboboxOption, | ||||
|   ComboboxOptions, | ||||
| } from '@headlessui/vue' | ||||
|  | ||||
| const props = defineProps({ | ||||
|   items: Array, | ||||
|   modelValue: Array, | ||||
|   id: String, | ||||
| }) | ||||
|  | ||||
| const emit = defineEmits(['update:modelValue']) | ||||
|  | ||||
| const query = ref('') | ||||
|  | ||||
| const selectedItems = computed({ | ||||
|   get: () => props.modelValue, | ||||
|   set: (value) => { | ||||
|     query.value = '' | ||||
|     emit('update:modelValue', value) | ||||
|   }, | ||||
| }) | ||||
|  | ||||
| const filteredItems = computed(() => | ||||
|   query.value === '' | ||||
|     ? props.items | ||||
|     : props.items.filter((item) => item.toLowerCase().includes(query.value.toLowerCase())), | ||||
| ) | ||||
| </script> | ||||
| @@ -59,7 +59,7 @@ | ||||
|               > | ||||
|             </InertiaLink> | ||||
|           </div> | ||||
|           <nav class="overflow-y-auto shrink-0 mt-5 h-full divide-y divide-blumilk-800"> | ||||
|           <nav class="overflow-y-auto shrink-0 mt-5 h-full space-y-5"> | ||||
|             <div class="px-2 space-y-1"> | ||||
|               <InertiaLink | ||||
|                 href="/" | ||||
| @@ -70,28 +70,53 @@ | ||||
|                 Strona główna | ||||
|               </InertiaLink> | ||||
|             </div> | ||||
|             <div class="pt-3 mt-3"> | ||||
|               <div class="py-1 px-2 space-y-1"> | ||||
|                 <InertiaLink | ||||
|                   v-for="item in navigation" | ||||
|                   :key="item.name" | ||||
|                   :href="item.href" | ||||
|                   :class="[$page.component.startsWith(item.section) ? 'bg-blumilk-800 text-white' : 'text-blumilk-100 hover:text-white hover:bg-blumilk-600', 'group flex items-center px-2 py-2 text-base font-medium rounded-md']" | ||||
|                   @click="sidebarOpen = false;" | ||||
|             <div | ||||
|               v-if="vacationNavigation.length" | ||||
|               class="py-1 px-2 space-y-1" | ||||
|             > | ||||
|               <InertiaLink | ||||
|                 v-for="item in vacationNavigation" | ||||
|                 :key="item.name" | ||||
|                 :href="item.href" | ||||
|                 :class="[$page.component.startsWith(item.section) ? 'bg-blumilk-800 text-white' : 'text-blumilk-100 hover:text-white hover:bg-blumilk-600', 'group flex items-center px-2 py-2 text-base font-medium rounded-md']" | ||||
|                 @click="sidebarOpen = false;" | ||||
|               > | ||||
|                 <component | ||||
|                   :is="item.icon" | ||||
|                   class="shrink-0 mr-4 w-6 h-6 text-blumilk-200" | ||||
|                 /> | ||||
|                 {{ item.name }} | ||||
|                 <span | ||||
|                   v-if="item.badge" | ||||
|                   class="py-0.5 px-2.5 ml-3 text-xs font-semibold text-gray-600 bg-gray-100 rounded-full 2xl:inline-block" | ||||
|                 > | ||||
|                   <component | ||||
|                     :is="item.icon" | ||||
|                     class="shrink-0 mr-4 w-6 h-6 text-blumilk-200" | ||||
|                   /> | ||||
|                   {{ item.name }} | ||||
|                   <span | ||||
|                     v-if="item.badge" | ||||
|                     class="py-0.5 px-2.5 ml-3 text-xs font-semibold text-gray-600 bg-gray-100 rounded-full 2xl:inline-block" | ||||
|                   > | ||||
|                     {{ item.badge }} | ||||
|                   </span> | ||||
|                 </InertiaLink> | ||||
|               </div> | ||||
|                   {{ item.badge }} | ||||
|                 </span> | ||||
|               </InertiaLink> | ||||
|             </div> | ||||
|             <div | ||||
|               v-if="miscNavigaction.length" | ||||
|               class="py-1 px-2 space-y-1" | ||||
|             > | ||||
|               <InertiaLink | ||||
|                 v-for="item in miscNavigaction" | ||||
|                 :key="item.name" | ||||
|                 :href="item.href" | ||||
|                 :class="[$page.component.startsWith(item.section) ? 'bg-blumilk-800 text-white' : 'text-blumilk-100 hover:text-white hover:bg-blumilk-600', 'group flex items-center px-2 py-2 text-base font-medium rounded-md']" | ||||
|                 @click="sidebarOpen = false;" | ||||
|               > | ||||
|                 <component | ||||
|                   :is="item.icon" | ||||
|                   class="shrink-0 mr-4 w-6 h-6 text-blumilk-200" | ||||
|                 /> | ||||
|                 {{ item.name }} | ||||
|                 <span | ||||
|                   v-if="item.badge" | ||||
|                   class="py-0.5 px-2.5 ml-3 text-xs font-semibold text-gray-600 bg-gray-100 rounded-full 2xl:inline-block" | ||||
|                 > | ||||
|                   {{ item.badge }} | ||||
|                 </span> | ||||
|               </InertiaLink> | ||||
|             </div> | ||||
|           </nav> | ||||
|         </div> | ||||
| @@ -110,7 +135,7 @@ | ||||
|           > | ||||
|         </InertiaLink> | ||||
|       </div> | ||||
|       <nav class="flex overflow-y-auto flex-col flex-1 px-2 mt-5 divide-y divide-blumilk-800"> | ||||
|       <nav class="flex overflow-y-auto flex-col flex-1 px-2 mt-5 space-y-4"> | ||||
|         <InertiaLink | ||||
|           href="/" | ||||
|           :class="[$page.component === 'Dashboard' ? 'bg-blumilk-800 text-white' : 'text-blumilk-100 hover:text-white hover:bg-blumilk-600', 'group flex items-center px-2 py-2 mt-1 text-sm leading-6 font-medium rounded-md']" | ||||
| @@ -118,9 +143,35 @@ | ||||
|           <HomeIcon class="shrink-0 mr-4 w-6 h-6 text-blumilk-200" /> | ||||
|           Strona główna | ||||
|         </InertiaLink> | ||||
|         <div class="pt-1 mt-1 space-y-1"> | ||||
|         <div | ||||
|           v-if="vacationNavigation.length" | ||||
|           class="pt-1 mt-1 space-y-1" | ||||
|         > | ||||
|           <InertiaLink | ||||
|             v-for="item in navigation" | ||||
|             v-for="item in vacationNavigation" | ||||
|             :key="item.name" | ||||
|             :href="item.href" | ||||
|             :class="[$page.component.startsWith(item.section) ? 'bg-blumilk-800 text-white' : 'text-blumilk-100 hover:text-white hover:bg-blumilk-600', 'group flex items-center px-2 py-2 text-sm leading-6 font-medium rounded-md']" | ||||
|           > | ||||
|             <component | ||||
|               :is="item.icon" | ||||
|               class="shrink-0 mr-4 w-6 h-6 text-blumilk-200" | ||||
|             /> | ||||
|             {{ item.name }} | ||||
|             <span | ||||
|               v-if="item.badge" | ||||
|               class="py-0.5 px-2.5 ml-3 text-xs font-semibold text-gray-600 bg-gray-100 rounded-full 2xl:inline-block" | ||||
|             > | ||||
|               {{ item.badge }} | ||||
|             </span> | ||||
|           </InertiaLink> | ||||
|         </div> | ||||
|         <div | ||||
|           v-if="miscNavigaction.length" | ||||
|           class="pt-1 mt-1 space-y-1" | ||||
|         > | ||||
|           <InertiaLink | ||||
|             v-for="item in miscNavigaction" | ||||
|             :key="item.name" | ||||
|             :href="item.href" | ||||
|             :class="[$page.component.startsWith(item.section) ? 'bg-blumilk-800 text-white' : 'text-blumilk-100 hover:text-white hover:bg-blumilk-600', 'group flex items-center px-2 py-2 text-sm leading-6 font-medium rounded-md']" | ||||
| @@ -295,6 +346,7 @@ import { | ||||
|   DocumentTextIcon, | ||||
|   AdjustmentsIcon, | ||||
|   KeyIcon, | ||||
|   TemplateIcon, BeakerIcon, | ||||
| } from '@heroicons/vue/outline' | ||||
| import { CheckIcon, ChevronDownIcon } from '@heroicons/vue/solid' | ||||
|  | ||||
| @@ -306,7 +358,7 @@ const props = defineProps({ | ||||
|  | ||||
| const sidebarOpen = ref(false) | ||||
|  | ||||
| const navigation = computed(() => | ||||
| const vacationNavigation = computed(() => | ||||
|   [ | ||||
|     { | ||||
|       name: 'Moje wnioski', | ||||
| @@ -358,20 +410,36 @@ const navigation = computed(() => | ||||
|       icon: ClipboardListIcon, | ||||
|       can: true, | ||||
|     }, | ||||
|     { | ||||
|       name: 'Użytkownicy', | ||||
|       href: '/users', | ||||
|       section: 'Users/', | ||||
|       icon: UserGroupIcon, | ||||
|       can: props.auth.can.manageUsers, | ||||
|     }, | ||||
|     { | ||||
|       name: 'Klucze', | ||||
|       href: '/keys', | ||||
|       section: 'Keys', | ||||
|       icon: KeyIcon, | ||||
|       can: true, | ||||
|     }, | ||||
|  | ||||
|   ].filter(item => item.can)) | ||||
|  | ||||
| const miscNavigaction = computed(() => [ | ||||
|   { | ||||
|     name: 'Użytkownicy', | ||||
|     href: '/users', | ||||
|     section: 'Users/', | ||||
|     icon: UserGroupIcon, | ||||
|     can: props.auth.can.manageUsers, | ||||
|   }, | ||||
|   { | ||||
|     name: 'Klucze', | ||||
|     href: '/keys', | ||||
|     section: 'Keys', | ||||
|     icon: KeyIcon, | ||||
|     can: true, | ||||
|   }, | ||||
|   { | ||||
|     name: 'Technologie', | ||||
|     href: '/technologies', | ||||
|     section: 'Technologies', | ||||
|     icon: BeakerIcon, | ||||
|     can: props.auth.can.manageResumes, | ||||
|   }, | ||||
|   { | ||||
|     name: 'CV', | ||||
|     href: '/resumes', | ||||
|     section: 'Resumes', | ||||
|     icon: TemplateIcon, | ||||
|     can: props.auth.can.manageResumes, | ||||
|   }, | ||||
| ].filter(item => item.can)) | ||||
| </script> | ||||
|   | ||||
| @@ -74,6 +74,11 @@ | ||||
|   "Key no :number has been given to :user.": "Klucz nr :number został przekazany użytkownikowi :user.", | ||||
|   ":sender gives key no :key to :recipient": ":sender przekazuje klucz nr :key :recipient", | ||||
|   ":recipient takes key no :key from :sender": ":recipient zabiera klucz nr :key :sender", | ||||
|   "Resume has been updated.": "CV zostało zaktualizowane.", | ||||
|   "Resume has been deleted.": "CV zostało usunięte.", | ||||
|   "Resume has been created.": "CV zostało utworzone.", | ||||
|   "Technology :name has been created.": "Technologia :name została utworzona.", | ||||
|   "Technology :name has been deleted.": "Technologia :name została usunięta.", | ||||
|   "The vacation request :title has been created successfully.": "Wniosek urlopowy :title został utworzony pomyślnie.", | ||||
|   ":x: I don't recognize the command. List of all commands:": ":x: Nie rozpoznaję polecenia. Lista wszystkich poleceń:", | ||||
|   "Summary for the day :day": "Podsumowanie dla dnia :day", | ||||
|   | ||||
							
								
								
									
										21
									
								
								resources/lang/pl/resume.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								resources/lang/pl/resume.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| return [ | ||||
|     "language_levels" => [ | ||||
|         1 => "A1", | ||||
|         2 => "A2", | ||||
|         3 => "B1", | ||||
|         4 => "B2", | ||||
|         5 => "C1", | ||||
|         6 => "C2", | ||||
|     ], | ||||
|     "technology_levels" => [ | ||||
|         1 => "Beginner", | ||||
|         2 => "Junior", | ||||
|         3 => "Regular", | ||||
|         4 => "Advanced", | ||||
|         5 => "Expert", | ||||
|     ], | ||||
| ]; | ||||
| @@ -114,6 +114,51 @@ return [ | ||||
|     "uploaded" => "Nie udało się wgrać pliku :attribute.", | ||||
|     "url" => "Format pola :attribute jest nieprawidłowy.", | ||||
|     "uuid" => "Pole :attribute musi być poprawnym identyfikatorem UUID.", | ||||
|     "custom" => [ | ||||
|         "education.*.school" => [ | ||||
|             "required" => "Nazwa szkoły jest wymagana.", | ||||
|         ], | ||||
|         "education.*.degree" => [ | ||||
|             "required" => "Stopień jest wymagany.", | ||||
|         ], | ||||
|         "education.*.fieldOfStudy" => [ | ||||
|             "required" => "Kierunek jest wymagany.", | ||||
|         ], | ||||
|         "education.*.startDate" => [ | ||||
|             "required" => "Data rozpoczęcia jest wymagana.", | ||||
|         ], | ||||
|         "education.*.endDate" => [ | ||||
|             "required" => "Data zakończenia jest wymagana.", | ||||
|             "after" => "Data zakończenia musi być datą późniejszą od daty rozpoczęcia.", | ||||
|             "required_if" => "Data zakończenia jest wymagana.", | ||||
|         ], | ||||
|         "languages.*.name" => [ | ||||
|             "distinct" => "Języki nie mogą się powtarzać.", | ||||
|             "required" => "Język jest wymagany.", | ||||
|         ], | ||||
|         "technologies.*.name" => [ | ||||
|             "distinct" => "Technologie nie mogą się powtarzać.", | ||||
|             "required" => "Technologia jest wymagana.", | ||||
|         ], | ||||
|         "projects.*.description" => [ | ||||
|             "required" => "Opis projektu jest wymagany.", | ||||
|         ], | ||||
|         "projects.*.technologies" => [ | ||||
|             "required" => "Opis projektu jest wymagany.", | ||||
|             "min" => "Musi być wybrana co najmniej jedna technologia." | ||||
|         ], | ||||
|         "projects.*.startDate" => [ | ||||
|             "required" => "Data rozpoczęcia jest wymagana.", | ||||
|         ], | ||||
|         "projects.*.endDate" => [ | ||||
|             "required" => "Data zakończenia jest wymagana.", | ||||
|             "after" => "Data zakończenia musi być datą późniejszą od daty rozpoczęcia.", | ||||
|             "required_if" => "Data zakończenia jest wymagana.", | ||||
|         ], | ||||
|         "projects.*.tasks" => [ | ||||
|             "required" => "Zadania w projekcie są wymagane.", | ||||
|         ], | ||||
|     ], | ||||
|     "attributes" => [ | ||||
|         "to" => "do", | ||||
|         "from" => "od", | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								resources/views/docx/resume_eng.docx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								resources/views/docx/resume_eng.docx
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| @@ -10,7 +10,9 @@ use Toby\Infrastructure\Http\Controllers\HolidayController; | ||||
| use Toby\Infrastructure\Http\Controllers\KeysController; | ||||
| use Toby\Infrastructure\Http\Controllers\LogoutController; | ||||
| use Toby\Infrastructure\Http\Controllers\MonthlyUsageController; | ||||
| use Toby\Infrastructure\Http\Controllers\ResumeController; | ||||
| use Toby\Infrastructure\Http\Controllers\SelectYearPeriodController; | ||||
| use Toby\Infrastructure\Http\Controllers\TechnologyController; | ||||
| use Toby\Infrastructure\Http\Controllers\TimesheetController; | ||||
| use Toby\Infrastructure\Http\Controllers\UserController; | ||||
| use Toby\Infrastructure\Http\Controllers\VacationCalendarController; | ||||
| @@ -34,6 +36,12 @@ Route::middleware(["auth", TrackUserLastActivity::class])->group(function (): vo | ||||
|         ->except("show") | ||||
|         ->whereNumber("holiday"); | ||||
|  | ||||
|     Route::resource("resumes", ResumeController::class) | ||||
|         ->whereNumber("resume"); | ||||
|     Route::resource("technologies", TechnologyController::class) | ||||
|         ->only(["index", "store", "destroy"]) | ||||
|         ->whereNumber("technology"); | ||||
|  | ||||
|     Route::get("/keys", [KeysController::class, "index"]); | ||||
|     Route::post("/keys", [KeysController::class, "store"]); | ||||
|     Route::delete("/keys/{key}", [KeysController::class, "destroy"]); | ||||
|   | ||||
							
								
								
									
										150
									
								
								tests/Feature/ResumeTest.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										150
									
								
								tests/Feature/ResumeTest.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,150 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Tests\Feature; | ||||
|  | ||||
| use Illuminate\Foundation\Testing\DatabaseMigrations; | ||||
| use Illuminate\Support\Carbon; | ||||
| use Illuminate\Support\Facades\Storage; | ||||
| use Inertia\Testing\AssertableInertia as Assert; | ||||
| use Tests\FeatureTestCase; | ||||
| use Toby\Domain\Enums\EmploymentForm; | ||||
| use Toby\Eloquent\Models\Resume; | ||||
| use Toby\Eloquent\Models\Technology; | ||||
| use Toby\Eloquent\Models\User; | ||||
|  | ||||
| class ResumeTest extends FeatureTestCase | ||||
| { | ||||
|     use DatabaseMigrations; | ||||
|  | ||||
|     protected function setUp(): void | ||||
|     { | ||||
|         parent::setUp(); | ||||
|  | ||||
|         Storage::fake(); | ||||
|  | ||||
|         Technology::factory()->createMany([ | ||||
|             ["name" => "Laravel"], | ||||
|             ["name" => "Symfony"], | ||||
|         ]); | ||||
|     } | ||||
|  | ||||
|     public function testAdminCanSeeResumesList(): void | ||||
|     { | ||||
|         Resume::factory()->count(10)->create(); | ||||
|         $admin = User::factory()->admin()->create(); | ||||
|  | ||||
|         $this->assertDatabaseCount("resumes", 10); | ||||
|  | ||||
|         $this->actingAs($admin) | ||||
|             ->get("/resumes") | ||||
|             ->assertOk() | ||||
|             ->assertInertia( | ||||
|                 fn(Assert $page) => $page | ||||
|                     ->component("Resumes/Index") | ||||
|                     ->has("resumes.data", 10), | ||||
|             ); | ||||
|     } | ||||
|  | ||||
|     public function testAdminCanCreateResumeForEmployee(): void | ||||
|     { | ||||
|         $admin = User::factory()->admin()->create(); | ||||
|         $user = User::factory()->hasProfile([ | ||||
|             "first_name" => "Jan", | ||||
|             "last_name" => "Kowalski", | ||||
|             "employment_form" => EmploymentForm::EmploymentContract, | ||||
|             "position" => "user", | ||||
|             "employment_date" => Carbon::createFromDate(2021, 1, 4), | ||||
|         ])->create(); | ||||
|  | ||||
|         $this->actingAs($admin) | ||||
|             ->post("/resumes", [ | ||||
|                 "user" => $user->id, | ||||
|             ]) | ||||
|             ->assertRedirect(); | ||||
|  | ||||
|         $this->assertDatabaseHas("resumes", [ | ||||
|             "user_id" => $user->id, | ||||
|         ]); | ||||
|     } | ||||
|  | ||||
|     public function testAdminCanCreateResumeForSomebodyWhoDoesNotExistInTheDatabase(): void | ||||
|     { | ||||
|         $admin = User::factory()->admin()->create(); | ||||
|  | ||||
|         $this->actingAs($admin) | ||||
|             ->post("/resumes", [ | ||||
|                 "name" => "Anna Nowak", | ||||
|             ]) | ||||
|             ->assertRedirect(); | ||||
|  | ||||
|         $this->assertDatabaseHas("resumes", [ | ||||
|             "name" => "Anna Nowak", | ||||
|         ]); | ||||
|     } | ||||
|  | ||||
|     public function testAdminCanEditResume(): void | ||||
|     { | ||||
|         $admin = User::factory()->admin()->create(); | ||||
|         $resume = Resume::factory([ | ||||
|             "name" => "Anna Nowak", | ||||
|             "education" => [ | ||||
|                 "school" => "Testowa Szkoła", | ||||
|                 "degree" => "inżynier", | ||||
|                 "fieldOfStudy" => "Informatyka", | ||||
|                 "current" => false, | ||||
|                 "startDate" => Carbon::createFromDate(2017, 9)->format("m/Y"), | ||||
|                 "endDate" => Carbon::createFromDate(2021, 3)->format("m/Y"), | ||||
|             ], | ||||
|             "languages" => [ | ||||
|                 "name" => "English", | ||||
|                 "level" => "C2", | ||||
|             ], | ||||
|             "technologies" => [ | ||||
|                 "name" => "Laravel", | ||||
|                 "level" => "Expert", | ||||
|             ], | ||||
|             "projects" => [ | ||||
|                 "description" => "Test project", | ||||
|                 "technologies" => Technology::all()->pluck("name"), | ||||
|                 "current" => false, | ||||
|                 "startDate" => Carbon::createFromDate(2021, 3)->format("m/Y"), | ||||
|                 "endDate" => Carbon::createFromDate(2022, 1)->format("m/Y"), | ||||
|                 "tasks" => "Tasks", | ||||
|             ], | ||||
|         ])->create(); | ||||
|  | ||||
|         $this->actingAs($admin) | ||||
|             ->put("/resumes/{$resume->id}", [ | ||||
|                 "name" => "Natalia Kowalska", | ||||
|             ]) | ||||
|             ->assertSessionHasNoErrors(); | ||||
|  | ||||
|         $this->assertDatabaseHas("resumes", [ | ||||
|             "name" => "Natalia Kowalska", | ||||
|         ]); | ||||
|     } | ||||
|  | ||||
|     public function testAdminCanGenerateResume(): void | ||||
|     { | ||||
|         $resume = Resume::factory()->create(); | ||||
|         $admin = User::factory()->admin()->create(); | ||||
|  | ||||
|         $this->actingAs($admin) | ||||
|             ->get("/resumes/{$resume->id}") | ||||
|             ->assertDownload("resume-{$resume->id}.docx"); | ||||
|     } | ||||
|  | ||||
|     public function testAdminCanDeleteResume(): void | ||||
|     { | ||||
|         $resume = Resume::factory()->create(); | ||||
|         $admin = User::factory()->admin()->create(); | ||||
|  | ||||
|         $this->actingAs($admin) | ||||
|             ->delete("/resumes/{$resume->id}") | ||||
|             ->assertSessionHasNoErrors(); | ||||
|  | ||||
|         $this->assertModelMissing($resume); | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user