From e1712c8fcd417256b26482fc08449c78f8b7481c Mon Sep 17 00:00:00 2001 From: Michal Feher Date: Mon, 17 Mar 2025 09:54:13 +0100 Subject: [PATCH] office365 integration --- .../Commands/FinishOffice365Subtasks.php | 53 +++++ be/app/Console/Commands/SyncOffice365.php | 102 +++++++++ be/app/Console/Kernel.php | 2 + be/app/Http/Resources/MasterTaskResource.php | 32 +-- .../Services/Office365/Office365Service.php | 214 ++++++++++++++++++ ...225_add_office365_id_to_blockers_table.php | 31 +++ ...34411_add_change_key_to_blockers_table.php | 31 +++ ..._144921_add_office365flags_to_subtasks.php | 35 +++ ..._add_estimated_duration_to_master_task.php | 31 +++ ...name_estimated_duration_in_master_task.php | 31 +++ be/routes/web.php | 5 +- 11 files changed, 551 insertions(+), 16 deletions(-) create mode 100644 be/app/Console/Commands/FinishOffice365Subtasks.php create mode 100644 be/app/Console/Commands/SyncOffice365.php create mode 100644 be/app/Services/Office365/Office365Service.php create mode 100644 be/database/migrations/2025_02_24_123225_add_office365_id_to_blockers_table.php create mode 100644 be/database/migrations/2025_02_24_134411_add_change_key_to_blockers_table.php create mode 100644 be/database/migrations/2025_03_07_144921_add_office365flags_to_subtasks.php create mode 100644 be/database/migrations/2025_03_14_120820_add_estimated_duration_to_master_task.php create mode 100644 be/database/migrations/2025_03_14_121819_rename_estimated_duration_in_master_task.php diff --git a/be/app/Console/Commands/FinishOffice365Subtasks.php b/be/app/Console/Commands/FinishOffice365Subtasks.php new file mode 100644 index 0000000..ed53df5 --- /dev/null +++ b/be/app/Console/Commands/FinishOffice365Subtasks.php @@ -0,0 +1,53 @@ +get()->filter(function ($subtask) use ($events) { + $event = collect($events)->firstWhere('id', $subtask->office365_id); + if ( ! $event) { + return false; + } + + return strtotime($event['end']['dateTime']) < time(); + }); + + // ak neexistuju ziadne subtasky, ktore uz skoncili + if ($subtasksThatFinished->isEmpty()) { + return Command::SUCCESS; + } + + Office365Service::finishSubtasks($subtasksThatFinished); + + return Command::SUCCESS; + } +} diff --git a/be/app/Console/Commands/SyncOffice365.php b/be/app/Console/Commands/SyncOffice365.php new file mode 100644 index 0000000..f48e1f4 --- /dev/null +++ b/be/app/Console/Commands/SyncOffice365.php @@ -0,0 +1,102 @@ +exists(); + }); + + $subtasks = array_filter($events, function ($event) { + $projectCode = explode(' ', $event['subject'])[0]; + + // if project code exist in database, then it is subtask + return Project::where('code', $projectCode)->exists(); + }); + + // get only new events - the ones that doesnt have any office365_id in blockers table and only events that doesnt have isCancelled => true + + $newBlockers = array_filter($blockers_from_office365, function ($event) { + return ! Blocker::where('office365_id', $event['id'])->exists() && ! $event['isCancelled']; + }); + + $newSubtasks = array_filter($subtasks, function ($event) { + return ! SubTask::where('office365_id', $event['id'])->exists() && ! $event['isCancelled']; + }); + + + Office365Service::addBlockers($newBlockers); + Office365Service::addSubtasks($newSubtasks); + + // get edited blockers - the ones, that has different changeKey from our database + $editedBlockers = array_filter($blockers_from_office365, function ($event) { + $blocker = Blocker::where('office365_id', $event['id'])->first(); + + return $blocker && $blocker->office365_change_key !== $event['changeKey']; + }); + + Office365Service::editBlockers($editedBlockers); + + // get all canceled blockers - the ones, that are in our database and in the response they have isCancelled => true + $removedBlockers = Blocker::whereNotNull('office365_id')->get()->filter(function ($blocker) use ( + $blockers_from_office365 + ) { + $event = collect($blockers_from_office365)->firstWhere('id', $blocker->office365_id); + + return $event && $event['isCancelled']; + }); + + // TODO dat do service + $removedBlockers->each(function ($blocker) { + $blocker->delete(); + }); + + dd('he'); + } +} diff --git a/be/app/Console/Kernel.php b/be/app/Console/Kernel.php index d5dab50..772a64d 100644 --- a/be/app/Console/Kernel.php +++ b/be/app/Console/Kernel.php @@ -31,6 +31,8 @@ class Kernel extends ConsoleKernel $schedule->command('karat:sync-projects')->everyFiveMinutes(); $schedule->command('karat:sync-projects-name')->hourly(); $schedule->command('karat:sync-timesheets')->everyFiveMinutes(); + $schedule->command('office365:sync')->everyFiveMinutes(); + $schedule->command('office365:finish-subtasks')->hourly(); } } diff --git a/be/app/Http/Resources/MasterTaskResource.php b/be/app/Http/Resources/MasterTaskResource.php index ff7d31c..e8abb0d 100644 --- a/be/app/Http/Resources/MasterTaskResource.php +++ b/be/app/Http/Resources/MasterTaskResource.php @@ -10,26 +10,28 @@ class MasterTaskResource extends JsonResource * Transform the resource into an array. * * @param \Illuminate\Http\Request $request + * * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable */ public function toArray($request) { return [ - 'id' => $this->id, - 'name' => $this->title, - 'created_by' => $this->created_by, - 'description' => $this->description, - 'duration' => $this->estimated_time, - 'time_worked' => $this->tracked_time, - 'is_finished' => $this->is_finished, - 'starts_at' => $this->created_at, - 'deadline' => $this->deadline, - 'project_code' => ($this->project) ? $this->project->code . ' ' . $this->project->name : null, - 'team' => ($this->team) ?: null, - 'employees' => UserResource::collection($this->usersOutsideTeam), - 'subtasks' => SubTaskResource::collection($this->subtasks), - 'files' => AttachmentResource::collection($this->attachments), - 'color' => $this->color, + 'id' => $this->id, + 'name' => $this->title, + 'created_by' => $this->created_by, + 'description' => $this->description, + 'duration' => $this->estimated_time, + 'estimated_duration' => $this->pricequote_time, + 'time_worked' => $this->tracked_time, + 'is_finished' => $this->is_finished, + 'starts_at' => $this->created_at, + 'deadline' => $this->deadline, + 'project_code' => ($this->project) ? $this->project->code . ' ' . $this->project->name : null, + 'team' => ($this->team) ?: null, + 'employees' => UserResource::collection($this->usersOutsideTeam), + 'subtasks' => SubTaskResource::collection($this->subtasks), + 'files' => AttachmentResource::collection($this->attachments), + 'color' => $this->color, ]; } } diff --git a/be/app/Services/Office365/Office365Service.php b/be/app/Services/Office365/Office365Service.php new file mode 100644 index 0000000..7062131 --- /dev/null +++ b/be/app/Services/Office365/Office365Service.php @@ -0,0 +1,214 @@ +get("https://graph.microsoft.com/v1.0/groups/$groupId/events"); + + return $response->json()['value']; + } + + public static function getAccessToken() + { + $tenantId = env('MICROSOFT_TENANT_ID'); + $clientId = env('MICROSOFT_CLIENT_ID'); + $clientSecret = env('MICROSOFT_CLIENT_SECRET'); + $username = env('MICROSOFT_USERNAME'); + $password = env('MICROSOFT_PASSWORD'); + + $response = Http::asForm()->post("https://login.microsoftonline.com/{$tenantId}/oauth2/v2.0/token", [ + 'grant_type' => 'password', + 'client_id' => $clientId, + 'client_secret' => $clientSecret, + 'username' => $username, + 'password' => $password, + 'scope' => 'https://graph.microsoft.com/.default', + ]); + + if ($response->failed()) { + dd($response->body()); + throw new \Exception('Failed to obtain access token: ' . $response->body()); + } + + return $response->json()['access_token']; + } + + public static function addBlockers(mixed $blockers) + { + foreach ($blockers as $blocker) { + self::addBlocker($blocker); + } + } + + private static function addBlocker(mixed $blocker) + { + $attendees = $blocker['attendees']; + + // pre kazdeho cloveka z eventu, ktory je zaroven userom v TM vytvorime blocker + foreach ($attendees as $attendee) { + $user = User::where('email', $attendee['emailAddress']['address'])->first(); + + if ( ! $user) { + continue; + } + + $blockerToInsert = []; + + $blockerToInsert['office365_id'] = $blocker['id']; + $blockerToInsert['office365_change_key'] = $blocker['changeKey']; + $blockerToInsert['user_id'] = $user->id; + $blockerToInsert['name'] = $blocker['subject']; + $blockerToInsert['type'] = "office365_blocker"; + $blockerToInsert['start'] = $blocker['start']['dateTime']; + $blockerToInsert['end'] = $blocker['end']['dateTime']; + $blockerToInsert['approved'] = 1; + $blockerToInsert['created_at'] = Carbon::now()->toDateTimeString(); + $blockerToInsert['updated_at'] = Carbon::now()->toDateTimeString(); + + Blocker::insert($blockerToInsert); + } + } + + public static function editBlockers(mixed $editedBlockers) + { + foreach ($editedBlockers as $editedBlocker) { + self::editBlocker($editedBlocker); + } + } + + private static function editBlocker(mixed $editedBlocker) + { + $attendees = $editedBlocker['attendees']; + + // pre kazdeho cloveka z eventu, ktory je zaroven userom v TM updatneme blocker + foreach ($attendees as $attendee) { + $user = User::where('email', $attendee['emailAddress']['address'])->first(); + + if ( ! $user) { + continue; + } + + $blocker = Blocker::where('office365_id', $editedBlocker['id'])->where('user_id', $user->id)->first(); + + $blocker->update([ + 'office365_change_key' => $editedBlocker['changeKey'], + 'name' => $editedBlocker['subject'], + 'start' => $editedBlocker['start']['dateTime'], + 'end' => $editedBlocker['end']['dateTime'], + 'updated_at' => Carbon::now()->toDateTimeString(), + ]); + } + } + + public static function addSubtasks(mixed $newSubtasks) + { + foreach ($newSubtasks as $newSubtask) { + $project = Project::where('code', explode(' ', $newSubtask['subject'])[0])->first(); + + // ak existuje viac master taskov, pouzijeme posledny + $mastertask = MasterTask::where('project_id', $project->id)->latest()->first(); + // ak neexistuje master task k danemu projektu, tak sa subtask neprida + if ( ! $mastertask) { + continue; + } + + self::addSubtask($newSubtask, $mastertask); + } + } + + private static function addSubtask(mixed $newSubtask, mixed $mastertask) + { + $attendees = $newSubtask['attendees']; + + $subtasksToInsert = []; + $i = 0; + + + // pre kazdeho cloveka z eventu, ktory je zaroven userom v TM vytvorime subtask + foreach ($attendees as $attendee) { + $user = User::where('email', $attendee['emailAddress']['address'])->first(); + $creator = User::where('email', $newSubtask['organizer']['emailAddress']['address'])->first(); + + if ( ! $user || ! $creator) { + continue; + } + + $subtasksToInsert[$i]['user_id'] = $user->id; + // vymazem prve slovo z title, ktore je project code + $subtasksToInsert[$i]['title'] = substr($newSubtask['subject'], + strpos($newSubtask['subject'], ' ') + 1); + + + // estimated_time je rozdiel medzi end.dateTime a start.dateTime zakhruhleny na najblizsiu 0.5 hodinu + $subtasksToInsert[$i]['estimated_time'] = round((strtotime($newSubtask['end']['dateTime']) - strtotime($newSubtask['start']['dateTime'])) / 3600, + 1); + $subtasksToInsert[$i]['tracked_time'] = 0; + $subtasksToInsert[$i]['created_by'] = $creator->id; + $subtasksToInsert[$i]['office365_id'] = $newSubtask['id']; + $subtasksToInsert[$i]['office365_change_key'] = $newSubtask['changeKey']; + $subtasksToInsert[$i]['type'] = 'office365'; + $subtasksToInsert[$i]['description'] = $newSubtask['body']['content']; + $subtasksToInsert[$i]['deadline'] = $newSubtask['end']['dateTime']; + $subtasksToInsert[$i]['office365_location'] = $newSubtask['location']['displayName']; + $subtasksToInsert[$i]['is_finished'] = 0; + } + + $subtasks = $mastertask->subtasks()->createMany($subtasksToInsert); + + // vytvorenie timeblocks + foreach ($subtasks as $subtask) { + $subtask->syncTimeblocks([ + [ + 'sub_task_id' => $subtask->id, + 'start' => $newSubtask['start']['dateTime'], + 'end' => $newSubtask['end']['dateTime'], + 'is_finished' => 0, + ] + ]); + } + + // manualne musim trigernut tuto operaciu, lebo cez Observer sa to nespusti - ten je nastaveny len na update subtasku + $mastertask->recalculateEstimatedTime(); + } + + public static function finishSubtasks(mixed $subtasks) + { + foreach ($subtasks as $subtask) { + // ukoncenie timeblocks + foreach ($subtask->timeblocks as $timeblock) { + $timeblock->update([ + 'is_finished' => 1, + ]); + } + + // pridanie odtrackovaneho casu + $subtask->syncTimesheets([ + [ + 'sub_task_id' => $subtask->id, + 'employee_id' => $subtask->user_id, + 'time' => $subtask->estimated_time, + ], + ]); + + $subtask->update([ + 'is_finished' => 1, + ]); + } + + // manualne musim trignernut tuto operaciu, lebo cez Observer sa to nespusti - ten je nastaveny len na update subtasku + $subtasks->first()->mastertask->recalculateTrackedTime(); + } +} diff --git a/be/database/migrations/2025_02_24_123225_add_office365_id_to_blockers_table.php b/be/database/migrations/2025_02_24_123225_add_office365_id_to_blockers_table.php new file mode 100644 index 0000000..45cbaff --- /dev/null +++ b/be/database/migrations/2025_02_24_123225_add_office365_id_to_blockers_table.php @@ -0,0 +1,31 @@ +string('office365_id')->index()->nullable()->default(null)->after('intranet_id'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('blockers', function (Blueprint $table) { + // + }); + } +}; diff --git a/be/database/migrations/2025_02_24_134411_add_change_key_to_blockers_table.php b/be/database/migrations/2025_02_24_134411_add_change_key_to_blockers_table.php new file mode 100644 index 0000000..0c9dec3 --- /dev/null +++ b/be/database/migrations/2025_02_24_134411_add_change_key_to_blockers_table.php @@ -0,0 +1,31 @@ +string('office365_change_key')->index()->nullable()->default(null)->after('office365_id'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('blockers', function (Blueprint $table) { + // + }); + } +}; diff --git a/be/database/migrations/2025_03_07_144921_add_office365flags_to_subtasks.php b/be/database/migrations/2025_03_07_144921_add_office365flags_to_subtasks.php new file mode 100644 index 0000000..d7c2ece --- /dev/null +++ b/be/database/migrations/2025_03_07_144921_add_office365flags_to_subtasks.php @@ -0,0 +1,35 @@ +string('office365_id')->index()->nullable()->default(null)->after('id'); + }); + + Schema::table('sub_tasks', function (Blueprint $table) { + $table->string('office365_change_key')->index()->nullable()->default(null)->after('office365_id'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('subtasks', function (Blueprint $table) { + // + }); + } +}; diff --git a/be/database/migrations/2025_03_14_120820_add_estimated_duration_to_master_task.php b/be/database/migrations/2025_03_14_120820_add_estimated_duration_to_master_task.php new file mode 100644 index 0000000..3874d6a --- /dev/null +++ b/be/database/migrations/2025_03_14_120820_add_estimated_duration_to_master_task.php @@ -0,0 +1,31 @@ +float('estimated_duration')->after('description')->comment('tento field je fixny a neupdatuje sa na zaklade subtaskov'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('master_task', function (Blueprint $table) { + // + }); + } +}; diff --git a/be/database/migrations/2025_03_14_121819_rename_estimated_duration_in_master_task.php b/be/database/migrations/2025_03_14_121819_rename_estimated_duration_in_master_task.php new file mode 100644 index 0000000..5bf1efb --- /dev/null +++ b/be/database/migrations/2025_03_14_121819_rename_estimated_duration_in_master_task.php @@ -0,0 +1,31 @@ +renameColumn('estimated_duration', 'pricequote_time'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('master_task', function (Blueprint $table) { + // + }); + } +}; diff --git a/be/routes/web.php b/be/routes/web.php index b2074ee..79271eb 100644 --- a/be/routes/web.php +++ b/be/routes/web.php @@ -14,7 +14,10 @@ use Illuminate\Support\Facades\Route; */ Route::get('/', function () { - return view('welcome'); +}); + +Route::get('/run-office365', function () { + \Artisan::call('office365:sync'); }); Route::prefix('be')->group(function () {