diff --git a/app/Console/Commands/Tests/SetupDummyAccount.php b/app/Console/Commands/Tests/SetupDummyAccount.php index e28cd9752..56ff6716a 100644 --- a/app/Console/Commands/Tests/SetupDummyAccount.php +++ b/app/Console/Commands/Tests/SetupDummyAccount.php @@ -31,6 +31,7 @@ use App\Services\Company\Project\CreateProjectStatus; use App\Services\Company\Employee\Answer\CreateAnswer; use App\Services\Company\Project\AddEmployeeToProject; +use App\Services\Company\Project\CreateProjectMessage; use App\Services\Company\Project\CreateProjectDecision; use App\Services\Company\Employee\Expense\CreateExpense; use App\Services\Company\Employee\Manager\AssignManager; @@ -38,6 +39,7 @@ use App\Services\Company\Adminland\Hardware\LendHardware; use App\Services\Company\Employee\Birthdate\SetBirthdate; use App\Services\Company\Employee\Team\AddEmployeeToTeam; +use App\Services\Company\Project\MarkProjectMessageasRead; use App\Services\Company\Adminland\Hardware\CreateHardware; use App\Services\Company\Adminland\Position\CreatePosition; use App\Services\Company\Adminland\Question\CreateQuestion; @@ -1684,6 +1686,39 @@ private function addProjects(): void 'decided_at' => '2019-06-29', 'deciders' => [$this->dwight->id, $this->oscar->id], ]); + + // add messages + $messages = collect([ + 'Let’s change how we do business', + 'Ryan promoted as the principal project manager', + 'Changing the name of the project - my thoughts', + 'Need more resources? Contact Corporate if you need help', + ]); + $content = 'Kelly tries to restart her relationship with Ryan, an effort he ignores until she (untruly) tells him she’s pregnant. He agrees to discuss the situation over dinner that night. Jim informs Pam that Dwight and Angela are secretly dating, only to discover that she has known this for quite some time. Meanwhile, Dwight attempts to make amends for the death of Angela’s cat Sprinkles by giving her a stray cat he found in his barn. Dwight’s cousin Mose named the cat Garbage because that’s what it eats. Angela rejects the gift, and Dwight attempts to dump the cat into the office of Vance Refrigeration. + +Creed dyes his hair jet-black (using ink cartridges) in an attempt to convince everyone that he’s much younger. After a conversation with Jan, Michael decides to formally challenge Dunder Mifflin Infinity by claiming that Ryan is being ageist. Michael brings the octogenarian co-founder of Dunder Mifflin into a meeting to make his point about old things still being useful, but shoves Dunder out after tiring of his rambling stories. Angela asks Dwight out to dinner, where she breaks up with him, saying that she can’t look into Dwight’s eyes without seeing Sprinkles’ corpse. + +* Alternate talking heads of different people reacting to Jim and Pam dating +* Michael drives a rental car to the office using a GPS. Jan calls him and yells at him for allegedly eating her Grape-Nuts'; + + foreach ($messages as $message) { + $message = (new CreateProjectMessage)->execute([ + 'company_id' => $this->company->id, + 'author_id' => $this->jim->id, + 'project_id' => $infinity->id, + 'title' => $message, + 'content' => $content, + ]); + + if (rand(1, 2) == 1) { + (new MarkProjectMessageasRead)->execute([ + 'company_id' => $this->company->id, + 'author_id' => $this->michael->id, + 'project_id' => $infinity->id, + 'project_message_id' => $message->id, + ]); + } + } } private function addSecondaryBlankAccount(): void diff --git a/app/Helpers/LogHelper.php b/app/Helpers/LogHelper.php index 07bd10ecd..ddbe5383f 100644 --- a/app/Helpers/LogHelper.php +++ b/app/Helpers/LogHelper.php @@ -870,8 +870,32 @@ public static function processAuditLog(AuditLog $log): string 'project_name' => $log->object->{'project_name'}, 'title' => $log->object->{'title'}, ]); + break; + + case 'project_message_created': + $sentence = trans('account.log_project_message_created', [ + 'project_id' => $log->object->{'project_id'}, + 'project_name' => $log->object->{'project_name'}, + 'title' => $log->object->{'title'}, + ]); + break; + + case 'project_message_destroyed': + $sentence = trans('account.log_project_message_destroyed', [ + 'project_id' => $log->object->{'project_id'}, + 'project_name' => $log->object->{'project_name'}, + 'title' => $log->object->{'title'}, + ]); + break; + + case 'project_message_updated': + $sentence = trans('account.log_project_message_updated', [ + 'project_id' => $log->object->{'project_id'}, + 'project_name' => $log->object->{'project_name'}, + 'title' => $log->object->{'project_message_title'}, + ]); + break; - // no break default: $sentence = ''; break; diff --git a/app/Http/Controllers/Company/Project/ProjectController.php b/app/Http/Controllers/Company/Project/ProjectController.php index d8e991d15..01b3714bc 100644 --- a/app/Http/Controllers/Company/Project/ProjectController.php +++ b/app/Http/Controllers/Company/Project/ProjectController.php @@ -372,41 +372,6 @@ public function clear(Request $request, int $companyId, int $projectId): JsonRes ], 200); } - /** - * Display the project messages. - * - * @param Request $request - * @param int $companyId - * @param int $projectId - * @return Response - */ - public function messages(Request $request, int $companyId, int $projectId): Response - { - $company = InstanceHelper::getLoggedCompany(); - - return Inertia::render('Project/Messages', [ - 'notifications' => NotificationHelper::getNotifications(InstanceHelper::getLoggedEmployee()), - ]); - } - - /** - * Display the project messages. - * - * @param Request $request - * @param int $companyId - * @param int $projectId - * @param int $messageId - * @return Response - */ - public function message(Request $request, int $companyId, int $projectId, int $messageId): Response - { - $company = InstanceHelper::getLoggedCompany(); - - return Inertia::render('Project/Message', [ - 'notifications' => NotificationHelper::getNotifications(InstanceHelper::getLoggedEmployee()), - ]); - } - /** * Display the create new project form. * diff --git a/app/Http/Controllers/Company/Project/ProjectMessagesController.php b/app/Http/Controllers/Company/Project/ProjectMessagesController.php new file mode 100644 index 000000000..d35c1dd22 --- /dev/null +++ b/app/Http/Controllers/Company/Project/ProjectMessagesController.php @@ -0,0 +1,250 @@ +id) + ->with('employees') + ->findOrFail($projectId); + } catch (ModelNotFoundException $e) { + return redirect('home'); + } + + return Inertia::render('Project/Messages/Index', [ + 'tab' => 'messages', + 'project' => ProjectViewHelper::info($project), + 'messages' => ProjectMessagesViewHelper::index($project, $employee), + 'notifications' => NotificationHelper::getNotifications(InstanceHelper::getLoggedEmployee()), + ]); + } + + /** + * Display the Create message view. + * + * @param Request $request + * @param int $companyId + * @param int $projectId + * + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|Response + */ + public function create(Request $request, int $companyId, int $projectId) + { + $company = InstanceHelper::getLoggedCompany(); + + try { + $project = Project::where('company_id', $company->id) + ->with('employees') + ->findOrFail($projectId); + } catch (ModelNotFoundException $e) { + return redirect('home'); + } + + return Inertia::render('Project/Messages/Create', [ + 'project' => ProjectViewHelper::info($project), + 'notifications' => NotificationHelper::getNotifications(InstanceHelper::getLoggedEmployee()), + ]); + } + + /** + * Create the message. + * + * @param Request $request + * @param int $companyId + * @param int $projectId + * @return JsonResponse + */ + public function store(Request $request, int $companyId, int $projectId): JsonResponse + { + $loggedEmployee = InstanceHelper::getLoggedEmployee(); + $company = InstanceHelper::getLoggedCompany(); + + $data = [ + 'company_id' => $company->id, + 'author_id' => $loggedEmployee->id, + 'project_id' => $projectId, + 'title' => $request->input('title'), + 'content' => $request->input('content'), + ]; + + $message = (new CreateProjectMessage)->execute($data); + + return response()->json([ + 'data' => $message->id, + ], 201); + } + + /** + * Display the detail of a given message. + * + * @param Request $request + * @param int $companyId + * @param int $projectId + * @param int $messageId + * + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|Response + */ + public function show(Request $request, int $companyId, int $projectId, int $messageId) + { + $loggedCompany = InstanceHelper::getLoggedCompany(); + $loggedEmployee = InstanceHelper::getLoggedEmployee(); + + try { + $project = Project::where('company_id', $loggedCompany->id) + ->with('employees') + ->findOrFail($projectId); + } catch (ModelNotFoundException $e) { + return redirect('home'); + } + + try { + $message = ProjectMessage::where('project_id', $project->id) + ->with('project') + ->findOrFail($messageId); + } catch (ModelNotFoundException $e) { + return redirect('home'); + } + + (new MarkProjectMessageasRead)->execute([ + 'company_id' => $loggedCompany->id, + 'author_id' => $loggedEmployee->id, + 'project_id' => $project->id, + 'project_message_id' => $message->id, + ]); + + return Inertia::render('Project/Messages/Show', [ + 'tab' => 'messages', + 'project' => ProjectViewHelper::info($project), + 'message' => ProjectMessagesViewHelper::show($message), + 'notifications' => NotificationHelper::getNotifications(InstanceHelper::getLoggedEmployee()), + ]); + } + + /** + * Display the edit message page. + * + * @param Request $request + * @param int $companyId + * @param int $projectId + * @param int $messageId + * + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|Response + */ + public function edit(Request $request, int $companyId, int $projectId, int $messageId) + { + $company = InstanceHelper::getLoggedCompany(); + + try { + $project = Project::where('company_id', $company->id) + ->with('employees') + ->findOrFail($projectId); + } catch (ModelNotFoundException $e) { + return redirect('home'); + } + + try { + $message = ProjectMessage::where('project_id', $project->id) + ->with('project') + ->findOrFail($messageId); + } catch (ModelNotFoundException $e) { + return redirect('home'); + } + + return Inertia::render('Project/Messages/Update', [ + 'tab' => 'messages', + 'project' => ProjectViewHelper::info($project), + 'message' => ProjectMessagesViewHelper::edit($message), + 'notifications' => NotificationHelper::getNotifications(InstanceHelper::getLoggedEmployee()), + ]); + } + + /** + * Actually update the message. + * + * @param Request $request + * @param int $companyId + * @param int $projectId + * @param int $projectMessageId + * @return JsonResponse + */ + public function update(Request $request, int $companyId, int $projectId, int $projectMessageId): JsonResponse + { + $loggedEmployee = InstanceHelper::getLoggedEmployee(); + $company = InstanceHelper::getLoggedCompany(); + + $data = [ + 'company_id' => $company->id, + 'author_id' => $loggedEmployee->id, + 'project_id' => $projectId, + 'project_message_id' => $projectMessageId, + 'title' => $request->input('title'), + 'content' => $request->input('content'), + ]; + + $message = (new UpdateProjectMessage)->execute($data); + + return response()->json([ + 'data' => $message->id, + ], 201); + } + + /** + * Destroy the message. + * + * @param Request $request + * @param int $companyId + * @param int $projectId + * @param int $projectMessageId + * @return JsonResponse + */ + public function destroy(Request $request, int $companyId, int $projectId, int $projectMessageId): JsonResponse + { + $loggedEmployee = InstanceHelper::getLoggedEmployee(); + $company = InstanceHelper::getLoggedCompany(); + + $data = [ + 'company_id' => $company->id, + 'author_id' => $loggedEmployee->id, + 'project_id' => $projectId, + 'project_message_id' => $projectMessageId, + ]; + + (new DestroyProjectMessage)->execute($data); + + return response()->json([ + 'data' => true, + ], 201); + } +} diff --git a/app/Http/ViewHelpers/Project/ProjectDecisionsViewHelper.php b/app/Http/ViewHelpers/Project/ProjectDecisionsViewHelper.php index 9975b4d73..353aaaba7 100644 --- a/app/Http/ViewHelpers/Project/ProjectDecisionsViewHelper.php +++ b/app/Http/ViewHelpers/Project/ProjectDecisionsViewHelper.php @@ -24,7 +24,7 @@ public static function decisions(Project $project): Collection $decisionsCollection = collect([]); foreach ($decisions as $decision) { - $deciders = $decision->deciders()->get(); + $deciders = $decision->deciders; $decidersCollection = collect([]); foreach ($deciders as $decider) { $decidersCollection->push([ diff --git a/app/Http/ViewHelpers/Project/ProjectMessagesViewHelper.php b/app/Http/ViewHelpers/Project/ProjectMessagesViewHelper.php new file mode 100644 index 000000000..b38acdb48 --- /dev/null +++ b/app/Http/ViewHelpers/Project/ProjectMessagesViewHelper.php @@ -0,0 +1,133 @@ +company; + $messages = $project->messages() + ->select('id', 'title', 'content', 'created_at', 'author_id') + ->with('author') + ->latest() + ->get(); + + $messageReadStatuses = DB::table('project_message_read_status') + ->whereIn('project_message_id', $messages->pluck('id')) + ->get(); + + $messagesCollection = collect([]); + foreach ($messages as $message) { + // check read status for each message + $readStatus = $messageReadStatuses->contains(function ($readStatus, $key) use ($message, $employee) { + return $readStatus->project_message_id == $message->id && $readStatus->employee_id == $employee->id; + }); + + $messagesCollection->push([ + 'id' => $message->id, + 'title' => $message->title, + 'content' => Str::words($message->content, 10, '...'), + 'read_status' => $readStatus, + 'written_at' => $message->created_at->diffForHumans(), + 'url' => route('projects.messages.show', [ + 'company' => $company, + 'project' => $project, + 'message' => $message, + ]), + 'author' => $message->author ? [ + 'id' => $message->author->id, + 'name' => $message->author->name, + 'avatar' => $message->author->avatar, + 'url_view' => route('employees.show', [ + 'company' => $company, + 'employee' => $message->author, + ]), + ] : null, + ]); + } + + return $messagesCollection; + } + + /** + * Array containing the information about a given message. + * + * @param ProjectMessage $projectMessage + * @return array + */ + public static function show(ProjectMessage $projectMessage): array + { + // check author role in project + $author = $projectMessage->author; + $role = null; + if ($author) { + $role = DB::table('employee_project') + ->where('employee_id', $author->id) + ->where('project_id', $projectMessage->project->id) + ->select('role', 'created_at') + ->first(); + } + + return [ + 'id' => $projectMessage->id, + 'title' => $projectMessage->title, + 'content' => $projectMessage->content, + 'parsed_content' => StringHelper::parse($projectMessage->content), + 'written_at' => DateHelper::formatDate($projectMessage->created_at), + 'written_at_human' => $projectMessage->created_at->diffForHumans(), + 'url_edit' => route('projects.messages.edit', [ + 'company' => $projectMessage->project->company_id, + 'project' => $projectMessage->project, + 'message' => $projectMessage, + ]), + 'author' => $projectMessage->author ? [ + 'id' => $projectMessage->author->id, + 'name' => $projectMessage->author->name, + 'avatar' => $projectMessage->author->avatar, + 'role' => $role ? $role->role : null, + 'added_at' => $role ? DateHelper::formatDate(Carbon::createFromFormat('Y-m-d H:i:s', $role->created_at)) : null, + 'position' => (! $projectMessage->author->position) ? null : [ + 'id' => $projectMessage->author->position->id, + 'title' => $projectMessage->author->position->title, + ], + 'url' => route('employees.show', [ + 'company' => $projectMessage->project->company_id, + 'employee' => $projectMessage->author, + ]), + ] : null, + ]; + } + + /** + * Array containing the information necessary to edit a message. + * + * @param ProjectMessage $projectMessage + * @return array + */ + public static function edit(ProjectMessage $projectMessage): array + { + return [ + 'id' => $projectMessage->id, + 'title' => $projectMessage->title, + 'content' => $projectMessage->content, + ]; + } +} diff --git a/app/Models/Company/Employee.php b/app/Models/Company/Employee.php index 45ae1fef9..d0dfe666f 100644 --- a/app/Models/Company/Employee.php +++ b/app/Models/Company/Employee.php @@ -460,7 +460,7 @@ public function projectsAsLead() } /** - * Get the project decision record associated with the employee. + * Get the project decision records associated with the employee. * * @return HasMany */ @@ -469,6 +469,16 @@ public function projectDecisions() return $this->hasMany(ProjectDecision::class, 'author_id', 'id'); } + /** + * Get the project message records associated with the employee. + * + * @return HasMany + */ + public function projectMessages() + { + return $this->hasMany(ProjectMessage::class, 'author_id', 'id'); + } + /** * Scope a query to only include unlocked users. * diff --git a/app/Models/Company/Project.php b/app/Models/Company/Project.php index 852bc1845..1127f61e4 100644 --- a/app/Models/Company/Project.php +++ b/app/Models/Company/Project.php @@ -127,4 +127,14 @@ public function decisions() { return $this->hasMany(ProjectDecision::class); } + + /** + * Get the project messages associated with the project. + * + * @return HasMany + */ + public function messages() + { + return $this->hasMany(ProjectMessage::class); + } } diff --git a/app/Models/Company/ProjectMessage.php b/app/Models/Company/ProjectMessage.php new file mode 100644 index 000000000..1f6abbda2 --- /dev/null +++ b/app/Models/Company/ProjectMessage.php @@ -0,0 +1,48 @@ +belongsTo(Project::class); + } + + /** + * Get the employee record associated with the project decision. + * + * @return BelongsTo + */ + public function author() + { + return $this->belongsTo(Employee::class, 'author_id'); + } +} diff --git a/app/Services/Company/Project/CreateProjectMessage.php b/app/Services/Company/Project/CreateProjectMessage.php new file mode 100644 index 000000000..7b33fb2a3 --- /dev/null +++ b/app/Services/Company/Project/CreateProjectMessage.php @@ -0,0 +1,100 @@ + 'required|integer|exists:companies,id', + 'author_id' => 'required|integer|exists:employees,id', + 'project_id' => 'required|integer|exists:projects,id', + 'title' => 'required|string|max:255', + 'content' => 'required|string|max:65535', + ]; + } + + /** + * Create a project message. + * + * @param array $data + * @return ProjectMessage + */ + public function execute(array $data): ProjectMessage + { + $this->data = $data; + $this->validate(); + $this->createMessage(); + $this->markAsReadForThisUser(); + $this->log(); + + return $this->projectMessage; + } + + private function validate(): void + { + $this->validateRules($this->data); + + $this->author($this->data['author_id']) + ->inCompany($this->data['company_id']) + ->asNormalUser() + ->canExecuteService(); + + $this->project = Project::where('company_id', $this->data['company_id']) + ->findOrFail($this->data['project_id']); + } + + private function createMessage(): void + { + $this->projectMessage = ProjectMessage::create([ + 'project_id' => $this->data['project_id'], + 'author_id' => $this->data['author_id'], + 'title' => $this->data['title'], + 'content' => $this->data['content'], + ]); + } + + private function markAsReadForThisUser(): void + { + (new MarkProjectMessageasRead)->execute([ + 'company_id' => $this->data['company_id'], + 'author_id' => $this->data['author_id'], + 'project_id' => $this->data['project_id'], + 'project_message_id' => $this->projectMessage->id, + ]); + } + + private function log(): void + { + LogAccountAudit::dispatch([ + 'company_id' => $this->data['company_id'], + 'action' => 'project_message_created', + 'author_id' => $this->author->id, + 'author_name' => $this->author->name, + 'audited_at' => Carbon::now(), + 'objects' => json_encode([ + 'project_id' => $this->project->id, + 'project_name' => $this->project->name, + 'title' => $this->projectMessage->title, + ]), + ])->onQueue('low'); + } +} diff --git a/app/Services/Company/Project/DestroyProjectMessage.php b/app/Services/Company/Project/DestroyProjectMessage.php new file mode 100644 index 000000000..e1b5dda03 --- /dev/null +++ b/app/Services/Company/Project/DestroyProjectMessage.php @@ -0,0 +1,83 @@ + 'required|integer|exists:companies,id', + 'author_id' => 'required|integer|exists:employees,id', + 'project_id' => 'required|integer|exists:projects,id', + 'project_message_id' => 'required|integer|exists:project_messages,id', + ]; + } + + /** + * Destroy a project message. + * + * @param array $data + */ + public function execute(array $data): void + { + $this->data = $data; + $this->validate(); + $this->destroyMessage(); + $this->log(); + } + + private function validate(): void + { + $this->validateRules($this->data); + + $this->author($this->data['author_id']) + ->inCompany($this->data['company_id']) + ->asNormalUser() + ->canExecuteService(); + + $this->project = Project::where('company_id', $this->data['company_id']) + ->findOrFail($this->data['project_id']); + + $this->projectMessage = ProjectMessage::where('project_id', $this->data['project_id']) + ->findOrFail($this->data['project_message_id']); + } + + private function destroyMessage(): void + { + $this->projectMessage->delete(); + } + + private function log(): void + { + LogAccountAudit::dispatch([ + 'company_id' => $this->data['company_id'], + 'action' => 'project_message_destroyed', + 'author_id' => $this->author->id, + 'author_name' => $this->author->name, + 'audited_at' => Carbon::now(), + 'objects' => json_encode([ + 'project_id' => $this->project->id, + 'project_name' => $this->project->name, + 'title' => $this->projectMessage->title, + ]), + ])->onQueue('low'); + } +} diff --git a/app/Services/Company/Project/MarkProjectMessageasRead.php b/app/Services/Company/Project/MarkProjectMessageasRead.php new file mode 100644 index 000000000..d88f24cc4 --- /dev/null +++ b/app/Services/Company/Project/MarkProjectMessageasRead.php @@ -0,0 +1,79 @@ + 'required|integer|exists:companies,id', + 'author_id' => 'required|integer|exists:employees,id', + 'project_id' => 'required|integer|exists:projects,id', + 'project_message_id' => 'required|integer|exists:project_messages,id', + ]; + } + + /** + * Mark a project message as read. + * + * @param array $data + */ + public function execute(array $data): void + { + $this->data = $data; + $this->validate(); + $this->read(); + } + + private function validate(): void + { + $this->validateRules($this->data); + + $this->author($this->data['author_id']) + ->inCompany($this->data['company_id']) + ->asNormalUser() + ->canExecuteService(); + + $this->project = Project::where('company_id', $this->data['company_id']) + ->findOrFail($this->data['project_id']); + + $this->projectMessage = ProjectMessage::where('project_id', $this->data['project_id']) + ->findOrFail($this->data['project_message_id']); + } + + private function read(): void + { + $count = DB::table('project_message_read_status') + ->where('project_message_id', $this->projectMessage->id) + ->where('employee_id', $this->author->id) + ->count(); + + if ($count > 0) { + return; + } + + DB::table('project_message_read_status')->insert([ + 'project_message_id' => $this->projectMessage->id, + 'employee_id' => $this->author->id, + 'created_at' => Carbon::now(), + ]); + } +} diff --git a/app/Services/Company/Project/UpdateProjectMessage.php b/app/Services/Company/Project/UpdateProjectMessage.php new file mode 100644 index 000000000..0b771e415 --- /dev/null +++ b/app/Services/Company/Project/UpdateProjectMessage.php @@ -0,0 +1,91 @@ + 'required|integer|exists:companies,id', + 'author_id' => 'required|integer|exists:employees,id', + 'project_id' => 'nullable|integer|exists:projects,id', + 'project_message_id' => 'nullable|integer|exists:project_messages,id', + 'title' => 'required|string|max:255', + 'content' => 'required|string|max:65535', + ]; + } + + /** + * Update the project message. + * + * @param array $data + * @return ProjectMessage + */ + public function execute(array $data): ProjectMessage + { + $this->data = $data; + $this->validate(); + $this->update(); + $this->log(); + + return $this->projectMessage; + } + + private function validate(): void + { + $this->validateRules($this->data); + + $this->author($this->data['author_id']) + ->inCompany($this->data['company_id']) + ->asNormalUser() + ->canExecuteService(); + + $this->project = Project::where('company_id', $this->data['company_id']) + ->findOrFail($this->data['project_id']); + + $this->projectMessage = ProjectMessage::where('project_id', $this->project->id) + ->findOrFail($this->data['project_message_id']); + } + + private function update(): void + { + $this->projectMessage->title = $this->data['title']; + $this->projectMessage->content = $this->data['content']; + $this->projectMessage->save(); + } + + private function log(): void + { + LogAccountAudit::dispatch([ + 'company_id' => $this->data['company_id'], + 'action' => 'project_message_updated', + 'author_id' => $this->author->id, + 'author_name' => $this->author->name, + 'audited_at' => Carbon::now(), + 'objects' => json_encode([ + 'project_id' => $this->project->id, + 'project_name' => $this->project->name, + 'project_message_id' => $this->projectMessage->id, + 'project_message_title' => $this->projectMessage->title, + ]), + ])->onQueue('low'); + } +} diff --git a/config/officelife.php b/config/officelife.php index b9f5fe229..19a342305 100644 --- a/config/officelife.php +++ b/config/officelife.php @@ -159,5 +159,6 @@ 'employee_work_anniversaries' => 'manage/employee-management.html#work-anniversaries', 'one_on_ones' => 'introduction.html', 'project_decisions' => 'operate/project-management.html#project-decisions', + 'project_messages' => 'operate/project-management.html#project-messages', ], ]; diff --git a/database/factories/CompanyFactory.php b/database/factories/CompanyFactory.php index 73ca718be..7fa9ecc2f 100644 --- a/database/factories/CompanyFactory.php +++ b/database/factories/CompanyFactory.php @@ -532,3 +532,22 @@ 'decided_at' => Carbon::now(), ]; }); + +$factory->define(App\Models\Company\ProjectMessage::class, function () { + $companyId = factory(App\Models\Company\Company::class)->create()->id; + + return [ + 'project_id' => function () use ($companyId) { + return factory(App\Models\Company\Project::class)->create([ + 'company_id' => $companyId, + ])->id; + }, + 'author_id' => function () use ($companyId) { + return factory(App\Models\Company\Employee::class)->create([ + 'company_id' => $companyId, + ])->id; + }, + 'title' => 'This is a title', + 'content' => 'This is a description', + ]; +}); diff --git a/database/migrations/2020_10_20_195724_create_project_messages_table.php b/database/migrations/2020_10_20_195724_create_project_messages_table.php new file mode 100644 index 000000000..f7fb0db89 --- /dev/null +++ b/database/migrations/2020_10_20_195724_create_project_messages_table.php @@ -0,0 +1,36 @@ +id(); + $table->unsignedBigInteger('project_id'); + $table->unsignedBigInteger('author_id')->nullable(); + $table->string('title'); + $table->text('content'); + $table->timestamps(); + $table->foreign('project_id')->references('id')->on('projects')->onDelete('cascade'); + $table->foreign('author_id')->references('id')->on('employees')->onDelete('set null'); + }); + + Schema::create('project_message_read_status', function (Blueprint $table) { + $table->unsignedBigInteger('project_message_id'); + $table->unsignedBigInteger('employee_id'); + $table->timestamps(); + $table->foreign('project_message_id')->references('id')->on('project_messages')->onDelete('cascade'); + $table->foreign('employee_id')->references('id')->on('employees')->onDelete('cascade'); + }); + } +} diff --git a/package.json b/package.json index df9e0395b..289ee1d84 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "devDependencies": { "@babel/plugin-syntax-dynamic-import": "^7.2.0", "cross-env": "^7.0", - "cypress": "^5.3.0", + "cypress": "5.5.0", "eslint": "^7.11.0", "eslint-config-standard": "^14.1.1", "eslint-plugin-cypress": "^2.11.2", diff --git a/public/img/streamline-icon-morning-news-1@140x140.png b/public/img/streamline-icon-morning-news-1@140x140.png new file mode 100644 index 000000000..63df9da71 Binary files /dev/null and b/public/img/streamline-icon-morning-news-1@140x140.png differ diff --git a/resources/js/Pages/Project/Decisions/Index.vue b/resources/js/Pages/Project/Decisions/Index.vue index b8d943d5b..2e2a4a03b 100644 --- a/resources/js/Pages/Project/Decisions/Index.vue +++ b/resources/js/Pages/Project/Decisions/Index.vue @@ -72,7 +72,7 @@

