* wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * lint fixes * missing empty lines * translations * fix vue version * #134 - fixes * fix * fix * #134 - fix * fix * fix * #134 - added tests * #134 - fix to translations * #134 - tests * #134 - fix * Update database/factories/ResumeFactory.php Co-authored-by: Krzysztof Rewak <krzysztof.rewak@gmail.com> * #134 - fix * #134 - fix Co-authored-by: EwelinaLasowy <ewelina.lasowy@blumilk.pl> Co-authored-by: Krzysztof Rewak <krzysztof.rewak@gmail.com>
This commit is contained in:
@@ -35,5 +35,6 @@ class AuthServiceProvider extends ServiceProvider
|
||||
Gate::define("manageVacationLimits", fn(User $user): bool => $user->role === Role::AdministrativeApprover);
|
||||
Gate::define("generateTimesheet", fn(User $user): bool => $user->role === Role::AdministrativeApprover);
|
||||
Gate::define("listMonthlyUsage", fn(User $user): bool => $user->role === Role::AdministrativeApprover);
|
||||
Gate::define("manageResumes", fn(User $user): bool => $user->role === Role::TechnicalApprover);
|
||||
}
|
||||
}
|
||||
|
113
app/Domain/ResumeGenerator.php
Normal file
113
app/Domain/ResumeGenerator.php
Normal file
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain;
|
||||
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
use PhpOffice\PhpWord\TemplateProcessor;
|
||||
use Toby\Eloquent\Models\Resume;
|
||||
|
||||
class ResumeGenerator
|
||||
{
|
||||
public function generate(Resume $resume): string
|
||||
{
|
||||
$processor = new TemplateProcessor($this->getTemplate());
|
||||
|
||||
$processor->setValue("id", $resume->id);
|
||||
$processor->setValue("name", $resume->user ? $resume->user->profile->full_name : $resume->name);
|
||||
|
||||
$this->fillTechnologies($processor, $resume);
|
||||
$this->fillLanguages($processor, $resume);
|
||||
$this->fillEducation($processor, $resume);
|
||||
$this->fillProjects($processor, $resume);
|
||||
|
||||
return $processor->save();
|
||||
}
|
||||
|
||||
public function getTemplate(): string
|
||||
{
|
||||
return resource_path("views/docx/resume_eng.docx");
|
||||
}
|
||||
|
||||
protected function fillTechnologies(TemplateProcessor $processor, Resume $resume): void
|
||||
{
|
||||
$processor->cloneBlock("technologies", 0, true, false, $this->getTechnologies($resume));
|
||||
}
|
||||
|
||||
protected function fillLanguages(TemplateProcessor $processor, Resume $resume): void
|
||||
{
|
||||
$processor->cloneBlock("languages", 0, true, false, $this->getLanguages($resume));
|
||||
}
|
||||
|
||||
protected function fillEducation(TemplateProcessor $processor, Resume $resume): void
|
||||
{
|
||||
$processor->cloneBlock("education", 0, true, false, $this->getEducation($resume));
|
||||
}
|
||||
|
||||
protected function fillProjects(TemplateProcessor $processor, Resume $resume): void
|
||||
{
|
||||
$processor->cloneBlock("projects", $resume->projects->count(), true, true);
|
||||
|
||||
foreach ($resume->projects as $index => $project) {
|
||||
++$index;
|
||||
$processor->setValues($this->getProject($project, $index));
|
||||
|
||||
$processor->cloneBlock("project_technologies#{$index}", 0, true, false, $this->getProjectTechnologies($project, $index));
|
||||
}
|
||||
}
|
||||
|
||||
protected function getProject(array $project, int $index): array
|
||||
{
|
||||
return [
|
||||
"index#{$index}" => $index,
|
||||
"start_date#{$index}" => Carbon::createFromFormat("m/Y", $project["startDate"])->format("n.Y"),
|
||||
"end_date#{$index}" => $project["current"] ? "present" : Carbon::createFromFormat("m/Y", $project["endDate"])->format("n.Y"),
|
||||
"description#{$index}" => $project["description"],
|
||||
"tasks#{$index}" => $this->withNewLines($project["tasks"]),
|
||||
];
|
||||
}
|
||||
|
||||
protected function withNewLines(string $text): string
|
||||
{
|
||||
return Str::replace("\n", "</w:t><w:br/><w:t>", $text);
|
||||
}
|
||||
|
||||
protected function getProjectTechnologies(array $project, int $index): array
|
||||
{
|
||||
$technologies = new Collection($project["technologies"] ?? []);
|
||||
|
||||
return $technologies->map(fn(string $name) => [
|
||||
"technology#{$index}" => $name,
|
||||
])->all();
|
||||
}
|
||||
|
||||
protected function getTechnologies(Resume $resume): array
|
||||
{
|
||||
return $resume->technologies->map(fn(array $technology): array => [
|
||||
"technology_name" => $technology["name"],
|
||||
"technology_level" => __("resume.technology_levels.{$technology["level"]}"),
|
||||
])->all();
|
||||
}
|
||||
|
||||
protected function getLanguages(Resume $resume): array
|
||||
{
|
||||
return $resume->languages->map(fn(array $language): array => [
|
||||
"language_name" => $language["name"],
|
||||
"language_level" => __("resume.language_levels.{$language["level"]}"),
|
||||
])->all();
|
||||
}
|
||||
|
||||
protected function getEducation(Resume $resume): array
|
||||
{
|
||||
return $resume->education->map(fn(array $project, int $index): array => [
|
||||
"start_date" => Carbon::createFromFormat("m/Y", $project["startDate"])->format("n.Y"),
|
||||
"end_date" => $project["current"] ? "present" : Carbon::createFromFormat("m/Y", $project["endDate"])->format("n.Y"),
|
||||
"school" => $project["school"],
|
||||
"field_of_study" => $project["fieldOfStudy"],
|
||||
"degree" => $project["degree"],
|
||||
])->all();
|
||||
}
|
||||
}
|
44
app/Eloquent/Models/Resume.php
Normal file
44
app/Eloquent/Models/Resume.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Eloquent\Models;
|
||||
|
||||
use Database\Factories\ResumeFactory;
|
||||
use Illuminate\Database\Eloquent\Casts\AsCollection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property ?User $user
|
||||
* @property string $name
|
||||
* @property Collection $education
|
||||
* @property Collection $languages
|
||||
* @property Collection $technologies
|
||||
* @property Collection $projects
|
||||
*/
|
||||
class Resume extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $guarded = [];
|
||||
protected $casts = [
|
||||
"education" => AsCollection::class,
|
||||
"languages" => AsCollection::class,
|
||||
"technologies" => AsCollection::class,
|
||||
"projects" => AsCollection::class,
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
protected static function newFactory(): ResumeFactory
|
||||
{
|
||||
return ResumeFactory::new();
|
||||
}
|
||||
}
|
25
app/Eloquent/Models/Technology.php
Normal file
25
app/Eloquent/Models/Technology.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Eloquent\Models;
|
||||
|
||||
use Database\Factories\TechnologyFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property string $name
|
||||
*/
|
||||
class Technology extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
protected static function newFactory(): TechnologyFactory
|
||||
{
|
||||
return TechnologyFactory::new();
|
||||
}
|
||||
}
|
137
app/Infrastructure/Http/Controllers/ResumeController.php
Normal file
137
app/Infrastructure/Http/Controllers/ResumeController.php
Normal file
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Infrastructure\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Inertia\Response;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse as BinaryFileResponseAlias;
|
||||
use Toby\Domain\ResumeGenerator;
|
||||
use Toby\Eloquent\Models\Resume;
|
||||
use Toby\Eloquent\Models\Technology;
|
||||
use Toby\Eloquent\Models\User;
|
||||
use Toby\Infrastructure\Http\Requests\ResumeRequest;
|
||||
use Toby\Infrastructure\Http\Resources\ResumeFormResource;
|
||||
use Toby\Infrastructure\Http\Resources\ResumeResource;
|
||||
use Toby\Infrastructure\Http\Resources\SimpleUserResource;
|
||||
|
||||
class ResumeController extends Controller
|
||||
{
|
||||
public function index(): Response
|
||||
{
|
||||
$this->authorize("manageResumes");
|
||||
|
||||
$resumes = Resume::query()
|
||||
->latest("updated_at")
|
||||
->paginate();
|
||||
|
||||
return inertia("Resumes/Index", [
|
||||
"resumes" => ResumeResource::collection($resumes),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(): Response
|
||||
{
|
||||
$this->authorize("manageResumes");
|
||||
|
||||
$users = User::query()
|
||||
->orderByProfileField("last_name")
|
||||
->orderByProfileField("first_name")
|
||||
->get();
|
||||
|
||||
return inertia("Resumes/Create", [
|
||||
"users" => SimpleUserResource::collection($users),
|
||||
"technologies" => Technology::all()->pluck("name"),
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(Resume $resume, ResumeGenerator $generator): BinaryFileResponseAlias
|
||||
{
|
||||
$this->authorize("manageResumes");
|
||||
|
||||
$path = $generator->generate($resume);
|
||||
|
||||
return response()
|
||||
->download($path, "resume-{$resume->id}.docx")
|
||||
->deleteFileAfterSend();
|
||||
}
|
||||
|
||||
public function store(ResumeRequest $request): RedirectResponse
|
||||
{
|
||||
$this->authorize("manageResumes");
|
||||
|
||||
$resume = new Resume();
|
||||
|
||||
if ($request->hasEmployee()) {
|
||||
$resume->user()->associate($request->getEmployee());
|
||||
} else {
|
||||
$resume->name = $request->getName();
|
||||
}
|
||||
|
||||
$resume->fill([
|
||||
"education" => $request->getEducation(),
|
||||
"languages" => $request->getLanguageLevels(),
|
||||
"technologies" => $request->getTechnologyLevels(),
|
||||
"projects" => $request->getProjects(),
|
||||
]);
|
||||
|
||||
$resume->save();
|
||||
|
||||
return redirect()
|
||||
->route("resumes.index")
|
||||
->with("success", __("Resume has been created."));
|
||||
}
|
||||
|
||||
public function edit(Resume $resume): Response
|
||||
{
|
||||
$this->authorize("manageResumes");
|
||||
|
||||
$users = User::query()
|
||||
->orderByProfileField("last_name")
|
||||
->orderByProfileField("first_name")
|
||||
->get();
|
||||
|
||||
return inertia("Resumes/Edit", [
|
||||
"resume" => new ResumeFormResource($resume),
|
||||
"users" => SimpleUserResource::collection($users),
|
||||
"technologies" => Technology::all()->pluck("name"),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Resume $resume, ResumeRequest $request): RedirectResponse
|
||||
{
|
||||
$this->authorize("manageResumes");
|
||||
|
||||
if ($request->hasEmployee()) {
|
||||
$resume->user()->associate($request->getEmployee());
|
||||
} else {
|
||||
$resume->user()->dissociate();
|
||||
$resume->name = $request->getName();
|
||||
}
|
||||
|
||||
$resume->fill([
|
||||
"education" => $request->getEducation(),
|
||||
"languages" => $request->getLanguageLevels(),
|
||||
"technologies" => $request->getTechnologyLevels(),
|
||||
"projects" => $request->getProjects(),
|
||||
]);
|
||||
|
||||
$resume->save();
|
||||
|
||||
return redirect()
|
||||
->route("resumes.index")
|
||||
->with("success", __("Resume has been updated."));
|
||||
}
|
||||
|
||||
public function destroy(Resume $resume): RedirectResponse
|
||||
{
|
||||
$this->authorize("manageResumes");
|
||||
|
||||
$resume->delete();
|
||||
|
||||
return redirect()
|
||||
->route("resumes.index")
|
||||
->with("success", __("Resume has been deleted."));
|
||||
}
|
||||
}
|
57
app/Infrastructure/Http/Controllers/TechnologyController.php
Normal file
57
app/Infrastructure/Http/Controllers/TechnologyController.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Infrastructure\Http\Controllers;
|
||||
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Inertia\Response;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Toby\Eloquent\Models\Technology;
|
||||
use Toby\Infrastructure\Http\Requests\TechnologyRequest;
|
||||
use Toby\Infrastructure\Http\Resources\TechnologyResource;
|
||||
|
||||
class TechnologyController extends Controller
|
||||
{
|
||||
public function index(): Response
|
||||
{
|
||||
$this->authorize("manageResumes");
|
||||
|
||||
$technologies = Technology::query()
|
||||
->orderBy("name")
|
||||
->get();
|
||||
|
||||
return inertia("Technologies", [
|
||||
"technologies" => TechnologyResource::collection($technologies),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function store(TechnologyRequest $request): RedirectResponse
|
||||
{
|
||||
$this->authorize("manageResumes");
|
||||
|
||||
$technology = Technology::query()->create($request->data());
|
||||
|
||||
return redirect()
|
||||
->back()
|
||||
->with("success", __("Technology :name has been created.", [
|
||||
"name" => $technology->name,
|
||||
]));
|
||||
}
|
||||
|
||||
public function destroy(Technology $technology): RedirectResponse
|
||||
{
|
||||
$this->authorize("manageResumes");
|
||||
|
||||
$technology->delete();
|
||||
|
||||
return redirect()
|
||||
->back()
|
||||
->with("success", __("Technology :name has been deleted.", [
|
||||
"name" => $technology->name,
|
||||
]));
|
||||
}
|
||||
}
|
@@ -39,6 +39,7 @@ class HandleInertiaRequests extends Middleware
|
||||
"manageUsers" => $user ? $user->can("manageUsers") : false,
|
||||
"listAllVacationRequests" => $user ? $user->can("listAll", VacationRequest::class) : false,
|
||||
"listMonthlyUsage" => $user ? $user->can("listMonthlyUsage") : false,
|
||||
"manageResumes" => $user ? $user->can("manageResumes") : false,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
79
app/Infrastructure/Http/Requests/ResumeRequest.php
Normal file
79
app/Infrastructure/Http/Requests/ResumeRequest.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Infrastructure\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Toby\Eloquent\Models\User;
|
||||
|
||||
class ResumeRequest extends FormRequest
|
||||
{
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
"user" => ["nullable", "exists:users,id"],
|
||||
"name" => ["required_without:user"],
|
||||
|
||||
"education.*.school" => ["required"],
|
||||
"education.*.degree" => ["required"],
|
||||
"education.*.fieldOfStudy" => ["required"],
|
||||
"education.*.startDate" => ["required", "date_format:m/Y"],
|
||||
"education.*.current" => ["required", "boolean"],
|
||||
"education.*.endDate" => ["required_if:education.*.current,false", "nullable", "date_format:m/Y", "after:education.*.startDate"],
|
||||
|
||||
"languages.*.name" => ["required", "distinct"],
|
||||
"languages.*.level" => ["required", Rule::in(1, 2, 3, 4, 5, 6)],
|
||||
|
||||
"technologies.*.name" => ["required", "distinct"],
|
||||
"technologies.*.level" => ["required", Rule::in(1, 2, 3, 4, 5)],
|
||||
|
||||
"projects.*.description" => ["required"],
|
||||
"projects.*.technologies" => ["array", "min:1", "distinct"],
|
||||
"projects.*.startDate" => ["required", "date_format:m/Y"],
|
||||
"projects.*.current" => ["required", "boolean"],
|
||||
"projects.*.endDate" => ["required_if:projects.*.current,false", "nullable", "date_format:m/Y", "after:projects.*.startDate"],
|
||||
"projects.*.tasks" => ["required"],
|
||||
];
|
||||
}
|
||||
|
||||
public function hasEmployee(): bool
|
||||
{
|
||||
return $this->has("user");
|
||||
}
|
||||
|
||||
public function getEmployee(): User
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = User::query()->find($this->get("user"));
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->get("name");
|
||||
}
|
||||
|
||||
public function getLanguageLevels(): Collection
|
||||
{
|
||||
return $this->collect("languages");
|
||||
}
|
||||
|
||||
public function getTechnologyLevels(): Collection
|
||||
{
|
||||
return $this->collect("technologies");
|
||||
}
|
||||
|
||||
public function getEducation(): Collection
|
||||
{
|
||||
return $this->collect("education");
|
||||
}
|
||||
|
||||
public function getProjects(): Collection
|
||||
{
|
||||
return $this->collect("projects");
|
||||
}
|
||||
}
|
29
app/Infrastructure/Http/Requests/TechnologyRequest.php
Normal file
29
app/Infrastructure/Http/Requests/TechnologyRequest.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Infrastructure\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class TechnologyRequest extends FormRequest
|
||||
{
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
"name" => [
|
||||
"required",
|
||||
Rule::unique("technologies", "name")->ignore($this->technology),
|
||||
"max:255",
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function data(): array
|
||||
{
|
||||
return [
|
||||
"name" => $this->get("name"),
|
||||
];
|
||||
}
|
||||
}
|
26
app/Infrastructure/Http/Resources/ResumeFormResource.php
Normal file
26
app/Infrastructure/Http/Resources/ResumeFormResource.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Infrastructure\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class ResumeFormResource extends JsonResource
|
||||
{
|
||||
public static $wrap = null;
|
||||
|
||||
public function toArray($request): array
|
||||
{
|
||||
return [
|
||||
"id" => $this->id,
|
||||
"user" => $this->user_id,
|
||||
"name" => $this->name,
|
||||
"description" => $this->description,
|
||||
"education" => $this->education,
|
||||
"languages" => $this->languages,
|
||||
"technologies" => $this->technologies,
|
||||
"projects" => $this->projects,
|
||||
];
|
||||
}
|
||||
}
|
28
app/Infrastructure/Http/Resources/ResumeResource.php
Normal file
28
app/Infrastructure/Http/Resources/ResumeResource.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Infrastructure\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class ResumeResource extends JsonResource
|
||||
{
|
||||
public static $wrap = null;
|
||||
|
||||
public function toArray($request): array
|
||||
{
|
||||
return [
|
||||
"id" => $this->id,
|
||||
"user" => new SimpleUserResource($this->user),
|
||||
"name" => $this->name,
|
||||
"description" => $this->description,
|
||||
"educationCount" => $this->education->count(),
|
||||
"languageCount" => $this->languages->count(),
|
||||
"technologyCount" => $this->technologies->count(),
|
||||
"projectCount" => $this->projects->count(),
|
||||
"createdAt" => $this->created_at->toDisplayString(),
|
||||
"updatedAt" => $this->updated_at->toDisplayString(),
|
||||
];
|
||||
}
|
||||
}
|
20
app/Infrastructure/Http/Resources/TechnologyResource.php
Normal file
20
app/Infrastructure/Http/Resources/TechnologyResource.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Infrastructure\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class TechnologyResource extends JsonResource
|
||||
{
|
||||
public static $wrap = null;
|
||||
|
||||
public function toArray($request): array
|
||||
{
|
||||
return [
|
||||
"id" => $this->id,
|
||||
"name" => $this->name,
|
||||
];
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user