#134 - fill users data for resume #144
| @@ -1,5 +1,4 @@ | |||||||
| module.exports = { | module.exports = { | ||||||
|   plugins: ['tailwindcss'], |  | ||||||
|   env: { |   env: { | ||||||
|     node: true, |     node: true, | ||||||
|     'vue/setup-compiler-macros': true, |     'vue/setup-compiler-macros': true, | ||||||
| @@ -16,11 +15,6 @@ module.exports = { | |||||||
|     'comma-dangle': ['error', 'always-multiline'], |     'comma-dangle': ['error', 'always-multiline'], | ||||||
|     'object-curly-spacing': ['error', 'always'], |     'object-curly-spacing': ['error', 'always'], | ||||||
|     'vue/require-default-prop': 0, |     '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, |     '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("manageVacationLimits", fn(User $user): bool => $user->role === Role::AdministrativeApprover); | ||||||
|         Gate::define("generateTimesheet", 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("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, |                 "manageUsers" => $user ? $user->can("manageUsers") : false, | ||||||
|                 "listAllVacationRequests" => $user ? $user->can("listAll", VacationRequest::class) : false, |                 "listAllVacationRequests" => $user ? $user->can("listAll", VacationRequest::class) : false, | ||||||
|                 "listMonthlyUsage" => $user ? $user->can("listMonthlyUsage") : 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/telescope": "^4.6", | ||||||
|         "laravel/tinker": "^2.5", |         "laravel/tinker": "^2.5", | ||||||
|         "maatwebsite/excel": "^3.1", |         "maatwebsite/excel": "^3.1", | ||||||
|  |         "phpoffice/phpword": "^0.18.3", | ||||||
|         "rackbeat/laravel-ui-avatars": "^1.0", |         "rackbeat/laravel-ui-avatars": "^1.0", | ||||||
|         "spatie/laravel-google-calendar": "^3.5", |         "spatie/laravel-google-calendar": "^3.5", | ||||||
|         "spatie/laravel-model-states": "^2.1", |         "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", |         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", | ||||||
|         "This file is @generated automatically" |         "This file is @generated automatically" | ||||||
|     ], |     ], | ||||||
|     "content-hash": "c23bb050dbab6338570f0f73adb83cf8", |     "content-hash": "4eda92fdbff36c6073984536a46dde77", | ||||||
|     "packages": [ |     "packages": [ | ||||||
|         { |         { | ||||||
|             "name": "azuyalabs/yasumi", |             "name": "azuyalabs/yasumi", | ||||||
| @@ -1039,16 +1039,16 @@ | |||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|             "name": "google/apiclient-services", |             "name": "google/apiclient-services", | ||||||
|             "version": "v0.247.0", |             "version": "v0.248.0", | ||||||
|             "source": { |             "source": { | ||||||
|                 "type": "git", |                 "type": "git", | ||||||
|                 "url": "https://github.com/googleapis/google-api-php-client-services.git", |                 "url": "https://github.com/googleapis/google-api-php-client-services.git", | ||||||
|                 "reference": "1fc6eab7512b4e2dd4b6c96b2697f9b6bfbaddc2" |                 "reference": "5967583706b1b09c49d04e3c1c3c05e459b0767e" | ||||||
|             }, |             }, | ||||||
|             "dist": { |             "dist": { | ||||||
|                 "type": "zip", |                 "type": "zip", | ||||||
|                 "url": "https://api.github.com/repos/googleapis/google-api-php-client-services/zipball/1fc6eab7512b4e2dd4b6c96b2697f9b6bfbaddc2", |                 "url": "https://api.github.com/repos/googleapis/google-api-php-client-services/zipball/5967583706b1b09c49d04e3c1c3c05e459b0767e", | ||||||
|                 "reference": "1fc6eab7512b4e2dd4b6c96b2697f9b6bfbaddc2", |                 "reference": "5967583706b1b09c49d04e3c1c3c05e459b0767e", | ||||||
|                 "shasum": "" |                 "shasum": "" | ||||||
|             }, |             }, | ||||||
|             "require": { |             "require": { | ||||||
| @@ -1077,9 +1077,9 @@ | |||||||
|             ], |             ], | ||||||
|             "support": { |             "support": { | ||||||
|                 "issues": "https://github.com/googleapis/google-api-php-client-services/issues", |                 "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", |             "name": "google/auth", | ||||||
| @@ -1678,17 +1678,79 @@ | |||||||
|             "time": "2021-12-16T16:49:26+00:00" |             "time": "2021-12-16T16:49:26+00:00" | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|             "name": "laravel/framework", |             "name": "laminas/laminas-escaper", | ||||||
|             "version": "v9.11.0", |             "version": "2.10.0", | ||||||
|             "source": { |             "source": { | ||||||
|                 "type": "git", |                 "type": "git", | ||||||
|                 "url": "https://github.com/laravel/framework.git", |                 "url": "https://github.com/laminas/laminas-escaper.git", | ||||||
|                 "reference": "598a8c84d452a66b90a3213b1d67189cc726c728" |                 "reference": "58af67282db37d24e584a837a94ee55b9c7552be" | ||||||
|             }, |             }, | ||||||
|             "dist": { |             "dist": { | ||||||
|                 "type": "zip", |                 "type": "zip", | ||||||
|                 "url": "https://api.github.com/repos/laravel/framework/zipball/598a8c84d452a66b90a3213b1d67189cc726c728", |                 "url": "https://api.github.com/repos/laminas/laminas-escaper/zipball/58af67282db37d24e584a837a94ee55b9c7552be", | ||||||
|                 "reference": "598a8c84d452a66b90a3213b1d67189cc726c728", |                 "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": "" |                 "shasum": "" | ||||||
|             }, |             }, | ||||||
|             "require": { |             "require": { | ||||||
| @@ -1854,7 +1916,7 @@ | |||||||
|                 "issues": "https://github.com/laravel/framework/issues", |                 "issues": "https://github.com/laravel/framework/issues", | ||||||
|                 "source": "https://github.com/laravel/framework" |                 "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", |             "name": "laravel/helpers", | ||||||
| @@ -3122,16 +3184,16 @@ | |||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|             "name": "monolog/monolog", |             "name": "monolog/monolog", | ||||||
|             "version": "2.5.0", |             "version": "2.6.0", | ||||||
|             "source": { |             "source": { | ||||||
|                 "type": "git", |                 "type": "git", | ||||||
|                 "url": "https://github.com/Seldaek/monolog.git", |                 "url": "https://github.com/Seldaek/monolog.git", | ||||||
|                 "reference": "4192345e260f1d51b365536199744b987e160edc" |                 "reference": "247918972acd74356b0a91dfaa5adcaec069b6c0" | ||||||
|             }, |             }, | ||||||
|             "dist": { |             "dist": { | ||||||
|                 "type": "zip", |                 "type": "zip", | ||||||
|                 "url": "https://api.github.com/repos/Seldaek/monolog/zipball/4192345e260f1d51b365536199744b987e160edc", |                 "url": "https://api.github.com/repos/Seldaek/monolog/zipball/247918972acd74356b0a91dfaa5adcaec069b6c0", | ||||||
|                 "reference": "4192345e260f1d51b365536199744b987e160edc", |                 "reference": "247918972acd74356b0a91dfaa5adcaec069b6c0", | ||||||
|                 "shasum": "" |                 "shasum": "" | ||||||
|             }, |             }, | ||||||
|             "require": { |             "require": { | ||||||
| @@ -3144,18 +3206,23 @@ | |||||||
|             "require-dev": { |             "require-dev": { | ||||||
|                 "aws/aws-sdk-php": "^2.4.9 || ^3.0", |                 "aws/aws-sdk-php": "^2.4.9 || ^3.0", | ||||||
|                 "doctrine/couchdb": "~1.0@dev", |                 "doctrine/couchdb": "~1.0@dev", | ||||||
|                 "elasticsearch/elasticsearch": "^7", |                 "elasticsearch/elasticsearch": "^7 || ^8", | ||||||
|  |                 "ext-json": "*", | ||||||
|                 "graylog2/gelf-php": "^1.4.2", |                 "graylog2/gelf-php": "^1.4.2", | ||||||
|  |                 "guzzlehttp/guzzle": "^7.4", | ||||||
|  |                 "guzzlehttp/psr7": "^2.2", | ||||||
|                 "mongodb/mongodb": "^1.8", |                 "mongodb/mongodb": "^1.8", | ||||||
|                 "php-amqplib/php-amqplib": "~2.4 || ^3", |                 "php-amqplib/php-amqplib": "~2.4 || ^3", | ||||||
|                 "php-console/php-console": "^3.1.3", |                 "php-console/php-console": "^3.1.3", | ||||||
|                 "phpspec/prophecy": "^1.6.1", |                 "phpspec/prophecy": "^1.15", | ||||||
|                 "phpstan/phpstan": "^0.12.91", |                 "phpstan/phpstan": "^0.12.91", | ||||||
|                 "phpunit/phpunit": "^8.5", |                 "phpunit/phpunit": "^8.5.14", | ||||||
|                 "predis/predis": "^1.1", |                 "predis/predis": "^1.1", | ||||||
|                 "rollbar/rollbar": "^1.3 || ^2 || ^3", |                 "rollbar/rollbar": "^1.3 || ^2 || ^3", | ||||||
|                 "ruflin/elastica": ">=0.90@dev", |                 "ruflin/elastica": "^7", | ||||||
|                 "swiftmailer/swiftmailer": "^5.3|^6.0" |                 "swiftmailer/swiftmailer": "^5.3|^6.0", | ||||||
|  |                 "symfony/mailer": "^5.4 || ^6", | ||||||
|  |                 "symfony/mime": "^5.4 || ^6" | ||||||
|             }, |             }, | ||||||
|             "suggest": { |             "suggest": { | ||||||
|                 "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", |                 "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", | ||||||
| @@ -3205,7 +3272,7 @@ | |||||||
|             ], |             ], | ||||||
|             "support": { |             "support": { | ||||||
|                 "issues": "https://github.com/Seldaek/monolog/issues", |                 "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": [ |             "funding": [ | ||||||
|                 { |                 { | ||||||
| @@ -3217,7 +3284,7 @@ | |||||||
|                     "type": "tidelift" |                     "type": "tidelift" | ||||||
|                 } |                 } | ||||||
|             ], |             ], | ||||||
|             "time": "2022-04-08T15:43:54+00:00" |             "time": "2022-05-10T09:36:00+00:00" | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|             "name": "myclabs/php-enum", |             "name": "myclabs/php-enum", | ||||||
| @@ -3963,6 +4030,118 @@ | |||||||
|             }, |             }, | ||||||
|             "time": "2022-04-24T13:53:10+00:00" |             "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", |             "name": "phpoption/phpoption", | ||||||
|             "version": "1.8.1", |             "version": "1.8.1", | ||||||
| @@ -4558,16 +4737,16 @@ | |||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|             "name": "psy/psysh", |             "name": "psy/psysh", | ||||||
|             "version": "v0.11.2", |             "version": "v0.11.4", | ||||||
|             "source": { |             "source": { | ||||||
|                 "type": "git", |                 "type": "git", | ||||||
|                 "url": "https://github.com/bobthecow/psysh.git", |                 "url": "https://github.com/bobthecow/psysh.git", | ||||||
|                 "reference": "7f7da640d68b9c9fec819caae7c744a213df6514" |                 "reference": "05c544b339b112226ad14803e1e5b09a61957454" | ||||||
|             }, |             }, | ||||||
|             "dist": { |             "dist": { | ||||||
|                 "type": "zip", |                 "type": "zip", | ||||||
|                 "url": "https://api.github.com/repos/bobthecow/psysh/zipball/7f7da640d68b9c9fec819caae7c744a213df6514", |                 "url": "https://api.github.com/repos/bobthecow/psysh/zipball/05c544b339b112226ad14803e1e5b09a61957454", | ||||||
|                 "reference": "7f7da640d68b9c9fec819caae7c744a213df6514", |                 "reference": "05c544b339b112226ad14803e1e5b09a61957454", | ||||||
|                 "shasum": "" |                 "shasum": "" | ||||||
|             }, |             }, | ||||||
|             "require": { |             "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" |                 "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": { |             "require-dev": { | ||||||
|                 "bamarni/composer-bin-plugin": "^1.2", |                 "bamarni/composer-bin-plugin": "^1.2" | ||||||
|                 "hoa/console": "3.17.05.02" |  | ||||||
|             }, |             }, | ||||||
|             "suggest": { |             "suggest": { | ||||||
|                 "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", |                 "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", | ||||||
|                 "ext-pdo-sqlite": "The doc command requires SQLite to work.", |                 "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-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.", |                 "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." |  | ||||||
|             }, |             }, | ||||||
|             "bin": [ |             "bin": [ | ||||||
|                 "bin/psysh" |                 "bin/psysh" | ||||||
| @@ -4630,9 +4807,9 @@ | |||||||
|             ], |             ], | ||||||
|             "support": { |             "support": { | ||||||
|                 "issues": "https://github.com/bobthecow/psysh/issues", |                 "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", |             "name": "rackbeat/laravel-ui-avatars", | ||||||
| @@ -7599,16 +7776,16 @@ | |||||||
|     "packages-dev": [ |     "packages-dev": [ | ||||||
|         { |         { | ||||||
|             "name": "blumilksoftware/codestyle", |             "name": "blumilksoftware/codestyle", | ||||||
|             "version": "v1.2.0", |             "version": "v1.3.0", | ||||||
|             "source": { |             "source": { | ||||||
|                 "type": "git", |                 "type": "git", | ||||||
|                 "url": "https://github.com/blumilksoftware/codestyle.git", |                 "url": "https://github.com/blumilksoftware/codestyle.git", | ||||||
|                 "reference": "124c55f0374d8f6952675011e359cb54f40f4090" |                 "reference": "bf694da19f2cd5d0575b8fad585d3570d6785c23" | ||||||
|             }, |             }, | ||||||
|             "dist": { |             "dist": { | ||||||
|                 "type": "zip", |                 "type": "zip", | ||||||
|                 "url": "https://api.github.com/repos/blumilksoftware/codestyle/zipball/124c55f0374d8f6952675011e359cb54f40f4090", |                 "url": "https://api.github.com/repos/blumilksoftware/codestyle/zipball/bf694da19f2cd5d0575b8fad585d3570d6785c23", | ||||||
|                 "reference": "124c55f0374d8f6952675011e359cb54f40f4090", |                 "reference": "bf694da19f2cd5d0575b8fad585d3570d6785c23", | ||||||
|                 "shasum": "" |                 "shasum": "" | ||||||
|             }, |             }, | ||||||
|             "require": { |             "require": { | ||||||
| @@ -7643,9 +7820,9 @@ | |||||||
|             "description": "Blumilk codestyle configurator", |             "description": "Blumilk codestyle configurator", | ||||||
|             "support": { |             "support": { | ||||||
|                 "issues": "https://github.com/blumilksoftware/codestyle/issues", |                 "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", |             "name": "composer/pcre", | ||||||
| @@ -8335,16 +8512,16 @@ | |||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|             "name": "laravel/dusk", |             "name": "laravel/dusk", | ||||||
|             "version": "v6.23.1", |             "version": "v6.24.0", | ||||||
|             "source": { |             "source": { | ||||||
|                 "type": "git", |                 "type": "git", | ||||||
|                 "url": "https://github.com/laravel/dusk.git", |                 "url": "https://github.com/laravel/dusk.git", | ||||||
|                 "reference": "41f6deb42ae42b9b7dae1c32c03cb35d365d3118" |                 "reference": "7fed3695741787d9998c5f04c94adfd62d70e766" | ||||||
|             }, |             }, | ||||||
|             "dist": { |             "dist": { | ||||||
|                 "type": "zip", |                 "type": "zip", | ||||||
|                 "url": "https://api.github.com/repos/laravel/dusk/zipball/41f6deb42ae42b9b7dae1c32c03cb35d365d3118", |                 "url": "https://api.github.com/repos/laravel/dusk/zipball/7fed3695741787d9998c5f04c94adfd62d70e766", | ||||||
|                 "reference": "41f6deb42ae42b9b7dae1c32c03cb35d365d3118", |                 "reference": "7fed3695741787d9998c5f04c94adfd62d70e766", | ||||||
|                 "shasum": "" |                 "shasum": "" | ||||||
|             }, |             }, | ||||||
|             "require": { |             "require": { | ||||||
| @@ -8402,9 +8579,9 @@ | |||||||
|             ], |             ], | ||||||
|             "support": { |             "support": { | ||||||
|                 "issues": "https://github.com/laravel/dusk/issues", |                 "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", |             "name": "mockery/mockery", | ||||||
| @@ -10592,16 +10769,16 @@ | |||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|             "name": "spatie/ignition", |             "name": "spatie/ignition", | ||||||
|             "version": "1.2.9", |             "version": "1.2.10", | ||||||
|             "source": { |             "source": { | ||||||
|                 "type": "git", |                 "type": "git", | ||||||
|                 "url": "https://github.com/spatie/ignition.git", |                 "url": "https://github.com/spatie/ignition.git", | ||||||
|                 "reference": "db25202fab2d5c14613b8914a1bb374998bbf870" |                 "reference": "dd8c3a21170b1d0f4d15048b2f4fa4a1a3a92a64" | ||||||
|             }, |             }, | ||||||
|             "dist": { |             "dist": { | ||||||
|                 "type": "zip", |                 "type": "zip", | ||||||
|                 "url": "https://api.github.com/repos/spatie/ignition/zipball/db25202fab2d5c14613b8914a1bb374998bbf870", |                 "url": "https://api.github.com/repos/spatie/ignition/zipball/dd8c3a21170b1d0f4d15048b2f4fa4a1a3a92a64", | ||||||
|                 "reference": "db25202fab2d5c14613b8914a1bb374998bbf870", |                 "reference": "dd8c3a21170b1d0f4d15048b2f4fa4a1a3a92a64", | ||||||
|                 "shasum": "" |                 "shasum": "" | ||||||
|             }, |             }, | ||||||
|             "require": { |             "require": { | ||||||
| @@ -10658,20 +10835,20 @@ | |||||||
|                     "type": "github" |                     "type": "github" | ||||||
|                 } |                 } | ||||||
|             ], |             ], | ||||||
|             "time": "2022-04-23T20:37:21+00:00" |             "time": "2022-05-10T12:21:27+00:00" | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|             "name": "spatie/laravel-ignition", |             "name": "spatie/laravel-ignition", | ||||||
|             "version": "1.2.2", |             "version": "1.2.3", | ||||||
|             "source": { |             "source": { | ||||||
|                 "type": "git", |                 "type": "git", | ||||||
|                 "url": "https://github.com/spatie/laravel-ignition.git", |                 "url": "https://github.com/spatie/laravel-ignition.git", | ||||||
|                 "reference": "924d1ae878874ad0bb49f63b69a9af759a34ee78" |                 "reference": "51e5daaa7e43c154fe57f1ddfbba862f9fe57646" | ||||||
|             }, |             }, | ||||||
|             "dist": { |             "dist": { | ||||||
|                 "type": "zip", |                 "type": "zip", | ||||||
|                 "url": "https://api.github.com/repos/spatie/laravel-ignition/zipball/924d1ae878874ad0bb49f63b69a9af759a34ee78", |                 "url": "https://api.github.com/repos/spatie/laravel-ignition/zipball/51e5daaa7e43c154fe57f1ddfbba862f9fe57646", | ||||||
|                 "reference": "924d1ae878874ad0bb49f63b69a9af759a34ee78", |                 "reference": "51e5daaa7e43c154fe57f1ddfbba862f9fe57646", | ||||||
|                 "shasum": "" |                 "shasum": "" | ||||||
|             }, |             }, | ||||||
|             "require": { |             "require": { | ||||||
| @@ -10748,7 +10925,7 @@ | |||||||
|                     "type": "github" |                     "type": "github" | ||||||
|                 } |                 } | ||||||
|             ], |             ], | ||||||
|             "time": "2022-04-14T18:04:51+00:00" |             "time": "2022-05-05T15:53:24+00:00" | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|             "name": "symfony/filesystem", |             "name": "symfony/filesystem", | ||||||
| @@ -11004,5 +11181,5 @@ | |||||||
|         "ext-redis": "*" |         "ext-redis": "*" | ||||||
|     }, |     }, | ||||||
|     "platform-dev": [], |     "platform-dev": [], | ||||||
|     "plugin-api-version": "2.3.0" |     "plugin-api-version": "2.1.0" | ||||||
| } | } | ||||||
|   | |||||||
| @@ -109,7 +109,7 @@ return [ | |||||||
|         VacationTypeConfigRetriever::KEY_IS_VACATION => true, |         VacationTypeConfigRetriever::KEY_IS_VACATION => true, | ||||||
|     ], |     ], | ||||||
|     VacationType::Absence->value => [ |     VacationType::Absence->value => [ | ||||||
|         VacationTypeConfigRetriever::KEY_TECHNICAL_APPROVAL => true, |         VacationTypeConfigRetriever::KEY_TECHNICAL_APPROVAL => false, | ||||||
|         VacationTypeConfigRetriever::KEY_ADMINISTRATIVE_APPROVAL => false, |         VacationTypeConfigRetriever::KEY_ADMINISTRATIVE_APPROVAL => false, | ||||||
|         VacationTypeConfigRetriever::KEY_BILLABLE => false, |         VacationTypeConfigRetriever::KEY_BILLABLE => false, | ||||||
|         VacationTypeConfigRetriever::KEY_HAS_LIMIT => 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\States\VacationRequest\WaitingForTechnical; | ||||||
| use Toby\Domain\WorkDaysCalculator; | use Toby\Domain\WorkDaysCalculator; | ||||||
| use Toby\Eloquent\Models\Key; | use Toby\Eloquent\Models\Key; | ||||||
|  | use Toby\Eloquent\Models\Resume; | ||||||
|  | use Toby\Eloquent\Models\Technology; | ||||||
| use Toby\Eloquent\Models\User; | use Toby\Eloquent\Models\User; | ||||||
| use Toby\Eloquent\Models\VacationLimit; | use Toby\Eloquent\Models\VacationLimit; | ||||||
| use Toby\Eloquent\Models\VacationRequest; | use Toby\Eloquent\Models\VacationRequest; | ||||||
| @@ -332,5 +334,35 @@ class DemoSeeder extends Seeder | |||||||
|                 ->for($user) |                 ->for($user) | ||||||
|                 ->create(); |                 ->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(); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										2111
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2111
									
								
								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", |         "prod": "npm run production", | ||||||
|         "production": "mix --production", |         "production": "mix --production", | ||||||
|         "lint": "./node_modules/.bin/eslint resources/js --ext .js,.vue", |         "lint": "./node_modules/.bin/eslint resources/js --ext .js,.vue", | ||||||
|         "lintf": "./node_modules/.bin/eslint resources/js --ext .js,.vue --fix", |         "lintf": "./node_modules/.bin/eslint resources/js --ext .js,.vue --fix" | ||||||
|         "postinstall": "npm run prod" |  | ||||||
|     }, |     }, | ||||||
|     "dependencies": { |     "dependencies": { | ||||||
|         "@headlessui/vue": "^1.6.1", |         "@headlessui/vue": "^1.6.1", | ||||||
| @@ -21,7 +20,7 @@ | |||||||
|         "@tailwindcss/forms": "^0.5.1", |         "@tailwindcss/forms": "^0.5.1", | ||||||
|         "@tailwindcss/line-clamp": "^0.4.0", |         "@tailwindcss/line-clamp": "^0.4.0", | ||||||
|         "@tailwindcss/typography": "^0.5.2", |         "@tailwindcss/typography": "^0.5.2", | ||||||
|         "@vue/compiler-sfc": "^3.2.33", |         "@vue/compiler-sfc": "^3.2.31", | ||||||
|         "autoprefixer": "^10.4.7", |         "autoprefixer": "^10.4.7", | ||||||
|         "axios": "^0.27.2", |         "axios": "^0.27.2", | ||||||
|         "echarts": "^5.3.2", |         "echarts": "^5.3.2", | ||||||
| @@ -32,17 +31,17 @@ | |||||||
|         "luxon": "^2.3.2", |         "luxon": "^2.3.2", | ||||||
|         "postcss": "^8.4.13", |         "postcss": "^8.4.13", | ||||||
|         "tailwindcss": "^3.0.24", |         "tailwindcss": "^3.0.24", | ||||||
|         "vue": "^3.2.33", |         "vue": "^3.2.21", | ||||||
|         "vue-echarts": "^6.0.2", |         "vue-echarts": "^6.0.2", | ||||||
|         "vue-flatpickr-component": "^9.0.6", |         "vue-flatpickr-component": "^9.0.6", | ||||||
|         "vue-loader": "^17.0.0", |         "vue-loader": "^17.0.0", | ||||||
|         "vue-material-design-icons": "^5.0.0", |         "vue-material-design-icons": "^5.0.0", | ||||||
|         "vue-toastification": "^2.0.0-rc.5", |         "vue-toastification": "^2.0.0-rc.5", | ||||||
|         "vue3-popper": "^1.4.2" |         "vue3-popper": "^1.4.2", | ||||||
|  |         "vuedraggable": "^4.1.0" | ||||||
|     }, |     }, | ||||||
|     "devDependencies": { |     "devDependencies": { | ||||||
|         "eslint": "^8.14.0", |         "eslint": "^8.14.0", | ||||||
|         "eslint-plugin-tailwindcss": "^3.5.0", |  | ||||||
|         "eslint-plugin-vue": "^8.7.1" |         "eslint-plugin-vue": "^8.7.1" | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -19,6 +19,7 @@ | |||||||
|     </coverage> |     </coverage> | ||||||
|     <php> |     <php> | ||||||
|         <server name="APP_ENV" value="testing"/> |         <server name="APP_ENV" value="testing"/> | ||||||
|  |         <env name="APP_URL" value="http://localhost"/> | ||||||
|         <env name="APP_KEY" value="base64:SKEJSy9oF9chQBCMbxqgj5zhtAvug9kwZ+cDiP1Y8A8="/> |         <env name="APP_KEY" value="base64:SKEJSy9oF9chQBCMbxqgj5zhtAvug9kwZ+cDiP1Y8A8="/> | ||||||
|         <env name="BCRYPT_ROUNDS" value="4"/> |         <env name="BCRYPT_ROUNDS" value="4"/> | ||||||
|         <env name="CACHE_DRIVER" value="array"/> |         <env name="CACHE_DRIVER" value="array"/> | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| @import 'flatpickr/dist/themes/light.css'; | @import 'flatpickr/dist/themes/light.css'; | ||||||
|  | @import 'flatpickr/dist/plugins/monthSelect/style.css'; | ||||||
| @import 'vue-toastification/dist/index.css'; | @import 'vue-toastification/dist/index.css'; | ||||||
|  |  | ||||||
| @tailwind base; | @tailwind base; | ||||||
| @@ -28,7 +29,8 @@ | |||||||
| .flatpickr-day.endRange.prevMonthDay, | .flatpickr-day.endRange.prevMonthDay, | ||||||
| .flatpickr-day.selected.nextMonthDay, | .flatpickr-day.selected.nextMonthDay, | ||||||
| .flatpickr-day.startRange.nextMonthDay, | .flatpickr-day.startRange.nextMonthDay, | ||||||
| .flatpickr-day.endRange.nextMonthDay { | .flatpickr-day.endRange.nextMonthDay, | ||||||
|  | .flatpickr-monthSelect-month.selected { | ||||||
|     background: #527ABA; |     background: #527ABA; | ||||||
|     -webkit-box-shadow: none; |     -webkit-box-shadow: none; | ||||||
|     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> |             </InertiaLink> | ||||||
|           </div> |           </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"> |             <div class="px-2 space-y-1"> | ||||||
|               <InertiaLink |               <InertiaLink | ||||||
|                 href="/" |                 href="/" | ||||||
| @@ -70,10 +70,12 @@ | |||||||
|                 Strona główna |                 Strona główna | ||||||
|               </InertiaLink> |               </InertiaLink> | ||||||
|             </div> |             </div> | ||||||
|             <div class="pt-3 mt-3"> |             <div | ||||||
|               <div class="py-1 px-2 space-y-1"> |               v-if="vacationNavigation.length" | ||||||
|  |               class="py-1 px-2 space-y-1" | ||||||
|  |             > | ||||||
|               <InertiaLink |               <InertiaLink | ||||||
|                   v-for="item in navigation" |                 v-for="item in vacationNavigation" | ||||||
|                 :key="item.name" |                 :key="item.name" | ||||||
|                 :href="item.href" |                 :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']" |                 :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']" | ||||||
| @@ -92,6 +94,29 @@ | |||||||
|                 </span> |                 </span> | ||||||
|               </InertiaLink> |               </InertiaLink> | ||||||
|             </div> |             </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> |             </div> | ||||||
|           </nav> |           </nav> | ||||||
|         </div> |         </div> | ||||||
| @@ -110,7 +135,7 @@ | |||||||
|           > |           > | ||||||
|         </InertiaLink> |         </InertiaLink> | ||||||
|       </div> |       </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 |         <InertiaLink | ||||||
|           href="/" |           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']" |           :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" /> |           <HomeIcon class="shrink-0 mr-4 w-6 h-6 text-blumilk-200" /> | ||||||
|           Strona główna |           Strona główna | ||||||
|         </InertiaLink> |         </InertiaLink> | ||||||
|         <div class="pt-1 mt-1 space-y-1"> |         <div | ||||||
|  |           v-if="vacationNavigation.length" | ||||||
|  |           class="pt-1 mt-1 space-y-1" | ||||||
|  |         > | ||||||
|           <InertiaLink |           <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" |             :key="item.name" | ||||||
|             :href="item.href" |             :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']" |             :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, |   DocumentTextIcon, | ||||||
|   AdjustmentsIcon, |   AdjustmentsIcon, | ||||||
|   KeyIcon, |   KeyIcon, | ||||||
|  |   TemplateIcon, BeakerIcon, | ||||||
| } from '@heroicons/vue/outline' | } from '@heroicons/vue/outline' | ||||||
| import { CheckIcon, ChevronDownIcon } from '@heroicons/vue/solid' | import { CheckIcon, ChevronDownIcon } from '@heroicons/vue/solid' | ||||||
|  |  | ||||||
| @@ -306,7 +358,7 @@ const props = defineProps({ | |||||||
|  |  | ||||||
| const sidebarOpen = ref(false) | const sidebarOpen = ref(false) | ||||||
|  |  | ||||||
| const navigation = computed(() => | const vacationNavigation = computed(() => | ||||||
|   [ |   [ | ||||||
|     { |     { | ||||||
|       name: 'Moje wnioski', |       name: 'Moje wnioski', | ||||||
| @@ -358,6 +410,9 @@ const navigation = computed(() => | |||||||
|       icon: ClipboardListIcon, |       icon: ClipboardListIcon, | ||||||
|       can: true, |       can: true, | ||||||
|     }, |     }, | ||||||
|  |   ].filter(item => item.can)) | ||||||
|  |  | ||||||
|  | const miscNavigaction = computed(() => [ | ||||||
|   { |   { | ||||||
|     name: 'Użytkownicy', |     name: 'Użytkownicy', | ||||||
|     href: '/users', |     href: '/users', | ||||||
| @@ -372,6 +427,19 @@ const navigation = computed(() => | |||||||
|     icon: KeyIcon, |     icon: KeyIcon, | ||||||
|     can: true, |     can: true, | ||||||
|   }, |   }, | ||||||
|  |   { | ||||||
|   ].filter(item => item.can)) |     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> | </script> | ||||||
|   | |||||||
| @@ -74,6 +74,11 @@ | |||||||
|   "Key no :number has been given to :user.": "Klucz nr :number został przekazany użytkownikowi :user.", |   "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", |   ":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", |   ":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.", |   "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ń:", |   ":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", |   "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.", |     "uploaded" => "Nie udało się wgrać pliku :attribute.", | ||||||
|     "url" => "Format pola :attribute jest nieprawidłowy.", |     "url" => "Format pola :attribute jest nieprawidłowy.", | ||||||
|     "uuid" => "Pole :attribute musi być poprawnym identyfikatorem UUID.", |     "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" => [ |     "attributes" => [ | ||||||
|         "to" => "do", |         "to" => "do", | ||||||
|         "from" => "od", |         "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\KeysController; | ||||||
| use Toby\Infrastructure\Http\Controllers\LogoutController; | use Toby\Infrastructure\Http\Controllers\LogoutController; | ||||||
| use Toby\Infrastructure\Http\Controllers\MonthlyUsageController; | use Toby\Infrastructure\Http\Controllers\MonthlyUsageController; | ||||||
|  | use Toby\Infrastructure\Http\Controllers\ResumeController; | ||||||
| use Toby\Infrastructure\Http\Controllers\SelectYearPeriodController; | use Toby\Infrastructure\Http\Controllers\SelectYearPeriodController; | ||||||
|  | use Toby\Infrastructure\Http\Controllers\TechnologyController; | ||||||
| use Toby\Infrastructure\Http\Controllers\TimesheetController; | use Toby\Infrastructure\Http\Controllers\TimesheetController; | ||||||
| use Toby\Infrastructure\Http\Controllers\UserController; | use Toby\Infrastructure\Http\Controllers\UserController; | ||||||
| use Toby\Infrastructure\Http\Controllers\VacationCalendarController; | use Toby\Infrastructure\Http\Controllers\VacationCalendarController; | ||||||
| @@ -34,6 +36,12 @@ Route::middleware(["auth", TrackUserLastActivity::class])->group(function (): vo | |||||||
|         ->except("show") |         ->except("show") | ||||||
|         ->whereNumber("holiday"); |         ->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::get("/keys", [KeysController::class, "index"]); | ||||||
|     Route::post("/keys", [KeysController::class, "store"]); |     Route::post("/keys", [KeysController::class, "store"]); | ||||||
|     Route::delete("/keys/{key}", [KeysController::class, "destroy"]); |     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