From 6d21621a2090c239b756af8919dd1a6ba1b748e6 Mon Sep 17 00:00:00 2001 From: Regis Freyd Date: Mon, 25 Jan 2021 17:55:22 -0500 Subject: [PATCH] feat: HR dashboard for timesheets (#512) --- app/Helpers/TimeHelper.php | 22 +- .../Dashboard/DashboardExpensesController.php | 1 + .../Dashboard/DashboardHRController.php | 43 ++- .../DashboardHRTimesheetController.php | 166 ++++++++++ .../Dashboard/DashboardManagerController.php | 5 +- ...> DashboardManagerTimesheetController.php} | 70 +++- .../Dashboard/DashboardMeController.php | 1 + .../Dashboard/DashboardTeamController.php | 1 + .../DashboardTimesheetController.php | 1 + .../Project/ProjectTasksViewHelper.php | 11 + .../Dashboard/DashboardHRViewHelper.php | 81 +++++ .../Dashboard/DashboardManagerViewHelper.php | 39 +-- .../HR/DashboardHRTimesheetViewHelper.php | 100 ++++++ .../DashboardManagerTimesheetViewHelper.php | 74 +++++ .../Team/TeamRecentShipViewHelper.php | 3 +- resources/js/Pages/Dashboard/HR/Index.vue | 60 ++++ .../Dashboard/HR/Partials/Timesheets.vue | 92 ++++++ .../Pages/Dashboard/HR/Timesheets/Index.vue | 210 ++++++++++++ .../js/Pages/Dashboard/HR/Timesheets/Show.vue | 306 ++++++++++++++++++ .../js/Pages/Dashboard/Manager/Index.vue | 6 +- .../Manager/Partials/TimesheetApprovals.vue | 169 ++-------- .../Dashboard/Manager/Timesheets/Index.vue | 211 ++++++++++++ .../Dashboard/Manager/Timesheets/Show.vue | 306 ++++++++++++++++++ .../Dashboard/Partials/DashboardMenu.vue | 4 +- .../js/Pages/Dashboard/Timesheet/Index.vue | 4 +- resources/lang/en/app.php | 4 + resources/lang/en/dashboard.php | 11 +- resources/lang/en/project.php | 1 + routes/web.php | 44 ++- tests/Unit/Helpers/TimeHelperTest.php | 20 ++ .../Project/ProjectTasksViewHelperTest.php | 6 + .../Dashboard/DashboardHRViewHelperTest.php | 109 +++++++ .../DashboardManagerViewHelperTest.php | 48 +-- .../HR/DashboardHRTimesheetViewHelperTest.php | 114 +++++++ ...ashboardManagerTimesheetViewHelperTest.php | 123 +++++++ .../Team/TeamRecentShipViewHelperTest.php | 22 +- 36 files changed, 2243 insertions(+), 245 deletions(-) create mode 100644 app/Http/Controllers/Company/Dashboard/DashboardHRTimesheetController.php rename app/Http/Controllers/Company/Dashboard/{DashboardTimesheetManagerController.php => DashboardManagerTimesheetController.php} (57%) create mode 100644 app/Http/ViewHelpers/Dashboard/DashboardHRViewHelper.php create mode 100644 app/Http/ViewHelpers/Dashboard/HR/DashboardHRTimesheetViewHelper.php create mode 100644 app/Http/ViewHelpers/Dashboard/Manager/DashboardManagerTimesheetViewHelper.php create mode 100644 resources/js/Pages/Dashboard/HR/Index.vue create mode 100644 resources/js/Pages/Dashboard/HR/Partials/Timesheets.vue create mode 100644 resources/js/Pages/Dashboard/HR/Timesheets/Index.vue create mode 100644 resources/js/Pages/Dashboard/HR/Timesheets/Show.vue create mode 100644 resources/js/Pages/Dashboard/Manager/Timesheets/Index.vue create mode 100644 resources/js/Pages/Dashboard/Manager/Timesheets/Show.vue create mode 100644 tests/Unit/ViewHelpers/Dashboard/DashboardHRViewHelperTest.php create mode 100644 tests/Unit/ViewHelpers/Dashboard/HR/DashboardHRTimesheetViewHelperTest.php create mode 100644 tests/Unit/ViewHelpers/Dashboard/Manager/DashboardManagerTimesheetViewHelperTest.php diff --git a/app/Helpers/TimeHelper.php b/app/Helpers/TimeHelper.php index 47aa30fce..85059e939 100644 --- a/app/Helpers/TimeHelper.php +++ b/app/Helpers/TimeHelper.php @@ -12,7 +12,7 @@ class TimeHelper */ public static function convertToHoursAndMinutes(int $minutes = null): array { - if (! $minutes) { + if (! $minutes || $minutes == 0) { return [ 'hours' => 0, 'minutes' => 0, @@ -31,4 +31,24 @@ public static function convertToHoursAndMinutes(int $minutes = null): array 'minutes' => $minutes, ]; } + + /** + * Gets a sentence representing the time given in the array. + * + * @param array $duration + * @return string + */ + public static function durationInHumanFormat(array $duration): string + { + $minutes = $duration['minutes'] == 0 ? '00' : $duration['minutes']; + + $time = trans('app.duration', [ + 'hours' => $duration['hours'], + 'minutes' => $minutes, + ]); + + $time = str_replace(' ', '', $time); + + return $time; + } } diff --git a/app/Http/Controllers/Company/Dashboard/DashboardExpensesController.php b/app/Http/Controllers/Company/Dashboard/DashboardExpensesController.php index 50389e3bc..cc57ebac7 100644 --- a/app/Http/Controllers/Company/Dashboard/DashboardExpensesController.php +++ b/app/Http/Controllers/Company/Dashboard/DashboardExpensesController.php @@ -39,6 +39,7 @@ public function index(): Response 'dashboard_view' => 'expenses', 'is_manager' => $employee->directReports->count() > 0, 'can_manage_expenses' => $employee->can_manage_expenses, + 'can_manage_hr' => $employee->permission_level <= config('officelife.permission_level.hr'), ]; return Inertia::render('Dashboard/Expenses/Index', [ diff --git a/app/Http/Controllers/Company/Dashboard/DashboardHRController.php b/app/Http/Controllers/Company/Dashboard/DashboardHRController.php index 276067409..71b27a7a8 100644 --- a/app/Http/Controllers/Company/Dashboard/DashboardHRController.php +++ b/app/Http/Controllers/Company/Dashboard/DashboardHRController.php @@ -2,18 +2,53 @@ namespace App\Http\Controllers\Company\Dashboard; +use Inertia\Inertia; use Illuminate\Http\Request; -use App\Models\Company\Company; +use App\Helpers\InstanceHelper; +use App\Helpers\NotificationHelper; use App\Http\Controllers\Controller; +use App\Jobs\UpdateDashboardPreference; +use App\Http\ViewHelpers\Dashboard\DashboardHRViewHelper; class DashboardHRController extends Controller { /** - * Company details. + * Index of the HR tab on the dashboard. * - * @param Request $request + * @return mixed */ - public function index(Request $request): void + public function index(Request $request) { + $company = InstanceHelper::getLoggedCompany(); + $employee = InstanceHelper::getLoggedEmployee(); + + // is this person HR? + if ($employee->permission_level > config('officelife.permission_level.hr')) { + return redirect('home'); + } + + UpdateDashboardPreference::dispatch([ + 'employee_id' => $employee->id, + 'company_id' => $company->id, + 'view' => 'hr', + ])->onQueue('low'); + + $employeeInformation = [ + 'id' => $employee->id, + 'dashboard_view' => 'hr', + 'is_manager' => true, + 'can_manage_expenses' => $employee->can_manage_expenses, + 'can_manage_hr' => $employee->permission_level <= config('officelife.permission_level.hr'), + ]; + + $employeesWithoutManagersWithPendingTimesheets = DashboardHRViewHelper::employeesWithoutManagersWithPendingTimesheets($company); + $statisticsAboutTimesheets = DashboardHRViewHelper::statisticsAboutTimesheets($company); + + return Inertia::render('Dashboard/HR/Index', [ + 'employee' => $employeeInformation, + 'notifications' => NotificationHelper::getNotifications($employee), + 'employeesWithoutManagersWithPendingTimesheets' => $employeesWithoutManagersWithPendingTimesheets, + 'statisticsAboutTimesheets' => $statisticsAboutTimesheets, + ]); } } diff --git a/app/Http/Controllers/Company/Dashboard/DashboardHRTimesheetController.php b/app/Http/Controllers/Company/Dashboard/DashboardHRTimesheetController.php new file mode 100644 index 000000000..de566cf7a --- /dev/null +++ b/app/Http/Controllers/Company/Dashboard/DashboardHRTimesheetController.php @@ -0,0 +1,166 @@ +permission_level > config('officelife.permission_level.hr')) { + return redirect('home'); + } + + $employees = DashboardHRTimesheetViewHelper::timesheetApprovalsForEmployeesWithoutManagers($company); + + return Inertia::render('Dashboard/HR/Timesheets/Index', [ + 'employee' => [ + 'id' => $employee->id, + ], + 'notifications' => NotificationHelper::getNotifications($employee), + 'employees' => $employees, + ]); + } + + /** + * Show the timesheet to validate. + * + * @param Request $request + * @param int $companyId + * @param int $timesheetId + * + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|Response + */ + public function show(Request $request, int $companyId, int $timesheetId) + { + $company = InstanceHelper::getLoggedCompany(); + $employee = InstanceHelper::getLoggedEmployee(); + + $timesheet = $this->canAccess($company, $timesheetId, $employee); + + $timesheetInformation = DashboardTimesheetViewHelper::show($timesheet); + $daysInHeader = DashboardTimesheetViewHelper::daysHeader($timesheet); + $approverInformation = DashboardTimesheetViewHelper::approverInformation($timesheet); + + return Inertia::render('Dashboard/HR/Timesheets/Show', [ + 'employee' => [ + 'id' => $employee->id, + 'name' => $employee->name, + ], + 'daysHeader' => $daysInHeader, + 'timesheet' => $timesheetInformation, + 'approverInformation' => $approverInformation, + 'notifications' => NotificationHelper::getNotifications($employee), + ]); + } + + /** + * Approve the timesheet. + * + * @param Request $request + * @param int $companyId + * @param int $timesheetId + * @return JsonResponse + */ + public function approve(Request $request, int $companyId, int $timesheetId): JsonResponse + { + $company = InstanceHelper::getLoggedCompany(); + $employee = InstanceHelper::getLoggedEmployee(); + + $timesheet = $this->canAccess($company, $timesheetId, $employee); + + $data = [ + 'company_id' => $company->id, + 'author_id' => $employee->id, + 'employee_id' => $timesheet->employee->id, + 'timesheet_id' => $timesheetId, + ]; + + $timesheet = (new ApproveTimesheet)->execute($data); + + return response()->json([ + 'data' => $timesheet->id, + ], 201); + } + + /** + * Reject the timesheet. + * + * @param Request $request + * @param int $companyId + * @param int $timesheetId + * @return JsonResponse + */ + public function reject(Request $request, int $companyId, int $timesheetId): JsonResponse + { + $company = InstanceHelper::getLoggedCompany(); + $employee = InstanceHelper::getLoggedEmployee(); + + $timesheet = $this->canAccess($company, $timesheetId, $employee); + + $data = [ + 'company_id' => $company->id, + 'author_id' => $employee->id, + 'employee_id' => $timesheet->employee->id, + 'timesheet_id' => $timesheetId, + ]; + + $timesheet = (new RejectTimesheet)->execute($data); + + return response()->json([ + 'data' => $timesheet->id, + ], 201); + } + + /** + * Check that the current employee has access to this method. + * @param Company $company + * @param int $timesheetId + * @param Employee $employee + * @return mixed + */ + private function canAccess(Company $company, int $timesheetId, Employee $employee) + { + try { + $timesheet = Timesheet::where('company_id', $company->id) + ->findOrFail($timesheetId); + } catch (ModelNotFoundException $e) { + return redirect('home'); + } + + if ($timesheet->status !== Timesheet::READY_TO_SUBMIT) { + return redirect('home'); + } + + // is this person HR? + if ($employee->permission_level > config('officelife.permission_level.hr')) { + return redirect('home'); + } + + return $timesheet; + } +} diff --git a/app/Http/Controllers/Company/Dashboard/DashboardManagerController.php b/app/Http/Controllers/Company/Dashboard/DashboardManagerController.php index c950b477d..0b30a31c3 100644 --- a/app/Http/Controllers/Company/Dashboard/DashboardManagerController.php +++ b/app/Http/Controllers/Company/Dashboard/DashboardManagerController.php @@ -57,12 +57,13 @@ public function index() 'dashboard_view' => 'manager', 'is_manager' => true, 'can_manage_expenses' => $employee->can_manage_expenses, + 'can_manage_hr' => $employee->permission_level <= config('officelife.permission_level.hr'), ]; $pendingExpenses = DashboardManagerViewHelper::pendingExpenses($employee, $directReports); $oneOnOnes = DashboardManagerViewHelper::oneOnOnes($employee, $directReports); $contractRenewals = DashboardManagerViewHelper::contractRenewals($employee, $directReports); - $timesheetApprovals = DashboardManagerViewHelper::timesheetApprovals($employee, $directReports); + $timesheetsStats = DashboardManagerViewHelper::employeesWithTimesheetsToApprove($employee, $directReports); return Inertia::render('Dashboard/Manager/Index', [ 'employee' => $employeeInformation, @@ -70,7 +71,7 @@ public function index() 'pendingExpenses' => $pendingExpenses, 'oneOnOnes' => $oneOnOnes, 'contractRenewals' => $contractRenewals, - 'timesheetApprovals' => $timesheetApprovals, + 'timesheetsStats' => $timesheetsStats, 'defaultCompanyCurrency' => $company->currency, ]); } diff --git a/app/Http/Controllers/Company/Dashboard/DashboardTimesheetManagerController.php b/app/Http/Controllers/Company/Dashboard/DashboardManagerTimesheetController.php similarity index 57% rename from app/Http/Controllers/Company/Dashboard/DashboardTimesheetManagerController.php rename to app/Http/Controllers/Company/Dashboard/DashboardManagerTimesheetController.php index c6b2ba9cb..ac725bf9d 100644 --- a/app/Http/Controllers/Company/Dashboard/DashboardTimesheetManagerController.php +++ b/app/Http/Controllers/Company/Dashboard/DashboardManagerTimesheetController.php @@ -2,6 +2,7 @@ namespace App\Http\Controllers\Company\Dashboard; +use Inertia\Inertia; use Inertia\Response; use Illuminate\Http\Request; use App\Helpers\InstanceHelper; @@ -9,14 +10,81 @@ use App\Models\Company\Employee; use App\Models\Company\Timesheet; use Illuminate\Http\JsonResponse; +use App\Helpers\NotificationHelper; use App\Http\Controllers\Controller; use App\Models\Company\DirectReport; use Illuminate\Database\Eloquent\ModelNotFoundException; use App\Services\Company\Employee\Timesheet\RejectTimesheet; use App\Services\Company\Employee\Timesheet\ApproveTimesheet; +use App\Http\ViewHelpers\Dashboard\DashboardTimesheetViewHelper; +use App\Http\ViewHelpers\Dashboard\Manager\DashboardManagerTimesheetViewHelper; -class DashboardTimesheetManagerController extends Controller +class DashboardManagerTimesheetController extends Controller { + /** + * Show the list of timesheets to validate. + * + * @return mixed + */ + public function index() + { + $company = InstanceHelper::getLoggedCompany(); + $employee = InstanceHelper::getLoggedEmployee(); + + // is the user a manager? + $directReports = DirectReport::where('company_id', $company->id) + ->where('manager_id', $employee->id) + ->with('directReport') + ->with('directReport.timesheets') + ->get(); + + if ($directReports->count() == 0) { + return redirect('home'); + } + + $timesheetApprovals = DashboardManagerTimesheetViewHelper::timesheetApprovals($employee, $directReports); + + return Inertia::render('Dashboard/Manager/Timesheets/Index', [ + 'employee' => [ + 'id' => $employee->id, + ], + 'notifications' => NotificationHelper::getNotifications($employee), + 'directReports' => $timesheetApprovals, + ]); + } + + /** + * Show the timesheet to validate. + * + * @param Request $request + * @param int $companyId + * @param int $timesheetId + * + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|Response + */ + public function show(Request $request, int $companyId, int $timesheetId) + { + $company = InstanceHelper::getLoggedCompany(); + $employee = InstanceHelper::getLoggedEmployee(); + + $timesheet = $this->canAccess($company, $timesheetId, $employee); + + $timesheetInformation = DashboardTimesheetViewHelper::show($timesheet); + $daysInHeader = DashboardTimesheetViewHelper::daysHeader($timesheet); + $approverInformation = DashboardTimesheetViewHelper::approverInformation($timesheet); + + return Inertia::render('Dashboard/Manager/Timesheets/Show', [ + 'employee' => [ + 'id' => $employee->id, + 'name' => $employee->name, + ], + 'daysHeader' => $daysInHeader, + 'timesheet' => $timesheetInformation, + 'approverInformation' => $approverInformation, + 'notifications' => NotificationHelper::getNotifications($employee), + ]); + } + /** * Approve the timesheet. * diff --git a/app/Http/Controllers/Company/Dashboard/DashboardMeController.php b/app/Http/Controllers/Company/Dashboard/DashboardMeController.php index 69ba5f3d0..dfa68f1a5 100644 --- a/app/Http/Controllers/Company/Dashboard/DashboardMeController.php +++ b/app/Http/Controllers/Company/Dashboard/DashboardMeController.php @@ -42,6 +42,7 @@ public function index(): Response 'is_manager' => $employee->directReports->count() > 0, 'has_worked_from_home_today' => WorkFromHomeHelper::hasWorkedFromHomeOnDate($employee, Carbon::now()), 'question' => DashboardMeViewHelper::question($employee), + 'can_manage_hr' => $employee->permission_level <= config('officelife.permission_level.hr'), ]; $defaultCompanyCurrency = [ diff --git a/app/Http/Controllers/Company/Dashboard/DashboardTeamController.php b/app/Http/Controllers/Company/Dashboard/DashboardTeamController.php index 173e692db..d4f0859c2 100644 --- a/app/Http/Controllers/Company/Dashboard/DashboardTeamController.php +++ b/app/Http/Controllers/Company/Dashboard/DashboardTeamController.php @@ -51,6 +51,7 @@ public function index(Request $request, int $companyId, int $teamId = null, $req 'dashboard_view' => 'team', 'can_manage_expenses' => $employee->can_manage_expenses, 'is_manager' => $employee->directReports->count() > 0, + 'can_manage_hr' => $employee->permission_level <= config('officelife.permission_level.hr'), ]; UpdateDashboardPreference::dispatch([ diff --git a/app/Http/Controllers/Company/Dashboard/DashboardTimesheetController.php b/app/Http/Controllers/Company/Dashboard/DashboardTimesheetController.php index 5ff344925..a026a6b71 100644 --- a/app/Http/Controllers/Company/Dashboard/DashboardTimesheetController.php +++ b/app/Http/Controllers/Company/Dashboard/DashboardTimesheetController.php @@ -43,6 +43,7 @@ public function index(): Response 'dashboard_view' => 'timesheet', 'can_manage_expenses' => $employee->can_manage_expenses, 'is_manager' => $employee->directReports->count() > 0, + 'can_manage_hr' => $employee->permission_level <= config('officelife.permission_level.hr'), ]; $currentTimesheet = (new CreateOrGetTimesheet)->execute([ diff --git a/app/Http/ViewHelpers/Company/Project/ProjectTasksViewHelper.php b/app/Http/ViewHelpers/Company/Project/ProjectTasksViewHelper.php index dad0af607..4c9828cf4 100644 --- a/app/Http/ViewHelpers/Company/Project/ProjectTasksViewHelper.php +++ b/app/Http/ViewHelpers/Company/Project/ProjectTasksViewHelper.php @@ -3,6 +3,7 @@ namespace App\Http\ViewHelpers\Company\Project; use App\Helpers\DateHelper; +use App\Helpers\TimeHelper; use App\Models\Company\Company; use App\Models\Company\Project; use Illuminate\Support\Collection; @@ -25,6 +26,7 @@ public static function index(Project $project): array ->with('list') ->with('assignee') ->with('author') + ->with('timeTrackingEntries') ->get(); // the goal of the following is to first display tasks without lists, @@ -82,10 +84,18 @@ public static function show(ProjectTask $task, Company $company): array return self::getTaskInfo($task, $company); } + /** + * Internal method used to populate the project information. + * + * @param ProjectTask $task + * @param Company $company + * @return array + */ private static function getTaskInfo(ProjectTask $task, Company $company): array { $author = $task->author; $assignee = $task->assignee; + $duration = TimeHelper::convertToHoursAndMinutes($task->timeTrackingEntries()->sum('duration')); return [ 'id' => $task->id, @@ -93,6 +103,7 @@ private static function getTaskInfo(ProjectTask $task, Company $company): array 'description' => $task->description, 'completed' => $task->completed, 'completed_at' => $task->completed_at ? DateHelper::formatDate($task->completed_at) : null, + 'duration' => TimeHelper::durationInHumanFormat($duration), 'author' => $author ? [ 'id' => $author->id, 'name' => $author->name, diff --git a/app/Http/ViewHelpers/Dashboard/DashboardHRViewHelper.php b/app/Http/ViewHelpers/Dashboard/DashboardHRViewHelper.php new file mode 100644 index 000000000..b94cffcf6 --- /dev/null +++ b/app/Http/ViewHelpers/Dashboard/DashboardHRViewHelper.php @@ -0,0 +1,81 @@ +id) + ->where('status', Timesheet::READY_TO_SUBMIT) + ->with('employee') + ->whereDate('started_at', '<', Carbon::now()->startOfWeek(Carbon::MONDAY)) + ->get(); + + // get the list of employees with manager, that we flatten + /** @phpstan-ignore-next-line */ + $listOfEmployeesWithManagers = DB::table('direct_reports') + ->where('company_id', $company->id) + ->select('employee_id') + ->get() + ->pluck('employee_id') + ->toArray(); + + $timesheetsWithUniqueEmployees = $timesheets->unique('employee_id'); + $timesheetsWithUniqueEmployees = $timesheetsWithUniqueEmployees->whereNotIn('employee_id', $listOfEmployeesWithManagers); + $timesheetsLeft = $timesheets->whereNotIn('employee_id', $listOfEmployeesWithManagers); + + $employeesCollection = collect([]); + foreach ($timesheetsWithUniqueEmployees as $timesheet) { + $employee = $timesheet->employee; + + $employeesCollection->push([ + 'id' => $employee->id, + 'name' => $employee->name, + 'avatar' => $employee->avatar, + ]); + } + + return [ + 'number_of_timesheets' => $timesheetsLeft->count(), + 'employees' => $employeesCollection, + 'url_view_all' => route('dashboard.hr.timesheet.index', [ + 'company' => $company, + ]), + ]; + } + + public static function statisticsAboutTimesheets(Company $company) + { + $totals = DB::table('timesheets') + ->whereDate('started_at', '>=', Carbon::now()->startOfWeek(Carbon::MONDAY)->subDays(30)) + ->whereDate('started_at', '<', Carbon::now()->startOfWeek(Carbon::MONDAY)) + ->selectRaw('count(*) as total') + ->selectRaw("count(case when status = '".Timesheet::REJECTED."' then 1 end) as rejected") + ->first(); + + return [ + 'total' => $totals->total, + 'rejected' => $totals->rejected, + ]; + } +} diff --git a/app/Http/ViewHelpers/Dashboard/DashboardManagerViewHelper.php b/app/Http/ViewHelpers/Dashboard/DashboardManagerViewHelper.php index 05a4c8171..c2de62bad 100644 --- a/app/Http/ViewHelpers/Dashboard/DashboardManagerViewHelper.php +++ b/app/Http/ViewHelpers/Dashboard/DashboardManagerViewHelper.php @@ -4,12 +4,10 @@ use Carbon\Carbon; use App\Helpers\DateHelper; -use App\Helpers\TimeHelper; use App\Helpers\MoneyHelper; use App\Models\Company\Expense; use App\Models\Company\Employee; use App\Models\Company\Timesheet; -use Illuminate\Support\Facades\DB; use App\Models\Company\OneOnOneEntry; use App\Models\Company\EmployeeStatus; use Illuminate\Database\Eloquent\Collection; @@ -216,16 +214,17 @@ public static function contractRenewals(Employee $manager, Collection $directRep } /** - * Get the information about timesheets that need approval. + * Get the list of employees who have timesheets to approve by this manager. * * @param Employee $manager * @param Collection $directReports - * @return SupportCollection|null + * @return array|null */ - public static function timesheetApprovals(Employee $manager, Collection $directReports): ?SupportCollection + public static function employeesWithTimesheetsToApprove(Employee $manager, Collection $directReports): ?array { $employeesCollection = collect([]); $company = $manager->company; + $totalNumberOfTimesheetsToValidate = 0; foreach ($directReports as $directReport) { $employee = $directReport->directReport; @@ -235,40 +234,26 @@ public static function timesheetApprovals(Employee $manager, Collection $directR ->orderBy('started_at', 'desc') ->get(); - $timesheetCollection = collect([]); - foreach ($pendingTimesheets as $timesheet) { - $totalWorkedInMinutes = DB::table('time_tracking_entries') - ->where('timesheet_id', $timesheet->id) - ->sum('duration'); - - $arrayOfTime = TimeHelper::convertToHoursAndMinutes($totalWorkedInMinutes); - - $timesheetCollection->push([ - 'id' => $timesheet->id, - 'started_at' => DateHelper::formatDate($timesheet->started_at), - 'ended_at' => DateHelper::formatDate($timesheet->ended_at), - 'duration' => trans('dashboard.manager_timesheet_approval_duration', [ - 'hours' => $arrayOfTime['hours'], - 'minutes' => $arrayOfTime['minutes'], - ]), - ]); - } + $totalNumberOfTimesheetsToValidate += $pendingTimesheets->count(); if ($pendingTimesheets->count() !== 0) { $employeesCollection->push([ 'id' => $employee->id, - 'name' => $employee->name, 'avatar' => $employee->avatar, - 'position' => (! $employee->position) ? null : $employee->position->title, 'url' => route('employees.show', [ 'company' => $company, 'employee' => $employee, ]), - 'timesheets' => $timesheetCollection, ]); } } - return $employeesCollection; + return [ + 'totalNumberOfTimesheetsToValidate' => $totalNumberOfTimesheetsToValidate, + 'employees' => $employeesCollection, + 'url_view_all'=> route('dashboard.manager.timesheet.index', [ + 'company' => $manager->company, + ]), + ]; } } diff --git a/app/Http/ViewHelpers/Dashboard/HR/DashboardHRTimesheetViewHelper.php b/app/Http/ViewHelpers/Dashboard/HR/DashboardHRTimesheetViewHelper.php new file mode 100644 index 000000000..3f72a1123 --- /dev/null +++ b/app/Http/ViewHelpers/Dashboard/HR/DashboardHRTimesheetViewHelper.php @@ -0,0 +1,100 @@ +id) + ->where('status', Timesheet::READY_TO_SUBMIT) + ->with('employee') + ->whereDate('started_at', '<', Carbon::now()->startOfWeek(Carbon::MONDAY)) + ->get(); + + // get the list of employees with manager, that we flatten + /** @phpstan-ignore-next-line */ + $listOfEmployeesWithManagers = DB::table('direct_reports') + ->where('company_id', $company->id) + ->select('employee_id') + ->get() + ->pluck('employee_id') + ->toArray(); + + $timesheetsWithUniqueEmployees = $timesheets->unique('employee_id'); + $timesheetsWithUniqueEmployees = $timesheetsWithUniqueEmployees->whereNotIn('employee_id', $listOfEmployeesWithManagers); + + $uniqueEmployeeCollection = collect([]); + foreach ($timesheetsWithUniqueEmployees as $timesheet) { + $employee = $timesheet->employee; + $uniqueEmployeeCollection->push($employee); + } + + $employeesCollection = collect([]); + foreach ($uniqueEmployeeCollection as $employee) { + $pendingTimesheets = $employee->timesheets() + ->where('status', Timesheet::READY_TO_SUBMIT) + ->orderBy('started_at', 'desc') + ->get(); + + $timesheetCollection = collect([]); + foreach ($pendingTimesheets as $timesheet) { + $totalWorkedInMinutes = DB::table('time_tracking_entries') + ->where('timesheet_id', $timesheet->id) + ->sum('duration'); + + $arrayOfTime = TimeHelper::convertToHoursAndMinutes($totalWorkedInMinutes); + + $timesheetCollection->push([ + 'id' => $timesheet->id, + 'started_at' => DateHelper::formatDate($timesheet->started_at), + 'ended_at' => DateHelper::formatDate($timesheet->ended_at), + 'duration' => trans('dashboard.manager_timesheet_approval_duration', [ + 'hours' => $arrayOfTime['hours'], + 'minutes' => $arrayOfTime['minutes'], + ]), + 'url' => route('dashboard.hr.timesheet.show', [ + 'company' => $company, + 'timesheet' => $timesheet, + ]), + ]); + } + + if ($pendingTimesheets->count() !== 0) { + $employeesCollection->push([ + 'id' => $employee->id, + 'name' => $employee->name, + 'avatar' => $employee->avatar, + 'position' => (! $employee->position) ? null : $employee->position->title, + 'url' => route('employees.show', [ + 'company' => $company, + 'employee' => $employee, + ]), + 'timesheets' => $timesheetCollection, + ]); + } + } + + return $employeesCollection; + } +} diff --git a/app/Http/ViewHelpers/Dashboard/Manager/DashboardManagerTimesheetViewHelper.php b/app/Http/ViewHelpers/Dashboard/Manager/DashboardManagerTimesheetViewHelper.php new file mode 100644 index 000000000..c4f6abf36 --- /dev/null +++ b/app/Http/ViewHelpers/Dashboard/Manager/DashboardManagerTimesheetViewHelper.php @@ -0,0 +1,74 @@ +company; + + foreach ($directReports as $directReport) { + $employee = $directReport->directReport; + + $pendingTimesheets = $employee->timesheets() + ->where('status', Timesheet::READY_TO_SUBMIT) + ->orderBy('started_at', 'desc') + ->get(); + + $timesheetCollection = collect([]); + foreach ($pendingTimesheets as $timesheet) { + $totalWorkedInMinutes = DB::table('time_tracking_entries') + ->where('timesheet_id', $timesheet->id) + ->sum('duration'); + + $arrayOfTime = TimeHelper::convertToHoursAndMinutes($totalWorkedInMinutes); + + $timesheetCollection->push([ + 'id' => $timesheet->id, + 'started_at' => DateHelper::formatDate($timesheet->started_at), + 'ended_at' => DateHelper::formatDate($timesheet->ended_at), + 'duration' => trans('dashboard.manager_timesheet_approval_duration', [ + 'hours' => $arrayOfTime['hours'], + 'minutes' => $arrayOfTime['minutes'], + ]), + 'url' => route('dashboard.manager.timesheet.show', [ + 'company' => $company, + 'timesheet' => $timesheet, + ]), + ]); + } + + if ($pendingTimesheets->count() !== 0) { + $employeesCollection->push([ + 'id' => $employee->id, + 'name' => $employee->name, + 'avatar' => $employee->avatar, + 'position' => (! $employee->position) ? null : $employee->position->title, + 'url' => route('employees.show', [ + 'company' => $company, + 'employee' => $employee, + ]), + 'timesheets' => $timesheetCollection, + ]); + } + } + + return $employeesCollection; + } +} diff --git a/app/Http/ViewHelpers/Team/TeamRecentShipViewHelper.php b/app/Http/ViewHelpers/Team/TeamRecentShipViewHelper.php index 4e0bea779..ee26a8dbd 100644 --- a/app/Http/ViewHelpers/Team/TeamRecentShipViewHelper.php +++ b/app/Http/ViewHelpers/Team/TeamRecentShipViewHelper.php @@ -19,7 +19,8 @@ class TeamRecentShipViewHelper */ public static function recentShips(Team $team): Collection { - $ships = $team->ships()->with('employees')->get(); + $ships = $team->ships()->orderBy('id', 'desc')->with('employees')->get(); + $shipsCollection = collect([]); foreach ($ships as $ship) { $employees = $ship->employees; diff --git a/resources/js/Pages/Dashboard/HR/Index.vue b/resources/js/Pages/Dashboard/HR/Index.vue new file mode 100644 index 000000000..a8beaa723 --- /dev/null +++ b/resources/js/Pages/Dashboard/HR/Index.vue @@ -0,0 +1,60 @@ + + + + + diff --git a/resources/js/Pages/Dashboard/HR/Partials/Timesheets.vue b/resources/js/Pages/Dashboard/HR/Partials/Timesheets.vue new file mode 100644 index 000000000..7520ec334 --- /dev/null +++ b/resources/js/Pages/Dashboard/HR/Partials/Timesheets.vue @@ -0,0 +1,92 @@ + + + + + diff --git a/resources/js/Pages/Dashboard/HR/Timesheets/Index.vue b/resources/js/Pages/Dashboard/HR/Timesheets/Index.vue new file mode 100644 index 000000000..d41268862 --- /dev/null +++ b/resources/js/Pages/Dashboard/HR/Timesheets/Index.vue @@ -0,0 +1,210 @@ + + + + + diff --git a/resources/js/Pages/Dashboard/HR/Timesheets/Show.vue b/resources/js/Pages/Dashboard/HR/Timesheets/Show.vue new file mode 100644 index 000000000..336b72771 --- /dev/null +++ b/resources/js/Pages/Dashboard/HR/Timesheets/Show.vue @@ -0,0 +1,306 @@ + + + + + diff --git a/resources/js/Pages/Dashboard/Manager/Index.vue b/resources/js/Pages/Dashboard/Manager/Index.vue index 68951e93c..b07f876b4 100644 --- a/resources/js/Pages/Dashboard/Manager/Index.vue +++ b/resources/js/Pages/Dashboard/Manager/Index.vue @@ -12,7 +12,7 @@ -.entry-item:first-child { - border-top-width: 1px; - border-top-style: solid; - border-top-left-radius: 3px; - border-top-right-radius: 3px; -} - -.entry-item:last-child { - border-bottom-left-radius: 3px; - border-bottom-right-radius: 3px; - margin-bottom: 20px; -} - -.direct-report-item:not(:first-child) { - margin-top: 30px; -} - -.avatar { - left: 1px; - top: 5px; - width: 35px; +.top-1 { + top: 20px; } -.team-member { - padding-left: 44px; - - .avatar { - top: 2px; - } +.small-avatar { + margin-left: -8px; + box-shadow: 0 0 0 2px #fff; } -.top-1 { - top: 20px; +.all-avatars { + left: 10px; } @@ -47,48 +25,36 @@
-
- no timesheets to validate +
+ no timesheets to validate

{{ $t('dashboard.manager_timesheet_blank_state') }}

-
- meeting - -
    -
  • - -
    - - - avatar - {{ directReport.name }} - - {{ directReport.position }} - - +
    + +
    + meeting + +

    + {{ $t('dashboard.manager_timesheet_summary_count', { count: timesheetsStats.totalNumberOfTimesheetsToValidate}) }} +

    + + +
    +
    + avatar
    +
    +
    - -
      -
    • - -
      -

      {{ timesheet.started_at }} → {{ timesheet.ended_at }}

      -

      {{ timesheet.duration }}

      -
      - - -
      - - -
      -
    • -
    -
  • -
+ + {{ $t('app.view') }}
@@ -96,12 +62,10 @@ diff --git a/resources/js/Pages/Dashboard/Manager/Timesheets/Index.vue b/resources/js/Pages/Dashboard/Manager/Timesheets/Index.vue new file mode 100644 index 000000000..21df43700 --- /dev/null +++ b/resources/js/Pages/Dashboard/Manager/Timesheets/Index.vue @@ -0,0 +1,211 @@ + + + + + diff --git a/resources/js/Pages/Dashboard/Manager/Timesheets/Show.vue b/resources/js/Pages/Dashboard/Manager/Timesheets/Show.vue new file mode 100644 index 000000000..26971691e --- /dev/null +++ b/resources/js/Pages/Dashboard/Manager/Timesheets/Show.vue @@ -0,0 +1,306 @@ + + + + + diff --git a/resources/js/Pages/Dashboard/Partials/DashboardMenu.vue b/resources/js/Pages/Dashboard/Partials/DashboardMenu.vue index 96413f3f8..57ff1e3a2 100644 --- a/resources/js/Pages/Dashboard/Partials/DashboardMenu.vue +++ b/resources/js/Pages/Dashboard/Partials/DashboardMenu.vue @@ -16,8 +16,8 @@ 🔒 {{ $t('dashboard.tab_expenses') }} - - HR area + + 🔒 {{ $t('dashboard.tab_hr') }} diff --git a/resources/js/Pages/Dashboard/Timesheet/Index.vue b/resources/js/Pages/Dashboard/Timesheet/Index.vue index b399c39df..501d81ce2 100644 --- a/resources/js/Pages/Dashboard/Timesheet/Index.vue +++ b/resources/js/Pages/Dashboard/Timesheet/Index.vue @@ -55,8 +55,8 @@

⚠️ {{ $t('dashboard.timesheet_rejected_timesheets') }}

    -
  • - {{ timesheet.started_at }} +
  • + {{ timesheetItem.started_at }}
diff --git a/resources/lang/en/app.php b/resources/lang/en/app.php index 4d33aadd8..85f1f907f 100644 --- a/resources/lang/en/app.php +++ b/resources/lang/en/app.php @@ -54,6 +54,7 @@ 'hide_help' => 'Hide help', 'breadcrumb_dashboard' => 'Home', + 'breadcrumb_dashboard_hr' => 'Human Resources', 'breadcrumb_dashboard_manager' => 'Manager', 'breadcrumb_dashboard_manager_expense_details' => 'Expense details', 'breadcrumb_account_home' => 'Account administration', @@ -102,6 +103,7 @@ 'breadcrumb_project_create_message' => 'Add a new message', 'breadcrumb_project_edit_message' => 'Edit message', 'breadcrumb_dashboard_one_on_one' => 'One on One', + 'breadcrumb_dashboard_manager_timesheets' => 'All timesheets', 'breadcrumb_team_list' => 'All teams', 'breadcrumb_team_show_team_news' => 'Team news', 'breadcrumb_team_add_team_news' => 'Add a team news', @@ -144,4 +146,6 @@ 'rate_manager_bad' => 'Not ideal', 'rate_manager_average' => 'It’s going well', 'rate_manager_good' => 'Simply great', + + 'duration' => ':hours h :minutes', ]; diff --git a/resources/lang/en/dashboard.php b/resources/lang/en/dashboard.php index b633b4ead..e21693ed9 100644 --- a/resources/lang/en/dashboard.php +++ b/resources/lang/en/dashboard.php @@ -5,8 +5,9 @@ 'tab_my_team' => 'Your team', 'tab_expenses' => 'Accountant area', 'tab_manager' => 'Manager area', + 'tab_hr' => 'HR area', - 'blank_state' => 'You are not associated with a team at the moment.', + 'dstate' => 'You are not associated with a team at the moment.', 'morale_title' => 'How do you feel?', 'morale_success_message' => 'Thanks for telling us how you feel.', @@ -147,6 +148,9 @@ 'manager_timesheet_approval_duration' => ':hours h :minutes', 'manager_timesheet_approved' => 'The timesheet has been approved', 'manager_timesheet_rejected' => 'The timesheet has been rejected', + 'manager_timesheet_view_details' => 'View details', + 'manager_timesheet_summary_count' => 'You have {count} timesheets to approve.', + 'manager_timesheet_index_title' => 'Timesheets of your direct reports to approve', 'accounting_expense_detail_cta' => 'Accept or reject this expense', 'accounting_expense_detail_expense_section' => 'Expense details', @@ -220,4 +224,9 @@ 'timesheet_create_project' => 'Create a project', 'timesheet_create_choose_project' => 'Choose a project', 'timesheet_create_choose_task' => 'Choose a task', + + 'hr_timesheets_title' => 'Timesheets', + 'hr_timesheet_index_title' => 'All the timesheets that need approvals', + 'hr_timesheet_summary_count' => '{count} timesheets to approve for employees who don’t have a manager', + 'hr_timesheet_summary_blank' => 'There are no timesheets to approve for now.', ]; diff --git a/resources/lang/en/project.php b/resources/lang/en/project.php index 6f68aaef7..4ca39497d 100644 --- a/resources/lang/en/project.php +++ b/resources/lang/en/project.php @@ -125,4 +125,5 @@ 'task_list_create_success' => 'The task list has been created.', 'task_list_update_success' => 'The task list has been updated.', 'task_list_destroy_success' => 'The task list has been deleted.', + 'task_item_duration' => ':hours h :minutes', ]; diff --git a/routes/web.php b/routes/web.php index e2c6dddda..a29cb3b81 100644 --- a/routes/web.php +++ b/routes/web.php @@ -61,9 +61,6 @@ // company Route::get('company', 'Company\\Dashboard\\DashboardCompanyController@index')->name('dashboard.company'); - // hr - Route::get('hr', 'Company\\Dashboard\\DashboardHRController@index')->name('dashboard.hr'); - // timesheet Route::get('timesheet/projects', 'Company\\Dashboard\\DashboardTimesheetController@projects')->name('dashboard.timesheet.projects'); Route::get('timesheet/{timesheet}/projects/{project}/tasks', 'Company\\Dashboard\\DashboardTimesheetController@tasks')->name('dashboard.timesheet.projects'); @@ -79,18 +76,6 @@ Route::get('team/{team}', 'Company\\Dashboard\\DashboardTeamController@index'); Route::get('team/{team}/{date}', 'Company\\Dashboard\\DashboardTeamController@worklogDetails'); - // manager - Route::get('manager', 'Company\\Dashboard\\DashboardManagerController@index')->name('dashboard.manager'); - Route::get('manager/expenses/{expense}', 'Company\\Dashboard\\DashboardManagerController@showExpense')->name('dashboard.manager.expense.show'); - Route::post('manager/expenses/{expense}/accept', 'Company\\Dashboard\\DashboardManagerController@accept'); - Route::post('manager/expenses/{expense}/reject', 'Company\\Dashboard\\DashboardManagerController@reject'); - Route::post('manager/timesheets/{timesheet}/approve', 'Company\\Dashboard\\DashboardTimesheetManagerController@approve'); - Route::post('manager/timesheets/{timesheet}/reject', 'Company\\Dashboard\\DashboardTimesheetManagerController@reject'); - - // rate your manager - Route::post('manager/rate/{answer}', 'Company\\Dashboard\\DashboardRateYourManagerController@store'); - Route::post('manager/rate/{answer}/comment', 'Company\\Dashboard\\DashboardRateYourManagerController@storeComment'); - // details of one on ones Route::get('oneonones/{entry}', 'Company\\Dashboard\\DashboardMeOneOnOneController@show')->name('dashboard.oneonones.show'); Route::post('oneonones/{entry}/happened', 'Company\\Dashboard\\DashboardMeOneOnOneController@markHappened'); @@ -108,6 +93,35 @@ Route::post('oneonones/{entry}/notes', 'Company\\Dashboard\\DashboardMeOneOnOneController@storeNote'); Route::post('oneonones/{entry}/notes/{note}', 'Company\\Dashboard\\DashboardMeOneOnOneController@updateNote'); Route::delete('oneonones/{entry}/notes/{note}', 'Company\\Dashboard\\DashboardMeOneOnOneController@destroyNote'); + + // manager tab + Route::prefix('manager')->group(function () { + Route::get('', 'Company\\Dashboard\\DashboardManagerController@index')->name('dashboard.manager'); + Route::get('expenses/{expense}', 'Company\\Dashboard\\DashboardManagerController@showExpense')->name('dashboard.manager.expense.show'); + Route::post('expenses/{expense}/accept', 'Company\\Dashboard\\DashboardManagerController@accept'); + Route::post('expenses/{expense}/reject', 'Company\\Dashboard\\DashboardManagerController@reject'); + + // timesheets + Route::get('timesheets', 'Company\\Dashboard\\DashboardManagerTimesheetController@index')->name('dashboard.manager.timesheet.index'); + Route::get('timesheets/{timesheet}', 'Company\\Dashboard\\DashboardManagerTimesheetController@show')->name('dashboard.manager.timesheet.show'); + Route::post('timesheets/{timesheet}/approve', 'Company\\Dashboard\\DashboardManagerTimesheetController@approve'); + Route::post('timesheets/{timesheet}/reject', 'Company\\Dashboard\\DashboardManagerTimesheetController@reject'); + + // rate your manager + Route::post('rate/{answer}', 'Company\\Dashboard\\DashboardRateYourManagerController@store'); + Route::post('rate/{answer}/comment', 'Company\\Dashboard\\DashboardRateYourManagerController@storeComment'); + }); + + // hr tab + Route::prefix('hr')->group(function () { + Route::get('', 'Company\\Dashboard\\DashboardHRController@index')->name('dashboard.hr'); + + // timesheets + Route::get('timesheets', 'Company\\Dashboard\\DashboardHRTimesheetController@index')->name('dashboard.hr.timesheet.index'); + Route::get('timesheets/{timesheet}', 'Company\\Dashboard\\DashboardHRTimesheetController@show')->name('dashboard.hr.timesheet.show'); + Route::post('timesheets/{timesheet}/approve', 'Company\\Dashboard\\DashboardHRTimesheetController@approve'); + Route::post('timesheets/{timesheet}/reject', 'Company\\Dashboard\\DashboardHRTimesheetController@reject'); + }); }); Route::prefix('employees')->group(function () { diff --git a/tests/Unit/Helpers/TimeHelperTest.php b/tests/Unit/Helpers/TimeHelperTest.php index 033c0f988..fcd085d1e 100644 --- a/tests/Unit/Helpers/TimeHelperTest.php +++ b/tests/Unit/Helpers/TimeHelperTest.php @@ -37,4 +37,24 @@ public function it_gets_the_number_of_hours_and_minutes(): void TimeHelper::convertToHoursAndMinutes(01) ); } + + /** @test */ + public function it_gets_a_string_representing_a_given_duration(): void + { + $this->assertEquals( + '1h40', + TimeHelper::durationInHumanFormat([ + 'hours' => 1, + 'minutes' => 40, + ]) + ); + + $this->assertEquals( + '0h00', + TimeHelper::durationInHumanFormat([ + 'hours' => 0, + 'minutes' => 00, + ]) + ); + } } diff --git a/tests/Unit/ViewHelpers/Company/Project/ProjectTasksViewHelperTest.php b/tests/Unit/ViewHelpers/Company/Project/ProjectTasksViewHelperTest.php index 069e6057e..4a34bcf12 100644 --- a/tests/Unit/ViewHelpers/Company/Project/ProjectTasksViewHelperTest.php +++ b/tests/Unit/ViewHelpers/Company/Project/ProjectTasksViewHelperTest.php @@ -43,6 +43,7 @@ public function it_gets_a_collection_of_tasks_without_task_lists(): void 'description' => $projectTaskA->description, 'completed' => true, 'completed_at' => 'Jan 01, 2018', + 'duration' => '0h00', 'author' => [ 'id' => $michael->id, 'name' => $michael->name, @@ -62,6 +63,7 @@ public function it_gets_a_collection_of_tasks_without_task_lists(): void 'description' => $projectTaskB->description, 'completed' => false, 'completed_at' => null, + 'duration' => '0h00', 'author' => null, 'assignee' => null, ], @@ -124,6 +126,7 @@ public function it_gets_a_collection_of_tasks_with_task_lists(): void 'description' => $projectTaskA->description, 'completed' => true, 'completed_at' => 'Jan 01, 2018', + 'duration' => '0h00', 'author' => [ 'id' => $michael->id, 'name' => $michael->name, @@ -163,6 +166,7 @@ public function it_gets_a_collection_of_tasks_with_task_lists(): void 'description' => $projectTaskB->description, 'completed' => false, 'completed_at' => null, + 'duration' => '0h00', 'author' => null, 'assignee' => null, ], @@ -178,6 +182,7 @@ public function it_gets_a_collection_of_tasks_with_task_lists(): void 'description' => $projectTaskC->description, 'completed' => false, 'completed_at' => null, + 'duration' => '0h00', 'author' => null, 'assignee' => null, ], @@ -210,6 +215,7 @@ public function it_gets_the_description_of_a_single_task(): void 'description' => $projectTaskA->description, 'completed' => true, 'completed_at' => 'Jan 01, 2018', + 'duration' => '0h00', 'author' => [ 'id' => $michael->id, 'name' => $michael->name, diff --git a/tests/Unit/ViewHelpers/Dashboard/DashboardHRViewHelperTest.php b/tests/Unit/ViewHelpers/Dashboard/DashboardHRViewHelperTest.php new file mode 100644 index 000000000..abeeb7e1f --- /dev/null +++ b/tests/Unit/ViewHelpers/Dashboard/DashboardHRViewHelperTest.php @@ -0,0 +1,109 @@ +createAdministrator(); + $dwight = Employee::factory()->create([ + 'company_id' => $michael->company_id, + ]); + + // michael will be direct report of dwight + (new AssignManager)->execute([ + 'company_id' => $michael->company_id, + 'author_id' => $michael->id, + 'employee_id' => $dwight->id, + 'manager_id' => $michael->id, + ]); + + // michael has one unapproved timesheets + Timesheet::factory()->create([ + 'company_id' => $michael->company_id, + 'employee_id' => $michael->id, + 'started_at' => '2017-12-25 00:00:00', + 'status' => Timesheet::READY_TO_SUBMIT, + ]); + + // dwight has one unapproved timesheets, but it shouldn't appear in the collection as he has a manager + Timesheet::factory()->create([ + 'company_id' => $michael->company_id, + 'employee_id' => $dwight->id, + 'started_at' => '2017-12-25 00:00:00', + 'status' => Timesheet::READY_TO_SUBMIT, + ]); + + $array = DashboardHRViewHelper::employeesWithoutManagersWithPendingTimesheets($michael->company); + + $this->assertEquals(1, $array['employees']->count()); + + $this->assertEquals( + [ + 0 => [ + 'id' => $michael->id, + 'name' => $michael->name, + 'avatar' => $michael->avatar, + ], + ], + $array['employees']->toArray() + ); + + $this->assertEquals( + env('APP_URL').'/'.$dwight->company_id.'/dashboard/hr/timesheets', + $array['url_view_all'] + ); + } + + /** @test */ + public function it_gets_a_collection_of_statistics_about_timesheets(): void + { + Carbon::setTestNow(Carbon::create(2018, 1, 1)); + + $company = Company::factory()->create(); + + // we'll have + // - 2 timesheets in a ready to submit state + // - 2 timesheets in a rejected state + // - 2 timesheets in a accepted state + Timesheet::factory()->count(2)->create([ + 'company_id' => $company->id, + 'started_at' => '2017-12-25 00:00:00', + 'status' => Timesheet::READY_TO_SUBMIT, + ]); + Timesheet::factory()->count(2)->create([ + 'company_id' => $company->id, + 'started_at' => '2017-12-25 00:00:00', + 'status' => Timesheet::REJECTED, + ]); + Timesheet::factory()->count(2)->create([ + 'company_id' => $company->id, + 'started_at' => '2017-12-25 00:00:00', + 'status' => Timesheet::APPROVED, + ]); + + $array = DashboardHRViewHelper::statisticsAboutTimesheets($company); + + $this->assertEquals( + [ + 'total' => 6, + 'rejected' => 2, + ], + $array + ); + } +} diff --git a/tests/Unit/ViewHelpers/Dashboard/DashboardManagerViewHelperTest.php b/tests/Unit/ViewHelpers/Dashboard/DashboardManagerViewHelperTest.php index 7435c52e9..b996a407e 100644 --- a/tests/Unit/ViewHelpers/Dashboard/DashboardManagerViewHelperTest.php +++ b/tests/Unit/ViewHelpers/Dashboard/DashboardManagerViewHelperTest.php @@ -227,7 +227,7 @@ public function it_gets_a_collection_of_employees_who_have_a_contract_renewed_so } /** @test */ - public function it_gets_a_collection_of_employees_who_have_timesheets_to_approve(): void + public function it_gets_an_array_of_data_about_employees_who_have_timesheets_to_approve(): void { Carbon::setTestNow(Carbon::create(2018, 1, 1)); $michael = $this->createAdministrator(); @@ -277,55 +277,27 @@ public function it_gets_a_collection_of_employees_who_have_timesheets_to_approve 'project_task_id' => $task->id, ]); - $collection = DashboardManagerViewHelper::timesheetApprovals($michael, $michael->directReports); + $array = DashboardManagerViewHelper::employeesWithTimesheetsToApprove($michael, $michael->directReports); // make sure there is only one employee $this->assertEquals( 1, - $collection->count() + $array['totalNumberOfTimesheetsToValidate'] ); - // now analyzing what's returned from the method - $this->assertEquals( - $dwight->id, - $collection->toArray()[0]['id'] - ); - $this->assertEquals( - 'Dwight Schrute', - $collection->toArray()[0]['name'] - ); - $this->assertEquals( - $dwight->avatar, - $collection->toArray()[0]['avatar'] - ); - $this->assertEquals( - $dwight->position->title, - $collection->toArray()[0]['position'] - ); - $this->assertEquals( - env('APP_URL').'/'.$dwight->company_id.'/employees/'.$dwight->id, - $collection->toArray()[0]['url'] - ); $this->assertEquals( - env('APP_URL').'/'.$dwight->company_id.'/employees/'.$dwight->id, - $collection->toArray()[0]['url'] + env('APP_URL').'/'.$dwight->company_id.'/dashboard/manager/timesheets', + $array['url_view_all'] ); - // analyzing timesheets - there should be only one timesheet + // now analyzing what's returned from the method $this->assertEquals( - 1, - $collection->toArray()[0]['timesheets']->count() + $dwight->id, + $array['employees']->toArray()[0]['id'] ); $this->assertEquals( - [ - 0 => [ - 'id' => $timesheetA->id, - 'started_at' => 'Jan 01, 2018', - 'ended_at' => 'Jan 07, 2018', - 'duration' => '01 h 40', - ], - ], - $collection->toArray()[0]['timesheets']->toArray() + env('APP_URL').'/'.$dwight->company_id.'/employees/'.$dwight->id, + $array['employees']->toArray()[0]['url'] ); } } diff --git a/tests/Unit/ViewHelpers/Dashboard/HR/DashboardHRTimesheetViewHelperTest.php b/tests/Unit/ViewHelpers/Dashboard/HR/DashboardHRTimesheetViewHelperTest.php new file mode 100644 index 000000000..deedd3b10 --- /dev/null +++ b/tests/Unit/ViewHelpers/Dashboard/HR/DashboardHRTimesheetViewHelperTest.php @@ -0,0 +1,114 @@ +createAdministrator(); + + // creating two employees and adding timesheets after this + $dwight = factory(Employee::class)->create([ + 'company_id' => $michael->company_id, + ]); + $jim = factory(Employee::class)->create([ + 'company_id' => $michael->company_id, + ]); + + (new AssignManager)->execute([ + 'company_id' => $michael->company_id, + 'author_id' => $michael->id, + 'employee_id' => $dwight->id, + 'manager_id' => $michael->id, + ]); + + // creating one timesheet ready to submit for dwight + $project = Project::factory()->create(); + $task = ProjectTask::factory()->create([ + 'project_id' => $project->id, + ]); + $timesheetA = Timesheet::factory()->create([ + 'company_id' => $dwight->company_id, + 'employee_id' => $dwight->id, + 'status' => Timesheet::READY_TO_SUBMIT, + 'started_at' => '2017-12-25 00:00:00', + ]); + + // creating one timesheet for jim + $timesheetB = Timesheet::factory()->create([ + 'company_id' => $dwight->company_id, + 'employee_id' => $jim->id, + 'status' => Timesheet::READY_TO_SUBMIT, + 'started_at' => '2017-12-25 00:00:00', + ]); + TimeTrackingEntry::factory()->create([ + 'timesheet_id' => $timesheetB->id, + 'project_task_id' => $task->id, + ]); + + $collection = DashboardHRTimesheetViewHelper::timesheetApprovalsForEmployeesWithoutManagers($michael->company); + + // make sure there is only one employee + $this->assertEquals( + 1, + $collection->count() + ); + + // now analyzing what's returned from the method + $this->assertEquals( + $jim->id, + $collection->toArray()[0]['id'] + ); + $this->assertEquals( + 'Dwight Schrute', + $collection->toArray()[0]['name'] + ); + $this->assertEquals( + $jim->avatar, + $collection->toArray()[0]['avatar'] + ); + $this->assertEquals( + $jim->position->title, + $collection->toArray()[0]['position'] + ); + $this->assertEquals( + env('APP_URL').'/'.$jim->company_id.'/employees/'.$jim->id, + $collection->toArray()[0]['url'] + ); + + // analyzing timesheets - there should be only one timesheet + $this->assertEquals( + 1, + $collection->toArray()[0]['timesheets']->count() + ); + + $this->assertEquals( + [ + 0 => [ + 'id' => $timesheetB->id, + 'started_at' => 'Dec 25, 2017', + 'ended_at' => 'Jan 07, 2018', + 'duration' => '01 h 40', + 'url' => env('APP_URL').'/'.$michael->company_id.'/dashboard/hr/timesheets/'.$timesheetB->id, + ], + ], + $collection->toArray()[0]['timesheets']->toArray() + ); + } +} diff --git a/tests/Unit/ViewHelpers/Dashboard/Manager/DashboardManagerTimesheetViewHelperTest.php b/tests/Unit/ViewHelpers/Dashboard/Manager/DashboardManagerTimesheetViewHelperTest.php new file mode 100644 index 000000000..41c6b7cad --- /dev/null +++ b/tests/Unit/ViewHelpers/Dashboard/Manager/DashboardManagerTimesheetViewHelperTest.php @@ -0,0 +1,123 @@ +createAdministrator(); + + // creating two employees and adding timesheets after this + $dwight = factory(Employee::class)->create([ + 'company_id' => $michael->company_id, + ]); + $jim = factory(Employee::class)->create([ + 'company_id' => $michael->company_id, + ]); + + (new AssignManager)->execute([ + 'company_id' => $michael->company_id, + 'author_id' => $michael->id, + 'employee_id' => $dwight->id, + 'manager_id' => $michael->id, + ]); + (new AssignManager)->execute([ + 'company_id' => $michael->company_id, + 'author_id' => $michael->id, + 'employee_id' => $jim->id, + 'manager_id' => $michael->id, + ]); + + // creating one timesheet ready to submit + $project = Project::factory()->create(); + $task = ProjectTask::factory()->create([ + 'project_id' => $project->id, + ]); + $timesheetA = Timesheet::factory()->create([ + 'employee_id' => $dwight->id, + 'status' => Timesheet::READY_TO_SUBMIT, + ]); + TimeTrackingEntry::factory()->create([ + 'timesheet_id' => $timesheetA->id, + 'project_task_id' => $task->id, + ]); + + // creating one timesheet but not ready - it shouldn't appear in the + // collection + $timesheetB = Timesheet::factory()->create([ + 'employee_id' => $dwight->id, + ]); + TimeTrackingEntry::factory()->create([ + 'timesheet_id' => $timesheetB->id, + 'project_task_id' => $task->id, + ]); + + $collection = DashboardManagerTimesheetViewHelper::timesheetApprovals($michael, $michael->directReports); + + // make sure there is only one employee + $this->assertEquals( + 1, + $collection->count() + ); + + // now analyzing what's returned from the method + $this->assertEquals( + $dwight->id, + $collection->toArray()[0]['id'] + ); + $this->assertEquals( + 'Dwight Schrute', + $collection->toArray()[0]['name'] + ); + $this->assertEquals( + $dwight->avatar, + $collection->toArray()[0]['avatar'] + ); + $this->assertEquals( + $dwight->position->title, + $collection->toArray()[0]['position'] + ); + $this->assertEquals( + env('APP_URL').'/'.$dwight->company_id.'/employees/'.$dwight->id, + $collection->toArray()[0]['url'] + ); + $this->assertEquals( + env('APP_URL').'/'.$dwight->company_id.'/employees/'.$dwight->id, + $collection->toArray()[0]['url'] + ); + + // analyzing timesheets - there should be only one timesheet + $this->assertEquals( + 1, + $collection->toArray()[0]['timesheets']->count() + ); + $this->assertEquals( + [ + 0 => [ + 'id' => $timesheetA->id, + 'started_at' => 'Jan 01, 2018', + 'ended_at' => 'Jan 07, 2018', + 'duration' => '01 h 40', + 'url' => env('APP_URL').'/'.$michael->company_id.'/dashboard/manager/timesheets/'.$timesheetA->id, + ], + ], + $collection->toArray()[0]['timesheets']->toArray() + ); + } +} diff --git a/tests/Unit/ViewHelpers/Team/TeamRecentShipViewHelperTest.php b/tests/Unit/ViewHelpers/Team/TeamRecentShipViewHelperTest.php index a61802d7a..06035ec7f 100644 --- a/tests/Unit/ViewHelpers/Team/TeamRecentShipViewHelperTest.php +++ b/tests/Unit/ViewHelpers/Team/TeamRecentShipViewHelperTest.php @@ -35,6 +35,17 @@ public function it_gets_a_collection_of_recent_ships(): void $this->assertEquals( [ 0 => [ + 'id' => $featureB->id, + 'title' => $featureB->title, + 'description' => $featureB->description, + 'employees' => null, + 'url' => route('ships.show', [ + 'company' => $featureB->team->company, + 'team' => $featureB->team, + 'ship' => $featureB->id, + ]), + ], + 1 => [ 'id' => $featureA->id, 'title' => $featureA->title, 'description' => $featureA->description, @@ -52,17 +63,6 @@ public function it_gets_a_collection_of_recent_ships(): void 'ship' => $featureA->id, ]), ], - 1 => [ - 'id' => $featureB->id, - 'title' => $featureB->title, - 'description' => $featureB->description, - 'employees' => null, - 'url' => route('ships.show', [ - 'company' => $featureB->team->company, - 'team' => $featureB->team, - 'ship' => $featureB->id, - ]), - ], ], $collection->toArray() );