diff --git a/.vscode/project-words.txt b/.vscode/project-words.txt index 0eab83c7e04..573e5460e5b 100644 --- a/.vscode/project-words.txt +++ b/.vscode/project-words.txt @@ -41,6 +41,7 @@ Laratrust Laravel Lcobucci licences +MENTEE Métis Mi'kmaw Michif diff --git a/api/app/Enums/ExecCoaching.php b/api/app/Enums/ExecCoaching.php new file mode 100644 index 00000000000..5046e32972b --- /dev/null +++ b/api/app/Enums/ExecCoaching.php @@ -0,0 +1,18 @@ +> + */ + public function rules(): array + { + $communityId = $this->arg('dreamRoleCommunity.connect'); + $workStreams = $communityId ? WorkStream::where('community_id', $communityId)->get('id')->pluck('id') : []; + + return [ + 'organizationTypeInterest' => ['nullable'], + 'organizationTypeInterest.*' => [Rule::in(array_column(OrganizationTypeInterest::cases(), 'name'))], + 'moveInterest' => ['nullable'], + 'moveInterest.*' => [Rule::in(array_column(MoveInterest::cases(), 'name'))], + 'mentorshipStatus' => ['nullable'], + 'mentorshipStatus.*' => [Rule::in(array_column(Mentorship::cases(), 'name'))], + 'mentorshipInterest' => ['nullable'], + 'mentorshipInterest.*' => [Rule::in(array_column(Mentorship::cases(), 'name'))], + 'execInterest' => ['nullable', 'boolean'], + 'execCoachingStatus' => ['nullable'], + 'execCoachingStatus.*' => [Rule::in(array_column(ExecCoaching::cases(), 'name'))], + 'execCoachingInterest' => ['nullable'], + 'execCoachingInterest.*' => [Rule::in(array_column(ExecCoaching::cases(), 'name'))], + + 'dreamRoleTitle' => ['nullable', 'string'], + 'dreamRoleAdditionalInformation' => ['nullable', 'string'], + 'dreamRoleCommunity.connect' => ['uuid', 'exists:communities,id'], + 'dreamRoleClassification.connect' => ['uuid', 'exists:classifications,id'], + 'dreamRoleWorkStream.connect' => ['prohibited_if:dreamRoleCommunity,null', 'uuid', 'exists:work_streams,id', Rule::in($workStreams)], + 'dreamRoleDepartments.sync.*' => ['uuid', 'exists:departments,id'], + + 'aboutYou' => ['nullable', 'string'], + 'careerGoals' => ['nullable', 'string'], + 'learningGoals' => ['nullable', 'string'], + 'workStyle' => ['nullable', 'string'], + ]; + } + + public function messages(): array + { + return [ + 'dreamRoleCommunity.connect.exists' => ApiErrorEnums::COMMUNITY_NOT_FOUND, + 'dreamRoleClassification.connect.exists' => ApiErrorEnums::CLASSIFICATION_NOT_FOUND, + 'dreamRoleWorkStream.connect.exists' => ApiErrorEnums::WORK_STREAM_NOT_FOUND, + 'dreamRoleWorkStream.connect.in' => ApiErrorEnums::WORK_STREAM_NOT_IN_COMMUNITY, + 'dreamRoleDepartments.sync.*.exists' => ApiErrorEnums::DEPARTMENT_NOT_FOUND, + ]; + } +} diff --git a/api/app/Models/EmployeeProfile.php b/api/app/Models/EmployeeProfile.php new file mode 100644 index 00000000000..85123c165eb --- /dev/null +++ b/api/app/Models/EmployeeProfile.php @@ -0,0 +1,73 @@ + 'array', + 'career_planning_move_interest' => 'array', + 'career_planning_mentorship_status' => 'array', + 'career_planning_mentorship_interest' => 'array', + 'career_planning_exec_interest' => 'boolean', + 'career_planning_exec_coaching_status' => 'array', + 'career_planning_exec_coaching_interest' => 'array', + ]; + + /** @return BelongsTo */ + public function dreamRoleCommunity(): BelongsTo + { + return $this->belongsTo(Community::class, 'dream_role_community_id'); + } + + /** @return BelongsTo */ + public function dreamRoleClassification(): BelongsTo + { + return $this->belongsTo(Classification::class, 'dream_role_classification_id'); + } + + /** @return BelongsTo */ + public function dreamRoleWorkStream(): BelongsTo + { + return $this->belongsTo(WorkStream::class, 'dream_role_work_stream_id'); + } + + /** @return BelongsToMany */ + public function dreamRoleDepartments(): BelongsToMany + { + return $this->belongsToMany(Department::class, 'department_user_dream_role', 'user_id', 'department_id'); + } + + /** @return HasOne */ + public function userPublicProfile(): HasOne + { + return $this->hasOne(User::class, 'id')->select(['email', 'firstName', 'lastName']); + } +} diff --git a/api/app/Models/User.php b/api/app/Models/User.php index 078c5fd8fe8..be7b36b3b95 100644 --- a/api/app/Models/User.php +++ b/api/app/Models/User.php @@ -24,6 +24,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Foundation\Auth\Access\Authorizable; use Illuminate\Support\Arr; @@ -303,6 +304,11 @@ public function poolCandidateSearchRequests(): HasMany return $this->hasMany(PoolCandidateSearchRequest::class); } + public function employeeProfile(): HasOne + { + return $this->hasOne(EmployeeProfile::class, 'id'); + } + // This method will add the specified skills to UserSkills if they don't exist yet. public function addSkills($skill_ids) { diff --git a/api/app/Policies/EmployeeProfilePolicy.php b/api/app/Policies/EmployeeProfilePolicy.php new file mode 100644 index 00000000000..387e2810953 --- /dev/null +++ b/api/app/Policies/EmployeeProfilePolicy.php @@ -0,0 +1,25 @@ +isAbleTo('view-own-employeeProfile') && $employeeProfile->id === $user->id; + } + + /** + * Determine whether the user can update the model. + */ + public function update(User $user, EmployeeProfile $employeeProfile): bool + { + return $user->isAbleTo('update-own-employeeProfile') && $employeeProfile->id === $user->id; + } +} diff --git a/api/config/rolepermission.php b/api/config/rolepermission.php index 363941d9b40..feaf08aa87c 100644 --- a/api/config/rolepermission.php +++ b/api/config/rolepermission.php @@ -63,6 +63,7 @@ 'user' => 'user', 'userBasicInfo' => 'userBasicInfo', 'userSub' => 'userSub', + 'employeeProfile' => 'employeeProfile', 'applicantProfile' => 'applicantProfile', 'draftPool' => 'draftPool', 'publishedPool' => 'publishedPool', @@ -243,6 +244,15 @@ 'fr' => 'Mettre à jour son propre profil de candidat', ], + 'view-own-employeeProfile' => [ + 'en' => 'View any Employee Profile', + 'fr' => 'Visionner tout profil de candidat', + ], + 'update-own-employeeProfile' => [ + 'en' => 'Update any Employee Profile', + 'fr' => 'Visionner tout profil de candidat', + ], + 'view-team-draftPool' => [ 'en' => 'View draft Pools in this Team', 'fr' => 'Voir les bassins de brouillons dans cette équipe', @@ -880,6 +890,9 @@ 'user' => [ 'own' => ['view', 'update'], ], + 'employeeProfile' => [ + 'own' => ['view', 'update'], + ], 'publishedPool' => [ 'any' => ['view'], ], diff --git a/api/database/factories/CommunityFactory.php b/api/database/factories/CommunityFactory.php index c332a140cb8..133c6706b6e 100644 --- a/api/database/factories/CommunityFactory.php +++ b/api/database/factories/CommunityFactory.php @@ -3,6 +3,7 @@ namespace Database\Factories; use App\Models\Community; +use App\Models\WorkStream; use Illuminate\Database\Eloquent\Factories\Factory; class CommunityFactory extends Factory @@ -74,4 +75,13 @@ public function withCommunityAdmins(?array $userIds = null) } }); } + + public function withWorkStreams(?int $min = 1, ?int $max = 3) + { + $count = $this->faker->numberBetween($min, $max); + + return $this->afterCreating(function (Community $community) use ($count) { + WorkStream::factory()->count($count)->create(['community_id' => $community->id]); + }); + } } diff --git a/api/database/factories/UserFactory.php b/api/database/factories/UserFactory.php index 80009fd8810..25905f24943 100644 --- a/api/database/factories/UserFactory.php +++ b/api/database/factories/UserFactory.php @@ -6,11 +6,15 @@ use App\Enums\CitizenshipStatus; use App\Enums\EstimatedLanguageAbility; use App\Enums\EvaluatedLanguageAbility; +use App\Enums\ExecCoaching; use App\Enums\GovEmployeeType; use App\Enums\IndigenousCommunity; use App\Enums\Language; +use App\Enums\Mentorship; +use App\Enums\MoveInterest; use App\Enums\NotificationFamily; use App\Enums\OperationalRequirement; +use App\Enums\OrganizationTypeInterest; use App\Enums\PositionDuration; use App\Enums\ProvinceOrTerritory; use App\Models\AwardExperience; @@ -208,6 +212,43 @@ public function asGovEmployee($isGovEmployee = true) }); } + public function withEmployeeProfile() + { + return $this->afterCreating(function (User $user) { + $community = Community::inRandomOrder()->first(); + if (is_null($community)) { + $community = Community::factory()->withWorkStreams()->create(); + } + $classification = Classification::inRandomOrder()->first(); + if (is_null($classification)) { + $classification = Classification::factory()->create(); + } + $workStream = $this->faker->randomElement($community->workStreams); + $departments = Department::inRandomOrder()->limit($this->faker->numberBetween(1, 3))->get(); + + $user->employeeProfile->dreamRoleDepartments()->sync($departments); + + $user->employeeProfile()->update([ + 'career_planning_organization_type_interest' => $this->faker->randomElements(array_column(OrganizationTypeInterest::cases(), 'name'), null), + 'career_planning_move_interest' => $this->faker->randomElements(array_column(MoveInterest::cases(), 'name'), null), + 'career_planning_mentorship_status' => $this->faker->optional(weight: 70)->randomElements(array_column(Mentorship::cases(), 'name'), null), + 'career_planning_mentorship_interest' => $this->faker->optional(weight: 70)->randomElements(array_column(Mentorship::cases(), 'name'), null), + 'career_planning_exec_interest' => $this->faker->boolean(), + 'career_planning_exec_coaching_status' => $this->faker->optional(weight: 80)->randomElements(array_column(ExecCoaching::cases(), 'name'), null), + 'career_planning_exec_coaching_interest' => $this->faker->optional(weight: 80)->randomElements(array_column(ExecCoaching::cases(), 'name'), null), + 'career_planning_about_you' => $this->faker->paragraph(), + 'career_planning_career_goals' => $this->faker->paragraph(), + 'career_planning_learning_goals' => $this->faker->paragraph(), + 'career_planning_work_style' => $this->faker->paragraph(), + 'dream_role_title' => $this->faker->words(3, true), + 'dream_role_additional_information' => $this->faker->paragraph(), + 'dream_role_community_id' => $community->id, + 'dream_role_classification_id' => $classification->id, + 'dream_role_work_stream_id' => $workStream->id, + ]); + }); + } + public function configure() { return $this->afterCreating(function (User $user) { diff --git a/api/database/helpers/ApiErrorEnums.php b/api/database/helpers/ApiErrorEnums.php index 965dd320e3a..3037dd57797 100644 --- a/api/database/helpers/ApiErrorEnums.php +++ b/api/database/helpers/ApiErrorEnums.php @@ -43,6 +43,17 @@ class ApiErrorEnums const CANDIDATE_NOT_PLACED = 'CandidateNotPlaced'; + // Employee profile validation + const COMMUNITY_NOT_FOUND = 'CommunityNotFound'; + + const CLASSIFICATION_NOT_FOUND = 'ClassificationNotFound'; + + const DEPARTMENT_NOT_FOUND = 'DepartmentNotFound'; + + const WORK_STREAM_NOT_FOUND = 'WorkStreamNotFound'; + + const WORK_STREAM_NOT_IN_COMMUNITY = 'WorkStreamNotInCommunity'; + // Localized Enums const ENUM_NOT_FOUND = 'EnumNotFound'; diff --git a/api/database/migrations/2024_12_18_133424_create_employee_profiles_table.php b/api/database/migrations/2024_12_18_133424_create_employee_profiles_table.php new file mode 100644 index 00000000000..453566b9fc9 --- /dev/null +++ b/api/database/migrations/2024_12_18_133424_create_employee_profiles_table.php @@ -0,0 +1,90 @@ +jsonb('career_planning_organization_type_interest')->nullable(); + $table->jsonb('career_planning_move_interest')->nullable(); + $table->jsonb('career_planning_mentorship_status')->nullable(); + $table->jsonb('career_planning_mentorship_interest')->nullable(); + $table->boolean('career_planning_exec_interest')->nullable(); + $table->jsonb('career_planning_exec_coaching_status')->nullable(); + $table->jsonb('career_planning_exec_coaching_interest')->nullable(); + $table->text('career_planning_about_you')->nullable(); + $table->text('career_planning_career_goals')->nullable(); + $table->text('career_planning_learning_goals')->nullable(); + $table->text('career_planning_work_style')->nullable(); + + $table->string('dream_role_title')->nullable(); + $table->text('dream_role_additional_information')->nullable(); + + $table->foreignUuid('dream_role_community_id') + ->nullable() + ->constrained(table: 'communities', column: 'id') + ->onDelete('cascade'); + $table->foreignUuid('dream_role_classification_id') + ->nullable() + ->constrained(table: 'classifications', column: 'id') + ->onDelete('cascade'); + $table->foreignUuid('dream_role_work_stream_id') + ->nullable() + ->constrained(table: 'work_streams', column: 'id') + ->onDelete('cascade'); + + }); + + Schema::create('department_user_dream_role', function (Blueprint $table) { + $table->uuid('id')->primary()->default(new Expression('public.gen_random_uuid()')); + $table->foreignUuid('user_id') + ->constrained() + ->onDelete('cascade'); + $table->foreignUuid('department_id') + ->constrained() + ->onDelete('cascade'); + }); + + } + + /** + * Reverse the migrations + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('career_planning_organization_type_interest'); + $table->dropColumn('career_planning_move_interest'); + $table->dropColumn('career_planning_mentorship_status'); + $table->dropColumn('career_planning_mentorship_interest'); + $table->dropColumn('career_planning_exec_interest'); + $table->dropColumn('career_planning_exec_coaching_status'); + $table->dropColumn('career_planning_exec_coaching_interest'); + $table->dropColumn('career_planning_about_you'); + $table->dropColumn('career_planning_work_style'); + $table->dropColumn('career_planning_career_goals'); + $table->dropColumn('career_planning_learning_goals'); + + $table->dropColumn('dream_role_title'); + $table->dropColumn('dream_role_additional_information'); + + $table->dropForeign(['dream_role_community_id']); + $table->dropForeign(['dream_role_work_stream_id']); + $table->dropForeign(['dream_role_classification_id']); + + $table->dropColumn('dream_role_community_id'); + $table->dropColumn('dream_role_classification_id'); + $table->dropColumn('dream_role_work_stream_id'); + }); + + Schema::dropIfExists('department_user_dream_role'); + } +}; diff --git a/api/database/seeders/UserTestSeeder.php b/api/database/seeders/UserTestSeeder.php index 40bad244cc9..7c7e9c7d3a1 100644 --- a/api/database/seeders/UserTestSeeder.php +++ b/api/database/seeders/UserTestSeeder.php @@ -89,6 +89,7 @@ public function run() User::factory() ->asApplicant() ->withSkillsAndExperiences() + ->withEmployeeProfile() ->create([ 'first_name' => 'Gul', 'last_name' => 'Fields', diff --git a/api/graphql/schema.graphql b/api/graphql/schema.graphql index e1b89f38782..7a2f525c8d2 100644 --- a/api/graphql/schema.graphql +++ b/api/graphql/schema.graphql @@ -113,6 +113,9 @@ type User { @rename(attribute: "accepted_operational_requirements") positionDuration: [PositionDuration] @rename(attribute: "position_duration") + # Employee profile info + employeeProfile: EmployeeProfile @hasOne @canFind(ability: "view") + # Pool info poolCandidates: [PoolCandidate] @hasMany(scopes: ["authorizedToView"]) @@ -148,6 +151,35 @@ type User { @canResolved(ability: "view") # Search requests submitted by a user } +type EmployeeProfile { + organizationTypeInterest: [LocalizedOrganizationTypeInterest!] + @rename(attribute: "career_planning_organization_type_interest") + moveInterest: [LocalizedMoveInterest!] + @rename(attribute: "career_planning_move_interest") + mentorshipStatus: [LocalizedMentorship!] + @rename(attribute: "career_planning_mentorship_status") + mentorshipInterest: [LocalizedMentorship!] + @rename(attribute: "career_planning_mentorship_interest") + execInterest: Boolean @rename(attribute: "career_planning_exec_interest") + execCoachingStatus: [LocalizedExecCoaching!] + @rename(attribute: "career_planning_exec_coaching_status") + execCoachingInterest: [LocalizedExecCoaching!] + @rename(attribute: "career_planning_exec_coaching_interest") + aboutYou: String @rename(attribute: "career_planning_about_you") + careerGoals: String @rename(attribute: "career_planning_career_goals") + learningGoals: String @rename(attribute: "career_planning_learning_goals") + workStyle: String @rename(attribute: "career_planning_work_style") + dreamRoleTitle: String @rename(attribute: "dream_role_title") + dreamRoleAdditionalInformation: String + @rename(attribute: "dream_role_additional_information") + dreamRoleCommunity: Community @belongsTo + dreamRoleClassification: Classification @belongsTo + dreamRoleWorkStream: WorkStream @belongsTo + dreamRoleDepartments: [Department!] @belongsToMany + + userPublicProfile: UserPublicProfile +} + interface Notification { id: ID! readAt: DateTime @rename(attribute: "read_at") @@ -1408,6 +1440,33 @@ input UpdateUserAsUserInput @validator { awardExperiences: AwardExperienceHasMany } +input UpdateEmployeeProfileInput @validator { + organizationTypeInterest: [OrganizationTypeInterest!] + @rename(attribute: "career_planning_organization_type_interest") + moveInterest: [MoveInterest!] + @rename(attribute: "career_planning_move_interest") + mentorshipStatus: [Mentorship!] + @rename(attribute: "career_planning_mentorship_status") + mentorshipInterest: [Mentorship!] + @rename(attribute: "career_planning_mentorship_interest") + execInterest: Boolean @rename(attribute: "career_planning_exec_interest") + execCoachingStatus: [ExecCoaching!] + @rename(attribute: "career_planning_exec_coaching_status") + execCoachingInterest: [ExecCoaching!] + @rename(attribute: "career_planning_exec_coaching_interest") + aboutYou: String @rename(attribute: "career_planning_about_you") + careerGoals: String @rename(attribute: "career_planning_career_goals") + learningGoals: String @rename(attribute: "career_planning_learning_goals") + workStyle: String @rename(attribute: "career_planning_work_style") + dreamRoleTitle: String @rename(attribute: "dream_role_title") + dreamRoleAdditionalInformation: String + @rename(attribute: "dream_role_additional_information") + dreamRoleCommunity: CommunityBelongsTo + dreamRoleClassification: ClassificationBelongsTo + dreamRoleWorkStream: WorkStreamBelongsTo + dreamRoleDepartments: DepartmentBelongsToMany +} + input LocalizedStringInput { en: String fr: String @@ -2144,6 +2203,13 @@ type Mutation { @guard @update(model: "User") @canModel(ability: "updateRoles", model: "User", injectArgs: true) + updateEmployeeProfile( + id: UUID! + employeeProfile: UpdateEmployeeProfileInput! @spread + ): EmployeeProfile + @guard + @update(model: "EmployeeProfile") + @canFind(ability: "update", find: "id", model: "EmployeeProfile") deleteUser(id: ID! @whereKey): User @delete @guard diff --git a/api/lang/en/exec_coaching.php b/api/lang/en/exec_coaching.php new file mode 100644 index 00000000000..956a5b2e480 --- /dev/null +++ b/api/lang/en/exec_coaching.php @@ -0,0 +1,6 @@ + 'Coaching', + 'learning' => 'Learning', +]; diff --git a/api/lang/en/mentorship.php b/api/lang/en/mentorship.php new file mode 100644 index 00000000000..3e069dfba07 --- /dev/null +++ b/api/lang/en/mentorship.php @@ -0,0 +1,6 @@ + 'Mentor', + 'mentee' => 'Mentee', +]; diff --git a/api/lang/en/move_interest.php b/api/lang/en/move_interest.php new file mode 100644 index 00000000000..a523f71d4b7 --- /dev/null +++ b/api/lang/en/move_interest.php @@ -0,0 +1,7 @@ + 'Above level/promotion', + 'at_level' => 'At level/lateral', + 'below_level' => 'Below level/demotion', +]; diff --git a/api/lang/en/organization_type_interest.php b/api/lang/en/organization_type_interest.php new file mode 100644 index 00000000000..ecc636145bd --- /dev/null +++ b/api/lang/en/organization_type_interest.php @@ -0,0 +1,8 @@ + 'Current department/agency/crown corp', + 'other_department' => 'Other departments', + 'other_agency' => 'Other agencies', + 'other_crown_corp' => 'Other crown corps', +]; diff --git a/api/lang/fr/exec_coaching.php b/api/lang/fr/exec_coaching.php new file mode 100644 index 00000000000..60bfe531d91 --- /dev/null +++ b/api/lang/fr/exec_coaching.php @@ -0,0 +1,6 @@ + 'Encadrement', + 'learning' => 'Apprentissage', +]; diff --git a/api/lang/fr/mentorship.php b/api/lang/fr/mentorship.php new file mode 100644 index 00000000000..fd5f9b383d9 --- /dev/null +++ b/api/lang/fr/mentorship.php @@ -0,0 +1,6 @@ + 'Mentor(e)', + 'mentee' => 'Mentoré(e)', +]; diff --git a/api/lang/fr/move_interest.php b/api/lang/fr/move_interest.php new file mode 100644 index 00000000000..e1fb1d641bd --- /dev/null +++ b/api/lang/fr/move_interest.php @@ -0,0 +1,7 @@ + 'Poste de niveau supérieur/promotion', + 'at_level' => 'Poste de même niveau/occasion latérale', + 'below_level' => 'Poste de niveau inférieur/rétrogradation', +]; diff --git a/api/lang/fr/organization_type_interest.php b/api/lang/fr/organization_type_interest.php new file mode 100644 index 00000000000..3ab4696bc3b --- /dev/null +++ b/api/lang/fr/organization_type_interest.php @@ -0,0 +1,8 @@ + 'Organisation actuelle', + 'other_department' => 'Autres ministères', + 'other_agency' => 'Autres organismes', + 'other_crown_corp' => 'Autres sociétés d\'État', +]; diff --git a/api/storage/app/lighthouse-schema.graphql b/api/storage/app/lighthouse-schema.graphql index c7d8660d4e0..d09ea2b0a06 100755 --- a/api/storage/app/lighthouse-schema.graphql +++ b/api/storage/app/lighthouse-schema.graphql @@ -113,6 +113,11 @@ type LocalizedEvaluatedLanguageAbility { label: LocalizedString! } +type LocalizedExecCoaching { + value: ExecCoaching! + label: LocalizedString! +} + type LocalizedExternalRoleSeniority { value: ExternalRoleSeniority! label: LocalizedString! @@ -163,11 +168,26 @@ type LocalizedLanguageAbility { label: LocalizedString! } +type LocalizedMentorship { + value: Mentorship! + label: LocalizedString! +} + +type LocalizedMoveInterest { + value: MoveInterest! + label: LocalizedString! +} + type LocalizedOperationalRequirement { value: OperationalRequirement! label: LocalizedString! } +type LocalizedOrganizationTypeInterest { + value: OrganizationTypeInterest! + label: LocalizedString! +} + type LocalizedPlacementType { value: PlacementType! label: LocalizedString! @@ -406,6 +426,7 @@ type Mutation { updateUserAsAdmin(id: ID!, user: UpdateUserAsAdminInput!): User updateUserSub(updateUserSubInput: UpdateUserSubInput!): UserAuthInfo updateUserRoles(updateUserRolesInput: UpdateUserRolesInput!): UserAuthInfo + updateEmployeeProfile(id: UUID!, employeeProfile: UpdateEmployeeProfileInput!): EmployeeProfile deleteUser(id: ID!): User restoreUser(id: ID!): User sendUserEmailVerification(emailType: EmailType): User @@ -602,6 +623,7 @@ type User { locationExemptions: String acceptedOperationalRequirements: [LocalizedOperationalRequirement] positionDuration: [PositionDuration] + employeeProfile: EmployeeProfile poolCandidates: [PoolCandidate] experiences: [Experience] awardExperiences: [AwardExperience] @@ -624,6 +646,27 @@ type User { poolCandidateSearchRequests: [PoolCandidateSearchRequest!] } +type EmployeeProfile { + organizationTypeInterest: [LocalizedOrganizationTypeInterest!] + moveInterest: [LocalizedMoveInterest!] + mentorshipStatus: [LocalizedMentorship!] + mentorshipInterest: [LocalizedMentorship!] + execInterest: Boolean + execCoachingStatus: [LocalizedExecCoaching!] + execCoachingInterest: [LocalizedExecCoaching!] + aboutYou: String + careerGoals: String + learningGoals: String + workStyle: String + dreamRoleTitle: String + dreamRoleAdditionalInformation: String + dreamRoleCommunity: Community + dreamRoleClassification: Classification + dreamRoleWorkStream: WorkStream + dreamRoleDepartments: [Department!] + userPublicProfile: UserPublicProfile +} + interface Notification { id: ID! readAt: DateTime @@ -1493,6 +1536,26 @@ input UpdateUserAsUserInput { awardExperiences: AwardExperienceHasMany } +input UpdateEmployeeProfileInput { + organizationTypeInterest: [OrganizationTypeInterest!] + moveInterest: [MoveInterest!] + mentorshipStatus: [Mentorship!] + mentorshipInterest: [Mentorship!] + execInterest: Boolean + execCoachingStatus: [ExecCoaching!] + execCoachingInterest: [ExecCoaching!] + aboutYou: String + careerGoals: String + learningGoals: String + workStyle: String + dreamRoleTitle: String + dreamRoleAdditionalInformation: String + dreamRoleCommunity: CommunityBelongsTo + dreamRoleClassification: ClassificationBelongsTo + dreamRoleWorkStream: WorkStreamBelongsTo + dreamRoleDepartments: DepartmentBelongsToMany +} + input LocalizedStringInput { en: String fr: String @@ -3074,6 +3137,11 @@ enum EvaluatedLanguageAbility { NOT_ASSESSED } +enum ExecCoaching { + COACHING + LEARNING +} + enum ExternalRoleSeniority { INTERN_COOP JUNIOR @@ -3160,6 +3228,17 @@ enum LanguageAbility { BILINGUAL } +enum Mentorship { + MENTOR + MENTEE +} + +enum MoveInterest { + ABOVE_LEVEL + AT_LEVEL + BELOW_LEVEL +} + enum NotificationFamily { SYSTEM_MESSAGE APPLICATION_UPDATE @@ -3181,6 +3260,13 @@ enum OperationalRequirement { OVERTIME_REGULAR } +enum OrganizationTypeInterest { + CURRENT + OTHER_DEPARTMENT + OTHER_AGENCY + OTHER_CROWN_CORP +} + enum OverallAssessmentStatus { TO_ASSESS DISQUALIFIED diff --git a/api/tests/Feature/EmployeeProfileTest.php b/api/tests/Feature/EmployeeProfileTest.php new file mode 100644 index 00000000000..9eaba6c209b --- /dev/null +++ b/api/tests/Feature/EmployeeProfileTest.php @@ -0,0 +1,216 @@ +seed([RolePermissionSeeder::class, DepartmentSeeder::class]); + + $this->user = User::factory() + ->asApplicant() + ->withEmployeeProfile() + ->create(); + } + + public function testCanViewEmployeeProfile() + { + + $this->actingAs($this->user, 'api') + ->graphQL(<<<'GRAPHQL' + query { + me { + employeeProfile { + organizationTypeInterest { value } + moveInterest { value } + mentorshipStatus { value } + mentorshipInterest { value } + execInterest + execCoachingStatus { value } + execCoachingInterest { value } + dreamRoleTitle + dreamRoleAdditionalInformation + dreamRoleClassification { id } + dreamRoleCommunity { id } + dreamRoleWorkStream { id } + dreamRoleDepartments { id } + aboutYou + careerGoals + learningGoals + workStyle + } + } + } + GRAPHQL) + ->assertJsonFragment([ + 'organizationTypeInterest' => $this->arrayToLocalizedEnum($this->user->employeeProfile->career_planning_organization_type_interest), + 'moveInterest' => $this->arrayToLocalizedEnum($this->user->employeeProfile->career_planning_move_interest), + 'mentorshipStatus' => $this->arrayToLocalizedEnum($this->user->employeeProfile->career_planning_mentorship_status), + 'mentorshipInterest' => $this->arrayToLocalizedEnum($this->user->employeeProfile->career_planning_mentorship_interest), + 'execInterest' => $this->user->employeeProfile->career_planning_exec_interest, + 'execCoachingStatus' => $this->arrayToLocalizedEnum($this->user->employeeProfile->career_planning_exec_coaching_status), + 'execCoachingInterest' => $this->arrayToLocalizedEnum($this->user->employeeProfile->career_planning_exec_coaching_interest), + 'dreamRoleTitle' => $this->user->employeeProfile->dream_role_title, + 'dreamRoleAdditionalInformation' => $this->user->employeeProfile->dream_role_additional_information, + 'dreamRoleClassification' => ['id' => $this->user->employeeProfile->dreamRoleClassification->id], + 'dreamRoleCommunity' => ['id' => $this->user->employeeProfile->dreamRoleCommunity->id], + 'dreamRoleWorkStream' => ['id' => $this->user->employeeProfile->dreamRoleWorkStream->id], + 'dreamRoleDepartments' => Arr::map($this->user->employeeProfile->dreamRoleDepartments->toArray(), fn ($value) => ['id' => $value['id']]), + 'aboutYou' => $this->user->employeeProfile->career_planning_about_you, + 'careerGoals' => $this->user->employeeProfile->career_planning_career_goals, + 'learningGoals' => $this->user->employeeProfile->career_planning_learning_goals, + 'workStyle' => $this->user->employeeProfile->career_planning_work_style, + ]); + } + + public function testCanUpdateEmployeeProfile() + { + + $community = Community::factory()->withWorkStreams()->create(); + $input = [ + 'organizationTypeInterest' => [OrganizationTypeInterest::CURRENT->name], + 'moveInterest' => [MoveInterest::AT_LEVEL->name], + 'mentorshipStatus' => [Mentorship::MENTOR->name], + 'mentorshipInterest' => array_column(Mentorship::cases(), 'name'), + 'execInterest' => true, + 'execCoachingStatus' => null, + 'execCoachingInterest' => [ExecCoaching::LEARNING->name], + 'dreamRoleTitle' => 'test dream role', + 'dreamRoleAdditionalInformation' => 'test additional information', + 'dreamRoleClassification' => ['connect' => Classification::factory()->create()->id], + 'dreamRoleCommunity' => ['connect' => $community->id], + 'dreamRoleWorkStream' => ['connect' => $community->workStreams->first()->id], + 'dreamRoleDepartments' => ['sync' => [Department::factory()->create()->id]], + 'aboutYou' => 'test about', + 'careerGoals' => 'test careerGoals', + 'learningGoals' => 'test learningGoals', + 'workStyle' => 'test workStyle', + ]; + + $this->actingAs($this->user, 'api') + ->graphQL(<<<'GRAPHQL' + mutation UpdateEmployeeProfile($id: UUID!, $employeeProfile: UpdateEmployeeProfileInput!) { + updateEmployeeProfile(id: $id, employeeProfile: $employeeProfile) { + organizationTypeInterest { value } + moveInterest { value } + mentorshipStatus { value } + mentorshipInterest { value } + execInterest + execCoachingStatus { value } + execCoachingInterest { value } + dreamRoleTitle + dreamRoleAdditionalInformation + dreamRoleClassification { id } + dreamRoleCommunity { id } + dreamRoleWorkStream { id } + dreamRoleDepartments { id } + aboutYou + careerGoals + learningGoals + workStyle + } + } + GRAPHQL, ['id' => $this->user->id, 'employeeProfile' => $input])->assertJsonFragment([ + 'organizationTypeInterest' => $this->arrayToLocalizedEnum($input['organizationTypeInterest']), + 'moveInterest' => $this->arrayToLocalizedEnum($input['moveInterest']), + 'mentorshipStatus' => $this->arrayToLocalizedEnum($input['mentorshipStatus']), + 'mentorshipInterest' => $this->arrayToLocalizedEnum($input['mentorshipInterest']), + 'execInterest' => $input['execInterest'], + 'execCoachingStatus' => $this->arrayToLocalizedEnum($input['execCoachingStatus']), + 'execCoachingInterest' => $this->arrayToLocalizedEnum($input['execCoachingInterest']), + 'dreamRoleTitle' => $input['dreamRoleTitle'], + 'dreamRoleAdditionalInformation' => $input['dreamRoleAdditionalInformation'], + 'dreamRoleClassification' => ['id' => $input['dreamRoleClassification']['connect']], + 'dreamRoleCommunity' => ['id' => $input['dreamRoleCommunity']['connect']], + 'dreamRoleWorkStream' => ['id' => $input['dreamRoleWorkStream']['connect']], + 'dreamRoleDepartments' => [['id' => $input['dreamRoleDepartments']['sync'][0]]], + 'aboutYou' => $input['aboutYou'], + 'careerGoals' => $input['careerGoals'], + 'learningGoals' => $input['learningGoals'], + 'workStyle' => $input['workStyle'], + ]); + } + + public function testUpdateEmployeeProfileBadInputFailsValidation() + { + + $unassociatedWorkStream = WorkStream::factory()->create(); + $community = Community::factory()->withWorkStreams()->create(); + $input = [ + 'dreamRoleClassification' => ['connect' => Str::uuid()], + 'dreamRoleCommunity' => ['connect' => $community->id], + 'dreamRoleWorkStream' => ['connect' => $unassociatedWorkStream->id], + 'dreamRoleDepartments' => ['sync' => [Str::uuid()]], + ]; + + $this->actingAs($this->user, 'api') + ->graphQL(<<<'GRAPHQL' + mutation UpdateEmployeeProfile($id: UUID!, $employeeProfile: UpdateEmployeeProfileInput!) { + updateEmployeeProfile(id: $id, employeeProfile: $employeeProfile) { + userPublicProfile { email } + } + } + GRAPHQL, + [ + 'id' => $this->user->id, + 'employeeProfile' => $input, + ]) + ->assertGraphQLValidationError('employeeProfile.dreamRoleClassification.connect', ApiErrorEnums::CLASSIFICATION_NOT_FOUND) + ->assertGraphQLValidationError('employeeProfile.dreamRoleWorkStream.connect', ApiErrorEnums::WORK_STREAM_NOT_IN_COMMUNITY) + ->assertGraphQLValidationError('employeeProfile.dreamRoleDepartments.sync.0', ApiErrorEnums::DEPARTMENT_NOT_FOUND); + } + + public function testCannotEditAnotherUsersEmployeeProfile() + { + /** @var \App\Models\User $otherUser */ + $otherUser = User::factory()->create(); + + $this->actingAs($otherUser, 'api') + ->graphQL(<<<'GRAPHQL' + mutation UpdateOtherUserEmployeeProfile($id: UUID!, $employeeProfile: UpdateEmployeeProfileInput!) { + updateEmployeeProfile(id: $id, employeeProfile: $employeeProfile) { + dreamRoleTitle + } + } + GRAPHQL, [ + 'id' => $this->user->id, + 'employeeProfile' => ['dreamRoleTitle' => 'test'], + ]) + ->assertGraphQLErrorMessage('This action is unauthorized.'); + } + + protected function arrayToLocalizedEnum(?array $arr) + { + return ! is_null($arr) ? Arr::map($arr, fn ($value) => ['value' => $value]) : null; + } +}