Skip to content

Commit

Permalink
feat: show fulfilled job openings in hr dashboard (#1278)
Browse files Browse the repository at this point in the history
  • Loading branch information
djaiss committed Aug 30, 2021
1 parent b2bc370 commit d61cd34
Show file tree
Hide file tree
Showing 10 changed files with 344 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,29 @@ public function index()
]);
}

/**
* Show the list of job openings that are fulfilled.
*
* @return mixed
*/
public function fulfilled()
{
$company = InstanceHelper::getLoggedCompany();
$employee = InstanceHelper::getLoggedEmployee();

// is this person HR?
if ($employee->permission_level > config('officelife.permission_level.hr')) {
return redirect('home');
}

$jobOpenings = DashboardHRJobOpeningsViewHelper::fulfilledJobOpenings($company);

return Inertia::render('Dashboard/HR/JobOpenings/Fulfilled', [
'notifications' => NotificationHelper::getNotifications($employee),
'jobOpenings' => $jobOpenings,
]);
}

/**
* Show the Create job opening view.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,62 @@ public static function openJobOpenings(Company $company): array
];
}

/**
* Get all the fulfilled job openings in the company.
*
* @param Company $company
* @return array
*/
public static function fulfilledJobOpenings(Company $company): array
{
$fulfilledJobOpenings = $company->jobOpenings()
->with('team')
->with('position')
->with('sponsors')
->where('fulfilled', true)
->orderBy('fulfilled_at', 'desc')
->get();

$jobOpeningsCollection = collect();
foreach ($fulfilledJobOpenings as $jobOpening) {
$team = $jobOpening->team;

$jobOpeningsCollection->push([
'id' => $jobOpening->id,
'title' => $jobOpening->title,
'reference_number' => $jobOpening->reference_number,
'fulfilled_at' => DateHelper::formatDate($jobOpening->fulfilled_at),
'team' => $team ? [
'id' => $team->id,
'name' => $team->name,
'url' => route('team.show', [
'company' => $company,
'team' => $team,
]),
] : null,
'url' => route('dashboard.hr.openings.show', [
'company' => $company,
'jobOpening' => $jobOpening,
]),
]);
}

$countOpenJobOpenings = $company->jobOpenings()->where('fulfilled', false)->count();

return [
'url_create' => route('dashboard.hr.openings.create', [
'company' => $company,
]),
'fulfilled_job_openings' => $jobOpeningsCollection,
'open_job_openings' => [
'count' => $countOpenJobOpenings,
'url' => route('dashboard.hr.openings.index', [
'company' => $company,
]),
],
];
}

