fix
This commit is contained in:
		| @@ -35,6 +35,6 @@ class AuthServiceProvider extends ServiceProvider | ||||
|         Gate::define("manageVacationLimits", fn(User $user): bool => $user->role === Role::AdministrativeApprover); | ||||
|         Gate::define("generateTimesheet", fn(User $user): bool => $user->role === Role::AdministrativeApprover); | ||||
|         Gate::define("listMonthlyUsage", fn(User $user): bool => $user->role === Role::AdministrativeApprover); | ||||
|         Gate::define("manageResumes", fn(User $user): bool => $user->role === Role::AdministrativeApprover); | ||||
|         Gate::define("manageResumes", fn(User $user): bool => $user->role === Role::TechnicalApprover); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -40,7 +40,7 @@ class ResumeGenerator | ||||
|  | ||||
|     protected function fillTechnologies(TemplateProcessor $processor, Resume $resume): void | ||||
|     { | ||||
|         if ($resume->technologies->count() <= 0) { | ||||
|         if ($resume->technologies->isEmpty()) { | ||||
|             $processor->deleteBlock("technologies"); | ||||
|  | ||||
|             return; | ||||
| @@ -51,7 +51,7 @@ class ResumeGenerator | ||||
|  | ||||
|     protected function fillLanguages(TemplateProcessor $processor, Resume $resume): void | ||||
|     { | ||||
|         if ($resume->education->count() <= 0) { | ||||
|         if ($resume->education->isEmpty()) { | ||||
|             $processor->deleteBlock("languages"); | ||||
|  | ||||
|             return; | ||||
| @@ -62,7 +62,7 @@ class ResumeGenerator | ||||
|  | ||||
|     protected function fillEducation(TemplateProcessor $processor, Resume $resume): void | ||||
|     { | ||||
|         if ($resume->education->count() <= 0) { | ||||
|         if ($resume->education->isEmpty()) { | ||||
|             $processor->deleteBlock("education"); | ||||
|  | ||||
|             return; | ||||
| @@ -73,7 +73,7 @@ class ResumeGenerator | ||||
|  | ||||
|     protected function fillProjects(TemplateProcessor $processor, Resume $resume): void | ||||
|     { | ||||
|         if ($resume->projects->count() <= 0) { | ||||
|         if ($resume->projects->isEmpty()) { | ||||
|             $processor->deleteBlock("projects"); | ||||
|  | ||||
|             return; | ||||
| @@ -94,7 +94,7 @@ class ResumeGenerator | ||||
|         return [ | ||||
|             "index#{$index}" => $index, | ||||
|             "start_date#{$index}" => Carbon::create($project["startDate"])->toDisplayString(), | ||||
|             "end_date#{$index}" => Carbon::create($project["endDate"])->toDisplayString(), | ||||
|             "end_date#{$index}" => $project["current"] ? "present" : Carbon::create($project["endDate"])->format("m.Y"), | ||||
|             "description#{$index}" => $project["description"], | ||||
|             "tasks#{$index}" => $this->withNewLines($project["tasks"]), | ||||
|         ]; | ||||
| @@ -134,7 +134,7 @@ class ResumeGenerator | ||||
|     { | ||||
|         return $resume->education->map(fn(array $project, int $index): array => [ | ||||
|             "start_date" => Carbon::create($project["startDate"])->toDisplayString(), | ||||
|             "end_date" => Carbon::create($project["endDate"])->toDisplayString(), | ||||
|             "end_date" => $project["current"] ? "present" : Carbon::create($project["endDate"])->format("m.Y"), | ||||
|             "school" => $project["school"], | ||||
|             "field_of_study" => $project["fieldOfStudy"], | ||||
|             "degree" => $project["degree"], | ||||
|   | ||||
| @@ -16,6 +16,8 @@ class Technology extends Model | ||||
| { | ||||
|     use HasFactory; | ||||
|  | ||||
|     protected $guarded = []; | ||||
|  | ||||
|     protected static function newFactory(): TechnologyFactory | ||||
|     { | ||||
|         return TechnologyFactory::new(); | ||||
|   | ||||
							
								
								
									
										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, | ||||
|             ])); | ||||
|     } | ||||
| } | ||||
| @@ -20,19 +20,21 @@ class ResumeRequest extends FormRequest | ||||
|             "education.*.school" => ["required"], | ||||
|             "education.*.degree" => ["required"], | ||||
|             "education.*.fieldOfStudy" => ["required"], | ||||
|             "education.*.startDate" => ["required", "date_format:Y-m-d"], | ||||
|             "education.*.endDate" => ["required", "date_format:Y-m-d", "after:education.*.startDate"], | ||||
|             "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", "exists:technologies,name", "distinct"], | ||||
|             "technologies.*.name" => ["required", "distinct"], | ||||
|             "technologies.*.level" => ["required", Rule::in(1, 2, 3, 4, 5)], | ||||
|  | ||||
|             "projects.*.description" => ["required"], | ||||
|             "projects.*.technologies" => ["array", "min:1", "exists:technologies,name", "distinct"], | ||||
|             "projects.*.startDate" => ["required", "date_format:Y-m-d"], | ||||
|             "projects.*.endDate" => ["required", "date_format:Y-m-d", "after:projects.*.startDate"], | ||||
|             "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"], | ||||
|         ]; | ||||
|     } | ||||
|   | ||||
							
								
								
									
										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"), | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										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, | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
| @@ -5,6 +5,7 @@ declare(strict_types=1); | ||||
| namespace Database\Factories; | ||||
|  | ||||
| use Illuminate\Database\Eloquent\Factories\Factory; | ||||
| use Illuminate\Support\Carbon; | ||||
| use Illuminate\Support\Collection; | ||||
| use Toby\Eloquent\Models\Resume; | ||||
| use Toby\Eloquent\Models\Technology; | ||||
| @@ -33,8 +34,9 @@ class ResumeFactory extends Factory | ||||
|                 "school" => $this->faker->sentence, | ||||
|                 "degree" => $this->faker->sentence, | ||||
|                 "fieldOfStudy" => $this->faker->sentence, | ||||
|                 "startDate" => $this->faker->date, | ||||
|                 "endDate" => $this->faker->date, | ||||
|                 "current" => false, | ||||
|                 "startDate" => Carbon::create($this->faker->date)->format("m/Y"), | ||||
|                 "endDate" => Carbon::create($this->faker->date)->format("m/Y"), | ||||
|             ]; | ||||
|         } | ||||
|  | ||||
| @@ -78,8 +80,9 @@ class ResumeFactory extends Factory | ||||
|             $items[] = [ | ||||
|                 "description" => $this->faker->text, | ||||
|                 "technologies" => $technologies->random($number)->all(), | ||||
|                 "startDate" => $this->faker->date, | ||||
|                 "endDate" => $this->faker->date, | ||||
|                 "current" => false, | ||||
|                 "startDate" => Carbon::create($this->faker->date)->format("m/Y"), | ||||
|                 "endDate" => Carbon::create($this->faker->date)->format("m/Y"), | ||||
|                 "tasks" => $this->faker->text, | ||||
|             ]; | ||||
|         } | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| @import 'flatpickr/dist/themes/light.css'; | ||||
| @import 'flatpickr/dist/plugins/monthSelect/style.css'; | ||||
| @import 'vue-toastification/dist/index.css'; | ||||
|  | ||||
| @tailwind base; | ||||
|   | ||||
| @@ -207,7 +207,7 @@ | ||||
|                 Data rozpoczęcia | ||||
|               </label> | ||||
|               <div class="mt-1 sm:mt-0"> | ||||
|                 <FlatPickr | ||||
|                 <MonthPicker | ||||
|                   v-model="element.startDate" | ||||
|                   placeholder="Wybierz datę" | ||||
|                   class="block w-full rounded-md shadow-sm sm:text-sm" | ||||
| @@ -226,12 +226,23 @@ | ||||
|                 Data zakończenia | ||||
|               </label> | ||||
|               <div class="mt-1 sm:mt-0"> | ||||
|                 <FlatPickr | ||||
|                 <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ę" | ||||
|                   class="block w-full rounded-md shadow-sm sm:text-sm" | ||||
|                     :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" | ||||
| @@ -436,12 +447,12 @@ | ||||
|                 Data rozpoczęcia | ||||
|               </label> | ||||
|               <div class="mt-1 sm:mt-0"> | ||||
|                 <FlatPickr | ||||
|                 <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[`educations.${index}.startDate`], 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors[`educations.${index}.startDate`] }" | ||||
|                   :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`]" | ||||
| @@ -459,13 +470,24 @@ | ||||
|                 Data zakończenia | ||||
|               </label> | ||||
|               <div class="mt-1 sm:mt-0"> | ||||
|                 <FlatPickr | ||||
|                 <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ę" | ||||
|                   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[`educations.${index}.endDate`], 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors[`educations.${index}.endDate`] }" | ||||
|                     :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" | ||||
| @@ -525,7 +547,7 @@ import { Listbox, ListboxOption, ListboxOptions, ListboxLabel, ListboxButton } f | ||||
| import { SelectorIcon, CheckIcon } from '@heroicons/vue/outline' | ||||
| import { ExclamationCircleIcon } from '@heroicons/vue/solid' | ||||
| import { useForm } from '@inertiajs/inertia-vue3' | ||||
| import FlatPickr from 'vue-flatpickr-component' | ||||
| import MonthPicker from '@/Shared/Forms/MonthPicker' | ||||
| import DynamicSection from '@/Shared/Forms/DynamicSection' | ||||
| import Combobox from '@/Shared/Forms/Combobox' | ||||
| import MultipleCombobox from '@/Shared/Forms/MultipleCombobox' | ||||
|   | ||||
| @@ -207,7 +207,7 @@ | ||||
|                 Data rozpoczęcia | ||||
|               </label> | ||||
|               <div class="mt-1 sm:mt-0"> | ||||
|                 <FlatPickr | ||||
|                 <MonthPicker | ||||
|                   v-model="element.startDate" | ||||
|                   placeholder="Wybierz datę" | ||||
|                   class="block w-full rounded-md shadow-sm sm:text-sm" | ||||
| @@ -226,12 +226,23 @@ | ||||
|                 Data zakończenia | ||||
|               </label> | ||||
|               <div class="mt-1 sm:mt-0"> | ||||
|                 <FlatPickr | ||||
|                 <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ę" | ||||
|                   class="block w-full rounded-md shadow-sm sm:text-sm" | ||||
|                     :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" | ||||
| @@ -436,12 +447,12 @@ | ||||
|                 Data rozpoczęcia | ||||
|               </label> | ||||
|               <div class="mt-1 sm:mt-0"> | ||||
|                 <FlatPickr | ||||
|                 <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[`educations.${index}.startDate`], 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors[`educations.${index}.startDate`] }" | ||||
|                   :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`]" | ||||
| @@ -459,13 +470,24 @@ | ||||
|                 Data zakończenia | ||||
|               </label> | ||||
|               <div class="mt-1 sm:mt-0"> | ||||
|                 <FlatPickr | ||||
|                 <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ę" | ||||
|                   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[`educations.${index}.endDate`], 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors[`educations.${index}.endDate`] }" | ||||
|                     :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" | ||||
| @@ -525,7 +547,7 @@ import { Listbox, ListboxOption, ListboxOptions, ListboxLabel, ListboxButton } f | ||||
| import { SelectorIcon, CheckIcon } from '@heroicons/vue/outline' | ||||
| import { ExclamationCircleIcon } from '@heroicons/vue/solid' | ||||
| import { useForm } from '@inertiajs/inertia-vue3' | ||||
| import FlatPickr from 'vue-flatpickr-component' | ||||
| import MonthPicker from '@/Shared/Forms/MonthPicker' | ||||
| import DynamicSection from '@/Shared/Forms/DynamicSection' | ||||
| import Combobox from '@/Shared/Forms/Combobox' | ||||
| import MultipleCombobox from '@/Shared/Forms/MultipleCombobox' | ||||
| @@ -552,11 +574,11 @@ const form = useForm(`EditResume:${props.resume.id}`,{ | ||||
|   educations: props.resume.education ?? [], | ||||
|   projects: props.resume.projects ?? [], | ||||
|   technologies: props.resume.technologies.map((technology) => ({ | ||||
|     name: props.technologies.find((tech) => tech === technology.name), | ||||
|     name: technology.name, | ||||
|     level: technologyLevels.find((level) => level.level === technology.level), | ||||
|   })) ?? [], | ||||
|   languages: props.resume.languages.map((language) => ({ | ||||
|     name: languages.find((lang) => lang === language.name), | ||||
|     name: language.name, | ||||
|     level: languageLevels.find((level) => level.level === language.level), | ||||
|   })) ?? [], | ||||
| }) | ||||
| @@ -606,7 +628,10 @@ function submitResume() { | ||||
|     .transform((data) => ({ | ||||
|       user: data.user?.id, | ||||
|       name: data.name, | ||||
|       education: data.educations, | ||||
|       education: data.educations.map(education => ({ | ||||
|         ...education, | ||||
|         endDate: education.current ? null: education.endDate, | ||||
|       })), | ||||
|       languages: data.languages.map(language => ({ | ||||
|         name: language.name, | ||||
|         level: language.level.level, | ||||
| @@ -615,7 +640,10 @@ function submitResume() { | ||||
|         name: technology.name, | ||||
|         level: technology.level.level, | ||||
|       })), | ||||
|       projects: data.projects, | ||||
|       projects: data.projects.map(project => ({ | ||||
|         ...project, | ||||
|         endDate: project.current ? null : project.endDate, | ||||
|       })), | ||||
|     })) | ||||
|     .put(`/resumes/${props.resume.id}`) | ||||
| } | ||||
|   | ||||
							
								
								
									
										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> | ||||
							
								
								
									
										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> | ||||
|  | ||||
| @@ -6,15 +6,31 @@ | ||||
|     multiple | ||||
|   > | ||||
|     <div class="flex flex-wrap gap-3"> | ||||
|       <button | ||||
|       <span | ||||
|         v-for="(item, index) in selectedItems" | ||||
|         :key="index" | ||||
|         type="button" | ||||
|         class="py-1 px-2 bg-gray-200 rounded-md" | ||||
|         @click="selectedItems.splice(index, 1)" | ||||
|         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 | ||||
|   | ||||
| @@ -59,7 +59,7 @@ | ||||
|               > | ||||
|             </InertiaLink> | ||||
|           </div> | ||||
|           <nav class="overflow-y-auto shrink-0 mt-5 h-full divide-y divide-blumilk-800"> | ||||
|           <nav class="overflow-y-auto shrink-0 mt-5 h-full space-y-5"> | ||||
|             <div class="px-2 space-y-1"> | ||||
|               <InertiaLink | ||||
|                 href="/" | ||||
| @@ -70,10 +70,12 @@ | ||||
|                 Strona główna | ||||
|               </InertiaLink> | ||||
|             </div> | ||||
|             <div class="pt-3 mt-3"> | ||||
|               <div class="py-1 px-2 space-y-1"> | ||||
|             <div | ||||
|               v-if="vacationNavigation.length" | ||||
|               class="py-1 px-2 space-y-1" | ||||
|             > | ||||
|               <InertiaLink | ||||
|                   v-for="item in navigation" | ||||
|                 v-for="item in vacationNavigation" | ||||
|                 :key="item.name" | ||||
|                 :href="item.href" | ||||
|                 :class="[$page.component.startsWith(item.section) ? 'bg-blumilk-800 text-white' : 'text-blumilk-100 hover:text-white hover:bg-blumilk-600', 'group flex items-center px-2 py-2 text-base font-medium rounded-md']" | ||||
| @@ -92,6 +94,29 @@ | ||||
|                 </span> | ||||
|               </InertiaLink> | ||||
|             </div> | ||||
|             <div | ||||
|               v-if="miscNavigaction.length" | ||||
|               class="py-1 px-2 space-y-1" | ||||
|             > | ||||
|               <InertiaLink | ||||
|                 v-for="item in miscNavigaction" | ||||
|                 :key="item.name" | ||||
|                 :href="item.href" | ||||
|                 :class="[$page.component.startsWith(item.section) ? 'bg-blumilk-800 text-white' : 'text-blumilk-100 hover:text-white hover:bg-blumilk-600', 'group flex items-center px-2 py-2 text-base font-medium rounded-md']" | ||||
|                 @click="sidebarOpen = false;" | ||||
|               > | ||||
|                 <component | ||||
|                   :is="item.icon" | ||||
|                   class="shrink-0 mr-4 w-6 h-6 text-blumilk-200" | ||||
|                 /> | ||||
|                 {{ item.name }} | ||||
|                 <span | ||||
|                   v-if="item.badge" | ||||
|                   class="py-0.5 px-2.5 ml-3 text-xs font-semibold text-gray-600 bg-gray-100 rounded-full 2xl:inline-block" | ||||
|                 > | ||||
|                   {{ item.badge }} | ||||
|                 </span> | ||||
|               </InertiaLink> | ||||
|             </div> | ||||
|           </nav> | ||||
|         </div> | ||||
| @@ -110,7 +135,7 @@ | ||||
|           > | ||||
|         </InertiaLink> | ||||
|       </div> | ||||
|       <nav class="flex overflow-y-auto flex-col flex-1 px-2 mt-5 divide-y divide-blumilk-800"> | ||||
|       <nav class="flex overflow-y-auto flex-col flex-1 px-2 mt-5 space-y-4"> | ||||
|         <InertiaLink | ||||
|           href="/" | ||||
|           :class="[$page.component === 'Dashboard' ? 'bg-blumilk-800 text-white' : 'text-blumilk-100 hover:text-white hover:bg-blumilk-600', 'group flex items-center px-2 py-2 mt-1 text-sm leading-6 font-medium rounded-md']" | ||||
| @@ -118,9 +143,35 @@ | ||||
|           <HomeIcon class="shrink-0 mr-4 w-6 h-6 text-blumilk-200" /> | ||||
|           Strona główna | ||||
|         </InertiaLink> | ||||
|         <div class="pt-1 mt-1 space-y-1"> | ||||
|         <div | ||||
|           v-if="vacationNavigation.length" | ||||
|           class="pt-1 mt-1 space-y-1" | ||||
|         > | ||||
|           <InertiaLink | ||||
|             v-for="item in navigation" | ||||
|             v-for="item in vacationNavigation" | ||||
|             :key="item.name" | ||||
|             :href="item.href" | ||||
|             :class="[$page.component.startsWith(item.section) ? 'bg-blumilk-800 text-white' : 'text-blumilk-100 hover:text-white hover:bg-blumilk-600', 'group flex items-center px-2 py-2 text-sm leading-6 font-medium rounded-md']" | ||||
|           > | ||||
|             <component | ||||
|               :is="item.icon" | ||||
|               class="shrink-0 mr-4 w-6 h-6 text-blumilk-200" | ||||
|             /> | ||||
|             {{ item.name }} | ||||
|             <span | ||||
|               v-if="item.badge" | ||||
|               class="py-0.5 px-2.5 ml-3 text-xs font-semibold text-gray-600 bg-gray-100 rounded-full 2xl:inline-block" | ||||
|             > | ||||
|               {{ item.badge }} | ||||
|             </span> | ||||
|           </InertiaLink> | ||||
|         </div> | ||||
|         <div | ||||
|           v-if="miscNavigaction.length" | ||||
|           class="pt-1 mt-1 space-y-1" | ||||
|         > | ||||
|           <InertiaLink | ||||
|             v-for="item in miscNavigaction" | ||||
|             :key="item.name" | ||||
|             :href="item.href" | ||||
|             :class="[$page.component.startsWith(item.section) ? 'bg-blumilk-800 text-white' : 'text-blumilk-100 hover:text-white hover:bg-blumilk-600', 'group flex items-center px-2 py-2 text-sm leading-6 font-medium rounded-md']" | ||||
| @@ -295,7 +346,7 @@ import { | ||||
|   DocumentTextIcon, | ||||
|   AdjustmentsIcon, | ||||
|   KeyIcon, | ||||
|   TemplateIcon, | ||||
|   TemplateIcon, BeakerIcon, | ||||
| } from '@heroicons/vue/outline' | ||||
| import { CheckIcon, ChevronDownIcon } from '@heroicons/vue/solid' | ||||
|  | ||||
| @@ -307,7 +358,7 @@ const props = defineProps({ | ||||
|  | ||||
| const sidebarOpen = ref(false) | ||||
|  | ||||
| const navigation = computed(() => | ||||
| const vacationNavigation = computed(() => | ||||
|   [ | ||||
|     { | ||||
|       name: 'Moje wnioski', | ||||
| @@ -359,6 +410,9 @@ const navigation = computed(() => | ||||
|       icon: ClipboardListIcon, | ||||
|       can: true, | ||||
|     }, | ||||
|   ].filter(item => item.can)) | ||||
|  | ||||
| const miscNavigaction = computed(() => [ | ||||
|   { | ||||
|     name: 'Użytkownicy', | ||||
|     href: '/users', | ||||
| @@ -373,6 +427,13 @@ const navigation = computed(() => | ||||
|     icon: KeyIcon, | ||||
|     can: true, | ||||
|   }, | ||||
|   { | ||||
|     name: 'Technologie', | ||||
|     href: '/technologies', | ||||
|     section: 'Technologies', | ||||
|     icon: BeakerIcon, | ||||
|     can: props.auth.can.manageResumes, | ||||
|   }, | ||||
|   { | ||||
|     name: 'CV', | ||||
|     href: '/resumes', | ||||
| @@ -380,6 +441,5 @@ const navigation = computed(() => | ||||
|     icon: TemplateIcon, | ||||
|     can: props.auth.can.manageResumes, | ||||
|   }, | ||||
|  | ||||
| ].filter(item => item.can)) | ||||
| </script> | ||||
|   | ||||
| @@ -76,5 +76,7 @@ | ||||
|   ":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." | ||||
|   "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." | ||||
| } | ||||
|   | ||||
| @@ -12,6 +12,7 @@ use Toby\Infrastructure\Http\Controllers\LogoutController; | ||||
| use Toby\Infrastructure\Http\Controllers\MonthlyUsageController; | ||||
| use Toby\Infrastructure\Http\Controllers\ResumeController; | ||||
| use Toby\Infrastructure\Http\Controllers\SelectYearPeriodController; | ||||
| use Toby\Infrastructure\Http\Controllers\TechnologyController; | ||||
| use Toby\Infrastructure\Http\Controllers\TimesheetController; | ||||
| use Toby\Infrastructure\Http\Controllers\UserController; | ||||
| use Toby\Infrastructure\Http\Controllers\VacationCalendarController; | ||||
| @@ -37,6 +38,9 @@ Route::middleware(["auth", TrackUserLastActivity::class])->group(function (): vo | ||||
|  | ||||
|     Route::resource("resumes", ResumeController::class) | ||||
|         ->whereNumber("resume"); | ||||
|     Route::resource("technologies", TechnologyController::class) | ||||
|         ->only(["index", "store", "destroy"]) | ||||
|         ->whereNumber("technology"); | ||||
|  | ||||
|     Route::get("/keys", [KeysController::class, "index"]); | ||||
|     Route::post("/keys", [KeysController::class, "store"]); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user