From e7352b135f7dd8d3e0a2665bb8ef692fbef3f5bc Mon Sep 17 00:00:00 2001 From: Mazarin Date: Thu, 9 Feb 2023 21:28:54 -0500 Subject: [PATCH] feat: life events (monicahq/chandler#412) --- .../Web/ViewHelpers/ContactShowViewHelper.php | 7 +- .../Services/CreateLifeEvent.php | 10 +- .../Services/CreateTimelineEvent.php | 75 ++++ .../Services/DestroyLifeEvent.php | 21 +- .../Services/DestroyTimelineEvent.php | 64 +++ .../Services/ToggleLifeEvent.php | 76 ++++ .../Services/ToggleTimelineEvent.php | 72 ++++ .../Services/UpdateLifeEvent.php | 13 +- .../Services/UpdateTimelineEvent.php | 75 ++++ .../ContactModuleLifeEventController.php | 74 ++++ .../ContactModuleTimelineEventController.php | 100 +++++ .../Controllers/ToggleLifeEventController.php | 27 ++ .../ToggleTimelineEventController.php | 26 ++ .../ViewHelpers/ModuleLifeEventViewHelper.php | 148 +++++++ .../CreateAccount/Jobs/SetupAccount.php | 16 + .../ManageVault/Services/CreateVault.php | 132 +++++++ .../UpdateVaultDashboardDefaultTab.php | 55 +++ .../Web/Controllers/VaultController.php | 16 +- .../VaultDefaultTabOnDashboardController.php | 27 ++ .../Controllers/VaultLifeEventController.php | 29 ++ app/Models/Contact.php | 10 + app/Models/LifeEvent.php | 29 +- app/Models/Module.php | 2 + app/Models/TimelineEvent.php | 118 ++++++ app/Models/Vault.php | 8 +- database/factories/LifeEventFactory.php | 4 +- database/factories/TimelineEventFactory.php | 29 ++ .../2014_10_12_000010_create_vaults_table.php | 1 + ..._05_17_155546_create_life_events_table.php | 24 +- lang/en/app.php | 4 +- lang/en/vault.php | 22 ++ public/img/contact_blank_life_event.svg | 1 + resources/js/Pages/Vault/Contact/Show.vue | 3 + resources/js/Pages/Vault/Dashboard/Index.vue | 180 +++++---- .../js/Shared/Modules/CreateLifeEvent.vue | 349 ++++++++++++++++ resources/js/Shared/Modules/LifeEvent.vue | 372 ++++++++++++++++++ routes/web.php | 17 + .../Services/CreateLifeEventTest.php | 45 ++- .../Services/CreateTimelineEventTest.php | 97 +++++ .../Services/DestroyLifeEventTest.php | 66 ++-- .../Services/DestroyTimelineEventTest.php | 119 ++++++ .../Services/ToggleLifeEventTest.php | 141 +++++++ .../Services/ToggleTimelineEventTest.php | 126 ++++++ .../Services/UpdateLifeEventTest.php | 57 ++- .../Services/UpdateTimelineEventTest.php | 129 ++++++ .../ModuleLifeEventViewHelperTest.php | 224 +++++++++++ .../Services/SetupAccountTest.php | 4 + .../UpdateVaultDashboardDefaultTabTest.php | 78 ++++ tests/Unit/Models/ContactTest.php | 12 + tests/Unit/Models/LifeEventTest.php | 16 +- tests/Unit/Models/TimelineEventTest.php | 82 ++++ tests/Unit/Models/VaultTest.php | 8 +- 52 files changed, 3286 insertions(+), 154 deletions(-) create mode 100644 app/Domains/Contact/ManageLifeEvents/Services/CreateTimelineEvent.php create mode 100644 app/Domains/Contact/ManageLifeEvents/Services/DestroyTimelineEvent.php create mode 100644 app/Domains/Contact/ManageLifeEvents/Services/ToggleLifeEvent.php create mode 100644 app/Domains/Contact/ManageLifeEvents/Services/ToggleTimelineEvent.php create mode 100644 app/Domains/Contact/ManageLifeEvents/Services/UpdateTimelineEvent.php create mode 100644 app/Domains/Contact/ManageLifeEvents/Web/Controllers/ContactModuleLifeEventController.php create mode 100644 app/Domains/Contact/ManageLifeEvents/Web/Controllers/ContactModuleTimelineEventController.php create mode 100644 app/Domains/Contact/ManageLifeEvents/Web/Controllers/ToggleLifeEventController.php create mode 100644 app/Domains/Contact/ManageLifeEvents/Web/Controllers/ToggleTimelineEventController.php create mode 100644 app/Domains/Contact/ManageLifeEvents/Web/ViewHelpers/ModuleLifeEventViewHelper.php create mode 100644 app/Domains/Vault/ManageVault/Services/UpdateVaultDashboardDefaultTab.php create mode 100644 app/Domains/Vault/ManageVault/Web/Controllers/VaultDefaultTabOnDashboardController.php create mode 100644 app/Domains/Vault/ManageVault/Web/Controllers/VaultLifeEventController.php create mode 100644 app/Models/TimelineEvent.php create mode 100644 database/factories/TimelineEventFactory.php create mode 100644 public/img/contact_blank_life_event.svg create mode 100644 resources/js/Shared/Modules/CreateLifeEvent.vue create mode 100644 resources/js/Shared/Modules/LifeEvent.vue create mode 100644 tests/Unit/Domains/Contact/ManageLifeEvents/Services/CreateTimelineEventTest.php create mode 100644 tests/Unit/Domains/Contact/ManageLifeEvents/Services/DestroyTimelineEventTest.php create mode 100644 tests/Unit/Domains/Contact/ManageLifeEvents/Services/ToggleLifeEventTest.php create mode 100644 tests/Unit/Domains/Contact/ManageLifeEvents/Services/ToggleTimelineEventTest.php create mode 100644 tests/Unit/Domains/Contact/ManageLifeEvents/Services/UpdateTimelineEventTest.php create mode 100644 tests/Unit/Domains/Contact/ManageLifeEvents/Web/ViewHelpers/ModuleLifeEventViewHelperTest.php create mode 100644 tests/Unit/Domains/Vault/ManageVault/Services/UpdateVaultDashboardDefaultTabTest.php create mode 100644 tests/Unit/Models/TimelineEventTest.php diff --git a/app/Domains/Contact/ManageContact/Web/ViewHelpers/ContactShowViewHelper.php b/app/Domains/Contact/ManageContact/Web/ViewHelpers/ContactShowViewHelper.php index 247cb36e7..7c873af47 100644 --- a/app/Domains/Contact/ManageContact/Web/ViewHelpers/ContactShowViewHelper.php +++ b/app/Domains/Contact/ManageContact/Web/ViewHelpers/ContactShowViewHelper.php @@ -14,6 +14,7 @@ use App\Domains\Contact\ManageGroups\Web\ViewHelpers\GroupsViewHelper; use App\Domains\Contact\ManageGroups\Web\ViewHelpers\ModuleGroupsViewHelper; use App\Domains\Contact\ManageJobInformation\Web\ViewHelpers\ModuleCompanyViewHelper; use App\Domains\Contact\ManageLabels\Web\ViewHelpers\ModuleLabelViewHelper; +use App\Domains\Contact\ManageLifeEvents\Web\ViewHelpers\ModuleLifeEventViewHelper; use App\Domains\Contact\ManageLoans\Web\ViewHelpers\ModuleLoanViewHelper; use App\Domains\Contact\ManageNotes\Web\ViewHelpers\ModuleNotesViewHelper; use App\Domains\Contact\ManagePets\Web\ViewHelpers\ModulePetsViewHelper; @@ -238,7 +239,7 @@ class ContactShowViewHelper } if ($module->type == Module::TYPE_FEED) { - // this is the only module where the data is loaded asynchroniously + // this module loads data asynchroniously // so it needs an URL to load the data from $data = route('contact.feed.show', [ 'vault' => $contact->vault_id, @@ -298,6 +299,10 @@ class ContactShowViewHelper $data = ModulePostsViewHelper::data($contact, $user); } + if ($module->type == Module::TYPE_LIFE_EVENTS) { + $data = ModuleLifeEventViewHelper::data($contact, $user); + } + $modulesCollection->push([ 'id' => $module->id, 'type' => $module->type, diff --git a/app/Domains/Contact/ManageLifeEvents/Services/CreateLifeEvent.php b/app/Domains/Contact/ManageLifeEvents/Services/CreateLifeEvent.php index 7d9618b17..195db2e89 100644 --- a/app/Domains/Contact/ManageLifeEvents/Services/CreateLifeEvent.php +++ b/app/Domains/Contact/ManageLifeEvents/Services/CreateLifeEvent.php @@ -6,6 +6,7 @@ use App\Interfaces\ServiceInterface; use App\Models\Contact; use App\Models\LifeEvent; use App\Models\LifeEventType; +use App\Models\TimelineEvent; use App\Services\BaseService; use Carbon\Carbon; use Illuminate\Support\Collection; @@ -14,6 +15,8 @@ class CreateLifeEvent extends BaseService implements ServiceInterface { private LifeEvent $lifeEvent; + private TimelineEvent $timelineEvent; + private Collection $partipantsCollection; private array $data; @@ -29,6 +32,7 @@ class CreateLifeEvent extends BaseService implements ServiceInterface 'account_id' => 'required|integer|exists:accounts,id', 'vault_id' => 'required|integer|exists:vaults,id', 'author_id' => 'required|integer|exists:users,id', + 'timeline_event_id' => 'required|integer|exists:timeline_events,id', 'life_event_type_id' => 'required|integer|exists:life_event_types,id', 'summary' => 'nullable|string|max:255', 'description' => 'nullable|string|max:65535', @@ -82,6 +86,9 @@ class CreateLifeEvent extends BaseService implements ServiceInterface $lifeEventType = LifeEventType::findOrFail($this->data['life_event_type_id']); + $this->timelineEvent = $this->vault->timelineEvents() + ->findOrFail($this->data['timeline_event_id']); + $this->vault->lifeEventCategories() ->findOrFail($lifeEventType->lifeEventCategory->id); @@ -113,13 +120,14 @@ class CreateLifeEvent extends BaseService implements ServiceInterface { foreach ($this->partipantsCollection as $participant) { $participant->lifeEvents()->attach($this->lifeEvent->id); + $participant->timelineEvents()->syncWithoutDetaching($this->timelineEvent->id); } } private function store(): void { $this->lifeEvent = LifeEvent::create([ - 'vault_id' => $this->data['vault_id'], + 'timeline_event_id' => $this->data['timeline_event_id'], 'life_event_type_id' => $this->data['life_event_type_id'], 'summary' => $this->valueOrNull($this->data, 'summary'), 'description' => $this->valueOrNull($this->data, 'description'), diff --git a/app/Domains/Contact/ManageLifeEvents/Services/CreateTimelineEvent.php b/app/Domains/Contact/ManageLifeEvents/Services/CreateTimelineEvent.php new file mode 100644 index 000000000..96b0b4ce8 --- /dev/null +++ b/app/Domains/Contact/ManageLifeEvents/Services/CreateTimelineEvent.php @@ -0,0 +1,75 @@ + 'required|integer|exists:accounts,id', + 'vault_id' => 'required|integer|exists:vaults,id', + 'author_id' => 'required|integer|exists:users,id', + 'label' => 'nullable|string|max:255', + 'started_at' => 'required|date|date_format:Y-m-d', + ]; + } + + /** + * Get the permissions that apply to the user calling the service. + * + * @return array + */ + public function permissions(): array + { + return [ + 'author_must_belong_to_account', + 'vault_must_belong_to_account', + 'author_must_be_vault_editor', + ]; + } + + /** + * Create a timeline event. + * A timeline event is a part of one or more contacts lives, and is itself + * composed of one or more life events. + * + * @param array $data + * @return TimelineEvent + */ + public function execute(array $data): TimelineEvent + { + $this->data = $data; + $this->validate(); + $this->store(); + + return $this->timelineEvent; + } + + private function validate(): void + { + $this->validateRules($this->data); + } + + private function store(): void + { + $this->timelineEvent = TimelineEvent::create([ + 'vault_id' => $this->data['vault_id'], + 'label' => $this->valueOrNull($this->data, 'summary'), + 'started_at' => $this->data['started_at'], + ]); + } +} diff --git a/app/Domains/Contact/ManageLifeEvents/Services/DestroyLifeEvent.php b/app/Domains/Contact/ManageLifeEvents/Services/DestroyLifeEvent.php index c32ac7659..79d3cad1d 100644 --- a/app/Domains/Contact/ManageLifeEvents/Services/DestroyLifeEvent.php +++ b/app/Domains/Contact/ManageLifeEvents/Services/DestroyLifeEvent.php @@ -4,12 +4,15 @@ namespace App\Domains\Contact\ManageLifeEvents\Services; use App\Interfaces\ServiceInterface; use App\Models\LifeEvent; +use App\Models\TimelineEvent; use App\Services\BaseService; class DestroyLifeEvent extends BaseService implements ServiceInterface { private LifeEvent $lifeEvent; + private TimelineEvent $timelineEvent; + private array $data; /** @@ -23,6 +26,7 @@ class DestroyLifeEvent extends BaseService implements ServiceInterface 'account_id' => 'required|integer|exists:accounts,id', 'vault_id' => 'required|integer|exists:vaults,id', 'author_id' => 'required|integer|exists:users,id', + 'timeline_event_id' => 'required|integer|exists:timeline_events,id', 'life_event_id' => 'required|integer|exists:life_events,id', ]; } @@ -52,13 +56,28 @@ class DestroyLifeEvent extends BaseService implements ServiceInterface $this->validate(); $this->lifeEvent->delete(); + + $this->deleteTimelineEvent(); } private function validate(): void { $this->validateRules($this->data); - $this->lifeEvent = $this->vault->lifeEvents() + $this->timelineEvent = $this->vault->timelineEvents() + ->findOrFail($this->data['timeline_event_id']); + + $this->lifeEvent = $this->timelineEvent->lifeEvents() ->findOrFail($this->data['life_event_id']); } + + private function deleteTimelineEvent(): void + { + // a LifeEvent is always associated with a timeline event + // if we delete the last life event of the timeline event, we need to + // delete the timeline event as well + if ($this->timelineEvent->lifeEvents()->count() === 0) { + $this->timelineEvent->delete(); + } + } } diff --git a/app/Domains/Contact/ManageLifeEvents/Services/DestroyTimelineEvent.php b/app/Domains/Contact/ManageLifeEvents/Services/DestroyTimelineEvent.php new file mode 100644 index 000000000..1536f6b1b --- /dev/null +++ b/app/Domains/Contact/ManageLifeEvents/Services/DestroyTimelineEvent.php @@ -0,0 +1,64 @@ + 'required|integer|exists:accounts,id', + 'vault_id' => 'required|integer|exists:vaults,id', + 'author_id' => 'required|integer|exists:users,id', + 'timeline_event_id' => 'required|integer|exists:timeline_events,id', + ]; + } + + /** + * Get the permissions that apply to the user calling the service. + * + * @return array + */ + public function permissions(): array + { + return [ + 'author_must_belong_to_account', + 'vault_must_belong_to_account', + 'author_must_be_vault_editor', + ]; + } + + /** + * Destroy a timeline event. + * + * @param array $data + */ + public function execute(array $data): void + { + $this->data = $data; + $this->validate(); + + $this->timelineEvent->delete(); + } + + private function validate(): void + { + $this->validateRules($this->data); + + $this->timelineEvent = $this->vault->timelineEvents() + ->findOrFail($this->data['timeline_event_id']); + } +} diff --git a/app/Domains/Contact/ManageLifeEvents/Services/ToggleLifeEvent.php b/app/Domains/Contact/ManageLifeEvents/Services/ToggleLifeEvent.php new file mode 100644 index 000000000..f2a108911 --- /dev/null +++ b/app/Domains/Contact/ManageLifeEvents/Services/ToggleLifeEvent.php @@ -0,0 +1,76 @@ + 'required|integer|exists:accounts,id', + 'vault_id' => 'required|integer|exists:vaults,id', + 'author_id' => 'required|integer|exists:users,id', + 'timeline_event_id' => 'required|integer|exists:timeline_events,id', + 'life_event_id' => 'required|integer|exists:life_events,id', + ]; + } + + /** + * Get the permissions that apply to the user calling the service. + * + * @return array + */ + public function permissions(): array + { + return [ + 'author_must_belong_to_account', + 'vault_must_belong_to_account', + 'author_must_be_vault_editor', + ]; + } + + /** + * Toggle a life event. + * + * @param array $data + * @return LifeEvent + */ + public function execute(array $data): LifeEvent + { + $this->data = $data; + $this->validate(); + $this->update(); + + return $this->lifeEvent; + } + + private function validate(): void + { + $this->validateRules($this->data); + + $timelineEvent = $this->vault->timelineEvents() + ->findOrFail($this->data['timeline_event_id']); + + $this->lifeEvent = $timelineEvent->lifeEvents() + ->findOrFail($this->data['life_event_id']); + } + + private function update(): void + { + $this->lifeEvent->collapsed = ! $this->lifeEvent->collapsed; + $this->lifeEvent->save(); + } +} diff --git a/app/Domains/Contact/ManageLifeEvents/Services/ToggleTimelineEvent.php b/app/Domains/Contact/ManageLifeEvents/Services/ToggleTimelineEvent.php new file mode 100644 index 000000000..2bc5344c5 --- /dev/null +++ b/app/Domains/Contact/ManageLifeEvents/Services/ToggleTimelineEvent.php @@ -0,0 +1,72 @@ + 'required|integer|exists:accounts,id', + 'vault_id' => 'required|integer|exists:vaults,id', + 'author_id' => 'required|integer|exists:users,id', + 'timeline_event_id' => 'required|integer|exists:timeline_events,id', + ]; + } + + /** + * Get the permissions that apply to the user calling the service. + * + * @return array + */ + public function permissions(): array + { + return [ + 'author_must_belong_to_account', + 'vault_must_belong_to_account', + 'author_must_be_vault_editor', + ]; + } + + /** + * Toggle a timeline event. + * + * @param array $data + * @return TimelineEvent + */ + public function execute(array $data): TimelineEvent + { + $this->data = $data; + $this->validate(); + $this->update(); + + return $this->timelineEvent; + } + + private function validate(): void + { + $this->validateRules($this->data); + + $this->timelineEvent = $this->vault->timelineEvents() + ->findOrFail($this->data['timeline_event_id']); + } + + private function update(): void + { + $this->timelineEvent->collapsed = ! $this->timelineEvent->collapsed; + $this->timelineEvent->save(); + } +} diff --git a/app/Domains/Contact/ManageLifeEvents/Services/UpdateLifeEvent.php b/app/Domains/Contact/ManageLifeEvents/Services/UpdateLifeEvent.php index 580bbb413..ff723e052 100644 --- a/app/Domains/Contact/ManageLifeEvents/Services/UpdateLifeEvent.php +++ b/app/Domains/Contact/ManageLifeEvents/Services/UpdateLifeEvent.php @@ -6,6 +6,7 @@ use App\Interfaces\ServiceInterface; use App\Models\Contact; use App\Models\LifeEvent; use App\Models\LifeEventType; +use App\Models\TimelineEvent; use App\Services\BaseService; use Carbon\Carbon; use Illuminate\Support\Collection; @@ -14,6 +15,8 @@ class UpdateLifeEvent extends BaseService implements ServiceInterface { private LifeEvent $lifeEvent; + private TimelineEvent $timelineEvent; + private Collection $partipantsCollection; private array $data; @@ -29,6 +32,7 @@ class UpdateLifeEvent extends BaseService implements ServiceInterface 'account_id' => 'required|integer|exists:accounts,id', 'vault_id' => 'required|integer|exists:vaults,id', 'author_id' => 'required|integer|exists:users,id', + 'timeline_event_id' => 'required|integer|exists:timeline_events,id', 'life_event_type_id' => 'required|integer|exists:life_event_types,id', 'life_event_id' => 'required|integer|exists:life_events,id', 'summary' => 'nullable|string|max:255', @@ -81,11 +85,14 @@ class UpdateLifeEvent extends BaseService implements ServiceInterface { $this->validateRules($this->data); - $this->lifeEvent = $this->vault->lifeEvents() - ->findOrFail($this->data['life_event_id']); + $this->timelineEvent = $this->vault->timelineEvents() + ->findOrFail($this->data['timeline_event_id']); $lifeEventType = LifeEventType::findOrFail($this->data['life_event_type_id']); + $this->lifeEvent = $this->timelineEvent->lifeEvents() + ->findOrFail($this->data['life_event_id']); + $this->vault->lifeEventCategories() ->findOrFail($lifeEventType->lifeEventCategory->id); @@ -105,6 +112,7 @@ class UpdateLifeEvent extends BaseService implements ServiceInterface private function update(): void { + $this->lifeEvent->timeline_event_id = $this->data['timeline_event_id']; $this->lifeEvent->life_event_type_id = $this->data['life_event_type_id']; $this->lifeEvent->summary = $this->valueOrNull($this->data, 'summary'); $this->lifeEvent->description = $this->valueOrNull($this->data, 'description'); @@ -132,6 +140,7 @@ class UpdateLifeEvent extends BaseService implements ServiceInterface { foreach ($this->partipantsCollection as $participant) { $participant->lifeEvents()->attach($this->lifeEvent->id); + $participant->timelineEvents()->syncWithoutDetaching($this->timelineEvent->id); } } } diff --git a/app/Domains/Contact/ManageLifeEvents/Services/UpdateTimelineEvent.php b/app/Domains/Contact/ManageLifeEvents/Services/UpdateTimelineEvent.php new file mode 100644 index 000000000..df9a6723e --- /dev/null +++ b/app/Domains/Contact/ManageLifeEvents/Services/UpdateTimelineEvent.php @@ -0,0 +1,75 @@ + 'required|integer|exists:accounts,id', + 'vault_id' => 'required|integer|exists:vaults,id', + 'author_id' => 'required|integer|exists:users,id', + 'timeline_event_id' => 'required|integer|exists:timeline_events,id', + 'label' => 'nullable|string|max:255', + 'started_at' => 'required|date|date_format:Y-m-d', + ]; + } + + /** + * Get the permissions that apply to the user calling the service. + * + * @return array + */ + public function permissions(): array + { + return [ + 'author_must_belong_to_account', + 'vault_must_belong_to_account', + 'author_must_be_vault_editor', + ]; + } + + /** + * Update a timeline event. + * + * @param array $data + * @return TimelineEvent + */ + public function execute(array $data): TimelineEvent + { + $this->data = $data; + $this->validate(); + $this->update(); + + return $this->timelineEvent; + } + + private function validate(): void + { + $this->validateRules($this->data); + + $this->timelineEvent = $this->vault->timelineEvents() + ->findOrFail($this->data['timeline_event_id']); + } + + private function update(): void + { + $this->timelineEvent->label = $this->valueOrNull($this->data, 'label'); + $this->timelineEvent->started_at = $this->data['started_at']; + $this->timelineEvent->save(); + } +} diff --git a/app/Domains/Contact/ManageLifeEvents/Web/Controllers/ContactModuleLifeEventController.php b/app/Domains/Contact/ManageLifeEvents/Web/Controllers/ContactModuleLifeEventController.php new file mode 100644 index 000000000..b3a773374 --- /dev/null +++ b/app/Domains/Contact/ManageLifeEvents/Web/Controllers/ContactModuleLifeEventController.php @@ -0,0 +1,74 @@ +input('participants')) + ->push(['id' => $contactId]) + ->unique('id') + ->pluck('id') + ->toArray(); + + $carbonDate = Carbon::parse($request->input('started_at')); + + $data = [ + 'account_id' => Auth::user()->account_id, + 'author_id' => Auth::id(), + 'vault_id' => $vaultId, + 'label' => $request->input('label'), + 'timeline_event_id' => $timelineEventId, + 'life_event_type_id' => $request->input('lifeEventTypeId'), + 'summary' => $request->input('summary'), + 'description' => $request->input('description'), + 'happened_at' => $carbonDate->format('Y-m-d'), + 'costs' => $request->input('costs'), + 'currency_id' => $request->input('currency_id'), + 'paid_by_contact_id' => $request->input('paid_by_contact_id'), + 'duration_in_minutes' => $request->input('duration_in_minutes'), + 'distance_in_km' => $request->input('distance_in_km'), + 'from_place' => $request->input('from_place'), + 'to_place' => $request->input('to_place'), + 'place' => $request->input('place'), + 'participant_ids' => $participants, + ]; + + $lifeEvent = (new CreateLifeEvent())->execute($data); + $contact = Contact::find($contactId); + + return response()->json([ + 'data' => ModuleLifeEventViewHelper::dtoLifeEvent($lifeEvent, Auth::user(), $contact), + ], 201); + } + + public function destroy(Request $request, int $vaultId, int $contactId, int $timelineEventId, int $lifeEventId) + { + $data = [ + 'account_id' => Auth::user()->account_id, + 'author_id' => Auth::id(), + 'vault_id' => $vaultId, + 'timeline_event_id' => $timelineEventId, + 'life_event_id' => $lifeEventId, + ]; + + (new DestroyLifeEvent())->execute($data); + + return response()->json([ + 'data' => true, + ], 200); + } +} diff --git a/app/Domains/Contact/ManageLifeEvents/Web/Controllers/ContactModuleTimelineEventController.php b/app/Domains/Contact/ManageLifeEvents/Web/Controllers/ContactModuleTimelineEventController.php new file mode 100644 index 000000000..12edda14a --- /dev/null +++ b/app/Domains/Contact/ManageLifeEvents/Web/Controllers/ContactModuleTimelineEventController.php @@ -0,0 +1,100 @@ +findOrFail($contactId); + + $timelineEvents = $contact + ->timelineEvents() + ->orderBy('started_at', 'desc') + ->paginate(15); + + return response()->json([ + 'data' => ModuleLifeEventViewHelper::timelineEvents($timelineEvents, Auth::user(), $contact), + 'paginator' => PaginatorHelper::getData($timelineEvents), + ], 200); + } + + public function store(Request $request, int $vaultId, int $contactId) + { + $data = [ + 'account_id' => Auth::user()->account_id, + 'author_id' => Auth::id(), + 'vault_id' => $vaultId, + 'label' => $request->input('label'), + 'started_at' => $request->input('started_at'), + ]; + + $timelineEvent = (new CreateTimelineEvent())->execute($data); + + // we also need to add the current contact to the list of participants + // finally, just so we are sure that we don't have the same participant + // twice in the list, we need to remove duplicates + $participants = collect($request->input('participants')) + ->push(['id' => $contactId]) + ->unique('id') + ->pluck('id') + ->toArray(); + + $carbonDate = Carbon::parse($request->input('started_at')); + + $data = [ + 'account_id' => Auth::user()->account_id, + 'author_id' => Auth::id(), + 'vault_id' => $vaultId, + 'label' => $request->input('label'), + 'timeline_event_id' => $timelineEvent->id, + 'life_event_type_id' => $request->input('lifeEventTypeId'), + 'summary' => $request->input('summary'), + 'description' => $request->input('description'), + 'happened_at' => $carbonDate->format('Y-m-d'), + 'costs' => $request->input('costs'), + 'currency_id' => $request->input('currency_id'), + 'paid_by_contact_id' => $request->input('paid_by_contact_id'), + 'duration_in_minutes' => $request->input('duration_in_minutes'), + 'distance_in_km' => $request->input('distance_in_km'), + 'from_place' => $request->input('from_place'), + 'to_place' => $request->input('to_place'), + 'place' => $request->input('place'), + 'participant_ids' => $participants, + ]; + + $lifeEvent = (new CreateLifeEvent())->execute($data); + $contact = Contact::find($contactId); + + return response()->json([ + 'data' => ModuleLifeEventViewHelper::dtoTimelineEvent($timelineEvent, Auth::user(), $contact), + ], 201); + } + + public function destroy(Request $request, int $vaultId, int $contactId, int $timelineEventId) + { + $data = [ + 'account_id' => Auth::user()->account_id, + 'author_id' => Auth::id(), + 'vault_id' => $vaultId, + 'timeline_event_id' => $timelineEventId, + ]; + + (new DestroyTimelineEvent())->execute($data); + + return response()->json([ + 'data' => true, + ], 200); + } +} diff --git a/app/Domains/Contact/ManageLifeEvents/Web/Controllers/ToggleLifeEventController.php b/app/Domains/Contact/ManageLifeEvents/Web/Controllers/ToggleLifeEventController.php new file mode 100644 index 000000000..e03a2004c --- /dev/null +++ b/app/Domains/Contact/ManageLifeEvents/Web/Controllers/ToggleLifeEventController.php @@ -0,0 +1,27 @@ + Auth::user()->account_id, + 'author_id' => Auth::id(), + 'vault_id' => $vaultId, + 'timeline_event_id' => $timelineEventId, + 'life_event_id' => $lifeEventId, + ]; + + (new ToggleLifeEvent())->execute($data); + + return response()->json([ + 'data' => true, + ], 200); + } +} diff --git a/app/Domains/Contact/ManageLifeEvents/Web/Controllers/ToggleTimelineEventController.php b/app/Domains/Contact/ManageLifeEvents/Web/Controllers/ToggleTimelineEventController.php new file mode 100644 index 000000000..b3d10756d --- /dev/null +++ b/app/Domains/Contact/ManageLifeEvents/Web/Controllers/ToggleTimelineEventController.php @@ -0,0 +1,26 @@ + Auth::user()->account_id, + 'author_id' => Auth::id(), + 'vault_id' => $vaultId, + 'timeline_event_id' => $timelineEventId, + ]; + + (new ToggleTimelineEvent())->execute($data); + + return response()->json([ + 'data' => true, + ], 200); + } +} diff --git a/app/Domains/Contact/ManageLifeEvents/Web/ViewHelpers/ModuleLifeEventViewHelper.php b/app/Domains/Contact/ManageLifeEvents/Web/ViewHelpers/ModuleLifeEventViewHelper.php new file mode 100644 index 000000000..e72ee469b --- /dev/null +++ b/app/Domains/Contact/ManageLifeEvents/Web/ViewHelpers/ModuleLifeEventViewHelper.php @@ -0,0 +1,148 @@ +vault->lifeEventCategories() + ->with('lifeEventTypes') + ->orderBy('position', 'asc') + ->get() + ->map(fn (LifeEventCategory $lifeEventCategory) => self::dtoLifeEventCategory($lifeEventCategory)); + + return [ + 'contact' => ContactCardHelper::data($contact), + 'current_date' => Carbon::now($user->timezone)->format('Y-m-d'), + 'current_date_human_format' => DateHelper::format(Carbon::now($user->timezone), $user), + 'life_event_categories' => $lifeEventCategoriesCollection, + 'url' => [ + 'load' => route('contact.timeline_event.index', [ + 'vault' => $contact->vault_id, + 'contact' => $contact->id, + ]), + 'store' => route('contact.timeline_event.store', [ + 'vault' => $contact->vault_id, + 'contact' => $contact->id, + ]), + ], + ]; + } + + public static function dtoLifeEventCategory(LifeEventCategory $lifeEventCategory): array + { + return [ + 'id' => $lifeEventCategory->id, + 'label' => $lifeEventCategory->label, + 'life_event_types' => $lifeEventCategory->lifeEventTypes() + ->orderBy('position', 'asc') + ->get() + ->map(fn (LifeEventType $lifeEventType) => self::dtoLifeEventType($lifeEventType)), + ]; + } + + public static function dtoLifeEventType(LifeEventType $lifeEventType): array + { + return [ + 'id' => $lifeEventType->id, + 'label' => $lifeEventType->label, + ]; + } + + public static function timelineEvents($timelineEvents, User $user, Contact $contact): array + { + $timelineEventsCollection = $timelineEvents->map( + fn (TimelineEvent $timelineEvent) => self::dtoTimelineEvent($timelineEvent, $user, $contact) + ); + + return [ + 'timeline_events' => $timelineEventsCollection, + ]; + } + + public static function dtoTimelineEvent(TimelineEvent $timelineEvent, User $user, Contact $contact): array + { + return [ + 'id' => $timelineEvent->id, + 'label' => $timelineEvent->label, + 'collapsed' => $timelineEvent->collapsed, + 'happened_at' => $timelineEvent->range, + 'life_events' => $timelineEvent->lifeEvents + ->map(fn (LifeEvent $lifeEvent) => self::dtoLifeEvent($lifeEvent, $user, $contact)), + 'url' => [ + 'store' => route('contact.life_event.store', [ + 'vault' => $contact->vault_id, + 'contact' => $contact->id, + 'timelineEvent' => $timelineEvent->id, + ]), + 'toggle' => route('contact.timeline_event.toggle', [ + 'vault' => $contact->vault_id, + 'contact' => $contact->id, + 'timelineEvent' => $timelineEvent->id, + ]), + 'destroy' => route('contact.timeline_event.destroy', [ + 'vault' => $contact->vault_id, + 'contact' => $contact->id, + 'timelineEvent' => $timelineEvent->id, + ]), + ], + ]; + } + + public static function dtoLifeEvent(LifeEvent $lifeEvent, User $user, Contact $contact): array + { + return [ + 'id' => $lifeEvent->id, + 'emotion_id' => $lifeEvent->emotion_id, + 'collapsed' => $lifeEvent->collapsed, + 'summary' => $lifeEvent->summary, + 'description' => $lifeEvent->description, + 'happened_at' => DateHelper::format($lifeEvent->happened_at, $user), + 'costs' => $lifeEvent->costs, + 'currency_id' => $lifeEvent->currency_id, + 'paid_by_contact_id' => $lifeEvent->paid_by_contact_id, + 'duration_in_minutes' => $lifeEvent->duration_in_minutes, + 'distance_in_km' => $lifeEvent->distance_in_km, + 'from_place' => $lifeEvent->from_place, + 'to_place' => $lifeEvent->to_place, + 'place' => $lifeEvent->place, + 'participants' => $lifeEvent->participants->map(fn (Contact $contact) => ContactCardHelper::data($contact)), + 'timeline_event' => [ + 'id' => $lifeEvent->timelineEvent->id, + ], + 'life_event_type' => [ + 'id' => $lifeEvent->lifeEventType->id, + 'label' => $lifeEvent->lifeEventType->label, + 'category' => [ + 'id' => $lifeEvent->lifeEventType->lifeEventCategory->id, + 'label' => $lifeEvent->lifeEventType->lifeEventCategory->label, + ], + ], + 'url' => [ + 'toggle' => route('contact.life_event.toggle', [ + 'vault' => $contact->vault_id, + 'contact' => $contact->id, + 'timelineEvent' => $lifeEvent->timelineEvent->id, + 'lifeEvent' => $lifeEvent->id, + ]), + 'destroy' => route('contact.life_event.destroy', [ + 'vault' => $contact->vault_id, + 'contact' => $contact->id, + 'timelineEvent' => $lifeEvent->timelineEvent->id, + 'lifeEvent' => $lifeEvent->id, + ]), + ], + ]; + } +} diff --git a/app/Domains/Settings/CreateAccount/Jobs/SetupAccount.php b/app/Domains/Settings/CreateAccount/Jobs/SetupAccount.php index c9accb935..52b7f2c77 100644 --- a/app/Domains/Settings/CreateAccount/Jobs/SetupAccount.php +++ b/app/Domains/Settings/CreateAccount/Jobs/SetupAccount.php @@ -419,6 +419,22 @@ class SetupAccount extends QueuableService implements ServiceInterface 'can_be_deleted' => true, ]); + // life events + $module = (new CreateModule())->execute([ + 'account_id' => $this->author->account_id, + 'author_id' => $this->author->id, + 'name' => trans('app.module_life_events'), + 'type' => Module::TYPE_LIFE_EVENTS, + 'can_be_deleted' => false, + ]); + (new AssociateModuleToTemplatePage())->execute([ + 'account_id' => $this->author->account_id, + 'author_id' => $this->author->id, + 'template_id' => $this->template->id, + 'template_page_id' => $templatePageSocial->id, + 'module_id' => $module->id, + ]); + // goals $module = (new CreateModule())->execute([ 'account_id' => $this->author->account_id, diff --git a/app/Domains/Vault/ManageVault/Services/CreateVault.php b/app/Domains/Vault/ManageVault/Services/CreateVault.php index 6c314ea8f..1401846e4 100644 --- a/app/Domains/Vault/ManageVault/Services/CreateVault.php +++ b/app/Domains/Vault/ManageVault/Services/CreateVault.php @@ -204,5 +204,137 @@ class CreateVault extends BaseService implements ServiceInterface 'label_translation_key' => 'vault.settings_life_event_type_transportation_metro', 'can_be_deleted' => true, ]); + + // social category + $category = LifeEventCategory::create([ + 'vault_id' => $this->vault->id, + 'position' => 2, + 'label' => null, + 'label_translation_key' => 'vault.settings_life_event_category_social', + 'can_be_deleted' => true, + ]); + LifeEventType::create([ + 'life_event_category_id' => $category->id, + 'position' => 1, + 'label' => null, + 'label_translation_key' => 'vault.settings_life_event_type_social_ate', + 'can_be_deleted' => true, + ]); + LifeEventType::create([ + 'life_event_category_id' => $category->id, + 'position' => 2, + 'label' => null, + 'label_translation_key' => 'vault.settings_life_event_type_social_drank', + 'can_be_deleted' => true, + ]); + LifeEventType::create([ + 'life_event_category_id' => $category->id, + 'position' => 3, + 'label' => null, + 'label_translation_key' => 'vault.settings_life_event_type_social_bar', + 'can_be_deleted' => true, + ]); + LifeEventType::create([ + 'life_event_category_id' => $category->id, + 'position' => 4, + 'label' => null, + 'label_translation_key' => 'vault.settings_life_event_type_social_movie', + 'can_be_deleted' => true, + ]); + LifeEventType::create([ + 'life_event_category_id' => $category->id, + 'position' => 5, + 'label' => null, + 'label_translation_key' => 'vault.settings_life_event_type_social_tv', + 'can_be_deleted' => true, + ]); + LifeEventType::create([ + 'life_event_category_id' => $category->id, + 'position' => 6, + 'label' => null, + 'label_translation_key' => 'vault.settings_life_event_type_social_tv_show', + 'can_be_deleted' => true, + ]); + + // sport category + $category = LifeEventCategory::create([ + 'vault_id' => $this->vault->id, + 'position' => 3, + 'label' => null, + 'label_translation_key' => 'vault.settings_life_event_category_sport', + 'can_be_deleted' => true, + ]); + LifeEventType::create([ + 'life_event_category_id' => $category->id, + 'position' => 1, + 'label' => null, + 'label_translation_key' => 'vault.settings_life_event_type_sport_ran', + 'can_be_deleted' => true, + ]); + LifeEventType::create([ + 'life_event_category_id' => $category->id, + 'position' => 2, + 'label' => null, + 'label_translation_key' => 'vault.settings_life_event_type_sport_soccer', + 'can_be_deleted' => true, + ]); + LifeEventType::create([ + 'life_event_category_id' => $category->id, + 'position' => 3, + 'label' => null, + 'label_translation_key' => 'vault.settings_life_event_type_sport_basketball', + 'can_be_deleted' => true, + ]); + LifeEventType::create([ + 'life_event_category_id' => $category->id, + 'position' => 4, + 'label' => null, + 'label_translation_key' => 'vault.settings_life_event_type_sport_golf', + 'can_be_deleted' => true, + ]); + LifeEventType::create([ + 'life_event_category_id' => $category->id, + 'position' => 5, + 'label' => null, + 'label_translation_key' => 'vault.settings_life_event_type_sport_tennis', + 'can_be_deleted' => true, + ]); + + // work category + $category = LifeEventCategory::create([ + 'vault_id' => $this->vault->id, + 'position' => 4, + 'label' => null, + 'label_translation_key' => 'vault.settings_life_event_category_work', + 'can_be_deleted' => true, + ]); + LifeEventType::create([ + 'life_event_category_id' => $category->id, + 'position' => 1, + 'label' => null, + 'label_translation_key' => 'vault.settings_life_event_type_work_job', + 'can_be_deleted' => true, + ]); + LifeEventType::create([ + 'life_event_category_id' => $category->id, + 'position' => 2, + 'label' => null, + 'label_translation_key' => 'vault.settings_life_event_type_work_quit', + 'can_be_deleted' => true, + ]); + LifeEventType::create([ + 'life_event_category_id' => $category->id, + 'position' => 3, + 'label' => null, + 'label_translation_key' => 'vault.settings_life_event_type_work_fired', + 'can_be_deleted' => true, + ]); + LifeEventType::create([ + 'life_event_category_id' => $category->id, + 'position' => 4, + 'label' => null, + 'label_translation_key' => 'vault.settings_life_event_type_sport_promotion', + 'can_be_deleted' => true, + ]); } } diff --git a/app/Domains/Vault/ManageVault/Services/UpdateVaultDashboardDefaultTab.php b/app/Domains/Vault/ManageVault/Services/UpdateVaultDashboardDefaultTab.php new file mode 100644 index 000000000..487539432 --- /dev/null +++ b/app/Domains/Vault/ManageVault/Services/UpdateVaultDashboardDefaultTab.php @@ -0,0 +1,55 @@ + 'required|integer|exists:accounts,id', + 'author_id' => 'required|integer|exists:users,id', + 'vault_id' => 'required|integer|exists:vaults,id', + 'show_activity_tab_on_dashboard' => 'required|boolean', + ]; + } + + /** + * Get the permissions that apply to the user calling the service. + * + * @return array + */ + public function permissions(): array + { + return [ + 'author_must_belong_to_account', + 'vault_must_belong_to_account', + 'author_must_be_in_vault', + ]; + } + + /** + * Update a vault's default tab displayed on the dashboard. + * + * @param array $data + * @return Vault + */ + public function execute(array $data): Vault + { + $this->validateRules($data); + + $this->vault->show_activity_tab_on_dashboard = $data['show_activity_tab_on_dashboard']; + $this->vault->save(); + + return $this->vault; + } +} diff --git a/app/Domains/Vault/ManageVault/Web/Controllers/VaultController.php b/app/Domains/Vault/ManageVault/Web/Controllers/VaultController.php index 29888f2c5..479c53252 100644 --- a/app/Domains/Vault/ManageVault/Web/Controllers/VaultController.php +++ b/app/Domains/Vault/ManageVault/Web/Controllers/VaultController.php @@ -2,6 +2,7 @@ namespace App\Domains\Vault\ManageVault\Web\Controllers; +use App\Domains\Contact\ManageLifeEvents\Web\ViewHelpers\ModuleLifeEventViewHelper; use App\Domains\Vault\ManageVault\Services\CreateVault; use App\Domains\Vault\ManageVault\Services\DestroyVault; use App\Domains\Vault\ManageVault\Web\ViewHelpers\VaultCreateViewHelper; @@ -52,6 +53,8 @@ class VaultController extends Controller { $vault = Vault::find($vaultId); + $contact = Auth::user()->getContactInVault($vault); + return Inertia::render('Vault/Dashboard/Index', [ 'layoutData' => VaultIndexViewHelper::layoutData($vault), 'lastUpdatedContacts' => VaultShowViewHelper::lastUpdatedContacts($vault), @@ -59,9 +62,16 @@ class VaultController extends Controller 'favorites' => VaultShowViewHelper::favorites($vault, Auth::user()), 'dueTasks' => VaultShowViewHelper::dueTasks($vault, Auth::user()), 'moodTrackingEvents' => VaultShowViewHelper::moodTrackingEvents($vault, Auth::user()), - 'loadFeedUrl' => route('vault.feed.show', [ - 'vault' => $vaultId, - ]), + 'activityTabShown' => $vault->show_activity_tab_on_dashboard, + 'lifeEvents' => ModuleLifeEventViewHelper::data($contact, Auth::user()), + 'url' => [ + 'feed' => route('vault.feed.show', [ + 'vault' => $vaultId, + ]), + 'default_tab' => route('vault.default_tab.update', [ + 'vault' => $vaultId, + ]), + ], ]); } diff --git a/app/Domains/Vault/ManageVault/Web/Controllers/VaultDefaultTabOnDashboardController.php b/app/Domains/Vault/ManageVault/Web/Controllers/VaultDefaultTabOnDashboardController.php new file mode 100644 index 000000000..32035c929 --- /dev/null +++ b/app/Domains/Vault/ManageVault/Web/Controllers/VaultDefaultTabOnDashboardController.php @@ -0,0 +1,27 @@ + Auth::user()->account_id, + 'author_id' => Auth::id(), + 'vault_id' => $vaultId, + 'show_activity_tab_on_dashboard' => $request->input('show_activity_tab_on_dashboard'), + ]; + + (new UpdateVaultDashboardDefaultTab())->execute($data); + + return response()->json([ + 'data' => true, + ], 200); + } +} diff --git a/app/Domains/Vault/ManageVault/Web/Controllers/VaultLifeEventController.php b/app/Domains/Vault/ManageVault/Web/Controllers/VaultLifeEventController.php new file mode 100644 index 000000000..0e6352fe7 --- /dev/null +++ b/app/Domains/Vault/ManageVault/Web/Controllers/VaultLifeEventController.php @@ -0,0 +1,29 @@ +getContactInVault($vault); + + $timelineEvents = $contact + ->timelineEvents() + ->orderBy('started_at', 'desc') + ->paginate(15); + + return response()->json([ + 'data' => ModuleLifeEventViewHelper::timelineEvents($timelineEvents, Auth::user(), $contact), + 'paginator' => PaginatorHelper::getData($timelineEvents), + ], 200); + } +} diff --git a/app/Models/Contact.php b/app/Models/Contact.php index 3c3ce38fb..0e7b2cb1b 100644 --- a/app/Models/Contact.php +++ b/app/Models/Contact.php @@ -386,6 +386,16 @@ class Contact extends Model return $this->belongsToMany(LifeEvent::class, 'life_event_participants', 'contact_id', 'life_event_id'); } + /** + * Get the timeline events associated with the contact. + * + * @return BelongsToMany + */ + public function timelineEvents(): BelongsToMany + { + return $this->belongsToMany(TimelineEvent::class, 'timeline_event_participants', 'contact_id', 'timeline_event_id'); + } + /** * Get the mood tracking events associated with the contact. * diff --git a/app/Models/LifeEvent.php b/app/Models/LifeEvent.php index 8d8cf2f77..e0503fb42 100644 --- a/app/Models/LifeEvent.php +++ b/app/Models/LifeEvent.php @@ -19,8 +19,10 @@ class LifeEvent extends Model * @var array */ protected $fillable = [ - 'vault_id', + 'timeline_event_id', 'life_event_type_id', + 'emotion_id', + 'collapsed', 'summary', 'description', 'happened_at', @@ -44,13 +46,22 @@ class LifeEvent extends Model ]; /** - * Get the vault associated with the life event. + * The attributes that should be cast to native types. + * + * @var array + */ + protected $casts = [ + 'collapsed' => 'boolean', + ]; + + /** + * Get the timeline event associated with the life event. * * @return BelongsTo */ - public function vault(): BelongsTo + public function timelineEvent(): BelongsTo { - return $this->belongsTo(Vault::class); + return $this->belongsTo(TimelineEvent::class); } /** @@ -73,6 +84,16 @@ class LifeEvent extends Model return $this->belongsTo(Currency::class); } + /** + * Get the emotion associated with the life event. + * + * @return BelongsTo + */ + public function emotion(): BelongsTo + { + return $this->belongsTo(Emotion::class); + } + /** * Get the contact who paid for the life event. * diff --git a/app/Models/Module.php b/app/Models/Module.php index 5997a0106..dd0f1e016 100644 --- a/app/Models/Module.php +++ b/app/Models/Module.php @@ -61,6 +61,8 @@ class Module extends Model public const TYPE_RELIGIONS = 'religions'; + public const TYPE_LIFE_EVENTS = 'life_events'; + /** * The attributes that are mass assignable. * diff --git a/app/Models/TimelineEvent.php b/app/Models/TimelineEvent.php new file mode 100644 index 000000000..fa64cd4bd --- /dev/null +++ b/app/Models/TimelineEvent.php @@ -0,0 +1,118 @@ + + */ + protected $fillable = [ + 'vault_id', + 'started_at', + 'label', + 'collapsed', + ]; + + /** + * The attributes that should be mutated to dates. + * + * @var array + */ + protected $dates = [ + 'started_at', + ]; + + /** + * The attributes that should be cast to native types. + * + * @var array + */ + protected $casts = [ + 'collapsed' => 'boolean', + ]; + + /** + * Get the vault associated with the timeline event. + * + * @return BelongsTo + */ + public function vault(): BelongsTo + { + return $this->belongsTo(Vault::class); + } + + /** + * Get the life events associated with the timeline event. + * + * @return HasMany + */ + public function lifeEvents(): HasMany + { + return $this->hasMany(LifeEvent::class); + } + + /** + * Get the contact records the timeline event is with. + * + * @return BelongsToMany + */ + public function participants(): BelongsToMany + { + return $this->belongsToMany(Contact::class, 'timeline_event_participants', 'timeline_event_id', 'contact_id'); + } + + /** + * Get the date range of the timeline event. + * A timeline event can span over a long period of time. + * While the `started_at` field is used to sort the timeline events, + * we still need to know the date range of the event. + * + * @return Attribute + */ + protected function range(): Attribute + { + return Attribute::make( + get: function ($value) { + $lifeEvents = $this->lifeEvents()->get(); + + if ($lifeEvents->count() === 0) { + return ''; + } + + $firstEvent = $lifeEvents->sortBy('happened_at')->first(); + $lastEvent = $lifeEvents->sortByDesc('happened_at')->first(); + + if ($firstEvent->id === $lastEvent->id) { + return DateHelper::format($firstEvent->happened_at, Auth::user()); + } + + return DateHelper::format($firstEvent->happened_at, Auth::user()). + ' — '. + DateHelper::format($lastEvent->happened_at, Auth::user()); + } + ); + } +} diff --git a/app/Models/Vault.php b/app/Models/Vault.php index e3f38f545..52b0cbf60 100644 --- a/app/Models/Vault.php +++ b/app/Models/Vault.php @@ -42,6 +42,7 @@ class Vault extends Model 'name', 'description', 'default_template_id', + 'show_activity_tab_on_dashboard', 'show_group_tab', 'show_tasks_tab', 'show_files_tab', @@ -61,6 +62,7 @@ class Vault extends Model 'show_journal_tab' => 'boolean', 'show_companies_tab' => 'boolean', 'show_reports_tab' => 'boolean', + 'show_activity_tab_on_dashboard' => 'boolean', ]; /** @@ -233,13 +235,13 @@ class Vault extends Model } /** - * Get the life events associated with the vault. + * Get the timeline events associated with the vault. * * @return HasMany */ - public function lifeEvents(): HasMany + public function timelineEvents(): HasMany { - return $this->hasMany(LifeEvent::class); + return $this->hasMany(TimelineEvent::class); } /** diff --git a/database/factories/LifeEventFactory.php b/database/factories/LifeEventFactory.php index dd4cc52cd..7d5b5aab1 100644 --- a/database/factories/LifeEventFactory.php +++ b/database/factories/LifeEventFactory.php @@ -5,7 +5,7 @@ namespace Database\Factories; use App\Models\Contact; use App\Models\LifeEvent; use App\Models\LifeEventType; -use App\Models\Vault; +use App\Models\TimelineEvent; use Illuminate\Database\Eloquent\Factories\Factory; /** @@ -23,7 +23,7 @@ class LifeEventFactory extends Factory public function definition() { return [ - 'vault_id' => Vault::factory(), + 'timeline_event_id' => TimelineEvent::factory(), 'life_event_type_id' => LifeEventType::factory(), 'summary' => $this->faker->sentence(), 'description' => $this->faker->sentence(), diff --git a/database/factories/TimelineEventFactory.php b/database/factories/TimelineEventFactory.php new file mode 100644 index 000000000..111bb8cd0 --- /dev/null +++ b/database/factories/TimelineEventFactory.php @@ -0,0 +1,29 @@ + + */ +class TimelineEventFactory extends Factory +{ + protected $model = TimelineEvent::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition() + { + return [ + 'vault_id' => Vault::factory(), + 'started_at' => $this->faker->dateTimeThisCentury(), + 'label' => $this->faker->sentence(), + ]; + } +} diff --git a/database/migrations/2014_10_12_000010_create_vaults_table.php b/database/migrations/2014_10_12_000010_create_vaults_table.php index 172d9dc9d..b1bf0dc00 100644 --- a/database/migrations/2014_10_12_000010_create_vaults_table.php +++ b/database/migrations/2014_10_12_000010_create_vaults_table.php @@ -19,6 +19,7 @@ return new class() extends Migration $table->string('name'); $table->string('description')->nullable(); $table->unsignedBigInteger('default_template_id')->nullable(); + $table->boolean('show_activity_tab_on_dashboard')->default(true); $table->boolean('show_group_tab')->default(true); $table->boolean('show_tasks_tab')->default(true); $table->boolean('show_files_tab')->default(true); diff --git a/database/migrations/2022_05_17_155546_create_life_events_table.php b/database/migrations/2022_05_17_155546_create_life_events_table.php index 3ad812e9f..32bcc10c4 100644 --- a/database/migrations/2022_05_17_155546_create_life_events_table.php +++ b/database/migrations/2022_05_17_155546_create_life_events_table.php @@ -43,15 +43,33 @@ return new class() extends Migration $table->foreign('life_event_category_id')->references('id')->on('life_event_categories')->onDelete('cascade'); }); - Schema::create('life_events', function (Blueprint $table) { + Schema::create('timeline_events', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('vault_id'); + $table->date('started_at'); + $table->string('label')->nullable(); + $table->boolean('collapsed')->default(true); + $table->timestamps(); + $table->foreign('vault_id')->references('id')->on('vaults')->onDelete('cascade'); + }); + + Schema::create('timeline_event_participants', function (Blueprint $table) { + $table->unsignedBigInteger('contact_id'); + $table->unsignedBigInteger('timeline_event_id'); + $table->timestamps(); + $table->foreign('contact_id')->references('id')->on('contacts')->onDelete('cascade'); + $table->foreign('timeline_event_id')->references('id')->on('timeline_events')->onDelete('cascade'); + }); + + Schema::create('life_events', function (Blueprint $table) { + $table->id(); + $table->unsignedBigInteger('timeline_event_id'); $table->unsignedBigInteger('life_event_type_id'); $table->unsignedBigInteger('emotion_id')->nullable(); + $table->date('happened_at'); $table->boolean('collapsed')->default(false); $table->string('summary')->nullable(); $table->text('description')->nullable(); - $table->date('happened_at'); $table->integer('costs')->nullable(); $table->unsignedBigInteger('currency_id')->nullable(); $table->unsignedBigInteger('paid_by_contact_id')->nullable(); @@ -61,7 +79,7 @@ return new class() extends Migration $table->string('to_place')->nullable(); $table->string('place')->nullable(); $table->timestamps(); - $table->foreign('vault_id')->references('id')->on('vaults')->onDelete('cascade'); + $table->foreign('timeline_event_id')->references('id')->on('timeline_events')->onDelete('cascade'); $table->foreign('life_event_type_id')->references('id')->on('life_event_types')->onDelete('cascade'); $table->foreign('currency_id')->references('id')->on('currencies')->onDelete('set null'); $table->foreign('paid_by_contact_id')->references('id')->on('contacts')->onDelete('set null'); diff --git a/lang/en/app.php b/lang/en/app.php index 5195a0680..76b4b5a87 100644 --- a/lang/en/app.php +++ b/lang/en/app.php @@ -66,6 +66,7 @@ return [ 'search' => 'Search', 'choose' => 'Choose', 'remove' => 'Remove', + 'change' => 'Change', 'publish' => 'Publish', 'yes' => 'Yes', 'no' => 'No', @@ -94,7 +95,7 @@ return [ 'default_template_page_social' => 'Social', 'default_template_page_feed' => 'Feed', 'default_template_page_information' => 'Information', - 'default_template_page_life_events' => 'Life events & goals', + 'default_template_page_life_events' => 'Life & goals', 'default_template_page_contact' => 'Ways to connect', 'module_names' => 'Contact name', @@ -120,6 +121,7 @@ return [ 'module_photos' => 'Photos', 'module_posts' => 'Posts about the contact', 'module_religions' => 'Religions', + 'module_life_events' => 'Life', 'module_option_default_number_of_items_to_display' => 'Default number of items to display', diff --git a/lang/en/vault.php b/lang/en/vault.php index 82ea4ef80..1719914ec 100644 --- a/lang/en/vault.php +++ b/lang/en/vault.php @@ -196,10 +196,32 @@ return [ 'settings_mood_tracking_parameters_meh' => '😐 Meh', 'settings_mood_tracking_parameters_bad' => '😔 Bad', 'settings_mood_tracking_parameters_awful' => '😩 Awful', + 'settings_life_event_category_transportation' => 'Transportation', 'settings_life_event_type_transportation_bike' => 'Rode a bike', 'settings_life_event_type_transportation_car' => 'Drove', 'settings_life_event_type_transportation_walk' => 'Walked', 'settings_life_event_type_transportation_bus' => 'Took the bus', 'settings_life_event_type_transportation_metro' => 'Took the metro', + + 'settings_life_event_category_social' => 'Social', + 'settings_life_event_type_social_ate' => 'Ate', + 'settings_life_event_type_social_drank' => 'Drank', + 'settings_life_event_type_social_bar' => 'Went to a bar', + 'settings_life_event_type_social_movie' => 'Watched a movie', + 'settings_life_event_type_social_tv' => 'Watched TV', + 'settings_life_event_type_social_tv_show' => 'Watched a tv show', + + 'settings_life_event_category_sport' => 'Sport', + 'settings_life_event_type_sport_ran' => 'Ran', + 'settings_life_event_type_sport_soccer' => 'Played soccer', + 'settings_life_event_type_sport_basketball' => 'Played basketball', + 'settings_life_event_type_sport_golf' => 'Played golf', + 'settings_life_event_type_sport_tennis' => 'Played tennis', + + 'settings_life_event_category_work' => 'Work', + 'settings_life_event_type_work_job' => 'Took a new job', + 'settings_life_event_type_work_quit' => 'Quit job', + 'settings_life_event_type_work_fired' => 'Got fired', + 'settings_life_event_type_sport_promotion' => 'Had a promotion', ]; diff --git a/public/img/contact_blank_life_event.svg b/public/img/contact_blank_life_event.svg new file mode 100644 index 000000000..ea5c80c02 --- /dev/null +++ b/public/img/contact_blank_life_event.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/js/Pages/Vault/Contact/Show.vue b/resources/js/Pages/Vault/Contact/Show.vue index ee62071e3..907616ca4 100644 --- a/resources/js/Pages/Vault/Contact/Show.vue +++ b/resources/js/Pages/Vault/Contact/Show.vue @@ -32,6 +32,7 @@ import Documents from '@/Shared/Modules/Documents.vue'; import Photos from '@/Shared/Modules/Photos.vue'; import Religion from '@/Shared/Modules/Religion.vue'; import Posts from '@/Shared/Modules/Posts.vue'; +import LifeEvent from '@/Shared/Modules/LifeEvent.vue'; import Uploadcare from '@/Components/Uploadcare.vue'; const props = defineProps({ @@ -333,6 +334,8 @@ const destroyAvatar = () => { + + diff --git a/resources/js/Pages/Vault/Dashboard/Index.vue b/resources/js/Pages/Vault/Dashboard/Index.vue index 2a60e8ccb..1d5ca6728 100644 --- a/resources/js/Pages/Vault/Dashboard/Index.vue +++ b/resources/js/Pages/Vault/Dashboard/Index.vue @@ -1,3 +1,55 @@ + + - - diff --git a/resources/js/Shared/Modules/LifeEvent.vue b/resources/js/Shared/Modules/LifeEvent.vue new file mode 100644 index 000000000..a01204349 --- /dev/null +++ b/resources/js/Shared/Modules/LifeEvent.vue @@ -0,0 +1,372 @@ + + + + + diff --git a/routes/web.php b/routes/web.php index cd5689288..c65c68dcd 100644 --- a/routes/web.php +++ b/routes/web.php @@ -23,6 +23,10 @@ use App\Domains\Contact\ManageGroups\Web\Controllers\ContactModuleGroupControlle use App\Domains\Contact\ManageGroups\Web\Controllers\GroupController; use App\Domains\Contact\ManageJobInformation\Web\Controllers\ContactModuleJobInformationController; use App\Domains\Contact\ManageLabels\Web\Controllers\ContactModuleLabelController; +use App\Domains\Contact\ManageLifeEvents\Web\Controllers\ContactModuleLifeEventController; +use App\Domains\Contact\ManageLifeEvents\Web\Controllers\ContactModuleTimelineEventController; +use App\Domains\Contact\ManageLifeEvents\Web\Controllers\ToggleLifeEventController; +use App\Domains\Contact\ManageLifeEvents\Web\Controllers\ToggleTimelineEventController; use App\Domains\Contact\ManageLoans\Web\Controllers\ContactModuleLoanController; use App\Domains\Contact\ManageLoans\Web\Controllers\ContactModuleToggleLoanController; use App\Domains\Contact\ManageMoodTrackingEvents\Web\Controllers\ContactMoodTrackingEventsController; @@ -103,6 +107,7 @@ use App\Domains\Vault\ManageReports\Web\Controllers\ReportIndexController; use App\Domains\Vault\ManageReports\Web\Controllers\ReportMoodTrackingEventController; use App\Domains\Vault\ManageTasks\Web\Controllers\VaultTaskController; use App\Domains\Vault\ManageVault\Web\Controllers\VaultController; +use App\Domains\Vault\ManageVault\Web\Controllers\VaultDefaultTabOnDashboardController; use App\Domains\Vault\ManageVault\Web\Controllers\VaultFeedController; use App\Domains\Vault\ManageVault\Web\Controllers\VaultReminderController; use App\Domains\Vault\ManageVaultSettings\Web\Controllers\VaultSettingsContactImportantDateTypeController; @@ -173,6 +178,9 @@ Route::middleware([ Route::middleware(['vault'])->prefix('{vault}')->group(function () { Route::get('', [VaultController::class, 'show'])->name('vault.show'); + // update dashboard's default tab + Route::put('defaultTab', [VaultDefaultTabOnDashboardController::class, 'update'])->name('vault.default_tab.update'); + // reminders Route::get('reminders', [VaultReminderController::class, 'index'])->name('vault.reminder.index'); @@ -330,6 +338,15 @@ Route::middleware([ Route::post('groups', [ContactModuleGroupController::class, 'store'])->name('contact.group.store'); Route::delete('groups/{group}', [ContactModuleGroupController::class, 'destroy'])->name('contact.group.destroy'); + // timeline events (which contain life events) + Route::get('timelineEvents', [ContactModuleTimelineEventController::class, 'index'])->name('contact.timeline_event.index'); + Route::post('timelineEvents', [ContactModuleTimelineEventController::class, 'store'])->name('contact.timeline_event.store'); + Route::post('timelineEvents/{timelineEvent}/toggle', [ToggleTimelineEventController::class, 'store'])->name('contact.timeline_event.toggle'); + Route::post('timelineEvents/{timelineEvent}', [ContactModuleLifeEventController::class, 'store'])->name('contact.life_event.store'); + Route::delete('timelineEvents/{timelineEvent}', [ContactModuleTimelineEventController::class, 'destroy'])->name('contact.timeline_event.destroy'); + Route::post('timelineEvents/{timelineEvent}/lifeEvents/{lifeEvent}/toggle', [ToggleLifeEventController::class, 'store'])->name('contact.life_event.toggle'); + Route::delete('timelineEvents/{timelineEvent}/lifeEvents/{lifeEvent}', [ContactModuleLifeEventController::class, 'destroy'])->name('contact.life_event.destroy'); + // mood tracking events Route::post('moodTrackingEvents', [ContactMoodTrackingEventsController::class, 'store'])->name('contact.mood_tracking_event.store'); }); diff --git a/tests/Unit/Domains/Contact/ManageLifeEvents/Services/CreateLifeEventTest.php b/tests/Unit/Domains/Contact/ManageLifeEvents/Services/CreateLifeEventTest.php index 0cf043478..56221c3bd 100644 --- a/tests/Unit/Domains/Contact/ManageLifeEvents/Services/CreateLifeEventTest.php +++ b/tests/Unit/Domains/Contact/ManageLifeEvents/Services/CreateLifeEventTest.php @@ -8,6 +8,7 @@ use App\Models\Account; use App\Models\Contact; use App\Models\LifeEventCategory; use App\Models\LifeEventType; +use App\Models\TimelineEvent; use App\Models\User; use App\Models\Vault; use Illuminate\Database\Eloquent\ModelNotFoundException; @@ -26,10 +27,11 @@ class CreateLifeEventTest extends TestCase $vault = $this->createVault($user->account); $vault = $this->setPermissionInVault($user, Vault::PERMISSION_EDIT, $vault); $contact = Contact::factory()->create(['vault_id' => $vault->id]); + $timelineEvent = TimelineEvent::factory()->create(['vault_id' => $vault->id]); $lifeEventCategory = LifeEventCategory::factory()->create(['vault_id' => $vault->id]); $lifeEventType = LifeEventType::factory()->create(['life_event_category_id' => $lifeEventCategory->id]); - $this->executeService($user, $user->account, $vault, $contact, $lifeEventType, 'test'); + $this->executeService($user, $user->account, $vault, $contact, $lifeEventType, $timelineEvent); } /** @test */ @@ -53,10 +55,11 @@ class CreateLifeEventTest extends TestCase $vault = $this->createVault($user->account); $vault = $this->setPermissionInVault($user, Vault::PERMISSION_EDIT, $vault); $contact = Contact::factory()->create(['vault_id' => $vault->id]); + $timelineEvent = TimelineEvent::factory()->create(['vault_id' => $vault->id]); $lifeEventCategory = LifeEventCategory::factory()->create(['vault_id' => $vault->id]); $lifeEventType = LifeEventType::factory()->create(['life_event_category_id' => $lifeEventCategory->id]); - $this->executeService($user, $account, $vault, $contact, $lifeEventType, 'test'); + $this->executeService($user, $account, $vault, $contact, $lifeEventType, $timelineEvent); } /** @test */ @@ -69,10 +72,11 @@ class CreateLifeEventTest extends TestCase $vault = $this->createVault($account); $vault = $this->setPermissionInVault($user, Vault::PERMISSION_EDIT, $vault); $contact = Contact::factory()->create(['vault_id' => $vault->id]); + $timelineEvent = TimelineEvent::factory()->create(['vault_id' => $vault->id]); $lifeEventCategory = LifeEventCategory::factory()->create(['vault_id' => $vault->id]); $lifeEventType = LifeEventType::factory()->create(['life_event_category_id' => $lifeEventCategory->id]); - $this->executeService($user, $user->account, $vault, $contact, $lifeEventType, 'test'); + $this->executeService($user, $user->account, $vault, $contact, $lifeEventType, $timelineEvent); } /** @test */ @@ -84,10 +88,27 @@ class CreateLifeEventTest extends TestCase $vault = $this->createVault($user->account); $vault = $this->setPermissionInVault($user, Vault::PERMISSION_VIEW, $vault); $contact = Contact::factory()->create(['vault_id' => $vault->id]); + $timelineEvent = TimelineEvent::factory()->create(['vault_id' => $vault->id]); $lifeEventCategory = LifeEventCategory::factory()->create(['vault_id' => $vault->id]); $lifeEventType = LifeEventType::factory()->create(['life_event_category_id' => $lifeEventCategory->id]); - $this->executeService($user, $user->account, $vault, $contact, $lifeEventType, 'test'); + $this->executeService($user, $user->account, $vault, $contact, $lifeEventType, $timelineEvent); + } + + /** @test */ + public function it_fails_if_timeline_event_doesnt_belong_to_vault(): void + { + $this->expectException(ModelNotFoundException::class); + + $user = $this->createUser(); + $vault = $this->createVault($user->account); + $vault = $this->setPermissionInVault($user, Vault::PERMISSION_EDIT, $vault); + $contact = Contact::factory()->create(); + $timelineEvent = TimelineEvent::factory()->create([]); + $lifeEventCategory = LifeEventCategory::factory()->create(['vault_id' => $vault->id]); + $lifeEventType = LifeEventType::factory()->create(['life_event_category_id' => $lifeEventCategory->id]); + + $this->executeService($user, $user->account, $vault, $contact, $lifeEventType, $timelineEvent); } /** @test */ @@ -99,10 +120,11 @@ class CreateLifeEventTest extends TestCase $vault = $this->createVault($user->account); $vault = $this->setPermissionInVault($user, Vault::PERMISSION_EDIT, $vault); $contact = Contact::factory()->create(); + $timelineEvent = TimelineEvent::factory()->create(['vault_id' => $vault->id]); $lifeEventCategory = LifeEventCategory::factory()->create(['vault_id' => $vault->id]); $lifeEventType = LifeEventType::factory()->create(['life_event_category_id' => $lifeEventCategory->id]); - $this->executeService($user, $user->account, $vault, $contact, $lifeEventType, 'test'); + $this->executeService($user, $user->account, $vault, $contact, $lifeEventType, $timelineEvent); } /** @test */ @@ -114,20 +136,21 @@ class CreateLifeEventTest extends TestCase $vault = $this->createVault($user->account); $vault = $this->setPermissionInVault($user, Vault::PERMISSION_EDIT, $vault); $contact = Contact::factory()->create(['vault_id' => $vault->id]); - + $timelineEvent = TimelineEvent::factory()->create(['vault_id' => $vault->id]); $lifeEventCategory = LifeEventCategory::factory()->create(); $lifeEventType = LifeEventType::factory()->create(['life_event_category_id' => $lifeEventCategory->id]); - $this->executeService($user, $user->account, $vault, $contact, $lifeEventType, 'test'); + $this->executeService($user, $user->account, $vault, $contact, $lifeEventType, $timelineEvent); } - private function executeService(User $author, Account $account, Vault $vault, Contact $contact, LifeEventType $life_event_type, string $summary): void + private function executeService(User $author, Account $account, Vault $vault, Contact $contact, LifeEventType $lifeEventType, TimelineEvent $timelineEvent): void { $request = [ 'account_id' => $account->id, 'vault_id' => $vault->id, 'author_id' => $author->id, - 'life_event_type_id' => $life_event_type->id, + 'timeline_event_id' => $timelineEvent->id, + 'life_event_type_id' => $lifeEventType->id, 'summary' => null, 'description' => null, 'happened_at' => '1982-02-04', @@ -146,8 +169,8 @@ class CreateLifeEventTest extends TestCase $this->assertDatabaseHas('life_events', [ 'id' => $lifeEvent->id, - 'vault_id' => $vault->id, - 'life_event_type_id' => $life_event_type->id, + 'timeline_event_id' => $timelineEvent->id, + 'life_event_type_id' => $lifeEventType->id, 'summary' => null, 'happened_at' => '1982-02-04 00:00:00', ]); diff --git a/tests/Unit/Domains/Contact/ManageLifeEvents/Services/CreateTimelineEventTest.php b/tests/Unit/Domains/Contact/ManageLifeEvents/Services/CreateTimelineEventTest.php new file mode 100644 index 000000000..21153168e --- /dev/null +++ b/tests/Unit/Domains/Contact/ManageLifeEvents/Services/CreateTimelineEventTest.php @@ -0,0 +1,97 @@ +createUser(); + $vault = $this->createVault($user->account); + $vault = $this->setPermissionInVault($user, Vault::PERMISSION_EDIT, $vault); + + $this->executeService($user, $user->account, $vault); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given(): void + { + $request = [ + 'summary' => 'Ross', + ]; + + $this->expectException(ValidationException::class); + (new CreateTimelineEvent())->execute($request); + } + + /** @test */ + public function it_fails_if_user_doesnt_belong_to_account(): void + { + $this->expectException(ModelNotFoundException::class); + + $user = $this->createUser(); + $account = Account::factory()->create(); + $vault = $this->createVault($user->account); + $vault = $this->setPermissionInVault($user, Vault::PERMISSION_EDIT, $vault); + + $this->executeService($user, $account, $vault); + } + + /** @test */ + public function it_fails_if_vault_doesnt_belong_to_account(): void + { + $this->expectException(ModelNotFoundException::class); + + $user = $this->createUser(); + $account = Account::factory()->create(); + $vault = $this->createVault($account); + $vault = $this->setPermissionInVault($user, Vault::PERMISSION_EDIT, $vault); + + $this->executeService($user, $user->account, $vault); + } + + /** @test */ + public function it_fails_if_user_isnt_vault_editor(): void + { + $this->expectException(NotEnoughPermissionException::class); + + $user = $this->createUser(); + $vault = $this->createVault($user->account); + $vault = $this->setPermissionInVault($user, Vault::PERMISSION_VIEW, $vault); + + $this->executeService($user, $user->account, $vault); + } + + private function executeService(User $author, Account $account, Vault $vault): void + { + $request = [ + 'account_id' => $account->id, + 'vault_id' => $vault->id, + 'author_id' => $author->id, + 'label' => null, + 'started_at' => '1982-02-04', + ]; + + $timelineEvent = (new CreateTimelineEvent())->execute($request); + + $this->assertDatabaseHas('timeline_events', [ + 'id' => $timelineEvent->id, + 'vault_id' => $vault->id, + 'label' => null, + 'started_at' => '1982-02-04 00:00:00', + ]); + } +} diff --git a/tests/Unit/Domains/Contact/ManageLifeEvents/Services/DestroyLifeEventTest.php b/tests/Unit/Domains/Contact/ManageLifeEvents/Services/DestroyLifeEventTest.php index 7a46ed89a..2dac9976f 100644 --- a/tests/Unit/Domains/Contact/ManageLifeEvents/Services/DestroyLifeEventTest.php +++ b/tests/Unit/Domains/Contact/ManageLifeEvents/Services/DestroyLifeEventTest.php @@ -6,8 +6,7 @@ use App\Domains\Contact\ManageLifeEvents\Services\DestroyLifeEvent; use App\Exceptions\NotEnoughPermissionException; use App\Models\Account; use App\Models\LifeEvent; -use App\Models\LifeEventCategory; -use App\Models\LifeEventType; +use App\Models\TimelineEvent; use App\Models\User; use App\Models\Vault; use Illuminate\Database\Eloquent\ModelNotFoundException; @@ -25,15 +24,13 @@ class DestroyLifeEventTest extends TestCase $user = $this->createUser(); $vault = $this->createVault($user->account); $vault = $this->setPermissionInVault($user, Vault::PERMISSION_EDIT, $vault); - $lifeEventCategory = LifeEventCategory::factory()->create(['vault_id' => $vault->id]); - $lifeEventType = LifeEventType::factory()->create(['life_event_category_id' => $lifeEventCategory->id]); + $timelineEvent = TimelineEvent::factory()->create(['vault_id' => $vault->id]); $lifeEvent = LifeEvent::factory()->create([ - 'vault_id' => $vault->id, - 'life_event_type_id' => $lifeEventType->id, + 'timeline_event_id' => $timelineEvent->id, ]); - $this->executeService($user, $user->account, $vault, $lifeEvent); + $this->executeService($user, $user->account, $vault, $lifeEvent, $timelineEvent); } /** @test */ @@ -56,15 +53,13 @@ class DestroyLifeEventTest extends TestCase $account = Account::factory()->create(); $vault = $this->createVault($user->account); $vault = $this->setPermissionInVault($user, Vault::PERMISSION_EDIT, $vault); - $lifeEventCategory = LifeEventCategory::factory()->create(['vault_id' => $vault->id]); - $lifeEventType = LifeEventType::factory()->create(['life_event_category_id' => $lifeEventCategory->id]); + $timelineEvent = TimelineEvent::factory()->create(['vault_id' => $vault->id]); $lifeEvent = LifeEvent::factory()->create([ - 'vault_id' => $vault->id, - 'life_event_type_id' => $lifeEventType->id, + 'timeline_event_id' => $timelineEvent->id, ]); - $this->executeService($user, $account, $vault, $lifeEvent); + $this->executeService($user, $account, $vault, $lifeEvent, $timelineEvent); } /** @test */ @@ -76,15 +71,13 @@ class DestroyLifeEventTest extends TestCase $account = Account::factory()->create(); $vault = $this->createVault($account); $vault = $this->setPermissionInVault($user, Vault::PERMISSION_EDIT, $vault); - $lifeEventCategory = LifeEventCategory::factory()->create(['vault_id' => $vault->id]); - $lifeEventType = LifeEventType::factory()->create(['life_event_category_id' => $lifeEventCategory->id]); + $timelineEvent = TimelineEvent::factory()->create(['vault_id' => $vault->id]); $lifeEvent = LifeEvent::factory()->create([ - 'vault_id' => $vault->id, - 'life_event_type_id' => $lifeEventType->id, + 'timeline_event_id' => $timelineEvent->id, ]); - $this->executeService($user, $user->account, $vault, $lifeEvent); + $this->executeService($user, $user->account, $vault, $lifeEvent, $timelineEvent); } /** @test */ @@ -95,41 +88,54 @@ class DestroyLifeEventTest extends TestCase $user = $this->createUser(); $vault = $this->createVault($user->account); $vault = $this->setPermissionInVault($user, Vault::PERMISSION_VIEW, $vault); - $lifeEventCategory = LifeEventCategory::factory()->create(['vault_id' => $vault->id]); - $lifeEventType = LifeEventType::factory()->create(['life_event_category_id' => $lifeEventCategory->id]); + $timelineEvent = TimelineEvent::factory()->create(['vault_id' => $vault->id]); $lifeEvent = LifeEvent::factory()->create([ - 'vault_id' => $vault->id, - 'life_event_type_id' => $lifeEventType->id, + 'timeline_event_id' => $timelineEvent->id, ]); - $this->executeService($user, $user->account, $vault, $lifeEvent); + $this->executeService($user, $user->account, $vault, $lifeEvent, $timelineEvent); } /** @test */ - public function it_fails_if_life_event_does_not_belong_to_vault(): void + public function it_fails_if_timeline_event_does_not_belong_to_vault(): void { $this->expectException(ModelNotFoundException::class); $user = $this->createUser(); $vault = $this->createVault($user->account); $vault = $this->setPermissionInVault($user, Vault::PERMISSION_EDIT, $vault); - $lifeEventCategory = LifeEventCategory::factory()->create(['vault_id' => $vault->id]); - $lifeEventType = LifeEventType::factory()->create(['life_event_category_id' => $lifeEventCategory->id]); + $timelineEvent = TimelineEvent::factory()->create([]); $lifeEvent = LifeEvent::factory()->create([ - 'life_event_type_id' => $lifeEventType->id, + 'timeline_event_id' => $timelineEvent->id, ]); - $this->executeService($user, $user->account, $vault, $lifeEvent); + $this->executeService($user, $user->account, $vault, $lifeEvent, $timelineEvent); } - private function executeService(User $user, Account $account, Vault $vault, LifeEvent $lifeEvent): void + /** @test */ + public function it_fails_if_life_events_doesn_belong_to_timeline_event(): void + { + $this->expectException(ModelNotFoundException::class); + + $user = $this->createUser(); + $vault = $this->createVault($user->account); + $vault = $this->setPermissionInVault($user, Vault::PERMISSION_EDIT, $vault); + $timelineEvent = TimelineEvent::factory()->create(['vault_id' => $vault->id]); + + $lifeEvent = LifeEvent::factory()->create([]); + + $this->executeService($user, $user->account, $vault, $lifeEvent, $timelineEvent); + } + + private function executeService(User $user, Account $account, Vault $vault, LifeEvent $lifeEvent, TimelineEvent $timelineEvent): void { $request = [ 'account_id' => $account->id, 'vault_id' => $vault->id, 'author_id' => $user->id, + 'timeline_event_id' => $timelineEvent->id, 'life_event_id' => $lifeEvent->id, ]; @@ -138,5 +144,9 @@ class DestroyLifeEventTest extends TestCase $this->assertDatabaseMissing('life_events', [ 'id' => $lifeEvent->id, ]); + + $this->assertDatabaseMissing('timeline_events', [ + 'id' => $timelineEvent->id, + ]); } } diff --git a/tests/Unit/Domains/Contact/ManageLifeEvents/Services/DestroyTimelineEventTest.php b/tests/Unit/Domains/Contact/ManageLifeEvents/Services/DestroyTimelineEventTest.php new file mode 100644 index 000000000..7c4450b15 --- /dev/null +++ b/tests/Unit/Domains/Contact/ManageLifeEvents/Services/DestroyTimelineEventTest.php @@ -0,0 +1,119 @@ +createUser(); + $vault = $this->createVault($user->account); + $vault = $this->setPermissionInVault($user, Vault::PERMISSION_EDIT, $vault); + $timelineEvent = TimelineEvent::factory()->create([ + 'vault_id' => $vault->id, + ]); + + $this->executeService($user, $user->account, $vault, $timelineEvent); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given(): void + { + $request = [ + 'summary' => 'Ross', + ]; + + $this->expectException(ValidationException::class); + (new DestroyTimelineEvent())->execute($request); + } + + /** @test */ + public function it_fails_if_user_doesnt_belong_to_account(): void + { + $this->expectException(ModelNotFoundException::class); + + $user = $this->createUser(); + $account = Account::factory()->create(); + $vault = $this->createVault($user->account); + $vault = $this->setPermissionInVault($user, Vault::PERMISSION_EDIT, $vault); + $timelineEvent = TimelineEvent::factory()->create([ + 'vault_id' => $vault->id, + ]); + + $this->executeService($user, $account, $vault, $timelineEvent); + } + + /** @test */ + public function it_fails_if_vault_doesnt_belong_to_account(): void + { + $this->expectException(ModelNotFoundException::class); + + $user = $this->createUser(); + $account = Account::factory()->create(); + $vault = $this->createVault($account); + $vault = $this->setPermissionInVault($user, Vault::PERMISSION_EDIT, $vault); + $timelineEvent = TimelineEvent::factory()->create([ + 'vault_id' => $vault->id, + ]); + + $this->executeService($user, $user->account, $vault, $timelineEvent); + } + + /** @test */ + public function it_fails_if_user_doesnt_have_right_permission_in_initial_vault(): void + { + $this->expectException(NotEnoughPermissionException::class); + + $user = $this->createUser(); + $vault = $this->createVault($user->account); + $vault = $this->setPermissionInVault($user, Vault::PERMISSION_VIEW, $vault); + $timelineEvent = TimelineEvent::factory()->create([ + 'vault_id' => $vault->id, + ]); + + $this->executeService($user, $user->account, $vault, $timelineEvent); + } + + /** @test */ + public function it_fails_if_timeline_event_does_not_belong_to_vault(): void + { + $this->expectException(ModelNotFoundException::class); + + $user = $this->createUser(); + $vault = $this->createVault($user->account); + $vault = $this->setPermissionInVault($user, Vault::PERMISSION_EDIT, $vault); + $timelineEvent = TimelineEvent::factory()->create([]); + + $this->executeService($user, $user->account, $vault, $timelineEvent); + } + + private function executeService(User $user, Account $account, Vault $vault, TimelineEvent $timelineEvent): void + { + $request = [ + 'account_id' => $account->id, + 'vault_id' => $vault->id, + 'author_id' => $user->id, + 'timeline_event_id' => $timelineEvent->id, + ]; + + (new DestroyTimelineEvent)->execute($request); + + $this->assertDatabaseMissing('timeline_events', [ + 'id' => $timelineEvent->id, + ]); + } +} diff --git a/tests/Unit/Domains/Contact/ManageLifeEvents/Services/ToggleLifeEventTest.php b/tests/Unit/Domains/Contact/ManageLifeEvents/Services/ToggleLifeEventTest.php new file mode 100644 index 000000000..0b4a40d5b --- /dev/null +++ b/tests/Unit/Domains/Contact/ManageLifeEvents/Services/ToggleLifeEventTest.php @@ -0,0 +1,141 @@ +createUser(); + $vault = $this->createVault($user->account); + $vault = $this->setPermissionInVault($user, Vault::PERMISSION_EDIT, $vault); + $contact = Contact::factory()->create(['vault_id' => $vault->id]); + $timelineEvent = TimelineEvent::factory()->create(['vault_id' => $vault->id]); + + $lifeEvent = LifeEvent::factory()->create([ + 'timeline_event_id' => $timelineEvent->id, + ]); + + $this->executeService($user, $user->account, $vault, $contact, $lifeEvent, $timelineEvent); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given(): void + { + $request = [ + 'summary' => 'Ross', + ]; + + $this->expectException(ValidationException::class); + (new ToggleLifeEvent())->execute($request); + } + + /** @test */ + public function it_fails_if_user_doesnt_belong_to_account(): void + { + $this->expectException(ModelNotFoundException::class); + + $user = $this->createUser(); + $account = Account::factory()->create(); + $vault = $this->createVault($user->account); + $vault = $this->setPermissionInVault($user, Vault::PERMISSION_EDIT, $vault); + $contact = Contact::factory()->create(['vault_id' => $vault->id]); + $timelineEvent = TimelineEvent::factory()->create(['vault_id' => $vault->id]); + + $lifeEvent = LifeEvent::factory()->create([ + 'timeline_event_id' => $timelineEvent->id, + ]); + + $this->executeService($user, $account, $vault, $contact, $lifeEvent, $timelineEvent); + } + + /** @test */ + public function it_fails_if_vault_doesnt_belong_to_account(): void + { + $this->expectException(ModelNotFoundException::class); + + $user = $this->createUser(); + $account = Account::factory()->create(); + $vault = $this->createVault($account); + $vault = $this->setPermissionInVault($user, Vault::PERMISSION_EDIT, $vault); + $contact = Contact::factory()->create(['vault_id' => $vault->id]); + $timelineEvent = TimelineEvent::factory()->create(['vault_id' => $vault->id]); + + $lifeEvent = LifeEvent::factory()->create([ + 'timeline_event_id' => $timelineEvent->id, + ]); + + $this->executeService($user, $user->account, $vault, $contact, $lifeEvent, $timelineEvent); + } + + /** @test */ + public function it_fails_if_user_isnt_vault_editor(): void + { + $this->expectException(NotEnoughPermissionException::class); + + $user = $this->createUser(); + $vault = $this->createVault($user->account); + $vault = $this->setPermissionInVault($user, Vault::PERMISSION_VIEW, $vault); + $contact = Contact::factory()->create(['vault_id' => $vault->id]); + $timelineEvent = TimelineEvent::factory()->create(['vault_id' => $vault->id]); + + $lifeEvent = LifeEvent::factory()->create([ + 'timeline_event_id' => $timelineEvent->id, + ]); + + $this->executeService($user, $user->account, $vault, $contact, $lifeEvent, $timelineEvent); + } + + /** @test */ + public function it_fails_if_timeline_event_doesnt_belong_to_account(): void + { + $this->expectException(ModelNotFoundException::class); + + $user = $this->createUser(); + $vault = $this->createVault($user->account); + $vault = $this->setPermissionInVault($user, Vault::PERMISSION_EDIT, $vault); + $contact = Contact::factory()->create(['vault_id' => $vault->id]); + + $timelineEvent = TimelineEvent::factory()->create(); + $lifeEvent = LifeEvent::factory()->create([ + 'timeline_event_id' => $timelineEvent->id, + ]); + + $this->executeService($user, $user->account, $vault, $contact, $lifeEvent, $timelineEvent); + } + + private function executeService(User $author, Account $account, Vault $vault, Contact $contact, LifeEvent $lifeEvent, TimelineEvent $timelineEvent): void + { + $request = [ + 'account_id' => $account->id, + 'vault_id' => $vault->id, + 'author_id' => $author->id, + 'timeline_event_id' => $timelineEvent->id, + 'life_event_id' => $lifeEvent->id, + ]; + + $lifeEvent = (new ToggleLifeEvent())->execute($request); + + $this->assertDatabaseHas('life_events', [ + 'id' => $lifeEvent->id, + 'timeline_event_id' => $timelineEvent->id, + 'collapsed' => true, + ]); + } +} diff --git a/tests/Unit/Domains/Contact/ManageLifeEvents/Services/ToggleTimelineEventTest.php b/tests/Unit/Domains/Contact/ManageLifeEvents/Services/ToggleTimelineEventTest.php new file mode 100644 index 000000000..68caf3456 --- /dev/null +++ b/tests/Unit/Domains/Contact/ManageLifeEvents/Services/ToggleTimelineEventTest.php @@ -0,0 +1,126 @@ +createUser(); + $vault = $this->createVault($user->account); + $vault = $this->setPermissionInVault($user, Vault::PERMISSION_EDIT, $vault); + + $timelineEvent = TimelineEvent::factory()->create([ + 'vault_id' => $vault->id, + ]); + + $this->executeService($user, $user->account, $vault, $timelineEvent); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given(): void + { + $request = [ + 'summary' => 'Ross', + ]; + + $this->expectException(ValidationException::class); + (new ToggleTimelineEvent())->execute($request); + } + + /** @test */ + public function it_fails_if_user_doesnt_belong_to_account(): void + { + $this->expectException(ModelNotFoundException::class); + + $user = $this->createUser(); + $account = Account::factory()->create(); + $vault = $this->createVault($user->account); + $vault = $this->setPermissionInVault($user, Vault::PERMISSION_EDIT, $vault); + + $timelineEvent = TimelineEvent::factory()->create([ + 'vault_id' => $vault->id, + ]); + + $this->executeService($user, $account, $vault, $timelineEvent); + } + + /** @test */ + public function it_fails_if_vault_doesnt_belong_to_account(): void + { + $this->expectException(ModelNotFoundException::class); + + $user = $this->createUser(); + $account = Account::factory()->create(); + $vault = $this->createVault($account); + $vault = $this->setPermissionInVault($user, Vault::PERMISSION_EDIT, $vault); + + $timelineEvent = TimelineEvent::factory()->create([ + 'vault_id' => $vault->id, + ]); + + $this->executeService($user, $user->account, $vault, $timelineEvent); + } + + /** @test */ + public function it_fails_if_user_isnt_vault_editor(): void + { + $this->expectException(NotEnoughPermissionException::class); + + $user = $this->createUser(); + $vault = $this->createVault($user->account); + $vault = $this->setPermissionInVault($user, Vault::PERMISSION_VIEW, $vault); + + $timelineEvent = TimelineEvent::factory()->create([ + 'vault_id' => $vault->id, + ]); + + $this->executeService($user, $user->account, $vault, $timelineEvent); + } + + /** @test */ + public function it_fails_if_timeline_event_doesnt_belong_to_vault(): void + { + $this->expectException(ModelNotFoundException::class); + + $user = $this->createUser(); + $vault = $this->createVault($user->account); + $vault = $this->setPermissionInVault($user, Vault::PERMISSION_EDIT, $vault); + + $timelineEvent = TimelineEvent::factory()->create(); + + $this->executeService($user, $user->account, $vault, $timelineEvent); + } + + private function executeService(User $author, Account $account, Vault $vault, TimelineEvent $timelineEvent): void + { + $request = [ + 'account_id' => $account->id, + 'vault_id' => $vault->id, + 'author_id' => $author->id, + 'timeline_event_id' => $timelineEvent->id, + ]; + + $timelineEvent = (new ToggleTimelineEvent())->execute($request); + + $this->assertDatabaseHas('timeline_events', [ + 'id' => $timelineEvent->id, + 'vault_id' => $vault->id, + 'collapsed' => false, + ]); + } +} diff --git a/tests/Unit/Domains/Contact/ManageLifeEvents/Services/UpdateLifeEventTest.php b/tests/Unit/Domains/Contact/ManageLifeEvents/Services/UpdateLifeEventTest.php index 9b5548798..c828ab3d8 100644 --- a/tests/Unit/Domains/Contact/ManageLifeEvents/Services/UpdateLifeEventTest.php +++ b/tests/Unit/Domains/Contact/ManageLifeEvents/Services/UpdateLifeEventTest.php @@ -9,6 +9,7 @@ use App\Models\Contact; use App\Models\LifeEvent; use App\Models\LifeEventCategory; use App\Models\LifeEventType; +use App\Models\TimelineEvent; use App\Models\User; use App\Models\Vault; use Illuminate\Database\Eloquent\ModelNotFoundException; @@ -27,15 +28,16 @@ class UpdateLifeEventTest extends TestCase $vault = $this->createVault($user->account); $vault = $this->setPermissionInVault($user, Vault::PERMISSION_EDIT, $vault); $contact = Contact::factory()->create(['vault_id' => $vault->id]); + $timelineEvent = TimelineEvent::factory()->create(['vault_id' => $vault->id]); $lifeEventCategory = LifeEventCategory::factory()->create(['vault_id' => $vault->id]); $lifeEventType = LifeEventType::factory()->create(['life_event_category_id' => $lifeEventCategory->id]); $lifeEvent = LifeEvent::factory()->create([ - 'vault_id' => $vault->id, + 'timeline_event_id' => $timelineEvent->id, 'life_event_type_id' => $lifeEventType->id, ]); - $this->executeService($user, $user->account, $vault, $contact, $lifeEventType, $lifeEvent, 'test'); + $this->executeService($user, $user->account, $vault, $contact, $lifeEventType, $lifeEvent, $timelineEvent); } /** @test */ @@ -59,15 +61,16 @@ class UpdateLifeEventTest extends TestCase $vault = $this->createVault($user->account); $vault = $this->setPermissionInVault($user, Vault::PERMISSION_EDIT, $vault); $contact = Contact::factory()->create(['vault_id' => $vault->id]); + $timelineEvent = TimelineEvent::factory()->create(['vault_id' => $vault->id]); $lifeEventCategory = LifeEventCategory::factory()->create(['vault_id' => $vault->id]); $lifeEventType = LifeEventType::factory()->create(['life_event_category_id' => $lifeEventCategory->id]); $lifeEvent = LifeEvent::factory()->create([ - 'vault_id' => $vault->id, + 'timeline_event_id' => $timelineEvent->id, 'life_event_type_id' => $lifeEventType->id, ]); - $this->executeService($user, $account, $vault, $contact, $lifeEventType, $lifeEvent, 'test'); + $this->executeService($user, $account, $vault, $contact, $lifeEventType, $lifeEvent, $timelineEvent); } /** @test */ @@ -80,15 +83,16 @@ class UpdateLifeEventTest extends TestCase $vault = $this->createVault($account); $vault = $this->setPermissionInVault($user, Vault::PERMISSION_EDIT, $vault); $contact = Contact::factory()->create(['vault_id' => $vault->id]); + $timelineEvent = TimelineEvent::factory()->create(['vault_id' => $vault->id]); $lifeEventCategory = LifeEventCategory::factory()->create(['vault_id' => $vault->id]); $lifeEventType = LifeEventType::factory()->create(['life_event_category_id' => $lifeEventCategory->id]); $lifeEvent = LifeEvent::factory()->create([ - 'vault_id' => $vault->id, + 'timeline_event_id' => $timelineEvent->id, 'life_event_type_id' => $lifeEventType->id, ]); - $this->executeService($user, $user->account, $vault, $contact, $lifeEventType, $lifeEvent, 'test'); + $this->executeService($user, $user->account, $vault, $contact, $lifeEventType, $lifeEvent, $timelineEvent); } /** @test */ @@ -100,15 +104,16 @@ class UpdateLifeEventTest extends TestCase $vault = $this->createVault($user->account); $vault = $this->setPermissionInVault($user, Vault::PERMISSION_VIEW, $vault); $contact = Contact::factory()->create(['vault_id' => $vault->id]); + $timelineEvent = TimelineEvent::factory()->create(['vault_id' => $vault->id]); $lifeEventCategory = LifeEventCategory::factory()->create(['vault_id' => $vault->id]); $lifeEventType = LifeEventType::factory()->create(['life_event_category_id' => $lifeEventCategory->id]); $lifeEvent = LifeEvent::factory()->create([ - 'vault_id' => $vault->id, + 'timeline_event_id' => $timelineEvent->id, 'life_event_type_id' => $lifeEventType->id, ]); - $this->executeService($user, $user->account, $vault, $contact, $lifeEventType, $lifeEvent, 'test'); + $this->executeService($user, $user->account, $vault, $contact, $lifeEventType, $lifeEvent, $timelineEvent); } /** @test */ @@ -120,15 +125,16 @@ class UpdateLifeEventTest extends TestCase $vault = $this->createVault($user->account); $vault = $this->setPermissionInVault($user, Vault::PERMISSION_EDIT, $vault); $contact = Contact::factory()->create(); + $timelineEvent = TimelineEvent::factory()->create(['vault_id' => $vault->id]); $lifeEventCategory = LifeEventCategory::factory()->create(['vault_id' => $vault->id]); $lifeEventType = LifeEventType::factory()->create(['life_event_category_id' => $lifeEventCategory->id]); $lifeEvent = LifeEvent::factory()->create([ - 'vault_id' => $vault->id, + 'timeline_event_id' => $timelineEvent->id, 'life_event_type_id' => $lifeEventType->id, ]); - $this->executeService($user, $user->account, $vault, $contact, $lifeEventType, $lifeEvent, 'test'); + $this->executeService($user, $user->account, $vault, $contact, $lifeEventType, $lifeEvent, $timelineEvent); } /** @test */ @@ -141,23 +147,46 @@ class UpdateLifeEventTest extends TestCase $vault = $this->setPermissionInVault($user, Vault::PERMISSION_EDIT, $vault); $contact = Contact::factory()->create(['vault_id' => $vault->id]); + $timelineEvent = TimelineEvent::factory()->create(['vault_id' => $vault->id]); $lifeEventCategory = LifeEventCategory::factory()->create(); $lifeEventType = LifeEventType::factory()->create(['life_event_category_id' => $lifeEventCategory->id]); $lifeEvent = LifeEvent::factory()->create([ - 'vault_id' => $vault->id, + 'timeline_event_id' => $timelineEvent->id, 'life_event_type_id' => $lifeEventType->id, ]); - $this->executeService($user, $user->account, $vault, $contact, $lifeEventType, $lifeEvent, 'test'); + $this->executeService($user, $user->account, $vault, $contact, $lifeEventType, $lifeEvent, $timelineEvent); } - private function executeService(User $author, Account $account, Vault $vault, Contact $contact, LifeEventType $lifeEventType, LifeEvent $lifeEvent, string $summary): void + /** @test */ + public function it_fails_if_timeline_event_doesnt_belong_to_account(): void + { + $this->expectException(ModelNotFoundException::class); + + $user = $this->createUser(); + $vault = $this->createVault($user->account); + $vault = $this->setPermissionInVault($user, Vault::PERMISSION_EDIT, $vault); + $contact = Contact::factory()->create(['vault_id' => $vault->id]); + + $timelineEvent = TimelineEvent::factory()->create(); + $lifeEventCategory = LifeEventCategory::factory()->create(['vault_id' => $vault->id]); + $lifeEventType = LifeEventType::factory()->create(['life_event_category_id' => $lifeEventCategory->id]); + $lifeEvent = LifeEvent::factory()->create([ + 'timeline_event_id' => $timelineEvent->id, + 'life_event_type_id' => $lifeEventType->id, + ]); + + $this->executeService($user, $user->account, $vault, $contact, $lifeEventType, $lifeEvent, $timelineEvent); + } + + private function executeService(User $author, Account $account, Vault $vault, Contact $contact, LifeEventType $lifeEventType, LifeEvent $lifeEvent, TimelineEvent $timelineEvent): void { $request = [ 'account_id' => $account->id, 'vault_id' => $vault->id, 'author_id' => $author->id, + 'timeline_event_id' => $timelineEvent->id, 'life_event_type_id' => $lifeEventType->id, 'life_event_id' => $lifeEvent->id, 'summary' => null, @@ -178,7 +207,7 @@ class UpdateLifeEventTest extends TestCase $this->assertDatabaseHas('life_events', [ 'id' => $lifeEvent->id, - 'vault_id' => $vault->id, + 'timeline_event_id' => $timelineEvent->id, 'life_event_type_id' => $lifeEventType->id, 'summary' => null, 'happened_at' => '1982-02-04 00:00:00', diff --git a/tests/Unit/Domains/Contact/ManageLifeEvents/Services/UpdateTimelineEventTest.php b/tests/Unit/Domains/Contact/ManageLifeEvents/Services/UpdateTimelineEventTest.php new file mode 100644 index 000000000..431697756 --- /dev/null +++ b/tests/Unit/Domains/Contact/ManageLifeEvents/Services/UpdateTimelineEventTest.php @@ -0,0 +1,129 @@ +createUser(); + $vault = $this->createVault($user->account); + $vault = $this->setPermissionInVault($user, Vault::PERMISSION_EDIT, $vault); + + $timelineEvent = TimelineEvent::factory()->create([ + 'vault_id' => $vault->id, + ]); + + $this->executeService($user, $user->account, $vault, $timelineEvent); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given(): void + { + $request = [ + 'summary' => 'Ross', + ]; + + $this->expectException(ValidationException::class); + (new UpdateTimelineEvent())->execute($request); + } + + /** @test */ + public function it_fails_if_user_doesnt_belong_to_account(): void + { + $this->expectException(ModelNotFoundException::class); + + $user = $this->createUser(); + $account = Account::factory()->create(); + $vault = $this->createVault($user->account); + $vault = $this->setPermissionInVault($user, Vault::PERMISSION_EDIT, $vault); + + $timelineEvent = TimelineEvent::factory()->create([ + 'vault_id' => $vault->id, + ]); + + $this->executeService($user, $account, $vault, $timelineEvent); + } + + /** @test */ + public function it_fails_if_vault_doesnt_belong_to_account(): void + { + $this->expectException(ModelNotFoundException::class); + + $user = $this->createUser(); + $account = Account::factory()->create(); + $vault = $this->createVault($account); + $vault = $this->setPermissionInVault($user, Vault::PERMISSION_EDIT, $vault); + + $timelineEvent = TimelineEvent::factory()->create([ + 'vault_id' => $vault->id, + ]); + + $this->executeService($user, $user->account, $vault, $timelineEvent); + } + + /** @test */ + public function it_fails_if_user_isnt_vault_editor(): void + { + $this->expectException(NotEnoughPermissionException::class); + + $user = $this->createUser(); + $vault = $this->createVault($user->account); + $vault = $this->setPermissionInVault($user, Vault::PERMISSION_VIEW, $vault); + + $timelineEvent = TimelineEvent::factory()->create([ + 'vault_id' => $vault->id, + ]); + + $this->executeService($user, $user->account, $vault, $timelineEvent); + } + + /** @test */ + public function it_fails_if_timeline_event_doesnt_belong_to_vault(): void + { + $this->expectException(ModelNotFoundException::class); + + $user = $this->createUser(); + $vault = $this->createVault($user->account); + $vault = $this->setPermissionInVault($user, Vault::PERMISSION_EDIT, $vault); + + $timelineEvent = TimelineEvent::factory()->create(); + + $this->executeService($user, $user->account, $vault, $timelineEvent); + } + + private function executeService(User $author, Account $account, Vault $vault, TimelineEvent $timelineEvent): void + { + $request = [ + 'account_id' => $account->id, + 'vault_id' => $vault->id, + 'author_id' => $author->id, + 'timeline_event_id' => $timelineEvent->id, + 'label' => null, + 'started_at' => '1982-02-04', + ]; + + $timelineEvent = (new UpdateTimelineEvent())->execute($request); + + $this->assertDatabaseHas('timeline_events', [ + 'id' => $timelineEvent->id, + 'vault_id' => $vault->id, + 'label' => null, + 'started_at' => '1982-02-04 00:00:00', + ]); + } +} diff --git a/tests/Unit/Domains/Contact/ManageLifeEvents/Web/ViewHelpers/ModuleLifeEventViewHelperTest.php b/tests/Unit/Domains/Contact/ManageLifeEvents/Web/ViewHelpers/ModuleLifeEventViewHelperTest.php new file mode 100644 index 000000000..a86d56943 --- /dev/null +++ b/tests/Unit/Domains/Contact/ManageLifeEvents/Web/ViewHelpers/ModuleLifeEventViewHelperTest.php @@ -0,0 +1,224 @@ +create(); + $user = User::factory()->create(); + + $array = ModuleLifeEventViewHelper::data($contact, $user); + + $this->assertEquals( + 5, + count($array) + ); + + $this->assertArrayHasKey('contact', $array); + $this->assertArrayHasKey('current_date', $array); + $this->assertArrayHasKey('current_date_human_format', $array); + $this->assertArrayHasKey('life_event_categories', $array); + $this->assertArrayHasKey('url', $array); + + $this->assertEquals( + '2018-01-01', + $array['current_date'] + ); + $this->assertEquals( + 'Jan 01, 2018', + $array['current_date_human_format'] + ); + + $this->assertEquals( + [ + 'load' => env('APP_URL').'/vaults/'.$contact->vault->id.'/contacts/'.$contact->id.'/timelineEvents', + 'store' => env('APP_URL').'/vaults/'.$contact->vault->id.'/contacts/'.$contact->id.'/timelineEvents', + ], + $array['url'] + ); + } + + /** @test */ + public function it_gets_the_data_transfer_object_for_categories(): void + { + $contact = Contact::factory()->create(); + $user = User::factory()->create(); + $lifeEventCategory = LifeEventCategory::factory()->create([ + 'vault_id' => $contact->vault_id, + 'label' => 'name', + ]); + + $array = ModuleLifeEventViewHelper::dtoLifeEventCategory($lifeEventCategory); + + $this->assertEquals( + 3, + count($array) + ); + $this->assertEquals( + $lifeEventCategory->id, + $array['id'] + ); + $this->assertEquals( + 'name', + $array['label'] + ); + $this->assertEquals( + [], + $array['life_event_types']->toArray() + ); + } + + /** @test */ + public function it_gets_the_data_transfer_object_for_types(): void + { + $contact = Contact::factory()->create(); + $lifeEventCategory = LifeEventCategory::factory()->create([ + 'vault_id' => $contact->vault_id, + 'label' => 'name', + ]); + $lifeEventType = LifeEventType::factory()->create([ + 'life_event_category_id' => $lifeEventCategory->id, + 'label' => 'name', + ]); + + $array = ModuleLifeEventViewHelper::dtoLifeEventType($lifeEventType); + + $this->assertEquals([ + 'id' => $lifeEventType->id, + 'label' => $lifeEventType->label, + ], + $array + ); + } + + /** @test */ + public function it_gets_the_data_transfer_object_for_the_timeline_objects(): void + { + $contact = Contact::factory()->create(); + $user = User::factory()->create(); + $lifeEvents = $contact + ->lifeEvents() + ->get(); + + $array = ModuleLifeEventViewHelper::timelineEvents($lifeEvents, $user, $contact); + + $this->assertEquals( + 1, + count($array) + ); + $this->assertArrayHasKey('timeline_events', $array); + } + + /** @test */ + public function it_gets_the_data_transfer_object_for_the_timeline_event(): void + { + $contact = Contact::factory()->create(); + $user = User::factory()->create(); + $timelineEvent = TimelineEvent::factory()->create([ + 'vault_id' => $contact->vault_id, + 'label' => 'test', + ]); + + $array = ModuleLifeEventViewHelper::dtoTimelineEvent($timelineEvent, $user, $contact); + + $this->assertEquals( + 6, + count($array) + ); + $this->assertArrayHasKey('id', $array); + $this->assertArrayHasKey('label', $array); + $this->assertArrayHasKey('collapsed', $array); + $this->assertArrayHasKey('happened_at', $array); + $this->assertArrayHasKey('life_events', $array); + $this->assertArrayHasKey('url', $array); + + $this->assertEquals( + $timelineEvent->id, + $array['id'] + ); + $this->assertEquals( + 'test', + $array['label'] + ); + $this->assertEquals( + [ + 'store' => env('APP_URL').'/vaults/'.$contact->vault->id.'/contacts/'.$contact->id.'/timelineEvents/'.$timelineEvent->id, + 'toggle' => env('APP_URL').'/vaults/'.$contact->vault->id.'/contacts/'.$contact->id.'/timelineEvents/'.$timelineEvent->id.'/toggle', + 'destroy' => env('APP_URL').'/vaults/'.$contact->vault->id.'/contacts/'.$contact->id.'/timelineEvents/'.$timelineEvent->id, + ], + $array['url'] + ); + } + + /** @test */ + public function it_gets_the_data_transfer_object_for_the_life_event(): void + { + $contact = Contact::factory()->create(); + $user = User::factory()->create(); + $timelineEvent = TimelineEvent::factory()->create([ + 'vault_id' => $contact->vault_id, + 'label' => 'test', + ]); + $lifeEventCategory = LifeEventCategory::factory()->create([ + 'label' => 'test', + ]); + $lifeEventType = LifeEventType::factory()->create([ + 'life_event_category_id' => $lifeEventCategory->id, + 'label' => 'test', + ]); + $lifeEvent = LifeEvent::factory()->create([ + 'life_event_type_id' => $lifeEventType->id, + 'timeline_event_id' => $timelineEvent->id, + 'happened_at' => '2018-01-01', + ]); + + $array = ModuleLifeEventViewHelper::dtoLifeEvent($lifeEvent, $user, $contact); + + $this->assertEquals( + 18, + count($array) + ); + + $this->assertArrayHasKey('id', $array); + $this->assertArrayHasKey('emotion_id', $array); + $this->assertArrayHasKey('collapsed', $array); + $this->assertArrayHasKey('summary', $array); + $this->assertArrayHasKey('description', $array); + $this->assertArrayHasKey('happened_at', $array); + $this->assertArrayHasKey('costs', $array); + $this->assertArrayHasKey('currency_id', $array); + $this->assertArrayHasKey('paid_by_contact_id', $array); + $this->assertArrayHasKey('duration_in_minutes', $array); + $this->assertArrayHasKey('distance_in_km', $array); + $this->assertArrayHasKey('from_place', $array); + $this->assertArrayHasKey('to_place', $array); + $this->assertArrayHasKey('place', $array); + $this->assertArrayHasKey('participants', $array); + $this->assertArrayHasKey('timeline_event', $array); + $this->assertArrayHasKey('life_event_type', $array); + $this->assertArrayHasKey('url', $array); + + $this->assertEquals( + $lifeEvent->id, + $array['id'] + ); + } +} diff --git a/tests/Unit/Domains/Settings/CreateAccount/Services/SetupAccountTest.php b/tests/Unit/Domains/Settings/CreateAccount/Services/SetupAccountTest.php index c9c0b57ca..587719e54 100644 --- a/tests/Unit/Domains/Settings/CreateAccount/Services/SetupAccountTest.php +++ b/tests/Unit/Domains/Settings/CreateAccount/Services/SetupAccountTest.php @@ -113,6 +113,10 @@ class SetupAccountTest extends TestCase 'account_id' => $user->account_id, 'name' => trans('app.module_goals'), ]); + $this->assertDatabaseHas('modules', [ + 'account_id' => $user->account_id, + 'name' => trans('app.module_life_events'), + ]); $this->assertDatabaseHas('modules', [ 'account_id' => $user->account_id, 'name' => trans('app.module_family_summary'), diff --git a/tests/Unit/Domains/Vault/ManageVault/Services/UpdateVaultDashboardDefaultTabTest.php b/tests/Unit/Domains/Vault/ManageVault/Services/UpdateVaultDashboardDefaultTabTest.php new file mode 100644 index 000000000..4dba497ec --- /dev/null +++ b/tests/Unit/Domains/Vault/ManageVault/Services/UpdateVaultDashboardDefaultTabTest.php @@ -0,0 +1,78 @@ +createUser(); + $vault = $this->createVault($ross->account); + $vault = $this->setPermissionInVault($ross, Vault::PERMISSION_MANAGE, $vault); + $this->executeService($ross, $ross->account, $vault); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given(): void + { + $request = [ + 'title' => 'Ross', + ]; + + $this->expectException(ValidationException::class); + (new UpdateVaultDashboardDefaultTab())->execute($request); + } + + /** @test */ + public function it_fails_if_user_doesnt_belong_to_account(): void + { + $this->expectException(ModelNotFoundException::class); + + $ross = $this->createUser(); + $account = Account::factory()->create(); + $vault = Vault::factory()->create([ + 'account_id' => $ross->account_id, + ]); + $this->executeService($ross, $account, $vault); + } + + /** @test */ + public function it_fails_if_vault_doesnt_belong_to_account(): void + { + $this->expectException(ModelNotFoundException::class); + + $ross = $this->createUser(); + $vault = Vault::factory()->create(); + $this->executeService($ross, $ross->account, $vault); + } + + private function executeService(User $author, Account $account, Vault $vault): void + { + $request = [ + 'account_id' => $account->id, + 'author_id' => $author->id, + 'vault_id' => $vault->id, + 'show_activity_tab_on_dashboard' => true, + ]; + + $vault = (new UpdateVaultDashboardDefaultTab())->execute($request); + + $this->assertDatabaseHas('vaults', [ + 'id' => $vault->id, + 'account_id' => $account->id, + 'show_activity_tab_on_dashboard' => true, + ]); + } +} diff --git a/tests/Unit/Models/ContactTest.php b/tests/Unit/Models/ContactTest.php index c6e5cfd6c..aaf02b1fa 100644 --- a/tests/Unit/Models/ContactTest.php +++ b/tests/Unit/Models/ContactTest.php @@ -26,6 +26,7 @@ use App\Models\Pronoun; use App\Models\RelationshipType; use App\Models\Religion; use App\Models\Template; +use App\Models\TimelineEvent; use App\Models\User; use Carbon\Carbon; use Illuminate\Foundation\Testing\DatabaseTransactions; @@ -283,6 +284,17 @@ class ContactTest extends TestCase $this->assertTrue($contact->lifeEvents()->exists()); } + /** @test */ + public function it_has_many_timeline_events(): void + { + $contact = Contact::factory()->create(); + $timelineEvent = TimelineEvent::factory()->create(); + + $contact->timelineEvents()->sync([$timelineEvent->id]); + + $this->assertTrue($contact->timelineEvents()->exists()); + } + /** @test */ public function it_has_many_mood_tracking_events(): void { diff --git a/tests/Unit/Models/LifeEventTest.php b/tests/Unit/Models/LifeEventTest.php index febd1a548..eba18d9a0 100644 --- a/tests/Unit/Models/LifeEventTest.php +++ b/tests/Unit/Models/LifeEventTest.php @@ -4,6 +4,7 @@ namespace Tests\Unit\Models; use App\Models\Contact; use App\Models\Currency; +use App\Models\Emotion; use App\Models\LifeEvent; use Illuminate\Foundation\Testing\DatabaseTransactions; use Tests\TestCase; @@ -13,11 +14,11 @@ class LifeEventTest extends TestCase use DatabaseTransactions; /** @test */ - public function it_has_one_vault() + public function it_has_one_timeline_event() { $lifeEvent = LifeEvent::factory()->create(); - $this->assertTrue($lifeEvent->vault()->exists()); + $this->assertTrue($lifeEvent->timelineEvent()->exists()); } /** @test */ @@ -39,6 +40,17 @@ class LifeEventTest extends TestCase $this->assertTrue($lifeEvent->currency()->exists()); } + /** @test */ + public function it_has_one_emotion() + { + $emotion = Emotion::factory()->create(); + $lifeEvent = LifeEvent::factory()->create([ + 'emotion_id' => $emotion->id, + ]); + + $this->assertTrue($lifeEvent->emotion()->exists()); + } + /** @test */ public function it_has_one_contact_who_pays_for_the_life_event() { diff --git a/tests/Unit/Models/TimelineEventTest.php b/tests/Unit/Models/TimelineEventTest.php new file mode 100644 index 000000000..c3dc031a1 --- /dev/null +++ b/tests/Unit/Models/TimelineEventTest.php @@ -0,0 +1,82 @@ +create(); + + $this->assertTrue($timeline->vault()->exists()); + } + + /** @test */ + public function it_has_many_life_events(): void + { + $timeline = TimelineEvent::factory()->create(); + LifeEvent::factory()->count(2)->create([ + 'timeline_event_id' => $timeline->id, + ]); + + $this->assertTrue($timeline->lifeEvents()->exists()); + } + + /** @test */ + public function it_has_many_participants(): void + { + $contact = Contact::factory()->create(); + $timeline = TimelineEvent::factory()->create(); + + $timeline->participants()->sync([$contact->id]); + + $this->assertTrue($timeline->participants()->exists()); + } + + /** @test */ + public function it_gets_the_date_range(): void + { + $user = User::factory()->create(); + $this->be($user); + $timeline = TimelineEvent::factory()->create(); + LifeEvent::factory()->create([ + 'timeline_event_id' => $timeline->id, + 'happened_at' => '2020-01-01', + ]); + + $this->assertEquals( + 'Jan 01, 2020', + $timeline->range + ); + + LifeEvent::factory()->create([ + 'timeline_event_id' => $timeline->id, + 'happened_at' => '2019-01-01', + ]); + + $this->assertEquals( + 'Jan 01, 2019 — Jan 01, 2020', + $timeline->range + ); + + LifeEvent::factory()->create([ + 'timeline_event_id' => $timeline->id, + 'happened_at' => '2019-03-01', + ]); + + $this->assertEquals( + 'Jan 01, 2019 — Jan 01, 2020', + $timeline->range + ); + } +} diff --git a/tests/Unit/Models/VaultTest.php b/tests/Unit/Models/VaultTest.php index 6fb92507b..dc3efb702 100644 --- a/tests/Unit/Models/VaultTest.php +++ b/tests/Unit/Models/VaultTest.php @@ -10,11 +10,11 @@ use App\Models\File; use App\Models\Group; use App\Models\Journal; use App\Models\Label; -use App\Models\LifeEvent; use App\Models\LifeEventCategory; use App\Models\MoodTrackingParameter; use App\Models\Tag; use App\Models\Template; +use App\Models\TimelineEvent; use App\Models\User; use App\Models\Vault; use Illuminate\Foundation\Testing\DatabaseTransactions; @@ -169,14 +169,14 @@ class VaultTest extends TestCase } /** @test */ - public function it_has_many_life_events(): void + public function it_has_many_timeline_events(): void { $vault = Vault::factory()->create(); - LifeEvent::factory()->count(2)->create([ + TimelineEvent::factory()->count(2)->create([ 'vault_id' => $vault->id, ]); - $this->assertTrue($vault->lifeEvents()->exists()); + $this->assertTrue($vault->timelineEvents()->exists()); } /** @test */