diff --git a/app/Architecture/ExceptionHandler.php b/app/Architecture/ExceptionHandler.php index c63890d..b286bd1 100644 --- a/app/Architecture/ExceptionHandler.php +++ b/app/Architecture/ExceptionHandler.php @@ -17,11 +17,29 @@ class ExceptionHandler extends Handler "password_confirmation", ]; + protected array $handleByInertia = [ + Response::HTTP_INTERNAL_SERVER_ERROR, + Response::HTTP_SERVICE_UNAVAILABLE, + Response::HTTP_TOO_MANY_REQUESTS, + 419, // CSRF + Response::HTTP_NOT_FOUND, + Response::HTTP_FORBIDDEN, + Response::HTTP_UNAUTHORIZED, + ]; + public function render($request, Throwable $e): Response { $response = parent::render($request, $e); - if (app()->environment("production") && in_array($response->status(), [500, 503, 429, 419, 404, 403, 401], true)) { + if (!app()->environment("production")) { + return $response; + } + + if ($response->status() === Response::HTTP_METHOD_NOT_ALLOWED) { + $response->setStatusCode(Response::HTTP_NOT_FOUND); + } + + if (in_array($response->status(), $this->handleByInertia, true)) { return Inertia::render("Error", [ "status" => $response->status(), ]) diff --git a/app/Domain/Actions/CreateUserAction.php b/app/Domain/Actions/CreateUserAction.php index 2107a1c..8894a77 100644 --- a/app/Domain/Actions/CreateUserAction.php +++ b/app/Domain/Actions/CreateUserAction.php @@ -9,12 +9,14 @@ use Toby\Eloquent\Models\YearPeriod; class CreateUserAction { - public function execute(array $data): User + public function execute(array $userData, array $profileData): User { - $user = new User($data); + $user = new User($userData); $user->save(); + $user->profile()->create($profileData); + $this->createVacationLimitsFor($user); return $user; diff --git a/app/Domain/Actions/UpdateUserAction.php b/app/Domain/Actions/UpdateUserAction.php new file mode 100644 index 0000000..b640788 --- /dev/null +++ b/app/Domain/Actions/UpdateUserAction.php @@ -0,0 +1,19 @@ +update($userData); + + $user->profile->update($profileData); + + return $user; + } +} diff --git a/app/Domain/Notifications/VacationRequestCreatedNotification.php b/app/Domain/Notifications/VacationRequestCreatedNotification.php index 6166be6..d84d108 100644 --- a/app/Domain/Notifications/VacationRequestCreatedNotification.php +++ b/app/Domain/Notifications/VacationRequestCreatedNotification.php @@ -39,7 +39,7 @@ class VacationRequestCreatedNotification extends Notification protected function buildMailMessage(string $url): MailMessage { - $user = $this->vacationRequest->user->first_name; + $user = $this->vacationRequest->user->profile->first_name; $type = $this->vacationRequest->type->label(); $from = $this->vacationRequest->from->toDisplayString(); $to = $this->vacationRequest->to->toDisplayString(); @@ -92,7 +92,7 @@ class VacationRequestCreatedNotification extends Notification return __("The vacation request :title has been created correctly by user :creator on your behalf in the :appName.", [ "title" => $this->vacationRequest->name, "appName" => $appName, - "creator" => $this->vacationRequest->creator->fullName, + "creator" => $this->vacationRequest->creator->profile->full_name, ]); } } diff --git a/app/Domain/Notifications/VacationRequestStatusChangedNotification.php b/app/Domain/Notifications/VacationRequestStatusChangedNotification.php index cfe0673..11594a1 100644 --- a/app/Domain/Notifications/VacationRequestStatusChangedNotification.php +++ b/app/Domain/Notifications/VacationRequestStatusChangedNotification.php @@ -42,14 +42,14 @@ class VacationRequestStatusChangedNotification extends Notification protected function buildMailMessage(string $url): MailMessage { - $user = $this->user->first_name; + $user = $this->user->profile->first_name; $title = $this->vacationRequest->name; $type = $this->vacationRequest->type->label(); $status = $this->vacationRequest->state->label(); $from = $this->vacationRequest->from->toDisplayString(); $to = $this->vacationRequest->to->toDisplayString(); $days = $this->vacationRequest->vacations()->count(); - $requester = $this->vacationRequest->user->fullName; + $requester = $this->vacationRequest->user->profile->full_name; return (new MailMessage()) ->greeting(__("Hi :user!", [ @@ -59,7 +59,7 @@ class VacationRequestStatusChangedNotification extends Notification "title" => $title, "status" => $status, ])) - ->line(__("The vacation request :title for user :requester has been :status.", [ + ->line(__("The vacation request :title from user :requester has been :status.", [ "title" => $title, "requester" => $requester, "status" => $status, diff --git a/app/Domain/Notifications/VacationRequestWaitsForApprovalNotification.php b/app/Domain/Notifications/VacationRequestWaitsForApprovalNotification.php index 3c88384..109eef9 100644 --- a/app/Domain/Notifications/VacationRequestWaitsForApprovalNotification.php +++ b/app/Domain/Notifications/VacationRequestWaitsForApprovalNotification.php @@ -43,7 +43,7 @@ class VacationRequestWaitsForApprovalNotification extends Notification protected function buildMailMessage(string $url): MailMessage { - $user = $this->user->first_name; + $user = $this->user->profile->first_name; $type = $this->vacationRequest->type->label(); $from = $this->vacationRequest->from->toDisplayString(); $to = $this->vacationRequest->to->toDisplayString(); @@ -84,7 +84,7 @@ class VacationRequestWaitsForApprovalNotification extends Notification protected function buildDescription(): string { $title = $this->vacationRequest->name; - $requester = $this->vacationRequest->user->fullName; + $requester = $this->vacationRequest->user->profile->full_name; if ($this->vacationRequest->state->equals(WaitingForTechnical::class)) { return __("The vacation request :title from user :requester is waiting for your technical approval.", [ diff --git a/app/Domain/TimesheetPerUserSheet.php b/app/Domain/TimesheetPerUserSheet.php index ce7634a..8547a5e 100644 --- a/app/Domain/TimesheetPerUserSheet.php +++ b/app/Domain/TimesheetPerUserSheet.php @@ -46,7 +46,7 @@ class TimesheetPerUserSheet implements WithTitle, WithHeadings, WithEvents, With public function title(): string { - return $this->user->fullName; + return $this->user->profile->full_name; } public function headings(): array diff --git a/app/Eloquent/Helpers/YearPeriodRetriever.php b/app/Eloquent/Helpers/YearPeriodRetriever.php index ad8b98f..cf512cb 100644 --- a/app/Eloquent/Helpers/YearPeriodRetriever.php +++ b/app/Eloquent/Helpers/YearPeriodRetriever.php @@ -33,7 +33,8 @@ class YearPeriodRetriever $selected = $this->selected(); $current = $this->current(); - $years = YearPeriod::query()->whereIn("year", $this->offset($selected->year))->get(); + $years = YearPeriod::all(); + $navigation = $years->map(fn(YearPeriod $yearPeriod) => $this->toNavigation($yearPeriod)); return [ @@ -43,11 +44,6 @@ class YearPeriodRetriever ]; } - protected function offset(int $year): array - { - return range($year - 2, $year + 2); - } - protected function toNavigation(YearPeriod $yearPeriod): array { return [ diff --git a/app/Eloquent/Models/Profile.php b/app/Eloquent/Models/Profile.php new file mode 100644 index 0000000..df237e9 --- /dev/null +++ b/app/Eloquent/Models/Profile.php @@ -0,0 +1,63 @@ + EmploymentForm::class, + "employment_date" => "date", + ]; + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function getAvatar(): string + { + return $this->getAvatarGenerator() + ->backgroundColor(ColorGenerator::generate($this->full_name)) + ->image(); + } + + public function getfullNameAttribute(): string + { + return "{$this->first_name} {$this->last_name}"; + } + + protected function getAvatarName(): string + { + return mb_substr($this->first_name, 0, 1) . mb_substr($this->last_name, 0, 1); + } + + protected static function newFactory(): ProfileFactory + { + return ProfileFactory::new(); + } +} diff --git a/app/Eloquent/Models/User.php b/app/Eloquent/Models/User.php index 1cab08a..d167dd5 100644 --- a/app/Eloquent/Models/User.php +++ b/app/Eloquent/Models/User.php @@ -8,27 +8,20 @@ use Database\Factories\UserFactory; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; -use Illuminate\Support\Carbon; use Illuminate\Support\Collection; -use Rackbeat\UIAvatars\HasAvatar; use Toby\Domain\Enums\EmploymentForm; use Toby\Domain\Enums\Role; -use Toby\Eloquent\Helpers\ColorGenerator; /** * @property int $id - * @property string $first_name - * @property string $last_name * @property string $email * @property string $password - * @property string $avatar - * @property string $position * @property Role $role - * @property EmploymentForm $employment_form - * @property Carbon $employment_date + * @property Profile $profile * @property Collection $vacationLimits * @property Collection $vacationRequests * @property Collection $vacations @@ -38,12 +31,12 @@ class User extends Authenticatable use HasFactory; use Notifiable; use SoftDeletes; - use HasAvatar; protected $guarded = []; protected $casts = [ "role" => Role::class, + "last_active_at" => "datetime", "employment_form" => EmploymentForm::class, "employment_date" => "date", ]; @@ -52,6 +45,15 @@ class User extends Authenticatable "remember_token", ]; + protected $with = [ + "profile", + ]; + + public function profile(): HasOne + { + return $this->hasOne(Profile::class); + } + public function vacationLimits(): HasMany { return $this->hasMany(VacationLimit::class); @@ -72,18 +74,6 @@ class User extends Authenticatable return $this->hasMany(Vacation::class); } - public function getAvatar(): string - { - return $this->getAvatarGenerator() - ->backgroundColor(ColorGenerator::generate($this->fullName)) - ->image(); - } - - public function getFullNameAttribute(): string - { - return "{$this->first_name} {$this->last_name}"; - } - public function hasRole(Role $role): bool { return $this->role === $role; @@ -104,9 +94,20 @@ class User extends Authenticatable } return $query - ->where("first_name", "ILIKE", "%{$text}%") - ->orWhere("last_name", "ILIKE", "%{$text}%") - ->orWhere("email", "ILIKE", "%{$text}%"); + ->where("email", "ILIKE", "%{$text}%") + ->orWhereRelation( + "profile", + fn(Builder $query) => $query + ->where("first_name", "ILIKE", "%{$text}%") + ->orWhere("last_name", "ILIKE", "%{$text}%"), + ); + } + + public function scopeOrderByProfileField(Builder $query, string $field): Builder + { + $profileQuery = Profile::query()->select($field)->whereColumn("users.id", "profiles.user_id"); + + return $query->orderBy($profileQuery); } public function scopeWithVacationLimitIn(Builder $query, YearPeriod $yearPeriod): Builder @@ -119,11 +120,6 @@ class User extends Authenticatable ); } - protected function getAvatarName(): string - { - return mb_substr($this->first_name, 0, 1) . mb_substr($this->last_name, 0, 1); - } - protected static function newFactory(): UserFactory { return UserFactory::new(); diff --git a/app/Eloquent/Models/VacationLimit.php b/app/Eloquent/Models/VacationLimit.php index f7e3208..9c64bab 100644 --- a/app/Eloquent/Models/VacationLimit.php +++ b/app/Eloquent/Models/VacationLimit.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace Toby\Eloquent\Models; use Database\Factories\VacationLimitFactory; -use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -37,13 +36,6 @@ class VacationLimit extends Model return $this->belongsTo(YearPeriod::class); } - public function scopeOrderByUserField(Builder $query, string $field): Builder - { - $userQuery = User::query()->select($field)->whereColumn("vacation_limits.user_id", "users.id"); - - return $query->orderBy($userQuery); - } - protected static function newFactory(): VacationLimitFactory { return VacationLimitFactory::new(); diff --git a/app/Infrastructure/Console/Commands/MoveUserDataToProfile.php b/app/Infrastructure/Console/Commands/MoveUserDataToProfile.php new file mode 100644 index 0000000..565d244 --- /dev/null +++ b/app/Infrastructure/Console/Commands/MoveUserDataToProfile.php @@ -0,0 +1,29 @@ +profile()->updateOrCreate(["user_id" => $user->id], [ + "first_name" => $user->first_name, + "last_name" => $user->last_name, + "position" => $user->position, + "employment_form" => $user->employment_form, + "employment_date" => $user->employment_date, + ]); + } + } +} diff --git a/app/Infrastructure/Http/Controllers/Api/GetAvailableVacationTypesController.php b/app/Infrastructure/Http/Controllers/Api/GetAvailableVacationTypesController.php index 20d923c..22a5f33 100644 --- a/app/Infrastructure/Http/Controllers/Api/GetAvailableVacationTypesController.php +++ b/app/Infrastructure/Http/Controllers/Api/GetAvailableVacationTypesController.php @@ -21,7 +21,7 @@ class GetAvailableVacationTypesController extends Controller $user = User::query()->find($request->get("user")); $types = VacationType::all() - ->filter(fn(VacationType $type) => $configRetriever->isAvailableFor($type, $user->employment_form)) + ->filter(fn(VacationType $type) => $configRetriever->isAvailableFor($type, $user->profile->employment_form)) ->map(fn(VacationType $type) => [ "label" => $type->label(), "value" => $type->value, diff --git a/app/Infrastructure/Http/Controllers/DashboardController.php b/app/Infrastructure/Http/Controllers/DashboardController.php index 1a94510..a0fb994 100644 --- a/app/Infrastructure/Http/Controllers/DashboardController.php +++ b/app/Infrastructure/Http/Controllers/DashboardController.php @@ -9,21 +9,23 @@ use Illuminate\Support\Carbon; use Inertia\Response; use Toby\Domain\UserVacationStatsRetriever; use Toby\Domain\VacationRequestStatesRetriever; -use Toby\Eloquent\Models\Holiday; +use Toby\Eloquent\Helpers\YearPeriodRetriever; use Toby\Eloquent\Models\Vacation; use Toby\Eloquent\Models\VacationRequest; -use Toby\Eloquent\Models\YearPeriod; use Toby\Infrastructure\Http\Resources\AbsenceResource; use Toby\Infrastructure\Http\Resources\HolidayResource; use Toby\Infrastructure\Http\Resources\VacationRequestResource; class DashboardController extends Controller { - public function __invoke(Request $request, UserVacationStatsRetriever $vacationStatsRetriever): Response - { + public function __invoke( + Request $request, + YearPeriodRetriever $yearPeriodRetriever, + UserVacationStatsRetriever $vacationStatsRetriever, + ): Response { $user = $request->user(); $now = Carbon::now(); - $yearPeriod = YearPeriod::findByYear($now->year); + $yearPeriod = $yearPeriodRetriever->selected(); $absences = Vacation::query() ->with(["user", "vacationRequest"]) @@ -32,19 +34,21 @@ class DashboardController extends Controller ->get(); if ($user->can("listAll", VacationRequest::class)) { - $vacationRequests = VacationRequest::query() + $vacationRequests = $yearPeriod->vacationRequests() ->states(VacationRequestStatesRetriever::waitingForUserActionStates($user)) ->latest("updated_at") ->limit(3) ->get(); } else { $vacationRequests = $user->vacationRequests() + ->whereBelongsTo($yearPeriod) ->latest("updated_at") ->limit(3) ->get(); } - $holidays = Holiday::query() + $holidays = $yearPeriod + ->holidays() ->whereDate("date", ">=", $now) ->orderBy("date") ->limit(3) diff --git a/app/Infrastructure/Http/Controllers/MonthlyUsageController.php b/app/Infrastructure/Http/Controllers/MonthlyUsageController.php index 6b7784b..fdffbd0 100644 --- a/app/Infrastructure/Http/Controllers/MonthlyUsageController.php +++ b/app/Infrastructure/Http/Controllers/MonthlyUsageController.php @@ -10,7 +10,7 @@ use Toby\Domain\Enums\Month; use Toby\Domain\UserVacationStatsRetriever; use Toby\Eloquent\Helpers\YearPeriodRetriever; use Toby\Eloquent\Models\User; -use Toby\Infrastructure\Http\Resources\UserResource; +use Toby\Infrastructure\Http\Resources\SimpleUserResource; class MonthlyUsageController extends Controller { @@ -27,8 +27,8 @@ class MonthlyUsageController extends Controller $users = User::query() ->withVacationLimitIn($currentYearPeriod) ->where("id", "!=", $currentUser->id) - ->orderBy("last_name") - ->orderBy("first_name") + ->orderByProfileField("last_name") + ->orderByProfileField("first_name") ->get(); if ($currentUser->hasVacationLimit($currentYearPeriod)) { @@ -45,7 +45,7 @@ class MonthlyUsageController extends Controller $remaining = $limit - $used - $pending; $monthlyUsage[] = [ - "user" => new UserResource($user), + "user" => new SimpleUserResource($user), "months" => $vacationsByMonth, "stats" => [ "used" => $used, diff --git a/app/Infrastructure/Http/Controllers/TimesheetController.php b/app/Infrastructure/Http/Controllers/TimesheetController.php index 69cd971..3408bf5 100644 --- a/app/Infrastructure/Http/Controllers/TimesheetController.php +++ b/app/Infrastructure/Http/Controllers/TimesheetController.php @@ -28,9 +28,9 @@ class TimesheetController extends Controller $carbonMonth = Carbon::create($yearPeriod->year, $month->toCarbonNumber()); $users = User::query() - ->where("employment_form", EmploymentForm::EmploymentContract) - ->orderBy("last_name") - ->orderBy("first_name") + ->whereRelation("profile", "employment_form", EmploymentForm::EmploymentContract) + ->orderByProfileField("last_name") + ->orderByProfileField("first_name") ->get(); $types = VacationType::all() diff --git a/app/Infrastructure/Http/Controllers/UserController.php b/app/Infrastructure/Http/Controllers/UserController.php index 39c35b4..654c0c7 100644 --- a/app/Infrastructure/Http/Controllers/UserController.php +++ b/app/Infrastructure/Http/Controllers/UserController.php @@ -9,6 +9,7 @@ use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Inertia\Response; use Toby\Domain\Actions\CreateUserAction; +use Toby\Domain\Actions\UpdateUserAction; use Toby\Domain\Enums\EmploymentForm; use Toby\Domain\Enums\Role; use Toby\Eloquent\Models\User; @@ -28,8 +29,8 @@ class UserController extends Controller $users = User::query() ->withTrashed() ->search($request->query("search")) - ->orderBy("last_name") - ->orderBy("first_name") + ->orderByProfileField("last_name") + ->orderByProfileField("first_name") ->paginate() ->withQueryString(); @@ -59,7 +60,7 @@ class UserController extends Controller { $this->authorize("manageUsers"); - $createUserAction->execute($request->data()); + $createUserAction->execute($request->userData(), $request->profileData()); return redirect() ->route("users.index") @@ -83,11 +84,11 @@ class UserController extends Controller /** * @throws AuthorizationException */ - public function update(UserRequest $request, User $user): RedirectResponse + public function update(UserRequest $request, UpdateUserAction $updateUserAction, User $user): RedirectResponse { $this->authorize("manageUsers"); - $user->update($request->data()); + $updateUserAction->execute($user, $request->userData(), $request->profileData()); return redirect() ->route("users.index") diff --git a/app/Infrastructure/Http/Controllers/VacationCalendarController.php b/app/Infrastructure/Http/Controllers/VacationCalendarController.php index a347ee4..20d1678 100644 --- a/app/Infrastructure/Http/Controllers/VacationCalendarController.php +++ b/app/Infrastructure/Http/Controllers/VacationCalendarController.php @@ -11,7 +11,7 @@ use Toby\Domain\CalendarGenerator; use Toby\Domain\Enums\Month; use Toby\Eloquent\Helpers\YearPeriodRetriever; use Toby\Eloquent\Models\User; -use Toby\Infrastructure\Http\Resources\UserResource; +use Toby\Infrastructure\Http\Resources\SimpleUserResource; class VacationCalendarController extends Controller { @@ -29,8 +29,8 @@ class VacationCalendarController extends Controller $users = User::query() ->where("id", "!=", $currentUser->id) - ->orderBy("last_name") - ->orderBy("first_name") + ->orderByProfileField("last_name") + ->orderByProfileField("first_name") ->get(); $users->prepend($currentUser); @@ -41,7 +41,7 @@ class VacationCalendarController extends Controller "calendar" => $calendar, "current" => Month::current(), "selected" => $month->value, - "users" => UserResource::collection($users), + "users" => SimpleUserResource::collection($users), "can" => [ "generateTimesheet" => $request->user()->can("generateTimesheet"), ], diff --git a/app/Infrastructure/Http/Controllers/VacationLimitController.php b/app/Infrastructure/Http/Controllers/VacationLimitController.php index a56f2c3..cb3c8ef 100644 --- a/app/Infrastructure/Http/Controllers/VacationLimitController.php +++ b/app/Infrastructure/Http/Controllers/VacationLimitController.php @@ -11,7 +11,7 @@ use Toby\Eloquent\Helpers\YearPeriodRetriever; use Toby\Eloquent\Models\VacationLimit; use Toby\Eloquent\Models\YearPeriod; use Toby\Infrastructure\Http\Requests\VacationLimitRequest; -use Toby\Infrastructure\Http\Resources\UserResource; +use Toby\Infrastructure\Http\Resources\SimpleUserResource; class VacationLimitController extends Controller { @@ -24,15 +24,15 @@ class VacationLimitController extends Controller $limits = $yearPeriod ->vacationLimits() - ->with("user") + ->with("user.profile") ->has("user") - ->orderByUserField("last_name") - ->orderByUserField("first_name") - ->get(); + ->get() + ->sortBy(fn(VacationLimit $limit): string => "{$limit->user->profile->last_name} {$limit->user->profile->first_name}") + ->values(); $limitsResource = $limits->map(fn(VacationLimit $limit) => [ "id" => $limit->id, - "user" => new UserResource($limit->user), + "user" => new SimpleUserResource($limit->user), "hasVacation" => $limit->hasVacation(), "days" => $limit->days, "remainingLastYear" => $previousYearPeriod diff --git a/app/Infrastructure/Http/Controllers/VacationRequestController.php b/app/Infrastructure/Http/Controllers/VacationRequestController.php index 6325789..64dfc56 100644 --- a/app/Infrastructure/Http/Controllers/VacationRequestController.php +++ b/app/Infrastructure/Http/Controllers/VacationRequestController.php @@ -27,14 +27,18 @@ use Toby\Eloquent\Helpers\YearPeriodRetriever; use Toby\Eloquent\Models\User; use Toby\Eloquent\Models\VacationRequest; use Toby\Infrastructure\Http\Requests\VacationRequestRequest; -use Toby\Infrastructure\Http\Resources\UserResource; +use Toby\Infrastructure\Http\Resources\SimpleUserResource; use Toby\Infrastructure\Http\Resources\VacationRequestActivityResource; use Toby\Infrastructure\Http\Resources\VacationRequestResource; class VacationRequestController extends Controller { - public function index(Request $request, YearPeriodRetriever $yearPeriodRetriever): Response + public function index(Request $request, YearPeriodRetriever $yearPeriodRetriever): Response|RedirectResponse { + if ($request->user()->can("listAll", VacationRequest::class)) { + return redirect()->route("vacation.requests.indexForApprovers"); + } + $status = $request->get("status", "all"); $vacationRequests = $request->user() @@ -103,13 +107,13 @@ class VacationRequestController extends Controller ->paginate(); $users = User::query() - ->orderBy("last_name") - ->orderBy("first_name") + ->orderByProfileField("last_name") + ->orderByProfileField("first_name") ->get(); return inertia("VacationRequest/IndexForApprovers", [ "requests" => VacationRequestResource::collection($vacationRequests), - "users" => UserResource::collection($users), + "users" => SimpleUserResource::collection($users), "filters" => [ "status" => $status, "user" => (int)$user, @@ -158,13 +162,13 @@ class VacationRequestController extends Controller public function create(Request $request): Response { $users = User::query() - ->orderBy("last_name") - ->orderBy("first_name") + ->orderByProfileField("last_name") + ->orderByProfileField("first_name") ->get(); return inertia("VacationRequest/Create", [ "vacationTypes" => VacationType::casesToSelect(), - "users" => UserResource::collection($users), + "users" => SimpleUserResource::collection($users), "can" => [ "createOnBehalfOfEmployee" => $request->user()->can("createOnBehalfOfEmployee", VacationRequest::class), "skipFlow" => $request->user()->can("skipFlow", VacationRequest::class), diff --git a/app/Infrastructure/Http/Middleware/HandleInertiaRequests.php b/app/Infrastructure/Http/Middleware/HandleInertiaRequests.php index 67db6de..7344b7f 100644 --- a/app/Infrastructure/Http/Middleware/HandleInertiaRequests.php +++ b/app/Infrastructure/Http/Middleware/HandleInertiaRequests.php @@ -4,8 +4,10 @@ declare(strict_types=1); namespace Toby\Infrastructure\Http\Middleware; +use Closure; use Illuminate\Http\Request; use Inertia\Middleware; +use Toby\Domain\VacationRequestStatesRetriever; use Toby\Eloquent\Helpers\YearPeriodRetriever; use Toby\Eloquent\Models\VacationRequest; use Toby\Infrastructure\Http\Resources\UserResource; @@ -18,24 +20,54 @@ class HandleInertiaRequests extends Middleware public function share(Request $request): array { - $user = $request->user(); - return array_merge(parent::share($request), [ - "auth" => fn() => [ - "user" => $user ? new UserResource($user) : null, - "can" => [ - "manageVacationLimits" => $user ? $user->can("manageVacationLimits") : false, - "manageUsers" => $user ? $user->can("manageUsers") : false, - "listAllVacationRequests" => $user ? $user->can("listAll", VacationRequest::class) : false, - "listMonthlyUsage" => $user ? $user->can("listMonthlyUsage") : false, - ], - ], - "flash" => fn() => [ - "success" => $request->session()->get("success"), - "error" => $request->session()->get("error"), - "info" => $request->session()->get("info"), - ], - "years" => fn() => $user ? $this->yearPeriodRetriever->links() : [], + "auth" => $this->getAuthData($request), + "flash" => $this->getFlashData($request), + "years" => $this->getYearsData($request), + "vacationRequestsCount" => $this->getVacationRequestsCount($request), ]); } + + protected function getAuthData(Request $request): Closure + { + $user = $request->user(); + + return fn() => [ + "user" => $user ? new UserResource($user) : null, + "can" => [ + "manageVacationLimits" => $user ? $user->can("manageVacationLimits") : false, + "manageUsers" => $user ? $user->can("manageUsers") : false, + "listAllVacationRequests" => $user ? $user->can("listAll", VacationRequest::class) : false, + "listMonthlyUsage" => $user ? $user->can("listMonthlyUsage") : false, + ], + ]; + } + + protected function getFlashData(Request $request): Closure + { + return fn() => [ + "success" => $request->session()->get("success"), + "error" => $request->session()->get("error"), + "info" => $request->session()->get("info"), + ]; + } + + protected function getYearsData(Request $request): Closure + { + return fn(): array => $request->user() ? $this->yearPeriodRetriever->links() : []; + } + + protected function getVacationRequestsCount(Request $request): Closure + { + $user = $request->user(); + + return fn(): ?int => $user && $user->can("listAll", VacationRequest::class) + ? VacationRequest::query() + ->whereBelongsTo($this->yearPeriodRetriever->selected()) + ->states( + VacationRequestStatesRetriever::waitingForUserActionStates($user), + ) + ->count() + : null; + } } diff --git a/app/Infrastructure/Http/Middleware/TrackUserLastActivity.php b/app/Infrastructure/Http/Middleware/TrackUserLastActivity.php new file mode 100644 index 0000000..e3a710d --- /dev/null +++ b/app/Infrastructure/Http/Middleware/TrackUserLastActivity.php @@ -0,0 +1,21 @@ +user()?->update([ + "last_active_at" => Carbon::now(), + ]); + + return $next($request); + } +} diff --git a/app/Infrastructure/Http/Requests/UserRequest.php b/app/Infrastructure/Http/Requests/UserRequest.php index e46e5f0..d3f094f 100644 --- a/app/Infrastructure/Http/Requests/UserRequest.php +++ b/app/Infrastructure/Http/Requests/UserRequest.php @@ -25,14 +25,20 @@ class UserRequest extends FormRequest ]; } - public function data(): array + public function userData(): array + { + return [ + "email" => $this->get("email"), + "role" => $this->get("role"), + ]; + } + + public function profileData(): array { return [ "first_name" => $this->get("firstName"), "last_name" => $this->get("lastName"), - "email" => $this->get("email"), "position" => $this->get("position"), - "role" => $this->get("role"), "employment_form" => $this->get("employmentForm"), "employment_date" => $this->get("employmentDate"), ]; diff --git a/app/Infrastructure/Http/Resources/AbsenceResource.php b/app/Infrastructure/Http/Resources/AbsenceResource.php index 27bb64f..e15d2fc 100644 --- a/app/Infrastructure/Http/Resources/AbsenceResource.php +++ b/app/Infrastructure/Http/Resources/AbsenceResource.php @@ -14,7 +14,7 @@ class AbsenceResource extends JsonResource { return [ "id" => $this->id, - "user" => new UserResource($this->user), + "user" => new SimpleUserResource($this->user), "date" => $this->date->toDisplayString(), ]; } diff --git a/app/Infrastructure/Http/Resources/HolidayResource.php b/app/Infrastructure/Http/Resources/HolidayResource.php index f835078..4b79b09 100644 --- a/app/Infrastructure/Http/Resources/HolidayResource.php +++ b/app/Infrastructure/Http/Resources/HolidayResource.php @@ -16,6 +16,7 @@ class HolidayResource extends JsonResource "id" => $this->id, "name" => $this->name, "date" => $this->date->toDateString(), + "isPast" => $this->date->isPast(), "displayDate" => $this->date->toDisplayString(), "dayOfWeek" => $this->date->dayName, ]; diff --git a/app/Infrastructure/Http/Resources/SimpleUserResource.php b/app/Infrastructure/Http/Resources/SimpleUserResource.php new file mode 100644 index 0000000..00a34fb --- /dev/null +++ b/app/Infrastructure/Http/Resources/SimpleUserResource.php @@ -0,0 +1,22 @@ + $this->id, + "name" => $this->profile->full_name, + "email" => $this->email, + "avatar" => $this->profile->getAvatar(), + ]; + } +} diff --git a/app/Infrastructure/Http/Resources/UserFormDataResource.php b/app/Infrastructure/Http/Resources/UserFormDataResource.php index 6847362..219a2c9 100644 --- a/app/Infrastructure/Http/Resources/UserFormDataResource.php +++ b/app/Infrastructure/Http/Resources/UserFormDataResource.php @@ -14,13 +14,13 @@ class UserFormDataResource extends JsonResource { return [ "id" => $this->id, - "firstName" => $this->first_name, - "lastName" => $this->last_name, + "firstName" => $this->profile->first_name, + "lastName" => $this->profile->last_name, "email" => $this->email, "role" => $this->role, - "position" => $this->position, - "employmentForm" => $this->employment_form, - "employmentDate" => $this->employment_date->toDateString(), + "position" => $this->profile->position, + "employmentForm" => $this->profile->employment_form, + "employmentDate" => $this->profile->employment_date->toDateString(), ]; } } diff --git a/app/Infrastructure/Http/Resources/UserResource.php b/app/Infrastructure/Http/Resources/UserResource.php index 2428f7f..142aafe 100644 --- a/app/Infrastructure/Http/Resources/UserResource.php +++ b/app/Infrastructure/Http/Resources/UserResource.php @@ -14,14 +14,15 @@ class UserResource extends JsonResource { return [ "id" => $this->id, - "name" => $this->fullName, + "name" => $this->profile->full_name, "email" => $this->email, "role" => $this->role->label(), - "position" => $this->position, - "avatar" => $this->getAvatar(), + "position" => $this->profile->position, + "avatar" => $this->profile->getAvatar(), "deleted" => $this->trashed(), - "employmentForm" => $this->employment_form->label(), - "employmentDate" => $this->employment_date->toDisplayString(), + "lastActiveAt" => $this->last_active_at?->toDateTimeString(), + "employmentForm" => $this->profile->employment_form->label(), + "employmentDate" => $this->profile->employment_date->toDisplayString(), ]; } } diff --git a/app/Infrastructure/Http/Resources/VacationRequestActivityResource.php b/app/Infrastructure/Http/Resources/VacationRequestActivityResource.php index 794f218..491468c 100644 --- a/app/Infrastructure/Http/Resources/VacationRequestActivityResource.php +++ b/app/Infrastructure/Http/Resources/VacationRequestActivityResource.php @@ -15,7 +15,7 @@ class VacationRequestActivityResource extends JsonResource return [ "date" => $this->created_at->toDisplayString(), "time" => $this->created_at->format("H:i"), - "user" => $this->user ? $this->user->fullName : __("System"), + "user" => $this->user ? $this->user->profile->full_name : __("System"), "state" => $this->to, ]; } diff --git a/app/Infrastructure/Http/Resources/VacationRequestResource.php b/app/Infrastructure/Http/Resources/VacationRequestResource.php index e7d5615..ddd6f4b 100644 --- a/app/Infrastructure/Http/Resources/VacationRequestResource.php +++ b/app/Infrastructure/Http/Resources/VacationRequestResource.php @@ -15,7 +15,7 @@ class VacationRequestResource extends JsonResource return [ "id" => $this->id, "name" => $this->name, - "user" => new UserResource($this->user), + "user" => new SimpleUserResource($this->user), "type" => $this->type, "state" => $this->state, "from" => $this->from->toDisplayString(), diff --git a/app/Infrastructure/Jobs/ClearVacationRequestDaysInGoogleCalendar.php b/app/Infrastructure/Jobs/ClearVacationRequestDaysInGoogleCalendar.php index ec5e6ff..56c042f 100644 --- a/app/Infrastructure/Jobs/ClearVacationRequestDaysInGoogleCalendar.php +++ b/app/Infrastructure/Jobs/ClearVacationRequestDaysInGoogleCalendar.php @@ -21,7 +21,7 @@ class ClearVacationRequestDaysInGoogleCalendar implements ShouldQueue public function handle(): void { - foreach ($this->vacationRequest->event_ids as $eventId) { + foreach ($this->vacationRequest->event_ids ?? [] as $eventId) { $calendarEvent = Event::find($eventId); if ($calendarEvent->googleEvent->getStatus() !== "cancelled") { diff --git a/app/Infrastructure/Jobs/SendVacationRequestDaysToGoogleCalendar.php b/app/Infrastructure/Jobs/SendVacationRequestDaysToGoogleCalendar.php index e912d87..9510f72 100644 --- a/app/Infrastructure/Jobs/SendVacationRequestDaysToGoogleCalendar.php +++ b/app/Infrastructure/Jobs/SendVacationRequestDaysToGoogleCalendar.php @@ -32,7 +32,7 @@ class SendVacationRequestDaysToGoogleCalendar implements ShouldQueue $ranges = $this->prepareRanges($days); foreach ($ranges as $range) { - $text = "{$this->vacationRequest->type->label()} - {$this->vacationRequest->user->fullName} [{$this->vacationRequest->name}]"; + $text = "{$this->vacationRequest->type->label()} - {$this->vacationRequest->user->profile->full_name} [{$this->vacationRequest->name}]"; $event = Event::create([ "name" => $text, diff --git a/database/factories/ProfileFactory.php b/database/factories/ProfileFactory.php new file mode 100644 index 0000000..706df10 --- /dev/null +++ b/database/factories/ProfileFactory.php @@ -0,0 +1,28 @@ + User::factory(), + "first_name" => $this->faker->firstName(), + "last_name" => $this->faker->lastName(), + "employment_form" => $this->faker->randomElement(EmploymentForm::cases()), + "position" => $this->faker->jobTitle(), + "employment_date" => Carbon::createFromInterface($this->faker->dateTimeBetween("2020-10-27"))->toDateString(), + ]; + } +} diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index 0b7d9d6..8ed78f4 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -5,10 +5,9 @@ declare(strict_types=1); namespace Database\Factories; use Illuminate\Database\Eloquent\Factories\Factory; -use Illuminate\Support\Carbon; use Illuminate\Support\Str; -use Toby\Domain\Enums\EmploymentForm; use Toby\Domain\Enums\Role; +use Toby\Eloquent\Models\Profile; use Toby\Eloquent\Models\User; class UserFactory extends Factory @@ -18,17 +17,21 @@ class UserFactory extends Factory public function definition(): array { return [ - "first_name" => $this->faker->firstName(), - "last_name" => $this->faker->lastName(), "email" => $this->faker->unique()->safeEmail(), - "employment_form" => $this->faker->randomElement(EmploymentForm::cases()), - "position" => $this->faker->jobTitle(), "role" => Role::Employee, - "employment_date" => Carbon::createFromInterface($this->faker->dateTimeBetween("2020-10-27"))->toDateString(), "remember_token" => Str::random(10), ]; } + public function configure(): self + { + return $this->afterCreating(function (User $user): void { + if (!$user->profile()->exists()) { + Profile::factory()->for($user)->create(); + } + }); + } + public function admin(): static { return $this->state([ diff --git a/database/migrations/2022_04_08_090405_create_profiles_table.php b/database/migrations/2022_04_08_090405_create_profiles_table.php new file mode 100644 index 0000000..11ce008 --- /dev/null +++ b/database/migrations/2022_04_08_090405_create_profiles_table.php @@ -0,0 +1,28 @@ +foreignIdFor(User::class)->primary(); + $table->string("first_name")->nullable(); + $table->string("last_name")->nullable(); + $table->string("position")->nullable(); + $table->string("employment_form")->nullable(); + $table->date("employment_date")->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists("profiles"); + } +}; diff --git a/database/migrations/2022_04_08_112108_remove_profile_columns_in_users_table.php b/database/migrations/2022_04_08_112108_remove_profile_columns_in_users_table.php new file mode 100644 index 0000000..e863201 --- /dev/null +++ b/database/migrations/2022_04_08_112108_remove_profile_columns_in_users_table.php @@ -0,0 +1,34 @@ +dropColumn("first_name"); + $table->dropColumn("last_name"); + $table->dropColumn("position"); + $table->dropColumn("employment_form"); + $table->dropColumn("employment_date"); + }); + } + + public function down(): void + { + Schema::table("users", function (Blueprint $table): void { + $table->string("first_name")->nullable(); + $table->string("last_name")->nullable(); + $table->string("position")->nullable(); + $table->string("employment_form")->nullable(); + $table->date("employment_date")->nullable(); + }); + } +}; diff --git a/database/migrations/2022_04_08_121027_add_last_active_at_column_in_users_table.php b/database/migrations/2022_04_08_121027_add_last_active_at_column_in_users_table.php new file mode 100644 index 0000000..9f0da83 --- /dev/null +++ b/database/migrations/2022_04_08_121027_add_last_active_at_column_in_users_table.php @@ -0,0 +1,23 @@ +timestamp("last_active_at")->nullable(); + }); + } + + public function down(): void + { + Schema::table("users", function (Blueprint $table): void { + $table->dropColumn("last_active_at"); + }); + } +}; diff --git a/database/seeders/DemoSeeder.php b/database/seeders/DemoSeeder.php index 4d50271..53454e2 100644 --- a/database/seeders/DemoSeeder.php +++ b/database/seeders/DemoSeeder.php @@ -30,75 +30,87 @@ class DemoSeeder extends Seeder public function run(): void { $user = User::factory([ - "first_name" => "Jan", - "last_name" => "Kowalski", "email" => env("LOCAL_EMAIL_FOR_LOGIN_VIA_GOOGLE"), - "employment_form" => EmploymentForm::EmploymentContract, - "position" => "programista", "role" => Role::Administrator, - "employment_date" => Carbon::createFromDate(2021, 12, 31), "remember_token" => Str::random(10), ]) + ->hasProfile([ + "first_name" => "Jan", + "last_name" => "Kowalski", + "employment_form" => EmploymentForm::EmploymentContract, + "position" => "programista", + "employment_date" => Carbon::createFromDate(2021, 12, 31), + ]) ->create(); User::factory([ - "first_name" => "Anna", - "last_name" => "Nowak", "email" => "anna.nowak@example.com", - "employment_form" => EmploymentForm::CommissionContract, - "position" => "tester", "role" => Role::Employee, - "employment_date" => Carbon::createFromDate(2021, 5, 10), "remember_token" => Str::random(10), ]) + ->hasProfile([ + "first_name" => "Anna", + "last_name" => "Nowak", + "employment_form" => EmploymentForm::CommissionContract, + "position" => "tester", + "employment_date" => Carbon::createFromDate(2021, 5, 10), + ]) ->create(); User::factory([ - "first_name" => "Tola", - "last_name" => "Sawicka", "email" => "tola.sawicka@example.com", - "employment_form" => EmploymentForm::B2bContract, - "position" => "programista", "role" => Role::Employee, - "employment_date" => Carbon::createFromDate(2021, 1, 4), "remember_token" => Str::random(10), ]) + ->hasProfile([ + "first_name" => "Tola", + "last_name" => "Sawicka", + "employment_form" => EmploymentForm::B2bContract, + "position" => "programista", + "employment_date" => Carbon::createFromDate(2021, 1, 4), + ]) ->create(); $technicalApprover = User::factory([ - "first_name" => "Maciej", - "last_name" => "Ziółkowski", "email" => "maciej.ziolkowski@example.com", - "employment_form" => EmploymentForm::BoardMemberContract, - "position" => "programista", "role" => Role::TechnicalApprover, - "employment_date" => Carbon::createFromDate(2021, 1, 4), "remember_token" => Str::random(10), ]) + ->hasProfile([ + "first_name" => "Maciej", + "last_name" => "Ziółkowski", + "employment_form" => EmploymentForm::BoardMemberContract, + "position" => "programista", + "employment_date" => Carbon::createFromDate(2021, 1, 4), + ]) ->create(); $administrativeApprover = User::factory([ - "first_name" => "Katarzyna", - "last_name" => "Zając", "email" => "katarzyna.zajac@example.com", - "employment_form" => EmploymentForm::EmploymentContract, - "position" => "dyrektor", "role" => Role::AdministrativeApprover, - "employment_date" => Carbon::createFromDate(2021, 1, 4), "remember_token" => Str::random(10), ]) + ->hasProfile([ + "first_name" => "Katarzyna", + "last_name" => "Zając", + "employment_form" => EmploymentForm::EmploymentContract, + "position" => "dyrektor", + "employment_date" => Carbon::createFromDate(2021, 1, 4), + ]) ->create(); User::factory([ - "first_name" => "Miłosz", - "last_name" => "Borowski", "email" => "milosz.borowski@example.com", - "employment_form" => EmploymentForm::EmploymentContract, - "position" => "administrator", "role" => Role::Administrator, - "employment_date" => Carbon::createFromDate(2021, 1, 4), "remember_token" => Str::random(10), ]) + ->hasProfile([ + "first_name" => "Miłosz", + "last_name" => "Borowski", + "employment_form" => EmploymentForm::EmploymentContract, + "position" => "administrator", + "employment_date" => Carbon::createFromDate(2021, 1, 4), + ]) ->create(); $users = User::all(); @@ -118,7 +130,7 @@ class DemoSeeder extends Seeder ->afterCreating(function (YearPeriod $yearPeriod) use ($users): void { foreach ($users as $user) { VacationLimit::factory([ - "days" => $user->employment_form === EmploymentForm::EmploymentContract ? 26 : null, + "days" => $user->profile->employment_form === EmploymentForm::EmploymentContract ? 26 : null, ]) ->for($yearPeriod) ->for($user) diff --git a/resources/js/Composables/vacationTypeInfo.js b/resources/js/Composables/vacationTypeInfo.js index 1091b25..4f154cb 100644 --- a/resources/js/Composables/vacationTypeInfo.js +++ b/resources/js/Composables/vacationTypeInfo.js @@ -14,8 +14,8 @@ const types = [ text: 'Urlop wypoczynkowy', value: 'vacation', icon: WhiteBalanceSunnyIcon, - color: 'text-amber-300', - border: 'border-amber-300', + color: 'text-yellow-500', + border: 'border-yellow-500', }, { text: 'Urlop na żądanie', diff --git a/resources/js/Pages/AnnualSummary.vue b/resources/js/Pages/AnnualSummary.vue index 72b7dba..46f434b 100644 --- a/resources/js/Pages/AnnualSummary.vue +++ b/resources/js/Pages/AnnualSummary.vue @@ -39,7 +39,7 @@ offset-distance="0" >