- 🗞 {{ $t('project.decision_index_title') }} + 🗞 {{ $t('project.decision_index_title') }} diff --git a/resources/js/Pages/Project/Members/Index.vue b/resources/js/Pages/Project/Members/Index.vue index fb651869f..ceae2ec11 100644 --- a/resources/js/Pages/Project/Members/Index.vue +++ b/resources/js/Pages/Project/Members/Index.vue @@ -223,7 +223,7 @@

- {{ $t('project.members_index_blank_role') }} + {{ $t('project.members_index_blank') }}

people hanging out -.avatar { - left: 1px; - top: 5px; - width: 35px; -} - -.comment-avatar { - width: 50px; -} - -.team-member { - padding-left: 44px; - - .avatar { - top: 2px; - } -} - - - - - diff --git a/resources/js/Pages/Project/Messages.vue b/resources/js/Pages/Project/Messages.vue deleted file mode 100644 index 8a097db4f..000000000 --- a/resources/js/Pages/Project/Messages.vue +++ /dev/null @@ -1,117 +0,0 @@ - - - - - diff --git a/resources/js/Pages/Project/Messages/Create.vue b/resources/js/Pages/Project/Messages/Create.vue new file mode 100644 index 000000000..0463dfa18 --- /dev/null +++ b/resources/js/Pages/Project/Messages/Create.vue @@ -0,0 +1,138 @@ + + + + + diff --git a/resources/js/Pages/Project/Messages/Index.vue b/resources/js/Pages/Project/Messages/Index.vue new file mode 100644 index 000000000..6110a25aa --- /dev/null +++ b/resources/js/Pages/Project/Messages/Index.vue @@ -0,0 +1,162 @@ + + + + + diff --git a/resources/js/Pages/Project/Messages/Show.vue b/resources/js/Pages/Project/Messages/Show.vue new file mode 100644 index 000000000..fae10b0d8 --- /dev/null +++ b/resources/js/Pages/Project/Messages/Show.vue @@ -0,0 +1,205 @@ + + + + + diff --git a/resources/js/Pages/Project/Messages/Update.vue b/resources/js/Pages/Project/Messages/Update.vue new file mode 100644 index 000000000..53f25b075 --- /dev/null +++ b/resources/js/Pages/Project/Messages/Update.vue @@ -0,0 +1,147 @@ + + + + + diff --git a/resources/js/Pages/Project/Partials/ProjectMenu.vue b/resources/js/Pages/Project/Partials/ProjectMenu.vue index c0dd68762..4b3aa3a93 100644 --- a/resources/js/Pages/Project/Partials/ProjectMenu.vue +++ b/resources/js/Pages/Project/Partials/ProjectMenu.vue @@ -22,7 +22,7 @@
-

