query(); // unscheduled tasks if (isset($parameters['type']) && $parameters['type'] == 'unscheduled') { return $this->indexUnscheduled($request); } // ongoing tasks if (isset($parameters['type']) && $parameters['type'] == 'ongoing') { return $this->indexOngoing($request); } // draft tasks (koncepty) if (isset($parameters['type']) && $parameters['type'] == 'draft') { return $this->indexDrafts($request); } $parameters['user_id'] = ($parameters['employee_id'] ?? null); $validator = Validator::make($request->all(), [ 'start' => 'required', 'end' => 'required', ]); $validator->after(function ($validator) use ($parameters) { if (! isset($parameters['employee_id']) && ! isset($parameters['team_id'])) { $validator->errors()->add( 'error', 'You must provide either employee_id or team_id.' ); } }); if ($validator->fails()) { return response()->json($validator->errors(), 400); } $start = Carbon::parse($parameters['start']); $end = Carbon::parse($parameters['end'])->addDay(); if (isset($parameters['team_id'])) { // ak je zadany team, tak najdem vsetkych userov, ktori su v tomto teame. potom najdem vsetky subtasky, ktore su priradene ludom v tomto teame. nevyhladavam na zaklade team_id v master_taskoch, pretoze toto je nepresne. Jandl chce vidiet tasky aj pre jednotlivcov v danom teame aj ked ten task nebol assignuty pod tymto teamom $userIdArray = User::whereHas('teams', function ($query) use ($parameters) { $query->where('team_id', $parameters['team_id']); })->get()->pluck('id')->toArray(); $subtasks = SubTask::published() ->whereIn('user_id', $userIdArray) ->whereHas('timeblocks', function ($query) use ($start, $end) { $query->where('start', '>=', $start); $query->where('end', '<', $end); })->get(); } elseif (isset($parameters['user_id'])) { $subtasks = SubTask::published() ->where('user_id', $parameters['user_id'])->whereHas('timeblocks', function ($query) use ($start, $end) { $query->where('start', '>', $start); $query->where('end', '<', $end); })->get(); $userIdArray = [$parameters['user_id']]; } $blockers1 = Blocker::whereIn('user_id', $userIdArray)->where('start', '>', $start)->get(); // vacation can last more than 7 days what is the date range we use in filter. we need to check also for vacations that started before 7 days and end after 7 days $blockers2 = Blocker::whereIn('user_id', $userIdArray)->where('start', '<=', $start)->where('end', '>', $start)->get(); $blockers = $blockers1->merge($blockers2); return response()->json([ 'subtasks' => SubTaskResource::collection($subtasks), 'blockers' => BlockerResource::collection($blockers), ], 200); } private function indexUnscheduled(Request $request) { // get query parameter from request $parameters = $request->query(); $validator = Validator::make($parameters, [ 'employee_id' => 'required', ]); if ($validator->fails()) { return response()->json($validator->errors(), 400); } $parameters['user_id'] = $parameters['employee_id']; $subtasks = SubTask::published() ->where('user_id', $parameters['user_id']) ->where('is_finished', 0) ->whereDoesntHave('timeblocks') ->get(); return response()->json([ 'subtasks' => SubTaskResource::collection($subtasks), ], 200); } private function indexOngoing(Request $request) { // get query parameter from request $parameters = $request->query(); $validator = Validator::make($parameters, [ 'employee_id' => 'required', ]); if ($validator->fails()) { return response()->json($validator->errors(), 400); } $parameters['user_id'] = $parameters['employee_id']; $subtasks = SubTask::published() ->where('user_id', $parameters['user_id']) ->where('is_finished', 0) ->get(); return response()->json([ 'subtasks' => SubTaskResource::collection($subtasks), ], 200); } /** * Zoznam všetkých konceptov (draftov) */ private function indexDrafts(Request $request) { $parameters = $request->query(); $query = SubTask::draft() ->with(['mastertask', 'user', 'timeblocks', 'attachments']) ->orderBy('created_at', 'desc'); // ak je zadané employee_id, filtrovať podľa neho // draft sa zobrazuje: 1) creatorovi (user_id) ALEBO 2) zamestnancom v draft_data if (isset($parameters['employee_id'])) { $employeeId = (int) $parameters['employee_id']; $query->where(function ($q) use ($employeeId) { // Creator vidí svoje drafty $q->where('user_id', $employeeId) // Alebo zamestnanec je v draft_data array (hľadáme objekt s daným id) ->orWhereRaw("JSON_CONTAINS(draft_data, CAST(? AS JSON), '\$')", [json_encode(['id' => $employeeId])]); }); } return response()->json([ 'subtasks' => SubTaskResource::collection($query->get()), ], 200); } public function store($mastertask_id, Request $request) { // zistenie, či ide o draft $isDraft = isset($request->all()['data']['is_draft']) && $request->all()['data']['is_draft'] == true; $validator = Validator::make($request->all(), [ 'data.employees' => 'required', ]); // validacia, ci uz subtask pre tohto usera a mastertask neexistuje // $validator->after(function ($validator) use ($mastertask_id, $request) { // if (SubTask::where('master_task_id', $mastertask_id)->where('user_id', $request->all()['data']['employee_id'])->exists()) { // $validator->errors()->add( // 'data.employee_id', 'Sub task already exists for this employee and master task.' // ); // } // }); if ($validator->fails()) { return response()->json($validator->errors(), 400); } // validacia na max 50MB size pre attachmenty. neslo dat do klasickeho laravel validation, tak je to separe if ($files = $request->file('data')) { foreach ($files['files'] as $file) { if ($file->getSize() > 52428800) { return response()->json([ 'files' => 'File size must be less than 50MB', ], 400); } } } $mastertask = MasterTask::find($mastertask_id); $creatorId = User::getUserIdFromBearerToken($request->bearerToken()); // Pre drafty vytvoríme iba jeden záznam pod tvorcom s uloženými dátami všetkých zamestnancov if ($isDraft) { // Pripravíme dáta pre draft $draftData = [ 'user_id' => $creatorId, 'created_by' => $creatorId, 'title' => $request->all()['data']['title'] ?? null, 'description' => $request->all()['data']['description'] ?? null, 'deadline' => $request->all()['data']['deadline'] ?? null, 'estimated_time' => 0, // celkový odhadovaný čas sa vypočíta z employees 'is_draft' => true, 'draft_data' => $request->all()['data']['employees'], // uložíme všetky employees data ]; // Vypočítame celkový odhadovaný čas foreach ($request->all()['data']['employees'] as $employee) { $draftData['estimated_time'] += $employee['duration'] ?? 0; } $subtask = $mastertask->subtasks()->create($draftData); // upload attachmentu if ($request->file('data')) { $attachments = Attachment::uploadAttachmentsToS3($request->file('data')['files'], $mastertask); $subtask->attachments()->createMany($attachments); } return response()->json([ 'message' => 'Draft created successfully', 'subtasks' => SubTaskResource::collection([$subtask]), ], 201); } // Pre normálne (nie draft) subtasky - pôvodná logika // upratanie requestu, pretoze frontend pouziva inu nomenklaturu fieldov ako appka $data['data'] = $request->all()['data']; foreach ($request->all()['data']['employees'] as $key => $employee) { $data['data']['employees'][$key]['user_id'] = $employee['id']; $data['data']['employees'][$key]['title'] = isset($request->all()['data']['title']) ? $request->all()['data']['title'] : null; $data['data']['employees'][$key]['estimated_time'] = $employee['duration']; $data['data']['employees'][$key]['created_by'] = $creatorId; $data['data']['employees'][$key]['description'] = $request->all()['data']['description']; $data['data']['employees'][$key]['deadline'] = $request->all()['data']['deadline']; $data['data']['employees'][$key]['is_draft'] = false; } $request->merge($data); $subtasks = $mastertask->subtasks()->createMany($request->all()['data']['employees']); // syncnutie timeblocks foreach ($request->all()['data']['employees'] as $key => $employee) { if (! isset($data['data']['employees'][$key]['timeblocks']) || $data['data']['employees'][$key]['timeblocks'] == null) { continue; } if ($data['data']['employees'][$key]['user_id'] == $employee['id']) { $subtasks[$key]->syncTimeblocks($data['data']['employees'][$key]['timeblocks']); } } // upload attachmentu if ($request->file('data')) { $attachments = Attachment::uploadAttachmentsToS3($request->file('data')['files'], $mastertask); } foreach ($subtasks as $subtask) { // upload attachmentu if ($request->file('data')) { $subtask->attachments()->createMany($attachments); } } return response()->json([ 'message' => 'Sub Task created successfully', 'subtasks' => SubTaskResource::collection($subtasks), ], 201); } // TODO zabezpecit, ze ked sa meni user, tak nech sa dvojmu nevlozia jeho vykazy public function update($id, Request $request) { $subtask = SubTask::find($id); if (! $subtask) { return response()->json([ 'message' => 'Subtask not found.', ], 404); } // ak je sub task v stave ukončený, nedovolime update. vynimkou je iba znovuotvorenie mastertasku if ($subtask->is_finished == 1 && isset($request->all()['data']['is_finished']) && $request->all()['data']['is_finished'] == 1) { return response()->json([ 'message' => 'Cannot update subtask with is_finished 1.', ], 400); } // ak chcem ukoncit subtask, ktory nema natrackovany cas, tak to nedovolim if (isset($request->all()['data']['is_finished']) && $request->all()['data']['is_finished'] == 1 && $subtask->timesheets->count() == 0) { return response()->json([ 'message' => 'Cannot finish subtask with untracked time.', ], 400); } // validácia pri publikovaní draftu (is_draft: false) $isPublishing = $subtask->is_draft && isset($request->all()['data']['is_draft']) && $request->all()['data']['is_draft'] == false; // Pri publikovaní draftu s draft_data (viacero zamestnancov) - vytvoríme subtasky pre každého if ($isPublishing && $subtask->draft_data) { return $this->publishDraftWithMultipleEmployees($subtask, $request); } if ($isPublishing) { $publishValidator = Validator::make([ 'master_task_id' => $subtask->master_task_id, 'user_id' => $request->all()['data']['employee_id'] ?? $subtask->user_id, 'estimated_time' => $request->all()['data']['duration'] ?? $subtask->estimated_time, ], [ 'master_task_id' => 'required', 'user_id' => 'required', 'estimated_time' => 'required|numeric|min:0', ]); if ($publishValidator->fails()) { return response()->json([ 'message' => 'Draft cannot be published. Missing required fields.', 'errors' => $publishValidator->errors(), ], 400); } } // Zistíme či ide o draft s viacerými zamestnancami a uložíme employees data pred úpravou requestu $employeesData = $request->all()['data']['employees'] ?? null; // Draft má viacerých zamestnancov ak: má employees v requeste ALEBO má draft_data v DB $hasDraftData = $subtask->is_draft && ($employeesData || $subtask->draft_data); $isDraftWithMultipleEmployees = $hasDraftData; // Pre drafty je validácia duration voliteľná if (! $subtask->is_draft) { $validator = Validator::make($request->all(), [ 'data.duration' => 'required', ]); if ($validator->fails()) { return response()->json($validator->errors(), 400); } } // validacia na max 50MB size pre attachmenty. neslo dat do klasickeho laravel validation, tak je to separe if ($files = $request->file('data')) { foreach ($files['files'] as $file) { if ($file->getSize() > 52428800) { return response()->json([ 'files' => 'File size must be less than 50MB', ], 400); } } } // upratanie requestu, pretoze frontend pouziva inu nomenklaturu fieldov ako appka $data['data'] = $request->all()['data']; // Pre draft s viacerými zamestnancami: NEMENIME user_id - zostáva na creatorovi if ($isDraftWithMultipleEmployees) { $data['data']['user_id'] = $subtask->user_id; } else { $data['data']['user_id'] = $request->all()['data']['employee_id'] ?? $subtask->user_id; } $data['data']['title'] = isset($request->all()['data']['title']) ? $request->all()['data']['title'] : null; $data['data']['deadline'] = isset($request->all()['data']['deadline']) ? $request->all()['data']['deadline'] : null; $data['data']['estimated_time'] = $request->all()['data']['duration'] ?? $subtask->estimated_time; $data['data']['updated_by'] = User::getUserIdFromBearerToken($request->bearerToken()); // publikovanie draftu if (isset($request->all()['data']['is_draft'])) { $data['data']['is_draft'] = $request->all()['data']['is_draft']; } // Odstránime employees z data payloadu aby sa neprepísalo draft_data pri update() unset($data['data']['employees']); $request->merge($data); $subtask->update($request->all()['data']); // Pre drafty s employees dátami - aktualizujeme draft_data SEPARÁTNE (po update) if ($subtask->is_draft && $employeesData) { $subtask->draft_data = $employeesData; $subtask->save(); } // upload attachmentu if ($request->file('data')) { $attachments = Attachment::uploadAttachmentsToS3($request->file('data')['files'], $subtask->mastertask); $subtask->attachments()->createMany($attachments); } // syncnutie vykazu if (isset($data['data']['timesheets'])) { $subtask->syncTimesheets($data['data']['timesheets']); } // syncnutie timeblocks - PRE DRAFTY s viacerými zamestnancami NIE! // Timeblocks pre drafty s employees sú uložené v draft_data, nie v tabuľke timeblocks if (isset($data['data']['timeblocks']) && ! $isDraftWithMultipleEmployees) { $subtask->syncTimeblocks($data['data']['timeblocks']); } $message = $isPublishing ? 'Draft published successfully' : 'Subtask updated successfully'; return response()->json([ 'message' => $message, 'subtask' => new SubTaskResource($subtask), ]); } /** * Publikovanie draftu s viacerými zamestnancami - rozdelí draft na jednotlivé subtasky */ private function publishDraftWithMultipleEmployees(SubTask $draft, Request $request) { // Použij employees z requestu ak existujú, inak fallback na draft_data z DB $employees = $request->all()['data']['employees'] ?? $draft->draft_data; // Ak je employees pole prázdne, zlyhaj if (empty($employees)) { return response()->json([ 'message' => 'Draft cannot be published. No employees data found.', ], 400); } // Validácia a doplnenie duration z timeblocks foreach ($employees as $key => $employee) { if (! isset($employee['id'])) { return response()->json([ 'message' => 'Draft cannot be published. Each employee must have id.', ], 400); } // Ak duration nie je nastavený, vypočítaj ho z timeblocks if (! isset($employee['duration']) || $employee['duration'] === null) { if (isset($employee['timeblocks']) && ! empty($employee['timeblocks'])) { $totalDurationMinutes = 0; foreach ($employee['timeblocks'] as $timeblock) { if (isset($timeblock['start']) && isset($timeblock['end'])) { $start = Carbon::parse($timeblock['start']); $end = Carbon::parse($timeblock['end']); $totalDurationMinutes += $end->diffInMinutes($start); } } // Konvertuj na hodiny (frontend očakáva hodiny) $employees[$key]['duration'] = $totalDurationMinutes / 60; } else { return response()->json([ 'message' => 'Draft cannot be published. Each employee must have duration or timeblocks.', ], 400); } } } $mastertask = $draft->mastertask; $createdSubtasks = []; // Použijeme transakciu pre atomicitu operácie try { DB::beginTransaction(); // Získame aktuálne hodnoty z requestu (ak existujú), inak fallback na draft $requestData = $request->all()['data'] ?? []; $title = $requestData['title'] ?? $draft->title; $description = $requestData['description'] ?? $draft->description; $deadline = $requestData['deadline'] ?? $draft->deadline; // Vytvoríme subtask pre každého zamestnanca foreach ($employees as $employee) { $subtaskData = [ 'user_id' => $employee['id'], 'title' => $title, 'description' => $description, 'deadline' => $deadline, 'estimated_time' => $employee['duration'], 'created_by' => $draft->created_by, 'is_draft' => false, 'draft_data' => null, ]; $newSubtask = $mastertask->subtasks()->create($subtaskData); // Sync timeblocks ak existujú - DÔLEŽITÉ: odstránime ID aby sa vytvorili nové záznamy if (isset($employee['timeblocks']) && ! empty($employee['timeblocks'])) { $cleanTimeblocks = array_map(function ($timeblock) { // Odstránime ID a sub_task_id, aby sa vytvorili nové záznamy unset($timeblock['id']); unset($timeblock['sub_task_id']); return $timeblock; }, $employee['timeblocks']); $newSubtask->createTimeblocks($cleanTimeblocks); } // Skopírujeme attachmenty z pôvodného draftu foreach ($draft->attachments as $attachment) { $newSubtask->attachments()->create([ 'name' => $attachment->name, 'path' => $attachment->path, 'size' => $attachment->size, 'type' => $attachment->type, ]); } $createdSubtasks[] = $newSubtask; } // Zmažeme pôvodný draft $draft->timeblocks()->delete(); $draft->attachments()->delete(); // attachmenty sú už skopírované $draft->delete(); DB::commit(); return response()->json([ 'message' => 'Draft published successfully', 'subtasks' => SubTaskResource::collection($createdSubtasks), ], 201); } catch (\Exception $e) { DB::rollBack(); Log::error('Failed to publish draft: ' . $e->getMessage(), [ 'draft_id' => $draft->id, 'employees' => $employees, 'exception' => $e, ]); return response()->json([ 'message' => 'Failed to publish draft. Please try again.', 'error' => $e->getMessage(), ], 500); } } public function delete($id) { $subtask = SubTask::find($id); // ak existuje timesheet k subtasku, nemoze sa zmazat (okrem draftov) if ($subtask->timesheets()->exists()) { if ($subtask->is_draft) { // Pre drafty zmazeme timesheety automaticky $subtask->timesheets()->delete(); } else { return response()->json([ 'message' => 'Cannot delete subtask with submitted timesheets.', ], 400); } } $subtask->timeblocks()->delete(); $subtask->deleteAttachments(); $subtask->delete(); return response()->json([ 'message' => 'Subtask deleted successfully.', 'subtask' => $subtask, ]); } public function submitTimesheet($id, Request $request) { $subtask = SubTask::find($id); $validator = Validator::make($request->all(), [ 'data.employee_id' => 'required', 'data.time' => 'required', ]); // validacia, ci dany user moze submitovat timesheet - ci mu patri subtask $validator->after(function ($validator) use ($subtask, $request) { if ($subtask->user_id != $request->all()['data']['employee_id']) { $validator->errors()->add( 'data.employee_id', 'This user cannot submit timesheet for the selected subtask. Subtask is assigned to user with id: '.$subtask->user_id.'.' ); } }); if ($validator->fails()) { return response()->json($validator->errors(), 400); } $subtask->timesheets()->insert([ 'time' => $request->all()['data']['time'], 'user_id' => $request->all()['data']['employee_id'], 'sub_task_id' => $subtask->id, 'created_at' => Carbon::now(), 'updated_at' => Carbon::now(), ]); return response()->json([ 'message' => 'Subtask timesheet submitted successfully.', 'subtask' => $subtask, ], 201); } }