diff --git a/app/Console/Commands/Tests/SetupDummyAccount.php b/app/Console/Commands/Tests/SetupDummyAccount.php index 56ff5a941..3e252929a 100644 --- a/app/Console/Commands/Tests/SetupDummyAccount.php +++ b/app/Console/Commands/Tests/SetupDummyAccount.php @@ -15,25 +15,31 @@ use App\Models\Company\Employee; use App\Models\Company\Position; use App\Models\Company\Question; +use Illuminate\Support\Facades\DB; use App\Models\Company\ECoffeeMatch; use App\Services\User\CreateAccount; use App\Models\Company\ProjectStatus; use App\Models\Company\EmployeeStatus; use App\Models\Company\ExpenseCategory; use App\Services\Company\Team\SetTeamLead; +use App\Services\Company\Group\CreateGroup; use Illuminate\Database\Eloquent\Collection; use App\Models\Company\RateYourManagerAnswer; use App\Models\Company\RateYourManagerSurvey; +use App\Services\Company\Group\CreateMeeting; use App\Services\Company\Project\StartProject; use App\Services\Company\Team\Ship\CreateShip; use App\Models\Company\EmployeePositionHistory; use App\Services\Company\Project\CreateProject; +use App\Services\Company\Group\CreateAgendaItem; +use App\Services\Company\Group\UpdateMeetingDate; use Symfony\Component\Console\Helper\ProgressBar; use App\Services\Company\Adminland\Team\CreateTeam; use App\Services\Company\Employee\Morale\LogMorale; use App\Services\Company\Project\CreateProjectLink; use App\Services\Company\Project\CreateProjectTask; use App\Services\Company\Employee\Worklog\LogWorklog; +use App\Services\Company\Group\CreateMeetingDecision; use App\Services\Company\Project\CreateProjectStatus; use App\Services\Company\Employee\Answer\CreateAnswer; use App\Services\Company\Project\AddEmployeeToProject; @@ -209,8 +215,10 @@ public function handle(): void $this->createTimeTrackingEntries(); $this->setContractRenewalDates(); $this->setECoffeeProcess(); + $this->addGroups(); $this->addPreviousPositionsHistory(); $this->addSecondaryBlankAccount(); + $this->validateUserAccounts(); $this->stop(); } @@ -2022,6 +2030,8 @@ private function setContractRenewalDates(): void private function setECoffeeProcess(): void { + $this->info('☐ Set e Coffee Process'); + $this->company->e_coffee_enabled = true; $this->company->save(); @@ -2043,6 +2053,92 @@ private function setECoffeeProcess(): void }); } + private function addGroups(): void + { + $this->info('☐ Add groups'); + + $groupNames = collect([ + 'Party planning committee', + 'Basketball lovers', + 'Monetisation executive meeting', + 'Front end developers guild', + ]); + + $meetingAgendaItems = collect([ + 'What should we do about the negociations with Home Depot?', + 'Discussion about strategic learnings', + 'Sales update: previous quarter’s results', + 'Team structure presentation', + 'Impact on shareholders', + 'iPhone 42 launch', + ]); + + $decisionItems = collect([ + 'Schedule meeting with supplier', + 'Angela to take responsability and pubicly apologize to Dwight', + 'Prepare forecasts for Q4', + 'Prepare UX for the future feature', + ]); + + foreach ($groupNames as $name) { + $randomEmployees = $this->employees->shuffle()->take(rand(4, 9))->pluck('id')->toArray(); + + // create group with random employees + $group = (new CreateGroup)->execute([ + 'company_id' => $this->company->id, + 'author_id' => $this->michael->id, + 'name' => $name, + 'employees' => $randomEmployees, + ]); + + $group->mission = 'This group was created to discuss all decisions we have to take together.'; + $group->save(); + + // create meetings + $date = Carbon::now()->subMonths(10); + for ($i = 0; $i < rand(4, 9); $i++) { + $meeting = (new CreateMeeting)->execute([ + 'company_id' => $this->company->id, + 'author_id' => $this->michael->id, + 'group_id' => $group->id, + ]); + + (new UpdateMeetingDate)->execute([ + 'company_id' => $this->company->id, + 'author_id' => $this->michael->id, + 'group_id' => $group->id, + 'meeting_id' => $meeting->id, + 'date' => $date->addDays(rand(10, 57))->format('Y-m-d'), + ]); + + // add agenda items + foreach ($meetingAgendaItems as $item) { + $agendaItem = (new CreateAgendaItem)->execute([ + 'company_id' => $this->company->id, + 'author_id' => $this->michael->id, + 'group_id' => $group->id, + 'meeting_id' => $meeting->id, + 'summary' => $item, + 'description' => null, + 'presented_by_id' => $this->employees->shuffle()->first()->id, + ]); + + $decisionItems = $decisionItems->shuffle()->take(rand(1, 3)); + foreach ($decisionItems as $item) { + (new CreateMeetingDecision)->execute([ + 'company_id' => $this->company->id, + 'author_id' => $this->michael->id, + 'group_id' => $group->id, + 'meeting_id' => $meeting->id, + 'agenda_item_id' => $agendaItem->id, + 'description' => $item, + ]); + } + } + } + } + } + private function addPreviousPositionsHistory(): void { foreach ($this->employees as $employee) { @@ -2088,6 +2184,12 @@ private function addSecondaryBlankAccount(): void ]); } + private function validateUserAccounts(): void + { + DB::table('users') + ->update(['email_verified_at' => Carbon::now()]); + } + private function artisan(string $message, string $command, array $arguments = []): void { $this->info($message); diff --git a/app/Helpers/LogHelper.php b/app/Helpers/LogHelper.php index 5e85fae71..e076caed9 100644 --- a/app/Helpers/LogHelper.php +++ b/app/Helpers/LogHelper.php @@ -1084,6 +1084,147 @@ public static function processAuditLog(AuditLog $log): string $sentence = trans('account.log_toggle_e_coffee_process'); break; + case 'group_created': + $sentence = trans('account.log_group_created', [ + 'group_id' => $log->object->{'group_id'}, + 'group_name' => $log->object->{'group_name'}, + ]); + break; + + case 'employee_added_to_group': + $sentence = trans('account.log_employee_added_to_group', [ + 'employee_id' => $log->object->{'employee_id'}, + 'employee_name' => $log->object->{'employee_name'}, + 'group_id' => $log->object->{'group_id'}, + 'group_name' => $log->object->{'group_name'}, + ]); + break; + + case 'employee_removed_from_group': + $sentence = trans('account.log_employee_removed_from_group', [ + 'employee_id' => $log->object->{'employee_id'}, + 'employee_name' => $log->object->{'employee_name'}, + 'group_id' => $log->object->{'group_id'}, + 'group_name' => $log->object->{'group_name'}, + ]); + break; + + case 'group_destroyed': + $sentence = trans('account.log_group_destroyed', [ + 'group_name' => $log->object->{'group_name'}, + ]); + break; + + case 'meeting_created': + $sentence = trans('account.log_meeting_created', [ + 'meeting_id' => $log->object->{'meeting_id'}, + 'group_id' => $log->object->{'group_id'}, + 'group_name' => $log->object->{'group_name'}, + ]); + break; + + case 'meeting_destroyed': + $sentence = trans('account.log_meeting_destroyed', [ + 'meeting_id' => $log->object->{'meeting_id'}, + 'group_id' => $log->object->{'group_id'}, + 'group_name' => $log->object->{'group_name'}, + ]); + break; + + case 'employee_marked_as_participant_in_meeting': + $sentence = trans('account.log_employee_marked_as_participant_in_meeting', [ + 'group_id' => $log->object->{'group_id'}, + 'group_name' => $log->object->{'group_name'}, + 'employee_id' => $log->object->{'employee_id'}, + 'employee_name' => $log->object->{'employee_name'}, + 'meeting_id' => $log->object->{'meeting_id'}, + ]); + break; + + case 'employee_removed_from_meeting': + $sentence = trans('account.log_employee_removed_from_meeting', [ + 'group_id' => $log->object->{'group_id'}, + 'group_name' => $log->object->{'group_name'}, + 'employee_id' => $log->object->{'employee_id'}, + 'employee_name' => $log->object->{'employee_name'}, + 'meeting_id' => $log->object->{'meeting_id'}, + ]); + break; + + case 'agenda_item_created': + $sentence = trans('account.log_agenda_item_created', [ + 'group_id' => $log->object->{'group_id'}, + 'group_name' => $log->object->{'group_name'}, + 'employee_id' => $log->object->{'employee_id'}, + 'employee_name' => $log->object->{'employee_name'}, + 'meeting_id' => $log->object->{'meeting_id'}, + ]); + break; + + case 'agenda_item_updated': + $sentence = trans('account.log_agenda_item_updated', [ + 'group_id' => $log->object->{'group_id'}, + 'group_name' => $log->object->{'group_name'}, + 'employee_id' => $log->object->{'employee_id'}, + 'employee_name' => $log->object->{'employee_name'}, + 'meeting_id' => $log->object->{'meeting_id'}, + ]); + break; + + case 'agenda_item_destroyed': + $sentence = trans('account.log_agenda_item_destroyed', [ + 'group_id' => $log->object->{'group_id'}, + 'group_name' => $log->object->{'group_name'}, + 'meeting_id' => $log->object->{'meeting_id'}, + ]); + break; + + case 'meeting_decision_created': + $sentence = trans('account.log_meeting_decision_created', [ + 'group_id' => $log->object->{'group_id'}, + 'group_name' => $log->object->{'group_name'}, + 'meeting_id' => $log->object->{'meeting_id'}, + ]); + break; + + case 'meeting_decision_destroyed': + $sentence = trans('account.log_meeting_decision_destroyed', [ + 'group_id' => $log->object->{'group_id'}, + 'group_name' => $log->object->{'group_name'}, + 'employee_id' => $log->object->{'employee_id'}, + 'employee_name' => $log->object->{'employee_name'}, + 'meeting_id' => $log->object->{'meeting_id'}, + ]); + break; + + case 'meeting_decision_updated': + $sentence = trans('account.log_meeting_decision_updated', [ + 'group_id' => $log->object->{'group_id'}, + 'group_name' => $log->object->{'group_name'}, + 'employee_id' => $log->object->{'employee_id'}, + 'employee_name' => $log->object->{'employee_name'}, + 'meeting_id' => $log->object->{'meeting_id'}, + ]); + break; + + case 'add_guest_to_meeting': + $sentence = trans('account.log_add_guest_to_meeting', [ + 'group_id' => $log->object->{'group_id'}, + 'group_name' => $log->object->{'group_name'}, + 'employee_id' => $log->object->{'employee_id'}, + 'employee_name' => $log->object->{'employee_name'}, + 'meeting_id' => $log->object->{'meeting_id'}, + ]); + break; + + case 'meeting_date_set': + $sentence = trans('account.log_meeting_date_set', [ + 'group_id' => $log->object->{'group_id'}, + 'group_name' => $log->object->{'group_name'}, + 'meeting_id' => $log->object->{'meeting_id'}, + ]); + break; + case 'company_logo_changed': $sentence = trans('account.log_company_logo_changed'); break; @@ -1094,6 +1235,14 @@ public static function processAuditLog(AuditLog $log): string ]); break; + case 'group_updated': + $sentence = trans('account.log_group_updated', [ + 'group_id' => $log->object->{'group_id'}, + 'group_name' => $log->object->{'group_name'}, + 'group_mission' => $log->object->{'group_mission'}, + ]); + break; + default: $sentence = ''; break; @@ -1588,6 +1737,84 @@ public static function processEmployeeLog(EmployeeLog $log): string ]); break; + case 'employee_added_to_group': + $sentence = trans('account.employee_log_employee_added_to_group', [ + 'group_id' => $log->object->{'group_id'}, + 'group_name' => $log->object->{'group_name'}, + ]); + break; + + case 'employee_removed_from_group': + $sentence = trans('account.employee_log_employee_removed_from_group', [ + 'group_id' => $log->object->{'group_id'}, + 'group_name' => $log->object->{'group_name'}, + ]); + break; + + case 'employee_marked_as_participant_in_meeting': + $sentence = trans('account.employee_log_employee_marked_as_participant_in_meeting', [ + 'group_id' => $log->object->{'group_id'}, + 'group_name' => $log->object->{'group_name'}, + 'meeting_id' => $log->object->{'meeting_id'}, + ]); + break; + + case 'employee_removed_from_meeting': + $sentence = trans('account.employee_log_employee_removed_from_meeting', [ + 'group_id' => $log->object->{'group_id'}, + 'group_name' => $log->object->{'group_name'}, + 'meeting_id' => $log->object->{'meeting_id'}, + ]); + break; + + case 'agenda_item_created': + $sentence = trans('account.employee_log_agenda_item_created', [ + 'group_id' => $log->object->{'group_id'}, + 'group_name' => $log->object->{'group_name'}, + 'meeting_id' => $log->object->{'meeting_id'}, + ]); + break; + + case 'agenda_item_updated': + $sentence = trans('account.employee_log_agenda_item_updated', [ + 'group_id' => $log->object->{'group_id'}, + 'group_name' => $log->object->{'group_name'}, + 'meeting_id' => $log->object->{'meeting_id'}, + ]); + break; + + case 'meeting_decision_created': + $sentence = trans('account.employee_log_meeting_decision_created', [ + 'group_id' => $log->object->{'group_id'}, + 'group_name' => $log->object->{'group_name'}, + 'meeting_id' => $log->object->{'meeting_id'}, + ]); + break; + + case 'meeting_decision_destroyed': + $sentence = trans('account.employee_log_meeting_decision_destroyed', [ + 'group_id' => $log->object->{'group_id'}, + 'group_name' => $log->object->{'group_name'}, + 'meeting_id' => $log->object->{'meeting_id'}, + ]); + break; + + case 'meeting_decision_updated': + $sentence = trans('account.employee_log_log_meeting_decision_updated', [ + 'group_id' => $log->object->{'group_id'}, + 'group_name' => $log->object->{'group_name'}, + 'meeting_id' => $log->object->{'meeting_id'}, + ]); + break; + + case 'add_guest_to_meeting': + $sentence = trans('account.employee_log_log_add_guest_to_meeting', [ + 'group_id' => $log->object->{'group_id'}, + 'group_name' => $log->object->{'group_name'}, + 'meeting_id' => $log->object->{'meeting_id'}, + ]); + break; + default: $sentence = ''; break; diff --git a/app/Http/Controllers/Company/Company/Group/GroupController.php b/app/Http/Controllers/Company/Company/Group/GroupController.php new file mode 100644 index 000000000..65b7b0dc6 --- /dev/null +++ b/app/Http/Controllers/Company/Company/Group/GroupController.php @@ -0,0 +1,241 @@ + $statistics, + 'tab' => 'groups', + 'groups' => GroupViewHelper::index($company), + 'notifications' => NotificationHelper::getNotifications(InstanceHelper::getLoggedEmployee()), + ]); + } + + /** + * Display the group. + * + * @param Request $request + * @param int $companyId + * @param int $groupId + */ + public function show(Request $request, int $companyId, int $groupId) + { + $company = InstanceHelper::getLoggedCompany(); + + try { + $group = Group::where('company_id', $company->id) + ->findOrFail($groupId); + } catch (ModelNotFoundException $e) { + return redirect('home'); + } + + return Inertia::render('Company/Group/Show', [ + 'group' => GroupShowViewHelper::information($group, $company), + 'meetings' => GroupShowViewHelper::meetings($group), + 'stats' => GroupShowViewHelper::stats($group), + 'tab' => 'info', + 'notifications' => NotificationHelper::getNotifications(InstanceHelper::getLoggedEmployee()), + ]); + } + + /** + * Display the create new group form. + * + * @param Request $request + * @param int $companyId + * @return Response + */ + public function create(Request $request, int $companyId): Response + { + return Inertia::render('Company/Group/Create', [ + 'notifications' => NotificationHelper::getNotifications(InstanceHelper::getLoggedEmployee()), + ]); + } + + /** + * Search an employee to add as a team lead. + * + * @param Request $request + * @param int $companyId + * @return JsonResponse + */ + public function search(Request $request, int $companyId): JsonResponse + { + $loggedCompany = InstanceHelper::getLoggedCompany(); + $employees = GroupCreateViewHelper::search($loggedCompany, $request->input('searchTerm')); + + return response()->json([ + 'data' => $employees, + ], 200); + } + + /** + * Actually create the new group. + * + * @param Request $request + * @param int $companyId + * @return JsonResponse + */ + public function store(Request $request, int $companyId): JsonResponse + { + $loggedEmployee = InstanceHelper::getLoggedEmployee(); + $loggedCompany = InstanceHelper::getLoggedCompany(); + + $employees = null; + if ($request->input('employees')) { + $employees = []; + foreach ($request->input('employees') as $employee) { + array_push($employees, $employee['id']); + } + } + + $data = [ + 'company_id' => $loggedCompany->id, + 'author_id' => $loggedEmployee->id, + 'name' => $request->input('name'), + 'employees' => $employees, + ]; + + $group = (new CreateGroup)->execute($data); + + return response()->json([ + 'data' => [ + 'id' => $group->id, + 'url' => route('groups.show', [ + 'company' => $loggedCompany, + 'group' => $group, + ]), + ], + ], 201); + } + + /** + * Show the delete group page. + * + * @param Request $request + * @param int $companyId + * @param int $groupId + * @return Response + */ + public function delete(Request $request, int $companyId, int $groupId): Response + { + $company = InstanceHelper::getLoggedCompany(); + + $group = Group::where('company_id', $company->id) + ->findOrFail($groupId); + + return Inertia::render('Company/Group/Delete', [ + 'group' => GroupShowViewHelper::information($group, $company), + 'notifications' => NotificationHelper::getNotifications(InstanceHelper::getLoggedEmployee()), + ]); + } + + /** + * Actually destroy the group. + * + * @param Request $request + * @param int $companyId + * @param int $groupId + * @return JsonResponse + */ + public function destroy(Request $request, int $companyId, int $groupId): JsonResponse + { + $loggedEmployee = InstanceHelper::getLoggedEmployee(); + $loggedCompany = InstanceHelper::getLoggedCompany(); + + (new DestroyGroup)->execute([ + 'company_id' => $loggedCompany->id, + 'author_id' => $loggedEmployee->id, + 'group_id' => $groupId, + ]); + + return response()->json([ + 'data' => true, + ]); + } + + /** + * Display the Edit group page. + * + * @param Request $request + * @param int $companyId + * @param int $groupId + */ + public function edit(Request $request, int $companyId, int $groupId) + { + $company = InstanceHelper::getLoggedCompany(); + + try { + $group = Group::where('company_id', $company->id) + ->findOrFail($groupId); + } catch (ModelNotFoundException $e) { + return redirect('home'); + } + + return Inertia::render('Company/Group/Edit', [ + 'group' => GroupShowViewHelper::edit($group), + 'notifications' => NotificationHelper::getNotifications(InstanceHelper::getLoggedEmployee()), + ]); + } + + /** + * Update the group information. + * + * @param Request $request + * @param int $companyId + * @param int $groupId + */ + public function update(Request $request, int $companyId, int $groupId): JsonResponse + { + $loggedEmployee = InstanceHelper::getLoggedEmployee(); + $loggedCompany = InstanceHelper::getLoggedCompany(); + + (new UpdateGroup)->execute([ + 'company_id' => $loggedCompany->id, + 'author_id' => $loggedEmployee->id, + 'group_id' => $groupId, + 'name' => $request->input('name'), + 'mission' => $request->input('mission'), + ]); + + return response()->json([ + 'data' => [ + 'url' => route('groups.show', [ + 'company' => $loggedCompany, + 'group' => $groupId, + ]), + ], + ]); + } +} diff --git a/app/Http/Controllers/Company/Company/Group/GroupMeetingsController.php b/app/Http/Controllers/Company/Company/Group/GroupMeetingsController.php new file mode 100644 index 000000000..d9b8634ce --- /dev/null +++ b/app/Http/Controllers/Company/Company/Group/GroupMeetingsController.php @@ -0,0 +1,518 @@ +id) + ->findOrFail($groupId); + } catch (ModelNotFoundException $e) { + return redirect('home'); + } + + $info = GroupShowViewHelper::information($group, $company); + $meetings = GroupMeetingsViewHelper::index($group); + + return Inertia::render('Company/Group/Meetings/Index', [ + 'group' => $info, + 'tab' => 'meetings', + 'data' => $meetings, + 'notifications' => NotificationHelper::getNotifications($employee), + ]); + } + + /** + * Create the meeting page and redirects to it. + * A meeting doesn't have a dedicated Create screen - viewing, creating + * and editing is done on the same page. + * + * @param Request $request + * @param int $companyId + * @param int $groupId + */ + public function create(Request $request, int $companyId, int $groupId) + { + $loggedCompany = InstanceHelper::getLoggedCompany(); + $loggedEmployee = InstanceHelper::getLoggedEmployee(); + + try { + $group = Group::where('company_id', $loggedCompany->id) + ->findOrFail($groupId); + } catch (ModelNotFoundException $e) { + return redirect('home'); + } + + $meeting = (new CreateMeeting)->execute([ + 'company_id' => $loggedCompany->id, + 'author_id' => $loggedEmployee->id, + 'group_id' => $group->id, + ]); + + return redirect()->route('groups.meetings.show', [ + 'company' => $loggedCompany->id, + 'group' => $group->id, + 'meeting' => $meeting->id, + ]); + } + + /** + * Show the meeting page. + * + * @param Request $request + * @param int $companyId + * @param int $groupId + * @param int $meetingId + */ + public function show(Request $request, int $companyId, int $groupId, int $meetingId) + { + $loggedCompany = InstanceHelper::getLoggedCompany(); + + try { + $group = Group::where('company_id', $loggedCompany->id) + ->findOrFail($groupId); + } catch (ModelNotFoundException $e) { + return redirect('home'); + } + + try { + $meeting = Meeting::where('group_id', $group->id) + ->findOrFail($meetingId); + } catch (ModelNotFoundException $e) { + return redirect('home'); + } + + $groupInfo = GroupShowViewHelper::information($group, $loggedCompany); + $meetingInfo = GroupMeetingsViewHelper::show($meeting, $loggedCompany); + $agenda = GroupMeetingsViewHelper::agenda($meeting, $loggedCompany); + + return Inertia::render('Company/Group/Meetings/Show', [ + 'group' => $groupInfo, + 'meeting' => $meetingInfo, + 'agenda' => $agenda, + 'notifications' => NotificationHelper::getNotifications(InstanceHelper::getLoggedEmployee()), + ]); + } + + /** + * Toggle the participation for a person in the meeting. + * + * @param Request $request + * @param int $companyId + * @param int $groupId + * @param int $meetingId + */ + public function toggleParticipant(Request $request, int $companyId, int $groupId, int $meetingId) + { + $loggedCompany = InstanceHelper::getLoggedCompany(); + $loggedEmployee = InstanceHelper::getLoggedEmployee(); + + (new ToggleEmployeeParticipationInMeeting)->execute([ + 'company_id' => $loggedCompany->id, + 'author_id' => $loggedEmployee->id, + 'group_id' => $groupId, + 'meeting_id' => $meetingId, + 'employee_id' => $request->input('id'), + ]); + + return response()->json([ + 'data' => true, + ]); + } + + /** + * Get the list of potential participants for this meeting. + * + * @param Request $request + * @param int $companyId + * @param int $groupId + * @param int $meetingId + * @return JsonResponse + */ + public function search(Request $request, int $companyId, int $groupId, int $meetingId): JsonResponse + { + $company = InstanceHelper::getLoggedCompany(); + + try { + $group = Group::where('company_id', $company->id) + ->findOrFail($groupId); + } catch (ModelNotFoundException $e) { + return redirect('home'); + } + + try { + $meeting = Meeting::where('group_id', $group->id) + ->findOrFail($meetingId); + } catch (ModelNotFoundException $e) { + return redirect('home'); + } + + $potentialMembers = GroupMeetingsViewHelper::potentialGuests( + $meeting, + $company, + $request->input('searchTerm') + ); + + return response()->json([ + 'data' => $potentialMembers, + ]); + } + + /** + * Add participant to the meeting. + * + * @param Request $request + * @param int $companyId + * @param int $groupId + * @param int $meetingId + */ + public function addParticipant(Request $request, int $companyId, int $groupId, int $meetingId): JsonResponse + { + $loggedCompany = InstanceHelper::getLoggedCompany(); + $loggedEmployee = InstanceHelper::getLoggedEmployee(); + + $employee = (new AddGuestToMeeting)->execute([ + 'company_id' => $loggedCompany->id, + 'author_id' => $loggedEmployee->id, + 'group_id' => $groupId, + 'meeting_id' => $meetingId, + 'employee_id' => $request->input('id'), + ]); + + return response()->json([ + 'data' => [ + 'id' => $employee->id, + 'name' => $employee->name, + 'avatar' => ImageHelper::getAvatar($employee, 23), + 'was_a_guest' => true, + 'attended' => false, + ], + ]); + } + + /** + * Remove participant from the meeting. + * + * @param Request $request + * @param int $companyId + * @param int $groupId + * @param int $meetingId + */ + public function removeParticipant(Request $request, int $companyId, int $groupId, int $meetingId): JsonResponse + { + $loggedCompany = InstanceHelper::getLoggedCompany(); + $loggedEmployee = InstanceHelper::getLoggedEmployee(); + + (new RemoveGuestFromMeeting)->execute([ + 'company_id' => $loggedCompany->id, + 'author_id' => $loggedEmployee->id, + 'group_id' => $groupId, + 'meeting_id' => $meetingId, + 'employee_id' => $request->input('id'), + ]); + + return response()->json([ + 'data' => true, + ]); + } + + /** + * Set the meeting date. + * + * @param Request $request + * @param int $companyId + * @param int $groupId + * @param int $meetingId + */ + public function setDate(Request $request, int $companyId, int $groupId, int $meetingId): JsonResponse + { + $loggedCompany = InstanceHelper::getLoggedCompany(); + $loggedEmployee = InstanceHelper::getLoggedEmployee(); + + $date = Carbon::createFromDate($request->input('year'), $request->input('month'), $request->input('day')); + + (new UpdateMeetingDate)->execute([ + 'company_id' => $loggedCompany->id, + 'author_id' => $loggedEmployee->id, + 'group_id' => $groupId, + 'meeting_id' => $meetingId, + 'date' => $date->format('Y-m-d'), + ]); + + return response()->json([ + 'data' => DateHelper::formatDate($date), + ]); + } + + /** + * Destroy the meeting. + * + * @param Request $request + * @param int $companyId + * @param int $groupId + * @param int $meetingId + */ + public function destroy(Request $request, int $companyId, int $groupId, int $meetingId): JsonResponse + { + $loggedCompany = InstanceHelper::getLoggedCompany(); + $loggedEmployee = InstanceHelper::getLoggedEmployee(); + + (new DestroyMeeting)->execute([ + 'company_id' => $loggedCompany->id, + 'author_id' => $loggedEmployee->id, + 'group_id' => $groupId, + 'meeting_id' => $meetingId, + ]); + + return response()->json([ + 'data' => true, + ]); + } + + /** + * Create an agenda item. + * + * @param Request $request + * @param int $companyId + * @param int $groupId + * @param int $meetingId + * @return JsonResponse + */ + public function createAgendaItem(Request $request, int $companyId, int $groupId, int $meetingId): JsonResponse + { + $loggedCompany = InstanceHelper::getLoggedCompany(); + $loggedEmployee = InstanceHelper::getLoggedEmployee(); + + $agendaItem = (new CreateAgendaItem)->execute([ + 'company_id' => $loggedCompany->id, + 'author_id' => $loggedEmployee->id, + 'group_id' => $groupId, + 'meeting_id' => $meetingId, + 'summary' => $request->input('summary'), + 'description' => $request->input('description'), + 'presented_by_id' => $request->input('presented_by_id'), + ]); + + return response()->json([ + 'data' => [ + 'id' => $agendaItem->id, + 'summary' => $agendaItem->summary, + 'description' => $agendaItem->description, + 'position' => $agendaItem->position, + 'presenter' => $agendaItem->presenter ? [ + 'id' => $agendaItem->presenter->id, + 'name' => $agendaItem->presenter->name, + 'avatar' => ImageHelper::getAvatar($agendaItem->presenter, 23), + 'url' => route('employees.show', [ + 'company' => $loggedCompany, + 'employee' => $agendaItem->presenter, + ]), + ] : null, + ], + ]); + } + + /** + * Update the agenda item summary. + * + * @param Request $request + * @param int $companyId + * @param int $groupId + * @param int $meetingId + * @param int $agendaItemId + * @return JsonResponse + */ + public function updateAgendaItem(Request $request, int $companyId, int $groupId, int $meetingId, int $agendaItemId): JsonResponse + { + $loggedCompany = InstanceHelper::getLoggedCompany(); + $loggedEmployee = InstanceHelper::getLoggedEmployee(); + + $agendaItem = (new UpdateAgendaItem)->execute([ + 'company_id' => $loggedCompany->id, + 'author_id' => $loggedEmployee->id, + 'group_id' => $groupId, + 'meeting_id' => $meetingId, + 'agenda_item_id' => $agendaItemId, + 'summary' => $request->input('summary'), + 'description' => $request->input('description'), + 'presented_by_id' => $request->input('presented_by_id'), + ]); + + return response()->json([ + 'data' => [ + 'id' => $agendaItem->id, + 'summary' => $agendaItem->summary, + 'description' => $agendaItem->description, + 'position' => $agendaItem->position, + 'presenter' => $agendaItem->presenter ? [ + 'id' => $agendaItem->presenter->id, + 'name' => $agendaItem->presenter->name, + 'avatar' => ImageHelper::getAvatar($agendaItem->presenter, 23), + 'url' => route('employees.show', [ + 'company' => $loggedCompany, + 'employee' => $agendaItem->presenter, + ]), + ] : null, + ], + ]); + } + + /** + * Destroy the agenda item. + * + * @param Request $request + * @param int $companyId + * @param int $groupId + * @param int $meetingId + * @param int $agendaItemId + */ + public function destroyAgendaItem(Request $request, int $companyId, int $groupId, int $meetingId, int $agendaItemId): JsonResponse + { + $loggedCompany = InstanceHelper::getLoggedCompany(); + $loggedEmployee = InstanceHelper::getLoggedEmployee(); + + (new DestroyAgendaItem)->execute([ + 'company_id' => $loggedCompany->id, + 'author_id' => $loggedEmployee->id, + 'group_id' => $groupId, + 'meeting_id' => $meetingId, + 'agenda_item_id' => $agendaItemId, + ]); + + return response()->json([ + 'data' => true, + ]); + } + + /** + * Get the potential presenters of the agenda item. + * + * @param Request $request + * @param int $companyId + * @param int $groupId + * @param int $meetingId + * @return JsonResponse + */ + public function getPresenters(Request $request, int $companyId, int $groupId, int $meetingId): JsonResponse + { + try { + $meeting = Meeting::where('group_id', $groupId) + ->findOrFail($meetingId); + } catch (ModelNotFoundException $e) { + return redirect('home'); + } + + try { + Group::where('company_id', $companyId) + ->findOrFail($groupId); + } catch (ModelNotFoundException $e) { + return redirect('home'); + } + + $presenters = GroupMeetingsViewHelper::potentialPresenters($meeting, InstanceHelper::getLoggedCompany()); + + return response()->json([ + 'data' => $presenters, + ]); + } + + /** + * Create a decision. + * + * @param Request $request + * @param int $companyId + * @param int $groupId + * @param int $meetingId + * @param int $agendaItemId + * @return JsonResponse + */ + public function createDecision(Request $request, int $companyId, int $groupId, int $meetingId, int $agendaItemId): JsonResponse + { + $loggedCompany = InstanceHelper::getLoggedCompany(); + $loggedEmployee = InstanceHelper::getLoggedEmployee(); + + $meetingDecision = (new CreateMeetingDecision)->execute([ + 'company_id' => $loggedCompany->id, + 'author_id' => $loggedEmployee->id, + 'group_id' => $groupId, + 'meeting_id' => $meetingId, + 'agenda_item_id' => $agendaItemId, + 'description' => $request->input('description'), + ]); + + return response()->json([ + 'data' => [ + 'id' => $meetingDecision->id, + 'description' => $meetingDecision->description, + ], + ]); + } + + /** + * Destroy a decision. + * + * @param Request $request + * @param int $companyId + * @param int $groupId + * @param int $meetingId + * @param int $agendaItemId + * @param int $decisionId + * @return JsonResponse + */ + public function destroyDecision(Request $request, int $companyId, int $groupId, int $meetingId, int $agendaItemId, int $decisionId): JsonResponse + { + $loggedCompany = InstanceHelper::getLoggedCompany(); + $loggedEmployee = InstanceHelper::getLoggedEmployee(); + + (new DestroyMeetingDecision)->execute([ + 'company_id' => $loggedCompany->id, + 'author_id' => $loggedEmployee->id, + 'group_id' => $groupId, + 'meeting_id' => $meetingId, + 'agenda_item_id' => $agendaItemId, + 'meeting_decision_id' => $decisionId, + ]); + + return response()->json([ + 'data' => true, + ]); + } +} diff --git a/app/Http/Controllers/Company/Company/Group/GroupMembersController.php b/app/Http/Controllers/Company/Company/Group/GroupMembersController.php new file mode 100644 index 000000000..c257f18b0 --- /dev/null +++ b/app/Http/Controllers/Company/Company/Group/GroupMembersController.php @@ -0,0 +1,141 @@ +id) + ->findOrFail($groupId); + } catch (ModelNotFoundException $e) { + return redirect('home'); + } + + $members = GroupMembersViewHelper::members($group); + $info = GroupShowViewHelper::information($group, $company); + + return Inertia::render('Company/Group/Members/Index', [ + 'members' => $members, + 'tab' => 'members', + 'group' => $info, + 'notifications' => NotificationHelper::getNotifications(InstanceHelper::getLoggedEmployee()), + ]); + } + + /** + * Get the list of potential new members for this group. + * + * @param Request $request + * @param int $companyId + * @param int $groupId + * @return JsonResponse + */ + public function search(Request $request, int $companyId, int $groupId): JsonResponse + { + $company = InstanceHelper::getLoggedCompany(); + + try { + $group = Group::where('company_id', $company->id) + ->findOrFail($groupId); + } catch (ModelNotFoundException $e) { + return redirect('home'); + } + + $potentialMembers = GroupMembersViewHelper::potentialMembers( + $company, + $group, + $request->input('searchTerm') + ); + + return response()->json([ + 'data' => $potentialMembers, + ]); + } + + /** + * Add the member to the group. + * + * @param Request $request + * @param int $companyId + * @param int $groupId + * @return JsonResponse + */ + public function store(Request $request, int $companyId, int $groupId): JsonResponse + { + $loggedCompany = InstanceHelper::getLoggedCompany(); + $loggedEmployee = InstanceHelper::getLoggedEmployee(); + + $group = Group::where('company_id', $loggedCompany->id) + ->findOrFail($groupId); + + $employee = (new AddEmployeeToGroup)->execute([ + 'company_id' => $loggedCompany->id, + 'author_id' => $loggedEmployee->id, + 'group_id' => $group->id, + 'employee_id' => $request->input('employee'), + 'role' => null, + ]); + + return response()->json([ + 'data' => [ + 'id' => $employee->id, + 'name' => $employee->name, + 'avatar' => ImageHelper::getAvatar($employee, 32), + ], + ]); + } + + /** + * Remove the member from the group. + * + * @param Request $request + * @param int $companyId + * @param int $groupId + * @return JsonResponse + */ + public function remove(Request $request, int $companyId, int $groupId): JsonResponse + { + $loggedCompany = InstanceHelper::getLoggedCompany(); + $loggedEmployee = InstanceHelper::getLoggedEmployee(); + + $group = Group::where('company_id', $loggedCompany->id) + ->findOrFail($groupId); + + (new RemoveEmployeeFromGroup)->execute([ + 'company_id' => $loggedCompany->id, + 'author_id' => $loggedEmployee->id, + 'employee_id' => $request->input('employee'), + 'group_id' => $group->id, + ]); + + return response()->json([ + 'data' => true, + ]); + } +} diff --git a/app/Http/Controllers/Company/Company/Project/ProjectController.php b/app/Http/Controllers/Company/Company/Project/ProjectController.php index 298af2436..1c325fa26 100644 --- a/app/Http/Controllers/Company/Company/Project/ProjectController.php +++ b/app/Http/Controllers/Company/Company/Project/ProjectController.php @@ -64,7 +64,8 @@ public function show(Request $request, int $companyId, int $projectId): Response { $company = InstanceHelper::getLoggedCompany(); $employee = InstanceHelper::getLoggedEmployee(); - $project = Project::findOrFail($projectId); + $project = Project::where('company_id', $company->id) + ->findOrFail($projectId); return Inertia::render('Company/Project/Show', [ 'project' => ProjectViewHelper::info($project), diff --git a/app/Http/Controllers/Company/Employee/Edit/EmployeeEditController.php b/app/Http/Controllers/Company/Employee/Edit/EmployeeEditController.php index ba4152630..5ca6d3bf0 100644 --- a/app/Http/Controllers/Company/Employee/Edit/EmployeeEditController.php +++ b/app/Http/Controllers/Company/Employee/Edit/EmployeeEditController.php @@ -2,7 +2,6 @@ namespace App\Http\Controllers\Company\Employee\Edit; -use Carbon\Carbon; use Inertia\Inertia; use Inertia\Response; use Illuminate\Http\Request; @@ -93,13 +92,10 @@ public function update(Request $request, int $companyId, int $employeeId): JsonR (new SetPersonalDetails)->execute($data); - $date = Carbon::createFromDate($request->input('year'), $request->input('month'), $request->input('day')); - $data = [ 'company_id' => $companyId, 'author_id' => $loggedEmployee->id, 'employee_id' => $employeeId, - 'date' => $date->format('Y-m-d'), 'year' => intval($request->input('year')), 'month' => intval($request->input('month')), 'day' => intval($request->input('day')), @@ -112,7 +108,6 @@ public function update(Request $request, int $companyId, int $employeeId): JsonR 'company_id' => $companyId, 'author_id' => $loggedEmployee->id, 'employee_id' => $employeeId, - 'date' => $date->format('Y-m-d'), 'year' => intval($request->input('hired_year')), 'month' => intval($request->input('hired_month')), 'day' => intval($request->input('hired_day')), diff --git a/app/Http/Controllers/Company/Team/TeamRecentShipController.php b/app/Http/Controllers/Company/Team/TeamRecentShipController.php index f915dce3f..65ecba3d2 100644 --- a/app/Http/Controllers/Company/Team/TeamRecentShipController.php +++ b/app/Http/Controllers/Company/Team/TeamRecentShipController.php @@ -56,7 +56,7 @@ public function index(Request $request, int $companyId, int $teamId) } /** - * Search an employee to add as a team lead. + * Search an employee to add to the recent ship entry. * * @param Request $request * @param int $companyId diff --git a/app/Http/ViewHelpers/Company/Group/GroupCreateViewHelper.php b/app/Http/ViewHelpers/Company/Group/GroupCreateViewHelper.php new file mode 100644 index 000000000..de5f191a0 --- /dev/null +++ b/app/Http/ViewHelpers/Company/Group/GroupCreateViewHelper.php @@ -0,0 +1,44 @@ +employees() + ->select('id', 'first_name', 'last_name', 'avatar_file_id') + ->notLocked() + ->where(function ($query) use ($criteria) { + $query->where('first_name', 'LIKE', '%'.$criteria.'%') + ->orWhere('last_name', 'LIKE', '%'.$criteria.'%') + ->orWhere('email', 'LIKE', '%'.$criteria.'%'); + }) + ->orderBy('last_name', 'asc') + ->take(10) + ->get(); + + $employeesCollection = collect([]); + foreach ($potentialEmployees as $employee) { + $employeesCollection->push([ + 'id' => $employee->id, + 'name' => $employee->name, + 'avatar' => ImageHelper::getAvatar($employee, 23), + ]); + } + + return $employeesCollection; + } +} diff --git a/app/Http/ViewHelpers/Company/Group/GroupMeetingsViewHelper.php b/app/Http/ViewHelpers/Company/Group/GroupMeetingsViewHelper.php new file mode 100644 index 000000000..cb2b7e4e6 --- /dev/null +++ b/app/Http/ViewHelpers/Company/Group/GroupMeetingsViewHelper.php @@ -0,0 +1,229 @@ +meetings()->orderBy('happened_at', 'desc')->with('employees')->get(); + + $meetingsCollection = collect([]); + foreach ($meetings as $meeting) { + $members = $meeting->employees() + ->inRandomOrder() + ->take(3) + ->get(); + + $totalMembersCount = $meeting->employees()->count(); + $totalMembersCount = $totalMembersCount - $members->count(); + + $membersCollection = collect([]); + foreach ($members as $member) { + $membersCollection->push([ + 'id' => $member->id, + 'avatar' => ImageHelper::getAvatar($member, 25), + 'url' => route('employees.show', [ + 'company' => $group->company_id, + 'employee' => $member, + ]), + ]); + } + + $meetingsCollection->push([ + 'id' => $meeting->id, + 'happened_at' => trans('group.meeting_index_item_title', ['date' => DateHelper::formatDate($meeting->happened_at)]), + 'url' => route('groups.meetings.show', [ + 'company' => $group->company_id, + 'group' => $group, + 'meeting' => $meeting, + ]), + 'preview_members' => $membersCollection, + 'remaining_members_count' => $totalMembersCount, + ]); + } + + return [ + 'meetings' => $meetingsCollection, + 'url_new' => route('groups.meetings.new', [ + 'company' => $group->company_id, + 'group' => $group, + ]), + ]; + } + + /** + * Get information of a specific meeting. + * + * @param Meeting $meeting + * @param Company $company + * @return array + */ + public static function show(Meeting $meeting, Company $company): array + { + $participants = $meeting->employees() + ->orderBy('last_name', 'asc') + ->get(); + + $participantsCollection = collect([]); + $guestsCollection = collect([]); + foreach ($participants as $employee) { + if ((bool) $employee->pivot->was_a_guest) { + $guestsCollection->push([ + 'id' => $employee->id, + 'name' => $employee->name, + 'avatar' => ImageHelper::getAvatar($employee, 23), + 'attended' => (bool) $employee->pivot->attended, + 'was_a_guest' => true, + 'url' => route('employees.show', [ + 'company' => $company, + 'employee' => $employee, + ]), + ]); + } else { + $participantsCollection->push([ + 'id' => $employee->id, + 'name' => $employee->name, + 'avatar' => ImageHelper::getAvatar($employee, 23), + 'attended' => (bool) $employee->pivot->attended, + 'was_a_guest' => false, + 'url' => route('employees.show', [ + 'company' => $company, + 'employee' => $employee, + ]), + ]); + } + } + + return [ + 'meeting' => [ + 'id' => $meeting->id, + 'happened_at' => DateHelper::formatDate($meeting->happened_at), + 'happened_at_max_year' => Carbon::now()->addYear()->year, + 'happened_at_year' => $meeting->happened_at->year, + 'happened_at_month' => $meeting->happened_at->month, + 'happened_at_day' => $meeting->happened_at->day, + ], + 'participants' => $participantsCollection, + 'guests' => $guestsCollection, + ]; + } + + /** + * Get potential guests of this meeting. + * + * @param Meeting $meeting + * @param Company $company + * @param string $criteria + * @return Collection + */ + public static function potentialGuests(Meeting $meeting, Company $company, string $criteria): Collection + { + $members = $meeting->employees() + ->select('id', 'first_name', 'last_name') + ->pluck('id') + ->toArray(); + + $potentialGuests = $company->employees() + ->select('id', 'first_name', 'last_name') + ->notLocked() + ->whereNotIn('id', $members) + ->where(function ($query) use ($criteria) { + $query->where('first_name', 'LIKE', '%'.$criteria.'%') + ->orWhere('last_name', 'LIKE', '%'.$criteria.'%') + ->orWhere('email', 'LIKE', '%'.$criteria.'%'); + }) + ->orderBy('last_name', 'asc') + ->take(10) + ->get(); + + $employeesCollection = collect([]); + foreach ($potentialGuests as $employee) { + $employeesCollection->push([ + 'id' => $employee->id, + 'name' => $employee->name, + ]); + } + + return $employeesCollection; + } + + /** + * Get agenda of the meeting. + * + * @param Meeting $meeting + * @param Company $company + * @return Collection + */ + public static function agenda(Meeting $meeting, Company $company): Collection + { + $items = $meeting->agendaItems() + ->orderBy('id', 'asc') + ->with('presenter') + ->with('decisions') + ->get(); + + $agendaCollection = collect([]); + foreach ($items as $agendaItem) { + $presenter = $agendaItem->presenter; + + // decisions + $decisionsCollection = collect([]); + foreach ($agendaItem->decisions as $decision) { + $decisionsCollection->push([ + 'id' => $decision->id, + 'description' => $decision->description, + ]); + } + + // preparing final collection + $agendaCollection->push([ + 'id' => $agendaItem->id, + 'position' => $agendaItem->position, + 'summary' => $agendaItem->summary, + 'description' => $agendaItem->description, + 'presenter' => $presenter ? [ + 'id' => $presenter->id, + 'name' => $presenter->name, + 'avatar' => ImageHelper::getAvatar($presenter, 23), + 'url' => route('employees.show', [ + 'company' => $company, + 'employee' => $presenter, + ]), + ] : null, + 'decisions' => $decisionsCollection, + ]); + } + + return $agendaCollection; + } + + public static function potentialPresenters(Meeting $meeting, Company $company): Collection + { + $participants = $meeting->employees()->notLocked()->get(); + + $employeesCollection = collect([]); + foreach ($participants as $employee) { + $employeesCollection->push([ + 'value' => $employee->id, + 'label' => $employee->name, + ]); + } + + return $employeesCollection; + } +} diff --git a/app/Http/ViewHelpers/Company/Group/GroupMembersViewHelper.php b/app/Http/ViewHelpers/Company/Group/GroupMembersViewHelper.php new file mode 100644 index 000000000..276f33261 --- /dev/null +++ b/app/Http/ViewHelpers/Company/Group/GroupMembersViewHelper.php @@ -0,0 +1,89 @@ +employees() + ->where('locked', false) + ->with('position') + ->orderBy('pivot_created_at', 'desc') + ->get(); + + $membersCollection = collect([]); + foreach ($members as $member) { + $membersCollection->push([ + 'id' => $member->id, + 'name' => $member->name, + 'avatar' => ImageHelper::getAvatar($member), + 'added_at' => DateHelper::formatDate($member->pivot->created_at), + 'position' => (! $member->position) ? null : [ + 'id' => $member->position->id, + 'title' => $member->position->title, + ], + 'url' => route('employees.show', [ + 'company' => $group->company_id, + 'employee' => $member, + ]), + ]); + } + + return $membersCollection; + } + + /** + * Returns the potential employees that can be assigned as members of the + * group, matching the name. + * This filters out the current members of the group (doh). + * + * @param Group $group + * @param Company $company + * @param string $criteria + * @return Collection + */ + public static function potentialMembers(Company $company, Group $group, string $criteria): Collection + { + $potentialEmployees = $company->employees() + ->select('id', 'first_name', 'last_name', 'avatar_file_id') + ->notLocked() + ->where(function ($query) use ($criteria) { + $query->where('first_name', 'LIKE', '%'.$criteria.'%') + ->orWhere('last_name', 'LIKE', '%'.$criteria.'%') + ->orWhere('email', 'LIKE', '%'.$criteria.'%'); + }) + ->orderBy('last_name', 'asc') + ->take(10) + ->get(); + + $currentMembers = $group->employees() + ->select('id', 'first_name', 'last_name', 'avatar_file_id') + ->get(); + + $potentialEmployees = $potentialEmployees->diff($currentMembers); + + $potentialEmployeesCollection = collect([]); + foreach ($potentialEmployees as $potential) { + $potentialEmployeesCollection->push([ + 'id' => $potential->id, + 'name' => $potential->name, + 'avatar' => ImageHelper::getAvatar($potential, 64), + ]); + } + + return $potentialEmployeesCollection; + } +} diff --git a/app/Http/ViewHelpers/Company/Group/GroupShowViewHelper.php b/app/Http/ViewHelpers/Company/Group/GroupShowViewHelper.php new file mode 100644 index 000000000..d1730ca1a --- /dev/null +++ b/app/Http/ViewHelpers/Company/Group/GroupShowViewHelper.php @@ -0,0 +1,168 @@ +employees() + ->notLocked() + ->orderBy('last_name', 'asc') + ->get(); + + $membersCollection = collect([]); + foreach ($groupMembers as $employee) { + $membersCollection->push([ + 'id' => $employee->id, + 'name' => $employee->name, + 'avatar' => ImageHelper::getAvatar($employee, 32), + 'position' => (! $employee->position) ? null : [ + 'id' => $employee->position->id, + 'title' => $employee->position->title, + ], + 'url' => route('employees.show', [ + 'company' => $company, + 'employee' => $employee, + ]), + ]); + } + + return [ + 'id' => $group->id, + 'name' => $group->name, + 'mission' => StringHelper::parse($group->mission), + 'members' => $membersCollection, + 'url_edit' => route('groups.edit', [ + 'company' => $company, + 'group' => $group, + ]), + 'url_delete' => route('groups.delete', [ + 'company' => $company, + 'group' => $group, + ]), + ]; + } + + /** + * Get the latest 3 meetings in the group. + * + * @param Group $group + * @return Collection + */ + public static function meetings(Group $group): Collection + { + $meetings = $group->meetings() + ->orderBy('happened_at', 'desc') + ->with('employees') + ->take(3) + ->get(); + + $meetingsCollection = collect([]); + foreach ($meetings as $meeting) { + $members = $meeting->employees() + ->inRandomOrder() + ->take(3) + ->get(); + + $totalMembersCount = $meeting->employees()->count(); + $totalMembersCount = $totalMembersCount - $members->count(); + + $membersCollection = collect([]); + foreach ($members as $member) { + $membersCollection->push([ + 'id' => $member->id, + 'avatar' => ImageHelper::getAvatar($member, 25), + 'url' => route('employees.show', [ + 'company' => $group->company_id, + 'employee' => $member, + ]), + ]); + } + + $meetingsCollection->push([ + 'id' => $meeting->id, + 'happened_at' => trans('group.meeting_index_item_title', ['date' => DateHelper::formatDate($meeting->happened_at)]), + 'url' => route('groups.meetings.show', [ + 'company' => $group->company_id, + 'group' => $group, + 'meeting' => $meeting, + ]), + 'preview_members' => $membersCollection, + 'remaining_members_count' => $totalMembersCount, + ]); + } + + return $meetingsCollection; + } + + /** + * Get the statistics of the group. + * + * @param Group $group + * @return array + */ + public static function stats(Group $group): array + { + $numberOfMeetings = $group->meetings()->count(); + + $allDatesOfMeetings = DB::table('meetings') + ->where('group_id', $group->id) + ->orderBy('happened_at', 'asc') + ->select('happened_at') + ->get() + ->pluck('happened_at') + ->toArray(); + + $datesCollection = collect(); + for ($i = 0; $i < count($allDatesOfMeetings) - 1; $i++) { + $date = Carbon::createFromFormat('Y-m-d H:i:s', $allDatesOfMeetings[$i]); + $nextDate = Carbon::createFromFormat('Y-m-d H:i:s', $allDatesOfMeetings[$i + 1]); + + $numberOfDays = $date->diffInDays($nextDate); + $datesCollection->push($numberOfDays); + } + + $frequency = null; + if (count($allDatesOfMeetings) >= 2) { + $frequency = $datesCollection->avg(); + } + + return [ + 'number_of_meetings' => $numberOfMeetings, + 'frequency' => round($frequency), + ]; + } + + /** + * Get the information about the group, required for editing it. + * + * @param Group $group + * @param Company $company + * @return array + */ + public static function edit(Group $group): array + { + return [ + 'id' => $group->id, + 'name' => $group->name, + 'mission' => $group->mission, + ]; + } +} diff --git a/app/Http/ViewHelpers/Company/Group/GroupViewHelper.php b/app/Http/ViewHelpers/Company/Group/GroupViewHelper.php new file mode 100644 index 000000000..2213b9998 --- /dev/null +++ b/app/Http/ViewHelpers/Company/Group/GroupViewHelper.php @@ -0,0 +1,160 @@ +groups() + ->with('employees') + ->orderBy('id', 'desc')->get(); + + $groupsCollection = collect([]); + foreach ($groups as $group) { + $members = $group->employees() + ->inRandomOrder() + ->take(3) + ->get(); + + $totalMembersCount = $group->employees()->count(); + $totalMembersCount = $totalMembersCount - $members->count(); + + $membersCollection = collect([]); + foreach ($members as $member) { + $membersCollection->push([ + 'id' => $member->id, + 'avatar' => ImageHelper::getAvatar($member, 25), + 'url' => route('employees.show', [ + 'company' => $group->company_id, + 'employee' => $member, + ]), + ]); + } + + $groupsCollection->push([ + 'id' => $group->id, + 'name' => $group->name, + 'mission' => $group->mission, + 'preview_members' => $membersCollection, + 'remaining_members_count' => $totalMembersCount, + 'url' => route('groups.show', [ + 'company' => $company, + 'group' => $group, + ]), + ]); + } + + return [ + 'data' => $groupsCollection, + 'url_create' => route('groups.new', [ + 'company' => $company, + ]), + ]; + } + + /** + * Get the latest 3 meetings in the group. + * + * @param Group $group + * @return Collection + */ + public static function meetings(Group $group): Collection + { + $meetings = $group->meetings() + ->orderBy('happened_at', 'desc') + ->with('employees') + ->take(3) + ->get(); + + $meetingsCollection = collect([]); + foreach ($meetings as $meeting) { + $members = $meeting->employees() + ->inRandomOrder() + ->take(3) + ->get(); + + $totalMembersCount = $meeting->employees()->count(); + $totalMembersCount = $totalMembersCount - $members->count(); + + $membersCollection = collect([]); + foreach ($members as $member) { + $membersCollection->push([ + 'id' => $member->id, + 'avatar' => ImageHelper::getAvatar($member, 25), + 'url' => route('employees.show', [ + 'company' => $group->company_id, + 'employee' => $member, + ]), + ]); + } + + $meetingsCollection->push([ + 'id' => $meeting->id, + 'happened_at' => trans('group.meeting_index_item_title', ['date' => DateHelper::formatDate($meeting->happened_at)]), + 'url' => route('groups.meetings.show', [ + 'company' => $group->company_id, + 'group' => $group, + 'meeting' => $meeting, + ]), + 'preview_members' => $membersCollection, + 'remaining_members_count' => $totalMembersCount, + ]); + } + + return $meetingsCollection; + } + + /** + * Get the statistics of the group. + * + * @param Group $group + * @return array + */ + public static function stats(Group $group): array + { + $numberOfMeetings = $group->meetings()->count(); + + $allDatesOfMeetings = DB::table('meetings') + ->where('group_id', $group->id) + ->orderBy('happened_at', 'asc') + ->select('happened_at') + ->get() + ->pluck('happened_at') + ->toArray(); + + $datesCollection = collect(); + for ($i = 0; $i < count($allDatesOfMeetings) - 1; $i++) { + $date = Carbon::createFromFormat('Y-m-d H:i:s', $allDatesOfMeetings[$i]); + $nextDate = Carbon::createFromFormat('Y-m-d H:i:s', $allDatesOfMeetings[$i + 1]); + + $numberOfDays = $date->diffInDays($nextDate); + $datesCollection->push($numberOfDays); + } + + $frequency = null; + if (count($allDatesOfMeetings) >= 2) { + $frequency = $datesCollection->avg(); + } + + return [ + 'number_of_meetings' => $numberOfMeetings, + 'frequency' => $frequency, + ]; + } +} diff --git a/app/Jobs/AttachEmployeeToGroup.php b/app/Jobs/AttachEmployeeToGroup.php new file mode 100644 index 000000000..85351d50b --- /dev/null +++ b/app/Jobs/AttachEmployeeToGroup.php @@ -0,0 +1,41 @@ +data = $data; + } + + /** + * Execute the job. + */ + public function handle(): void + { + (new AddEmployeeToGroup)->execute([ + 'company_id' => $this->data['company_id'], + 'author_id' => $this->data['author_id'], + 'employee_id' => $this->data['employee_id'], + 'group_id' => $this->data['group_id'], + ]); + } +} diff --git a/app/Models/Company/AgendaItem.php b/app/Models/Company/AgendaItem.php new file mode 100644 index 000000000..c880c00b9 --- /dev/null +++ b/app/Models/Company/AgendaItem.php @@ -0,0 +1,69 @@ + 'boolean', + ]; + + /** + * Get the meeting record associated with the agenda item. + * + * @return BelongsTo + */ + public function meeting() + { + return $this->belongsTo(Meeting::class); + } + + /** + * Get the employee record associated with the agenda item. + * + * @return HasOne + */ + public function presenter() + { + return $this->hasOne(Employee::class, 'id', 'presented_by_id'); + } + + /** + * Get the decisions associated with the agenda item. + * + * @return HasMany + */ + public function decisions() + { + return $this->hasMany(MeetingDecision::class); + } +} diff --git a/app/Models/Company/Company.php b/app/Models/Company/Company.php index 7b2efb42e..1eca91904 100644 --- a/app/Models/Company/Company.php +++ b/app/Models/Company/Company.php @@ -250,6 +250,16 @@ public function importJobs() return $this->hasMany(ImportJob::class); } + /** + * Get all groups in the company. + * + * @return HasMany + */ + public function groups() + { + return $this->hasMany(Group::class); + } + /** * Get the logo associated with the company. * diff --git a/app/Models/Company/Employee.php b/app/Models/Company/Employee.php index 25eb3d5f4..7bd0d7f39 100644 --- a/app/Models/Company/Employee.php +++ b/app/Models/Company/Employee.php @@ -509,6 +509,36 @@ public function timesheetsAsApprover() return $this->hasMany(Timesheet::class, 'approver_id', 'id'); } + /** + * Get the group records associated with the employee. + * + * @return BelongsToMany + */ + public function groups() + { + return $this->belongsToMany(Group::class)->withTimestamps(); + } + + /** + * Get the meeting objects the employee has participated. + * + * @return belongsToMany + */ + public function meetings() + { + return $this->belongsToMany(Meeting::class)->withTimestamps()->withPivot('was_a_guest', 'attended'); + } + + /** + * Get the agenda item objects presented by the employee. + * + * @return HasMany + */ + public function agendaItems() + { + return $this->hasMany(AgendaItem::class, 'presented_by_id'); + } + /** * Get the file records associated with the employee as the uploader. * diff --git a/app/Models/Company/Group.php b/app/Models/Company/Group.php new file mode 100644 index 000000000..922d66a3a --- /dev/null +++ b/app/Models/Company/Group.php @@ -0,0 +1,55 @@ +belongsTo(Company::class); + } + + /** + * Get the employee records associated with the group. + * + * @return BelongsToMany + */ + public function employees() + { + return $this->belongsToMany(Employee::class)->withTimestamps(); + } + + /** + * Get the meeting records associated with the group. + * + * @return HasMany + */ + public function meetings() + { + return $this->hasMany(Meeting::class); + } +} diff --git a/app/Models/Company/Meeting.php b/app/Models/Company/Meeting.php new file mode 100644 index 000000000..8e7b9efc2 --- /dev/null +++ b/app/Models/Company/Meeting.php @@ -0,0 +1,73 @@ + 'boolean', + ]; + + /** + * Get the group record associated with the meeting. + * + * @return BelongsTo + */ + public function group() + { + return $this->belongsTo(Group::class); + } + + /** + * Get the employee records associated with the meeting. + * + * @return BelongsToMany + */ + public function employees() + { + return $this->belongsToMany(Employee::class)->withTimestamps()->withPivot('was_a_guest', 'attended'); + } + + /** + * Get the agenda item records associated with the meeting. + * + * @return hasMany + */ + public function agendaItems() + { + return $this->hasMany(AgendaItem::class); + } +} diff --git a/app/Models/Company/MeetingDecision.php b/app/Models/Company/MeetingDecision.php new file mode 100644 index 000000000..bdcaf70c0 --- /dev/null +++ b/app/Models/Company/MeetingDecision.php @@ -0,0 +1,34 @@ +belongsTo(AgendaItem::class); + } +} diff --git a/app/Services/Company/Group/AddEmployeeToGroup.php b/app/Services/Company/Group/AddEmployeeToGroup.php new file mode 100644 index 000000000..24debbada --- /dev/null +++ b/app/Services/Company/Group/AddEmployeeToGroup.php @@ -0,0 +1,105 @@ + 'required|integer|exists:companies,id', + 'author_id' => 'required|integer|exists:employees,id', + 'group_id' => 'required|integer|exists:groups,id', + 'employee_id' => 'required|integer|exists:employees,id', + 'role' => 'nullable|string', + ]; + } + + /** + * Add an employee to a group. + * + * @param array $data + * @return Employee + */ + public function execute(array $data): Employee + { + $this->data = $data; + $this->validate(); + + $this->attachEmployee(); + $this->log(); + + return $this->employee; + } + + private function validate(): void + { + $this->validateRules($this->data); + + $this->author($this->data['author_id']) + ->inCompany($this->data['company_id']) + ->asNormalUser() + ->canExecuteService(); + + $this->employee = $this->validateEmployeeBelongsToCompany($this->data); + + $this->group = Group::where('company_id', $this->data['company_id']) + ->findOrFail($this->data['group_id']); + } + + private function attachEmployee(): void + { + $this->group->employees()->syncWithoutDetaching([ + $this->data['employee_id'] => [ + 'role' => $this->valueOrNull($this->data, 'role'), + ], + ]); + } + + private function log(): void + { + LogAccountAudit::dispatch([ + 'company_id' => $this->data['company_id'], + 'action' => 'employee_added_to_group', + 'author_id' => $this->author->id, + 'author_name' => $this->author->name, + 'audited_at' => Carbon::now(), + 'objects' => json_encode([ + 'group_id' => $this->group->id, + 'group_name' => $this->group->name, + 'employee_id' => $this->employee->id, + 'employee_name' => $this->employee->name, + ]), + ])->onQueue('low'); + + LogEmployeeAudit::dispatch([ + 'employee_id' => $this->employee->id, + 'action' => 'employee_added_to_group', + 'author_id' => $this->author->id, + 'author_name' => $this->author->name, + 'audited_at' => Carbon::now(), + 'objects' => json_encode([ + 'group_id' => $this->group->id, + 'group_name' => $this->group->name, + ]), + ])->onQueue('low'); + } +} diff --git a/app/Services/Company/Group/AddGuestToMeeting.php b/app/Services/Company/Group/AddGuestToMeeting.php new file mode 100644 index 000000000..88d44ff06 --- /dev/null +++ b/app/Services/Company/Group/AddGuestToMeeting.php @@ -0,0 +1,121 @@ + 'required|integer|exists:companies,id', + 'author_id' => 'required|integer|exists:employees,id', + 'group_id' => 'required|integer|exists:groups,id', + 'meeting_id' => 'required|integer|exists:meetings,id', + 'employee_id' => 'required|integer|exists:employees,id', + ]; + } + + /** + * Add an employee as guest in the meeting. + * + * @param array $data + * @return Employee + */ + public function execute(array $data): Employee + { + $this->data = $data; + $this->validate(); + $this->attachEmployee(); + + return $this->employee; + } + + private function validate(): void + { + $this->validateRules($this->data); + + $this->author($this->data['author_id']) + ->inCompany($this->data['company_id']) + ->asNormalUser() + ->canExecuteService(); + + $this->employee = $this->validateEmployeeBelongsToCompany($this->data); + + $this->group = Group::where('company_id', $this->data['company_id']) + ->findOrFail($this->data['group_id']); + + $this->meeting = Meeting::where('group_id', $this->data['group_id']) + ->findOrFail($this->data['meeting_id']); + } + + private function attachEmployee(): void + { + if (! $this->isEmployeePartOfGroup()) { + $this->meeting->employees()->syncWithoutDetaching([ + $this->data['employee_id'] => [ + 'was_a_guest' => true, + ], + ]); + + $this->log(); + } + } + + private function isEmployeePartOfGroup(): bool + { + return DB::table('employee_group') + ->where('employee_id', $this->data['employee_id']) + ->where('group_id', $this->group->id) + ->count() == 1; + } + + private function log(): void + { + LogAccountAudit::dispatch([ + 'company_id' => $this->data['company_id'], + 'action' => 'add_guest_to_meeting', + 'author_id' => $this->author->id, + 'author_name' => $this->author->name, + 'audited_at' => Carbon::now(), + 'objects' => json_encode([ + 'group_id' => $this->group->id, + 'group_name' => $this->group->name, + 'employee_id' => $this->employee->id, + 'employee_name' => $this->employee->name, + 'meeting_id' => $this->meeting->id, + ]), + ])->onQueue('low'); + + LogEmployeeAudit::dispatch([ + 'employee_id' => $this->employee->id, + 'action' => 'add_guest_to_meeting', + 'author_id' => $this->author->id, + 'author_name' => $this->author->name, + 'audited_at' => Carbon::now(), + 'objects' => json_encode([ + 'group_id' => $this->group->id, + 'group_name' => $this->group->name, + 'meeting_id' => $this->meeting->id, + ]), + ])->onQueue('low'); + } +} diff --git a/app/Services/Company/Group/CreateAgendaItem.php b/app/Services/Company/Group/CreateAgendaItem.php new file mode 100644 index 000000000..f9f77665b --- /dev/null +++ b/app/Services/Company/Group/CreateAgendaItem.php @@ -0,0 +1,120 @@ + 'required|integer|exists:companies,id', + 'author_id' => 'required|integer|exists:employees,id', + 'group_id' => 'required|integer|exists:groups,id', + 'meeting_id' => 'required|integer|exists:meetings,id', + 'summary' => 'required|string|max:255', + 'description' => 'nullable|string|max:65535', + 'presented_by_id' => 'nullable|integer|exists:employees,id', + ]; + } + + /** + * Create an agenda item in a meeting. + * + * @param array $data + * @return AgendaItem + */ + public function execute(array $data): AgendaItem + { + $this->data = $data; + $this->validate(); + $this->createAgendaItem(); + $this->log(); + + return $this->agendaItem; + } + + private function validate(): void + { + $this->validateRules($this->data); + + $this->author($this->data['author_id']) + ->inCompany($this->data['company_id']) + ->asNormalUser() + ->canExecuteService(); + + $this->group = Group::where('company_id', $this->data['company_id']) + ->findOrFail($this->data['group_id']); + + $this->meeting = Meeting::where('group_id', $this->data['group_id']) + ->findOrFail($this->data['meeting_id']); + + if ($this->data['presented_by_id']) { + $this->presenter = Employee::where('company_id', $this->data['company_id']) + ->findOrFail($this->data['presented_by_id']); + } + } + + private function createAgendaItem(): void + { + // get the position of the agenda item + $max = AgendaItem::where('meeting_id', $this->meeting->id) + ->max('position'); + + $this->agendaItem = AgendaItem::create([ + 'meeting_id' => $this->data['meeting_id'], + 'position' => $max + 1, + 'summary' => $this->data['summary'], + 'description' => $this->data['description'], + 'presented_by_id' => $this->data['presented_by_id'] ? $this->data['presented_by_id'] : null, + ]); + } + + private function log(): void + { + LogAccountAudit::dispatch([ + 'company_id' => $this->data['company_id'], + 'action' => 'agenda_item_created', + 'author_id' => $this->author->id, + 'author_name' => $this->author->name, + 'audited_at' => Carbon::now(), + 'objects' => json_encode([ + 'group_id' => $this->group->id, + 'group_name' => $this->group->name, + 'meeting_id' => $this->meeting->id, + ]), + ])->onQueue('low'); + + LogEmployeeAudit::dispatch([ + 'employee_id' => $this->author->id, + 'action' => 'agenda_item_created', + 'author_id' => $this->author->id, + 'author_name' => $this->author->name, + 'audited_at' => Carbon::now(), + 'objects' => json_encode([ + 'group_id' => $this->group->id, + 'group_name' => $this->group->name, + 'meeting_id' => $this->meeting->id, + ]), + ])->onQueue('low'); + } +} diff --git a/app/Services/Company/Group/CreateGroup.php b/app/Services/Company/Group/CreateGroup.php new file mode 100644 index 000000000..1b3defeb6 --- /dev/null +++ b/app/Services/Company/Group/CreateGroup.php @@ -0,0 +1,96 @@ + 'required|integer|exists:companies,id', + 'author_id' => 'required|integer|exists:employees,id', + 'name' => 'required|string|max:255', + 'employees' => 'nullable|array', + ]; + } + + /** + * Create a group. + * + * @param array $data + * @return Group + */ + public function execute(array $data): Group + { + $this->data = $data; + $this->validate(); + $this->createGroup(); + $this->attachEmployees(); + $this->log(); + + return $this->group; + } + + private function validate(): void + { + $this->validateRules($this->data); + + $this->author($this->data['author_id']) + ->inCompany($this->data['company_id']) + ->asNormalUser() + ->canExecuteService(); + } + + private function createGroup(): void + { + $this->group = Group::create([ + 'company_id' => $this->data['company_id'], + 'name' => $this->data['name'], + ]); + } + + private function attachEmployees(): void + { + if (! $this->data['employees']) { + return; + } + + foreach ($this->data['employees'] as $key => $employeeId) { + AttachEmployeeToGroup::dispatch([ + 'company_id' => $this->data['company_id'], + 'author_id' => $this->data['author_id'], + 'employee_id' => $employeeId, + 'group_id' => $this->group->id, + ])->onQueue('low'); + } + } + + private function log(): void + { + LogAccountAudit::dispatch([ + 'company_id' => $this->data['company_id'], + 'action' => 'group_created', + 'author_id' => $this->author->id, + 'author_name' => $this->author->name, + 'audited_at' => Carbon::now(), + 'objects' => json_encode([ + 'group_id' => $this->group->id, + 'group_name' => $this->group->name, + ]), + ])->onQueue('low'); + } +} diff --git a/app/Services/Company/Group/CreateMeeting.php b/app/Services/Company/Group/CreateMeeting.php new file mode 100644 index 000000000..7f1e0d4c2 --- /dev/null +++ b/app/Services/Company/Group/CreateMeeting.php @@ -0,0 +1,95 @@ + 'required|integer|exists:companies,id', + 'author_id' => 'required|integer|exists:employees,id', + 'group_id' => 'required|integer|exists:groups,id', + ]; + } + + /** + * Create a meeting. + * + * @param array $data + * @return Meeting + */ + public function execute(array $data): Meeting + { + $this->data = $data; + $this->validate(); + $this->createMeeting(); + $this->addParticipants(); + $this->log(); + + return $this->meeting; + } + + private function validate(): void + { + $this->validateRules($this->data); + + $this->author($this->data['author_id']) + ->inCompany($this->data['company_id']) + ->asNormalUser() + ->canExecuteService(); + + $this->group = Group::where('company_id', $this->data['company_id']) + ->findOrFail($this->data['group_id']); + } + + private function createMeeting(): void + { + $this->meeting = Meeting::create([ + 'group_id' => $this->data['group_id'], + 'happened_at' => Carbon::now()->format('Y-m-d'), + ]); + } + + private function addParticipants(): void + { + $members = $this->group->employees() + ->select('id') + ->get() + ->pluck('id') + ->toArray(); + + $this->meeting->employees()->syncWithoutDetaching($members); + } + + private function log(): void + { + LogAccountAudit::dispatch([ + 'company_id' => $this->data['company_id'], + 'action' => 'meeting_created', + 'author_id' => $this->author->id, + 'author_name' => $this->author->name, + 'audited_at' => Carbon::now(), + 'objects' => json_encode([ + 'group_id' => $this->group->id, + 'group_name' => $this->group->name, + 'meeting_id' => $this->meeting->id, + ]), + ])->onQueue('low'); + } +} diff --git a/app/Services/Company/Group/CreateMeetingDecision.php b/app/Services/Company/Group/CreateMeetingDecision.php new file mode 100644 index 000000000..d146c4a60 --- /dev/null +++ b/app/Services/Company/Group/CreateMeetingDecision.php @@ -0,0 +1,110 @@ + 'required|integer|exists:companies,id', + 'author_id' => 'required|integer|exists:employees,id', + 'group_id' => 'required|integer|exists:groups,id', + 'meeting_id' => 'required|integer|exists:meetings,id', + 'agenda_item_id' => 'required|integer|exists:agenda_items,id', + 'description' => 'required|string|max:65535', + ]; + } + + /** + * Create a decision about an agenda item in a meeting. + * + * @param array $data + * @return MeetingDecision + */ + public function execute(array $data): MeetingDecision + { + $this->data = $data; + $this->validate(); + $this->createDecision(); + $this->log(); + + return $this->meetingDecision; + } + + private function validate(): void + { + $this->validateRules($this->data); + + $this->author($this->data['author_id']) + ->inCompany($this->data['company_id']) + ->asNormalUser() + ->canExecuteService(); + + $this->group = Group::where('company_id', $this->data['company_id']) + ->findOrFail($this->data['group_id']); + + $this->meeting = Meeting::where('group_id', $this->data['group_id']) + ->findOrFail($this->data['meeting_id']); + + $this->agendaItem = AgendaItem::where('meeting_id', $this->data['meeting_id']) + ->findOrFail($this->data['agenda_item_id']); + } + + private function createDecision(): void + { + $this->meetingDecision = MeetingDecision::create([ + 'agenda_item_id' => $this->data['agenda_item_id'], + 'description' => $this->data['description'], + ]); + } + + private function log(): void + { + LogAccountAudit::dispatch([ + 'company_id' => $this->data['company_id'], + 'action' => 'meeting_decision_created', + 'author_id' => $this->author->id, + 'author_name' => $this->author->name, + 'audited_at' => Carbon::now(), + 'objects' => json_encode([ + 'group_id' => $this->group->id, + 'group_name' => $this->group->name, + 'meeting_id' => $this->meeting->id, + ]), + ])->onQueue('low'); + + LogEmployeeAudit::dispatch([ + 'employee_id' => $this->author->id, + 'action' => 'meeting_decision_created', + 'author_id' => $this->author->id, + 'author_name' => $this->author->name, + 'audited_at' => Carbon::now(), + 'objects' => json_encode([ + 'group_id' => $this->group->id, + 'group_name' => $this->group->name, + 'meeting_id' => $this->meeting->id, + ]), + ])->onQueue('low'); + } +} diff --git a/app/Services/Company/Group/DestroyAgendaItem.php b/app/Services/Company/Group/DestroyAgendaItem.php new file mode 100644 index 000000000..974b6d4c1 --- /dev/null +++ b/app/Services/Company/Group/DestroyAgendaItem.php @@ -0,0 +1,102 @@ + 'required|integer|exists:companies,id', + 'author_id' => 'required|integer|exists:employees,id', + 'group_id' => 'nullable|integer|exists:groups,id', + 'meeting_id' => 'nullable|integer|exists:meetings,id', + 'agenda_item_id' => 'nullable|integer|exists:agenda_items,id', + ]; + } + + /** + * Delete an agenda item. + * + * @param array $data + */ + public function execute(array $data): void + { + $this->data = $data; + $this->validate(); + $this->destroy(); + $this->reorderPosition(); + $this->log(); + } + + private function validate(): void + { + $this->validateRules($this->data); + + $this->author($this->data['author_id']) + ->inCompany($this->data['company_id']) + ->asNormalUser() + ->canExecuteService(); + + $this->group = Group::where('company_id', $this->data['company_id']) + ->findOrFail($this->data['group_id']); + + $this->meeting = Meeting::where('group_id', $this->data['group_id']) + ->findOrFail($this->data['meeting_id']); + + $this->agendaItem = AgendaItem::where('meeting_id', $this->data['meeting_id']) + ->findOrFail($this->data['agenda_item_id']); + } + + private function destroy(): void + { + $this->agendaItem->delete(); + } + + private function reorderPosition(): void + { + $formerPosition = $this->agendaItem->position; + + $agendaItems = AgendaItem::where('meeting_id', $this->meeting->id) + ->where('position', '>', $formerPosition) + ->get(); + + foreach ($agendaItems as $item) { + $item->position = $item->position - 1; + $item->save(); + } + } + + private function log(): void + { + LogAccountAudit::dispatch([ + 'company_id' => $this->data['company_id'], + 'action' => 'agenda_item_destroyed', + 'author_id' => $this->author->id, + 'author_name' => $this->author->name, + 'audited_at' => Carbon::now(), + 'objects' => json_encode([ + 'group_id' => $this->group->id, + 'group_name' => $this->group->name, + 'meeting_id' => $this->meeting->id, + ]), + ])->onQueue('low'); + } +} diff --git a/app/Services/Company/Group/DestroyGroup.php b/app/Services/Company/Group/DestroyGroup.php new file mode 100644 index 000000000..fcc58763e --- /dev/null +++ b/app/Services/Company/Group/DestroyGroup.php @@ -0,0 +1,74 @@ + 'required|integer|exists:companies,id', + 'author_id' => 'required|integer|exists:employees,id', + 'group_id' => 'nullable|integer|exists:groups,id', + ]; + } + + /** + * Delete a group. + * + * @param array $data + */ + public function execute(array $data): void + { + $this->data = $data; + $this->validate(); + $this->destroy(); + $this->log(); + } + + private function validate(): void + { + $this->validateRules($this->data); + + $this->author($this->data['author_id']) + ->inCompany($this->data['company_id']) + ->asNormalUser() + ->canExecuteService(); + + // make sure the group belongs to the company + $this->group = Group::where('company_id', $this->data['company_id']) + ->findOrFail($this->data['group_id']); + } + + private function destroy(): void + { + $this->group->delete(); + } + + private function log(): void + { + LogAccountAudit::dispatch([ + 'company_id' => $this->data['company_id'], + 'action' => 'group_destroyed', + 'author_id' => $this->author->id, + 'author_name' => $this->author->name, + 'audited_at' => Carbon::now(), + 'objects' => json_encode([ + 'group_name' => $this->group->name, + ]), + ])->onQueue('low'); + } +} diff --git a/app/Services/Company/Group/DestroyMeeting.php b/app/Services/Company/Group/DestroyMeeting.php new file mode 100644 index 000000000..05c06e81d --- /dev/null +++ b/app/Services/Company/Group/DestroyMeeting.php @@ -0,0 +1,79 @@ + 'required|integer|exists:companies,id', + 'author_id' => 'required|integer|exists:employees,id', + 'group_id' => 'nullable|integer|exists:groups,id', + 'meeting_id' => 'nullable|integer|exists:meetings,id', + ]; + } + + /** + * Delete a meeting. + * + * @param array $data + */ + public function execute(array $data): void + { + $this->data = $data; + $this->validate(); + $this->destroy(); + $this->log(); + } + + private function validate(): void + { + $this->validateRules($this->data); + + $this->author($this->data['author_id']) + ->inCompany($this->data['company_id']) + ->asNormalUser() + ->canExecuteService(); + + $this->group = Group::where('company_id', $this->data['company_id']) + ->findOrFail($this->data['group_id']); + + $this->meeting = Meeting::where('group_id', $this->data['group_id']) + ->findOrFail($this->data['meeting_id']); + } + + private function destroy(): void + { + $this->meeting->delete(); + } + + private function log(): void + { + LogAccountAudit::dispatch([ + 'company_id' => $this->data['company_id'], + 'action' => 'meeting_destroyed', + 'author_id' => $this->author->id, + 'author_name' => $this->author->name, + 'audited_at' => Carbon::now(), + 'objects' => json_encode([ + 'group_name' => $this->group->name, + ]), + ])->onQueue('low'); + } +} diff --git a/app/Services/Company/Group/DestroyMeetingDecision.php b/app/Services/Company/Group/DestroyMeetingDecision.php new file mode 100644 index 000000000..f1b896990 --- /dev/null +++ b/app/Services/Company/Group/DestroyMeetingDecision.php @@ -0,0 +1,91 @@ + 'required|integer|exists:companies,id', + 'author_id' => 'required|integer|exists:employees,id', + 'group_id' => 'nullable|integer|exists:groups,id', + 'meeting_id' => 'nullable|integer|exists:meetings,id', + 'agenda_item_id' => 'nullable|integer|exists:agenda_items,id', + 'meeting_decision_id' => 'nullable|integer|exists:meeting_decisions,id', + ]; + } + + /** + * Delete a meeting decision. + * + * @param array $data + */ + public function execute(array $data): void + { + $this->data = $data; + $this->validate(); + $this->destroy(); + $this->log(); + } + + private function validate(): void + { + $this->validateRules($this->data); + + $this->author($this->data['author_id']) + ->inCompany($this->data['company_id']) + ->asNormalUser() + ->canExecuteService(); + + $this->group = Group::where('company_id', $this->data['company_id']) + ->findOrFail($this->data['group_id']); + + $this->meeting = Meeting::where('group_id', $this->data['group_id']) + ->findOrFail($this->data['meeting_id']); + + $this->agendaItem = AgendaItem::where('meeting_id', $this->data['meeting_id']) + ->findOrFail($this->data['agenda_item_id']); + + $this->meetingDecision = MeetingDecision::where('agenda_item_id', $this->data['agenda_item_id']) + ->findOrFail($this->data['meeting_decision_id']); + } + + private function destroy(): void + { + $this->meetingDecision->delete(); + } + + private function log(): void + { + LogAccountAudit::dispatch([ + 'company_id' => $this->data['company_id'], + 'action' => 'meeting_decision_destroyed', + 'author_id' => $this->author->id, + 'author_name' => $this->author->name, + 'audited_at' => Carbon::now(), + 'objects' => json_encode([ + 'group_name' => $this->group->name, + ]), + ])->onQueue('low'); + } +} diff --git a/app/Services/Company/Group/RemoveEmployeeFromGroup.php b/app/Services/Company/Group/RemoveEmployeeFromGroup.php new file mode 100644 index 000000000..9415eaeae --- /dev/null +++ b/app/Services/Company/Group/RemoveEmployeeFromGroup.php @@ -0,0 +1,97 @@ + 'required|integer|exists:companies,id', + 'author_id' => 'required|integer|exists:employees,id', + 'employee_id' => 'required|integer|exists:employees,id', + 'group_id' => 'required|integer|exists:groups,id', + ]; + } + + /** + * Detach an employee from a group. + * + * @param array $data + */ + public function execute(array $data): void + { + $this->data = $data; + $this->validate(); + $this->detachEmployee(); + $this->log(); + } + + private function validate(): void + { + $this->validateRules($this->data); + + $this->author($this->data['author_id']) + ->inCompany($this->data['company_id']) + ->asAtLeastHR() + ->canBypassPermissionLevelIfEmployee($this->data['employee_id']) + ->canExecuteService(); + + $this->employee = $this->validateEmployeeBelongsToCompany($this->data); + + $this->group = Group::where('company_id', $this->data['company_id']) + ->findOrFail($this->data['group_id']); + } + + private function detachEmployee(): void + { + $this->group->employees()->detach($this->data['employee_id']); + } + + private function log(): void + { + LogAccountAudit::dispatch([ + 'company_id' => $this->data['company_id'], + 'action' => 'employee_removed_from_group', + 'author_id' => $this->author->id, + 'author_name' => $this->author->name, + 'audited_at' => Carbon::now(), + 'objects' => json_encode([ + 'group_id' => $this->group->id, + 'group_name' => $this->group->name, + 'employee_id' => $this->employee->id, + 'employee_name' => $this->employee->name, + ]), + ])->onQueue('low'); + + LogEmployeeAudit::dispatch([ + 'employee_id' => $this->employee->id, + 'action' => 'employee_removed_from_group', + 'author_id' => $this->author->id, + 'author_name' => $this->author->name, + 'audited_at' => Carbon::now(), + 'objects' => json_encode([ + 'group_id' => $this->group->id, + 'group_name' => $this->group->name, + ]), + ])->onQueue('low'); + } +} diff --git a/app/Services/Company/Group/RemoveGuestFromMeeting.php b/app/Services/Company/Group/RemoveGuestFromMeeting.php new file mode 100644 index 000000000..fda124a14 --- /dev/null +++ b/app/Services/Company/Group/RemoveGuestFromMeeting.php @@ -0,0 +1,103 @@ + 'required|integer|exists:companies,id', + 'author_id' => 'required|integer|exists:employees,id', + 'group_id' => 'required|integer|exists:groups,id', + 'meeting_id' => 'required|integer|exists:meetings,id', + 'employee_id' => 'required|integer|exists:employees,id', + ]; + } + + /** + * Remove an employee as guest from a meeting. + * + * @param array $data + */ + public function execute(array $data): void + { + $this->data = $data; + $this->validate(); + + $this->detachEmployee(); + $this->log(); + } + + private function validate(): void + { + $this->validateRules($this->data); + + $this->author($this->data['author_id']) + ->inCompany($this->data['company_id']) + ->asNormalUser() + ->canExecuteService(); + + $this->employee = $this->validateEmployeeBelongsToCompany($this->data); + + $this->group = Group::where('company_id', $this->data['company_id']) + ->findOrFail($this->data['group_id']); + + $this->meeting = Meeting::where('group_id', $this->data['group_id']) + ->findOrFail($this->data['meeting_id']); + } + + private function detachEmployee(): void + { + $this->meeting->employees()->detach($this->data['employee_id']); + } + + private function log(): void + { + LogAccountAudit::dispatch([ + 'company_id' => $this->data['company_id'], + 'action' => 'employee_removed_from_meeting', + 'author_id' => $this->author->id, + 'author_name' => $this->author->name, + 'audited_at' => Carbon::now(), + 'objects' => json_encode([ + 'group_id' => $this->group->id, + 'group_name' => $this->group->name, + 'employee_id' => $this->employee->id, + 'employee_name' => $this->employee->name, + 'meeting_id' => $this->meeting->id, + ]), + ])->onQueue('low'); + + LogEmployeeAudit::dispatch([ + 'employee_id' => $this->employee->id, + 'action' => 'employee_removed_from_meeting', + 'author_id' => $this->author->id, + 'author_name' => $this->author->name, + 'audited_at' => Carbon::now(), + 'objects' => json_encode([ + 'group_id' => $this->group->id, + 'group_name' => $this->group->name, + 'meeting_id' => $this->meeting->id, + ]), + ])->onQueue('low'); + } +} diff --git a/app/Services/Company/Group/ToggleEmployeeParticipationInMeeting.php b/app/Services/Company/Group/ToggleEmployeeParticipationInMeeting.php new file mode 100644 index 000000000..f2b96517e --- /dev/null +++ b/app/Services/Company/Group/ToggleEmployeeParticipationInMeeting.php @@ -0,0 +1,129 @@ + 'required|integer|exists:companies,id', + 'author_id' => 'required|integer|exists:employees,id', + 'group_id' => 'required|integer|exists:groups,id', + 'meeting_id' => 'required|integer|exists:meetings,id', + 'employee_id' => 'required|integer|exists:employees,id', + ]; + } + + /** + * Toggle the participation of an employee in a meeting. + * When marking an employee, we should check if the employee is part of the + * meeting. + * If the employee is not part of the meeting, this process fails. + * + * @param array $data + * @return Employee + */ + public function execute(array $data): Employee + { + $this->data = $data; + $this->validate(); + + $this->attachEmployee(); + $this->log(); + + return $this->employee; + } + + private function validate(): void + { + $this->validateRules($this->data); + + $this->author($this->data['author_id']) + ->inCompany($this->data['company_id']) + ->asNormalUser() + ->canExecuteService(); + + $this->employee = $this->validateEmployeeBelongsToCompany($this->data); + + $this->group = Group::where('company_id', $this->data['company_id']) + ->findOrFail($this->data['group_id']); + + $this->meeting = Meeting::where('group_id', $this->data['group_id']) + ->findOrFail($this->data['meeting_id']); + } + + private function attachEmployee(): void + { + $object = $this->employeeMeeting(); + + if (! $object) { + throw new NotEnoughPermissionException(); + } + + $this->meeting->employees()->syncWithoutDetaching([ + $this->data['employee_id'] => [ + 'attended' => ! $object->attended, + ], + ]); + } + + private function employeeMeeting() + { + return DB::table('employee_meeting') + ->where('employee_id', $this->data['employee_id']) + ->where('meeting_id', $this->meeting->id) + ->first(); + } + + private function log(): void + { + LogAccountAudit::dispatch([ + 'company_id' => $this->data['company_id'], + 'action' => 'employee_marked_as_participant_in_meeting', + 'author_id' => $this->author->id, + 'author_name' => $this->author->name, + 'audited_at' => Carbon::now(), + 'objects' => json_encode([ + 'group_id' => $this->group->id, + 'group_name' => $this->group->name, + 'employee_id' => $this->employee->id, + 'employee_name' => $this->employee->name, + 'meeting_id' => $this->meeting->id, + ]), + ])->onQueue('low'); + + LogEmployeeAudit::dispatch([ + 'employee_id' => $this->employee->id, + 'action' => 'employee_marked_as_participant_in_meeting', + 'author_id' => $this->author->id, + 'author_name' => $this->author->name, + 'audited_at' => Carbon::now(), + 'objects' => json_encode([ + 'group_id' => $this->group->id, + 'group_name' => $this->group->name, + 'meeting_id' => $this->meeting->id, + ]), + ])->onQueue('low'); + } +} diff --git a/app/Services/Company/Group/UpdateAgendaItem.php b/app/Services/Company/Group/UpdateAgendaItem.php new file mode 100644 index 000000000..c9074cbef --- /dev/null +++ b/app/Services/Company/Group/UpdateAgendaItem.php @@ -0,0 +1,120 @@ + 'required|integer|exists:companies,id', + 'author_id' => 'required|integer|exists:employees,id', + 'group_id' => 'required|integer|exists:groups,id', + 'meeting_id' => 'required|integer|exists:meetings,id', + 'agenda_item_id' => 'required|integer|exists:agenda_items,id', + 'summary' => 'required|string|max:255', + 'description' => 'nullable|string|max:65535', + 'presented_by_id' => 'nullable|integer|exists:employees,id', + ]; + } + + /** + * Update an existing agenda item in a meeting. + * + * @param array $data + * @return AgendaItem + */ + public function execute(array $data): AgendaItem + { + $this->data = $data; + $this->validate(); + $this->updateAgendaItem(); + $this->log(); + + $this->agendaItem->refresh(); + + return $this->agendaItem; + } + + private function validate(): void + { + $this->validateRules($this->data); + + $this->author($this->data['author_id']) + ->inCompany($this->data['company_id']) + ->asNormalUser() + ->canExecuteService(); + + $this->group = Group::where('company_id', $this->data['company_id']) + ->findOrFail($this->data['group_id']); + + $this->meeting = Meeting::where('group_id', $this->data['group_id']) + ->findOrFail($this->data['meeting_id']); + + $this->agendaItem = AgendaItem::where('meeting_id', $this->data['meeting_id']) + ->findOrFail($this->data['agenda_item_id']); + + if ($this->data['presented_by_id']) { + $this->presenter = Employee::where('company_id', $this->data['company_id']) + ->findOrFail($this->data['presented_by_id']); + } + } + + private function updateAgendaItem(): void + { + AgendaItem::where('id', $this->agendaItem->id)->update([ + 'summary' => $this->data['summary'], + 'description' => $this->data['description'], + 'presented_by_id' => $this->data['presented_by_id'] ? $this->data['presented_by_id'] : null, + ]); + } + + private function log(): void + { + LogAccountAudit::dispatch([ + 'company_id' => $this->data['company_id'], + 'action' => 'agenda_item_updated', + 'author_id' => $this->author->id, + 'author_name' => $this->author->name, + 'audited_at' => Carbon::now(), + 'objects' => json_encode([ + 'group_id' => $this->group->id, + 'group_name' => $this->group->name, + 'meeting_id' => $this->meeting->id, + ]), + ])->onQueue('low'); + + LogEmployeeAudit::dispatch([ + 'employee_id' => $this->author->id, + 'action' => 'agenda_item_updated', + 'author_id' => $this->author->id, + 'author_name' => $this->author->name, + 'audited_at' => Carbon::now(), + 'objects' => json_encode([ + 'group_id' => $this->group->id, + 'group_name' => $this->group->name, + 'meeting_id' => $this->meeting->id, + ]), + ])->onQueue('low'); + } +} diff --git a/app/Services/Company/Group/UpdateGroup.php b/app/Services/Company/Group/UpdateGroup.php new file mode 100644 index 000000000..bc60a53be --- /dev/null +++ b/app/Services/Company/Group/UpdateGroup.php @@ -0,0 +1,82 @@ + 'required|integer|exists:companies,id', + 'author_id' => 'required|integer|exists:employees,id', + 'group_id' => 'required|integer|exists:groups,id', + 'name' => 'required|string', + 'mission' => 'nullable|string|max:65535', + ]; + } + + /** + * Update group information. + * + * @param array $data + */ + public function execute(array $data): void + { + $this->data = $data; + $this->validate(); + + $this->update(); + $this->log(); + } + + private function validate(): void + { + $this->validateRules($this->data); + + $this->author($this->data['author_id']) + ->inCompany($this->data['company_id']) + ->asNormalUser() + ->canExecuteService(); + + $this->group = Group::where('company_id', $this->data['company_id']) + ->findOrFail($this->data['group_id']); + } + + private function update(): void + { + $this->group->name = $this->data['name']; + $this->group->mission = $this->valueOrNull($this->data, 'mission'); + $this->group->save(); + } + + private function log(): void + { + LogAccountAudit::dispatch([ + 'company_id' => $this->data['company_id'], + 'action' => 'group_updated', + 'author_id' => $this->author->id, + 'author_name' => $this->author->name, + 'audited_at' => Carbon::now(), + 'objects' => json_encode([ + 'group_id' => $this->group->id, + 'group_name' => $this->group->name, + 'group_mission' => $this->valueOrNull($this->data, 'mission'), + ]), + ])->onQueue('low'); + } +} diff --git a/app/Services/Company/Group/UpdateMeetingDate.php b/app/Services/Company/Group/UpdateMeetingDate.php new file mode 100644 index 000000000..e7973b227 --- /dev/null +++ b/app/Services/Company/Group/UpdateMeetingDate.php @@ -0,0 +1,86 @@ + 'required|integer|exists:companies,id', + 'author_id' => 'required|integer|exists:employees,id', + 'group_id' => 'required|integer|exists:groups,id', + 'meeting_id' => 'required|integer|exists:meetings,id', + 'date' => 'required|date_format:Y-m-d', + ]; + } + + /** + * Set the meeting date. + * + * @param array $data + */ + public function execute(array $data): void + { + $this->data = $data; + $this->validate(); + + $this->setDate(); + $this->log(); + } + + private function validate(): void + { + $this->validateRules($this->data); + + $this->author($this->data['author_id']) + ->inCompany($this->data['company_id']) + ->asNormalUser() + ->canExecuteService(); + + $this->group = Group::where('company_id', $this->data['company_id']) + ->findOrFail($this->data['group_id']); + + $this->meeting = Meeting::where('group_id', $this->data['group_id']) + ->findOrFail($this->data['meeting_id']); + } + + private function setDate(): void + { + $this->meeting->happened_at = $this->data['date']; + $this->meeting->save(); + } + + private function log(): void + { + LogAccountAudit::dispatch([ + 'company_id' => $this->data['company_id'], + 'action' => 'meeting_date_set', + 'author_id' => $this->author->id, + 'author_name' => $this->author->name, + 'audited_at' => Carbon::now(), + 'objects' => json_encode([ + 'group_id' => $this->group->id, + 'group_name' => $this->group->name, + 'meeting_id' => $this->meeting->id, + ]), + ])->onQueue('low'); + } +} diff --git a/app/Services/Company/Group/UpdateMeetingDecision.php b/app/Services/Company/Group/UpdateMeetingDecision.php new file mode 100644 index 000000000..d47adea80 --- /dev/null +++ b/app/Services/Company/Group/UpdateMeetingDecision.php @@ -0,0 +1,115 @@ + 'required|integer|exists:companies,id', + 'author_id' => 'required|integer|exists:employees,id', + 'group_id' => 'required|integer|exists:groups,id', + 'meeting_id' => 'required|integer|exists:meetings,id', + 'agenda_item_id' => 'required|integer|exists:agenda_items,id', + 'meeting_decision_id' => 'required|integer|exists:meeting_decisions,id', + 'description' => 'required|string|max:65535', + ]; + } + + /** + * Destroy an existing meeting decision. + * + * @param array $data + * @return MeetingDecision + */ + public function execute(array $data): MeetingDecision + { + $this->data = $data; + $this->validate(); + $this->updateMeetingDecision(); + $this->log(); + + $this->meetingDecision->refresh(); + + return $this->meetingDecision; + } + + private function validate(): void + { + $this->validateRules($this->data); + + $this->author($this->data['author_id']) + ->inCompany($this->data['company_id']) + ->asNormalUser() + ->canExecuteService(); + + $this->group = Group::where('company_id', $this->data['company_id']) + ->findOrFail($this->data['group_id']); + + $this->meeting = Meeting::where('group_id', $this->data['group_id']) + ->findOrFail($this->data['meeting_id']); + + $this->agendaItem = AgendaItem::where('meeting_id', $this->data['meeting_id']) + ->findOrFail($this->data['agenda_item_id']); + + $this->meetingDecision = MeetingDecision::where('agenda_item_id', $this->data['agenda_item_id']) + ->findOrFail($this->data['meeting_decision_id']); + } + + private function updateMeetingDecision(): void + { + MeetingDecision::where('id', $this->meetingDecision->id)->update([ + 'description' => $this->data['description'], + ]); + } + + private function log(): void + { + LogAccountAudit::dispatch([ + 'company_id' => $this->data['company_id'], + 'action' => 'meeting_decision_updated', + 'author_id' => $this->author->id, + 'author_name' => $this->author->name, + 'audited_at' => Carbon::now(), + 'objects' => json_encode([ + 'group_id' => $this->group->id, + 'group_name' => $this->group->name, + 'meeting_id' => $this->meeting->id, + ]), + ])->onQueue('low'); + + LogEmployeeAudit::dispatch([ + 'employee_id' => $this->author->id, + 'action' => 'meeting_decision_updated', + 'author_id' => $this->author->id, + 'author_name' => $this->author->name, + 'audited_at' => Carbon::now(), + 'objects' => json_encode([ + 'group_id' => $this->group->id, + 'group_name' => $this->group->name, + 'meeting_id' => $this->meeting->id, + ]), + ])->onQueue('low'); + } +} diff --git a/database/factories/Company/AgendaItemFactory.php b/database/factories/Company/AgendaItemFactory.php new file mode 100644 index 000000000..306dc7357 --- /dev/null +++ b/database/factories/Company/AgendaItemFactory.php @@ -0,0 +1,34 @@ +create(); + + return [ + 'meeting_id' => $meeting->id, + 'position' => 1, + 'summary' => 'This is the summary', + 'description' => 'This is the description', + ]; + } +} diff --git a/database/factories/Company/GroupFactory.php b/database/factories/Company/GroupFactory.php new file mode 100644 index 000000000..6706f0519 --- /dev/null +++ b/database/factories/Company/GroupFactory.php @@ -0,0 +1,33 @@ +create(); + + return [ + 'company_id' => $company->id, + 'name' => 'Group name', + 'mission' => 'Employees happiness', + ]; + } +} diff --git a/database/factories/Company/MeetingDecisionFactory.php b/database/factories/Company/MeetingDecisionFactory.php new file mode 100644 index 000000000..aa7c5b046 --- /dev/null +++ b/database/factories/Company/MeetingDecisionFactory.php @@ -0,0 +1,32 @@ +create([]); + + return [ + 'agenda_item_id' => $agenda->id, + 'description' => 'This is a description', + ]; + } +} diff --git a/database/factories/Company/MeetingFactory.php b/database/factories/Company/MeetingFactory.php new file mode 100644 index 000000000..9cf47f6cd --- /dev/null +++ b/database/factories/Company/MeetingFactory.php @@ -0,0 +1,45 @@ + Group::factory(), + ]; + } + + /** + * Indicate that the meeting has happened. + * + * @return \Illuminate\Database\Eloquent\Factories\Factory + */ + public function happened() + { + return $this->state(function (array $attributes) { + return [ + 'happened' => true, + 'happened_at' => Carbon::now(), + ]; + }); + } +} diff --git a/database/migrations/2021_03_03_031835_create_groups_table.php b/database/migrations/2021_03_03_031835_create_groups_table.php new file mode 100644 index 000000000..420ad5495 --- /dev/null +++ b/database/migrations/2021_03_03_031835_create_groups_table.php @@ -0,0 +1,75 @@ +id(); + $table->unsignedBigInteger('company_id'); + $table->string('name'); + $table->text('mission')->nullable(); + $table->timestamps(); + $table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade'); + }); + + Schema::create('employee_group', function (Blueprint $table) { + $table->unsignedBigInteger('employee_id'); + $table->unsignedBigInteger('group_id'); + $table->string('role')->nullable(); + $table->timestamps(); + $table->foreign('employee_id')->references('id')->on('employees')->onDelete('cascade'); + $table->foreign('group_id')->references('id')->on('groups')->onDelete('cascade'); + }); + + Schema::create('meetings', function (Blueprint $table) { + $table->id(); + $table->unsignedBigInteger('group_id'); + $table->boolean('happened')->default(false); + $table->datetime('happened_at')->nullable(); + $table->timestamps(); + $table->foreign('group_id')->references('id')->on('groups')->onDelete('cascade'); + }); + + Schema::create('agenda_items', function (Blueprint $table) { + $table->id(); + $table->unsignedBigInteger('meeting_id'); + $table->integer('position'); + $table->boolean('checked')->default(false); + $table->string('summary'); + $table->text('description')->nullable(); + $table->unsignedBigInteger('presented_by_id')->nullable(); + $table->timestamps(); + $table->foreign('meeting_id')->references('id')->on('meetings')->onDelete('cascade'); + $table->foreign('presented_by_id')->references('id')->on('employees')->onDelete('set null'); + }); + + Schema::create('meeting_decisions', function (Blueprint $table) { + $table->id(); + $table->unsignedBigInteger('agenda_item_id'); + $table->text('description'); + $table->timestamps(); + $table->foreign('agenda_item_id')->references('id')->on('agenda_items')->onDelete('cascade'); + }); + + Schema::create('employee_meeting', function (Blueprint $table) { + $table->unsignedBigInteger('employee_id'); + $table->unsignedBigInteger('meeting_id'); + $table->boolean('attended')->default(false); + $table->boolean('was_a_guest')->default(false); + $table->timestamps(); + $table->foreign('employee_id')->references('id')->on('employees')->onDelete('cascade'); + $table->foreign('meeting_id')->references('id')->on('meetings')->onDelete('cascade'); + }); + } +} diff --git a/phpstan.neon b/phpstan.neon index 40b94c28f..8863582c7 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -11,6 +11,11 @@ parameters: excludePaths: - app/Http/ViewHelpers/Employee/EmployeeShowViewHelper.php - app/Http/ViewHelpers/Company/HR/CompanyHRViewHelper.php + - app/Http/ViewHelpers/Company/Group/GroupMembersViewHelper.php + - app/Services/Company/Group/CreateMeeting.php + - app/Http/ViewHelpers/Company/Group/GroupMeetingsViewHelper.php + - app/Http/ViewHelpers/Company/Group/GroupShowViewHelper.php + - app/Http/ViewHelpers/Company/Group/GroupViewHelper.php inferPrivatePropertyTypeFromConstructor: true checkMissingIterableValueType: false reportUnmatchedIgnoredErrors: false diff --git a/phpunit.xml b/phpunit.xml index 91cd68ab0..90ad2f001 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -8,7 +8,7 @@ convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" - stopOnFailure="false" + stopOnFailure="true" > diff --git a/public/humans.txt b/public/humans.txt index 8850cecae..42fbbbe2c 100644 --- a/public/humans.txt +++ b/public/humans.txt @@ -4,3 +4,9 @@ Twitter: @djaiss GitHub: @djaiss From: Montréal, QC, Canada + + Developer: Alexis Saettler + Contact: alexis [at] officelife.io + Twitter: @asbin + GitHub: @asbiin + From: Paris, France diff --git a/public/img/favicon.png b/public/img/favicon.png new file mode 100644 index 000000000..4b5bed27a Binary files /dev/null and b/public/img/favicon.png differ diff --git a/public/img/streamline-icon-coffee-break-2-1@140x140.png b/public/img/streamline-icon-coffee-break-2-1@140x140.png new file mode 100644 index 000000000..5f6b8dcae Binary files /dev/null and b/public/img/streamline-icon-coffee-break-2-1@140x140.png differ diff --git a/public/img/streamline-icon-meeting-table-3@140x140.png b/public/img/streamline-icon-meeting-table-3@140x140.png new file mode 100644 index 000000000..4afae7aa0 Binary files /dev/null and b/public/img/streamline-icon-meeting-table-3@140x140.png differ diff --git a/resources/js/Pages/Company/Group/Create.vue b/resources/js/Pages/Company/Group/Create.vue new file mode 100644 index 000000000..6b22f8ca8 --- /dev/null +++ b/resources/js/Pages/Company/Group/Create.vue @@ -0,0 +1,243 @@ + + + + + diff --git a/resources/js/Pages/Company/Group/Delete.vue b/resources/js/Pages/Company/Group/Delete.vue new file mode 100644 index 000000000..94e4eaa91 --- /dev/null +++ b/resources/js/Pages/Company/Group/Delete.vue @@ -0,0 +1,103 @@ + + + diff --git a/resources/js/Pages/Company/Group/Edit.vue b/resources/js/Pages/Company/Group/Edit.vue new file mode 100644 index 000000000..3770a31f9 --- /dev/null +++ b/resources/js/Pages/Company/Group/Edit.vue @@ -0,0 +1,129 @@ + + + diff --git a/resources/js/Pages/Company/Group/Index.vue b/resources/js/Pages/Company/Group/Index.vue new file mode 100644 index 000000000..410fba5f8 --- /dev/null +++ b/resources/js/Pages/Company/Group/Index.vue @@ -0,0 +1,138 @@ + + + + + diff --git a/resources/js/Pages/Company/Group/Meetings/Index.vue b/resources/js/Pages/Company/Group/Meetings/Index.vue new file mode 100644 index 000000000..f85cc092e --- /dev/null +++ b/resources/js/Pages/Company/Group/Meetings/Index.vue @@ -0,0 +1,213 @@ + + + + + diff --git a/resources/js/Pages/Company/Group/Meetings/Partials/Agenda.vue b/resources/js/Pages/Company/Group/Meetings/Partials/Agenda.vue new file mode 100644 index 000000000..af8f733a7 --- /dev/null +++ b/resources/js/Pages/Company/Group/Meetings/Partials/Agenda.vue @@ -0,0 +1,552 @@ + + + + + diff --git a/resources/js/Pages/Company/Group/Meetings/Partials/Date.vue b/resources/js/Pages/Company/Group/Meetings/Partials/Date.vue new file mode 100644 index 000000000..1242ea25c --- /dev/null +++ b/resources/js/Pages/Company/Group/Meetings/Partials/Date.vue @@ -0,0 +1,147 @@ + + + + + diff --git a/resources/js/Pages/Company/Group/Meetings/Partials/Participants.vue b/resources/js/Pages/Company/Group/Meetings/Partials/Participants.vue new file mode 100644 index 000000000..4c47790ac --- /dev/null +++ b/resources/js/Pages/Company/Group/Meetings/Partials/Participants.vue @@ -0,0 +1,275 @@ + + + + + diff --git a/resources/js/Pages/Company/Group/Meetings/Show.vue b/resources/js/Pages/Company/Group/Meetings/Show.vue new file mode 100644 index 000000000..80f79ee1b --- /dev/null +++ b/resources/js/Pages/Company/Group/Meetings/Show.vue @@ -0,0 +1,141 @@ + + + + + diff --git a/resources/js/Pages/Company/Group/Members/Index.vue b/resources/js/Pages/Company/Group/Members/Index.vue new file mode 100644 index 000000000..834e0ef95 --- /dev/null +++ b/resources/js/Pages/Company/Group/Members/Index.vue @@ -0,0 +1,311 @@ + + + + + diff --git a/resources/js/Pages/Company/Group/Partials/GroupMenu.vue b/resources/js/Pages/Company/Group/Partials/GroupMenu.vue new file mode 100644 index 000000000..2be2b6048 --- /dev/null +++ b/resources/js/Pages/Company/Group/Partials/GroupMenu.vue @@ -0,0 +1,71 @@ + + + + + diff --git a/resources/js/Pages/Company/Group/Show.vue b/resources/js/Pages/Company/Group/Show.vue new file mode 100644 index 000000000..ce56004bc --- /dev/null +++ b/resources/js/Pages/Company/Group/Show.vue @@ -0,0 +1,158 @@ + + + + + diff --git a/resources/js/Pages/Company/Partials/Tab.vue b/resources/js/Pages/Company/Partials/Tab.vue index e2f338b67..52bdd2ee5 100644 --- a/resources/js/Pages/Company/Partials/Tab.vue +++ b/resources/js/Pages/Company/Partials/Tab.vue @@ -7,6 +7,9 @@ {{ $t('company.menu_projects') }} + + {{ $t('company.menu_groups') }} + {{ $t('company.menu_hr') }} diff --git a/resources/js/Pages/Company/Project/Delete.vue b/resources/js/Pages/Company/Project/Delete.vue index c6fdf94d3..d6ef316d2 100644 --- a/resources/js/Pages/Company/Project/Delete.vue +++ b/resources/js/Pages/Company/Project/Delete.vue @@ -34,7 +34,7 @@ -
+
diff --git a/resources/js/Pages/Company/Project/Index.vue b/resources/js/Pages/Company/Project/Index.vue index 5af23f0ac..f7c89e7e6 100644 --- a/resources/js/Pages/Company/Project/Index.vue +++ b/resources/js/Pages/Company/Project/Index.vue @@ -64,9 +64,9 @@

number of teams

{{ statistics.number_of_teams }}

-
-

founded

-

1987

+
+

{{ $t('company.stat_founded_at') }}

+

{{ statistics.founded_at }}

diff --git a/resources/js/Pages/Company/Project/Messages/Show.vue b/resources/js/Pages/Company/Project/Messages/Show.vue index 3335828e4..beb55b743 100644 --- a/resources/js/Pages/Company/Project/Messages/Show.vue +++ b/resources/js/Pages/Company/Project/Messages/Show.vue @@ -122,7 +122,7 @@
  • {{ $t('project.message_show_edit') }}
  • -
  • {{ $t('project.message_show_destroy') }}
  • +
  • {{ $t('project.message_show_destroy') }}
  • {{ $t('app.sure') }} diff --git a/resources/js/Pages/Company/Project/Partials/Description.vue b/resources/js/Pages/Company/Project/Partials/Description.vue index b6f5d2857..de0f70752 100644 --- a/resources/js/Pages/Company/Project/Partials/Description.vue +++ b/resources/js/Pages/Company/Project/Partials/Description.vue @@ -49,7 +49,7 @@
  • - + {{ $t('app.cancel') }} diff --git a/resources/js/Pages/Company/Project/Partials/ProjectMenu.vue b/resources/js/Pages/Company/Project/Partials/ProjectMenu.vue index 02e27292e..3d720588e 100644 --- a/resources/js/Pages/Company/Project/Partials/ProjectMenu.vue +++ b/resources/js/Pages/Company/Project/Partials/ProjectMenu.vue @@ -33,7 +33,7 @@
    -

    Project members

    +

    {{ $t('project.menu_members_icons') }}

    diff --git a/resources/js/Pages/Company/Project/Show.vue b/resources/js/Pages/Company/Project/Show.vue index 40ad36b6c..129ba3da4 100644 --- a/resources/js/Pages/Company/Project/Show.vue +++ b/resources/js/Pages/Company/Project/Show.vue @@ -58,7 +58,7 @@
    • {{ $t('project.summary_edit') }}
    • -
    • {{ $t('project.summary_delete') }}
    • +
    • {{ $t('project.summary_delete') }}
    diff --git a/resources/js/Pages/Company/Project/Tasks/Show.vue b/resources/js/Pages/Company/Project/Tasks/Show.vue index 6b3cd3496..a2779bc8a 100644 --- a/resources/js/Pages/Company/Project/Tasks/Show.vue +++ b/resources/js/Pages/Company/Project/Tasks/Show.vue @@ -301,7 +301,7 @@ input[type=checkbox] {
    -
  • {{ $t('app.delete') }}
  • +
  • {{ $t('app.delete') }}
  • {{ $t('app.sure') }} diff --git a/resources/js/Pages/Team/Ship/Index.vue b/resources/js/Pages/Team/Ship/Index.vue index 977e38500..ebabb3e0d 100644 --- a/resources/js/Pages/Team/Ship/Index.vue +++ b/resources/js/Pages/Team/Ship/Index.vue @@ -41,7 +41,7 @@
    • - +
  • diff --git a/resources/js/Shared/Avatar.vue b/resources/js/Shared/Avatar.vue index fc5666cb0..7ca3c53fc 100644 --- a/resources/js/Shared/Avatar.vue +++ b/resources/js/Shared/Avatar.vue @@ -38,6 +38,20 @@ export default { } }, + data() { + return { + localClasses: '', + }; + }, + + mounted() { + this.localClasses = this.classes; + + if (this.url) { + this.localClasses = this.localClasses + ' pointer'; + } + }, + methods: { navigateTo() { if (this.url) { diff --git a/resources/js/Shared/SmallNameAndAvatar.vue b/resources/js/Shared/SmallNameAndAvatar.vue index 4b7488457..280a582c8 100644 --- a/resources/js/Shared/SmallNameAndAvatar.vue +++ b/resources/js/Shared/SmallNameAndAvatar.vue @@ -2,6 +2,10 @@ span { margin-left: 29px; } + +a { + border-bottom: 0; +}