+

{{ project.name }} {{ project.code }} @@ -31,7 +31,7 @@

-
+

Project members

avatar 'Removed :employee_name as the project lead of the project called :project_name.', 'log_project_decision_created' => 'Added a decision called :title in the project called :project_name.', 'log_project_decision_destroyed' => 'Deleted a decision called :title in the project called :project_name.', + 'log_project_message_created' => 'Added a message called :title in the project called :project_name.', + 'log_project_message_destroyed' => 'Deleted a message called :title in the project called :project_name.', + 'log_project_message_updated' => 'Updated a message called :title in the project called :project_name.', // employee logs 'employee_log_employee_created' => 'Created this employee entry.', diff --git a/resources/lang/en/app.php b/resources/lang/en/app.php index 06bdd2ee4..a0b6ce435 100644 --- a/resources/lang/en/app.php +++ b/resources/lang/en/app.php @@ -95,6 +95,8 @@ 'breadcrumb_project_create' => 'Add a new project', 'breadcrumb_project_edit' => 'Edit project', 'breadcrumb_project_delete' => 'Delete project', + 'breadcrumb_project_create_message' => 'Add a new message', + 'breadcrumb_project_edit_message' => 'Edit message', 'breadcrumb_dashboard_one_on_one' => 'One on One', 'breadcrumb_team_list' => 'All teams', 'breadcrumb_team_show_team_news' => 'Team news', diff --git a/resources/lang/en/project.php b/resources/lang/en/project.php index 0617e6dfb..e991d3227 100644 --- a/resources/lang/en/project.php +++ b/resources/lang/en/project.php @@ -74,7 +74,7 @@ 'members_index_add_role_no_role' => 'No role', 'members_index_add_role_create_new_one' => 'Or create a new role', 'members_index_count' => 'This project has {count} members.', - 'members_index_blank' => 'Projects are more fun when employees are assigned to them.', + 'members_index_blank' => 'Projects are more fun when employees are working on them.', 'members_index_blank_role' => 'No role created yet', 'members_index_add_success' => 'The member has been added.', 'members_index_remove_success' => 'The member has been removed.', @@ -91,4 +91,22 @@ 'decision_index_add_success' => 'The decision has been recorded.', 'decision_index_destroy_success' => 'The decision has been forgotten.', 'decision_index_title' => 'Decisions in the project', + + 'message_title' => 'Messages', + 'message_blank' => 'Messages are essential to make announcements, give feedback, ask questions and have a central point of documentation in the project.', + 'message_cta' => 'Write a message', + 'message_show_written_by' => 'Written by', + 'message_show_written_on' => 'Written on', + 'message_show_actions' => 'Actions', + 'message_show_edit' => 'Edit message', + 'message_show_destroy' => 'Delete message', + 'message_create_success' => 'The message has been created.', + 'message_update_success' => 'The message has been updated.', + 'message_destroy_success' => 'The message has been deleted.', + 'message_create_title' => 'Add a new message', + 'message_create_title_message' => 'Title of the message', + 'message_create_title_message_help' => 'Keep it informative and to the point.', + 'message_create_title_content' =>'Content of the message', + 'message_create_title_content_help' => 'Everyone in the company will be able to read this message.', + 'message_edit_title_message' => 'Edit message', ]; diff --git a/routes/web.php b/routes/web.php index d657be539..c31145671 100644 --- a/routes/web.php +++ b/routes/web.php @@ -213,9 +213,6 @@ Route::post('{project}/links', 'Company\\Project\\ProjectController@createLink'); Route::delete('{project}/links/{link}', 'Company\\Project\\ProjectController@destroyLink'); - Route::get('{project}/messages', 'Company\\Project\\ProjectController@messages'); - Route::get('{project}/messages/{message}', 'Company\\Project\\ProjectController@message'); - Route::get('{project}/status', 'Company\\Project\\ProjectController@createStatus'); Route::put('{project}/status', 'Company\\Project\\ProjectController@postStatus'); @@ -230,6 +227,9 @@ Route::get('{project}/members/search', 'Company\\Project\\ProjectMembersController@search'); Route::post('{project}/members/store', 'Company\\Project\\ProjectMembersController@store'); Route::post('{project}/members/remove', 'Company\\Project\\ProjectMembersController@remove'); + + // project messages + Route::resource('{project}/messages', 'Company\\Project\\ProjectMessagesController', ['as' => 'projects']); }); Route::prefix('company')->group(function () { diff --git a/tests/Features/integration/project/delete_project_spec.js b/tests/Features/integration/project/delete_project_spec.js index ac07c2eb1..0456db745 100644 --- a/tests/Features/integration/project/delete_project_spec.js +++ b/tests/Features/integration/project/delete_project_spec.js @@ -11,7 +11,7 @@ describe('Project - project deletion', function () { cy.get('[data-cy=project-delete]').click(); cy.get('[data-cy=submit-delete-project-button]').click(); cy.url().should('include', '/1/projects'); - cy.wait(200); + cy.visit('/1/projects'); }); it('should delete a project as hr', function () { @@ -28,7 +28,7 @@ describe('Project - project deletion', function () { cy.get('[data-cy=project-delete]').click(); cy.get('[data-cy=submit-delete-project-button]').click(); cy.url().should('include', '/1/projects'); - cy.wait(200); + cy.visit('/1/projects'); }); it('should delete a project as normal user', function () { @@ -46,5 +46,6 @@ describe('Project - project deletion', function () { cy.get('[data-cy=project-delete]').click(); cy.get('[data-cy=submit-delete-project-button]').click(); cy.url().should('include', '/1/projects'); + cy.visit('/1/projects'); }); }); diff --git a/tests/Features/integration/project/manage_project_messages_spec.js b/tests/Features/integration/project/manage_project_messages_spec.js new file mode 100644 index 000000000..07da83e5e --- /dev/null +++ b/tests/Features/integration/project/manage_project_messages_spec.js @@ -0,0 +1,116 @@ +describe('Project - messages', function () { + it('should let an employee add a message as administrator', function () { + cy.loginLegacy(); + + cy.createCompany(); + + cy.createProject(1, 'project 1'); + + // write a message + cy.visit('/1/projects/1/messages'); + cy.get('[data-cy=messages-blank-state]').should('exist'); + + cy.get('[data-cy=add-message]').click(); + cy.get('[data-cy=message-title-input]').type('message title'); + cy.get('[data-cy=message-content-textarea]').type('message content'); + cy.get('[data-cy=submit-add-message-button]').click(); + + cy.get('[data-cy=project-title]').contains('message title'); + cy.get('[data-cy=project-content]').contains('message content'); + cy.hasAuditLog('Added a message called message title', '/1/projects/1/messages/1'); + + // edit the message + cy.get('[data-cy=project-edit]').click(); + cy.get('[data-cy=message-title-input]').clear(); + cy.get('[data-cy=message-title-input]').type('message title'); + cy.get('[data-cy=message-content-textarea]').clear(); + cy.get('[data-cy=message-content-textarea]').type('message content'); + cy.get('[data-cy=submit-update-message-button]').click(); + + // delete the message + cy.get('[data-cy=project-delete]').click(); + cy.get('[data-cy=cancel-project-deletion]').click(); + cy.get('[data-cy=project-delete]').click(); + cy.get('[data-cy=confirm-project-deletion]').click(); + + cy.url().should('include', '/messages'); + cy.visit('/1/projects'); + }); + + it('should let an employee add a message as HR', function () { + cy.loginLegacy(); + + cy.createCompany(); + + cy.changePermission(1, 200); + + cy.createProject(1, 'project 1'); + + // write a message + cy.visit('/1/projects/1/messages'); + cy.get('[data-cy=messages-blank-state]').should('exist'); + + cy.get('[data-cy=add-message]').click(); + cy.get('[data-cy=message-title-input]').type('message title'); + cy.get('[data-cy=message-content-textarea]').type('message content'); + cy.get('[data-cy=submit-add-message-button]').click(); + + cy.get('[data-cy=project-title]').contains('message title'); + cy.get('[data-cy=project-content]').contains('message content'); + + // edit the message + cy.get('[data-cy=project-edit]').click(); + cy.get('[data-cy=message-title-input]').clear(); + cy.get('[data-cy=message-title-input]').type('message title'); + cy.get('[data-cy=message-content-textarea]').clear(); + cy.get('[data-cy=message-content-textarea]').type('message content'); + cy.get('[data-cy=submit-update-message-button]').click(); + + // delete the message + cy.get('[data-cy=project-delete]').click(); + cy.get('[data-cy=cancel-project-deletion]').click(); + cy.get('[data-cy=project-delete]').click(); + cy.get('[data-cy=confirm-project-deletion]').click(); + + cy.url().should('include', '/messages'); + cy.visit('/1/projects'); + }); + + it('should let an employee add a message a project as normal user', function () { + cy.loginLegacy(); + + cy.createCompany(); + + cy.changePermission(1, 300); + + cy.createProject(1, 'project 1'); + + // write a message + cy.visit('/1/projects/1/messages'); + cy.get('[data-cy=messages-blank-state]').should('exist'); + + cy.get('[data-cy=add-message]').click(); + cy.get('[data-cy=message-title-input]').type('message title'); + cy.get('[data-cy=message-content-textarea]').type('message content'); + cy.get('[data-cy=submit-add-message-button]').click(); + + cy.get('[data-cy=project-title]').contains('message title'); + cy.get('[data-cy=project-content]').contains('message content'); + + // edit the message + cy.get('[data-cy=project-edit]').click(); + cy.get('[data-cy=message-title-input]').clear(); + cy.get('[data-cy=message-title-input]').type('message title'); + cy.get('[data-cy=message-content-textarea]').clear(); + cy.get('[data-cy=message-content-textarea]').type('message content'); + cy.get('[data-cy=submit-update-message-button]').click(); + + // delete the message + cy.get('[data-cy=project-delete]').click(); + cy.get('[data-cy=cancel-project-deletion]').click(); + cy.get('[data-cy=project-delete]').click(); + cy.get('[data-cy=confirm-project-deletion]').click(); + + cy.url().should('include', '/messages'); + }); +}); diff --git a/tests/Unit/Models/Company/ProjectMessageTest.php b/tests/Unit/Models/Company/ProjectMessageTest.php new file mode 100644 index 000000000..30e26268c --- /dev/null +++ b/tests/Unit/Models/Company/ProjectMessageTest.php @@ -0,0 +1,26 @@ +create([]); + $this->assertTrue($decision->project()->exists()); + } + + /** @test */ + public function it_belongs_to_a_employee(): void + { + $decision = factory(ProjectDecision::class)->create([]); + $this->assertTrue($decision->author()->exists()); + } +} diff --git a/tests/Unit/Models/Company/ProjectTest.php b/tests/Unit/Models/Company/ProjectTest.php index 5ea4cb97c..3702ad224 100644 --- a/tests/Unit/Models/Company/ProjectTest.php +++ b/tests/Unit/Models/Company/ProjectTest.php @@ -8,6 +8,7 @@ use App\Models\Company\Employee; use App\Models\Company\ProjectLink; use App\Models\Company\ProjectStatus; +use App\Models\Company\ProjectMessage; use App\Models\Company\ProjectDecision; use Illuminate\Foundation\Testing\DatabaseTransactions; @@ -100,4 +101,15 @@ public function it_has_many_decisions(): void $this->assertTrue($project->decisions()->exists()); } + + /** @test */ + public function it_has_many_messages(): void + { + $project = factory(Project::class)->create(); + factory(ProjectMessage::class, 2)->create([ + 'project_id' => $project->id, + ]); + + $this->assertTrue($project->messages()->exists()); + } } diff --git a/tests/Unit/Services/Company/Project/CreateProjectMessageTest.php b/tests/Unit/Services/Company/Project/CreateProjectMessageTest.php new file mode 100644 index 000000000..b06abbb87 --- /dev/null +++ b/tests/Unit/Services/Company/Project/CreateProjectMessageTest.php @@ -0,0 +1,113 @@ +createAdministrator(); + $project = factory(Project::class)->create([ + 'company_id' => $michael->company_id, + ]); + $this->executeService($michael, $project); + } + + /** @test */ + public function it_adds_a_message_to_a_project_as_hr(): void + { + $michael = $this->createHR(); + $project = factory(Project::class)->create([ + 'company_id' => $michael->company_id, + ]); + $this->executeService($michael, $project); + } + + /** @test */ + public function it_adds_a_message_to_a_project_as_normal_user(): void + { + $michael = $this->createEmployee(); + $project = factory(Project::class)->create([ + 'company_id' => $michael->company_id, + ]); + $this->executeService($michael, $project); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given(): void + { + $request = [ + 'first_name' => 'Dwight', + ]; + + $this->expectException(ValidationException::class); + (new CreateProjectStatus)->execute($request); + } + + /** @test */ + public function it_fails_if_the_project_is_not_in_the_company(): void + { + $michael = $this->createAdministrator(); + $project = factory(Project::class)->create(); + + $this->expectException(ModelNotFoundException::class); + $this->executeService($michael, $project); + } + + private function executeService(Employee $michael, Project $project): void + { + Queue::fake(); + + $request = [ + 'company_id' => $michael->company_id, + 'author_id' => $michael->id, + 'project_id' => $project->id, + 'title' => 'email', + 'content' => 'content', + ]; + + $projectMessage = (new CreateProjectMessage)->execute($request); + + $this->assertDatabaseHas('project_messages', [ + 'id' => $projectMessage->id, + 'project_id' => $project->id, + 'title' => 'email', + 'content' => 'content', + ]); + + $this->assertDatabaseHas('project_message_read_status', [ + 'project_message_id' => $projectMessage->id, + 'employee_id' => $michael->id, + ]); + + $this->assertInstanceOf( + ProjectMessage::class, + $projectMessage + ); + + Queue::assertPushed(LogAccountAudit::class, function ($job) use ($michael, $project, $projectMessage) { + return $job->auditLog['action'] === 'project_message_created' && + $job->auditLog['author_id'] === $michael->id && + $job->auditLog['objects'] === json_encode([ + 'project_id' => $project->id, + 'project_name' => $project->name, + 'title' => $projectMessage->title, + ]); + }); + } +} diff --git a/tests/Unit/Services/Company/Project/DestroyProjectMessageTest.php b/tests/Unit/Services/Company/Project/DestroyProjectMessageTest.php new file mode 100644 index 000000000..aa101a280 --- /dev/null +++ b/tests/Unit/Services/Company/Project/DestroyProjectMessageTest.php @@ -0,0 +1,124 @@ +createAdministrator(); + $project = factory(Project::class)->create([ + 'company_id' => $michael->company_id, + ]); + $projectMessage = factory(ProjectMessage::class)->create([ + 'project_id' => $project->id, + ]); + $this->executeService($michael, $project, $projectMessage); + } + + /** @test */ + public function it_destroys_a_message_from_a_project_as_hr(): void + { + $michael = $this->createHR(); + $project = factory(Project::class)->create([ + 'company_id' => $michael->company_id, + ]); + $projectMessage = factory(ProjectMessage::class)->create([ + 'project_id' => $project->id, + ]); + $this->executeService($michael, $project, $projectMessage); + } + + /** @test */ + public function it_destroys_a_message_from_a_project_as_normal_user(): void + { + $michael = $this->createEmployee(); + $project = factory(Project::class)->create([ + 'company_id' => $michael->company_id, + ]); + $projectMessage = factory(ProjectMessage::class)->create([ + 'project_id' => $project->id, + ]); + $this->executeService($michael, $project, $projectMessage); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given(): void + { + $request = [ + 'first_name' => 'Dwight', + ]; + + $this->expectException(ValidationException::class); + (new DestroyProjectDecision)->execute($request); + } + + /** @test */ + public function it_fails_if_the_project_is_not_in_the_company(): void + { + $michael = $this->createAdministrator(); + $project = factory(Project::class)->create(); + $projectMessage = factory(ProjectMessage::class)->create([ + 'project_id' => $project->id, + ]); + + $this->expectException(ModelNotFoundException::class); + $this->executeService($michael, $project, $projectMessage); + } + + /** @test */ + public function it_fails_if_the_project_message_is_not_part_of_the_project(): void + { + $michael = $this->createAdministrator(); + $project = factory(Project::class)->create([ + 'company_id' => $michael->company_id, + ]); + $projectMessage = factory(ProjectMessage::class)->create([]); + + $this->expectException(ModelNotFoundException::class); + $this->executeService($michael, $project, $projectMessage); + } + + private function executeService(Employee $michael, Project $project, ProjectMessage $message): void + { + Queue::fake(); + + $request = [ + 'company_id' => $michael->company_id, + 'author_id' => $michael->id, + 'project_id' => $project->id, + 'project_message_id' => $message->id, + ]; + + (new DestroyProjectMessage)->execute($request); + + $this->assertDatabaseMissing('project_messages', [ + 'id' => $message->id, + ]); + + Queue::assertPushed(LogAccountAudit::class, function ($job) use ($michael, $project, $message) { + return $job->auditLog['action'] === 'project_message_destroyed' && + $job->auditLog['author_id'] === $michael->id && + $job->auditLog['objects'] === json_encode([ + 'project_id' => $project->id, + 'project_name' => $project->name, + 'title' => $message->title, + ]); + }); + } +} diff --git a/tests/Unit/Services/Company/Project/MarkProjectMessageasReadTest.php b/tests/Unit/Services/Company/Project/MarkProjectMessageasReadTest.php new file mode 100644 index 000000000..49f394416 --- /dev/null +++ b/tests/Unit/Services/Company/Project/MarkProjectMessageasReadTest.php @@ -0,0 +1,114 @@ +createAdministrator(); + $project = factory(Project::class)->create([ + 'company_id' => $michael->company_id, + ]); + $projectMessage = factory(ProjectMessage::class)->create([ + 'project_id' => $project->id, + ]); + $this->executeService($michael, $project, $projectMessage); + } + + /** @test */ + public function it_reads_a_message_from_a_project_as_hr(): void + { + $michael = $this->createHR(); + $project = factory(Project::class)->create([ + 'company_id' => $michael->company_id, + ]); + $projectMessage = factory(ProjectMessage::class)->create([ + 'project_id' => $project->id, + ]); + $this->executeService($michael, $project, $projectMessage); + } + + /** @test */ + public function it_reads_a_message_from_a_project_as_normal_user(): void + { + $michael = $this->createEmployee(); + $project = factory(Project::class)->create([ + 'company_id' => $michael->company_id, + ]); + $projectMessage = factory(ProjectMessage::class)->create([ + 'project_id' => $project->id, + ]); + $this->executeService($michael, $project, $projectMessage); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given(): void + { + $request = [ + 'first_name' => 'Dwight', + ]; + + $this->expectException(ValidationException::class); + (new DestroyProjectDecision)->execute($request); + } + + /** @test */ + public function it_fails_if_the_project_is_not_in_the_company(): void + { + $michael = $this->createAdministrator(); + $project = factory(Project::class)->create(); + $projectMessage = factory(ProjectMessage::class)->create([ + 'project_id' => $project->id, + ]); + + $this->expectException(ModelNotFoundException::class); + $this->executeService($michael, $project, $projectMessage); + } + + /** @test */ + public function it_fails_if_the_project_message_is_not_part_of_the_project(): void + { + $michael = $this->createAdministrator(); + $project = factory(Project::class)->create([ + 'company_id' => $michael->company_id, + ]); + $projectMessage = factory(ProjectMessage::class)->create([]); + + $this->expectException(ModelNotFoundException::class); + $this->executeService($michael, $project, $projectMessage); + } + + private function executeService(Employee $michael, Project $project, ProjectMessage $message): void + { + Queue::fake(); + + $request = [ + 'company_id' => $michael->company_id, + 'author_id' => $michael->id, + 'project_id' => $project->id, + 'project_message_id' => $message->id, + ]; + + (new MarkProjectMessageasRead)->execute($request); + + $this->assertDatabaseHas('project_message_read_status', [ + 'project_message_id' => $message->id, + 'employee_id' => $michael->id, + ]); + } +} diff --git a/tests/Unit/Services/Company/Project/UpdateProjectMessageTest.php b/tests/Unit/Services/Company/Project/UpdateProjectMessageTest.php new file mode 100644 index 000000000..d1f2078ab --- /dev/null +++ b/tests/Unit/Services/Company/Project/UpdateProjectMessageTest.php @@ -0,0 +1,128 @@ +createAdministrator(); + $project = factory(Project::class)->create([ + 'company_id' => $michael->company_id, + ]); + $projectMessage = factory(ProjectMessage::class)->create([ + 'project_id' => $project->id, + ]); + $this->executeService($michael, $project, $projectMessage); + } + + /** @test */ + public function it_updates_the_project_message_as_hr(): void + { + $michael = $this->createHR(); + $project = factory(Project::class)->create([ + 'company_id' => $michael->company_id, + ]); + $projectMessage = factory(ProjectMessage::class)->create([ + 'project_id' => $project->id, + ]); + $this->executeService($michael, $project, $projectMessage); + } + + /** @test */ + public function it_updates_the_project_message_as_normal_user(): void + { + $michael = $this->createEmployee(); + $project = factory(Project::class)->create([ + 'company_id' => $michael->company_id, + ]); + $projectMessage = factory(ProjectMessage::class)->create([ + 'project_id' => $project->id, + ]); + $this->executeService($michael, $project, $projectMessage); + } + + /** @test */ + public function it_fails_if_project_is_not_part_of_the_company(): void + { + $michael = factory(Employee::class)->create([]); + $project = factory(Project::class)->create(); + + $projectMessage = factory(ProjectMessage::class)->create([ + 'project_id' => $project->id, + ]); + $this->expectException(ModelNotFoundException::class); + $this->executeService($michael, $project, $projectMessage); + } + + /** @test */ + public function it_fails_if_project_message_is_not_part_of_the_project(): void + { + $michael = factory(Employee::class)->create([]); + $project = factory(Project::class)->create(); + $projectMessage = factory(ProjectMessage::class)->create([]); + + $this->expectException(ModelNotFoundException::class); + $this->executeService($michael, $project, $projectMessage); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given(): void + { + $michael = factory(Employee::class)->create([]); + + $request = [ + 'company_id' => $michael->company_id, + ]; + + $this->expectException(ValidationException::class); + (new UpdateProjectMessage)->execute($request); + } + + private function executeService(Employee $michael, Project $project, ProjectMessage $message): void + { + Queue::fake(); + + $request = [ + 'company_id' => $michael->company_id, + 'author_id' => $michael->id, + 'project_id' => $project->id, + 'project_message_id' => $message->id, + 'title' => 'Update', + 'content' => 'Content', + ]; + + $message = (new UpdateProjectMessage)->execute($request); + + $this->assertDatabaseHas('project_messages', [ + 'id' => $message->id, + 'title' => 'Update', + 'content' => 'Content', + ]); + + Queue::assertPushed(LogAccountAudit::class, function ($job) use ($michael, $project, $message) { + return $job->auditLog['action'] === 'project_message_updated' && + $job->auditLog['author_id'] === $michael->id && + $job->auditLog['objects'] === json_encode([ + 'project_id' => $project->id, + 'project_name' => $project->name, + 'project_message_id' => $message->id, + 'project_message_title' => $message->title, + ]); + }); + } +} diff --git a/tests/Unit/ViewHelpers/Project/ProjectMessagesViewHelperTest.php b/tests/Unit/ViewHelpers/Project/ProjectMessagesViewHelperTest.php new file mode 100644 index 000000000..bb0be7e1b --- /dev/null +++ b/tests/Unit/ViewHelpers/Project/ProjectMessagesViewHelperTest.php @@ -0,0 +1,115 @@ +createAdministrator(); + $jim = $this->createAnotherEmployee($michael); + $project = factory(Project::class)->create([ + 'company_id' => $michael->company_id, + ]); + $projectMessageA = factory(ProjectMessage::class)->create([ + 'project_id' => $project->id, + 'author_id' => $michael->id, + ]); + $projectMessageB = factory(ProjectMessage::class)->create([ + 'project_id' => $project->id, + 'author_id' => null, + ]); + + DB::table('project_message_read_status')->insert([ + 'project_message_id' => $projectMessageA->id, + 'employee_id' => $michael->id, + 'created_at' => Carbon::now(), + ]); + + $collection = ProjectMessagesViewHelper::index($project, $michael); + $this->assertEquals( + [ + 0 => [ + 'id' => $projectMessageA->id, + 'title' => $projectMessageA->title, + 'content' => 'This is a description', + 'read_status' => true, + 'written_at' => $projectMessageA->created_at->diffForHumans(), + 'url' => env('APP_URL').'/'.$michael->company_id.'/projects/'.$project->id.'/messages/'.$projectMessageA->id, + 'author' => [ + 'id' => $michael->id, + 'name' => $michael->name, + 'avatar' => $michael->avatar, + 'url_view' => env('APP_URL').'/'.$michael->company_id.'/employees/'.$michael->id, + ], + ], + 1 => [ + 'id' => $projectMessageB->id, + 'title' => $projectMessageB->title, + 'content' => 'This is a description', + 'read_status' => false, + 'written_at' => $projectMessageB->created_at->diffForHumans(), + 'url' => env('APP_URL').'/'.$michael->company_id.'/projects/'.$project->id.'/messages/'.$projectMessageB->id, + 'author' => null, + ], + ], + $collection->toArray() + ); + } + + /** @test */ + public function it_gets_an_array_containing_all_the_information_about_a_given_message(): void + { + $michael = $this->createAdministrator(); + $project = factory(Project::class)->create([ + 'company_id' => $michael->company_id, + ]); + $projectMessage = factory(ProjectMessage::class)->create([ + 'project_id' => $project->id, + 'author_id' => $michael->id, + ]); + + $array = ProjectMessagesViewHelper::show($projectMessage); + $this->assertEquals( + [ + 'id' => $projectMessage->id, + 'title' => $projectMessage->title, + 'content' => $projectMessage->content, + 'parsed_content' => StringHelper::parse($projectMessage->content), + 'written_at' => DateHelper::formatDate($projectMessage->created_at), + 'written_at_human' => $projectMessage->created_at->diffForHumans(), + 'url_edit' => route('projects.messages.edit', [ + 'company' => $projectMessage->project->company_id, + 'project' => $projectMessage->project, + 'message' => $projectMessage, + ]), + 'author' => [ + 'id' => $michael->id, + 'name' => $michael->name, + 'avatar' => $michael->avatar, + 'role' => null, + 'added_at' => null, + 'position' => [ + 'id' => $michael->position->id, + 'title' => $michael->position->title, + ], + 'url' => env('APP_URL').'/'.$michael->company_id.'/employees/'.$michael->id, + ], + ], + $array + ); + } +} diff --git a/yarn.lock b/yarn.lock index a18dc7e0d..cb6a96895 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2784,10 +2784,10 @@ cyclist@^1.0.1: resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk= -cypress@^5.3.0: - version "5.4.0" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-5.4.0.tgz#8833a76e91129add601f823d43c53eb512d162c5" - integrity sha512-BJR+u3DRSYMqaBS1a3l1rbh5AkMRHugbxcYYzkl+xYlO6dzcJVE8uAhghzVI/hxijCyBg1iuSe4TRp/g1PUg8Q== +cypress@5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-5.5.0.tgz#1da0355794a43247f8a80cb7f505e83e1cf847cb" + integrity sha512-UHEiTca8AUTevbT2pWkHQlxoHtXmbq+h6Eiu/Mz8DqpNkF98zjTBLv/HFiKJUU5rQzp9EwSWtms33p5TWCJ8tQ== dependencies: "@cypress/listr-verbose-renderer" "^0.4.1" "@cypress/request" "^2.88.5" @@ -2819,10 +2819,10 @@ cypress@^5.3.0: minimist "^1.2.5" moment "^2.27.0" ospath "^1.2.2" - pretty-bytes "^5.3.0" + pretty-bytes "^5.4.1" ramda "~0.26.1" request-progress "^3.0.0" - supports-color "^7.1.0" + supports-color "^7.2.0" tmp "~0.2.1" untildify "^4.0.0" url "^0.11.0" @@ -7392,7 +7392,7 @@ prettier@^2.1.2: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.1.2.tgz#3050700dae2e4c8b67c4c3f666cdb8af405e1ce5" integrity sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg== -pretty-bytes@^5.3.0: +pretty-bytes@^5.4.1: version "5.4.1" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.4.1.tgz#cd89f79bbcef21e3d21eb0da68ffe93f803e884b" integrity sha512-s1Iam6Gwz3JI5Hweaz4GoCD1WUNUIyzePFy5+Js2hjwGVt2Z79wNN+ZKOZ2vB6C+Xs6njyB84Z1IthQg8d9LxA== @@ -8858,7 +8858,7 @@ supports-color@^6.1.0: dependencies: has-flag "^3.0.0" -supports-color@^7.0.0, supports-color@^7.1.0: +supports-color@^7.0.0, supports-color@^7.1.0, supports-color@^7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==