/**
* Get all the details about a specific job opening.
*
Expand Down
3 changes: 3 additions & 0 deletions app/Models/Company/JobOpening.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ class JobOpening extends Model
'title',
'description',
'created_at',
'activated_at',
'fulfilled_at',
];

/**
Expand All @@ -47,6 +49,7 @@ class JobOpening extends Model
*/
protected $dates = [
'activated_at',
'fulfilled_at',
];

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ private function markJobOpeningAsFulfilled(): void
{
$this->jobOpening->active = false;
$this->jobOpening->fulfilled = true;
$this->jobOpening->fulfilled_at = Carbon::now();
$this->jobOpening->fulfilled_by_candidate_id = $this->candidate->id;
$this->jobOpening->save();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class AddFulfilledAtDateToJobOpeningTable extends Migration
{
/**
* Run the migrations.
*/
public function up()
{
// necessary for SQLlite
Schema::enableForeignKeyConstraints();

Schema::table('job_openings', function (Blueprint $table) {
$table->datetime('fulfilled_at')->nullable()->after('activated_at');
});
}
}
182 changes: 182 additions & 0 deletions resources/js/Pages/Dashboard/HR/JobOpenings/Fulfilled.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
<style lang="scss" scoped>
.job-opening-item:first-child:hover {
border-top-left-radius: 10px;
border-top-right-radius: 10px;
}
.job-opening-item:last-child {
border-bottom: 0;
&:hover {
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
}
}
.reference-number {
padding: 2px 6px;
border-radius: 6px;
top: -4px;
background-color: #ddf4ff;
color: #0969da;
}
.sidebar {
svg {
width: 20px;
top: 3px;
color: #9da3ae;
}
span {
top: -2px;
}
.active {
background-color: #e5e7ea;
svg {
color: #6c727f;
}
span {
color: #121826;
}
}
}
.dot {
height: 11px;
width: 11px;
top: 3px;
}
.active {
.dot {
background-color: #56bb76;
}
}
.inactive {
.dot {
background-color: #c8d7cd;
}
}
</style>

<template>
<layout :notifications="notifications">
<div class="ph2 ph5-ns">
<breadcrumb
:previous-url="'/' + $page.props.auth.company.id + '/dashboard/hr'"
:previous="$t('app.breadcrumb_hr')"
:custom-class="'mb4'"
>
{{ $t('app.breadcrumb_hr_job_openings_active') }}
</breadcrumb>

<!-- BODY -->
<div class="mw8 center br3 mb5 relative z-1">
<div class="cf center">
<!-- LEFT COLUMN -->
<div class="fl w-20-l w-100">
<!-- sidebar menu -->
<ul class="list ma0 pl0 sidebar">
<!-- open job openings -->
<li class="pa2 br3 relative f6">
<svg class="relative mr2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
<span class="relative">
<inertia-link :href="jobOpenings.open_job_openings.url">{{ $tc('dashboard.job_opening_index_sidebar_open', jobOpenings.open_job_openings.count, {count: jobOpenings.open_job_openings.count}) }}</inertia-link>
</span>
</li>

<!-- fulfilled job openings -->
<li class="pa2 active br3 relative f6">
<svg class="relative mr2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 19a2 2 0 01-2-2V7a2 2 0 012-2h4l2 2h4a2 2 0 012 2v1M5 19h14a2 2 0 002-2v-5a2 2 0 00-2-2H9a2 2 0 00-2 2v5a2 2 0 01-2 2z" />
</svg>
<span class="relative">{{ $tc('dashboard.job_opening_index_sidebar_open', jobOpenings.fulfilled_job_openings.length, {count: jobOpenings.fulfilled_job_openings.length}) }}</span>
</li>
</ul>
</div>

<!-- RIGHT COLUMN -->
<div class="fl w-80-l w-100 pl4-l">
<!-- cta -->
<p class="mt0 db fw5 mb2 flex justify-between items-center">
<span>
{{ $t('dashboard.job_opening_index_fulfilled_title') }}

<help :url="$page.props.help_links.project_messages" :top="'3px'" />
</span>
</p>

<div class="bg-white box mb4">
<!-- list of job opening -->
<ul v-if="jobOpenings.fulfilled_job_openings.length > 0" class="list ma0 pl0">
<li v-for="jobOpening in jobOpenings.fulfilled_job_openings" :key="jobOpening.id" class="pa3 job-opening-item bb bb-gray bb-gray-hover flex items-center">
<!-- title and ref number -->
<div>
<div :class="jobOpening.team ? 'mb2': ''" class="db">
<inertia-link :href="jobOpening.url" class="mr2">{{ jobOpening.title }}</inertia-link>
<span v-if="jobOpening.reference_number" class="reference-number f7 code fw4">{{ jobOpening.reference_number }}</span>
</div>

<ul class="list pl0 ma0 f7 gray">
<li v-if="jobOpening.team" class="di mr3">{{ $t('dashboard.job_opening_index_fulfilled_team') }} <inertia-link :href="jobOpening.team.url">{{ jobOpening.team.name }}</inertia-link></li>
<li class="di">{{ $t('dashboard.job_opening_index_fulfilled_on', { date: jobOpening.fulfilled_at }) }}</li>
</ul>
</div>
</li>
</ul>

<!-- blank state -->
<div v-else>
<p class="tc measure center mb4 lh-copy">
{{ $t('dashboard.job_opening_index_fulfilled_blank') }}
</p>

<img loading="lazy" src="/img/streamline-icon-nomad-freelance-2-5@140x140.png" alt="add email symbol" class="db center mb4" height="80"
width="80"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</layout>
</template>

<script>
import Layout from '@/Shared/Layout';
import Breadcrumb from '@/Shared/Layout/Breadcrumb';
export default {
components: {
Layout,
Breadcrumb,
},
props: {
notifications: {
type: Array,
default: null,
},
jobOpenings: {
type: Object,
default: null,
},
},
mounted() {
if (localStorage.success) {
this.flash(localStorage.success, 'success');
localStorage.removeItem('success');
}
},
};
</script>
4 changes: 4 additions & 0 deletions resources/lang/en/dashboard.php
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,10 @@
'job_opening_index_blank' => 'There are no open job openings at this time.',
'job_opening_index_cta' => 'Create a job opening',
'job_opening_index_title' => 'All the open job openings',
'job_opening_index_fulfilled_title' => 'All the job openings that are closed',
'job_opening_index_fulfilled_blank' => 'There are no closed job openings at this time.',
'job_opening_index_fulfilled_team' => 'Team:',
'job_opening_index_fulfilled_on' => 'Fulfilled on {date}',
'job_opening_show_blank' => 'There are no candidates for now.',
'job_opening_show_toggle_active' => 'Publish job opening',
'job_opening_show_toggle_inactive' => 'Unpublish job opening',
Expand Down
2 changes: 1 addition & 1 deletion routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@

// job openings
Route::get('job-openings', 'Company\\Dashboard\\HR\\DashboardHRJobOpeningController@index')->name('dashboard.hr.openings.index');
Route::get('job-openings/fulfilled', 'Company\\Dashboard\\HR\\DashboardHRJobOpeningController@index')->name('dashboard.hr.openings.index.fulfilled');
Route::get('job-openings/fulfilled', 'Company\\Dashboard\\HR\\DashboardHRJobOpeningController@fulfilled')->name('dashboard.hr.openings.index.fulfilled');
Route::get('job-openings/create', 'Company\\Dashboard\\HR\\DashboardHRJobOpeningController@create')->name('dashboard.hr.openings.create');
Route::get('job-openings/{jobOpening}', 'Company\\Dashboard\\HR\\DashboardHRJobOpeningController@show')->name('dashboard.hr.openings.show');
Route::get('job-openings/{jobOpening}/rejected', 'Company\\Dashboard\\HR\\DashboardHRJobOpeningController@showRejected')->name('dashboard.hr.openings.show.rejected');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,12 @@
use App\Models\Company\Candidate;
use App\Models\Company\JobOpening;
use Illuminate\Support\Facades\Queue;
use App\Models\Company\CandidateStage;
use App\Models\Company\CompanyPTOPolicy;
use App\Models\Company\CandidateStageNote;
use Illuminate\Validation\ValidationException;
use App\Exceptions\NotEnoughPermissionException;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use App\Services\Company\Adminland\JobOpening\HireCandidate;
use App\Services\Company\Adminland\JobOpening\UpdateCandidateStageNote;

class HireCandidateTest extends TestCase
{
Expand Down Expand Up @@ -78,7 +75,7 @@ public function it_fails_if_wrong_parameters_are_given(): void
];

$this->expectException(ValidationException::class);
(new UpdateCandidateStageNote)->execute($request);
(new HireCandidate)->execute($request);
}

/** @test */
Expand All @@ -92,13 +89,6 @@ public function it_fails_if_job_opening_doesnt_belong_to_company(): void
'company_id' => $michael->company_id,
'job_opening_id' => $opening->id,
]);
$candidateStage = CandidateStage::factory()->create([
'candidate_id' => $candidate->id,
'stage_position' => 1,
]);
$candidateStageNote = CandidateStageNote::factory()->create([
'candidate_stage_id' => $candidateStage->id,
]);

$this->executeService($michael, $opening, $candidate);
}
Expand Down Expand Up @@ -128,6 +118,7 @@ private function executeService(Employee $author, JobOpening $opening, Candidate
$this->assertDatabaseHas('job_openings', [
'id' => $opening->id,
'fulfilled_by_candidate_id' => $candidate->id,
'fulfilled_at' => '2018-01-01 00:00:00',
'active' => false,
'fulfilled' => true,
]);
Expand Down

0 comments on commit d61cd34

Please sign in to comment.