From 94a5776cbccef551afced82e3d9c5c1e1e44fcf1 Mon Sep 17 00:00:00 2001 From: br0kenpixel <23280129+br0kenpixel@users.noreply.github.com> Date: Thu, 23 Oct 2025 14:16:13 +0200 Subject: [PATCH 001/205] feat: add internship retrieval and display functionality --- .../Http/Controllers/InternshipController.php | 25 ++++++ backend/routes/api.php | 5 +- frontend/app/pages/dashboard/student.vue | 86 ++++++++++++++++++- frontend/app/types/internship_status.ts | 35 ++++++++ frontend/app/types/internships.ts | 26 ++++++ 5 files changed, 174 insertions(+), 3 deletions(-) create mode 100644 frontend/app/types/internship_status.ts create mode 100644 frontend/app/types/internships.ts diff --git a/backend/app/Http/Controllers/InternshipController.php b/backend/app/Http/Controllers/InternshipController.php index b93eca6..69599d1 100644 --- a/backend/app/Http/Controllers/InternshipController.php +++ b/backend/app/Http/Controllers/InternshipController.php @@ -2,11 +2,36 @@ namespace App\Http\Controllers; +use App\Models\Company; use App\Models\Internship; +use App\Models\InternshipStatus; +use App\Models\User; use Illuminate\Http\Request; class InternshipController extends Controller { + public function all() + { + $internships = Internship::where('user_id', auth()->id())->get()->makeHidden(['created_at', 'updated_at']); + + $internships->each(function ($internship) { + $internship->company = Company::find($internship->company_id)->makeHidden(['created_at', 'updated_at']); + unset($internship->company_id); + }); + + $internships->each(function ($internship) { + $internship->contact = User::find($internship->company->contact)->makeHidden(['created_at', 'updated_at', 'email_verified_at']); + unset($internship->company->contact); + }); + + $internships->each(function ($internship) { + $internship->status = InternshipStatus::whereColumn('internship_id', '=', $internship->id)->get()->first()->makeHidden(['created_at', 'updated_at', 'id']); + $internship->status->modified_by = User::find($internship->status->modified_by)->makeHidden(['created_at', 'updated_at', 'email_verified_at']); + }); + + return response()->json($internships); + } + /** * Display a listing of the resource. */ diff --git a/backend/routes/api.php b/backend/routes/api.php index 1092813..196a44c 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -1,6 +1,7 @@ get('/user', function (Request $request) { Route::post('/password-reset', [RegisteredUserController::class, 'reset_password']) ->middleware(['guest', 'throttle:6,1']) - ->name('password.reset'); \ No newline at end of file + ->name('password.reset'); + +Route::get('/internships', [InternshipController::class, 'all'])->middleware(['auth'])->name("api.internships"); \ No newline at end of file diff --git a/frontend/app/pages/dashboard/student.vue b/frontend/app/pages/dashboard/student.vue index e7558bb..68ff8f9 100644 --- a/frontend/app/pages/dashboard/student.vue +++ b/frontend/app/pages/dashboard/student.vue @@ -1,4 +1,6 @@ \ No newline at end of file + + + \ No newline at end of file diff --git a/frontend/app/types/internship_status.ts b/frontend/app/types/internship_status.ts new file mode 100644 index 0000000..b813f95 --- /dev/null +++ b/frontend/app/types/internship_status.ts @@ -0,0 +1,35 @@ +import type { User } from "./user"; + +export interface InternshipStatusData { + internship_id: number; + user_id: string; + status: InternshipStatus; + changed: string; + note: string; + modified_by: User; +}; + +export enum InternshipStatus { + SUBMITTED = 'SUBMITTED', + CONFIRMED = 'CONFIRMED', + DENIED = 'DENIED', + DEFENDED = 'DEFENDED', + NOT_DEFENDED = 'NOT_DEFENDED' +}; + +export function prettyInternshipStatus(status: InternshipStatus) { + switch (status) { + case InternshipStatus.SUBMITTED: + return "Zadané"; + case InternshipStatus.CONFIRMED: + return "Potvrdené"; + case InternshipStatus.DENIED: + return "Zamítnuté"; + case InternshipStatus.DEFENDED: + return "Obhájené"; + case InternshipStatus.NOT_DEFENDED: + return "Neobhájené"; + default: + throw new Error("Unknown status"); + } +} \ No newline at end of file diff --git a/frontend/app/types/internships.ts b/frontend/app/types/internships.ts new file mode 100644 index 0000000..3aa05a1 --- /dev/null +++ b/frontend/app/types/internships.ts @@ -0,0 +1,26 @@ +import type { CompanyData } from "./company_data"; +import type { InternshipStatusData } from "./internship_status"; + +export interface Internship { + id: number; + user_id: string; + company: CompanyData; + start: string; + end: string; + year_of_study: number; + semester: string; + position_description: string; + agreement?: Uint8Array; + status: InternshipStatusData; +}; + +export interface NewInternship { + user_id: string; + company_id: string; + start: number; + end: number; + year_of_study: boolean; + semester: string; + position_description: string; + agreement?: Uint8Array; +}; \ No newline at end of file From 426ea6f910498991b3b7f94481f265460ca4bcc9 Mon Sep 17 00:00:00 2001 From: br0kenpixel <23280129+br0kenpixel@users.noreply.github.com> Date: Tue, 28 Oct 2025 09:29:22 +0100 Subject: [PATCH 002/205] feat: display error message for internship data retrieval --- frontend/app/pages/dashboard/student.vue | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend/app/pages/dashboard/student.vue b/frontend/app/pages/dashboard/student.vue index 68ff8f9..5d79e36 100644 --- a/frontend/app/pages/dashboard/student.vue +++ b/frontend/app/pages/dashboard/student.vue @@ -25,7 +25,7 @@ const headers = [ ]; const user = useSanctumUser(); -const { data, status, error } = await useSanctumFetch('/api/internships'); +const { data, error } = await useSanctumFetch('/api/internships'); const serverItems = [ { @@ -63,6 +63,11 @@ const loading = false;

Moje praxe

+ + + + From ce9a04ae72cd0f1bbe59f912662d6da2e43a9595 Mon Sep 17 00:00:00 2001 From: br0kenpixel <23280129+br0kenpixel@users.noreply.github.com> Date: Tue, 28 Oct 2025 09:29:54 +0100 Subject: [PATCH 003/205] refactor: remove unused variables from student dashboard --- frontend/app/pages/dashboard/student.vue | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/frontend/app/pages/dashboard/student.vue b/frontend/app/pages/dashboard/student.vue index 5d79e36..e43623e 100644 --- a/frontend/app/pages/dashboard/student.vue +++ b/frontend/app/pages/dashboard/student.vue @@ -26,20 +26,6 @@ const headers = [ const user = useSanctumUser(); const { data, error } = await useSanctumFetch('/api/internships'); - -const serverItems = [ - { - company: "Kutil s.r.o.", - start: "01.01.2025", - end: "30.01.2025", - year_of_study: 1, - semester: "zinmý", - status: "Zadané", - } -]; -const totalItems = 0; -const loading = false; - + + + \ No newline at end of file From d583a4fcb014d4e916d5d46c14f8aab9004c1f7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veronika=20Feh=C3=A9rv=C3=ADziov=C3=A1?= <128744051+VeronikaFeherviziova@users.noreply.github.com> Date: Fri, 31 Oct 2025 19:37:08 +0100 Subject: [PATCH 015/205] feat: add company list for admins --- .../pages/dashboard/admin/companies/index.vue | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 frontend/app/pages/dashboard/admin/companies/index.vue diff --git a/frontend/app/pages/dashboard/admin/companies/index.vue b/frontend/app/pages/dashboard/admin/companies/index.vue new file mode 100644 index 0000000..6bd4cac --- /dev/null +++ b/frontend/app/pages/dashboard/admin/companies/index.vue @@ -0,0 +1,85 @@ + + + + + \ No newline at end of file From 72edd247b3d04b89947221fa080150334ce583fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veronika=20Feh=C3=A9rv=C3=ADziov=C3=A1?= <128744051+VeronikaFeherviziova@users.noreply.github.com> Date: Fri, 31 Oct 2025 19:37:27 +0100 Subject: [PATCH 016/205] feat: add sample page for company editing --- frontend/app/pages/dashboard/admin/companies/edit/[id].vue | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 frontend/app/pages/dashboard/admin/companies/edit/[id].vue diff --git a/frontend/app/pages/dashboard/admin/companies/edit/[id].vue b/frontend/app/pages/dashboard/admin/companies/edit/[id].vue new file mode 100644 index 0000000..f1b402e --- /dev/null +++ b/frontend/app/pages/dashboard/admin/companies/edit/[id].vue @@ -0,0 +1,3 @@ + \ No newline at end of file From 00f5bd495c8b477a7f611b3fd759e9ece0cad806 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veronika=20Feh=C3=A9rv=C3=ADziov=C3=A1?= <128744051+VeronikaFeherviziova@users.noreply.github.com> Date: Fri, 31 Oct 2025 20:08:26 +0100 Subject: [PATCH 017/205] fix: incorrect start and end date range --- backend/database/factories/InternshipFactory.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/backend/database/factories/InternshipFactory.php b/backend/database/factories/InternshipFactory.php index d1d9015..8981b71 100644 --- a/backend/database/factories/InternshipFactory.php +++ b/backend/database/factories/InternshipFactory.php @@ -16,11 +16,14 @@ class InternshipFactory extends Factory */ public function definition(): array { + $start = fake()->dateTime(); + $end = (clone $start)->modify('+' . fake()->numberBetween(150, 160) . ' hours'); + return [ 'user_id' => 0, 'company_id' => 0, - 'start' => fake()->dateTime(), - 'end' => fake()->dateTime("+30 days"), + 'start' => $start, + 'end' => $end, 'year_of_study' => fake()->randomElement([1, 2, 3, 4, 5]), 'semester' => fake()->randomElement(["WINTER", "SUMMER"]), 'position_description' => fake()->jobTitle(), From 1057a8250cbe5912b7c6c57bd93df1583822c9f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veronika=20Feh=C3=A9rv=C3=ADziov=C3=A1?= <128744051+VeronikaFeherviziova@users.noreply.github.com> Date: Fri, 31 Oct 2025 20:08:58 +0100 Subject: [PATCH 018/205] fix: make time always zero in start and end date-time --- backend/database/factories/InternshipFactory.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/database/factories/InternshipFactory.php b/backend/database/factories/InternshipFactory.php index 8981b71..33837de 100644 --- a/backend/database/factories/InternshipFactory.php +++ b/backend/database/factories/InternshipFactory.php @@ -16,8 +16,8 @@ class InternshipFactory extends Factory */ public function definition(): array { - $start = fake()->dateTime(); - $end = (clone $start)->modify('+' . fake()->numberBetween(150, 160) . ' hours'); + $start = fake()->dateTime()->setTime(0, 0, 0, 0); + $end = (clone $start)->modify('+' . fake()->numberBetween(150, 160) . ' hours')->setTime(0, 0, 0, 0); return [ 'user_id' => 0, From 1683155ae33cec5066b6e4700dfab1f825db0c51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veronika=20Feh=C3=A9rv=C3=ADziov=C3=A1?= <128744051+VeronikaFeherviziova@users.noreply.github.com> Date: Fri, 31 Oct 2025 20:32:16 +0100 Subject: [PATCH 019/205] refactor: change API endpoint for getting student's personal internships --- backend/app/Http/Controllers/InternshipController.php | 2 +- backend/routes/api.php | 1 + frontend/app/pages/dashboard/student/index.vue | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/app/Http/Controllers/InternshipController.php b/backend/app/Http/Controllers/InternshipController.php index 5d2a680..4677cd1 100644 --- a/backend/app/Http/Controllers/InternshipController.php +++ b/backend/app/Http/Controllers/InternshipController.php @@ -10,7 +10,7 @@ use Illuminate\Http\Request; class InternshipController extends Controller { - public function all() + public function all_student() { $internships = Internship::where('user_id', auth()->id())->get()->makeHidden(['created_at', 'updated_at']); diff --git a/backend/routes/api.php b/backend/routes/api.php index 392241f..48ec91f 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -26,6 +26,7 @@ Route::post('/password-reset', [RegisteredUserController::class, 'reset_password Route::prefix('/internships')->group(function () { Route::get("/", [InternshipController::class, 'all'])->name("api.internships"); + Route::get("/my", [InternshipController::class, 'all_student'])->name("api.internships.student"); Route::middleware("auth:sanctum")->group(function () { Route::put("/new", [InternshipController::class, 'store'])->name("api.internships.create"); diff --git a/frontend/app/pages/dashboard/student/index.vue b/frontend/app/pages/dashboard/student/index.vue index 00b72e2..6536057 100644 --- a/frontend/app/pages/dashboard/student/index.vue +++ b/frontend/app/pages/dashboard/student/index.vue @@ -25,7 +25,7 @@ const headers = [ ]; const user = useSanctumUser(); -const { data, error } = await useSanctumFetch('/api/internships'); +const { data, error } = await useSanctumFetch('/api/internships/my'); From 2e5dcda640418fa2dddb3fc947635f3191a6acdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veronika=20Feh=C3=A9rv=C3=ADziov=C3=A1?= <128744051+VeronikaFeherviziova@users.noreply.github.com> Date: Fri, 31 Oct 2025 21:46:07 +0100 Subject: [PATCH 027/205] feat: add API route for getting a single internship --- .../Http/Controllers/InternshipController.php | 27 +++++++++++++++++++ backend/routes/api.php | 1 + 2 files changed, 28 insertions(+) diff --git a/backend/app/Http/Controllers/InternshipController.php b/backend/app/Http/Controllers/InternshipController.php index b7ebf14..80d41b3 100644 --- a/backend/app/Http/Controllers/InternshipController.php +++ b/backend/app/Http/Controllers/InternshipController.php @@ -65,6 +65,33 @@ class InternshipController extends Controller return response()->json($internships); } + public function get(int $id) { + $user = auth()->user(); + + $internship = Internship::find($id); + + if(!$internship) { + return response()->json([ + 'message' => 'No such internship exists.' + ], 400); + } + + if ($user->role !== 'ADMIN' && $internship->user_id !== $user->id) { + abort(403, 'Unauthorized'); + } + + $internship->company = Company::find($internship->company_id)->makeHidden(['created_at', 'updated_at']); + unset($internship->company_id); + + $internship->contact = User::find($internship->company->contact)->makeHidden(['created_at', 'updated_at', 'email_verified_at']); + unset($internship->company->contact); + + $internship->status = InternshipStatus::whereColumn('internship_id', '=', $internship->id)->get()->first()->makeHidden(['created_at', 'updated_at', 'id']); + $internship->status->modified_by = User::find($internship->status->modified_by)->makeHidden(['created_at', 'updated_at', 'email_verified_at']); + + return response()->json($internship); + } + /** * Display a listing of the resource. */ diff --git a/backend/routes/api.php b/backend/routes/api.php index 48ec91f..a1c41d3 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -29,6 +29,7 @@ Route::prefix('/internships')->group(function () { Route::get("/my", [InternshipController::class, 'all_student'])->name("api.internships.student"); Route::middleware("auth:sanctum")->group(function () { + Route::get("/{id}", [InternshipController::class, 'get'])->name("api.internships.get"); Route::put("/new", [InternshipController::class, 'store'])->name("api.internships.create"); }); }); From 8a5914dc04e3b3e6d4c1b8ab46a5a888bf2faea2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veronika=20Feh=C3=A9rv=C3=ADziov=C3=A1?= <128744051+VeronikaFeherviziova@users.noreply.github.com> Date: Fri, 31 Oct 2025 22:26:52 +0100 Subject: [PATCH 028/205] fix: incorrect value for "Semester" select if a value is specified in a prop --- frontend/app/components/InternshipEditor.vue | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/frontend/app/components/InternshipEditor.vue b/frontend/app/components/InternshipEditor.vue index 1069565..8306e2f 100644 --- a/frontend/app/components/InternshipEditor.vue +++ b/frontend/app/components/InternshipEditor.vue @@ -30,6 +30,16 @@ const year_of_study_choices = [ subtitle: 'magisterské', } ]; +const semester_choices = [ + { + title: "Zimný", + value: "WINTER" + }, + { + title: "Letný", + value: "SUMMER" + } +]; const props = defineProps({ start: { @@ -118,7 +128,7 @@ const { data, error } = await useSanctumFetch('/api/companies/sim :item-props="(item) => { return { title: item.title, subtitle: item.subtitle } }" :item-value="yearOfStudyValueHandler" :rules="[rules.required]"> - From c72b507245ce9d92d2da7585fb86127fee6ef7d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veronika=20Feh=C3=A9rv=C3=ADziov=C3=A1?= <128744051+VeronikaFeherviziova@users.noreply.github.com> Date: Fri, 31 Oct 2025 22:31:08 +0100 Subject: [PATCH 029/205] feat: add API endpoint for updating basic internship information --- .../Http/Controllers/InternshipController.php | 92 +++++++++++++------ backend/routes/api.php | 6 +- 2 files changed, 67 insertions(+), 31 deletions(-) diff --git a/backend/app/Http/Controllers/InternshipController.php b/backend/app/Http/Controllers/InternshipController.php index 80d41b3..8800d2d 100644 --- a/backend/app/Http/Controllers/InternshipController.php +++ b/backend/app/Http/Controllers/InternshipController.php @@ -115,36 +115,8 @@ class InternshipController extends Controller { $user = auth()->user(); - $request->validate([ - 'company_id' => ['required', 'exists:companies,id'], - 'start' => ['required', 'date'], - 'end' => ['required', 'date', 'after:start'], - 'year_of_study' => ['required', 'integer', 'between:1,5'], - 'semester' => ['required', 'in:WINTER,SUMMER'], - 'position_description' => ['required', 'string', 'min:1'] - ]); - - $request->merge([ - 'start' => date('Y-m-d H:i:s', strtotime($request->start)), - 'end' => date('Y-m-d H:i:s', strtotime($request->end)) - ]); - - // je už nejaká prax ktorá sa časovo prekrýva? - $existingInternship = Internship::where('user_id', $user->id) - ->where(function ($query) use ($request) { - $query->whereBetween('start', [$request->start, $request->end]) - ->orWhereBetween('end', [$request->start, $request->end]) - ->orWhere(function ($q) use ($request) { - $q->where('start', '<=', $request->start) - ->where('end', '>=', $request->end); - }); - }) - ->exists(); - if ($existingInternship) { - return response()->json([ - 'message' => 'You already have an internship during this period.' - ], 422); - } + $this->validateNewInternship($request); + $this->checkOverlap($user->id, $request->start, $request->end); $Internship = Internship::create([ 'user_id' => $user->id, @@ -184,6 +156,31 @@ class InternshipController extends Controller // } + public function update_basic(int $id, Request $request) + { + $user = auth()->user(); + $internship = Internship::find($id); + + if(!$internship) { + return response()->json([ + 'message' => 'No such internship exists.' + ], 400); + } + + if ($user->role !== 'ADMIN' && $internship->user_id !== $user->id) { + abort(403, 'Unauthorized'); + } + + $this->validateNewInternship($request); + + if($internship->start !== $request->start || $internship->end !== $request->end) { + $this->checkOverlap($user->id, $request->start, $request->end); + } + + $internship->update($request->except(['user_id'])); + return response()->noContent(); + } + /** * Update the specified resource in storage. */ @@ -199,4 +196,39 @@ class InternshipController extends Controller { // } + + private function validateNewInternship(Request $request) { + $request->validate([ + 'company_id' => ['required', 'exists:companies,id'], + 'start' => ['required', 'date'], + 'end' => ['required', 'date', 'after:start'], + 'year_of_study' => ['required', 'integer', 'between:1,5'], + 'semester' => ['required', 'in:WINTER,SUMMER'], + 'position_description' => ['required', 'string', 'min:1'] + ]); + + $request->merge([ + 'start' => date('Y-m-d H:i:s', strtotime($request->start)), + 'end' => date('Y-m-d H:i:s', strtotime($request->end)) + ]); + } + + private function checkOverlap(int $user_id, string $start_date, string $end_date) { + $existingInternship = Internship::where('user_id', $user_id) + ->where(function ($query) use ($start_date, $end_date) { + $query->whereBetween('start', [$start_date, $end_date]) + ->orWhereBetween('end', [$start_date, $end_date]) + ->orWhere(function ($q) use ($end_date) { + $q->where('start', '<=', $end_date) + ->where('end', '>=', $end_date); + }); + }) + ->exists(); + + if ($existingInternship) { + abort(response()->json([ + 'message' => 'You already have an internship during this period.' + ], 400)); + } + } } diff --git a/backend/routes/api.php b/backend/routes/api.php index a1c41d3..d1a3354 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -29,7 +29,11 @@ Route::prefix('/internships')->group(function () { Route::get("/my", [InternshipController::class, 'all_student'])->name("api.internships.student"); Route::middleware("auth:sanctum")->group(function () { - Route::get("/{id}", [InternshipController::class, 'get'])->name("api.internships.get"); + Route::prefix('/{id}')->group(function () { + Route::get("/", [InternshipController::class, 'get'])->name("api.internships.get"); + Route::post("/basic", [InternshipController::class, 'update_basic'])->name("api.internships.update.basic"); + }); + Route::put("/new", [InternshipController::class, 'store'])->name("api.internships.create"); }); }); From d4252f68dea09355bf368d967b37e7e774862eda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veronika=20Feh=C3=A9rv=C3=ADziov=C3=A1?= <128744051+VeronikaFeherviziova@users.noreply.github.com> Date: Fri, 31 Oct 2025 22:31:49 +0100 Subject: [PATCH 030/205] feat: add basic internship editor for admins --- .../dashboard/admin/internships/edit/[id].vue | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 frontend/app/pages/dashboard/admin/internships/edit/[id].vue diff --git a/frontend/app/pages/dashboard/admin/internships/edit/[id].vue b/frontend/app/pages/dashboard/admin/internships/edit/[id].vue new file mode 100644 index 0000000..050d78a --- /dev/null +++ b/frontend/app/pages/dashboard/admin/internships/edit/[id].vue @@ -0,0 +1,101 @@ + + + + + \ No newline at end of file From 107cf8a7eae291931154ed1399284a73898bb108 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veronika=20Feh=C3=A9rv=C3=ADziov=C3=A1?= <128744051+VeronikaFeherviziova@users.noreply.github.com> Date: Fri, 31 Oct 2025 22:39:56 +0100 Subject: [PATCH 031/205] refactor: reformat datetime value to display date only --- backend/app/Http/Controllers/InternshipController.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/backend/app/Http/Controllers/InternshipController.php b/backend/app/Http/Controllers/InternshipController.php index 8800d2d..7c32f5e 100644 --- a/backend/app/Http/Controllers/InternshipController.php +++ b/backend/app/Http/Controllers/InternshipController.php @@ -7,6 +7,7 @@ use App\Models\Internship; use App\Models\InternshipStatus; use App\Models\User; use Illuminate\Http\Request; +use Carbon\Carbon; class InternshipController extends Controller { @@ -40,6 +41,11 @@ class InternshipController extends Controller $internship->status->modified_by = User::find($internship->status->modified_by)->makeHidden(['created_at', 'updated_at', 'email_verified_at']); }); + $internships->each(function ($internship) { + $internship->start = Carbon::parse($internship->start)->format('d.m.Y'); + $internship->end = Carbon::parse($internship->end)->format('d.m.Y'); + }); + return response()->json($internships); } @@ -62,6 +68,11 @@ class InternshipController extends Controller $internship->status->modified_by = User::find($internship->status->modified_by)->makeHidden(['created_at', 'updated_at', 'email_verified_at']); }); + $internships->each(function ($internship) { + $internship->start = Carbon::parse($internship->start)->format('d.m.Y'); + $internship->end = Carbon::parse($internship->end)->format('d.m.Y'); + }); + return response()->json($internships); } From 5065b2ad8decc1032b85652bc21410ed27d00fe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veronika=20Feh=C3=A9rv=C3=ADziov=C3=A1?= <128744051+VeronikaFeherviziova@users.noreply.github.com> Date: Fri, 31 Oct 2025 22:43:59 +0100 Subject: [PATCH 032/205] fix: add option to ignore overlapping internships if they have the same ID --- .../app/Http/Controllers/InternshipController.php | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/backend/app/Http/Controllers/InternshipController.php b/backend/app/Http/Controllers/InternshipController.php index 7c32f5e..6d1ddde 100644 --- a/backend/app/Http/Controllers/InternshipController.php +++ b/backend/app/Http/Controllers/InternshipController.php @@ -183,10 +183,7 @@ class InternshipController extends Controller } $this->validateNewInternship($request); - - if($internship->start !== $request->start || $internship->end !== $request->end) { - $this->checkOverlap($user->id, $request->start, $request->end); - } + $this->checkOverlap($user->id, $request->start, $request->end, $internship->id); $internship->update($request->except(['user_id'])); return response()->noContent(); @@ -224,8 +221,13 @@ class InternshipController extends Controller ]); } - private function checkOverlap(int $user_id, string $start_date, string $end_date) { + private function checkOverlap(int $user_id, string $start_date, string $end_date, ?int $current_id = null) { $existingInternship = Internship::where('user_id', $user_id) + // check if the two internships do not have the same ID + ->when($current_id, function ($query) use ($current_id) { + $query->where('id', '!=', $current_id); + }) + // check if the start/end period collides with another internship ->where(function ($query) use ($start_date, $end_date) { $query->whereBetween('start', [$start_date, $end_date]) ->orWhereBetween('end', [$start_date, $end_date]) @@ -241,5 +243,5 @@ class InternshipController extends Controller 'message' => 'You already have an internship during this period.' ], 400)); } - } + } } From 75f3dd3f049d4fed1c47aa76650194bd7a3d7299 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veronika=20Feh=C3=A9rv=C3=ADziov=C3=A1?= <128744051+VeronikaFeherviziova@users.noreply.github.com> Date: Fri, 31 Oct 2025 23:05:19 +0100 Subject: [PATCH 033/205] fix: incorrect user ID being used with overlap checking --- backend/app/Http/Controllers/InternshipController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/app/Http/Controllers/InternshipController.php b/backend/app/Http/Controllers/InternshipController.php index 6d1ddde..418cdb3 100644 --- a/backend/app/Http/Controllers/InternshipController.php +++ b/backend/app/Http/Controllers/InternshipController.php @@ -183,7 +183,7 @@ class InternshipController extends Controller } $this->validateNewInternship($request); - $this->checkOverlap($user->id, $request->start, $request->end, $internship->id); + $this->checkOverlap($internship->user_id, $request->start, $request->end, $internship->id); $internship->update($request->except(['user_id'])); return response()->noContent(); From a69674a9c7b5253805e03f68c224665020f4e0dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veronika=20Feh=C3=A9rv=C3=ADziov=C3=A1?= <128744051+VeronikaFeherviziova@users.noreply.github.com> Date: Fri, 31 Oct 2025 23:15:13 +0100 Subject: [PATCH 034/205] fix: ignore time value of start and end values in requests --- backend/app/Http/Controllers/InternshipController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/app/Http/Controllers/InternshipController.php b/backend/app/Http/Controllers/InternshipController.php index 418cdb3..dd9b385 100644 --- a/backend/app/Http/Controllers/InternshipController.php +++ b/backend/app/Http/Controllers/InternshipController.php @@ -216,8 +216,8 @@ class InternshipController extends Controller ]); $request->merge([ - 'start' => date('Y-m-d H:i:s', strtotime($request->start)), - 'end' => date('Y-m-d H:i:s', strtotime($request->end)) + 'start' => date('Y-m-d 00:00:00', strtotime($request->start)), + 'end' => date('Y-m-d 00:00:00', strtotime($request->end)) ]); } From 7b968df48ec9555390d7e49f5937d73fd5bc76cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veronika=20Feh=C3=A9rv=C3=ADziov=C3=A1?= <128744051+VeronikaFeherviziova@users.noreply.github.com> Date: Fri, 31 Oct 2025 23:29:14 +0100 Subject: [PATCH 035/205] fix: incorrect date due to localtime/utc --- frontend/app/components/InternshipEditor.vue | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/frontend/app/components/InternshipEditor.vue b/frontend/app/components/InternshipEditor.vue index 8306e2f..98bf7cc 100644 --- a/frontend/app/components/InternshipEditor.vue +++ b/frontend/app/components/InternshipEditor.vue @@ -91,12 +91,19 @@ const form = ref({ const user = useSanctumUser(); +function dateTimeFixup(datetime: Date) { + const year = datetime.getFullYear() + const month = String(datetime.getMonth() + 1).padStart(2, '0') + const day = String(datetime.getDate()).padStart(2, '0') + return `${year}-${month}-${day}`; +} + function triggerSubmit() { const new_internship: NewInternship = { user_id: user.value?.id!, company_id: form.value.company_id!, - start: form.value.start, - end: form.value.end, + start: dateTimeFixup(form.value.start as any), + end: dateTimeFixup(form.value.end as any), year_of_study: form.value.year_of_study, semester: form.value.semester === "Zimný" ? "WINTER" : "SUMMER", position_description: form.value.description From d49133a377db34f8b24f8abb0c81ee45623c674a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veronika=20Feh=C3=A9rv=C3=ADziov=C3=A1?= <128744051+VeronikaFeherviziova@users.noreply.github.com> Date: Fri, 31 Oct 2025 23:29:46 +0100 Subject: [PATCH 036/205] fix: handle errors with status code 400 --- frontend/app/pages/dashboard/admin/internships/edit/[id].vue | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/app/pages/dashboard/admin/internships/edit/[id].vue b/frontend/app/pages/dashboard/admin/internships/edit/[id].vue index 050d78a..1dcc077 100644 --- a/frontend/app/pages/dashboard/admin/internships/edit/[id].vue +++ b/frontend/app/pages/dashboard/admin/internships/edit/[id].vue @@ -31,10 +31,8 @@ async function handleUpdateOfBasicInfo(internship: NewInternship) { navigateTo("/dashboard/admin/internships"); } catch (e) { - if (e instanceof FetchError && e.response?.status === 422) { + if (e instanceof FetchError && (e.response?.status === 422 || e.response?.status === 400)) { action_error.value = e.response?._data.message; - } else { - action_error.value = "Nepodarilo sa pripojiť na API."; } } finally { loading.value = false; From 65bdbbe512a4cc76489826b3360f4ed5ba2c1e3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veronika=20Feh=C3=A9rv=C3=ADziov=C3=A1?= <128744051+VeronikaFeherviziova@users.noreply.github.com> Date: Fri, 31 Oct 2025 23:32:28 +0100 Subject: [PATCH 037/205] fix: do not try to fix date values of type string --- frontend/app/components/InternshipEditor.vue | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/app/components/InternshipEditor.vue b/frontend/app/components/InternshipEditor.vue index 98bf7cc..f2bcc94 100644 --- a/frontend/app/components/InternshipEditor.vue +++ b/frontend/app/components/InternshipEditor.vue @@ -91,7 +91,11 @@ const form = ref({ const user = useSanctumUser(); -function dateTimeFixup(datetime: Date) { +function dateTimeFixup(datetime: Date|string) { + if(typeof datetime === 'string') { + return datetime; + } + const year = datetime.getFullYear() const month = String(datetime.getMonth() + 1).padStart(2, '0') const day = String(datetime.getDate()).padStart(2, '0') From e7ef97352df3e1e1adb1dd8b77db648e1bb06c9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veronika=20Feh=C3=A9rv=C3=ADziov=C3=A1?= <128744051+VeronikaFeherviziova@users.noreply.github.com> Date: Sat, 1 Nov 2025 15:51:12 +0100 Subject: [PATCH 038/205] feat: display currect internship status in the editor for admins --- .../app/pages/dashboard/admin/internships/edit/[id].vue | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/frontend/app/pages/dashboard/admin/internships/edit/[id].vue b/frontend/app/pages/dashboard/admin/internships/edit/[id].vue index 1dcc077..f4f5e89 100644 --- a/frontend/app/pages/dashboard/admin/internships/edit/[id].vue +++ b/frontend/app/pages/dashboard/admin/internships/edit/[id].vue @@ -1,5 +1,6 @@ + + + + diff --git a/frontend/app/pages/dashboard/admin/internships/edit/[id].vue b/frontend/app/pages/dashboard/admin/internships/edit/[id].vue index 823ea92..b5d2549 100644 --- a/frontend/app/pages/dashboard/admin/internships/edit/[id].vue +++ b/frontend/app/pages/dashboard/admin/internships/edit/[id].vue @@ -79,8 +79,11 @@ const { data, error } = await useSanctumFetch(`/api/internships/${ro

{{ prettyInternshipStatus(data?.status.status!) }}

Poznámka: {{ data?.status.note }}

Posledná zmena: {{ data?.status.changed }}, {{ data?.status.modified_by.name }}

+
+

História

+
From 733090f64359dea31a81e7a244f4a20fc89b6dc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veronika=20Feh=C3=A9rv=C3=ADziov=C3=A1?= <128744051+VeronikaFeherviziova@users.noreply.github.com> Date: Sat, 1 Nov 2025 16:36:08 +0100 Subject: [PATCH 042/205] fix: typo in prettyInternshipStatus --- frontend/app/types/internship_status.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app/types/internship_status.ts b/frontend/app/types/internship_status.ts index b813f95..5789229 100644 --- a/frontend/app/types/internship_status.ts +++ b/frontend/app/types/internship_status.ts @@ -24,7 +24,7 @@ export function prettyInternshipStatus(status: InternshipStatus) { case InternshipStatus.CONFIRMED: return "Potvrdené"; case InternshipStatus.DENIED: - return "Zamítnuté"; + return "Zamietnuté"; case InternshipStatus.DEFENDED: return "Obhájené"; case InternshipStatus.NOT_DEFENDED: From 84d34597819a0fcb379a85ab7efe53a468367d83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veronika=20Feh=C3=A9rv=C3=ADziov=C3=A1?= <128744051+VeronikaFeherviziova@users.noreply.github.com> Date: Sat, 1 Nov 2025 20:04:13 +0100 Subject: [PATCH 043/205] feat: add function for getting possible new internship statuses based on current one --- frontend/app/types/internship_status.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/frontend/app/types/internship_status.ts b/frontend/app/types/internship_status.ts index 5789229..5a57726 100644 --- a/frontend/app/types/internship_status.ts +++ b/frontend/app/types/internship_status.ts @@ -32,4 +32,21 @@ export function prettyInternshipStatus(status: InternshipStatus) { default: throw new Error("Unknown status"); } +} + +export function possibleNextStates(status: InternshipStatus) { + switch (status) { + case InternshipStatus.SUBMITTED: + return [InternshipStatus.CONFIRMED, InternshipStatus.DENIED]; + case InternshipStatus.CONFIRMED: + return [InternshipStatus.SUBMITTED, InternshipStatus.DENIED, InternshipStatus.DEFENDED, InternshipStatus.NOT_DEFENDED]; + case InternshipStatus.DENIED: + return [InternshipStatus.SUBMITTED, InternshipStatus.CONFIRMED]; + case InternshipStatus.DEFENDED: + return []; + case InternshipStatus.NOT_DEFENDED: + return []; + default: + throw new Error("Unknown status"); + } } \ No newline at end of file From 8c41e72f1fcd3ee864a07c612276c4f15d2fe537 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veronika=20Feh=C3=A9rv=C3=ADziov=C3=A1?= <128744051+VeronikaFeherviziova@users.noreply.github.com> Date: Sat, 1 Nov 2025 20:09:24 +0100 Subject: [PATCH 044/205] feat: limit possible new internship states for employers --- frontend/app/types/internship_status.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/frontend/app/types/internship_status.ts b/frontend/app/types/internship_status.ts index 5a57726..f952249 100644 --- a/frontend/app/types/internship_status.ts +++ b/frontend/app/types/internship_status.ts @@ -1,3 +1,4 @@ +import { Role } from "./role"; import type { User } from "./user"; export interface InternshipStatusData { @@ -34,13 +35,25 @@ export function prettyInternshipStatus(status: InternshipStatus) { } } -export function possibleNextStates(status: InternshipStatus) { +export function possibleNextStates(status: InternshipStatus, user_role: Role) { switch (status) { case InternshipStatus.SUBMITTED: + if (user_role === Role.EMPLOYER) { + return []; + } + return [InternshipStatus.CONFIRMED, InternshipStatus.DENIED]; case InternshipStatus.CONFIRMED: + if (user_role === Role.EMPLOYER) { + return [InternshipStatus.DENIED]; + } + return [InternshipStatus.SUBMITTED, InternshipStatus.DENIED, InternshipStatus.DEFENDED, InternshipStatus.NOT_DEFENDED]; case InternshipStatus.DENIED: + if (user_role === Role.EMPLOYER) { + return [InternshipStatus.CONFIRMED]; + } + return [InternshipStatus.SUBMITTED, InternshipStatus.CONFIRMED]; case InternshipStatus.DEFENDED: return []; From e972463da21e50e8b87d46758d9f2182b6144a91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veronika=20Feh=C3=A9rv=C3=ADziov=C3=A1?= <128744051+VeronikaFeherviziova@users.noreply.github.com> Date: Sat, 1 Nov 2025 20:18:27 +0100 Subject: [PATCH 045/205] feat: add interface for new internship status objects --- frontend/app/types/internship_status.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frontend/app/types/internship_status.ts b/frontend/app/types/internship_status.ts index f952249..01ff86d 100644 --- a/frontend/app/types/internship_status.ts +++ b/frontend/app/types/internship_status.ts @@ -10,6 +10,12 @@ export interface InternshipStatusData { modified_by: User; }; +export interface NewInternshipStatusData { + internship_id: number; + status: InternshipStatus; + note: string; +}; + export enum InternshipStatus { SUBMITTED = 'SUBMITTED', CONFIRMED = 'CONFIRMED', From 0932584faa737efaf2064ff787bee6414c456cba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veronika=20Feh=C3=A9rv=C3=ADziov=C3=A1?= <128744051+VeronikaFeherviziova@users.noreply.github.com> Date: Sat, 1 Nov 2025 20:51:07 +0100 Subject: [PATCH 046/205] feat: add API for updating internship status --- .../InternshipStatusController.php | 64 ++++++++++++++++++- backend/routes/api.php | 1 + 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/backend/app/Http/Controllers/InternshipStatusController.php b/backend/app/Http/Controllers/InternshipStatusController.php index 2c81c41..f7b6a60 100644 --- a/backend/app/Http/Controllers/InternshipStatusController.php +++ b/backend/app/Http/Controllers/InternshipStatusController.php @@ -74,9 +74,40 @@ class InternshipStatusController extends Controller /** * Update the specified resource in storage. */ - public function update(Request $request, InternshipStatus $internshipStatus) + public function update(int $id, Request $request) { - // + $user = auth()->user(); + $internship = Internship::find($id); + + if(!$internship) { + return response()->json([ + 'message' => 'No such internship exists.' + ], 400); + } + + $company_contact = User::find($internship->contact); + + if ($user->role !== 'ADMIN' && $user->id !== $company_contact->id) { + abort(403, 'Unauthorized'); + } + + $internshipStatus = $this->currentInternshipStatus($internship); + $newStatusValidator = 'in:' . implode(',', $this->possibleNewStatuses($internshipStatus->status, $user->role)); + + $request->validate([ + 'status' => ['required', 'string', 'uppercase', $newStatusValidator], + 'note' => ['required', 'string', 'min:1'] + ]); + + InternshipStatus::create([ + 'internship_id' => $id, + 'status' => $request->status, + 'note' => $request->note, + 'changed' => now(), + 'modified_by' => $user->id + ]); + + return response()->noContent(); } /** @@ -86,4 +117,33 @@ class InternshipStatusController extends Controller { // } + + private function possibleNewStatuses(string $current_status, string $userRole) { + switch ($current_status) { + case 'SUBMITTED': + if ($userRole === 'EMPLOYER') { + return []; + } + return ['CONFIRMED', 'DENIED']; + case 'CONFIRMED': + if ($userRole === 'EMPLOYER') { + return ['DENIED']; + } + return ['SUBMITTED', 'DENIED', 'DEFENDED', 'NOT_DEFENDED']; + case 'DENIED': + if ($userRole === 'EMPLOYER') { + return ['CONFIRMED']; + } + return ['SUBMITTED', 'CONFIRMED']; + case 'DEFENDED': + case 'NOT_DEFENDED': + return []; + default: + throw new \InvalidArgumentException('Unknown status'); + } + } + + private function currentInternshipStatus(Internship $internship) { + return InternshipStatus::whereInternshipId($internship->id)->orderByDesc('changed')->firstOrFail(); + } } diff --git a/backend/routes/api.php b/backend/routes/api.php index fd1e876..e750141 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -32,6 +32,7 @@ Route::prefix('/internships')->group(function () { Route::middleware("auth:sanctum")->group(function () { Route::prefix('/{id}')->group(function () { Route::get("/", [InternshipController::class, 'get'])->name("api.internships.get"); + Route::put("/status", [InternshipStatusController::class, 'update'])->name("api.internships.status.update"); Route::get("/statuses", [InternshipStatusController::class, 'get'])->name("api.internships.get"); Route::post("/basic", [InternshipController::class, 'update_basic'])->name("api.internships.update.basic"); }); From 95c49f89b353877fcb9779f85926dd7a2b954f07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veronika=20Feh=C3=A9rv=C3=ADziov=C3=A1?= <128744051+VeronikaFeherviziova@users.noreply.github.com> Date: Sat, 1 Nov 2025 21:05:09 +0100 Subject: [PATCH 047/205] fix: internship status ordering --- backend/app/Http/Controllers/InternshipController.php | 6 +++--- backend/app/Http/Controllers/InternshipStatusController.php | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/app/Http/Controllers/InternshipController.php b/backend/app/Http/Controllers/InternshipController.php index dd9b385..1eeb796 100644 --- a/backend/app/Http/Controllers/InternshipController.php +++ b/backend/app/Http/Controllers/InternshipController.php @@ -37,7 +37,7 @@ class InternshipController extends Controller }); $internships->each(function ($internship) { - $internship->status = InternshipStatus::whereColumn('internship_id', '=', $internship->id)->get()->first()->makeHidden(['created_at', 'updated_at', 'id']); + $internship->status = InternshipStatus::whereColumn('internship_id', '=', $internship->id)->orderByDesc('changed')->get()->first()->makeHidden(['created_at', 'updated_at', 'id']); $internship->status->modified_by = User::find($internship->status->modified_by)->makeHidden(['created_at', 'updated_at', 'email_verified_at']); }); @@ -64,7 +64,7 @@ class InternshipController extends Controller }); $internships->each(function ($internship) { - $internship->status = InternshipStatus::whereColumn('internship_id', '=', $internship->id)->get()->first()->makeHidden(['created_at', 'updated_at', 'id']); + $internship->status = InternshipStatus::whereColumn('internship_id', '=', $internship->id)->orderByDesc('changed')->get()->first()->makeHidden(['created_at', 'updated_at', 'id']); $internship->status->modified_by = User::find($internship->status->modified_by)->makeHidden(['created_at', 'updated_at', 'email_verified_at']); }); @@ -97,7 +97,7 @@ class InternshipController extends Controller $internship->contact = User::find($internship->company->contact)->makeHidden(['created_at', 'updated_at', 'email_verified_at']); unset($internship->company->contact); - $internship->status = InternshipStatus::whereColumn('internship_id', '=', $internship->id)->get()->first()->makeHidden(['created_at', 'updated_at', 'id']); + $internship->status = InternshipStatus::whereColumn('internship_id', '=', $internship->id)->orderByDesc('changed')->get()->first()->makeHidden(['created_at', 'updated_at', 'id']); $internship->status->modified_by = User::find($internship->status->modified_by)->makeHidden(['created_at', 'updated_at', 'email_verified_at']); return response()->json($internship); diff --git a/backend/app/Http/Controllers/InternshipStatusController.php b/backend/app/Http/Controllers/InternshipStatusController.php index f7b6a60..a68f08f 100644 --- a/backend/app/Http/Controllers/InternshipStatusController.php +++ b/backend/app/Http/Controllers/InternshipStatusController.php @@ -11,7 +11,7 @@ class InternshipStatusController extends Controller { public function get(int $id) { $user = auth()->user(); - $internship_statuses = InternshipStatus::whereInternshipId($id)->get()->makeHidden(['created_at', 'updated_at', 'id']); + $internship_statuses = InternshipStatus::whereInternshipId($id)->orderByDesc('changed')->get()->makeHidden(['created_at', 'updated_at', 'id']); if(!$internship_statuses) { return response()->json([ From f75b9650d006c8e58c9388c8df840841377b2bcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veronika=20Feh=C3=A9rv=C3=ADziov=C3=A1?= <128744051+VeronikaFeherviziova@users.noreply.github.com> Date: Sat, 1 Nov 2025 21:05:34 +0100 Subject: [PATCH 048/205] refact: remove internship ID property from new internship type --- frontend/app/types/internship_status.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/app/types/internship_status.ts b/frontend/app/types/internship_status.ts index 01ff86d..6567eda 100644 --- a/frontend/app/types/internship_status.ts +++ b/frontend/app/types/internship_status.ts @@ -11,7 +11,6 @@ export interface InternshipStatusData { }; export interface NewInternshipStatusData { - internship_id: number; status: InternshipStatus; note: string; }; From 1040d2525d236a1ebb45f929a8ab5a4adf2fb280 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veronika=20Feh=C3=A9rv=C3=ADziov=C3=A1?= <128744051+VeronikaFeherviziova@users.noreply.github.com> Date: Sat, 1 Nov 2025 21:06:01 +0100 Subject: [PATCH 049/205] feat: add internship status editor component --- .../app/components/InternshipStatusEditor.vue | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 frontend/app/components/InternshipStatusEditor.vue diff --git a/frontend/app/components/InternshipStatusEditor.vue b/frontend/app/components/InternshipStatusEditor.vue new file mode 100644 index 0000000..b4037f2 --- /dev/null +++ b/frontend/app/components/InternshipStatusEditor.vue @@ -0,0 +1,81 @@ + + + + + \ No newline at end of file From 134563fe3a9bfdd301fb7bf9c8b2ceaa63309b48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veronika=20Feh=C3=A9rv=C3=ADziov=C3=A1?= <128744051+VeronikaFeherviziova@users.noreply.github.com> Date: Sat, 1 Nov 2025 21:06:29 +0100 Subject: [PATCH 050/205] feat: autorefresh internship statuses after change --- .../pages/dashboard/admin/internships/edit/[id].vue | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/frontend/app/pages/dashboard/admin/internships/edit/[id].vue b/frontend/app/pages/dashboard/admin/internships/edit/[id].vue index b5d2549..cef5c1d 100644 --- a/frontend/app/pages/dashboard/admin/internships/edit/[id].vue +++ b/frontend/app/pages/dashboard/admin/internships/edit/[id].vue @@ -40,7 +40,7 @@ async function handleUpdateOfBasicInfo(internship: NewInternship) { } } -const { data, error } = await useSanctumFetch(`/api/internships/${route.params.id}`); +const { data, error, refresh } = await useSanctumFetch(`/api/internships/${route.params.id}`); - - + \ No newline at end of file From 95b72c886639a6e3f41cb03a4431bfe35bec87ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veronika=20Feh=C3=A9rv=C3=ADziov=C3=A1?= <128744051+VeronikaFeherviziova@users.noreply.github.com> Date: Sat, 1 Nov 2025 22:13:56 +0100 Subject: [PATCH 053/205] refactor: simplify internship editor component --- frontend/app/components/InternshipEditor.vue | 54 ++++--------------- .../dashboard/admin/internships/edit/[id].vue | 4 +- 2 files changed, 12 insertions(+), 46 deletions(-) diff --git a/frontend/app/components/InternshipEditor.vue b/frontend/app/components/InternshipEditor.vue index bd496cf..add890c 100644 --- a/frontend/app/components/InternshipEditor.vue +++ b/frontend/app/components/InternshipEditor.vue @@ -1,6 +1,6 @@ + + diff --git a/frontend/app/pages/dashboard/admin/index.vue b/frontend/app/pages/dashboard/admin/index.vue index 5bffeee..1950239 100644 --- a/frontend/app/pages/dashboard/admin/index.vue +++ b/frontend/app/pages/dashboard/admin/index.vue @@ -23,6 +23,9 @@ const user = useSanctumUser();
+ + Študenti + Firmy diff --git a/frontend/app/pages/dashboard/admin/students/edit/[id].vue b/frontend/app/pages/dashboard/admin/students/edit/[id].vue new file mode 100644 index 0000000..43c7d44 --- /dev/null +++ b/frontend/app/pages/dashboard/admin/students/edit/[id].vue @@ -0,0 +1,137 @@ + + + + + diff --git a/frontend/app/pages/dashboard/admin/students/index.vue b/frontend/app/pages/dashboard/admin/students/index.vue new file mode 100644 index 0000000..d3eb4c7 --- /dev/null +++ b/frontend/app/pages/dashboard/admin/students/index.vue @@ -0,0 +1,91 @@ + + + + + From 3ff12fe57ec90886090bddee684cb5b2164d8361 Mon Sep 17 00:00:00 2001 From: dkecskes Date: Mon, 3 Nov 2025 00:35:11 +0100 Subject: [PATCH 060/205] feat: add student deletion functionality with confirmation dialog --- .../Auth/RegisteredUserController.php | 27 ++++- .../Controllers/StudentDataController.php | 66 +++++++++++++ backend/app/Models/Internship.php | 8 ++ backend/app/Models/User.php | 8 ++ backend/routes/api.php | 1 + .../dashboard/admin/students/edit/[id].vue | 91 +++++++++++++++++ .../pages/dashboard/admin/students/index.vue | 98 +++++++++++++++++++ 7 files changed, 295 insertions(+), 4 deletions(-) diff --git a/backend/app/Http/Controllers/Auth/RegisteredUserController.php b/backend/app/Http/Controllers/Auth/RegisteredUserController.php index 98509eb..ba2d421 100644 --- a/backend/app/Http/Controllers/Auth/RegisteredUserController.php +++ b/backend/app/Http/Controllers/Auth/RegisteredUserController.php @@ -26,7 +26,7 @@ class RegisteredUserController extends Controller $password = bin2hex(random_bytes(16)); $request->validate([ - 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class], + 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:' . User::class], 'first_name' => ['required', 'string', 'max:64'], 'last_name' => ['required', 'string', 'max:64'], 'phone' => ['required', 'string', 'max:13'], @@ -56,14 +56,14 @@ class RegisteredUserController extends Controller 'password' => Hash::make($password), ]); - if($user->role === "STUDENT") { + if ($user->role === "STUDENT") { StudentData::create([ 'user_id' => $user->id, 'address' => $request->student_data['address'], 'personal_email' => $request->student_data['personal_email'], 'study_field' => $request->student_data['study_field'], ]); - } else if($user->role === "EMPLOYER") { + } else if ($user->role === "EMPLOYER") { Company::create([ 'name' => $request->company_data['name'], 'address' => $request->company_data['address'], @@ -79,7 +79,8 @@ class RegisteredUserController extends Controller return response()->noContent(); } - public function reset_password(Request $request): Response { + public function reset_password(Request $request): Response + { $request->validate([ 'email' => ['required', 'string', 'lowercase', 'email', 'max:255'], ]); @@ -97,4 +98,22 @@ class RegisteredUserController extends Controller return response()->noContent(); } + + public function reset_password_2(Request $request): Response + { + $request->validate([ + 'id' => ['required', 'string', 'lowercase', 'email', 'max:255'], + 'password' => ['required', 'string', 'lowercase', 'email', 'max:255'], + ]); + + $user = User::whereEmail($request->email)->first(); + if (!$user) { + return response(status: 400); + } + + $user->password = Hash::make($request->password); + $user->save(); + + return response()->noContent(); + } } \ No newline at end of file diff --git a/backend/app/Http/Controllers/StudentDataController.php b/backend/app/Http/Controllers/StudentDataController.php index 1187b0d..6506fa3 100644 --- a/backend/app/Http/Controllers/StudentDataController.php +++ b/backend/app/Http/Controllers/StudentDataController.php @@ -4,7 +4,9 @@ namespace App\Http\Controllers; use App\Models\StudentData; use App\Models\User; +use App\Models\InternshipStatus; use Illuminate\Http\Request; +use Illuminate\Support\Facades\DB; class StudentDataController extends Controller { @@ -168,4 +170,68 @@ class StudentDataController extends Controller { // } + + /** + * Delete a student and all related data. + */ + public function delete(int $id) + { + $user = auth()->user(); + + // Admin kontrola + if ($user->role !== 'ADMIN') { + abort(403, 'Unauthorized'); + } + + $student = User::find($id); + + if (!$student) { + return response()->json([ + 'message' => 'No such student exists.' + ], 400); + } + + if ($student->role !== 'STUDENT') { + return response()->json([ + 'message' => 'User is not a student.' + ], 400); + } + + try { + DB::beginTransaction(); + + // 1. Získaj internship IDs + $internshipIds = $student->internships()->pluck('id')->toArray(); + + // 2. Vymaž internship statuses + if (!empty($internshipIds)) { + InternshipStatus::whereIn('internship_id', $internshipIds)->delete(); + } + + // 3. Vymaž internships + $student->internships()->delete(); + + // 4. Vymaž student_data + if ($student->studentData) { + $student->studentData()->delete(); + } + + // 5. Vymaž usera + $student->delete(); + + DB::commit(); + + return response()->json([ + 'message' => 'Student successfully deleted.' + ], 200); + + } catch (\Exception $e) { + DB::rollBack(); + + return response()->json([ + 'message' => 'Error deleting student.', + 'error' => $e->getMessage() + ], 500); + } + } } diff --git a/backend/app/Models/Internship.php b/backend/app/Models/Internship.php index a549885..17cd8e7 100644 --- a/backend/app/Models/Internship.php +++ b/backend/app/Models/Internship.php @@ -25,4 +25,12 @@ class Internship extends Model 'position_description', 'agreement', ]; + + /** + * Get the statuses for the internship. + */ + public function statuses() + { + return $this->hasMany(InternshipStatus::class, 'internship_id'); + } } diff --git a/backend/app/Models/User.php b/backend/app/Models/User.php index a052880..de3bb49 100644 --- a/backend/app/Models/User.php +++ b/backend/app/Models/User.php @@ -57,4 +57,12 @@ class User extends Authenticatable { return $this->hasOne(StudentData::class, 'user_id'); } + + /** + * Get the internships for the user. + */ + public function internships() + { + return $this->hasMany(Internship::class, 'user_id'); + } } diff --git a/backend/routes/api.php b/backend/routes/api.php index 8eef125..6af5e6e 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -26,6 +26,7 @@ Route::middleware(['auth:sanctum'])->prefix('/students')->group(function () { Route::get('/', [StudentDataController::class, 'all']); Route::get('/{id}', [StudentDataController::class, 'get']); Route::post('/{id}', [StudentDataController::class, 'update_all']); + Route::delete('/{id}', [StudentDataController::class, 'delete']); }); Route::post('/password-reset', [RegisteredUserController::class, 'reset_password']) diff --git a/frontend/app/pages/dashboard/admin/students/edit/[id].vue b/frontend/app/pages/dashboard/admin/students/edit/[id].vue index 43c7d44..5ebb8e4 100644 --- a/frontend/app/pages/dashboard/admin/students/edit/[id].vue +++ b/frontend/app/pages/dashboard/admin/students/edit/[id].vue @@ -14,6 +14,12 @@ const student = ref(null); const loading = ref(true); const saving = ref(false); +// Delete state +const deleteDialog = ref(false); +const deleteLoading = ref(false); +const deleteError = ref(null); +const deleteSuccess = ref(false); + const form = ref({ name: '', email: '', @@ -70,6 +76,49 @@ async function saveChanges() { function cancel() { navigateTo('/dashboard/admin/students'); } + +// Funkcia na otvorenie delete dialogu +const openDeleteDialog = () => { + deleteDialog.value = true; + deleteError.value = null; +}; + +// Funkcia na zatvorenie dialogu +const closeDeleteDialog = () => { + deleteDialog.value = false; + deleteError.value = null; + deleteSuccess.value = false; +}; + +// Funkcia na vymazanie študenta +const deleteStudent = async () => { + if (!studentId) return; + + deleteLoading.value = true; + deleteError.value = null; + + try { + await client(`/api/students/${studentId}`, { + method: 'DELETE' + }); + + deleteSuccess.value = true; + + // Presmeruj na zoznam po 1.5 sekundách + setTimeout(() => { + navigateTo('/dashboard/admin/students'); + }, 1500); + + } catch (e) { + if (e instanceof FetchError) { + deleteError.value = e.response?._data?.message || 'Chyba pri mazaní študenta.'; + } else { + deleteError.value = 'Neznáma chyba pri mazaní študenta.'; + } + } finally { + deleteLoading.value = false; + } +}; diff --git a/frontend/app/pages/dashboard/admin/students/index.vue b/frontend/app/pages/dashboard/admin/students/index.vue index d3eb4c7..ebd743a 100644 --- a/frontend/app/pages/dashboard/admin/students/index.vue +++ b/frontend/app/pages/dashboard/admin/students/index.vue @@ -22,8 +22,66 @@ const headers = [ { title: 'Operácie', key: 'ops', align: 'middle' }, ]; +const client = useSanctumClient(); + // Načítame všetkých študentov const { data: students, error } = await useSanctumFetch('/api/students'); + +// State pre delete dialog +const deleteDialog = ref(false); +const studentToDelete = ref(null); +const deleteLoading = ref(false); +const deleteError = ref(null); +const deleteSuccess = ref(false); + +// Funkcia na otvorenie delete dialogu +const openDeleteDialog = (student: User) => { + studentToDelete.value = student; + deleteDialog.value = true; + deleteError.value = null; +}; + +// Funkcia na zatvorenie dialogu +const closeDeleteDialog = () => { + deleteDialog.value = false; + studentToDelete.value = null; + deleteError.value = null; + deleteSuccess.value = false; +}; + +// Funkcia na vymazanie študenta +const deleteStudent = async () => { + if (!studentToDelete.value) return; + + deleteLoading.value = true; + deleteError.value = null; + + try { + await client(`/api/students/${studentToDelete.value.id}`, { + method: 'DELETE' + }); + + // Odstránime študenta zo zoznamu + if (students.value) { + const index = students.value.findIndex(s => s.id === studentToDelete.value!.id); + if (index > -1) { + students.value.splice(index, 1); + } + } + + deleteSuccess.value = true; + + // Zavri dialog po 1.5 sekundách + setTimeout(() => { + closeDeleteDialog(); + }, 1500); + + } catch (err: any) { + deleteError.value = err.data?.message || 'Chyba pri mazaní študenta.'; + } finally { + deleteLoading.value = false; + } +}; From f55b8bc58045dceb8c2fd691632b3fdb577c04cf Mon Sep 17 00:00:00 2001 From: Andrej Date: Mon, 3 Nov 2025 12:10:34 +0100 Subject: [PATCH 061/205] Create CreateGarant.php MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vytvorenie CLi príkazu na pridanie Granata (ADMINA) do databázy. --- backend/app/Console/Commands/CreateGarant.php | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 backend/app/Console/Commands/CreateGarant.php diff --git a/backend/app/Console/Commands/CreateGarant.php b/backend/app/Console/Commands/CreateGarant.php new file mode 100644 index 0000000..cff78db --- /dev/null +++ b/backend/app/Console/Commands/CreateGarant.php @@ -0,0 +1,59 @@ +info('=== Vytvorenie garanta (admin) ==='); + + // Načítanie údajov interaktívne + $firstName = $this->ask('Zadaj krstné meno'); + $lastName = $this->ask('Zadaj priezvisko'); + $email = $this->ask('Zadaj email'); + + // Kontrola duplicity emailu + if (User::where('email', $email)->exists()) { + $this->error('Používateľ s týmto emailom už existuje.'); + return 1; + } + + $password = $this->secret('Zadaj heslo (nebude sa zobrazovať)'); + $phone = $this->ask('Zadaj telefón '); + + // Vytvorenie používateľa + $user = User::create([ + 'name' => $firstName . ' ' . $lastName, + 'first_name' => $firstName, + 'last_name' => $lastName, + 'email' => $email, + 'phone' => $phone, + 'role' => 'ADMIN', + 'password' => Hash::make($password), + ]); + + $this->info("\n Garant {$user->first_name} {$user->last_name} bol úspešne vytvorený s rolou ADMIN."); + $this->info("Email: {$user->email}"); + + return 0; + } +} From 6bf5900533fc759be398684287a33c0b3ea761be Mon Sep 17 00:00:00 2001 From: br0kenpixel <23280129+br0kenpixel@users.noreply.github.com> Date: Mon, 3 Nov 2025 12:25:23 +0100 Subject: [PATCH 062/205] fix: `name` fields being updated instead of `first_name` and `last_name` --- .../Http/Controllers/CompanyController.php | 7 +++-- .../Controllers/StudentDataController.php | 7 +++-- .../dashboard/admin/companies/edit/[id].vue | 11 +++++-- .../dashboard/admin/students/edit/[id].vue | 30 ++++++++++--------- 4 files changed, 34 insertions(+), 21 deletions(-) diff --git a/backend/app/Http/Controllers/CompanyController.php b/backend/app/Http/Controllers/CompanyController.php index f8499d6..7b0222c 100644 --- a/backend/app/Http/Controllers/CompanyController.php +++ b/backend/app/Http/Controllers/CompanyController.php @@ -68,7 +68,8 @@ class CompanyController extends Controller 'address' => ['required', 'string', 'max:500'], 'ico' => ['required', 'integer'], 'hiring' => ['required', 'boolean'], - 'contact.name' => ['required', 'string', 'max:255'], + 'contact.first_name' => ['required', 'string', 'max:255'], + 'contact.last_name' => ['required', 'string', 'max:255'], 'contact.email' => ['required', 'email', 'max:255', 'unique:users,email,' . $company->contact], 'contact.phone' => ['nullable', 'string', 'max:20'], ]); @@ -87,7 +88,9 @@ class CompanyController extends Controller if ($contactPerson) { $contactPerson->update([ - 'name' => $request->contact['name'], + 'first_name' => $request->contact['first_name'], + 'last_name' => $request->contact['last_name'], + 'name' => $request->contact['first_name'] . ' ' . $request->contact['last_name'], 'email' => $request->contact['email'], 'phone' => $request->contact['phone'] ?? null, ]); diff --git a/backend/app/Http/Controllers/StudentDataController.php b/backend/app/Http/Controllers/StudentDataController.php index 1187b0d..0de7529 100644 --- a/backend/app/Http/Controllers/StudentDataController.php +++ b/backend/app/Http/Controllers/StudentDataController.php @@ -84,7 +84,8 @@ class StudentDataController extends Controller // Validácia dát $request->validate([ - 'name' => ['required', 'string', 'max:255'], + 'first_name' => ['required', 'string', 'max:255'], + 'last_name' => ['required', 'string', 'max:255'], 'email' => ['required', 'email', 'max:255', 'unique:users,email,' . $id], 'phone' => ['nullable', 'string', 'max:20'], 'student_data.study_field' => ['nullable', 'string', 'max:255'], @@ -94,7 +95,9 @@ class StudentDataController extends Controller // Aktualizácia User údajov $student->update([ - 'name' => $request->name, + 'name' => $request->first_name . ' ' . $request->last_name, + 'first_name' => $request->first_name, + 'last_name' => $request->last_name, 'email' => $request->email, 'phone' => $request->phone, ]); diff --git a/frontend/app/pages/dashboard/admin/companies/edit/[id].vue b/frontend/app/pages/dashboard/admin/companies/edit/[id].vue index f0cca5c..95462a3 100644 --- a/frontend/app/pages/dashboard/admin/companies/edit/[id].vue +++ b/frontend/app/pages/dashboard/admin/companies/edit/[id].vue @@ -19,7 +19,8 @@ const form = ref({ ico: 0, hiring: false, contact: { - name: '', + first_name: '', + last_name: '', email: '', phone: '' } @@ -34,7 +35,8 @@ watch(data, (newData) => { form.value.address = newData.address; form.value.ico = newData.ico; form.value.hiring = !!newData.hiring; - form.value.contact.name = newData.contact?.name; + form.value.contact.first_name = newData.contact?.first_name; + form.value.contact.last_name = newData.contact?.last_name; form.value.contact.email = newData.contact?.email; form.value.contact.phone = newData.contact?.phone; loading.value = false; @@ -102,7 +104,10 @@ function cancel() {

Kontaktná osoba

- + + (`/api/students/${studentId}`); + // Načítanie dát študenta watch(data, (newData) => { if (newData) { student.value = newData; - form.value = { - name: newData.name || '', - email: newData.email || '', - phone: newData.phone || '', - student_data: { - study_field: newData.student_data?.study_field || '', - personal_email: newData.student_data?.personal_email || '', - address: newData.student_data?.address || '' - } - }; + form.value.first_name = newData.first_name; + form.value.last_name = newData.last_name; + form.value.email = newData.email; + form.value.phone = newData.phone; + form.value.student_data.study_field = newData.student_data!.study_field; + form.value.student_data.personal_email = newData.student_data!.personal_email; + form.value.student_data.address = newData.student_data!.address; } navigateTo('/dashboard/admin/students'); -} -); +}); // Uloženie zmien async function saveChanges() { @@ -91,7 +90,10 @@ function cancel() { Základné údaje - + + Date: Mon, 3 Nov 2025 12:28:32 +0100 Subject: [PATCH 063/205] fix: data watcher not firing immediately and not disabling `loading` status in student editor --- frontend/app/pages/dashboard/admin/students/edit/[id].vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/app/pages/dashboard/admin/students/edit/[id].vue b/frontend/app/pages/dashboard/admin/students/edit/[id].vue index c95fe54..4d41eb8 100644 --- a/frontend/app/pages/dashboard/admin/students/edit/[id].vue +++ b/frontend/app/pages/dashboard/admin/students/edit/[id].vue @@ -40,10 +40,10 @@ watch(data, (newData) => { form.value.student_data.study_field = newData.student_data!.study_field; form.value.student_data.personal_email = newData.student_data!.personal_email; form.value.student_data.address = newData.student_data!.address; - } - navigateTo('/dashboard/admin/students'); -}); + loading.value = false; + } +}, { immediate: true }); // Uloženie zmien async function saveChanges() { From f467550d8deb5a1987988a05c464082abb62c9c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sofia=20Reh=C3=A1kov=C3=A1?= <125893846+sofiarehakova@users.noreply.github.com> Date: Mon, 3 Nov 2025 16:01:18 +0100 Subject: [PATCH 064/205] dokumenty admin --- .../dashboard/admin/internships/edit/[id].vue | 119 +++++++++++++++++- 1 file changed, 117 insertions(+), 2 deletions(-) diff --git a/frontend/app/pages/dashboard/admin/internships/edit/[id].vue b/frontend/app/pages/dashboard/admin/internships/edit/[id].vue index 11a5f93..0c15a7d 100644 --- a/frontend/app/pages/dashboard/admin/internships/edit/[id].vue +++ b/frontend/app/pages/dashboard/admin/internships/edit/[id].vue @@ -42,6 +42,36 @@ async function handleUpdateOfBasicInfo(internship: NewInternship) { } const { data, error, refresh } = await useSanctumFetch(`/api/internships/${route.params.id}`); + +// ---- helpery pre sekciu Dokumenty ---- +const docs = computed(() => { + const d: any = data.value ?? {} + return { + contract: d.documents?.contract ?? d.contract ?? null, + report: d.documents?.report ?? d.report ?? null, + } +}) + +function docUrl(doc: any) { + return doc?.url ?? doc?.download_url ?? doc?.link ?? null +} +function docName(doc: any) { + return doc?.fileName ?? doc?.filename ?? doc?.name ?? 'dokument.pdf' +} +function docSize(doc: any) { + const bytes = doc?.size ?? doc?.filesize ?? null + if (!bytes && bytes !== 0) return null + if (bytes < 1024) return `${bytes} B` + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB` + return `${(bytes / (1024 * 1024)).toFixed(1)} MB` +} +function docDate(doc: any) { + const dt = doc?.uploadedAt ?? doc?.created_at ?? doc?.uploaded_at ?? null + return dt ? new Date(dt).toLocaleString() : null +} +function docBy(doc: any) { + return doc?.uploadedBy?.name ?? doc?.uploaded_by?.name ?? doc?.uploaded_by ?? null +} + + \ No newline at end of file From 797915d597a09aa9815f0b4f07a1ed899c413911 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sofia=20Reh=C3=A1kov=C3=A1?= <125893846+sofiarehakova@users.noreply.github.com> Date: Mon, 3 Nov 2025 17:22:49 +0100 Subject: [PATCH 067/205] =?UTF-8?q?edit=20company=20str=C3=A1nky?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/app/pages/dashboard/company.vue | 367 ------------------ .../app/pages/dashboard/company/index.vue | 43 ++ 2 files changed, 43 insertions(+), 367 deletions(-) delete mode 100644 frontend/app/pages/dashboard/company.vue create mode 100644 frontend/app/pages/dashboard/company/index.vue diff --git a/frontend/app/pages/dashboard/company.vue b/frontend/app/pages/dashboard/company.vue deleted file mode 100644 index 5811343..0000000 --- a/frontend/app/pages/dashboard/company.vue +++ /dev/null @@ -1,367 +0,0 @@ - - - - - \ No newline at end of file diff --git a/frontend/app/pages/dashboard/company/index.vue b/frontend/app/pages/dashboard/company/index.vue new file mode 100644 index 0000000..6f2d8e2 --- /dev/null +++ b/frontend/app/pages/dashboard/company/index.vue @@ -0,0 +1,43 @@ + + + + + \ No newline at end of file From 3e1783f7333206476848739e68ef333c4b9e123e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sofia=20Reh=C3=A1kov=C3=A1?= <125893846+sofiarehakova@users.noreply.github.com> Date: Mon, 3 Nov 2025 17:23:08 +0100 Subject: [PATCH 068/205] =?UTF-8?q?vytvorenie=20internship=20podstr=C3=A1n?= =?UTF-8?q?ky=20pre=20company=20na=20editovanie=20vykazov=20praxe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/dashboard/company/internship.vue | 358 ++++++++++++++++++ 1 file changed, 358 insertions(+) create mode 100644 frontend/app/pages/dashboard/company/internship.vue diff --git a/frontend/app/pages/dashboard/company/internship.vue b/frontend/app/pages/dashboard/company/internship.vue new file mode 100644 index 0000000..83903a9 --- /dev/null +++ b/frontend/app/pages/dashboard/company/internship.vue @@ -0,0 +1,358 @@ + + + + + \ No newline at end of file From f32ce9fc99fe4e4f5e535d8bcd88292bc2c892cb Mon Sep 17 00:00:00 2001 From: br0kenpixel <23280129+br0kenpixel@users.noreply.github.com> Date: Mon, 3 Nov 2025 18:23:13 +0100 Subject: [PATCH 069/205] feat: simplify internship document viewer and add support for reports --- .../components/InternshipDocumentViewer.vue | 61 +++++++++ .../dashboard/admin/internships/edit/[id].vue | 118 +----------------- frontend/app/types/internships.ts | 2 + 3 files changed, 64 insertions(+), 117 deletions(-) create mode 100644 frontend/app/components/InternshipDocumentViewer.vue diff --git a/frontend/app/components/InternshipDocumentViewer.vue b/frontend/app/components/InternshipDocumentViewer.vue new file mode 100644 index 0000000..5e96484 --- /dev/null +++ b/frontend/app/components/InternshipDocumentViewer.vue @@ -0,0 +1,61 @@ + + + diff --git a/frontend/app/pages/dashboard/admin/internships/edit/[id].vue b/frontend/app/pages/dashboard/admin/internships/edit/[id].vue index 0c15a7d..df93096 100644 --- a/frontend/app/pages/dashboard/admin/internships/edit/[id].vue +++ b/frontend/app/pages/dashboard/admin/internships/edit/[id].vue @@ -42,36 +42,6 @@ async function handleUpdateOfBasicInfo(internship: NewInternship) { } const { data, error, refresh } = await useSanctumFetch(`/api/internships/${route.params.id}`); - -// ---- helpery pre sekciu Dokumenty ---- -const docs = computed(() => { - const d: any = data.value ?? {} - return { - contract: d.documents?.contract ?? d.contract ?? null, - report: d.documents?.report ?? d.report ?? null, - } -}) - -function docUrl(doc: any) { - return doc?.url ?? doc?.download_url ?? doc?.link ?? null -} -function docName(doc: any) { - return doc?.fileName ?? doc?.filename ?? doc?.name ?? 'dokument.pdf' -} -function docSize(doc: any) { - const bytes = doc?.size ?? doc?.filesize ?? null - if (!bytes && bytes !== 0) return null - if (bytes < 1024) return `${bytes} B` - if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB` - return `${(bytes / (1024 * 1024)).toFixed(1)} MB` -} -function docDate(doc: any) { - const dt = doc?.uploadedAt ?? doc?.created_at ?? doc?.uploaded_at ?? null - return dt ? new Date(dt).toLocaleString() : null -} -function docBy(doc: any) { - return doc?.uploadedBy?.name ?? doc?.uploaded_by?.name ?? doc?.uploaded_by ?? null -} From 28a79cf7c60cb5d5d0e264e889e6cbcf078e9f2e Mon Sep 17 00:00:00 2001 From: br0kenpixel <23280129+br0kenpixel@users.noreply.github.com> Date: Mon, 3 Nov 2025 19:54:22 +0100 Subject: [PATCH 074/205] fix: correct internship retrieval method in `get` function of `InternshipStatusController` --- backend/app/Http/Controllers/InternshipStatusController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/app/Http/Controllers/InternshipStatusController.php b/backend/app/Http/Controllers/InternshipStatusController.php index 3cee9dc..a25e20d 100644 --- a/backend/app/Http/Controllers/InternshipStatusController.php +++ b/backend/app/Http/Controllers/InternshipStatusController.php @@ -19,7 +19,7 @@ class InternshipStatusController extends Controller ], 400); } - $internship = Internship::where($id); + $internship = Internship::find($id); if ($user->role !== 'ADMIN' && $internship->user_id !== $user->id) { abort(403, 'Unauthorized'); } From 04acbf1b58b98b46cddcdd9d806b1c4a54f7c839 Mon Sep 17 00:00:00 2001 From: br0kenpixel <23280129+br0kenpixel@users.noreply.github.com> Date: Mon, 3 Nov 2025 19:55:00 +0100 Subject: [PATCH 075/205] feat: enhance internship edit page with status display and document upload functionality for students --- .../student/internship/edit/[id].vue | 83 +++++++++++++++++-- 1 file changed, 77 insertions(+), 6 deletions(-) diff --git a/frontend/app/pages/dashboard/student/internship/edit/[id].vue b/frontend/app/pages/dashboard/student/internship/edit/[id].vue index 81ac5a2..f330165 100644 --- a/frontend/app/pages/dashboard/student/internship/edit/[id].vue +++ b/frontend/app/pages/dashboard/student/internship/edit/[id].vue @@ -1,6 +1,7 @@ @@ -36,4 +102,9 @@ const { data, refresh } = await useSanctumFetch(`/api/internships/${ padding-right: 10px; padding-bottom: 10px; } + +hr { + margin-top: 20px; + margin-bottom: 20px; +} \ No newline at end of file From 6d2f57393db7fa1772c19704be56c7ebc92f5bb9 Mon Sep 17 00:00:00 2001 From: br0kenpixel <23280129+br0kenpixel@users.noreply.github.com> Date: Mon, 3 Nov 2025 20:00:58 +0100 Subject: [PATCH 076/205] refactor: update layout and styling of company dashboard page --- .../app/pages/dashboard/company/index.vue | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/frontend/app/pages/dashboard/company/index.vue b/frontend/app/pages/dashboard/company/index.vue index 6f2d8e2..085f377 100644 --- a/frontend/app/pages/dashboard/company/index.vue +++ b/frontend/app/pages/dashboard/company/index.vue @@ -18,26 +18,28 @@ const user = useSanctumUser(); \ No newline at end of file From cb5540ec8d4fca4372b1c1b0c05ab07a991a6819 Mon Sep 17 00:00:00 2001 From: br0kenpixel <23280129+br0kenpixel@users.noreply.github.com> Date: Mon, 3 Nov 2025 20:02:17 +0100 Subject: [PATCH 077/205] feat: rename internship route and replace `internship` page with `internships` page --- .../app/pages/dashboard/company/index.vue | 2 +- .../{internship.vue => internships/index.vue} | 200 ++++++++---------- 2 files changed, 85 insertions(+), 117 deletions(-) rename frontend/app/pages/dashboard/company/{internship.vue => internships/index.vue} (69%) diff --git a/frontend/app/pages/dashboard/company/index.vue b/frontend/app/pages/dashboard/company/index.vue index 085f377..ee77806 100644 --- a/frontend/app/pages/dashboard/company/index.vue +++ b/frontend/app/pages/dashboard/company/index.vue @@ -27,7 +27,7 @@ const user = useSanctumUser(); Môj profil - + Praxe diff --git a/frontend/app/pages/dashboard/company/internship.vue b/frontend/app/pages/dashboard/company/internships/index.vue similarity index 69% rename from frontend/app/pages/dashboard/company/internship.vue rename to frontend/app/pages/dashboard/company/internships/index.vue index 83903a9..c1fadb3 100644 --- a/frontend/app/pages/dashboard/company/internship.vue +++ b/frontend/app/pages/dashboard/company/internships/index.vue @@ -66,7 +66,7 @@ const students = ref([ { id: 2, name: 'Petra Kováčová', - existingReport: null, + existingReport: null, reportFile: null, localPreviewUrl: null, decision: null, @@ -181,26 +181,13 @@ async function submitFor(s: Student) {
- + Otvoriť - + Stiahnuť
@@ -210,43 +197,33 @@ async function submitFor(s: Student) { - -
Súbor: {{ s.existingReport.fileName }} ({{ formatSize(s.existingReport.size) }})
-
Nahraté: {{ new Date(s.existingReport.uploadedAt).toLocaleString() }}
+ +
Súbor: {{ s.existingReport.fileName }} ({{ + formatSize(s.existingReport.size) }})
+
Nahraté: {{ new + Date(s.existingReport.uploadedAt).toLocaleString() }}

Výkaz

- + accept=".pdf,application/pdf" prepend-icon="" label="Nahrať PDF výkaz" + variant="outlined" show-size clearable :disabled="s.loading" + hint="Povolené: PDF, max 10 MB" persistent-hint />
- + Otvoriť náhľad - + Stiahnuť vybraný súbor
@@ -254,20 +231,15 @@ async function submitFor(s: Student) {

Rozhodnutie

- + - +
@@ -277,20 +249,9 @@ async function submitFor(s: Student) {
- - + +
@@ -302,57 +263,64 @@ async function submitFor(s: Student) { \ No newline at end of file From ec463c98b0d9a06e647668892dbee2bcd3a93a02 Mon Sep 17 00:00:00 2001 From: br0kenpixel <23280129+br0kenpixel@users.noreply.github.com> Date: Mon, 3 Nov 2025 20:38:44 +0100 Subject: [PATCH 078/205] fix: update semester handling in internship submission --- frontend/app/components/InternshipEditor.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app/components/InternshipEditor.vue b/frontend/app/components/InternshipEditor.vue index add890c..2cdccbb 100644 --- a/frontend/app/components/InternshipEditor.vue +++ b/frontend/app/components/InternshipEditor.vue @@ -77,7 +77,7 @@ function triggerSubmit() { start: dateTimeFixup(form.value.start as any), end: dateTimeFixup(form.value.end as any), year_of_study: form.value.year_of_study, - semester: form.value.semester === "Zimný" ? "WINTER" : "SUMMER", + semester: form.value.semester, position_description: form.value.description }; From ee3418e6582f670e9f4477c24d6a6513a11f714e Mon Sep 17 00:00:00 2001 From: br0kenpixel <23280129+br0kenpixel@users.noreply.github.com> Date: Mon, 3 Nov 2025 20:44:46 +0100 Subject: [PATCH 079/205] feat: add company relationship to `Internship` model --- backend/app/Models/Internship.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/backend/app/Models/Internship.php b/backend/app/Models/Internship.php index 7161233..4974156 100644 --- a/backend/app/Models/Internship.php +++ b/backend/app/Models/Internship.php @@ -39,4 +39,9 @@ class Internship extends Model 'report_confirmed' => 'boolean', ]; } + + public function company() + { + return $this->belongsTo(Company::class, 'company_id'); + } } From 30973b2cedb39d3b7c4f46caae8bcf55e81ed738 Mon Sep 17 00:00:00 2001 From: br0kenpixel <23280129+br0kenpixel@users.noreply.github.com> Date: Mon, 3 Nov 2025 20:45:45 +0100 Subject: [PATCH 080/205] feat: update internship data retrieval permission checks --- .../Http/Controllers/InternshipController.php | 35 ++++++++++++++----- .../InternshipStatusController.php | 13 +++---- backend/routes/api.php | 2 +- 3 files changed, 33 insertions(+), 17 deletions(-) diff --git a/backend/app/Http/Controllers/InternshipController.php b/backend/app/Http/Controllers/InternshipController.php index 62b7bea..ee474f5 100644 --- a/backend/app/Http/Controllers/InternshipController.php +++ b/backend/app/Http/Controllers/InternshipController.php @@ -54,9 +54,28 @@ class InternshipController extends Controller return response()->json($internships); } - public function all_student() + public function all_my() { - $internships = Internship::where('user_id', auth()->id())->get()->makeHidden(['created_at', 'updated_at']); + $user = auth()->user(); + + if ($user->role === 'STUDENT') { + $internships = Internship::whereUserId($user->id)->get()->makeHidden(['created_at', 'updated_at']); + } elseif ($user->role === 'EMPLOYER') { + $company = Company::whereContact($user->id)->first(); + if (!$company) { + return response()->json(['message' => 'No company associated with this user.'], 404); + } + $internships = Internship::whereCompanyId($company->id)->get()->makeHidden(['created_at', 'updated_at']); + } else { + abort(403, 'Unauthorized'); + } + + if($user->role === "EMPLOYER") { + $internships->each(function ($internship) { + $internship->user = User::find($internship->user_id)->makeHidden(['created_at', 'updated_at', 'email_verified_at']); + unset($internship->user_id); + }); + } $internships->each(function ($internship) { $internship->company = Company::find($internship->company_id)->makeHidden(['created_at', 'updated_at']); @@ -97,16 +116,16 @@ class InternshipController extends Controller ], 400); } - if ($user->role !== 'ADMIN' && $internship->user_id !== $user->id) { - abort(403, 'Unauthorized'); - } - $internship->company = Company::find($internship->company_id)->makeHidden(['created_at', 'updated_at']); unset($internship->company_id); + if($user->role !== 'ADMIN' && $internship->user_id !== $user->id && $user->id !== $internship->company->contact) { + abort(403, 'Unauthorized'); + } + $internship->contact = User::find($internship->company->contact)->makeHidden(['created_at', 'updated_at', 'email_verified_at']); unset($internship->company->contact); - + $internship->status = InternshipStatus::whereColumn('internship_id', '=', $internship->id)->orderByDesc('changed')->get()->first()->makeHidden(['created_at', 'updated_at', 'id']); $internship->status->modified_by = User::find($internship->status->modified_by)->makeHidden(['created_at', 'updated_at', 'email_verified_at']); @@ -191,7 +210,7 @@ class InternshipController extends Controller ], 400); } - if ($user->role !== 'ADMIN' && $internship->user_id !== $user->id) { + if ($user->role !== 'ADMIN' && $internship->user_id !== $user->id && $user->id !== $internship->company->contact) { abort(403, 'Unauthorized'); } diff --git a/backend/app/Http/Controllers/InternshipStatusController.php b/backend/app/Http/Controllers/InternshipStatusController.php index a25e20d..2621faa 100644 --- a/backend/app/Http/Controllers/InternshipStatusController.php +++ b/backend/app/Http/Controllers/InternshipStatusController.php @@ -20,7 +20,7 @@ class InternshipStatusController extends Controller } $internship = Internship::find($id); - if ($user->role !== 'ADMIN' && $internship->user_id !== $user->id) { + if ($user->role !== 'ADMIN' && $internship->user_id !== $user->id && $user->id !== $internship->company->contact) { abort(403, 'Unauthorized'); } @@ -41,7 +41,7 @@ class InternshipStatusController extends Controller ], 400); } - if ($user->role !== 'ADMIN' && $internship->user_id !== $user->id && $user->id !== $internship->contact) { + if ($user->role !== 'ADMIN' && $internship->user_id !== $user->id && $user->id !== $internship->company->contact) { abort(403, 'Unauthorized'); } @@ -105,9 +105,7 @@ class InternshipStatusController extends Controller ], 400); } - $company_contact = User::find($internship->contact); - - if ($user->role !== 'ADMIN' && $user->id !== $company_contact->id) { + if ($user->role !== 'ADMIN' && $user->id !== $internship->company->contact) { abort(403, 'Unauthorized'); } @@ -139,11 +137,10 @@ class InternshipStatusController extends Controller } private function possibleNewStatuses(string $current_status, string $userRole) { + if($userRole === "STUDENT") return []; + switch ($current_status) { case 'SUBMITTED': - if ($userRole === 'EMPLOYER') { - return []; - } return ['CONFIRMED', 'DENIED']; case 'CONFIRMED': if ($userRole === 'EMPLOYER') { diff --git a/backend/routes/api.php b/backend/routes/api.php index 367f259..a25dc1f 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -34,7 +34,7 @@ Route::post('/password-reset', [RegisteredUserController::class, 'reset_password Route::prefix('/internships')->group(function () { Route::get("/", [InternshipController::class, 'all'])->name("api.internships"); - Route::get("/my", [InternshipController::class, 'all_student'])->name("api.internships.student"); + Route::get("/my", [InternshipController::class, 'all_my'])->name("api.internships.my"); Route::middleware("auth:sanctum")->group(function () { Route::prefix('/{id}')->group(function () { From 8915a7cec81b605e0001a6ac8dfb1d65feb45878 Mon Sep 17 00:00:00 2001 From: br0kenpixel <23280129+br0kenpixel@users.noreply.github.com> Date: Mon, 3 Nov 2025 20:46:04 +0100 Subject: [PATCH 081/205] feat: implement internship editing functionality for companies --- .../company/internships/edit/[id].vue | 116 ++++++ .../dashboard/company/internships/index.vue | 357 +++--------------- 2 files changed, 175 insertions(+), 298 deletions(-) create mode 100644 frontend/app/pages/dashboard/company/internships/edit/[id].vue diff --git a/frontend/app/pages/dashboard/company/internships/edit/[id].vue b/frontend/app/pages/dashboard/company/internships/edit/[id].vue new file mode 100644 index 0000000..74cac5d --- /dev/null +++ b/frontend/app/pages/dashboard/company/internships/edit/[id].vue @@ -0,0 +1,116 @@ + + + + + \ No newline at end of file diff --git a/frontend/app/pages/dashboard/company/internships/index.vue b/frontend/app/pages/dashboard/company/internships/index.vue index c1fadb3..7ff86fd 100644 --- a/frontend/app/pages/dashboard/company/internships/index.vue +++ b/frontend/app/pages/dashboard/company/internships/index.vue @@ -1,326 +1,87 @@ \ No newline at end of file From 69df349d3fbcddaf2ef31d431fafeee77fb0696e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sofia=20Reh=C3=A1kov=C3=A1?= <125893846+sofiarehakova@users.noreply.github.com> Date: Mon, 3 Nov 2025 22:56:35 +0100 Subject: [PATCH 082/205] potvrdenie vykazu a oprava databazy --- .../Http/Controllers/InternshipController.php | 25 +++++++++++-------- .../components/InternshipDocumentEditor.vue | 9 ++++++- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/backend/app/Http/Controllers/InternshipController.php b/backend/app/Http/Controllers/InternshipController.php index ee474f5..0be763f 100644 --- a/backend/app/Http/Controllers/InternshipController.php +++ b/backend/app/Http/Controllers/InternshipController.php @@ -37,7 +37,7 @@ class InternshipController extends Controller }); $internships->each(function ($internship) { - $internship->status = InternshipStatus::whereColumn('internship_id', '=', $internship->id)->orderByDesc('changed')->get()->first()->makeHidden(['created_at', 'updated_at', 'id']); + $internship->status = InternshipStatus::whereInternshipId($internship->id)->orderByDesc('changed')->get()->first()->makeHidden(['created_at', 'updated_at', 'id']); $internship->status->modified_by = User::find($internship->status->modified_by)->makeHidden(['created_at', 'updated_at', 'email_verified_at']); }); @@ -126,7 +126,7 @@ class InternshipController extends Controller $internship->contact = User::find($internship->company->contact)->makeHidden(['created_at', 'updated_at', 'email_verified_at']); unset($internship->company->contact); - $internship->status = InternshipStatus::whereColumn('internship_id', '=', $internship->id)->orderByDesc('changed')->get()->first()->makeHidden(['created_at', 'updated_at', 'id']); + $internship->status = InternshipStatus::whereInternshipId($internship->id)->orderByDesc('changed')->get()->first()->makeHidden(['created_at', 'updated_at', 'id']); $internship->status->modified_by = User::find($internship->status->modified_by)->makeHidden(['created_at', 'updated_at', 'email_verified_at']); $internship->agreement = $internship->agreement !== null; @@ -231,21 +231,16 @@ class InternshipController extends Controller ], 400); } - if ($user->role !== 'ADMIN' && $internship->user_id !== $user->id && $user->id !== $internship->contact) { + if ($internship->user_id !== $user->id && $user->id !== $internship->company->contact) { abort(403, 'Unauthorized'); } $request->validate([ 'agreement' => ['nullable', 'file', 'mimes:pdf', 'max:10240'], - 'report' => ['nullable', 'file', 'mimes:pdf', 'max:10240'] + 'report' => ['nullable', 'file', 'mimes:pdf', 'max:10240'], + 'report_confirmed' => ['required', 'boolean'], ]); - if (!$request->hasFile('agreement') && !$request->hasFile('report')) { - return response()->json([ - 'message' => 'At least one document (agreement or report) must be provided.' - ], 400); - } - if ($request->hasFile('agreement')) { $internship->agreement = file_get_contents($request->file('agreement')->getRealPath()); } @@ -254,6 +249,16 @@ class InternshipController extends Controller $internship->report = file_get_contents($request->file('report')->getRealPath()); } + if($user->role === 'EMPLOYER') { + if($request->report_confirmed && (!$internship->agreement || !$internship->report)) { + return response()->json([ + 'message' => 'Report cannot be confirmed without an agreement and report.' + ], 400); + } + + $internship->report_confirmed = $request->report_confirmed; + } + $internship->save(); return response()->noContent(); } diff --git a/frontend/app/components/InternshipDocumentEditor.vue b/frontend/app/components/InternshipDocumentEditor.vue index deaa7c0..965c699 100644 --- a/frontend/app/components/InternshipDocumentEditor.vue +++ b/frontend/app/components/InternshipDocumentEditor.vue @@ -1,6 +1,8 @@ From eb122c41ab1be5514412b53429d1b8f3d365cff6 Mon Sep 17 00:00:00 2001 From: Andrej Date: Tue, 4 Nov 2025 17:26:31 +0100 Subject: [PATCH 096/205] pridanie atributov pouzivatelovi pre aktivaciu uctu --- backend/app/Models/User.php | 6 ++++++ backend/database/factories/UserFactory.php | 2 ++ .../migrations/0001_01_01_000000_create_users_table.php | 3 +++ 3 files changed, 11 insertions(+) diff --git a/backend/app/Models/User.php b/backend/app/Models/User.php index de3bb49..c80bbdc 100644 --- a/backend/app/Models/User.php +++ b/backend/app/Models/User.php @@ -25,6 +25,9 @@ class User extends Authenticatable 'email', 'role', 'password', + 'active', + 'needs_password_change', + 'activation_token', ]; /** @@ -35,6 +38,7 @@ class User extends Authenticatable protected $hidden = [ 'password', 'remember_token', + 'activation_token', ]; /** @@ -47,6 +51,8 @@ class User extends Authenticatable return [ 'email_verified_at' => 'datetime', 'password' => 'hashed', + 'active' => 'boolean', + 'needs_password_change' => 'boolean' ]; } diff --git a/backend/database/factories/UserFactory.php b/backend/database/factories/UserFactory.php index 39a6cef..fdbf6e3 100644 --- a/backend/database/factories/UserFactory.php +++ b/backend/database/factories/UserFactory.php @@ -36,6 +36,8 @@ class UserFactory extends Factory 'email_verified_at' => now(), 'password' => static::$password ??= Hash::make('password'), 'remember_token' => Str::random(10), + 'active' => true, + 'needs_password_change' => false, ]; } diff --git a/backend/database/migrations/0001_01_01_000000_create_users_table.php b/backend/database/migrations/0001_01_01_000000_create_users_table.php index 05fb5d9..9004abd 100644 --- a/backend/database/migrations/0001_01_01_000000_create_users_table.php +++ b/backend/database/migrations/0001_01_01_000000_create_users_table.php @@ -17,6 +17,9 @@ return new class extends Migration $table->string('email')->unique(); $table->timestamp('email_verified_at')->nullable(); $table->string('password'); + $table->boolean('active')->default(false); + $table->boolean('needs_password_change')->default(false); + $table->string('activation_token')->nullable(); $table->rememberToken(); $table->timestamps(); }); From 3f2d2c6438f671601fcaa3fe05b71618128f5048 Mon Sep 17 00:00:00 2001 From: Andrej Date: Tue, 4 Nov 2025 17:27:08 +0100 Subject: [PATCH 097/205] overenie aktivneho uctu pri prihlasovani --- backend/app/Http/Requests/Auth/LoginRequest.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/backend/app/Http/Requests/Auth/LoginRequest.php b/backend/app/Http/Requests/Auth/LoginRequest.php index f5692bf..c99dde2 100644 --- a/backend/app/Http/Requests/Auth/LoginRequest.php +++ b/backend/app/Http/Requests/Auth/LoginRequest.php @@ -49,6 +49,15 @@ class LoginRequest extends FormRequest ]); } + // Check if the authenticated user's account is active + if (! Auth::user()->active) { + Auth::logout(); + + throw ValidationException::withMessages([ + 'email' => __('auth.inactive_account'), + ]); + } + RateLimiter::clear($this->throttleKey()); } From 7feba39bc93bac8bc67d39c3b934090d098c53b8 Mon Sep 17 00:00:00 2001 From: Andrej Date: Tue, 4 Nov 2025 17:27:44 +0100 Subject: [PATCH 098/205] emaily na aktivaciu uctu --- backend/app/Mail/UserAccountActivated.php | 58 +++++++++++++++++++ .../app/Mail/UserRegistrationCompleted.php | 8 +-- .../views/mail/activation/completed.blade.php | 8 +++ .../mail/registration/completed.blade.php | 7 ++- 4 files changed, 76 insertions(+), 5 deletions(-) create mode 100644 backend/app/Mail/UserAccountActivated.php create mode 100644 backend/resources/views/mail/activation/completed.blade.php diff --git a/backend/app/Mail/UserAccountActivated.php b/backend/app/Mail/UserAccountActivated.php new file mode 100644 index 0000000..a60ba9c --- /dev/null +++ b/backend/app/Mail/UserAccountActivated.php @@ -0,0 +1,58 @@ +name = $name; + } + + /** + * Get the message envelope. + */ + public function envelope(): Envelope + { + return new Envelope( + subject: 'User Account Activated', + ); + } + + /** + * Get the message content definition. + */ + public function content(): Content + { + return new Content( + view: 'mail.activation.completed', + with: [ + "name" => $this->name, + ] + ); + } + + /** + * Get the attachments for the message. + * + * @return array + */ + public function attachments(): array + { + return []; + } +} diff --git a/backend/app/Mail/UserRegistrationCompleted.php b/backend/app/Mail/UserRegistrationCompleted.php index f4c6a57..523343d 100644 --- a/backend/app/Mail/UserRegistrationCompleted.php +++ b/backend/app/Mail/UserRegistrationCompleted.php @@ -14,15 +14,15 @@ class UserRegistrationCompleted extends Mailable use Queueable, SerializesModels; private string $name; - private string $password; + private string $activation_token; /** * Create a new message instance. */ - public function __construct(string $name, string $password) + public function __construct(string $name, string $activation_token) { $this->name = $name; - $this->password = $password; + $this->activation_token = $activation_token; } /** @@ -44,7 +44,7 @@ class UserRegistrationCompleted extends Mailable view: 'mail.registration.completed', with: [ "name" => $this->name, - "password" => $this->password + "activation_token" => $this->activation_token ] ); } diff --git a/backend/resources/views/mail/activation/completed.blade.php b/backend/resources/views/mail/activation/completed.blade.php new file mode 100644 index 0000000..d6212b4 --- /dev/null +++ b/backend/resources/views/mail/activation/completed.blade.php @@ -0,0 +1,8 @@ +@include("parts.header") +

Vážená/ý {{ $name }},

+

úspešne ste aktivovali váš účet!

+
+ +

s pozdravom

+

Systém ISOP UKF

+@include("parts.footer") \ No newline at end of file diff --git a/backend/resources/views/mail/registration/completed.blade.php b/backend/resources/views/mail/registration/completed.blade.php index a712bc7..b35e6f6 100644 --- a/backend/resources/views/mail/registration/completed.blade.php +++ b/backend/resources/views/mail/registration/completed.blade.php @@ -3,7 +3,12 @@

vaša registrácia do systému ISOP UKF prebehla úspešne!


-

Vaše heslo je: {{ $password }}

+

Aktivujte účet pomocou nasledujúceho linku:

+
+

+ {{ config('app.frontend_url') }}/account/activation/{{ $activation_token }} +


From fcf5ca373bfe1f419427011edfc1b2273242aa65 Mon Sep 17 00:00:00 2001 From: Andrej Date: Tue, 4 Nov 2025 17:28:27 +0100 Subject: [PATCH 099/205] pridanie API na aktivaciu uctu --- .../Auth/RegisteredUserController.php | 26 ++++++++++++++++++- backend/routes/api.php | 4 +++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/backend/app/Http/Controllers/Auth/RegisteredUserController.php b/backend/app/Http/Controllers/Auth/RegisteredUserController.php index 00d2649..6b5937a 100644 --- a/backend/app/Http/Controllers/Auth/RegisteredUserController.php +++ b/backend/app/Http/Controllers/Auth/RegisteredUserController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; +use App\Mail\UserAccountActivated; use App\Mail\UserPasswordReset; use App\Mail\UserRegistrationCompleted; use App\Models\Company; @@ -25,6 +26,7 @@ class RegisteredUserController extends Controller public function store(Request $request): Response { $password = bin2hex(random_bytes(16)); + $activation_token = bin2hex(random_bytes(16)); $request->validate([ 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:' . User::class], @@ -58,6 +60,7 @@ class RegisteredUserController extends Controller 'phone' => $request->phone, 'role' => $request->role, 'password' => Hash::make($password), + 'activation_token' => $activation_token ]); if ($user->role === "STUDENT") { @@ -83,12 +86,33 @@ class RegisteredUserController extends Controller throw $e; } - Mail::to($user)->sendNow(new UserRegistrationCompleted($user->name, $password)); + Mail::to($user)->sendNow(new UserRegistrationCompleted($user->name, $activation_token)); event(new Registered($user)); return response()->noContent(); } + public function activate(Request $request) { + $request->validate([ + 'token' => ['required', 'string', 'exists:users,activation_token'], + 'password' => ['required', 'string', 'min:8'], + ]); + + $user = User::where('activation_token', '=', $request->token)->first(); + + if (!$user) { + return response()->json(['message' => 'Invalid activation token'], 400); + } + + $user->active = true; + $user->activation_token = null; + $user->password = Hash::make($request->password); + $user->save(); + + Mail::to($user)->sendNow(new UserAccountActivated($user->name)); + return response()->noContent(); + } + public function reset_password(Request $request): Response { $request->validate([ diff --git a/backend/routes/api.php b/backend/routes/api.php index 3190b4d..ccae435 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -22,6 +22,10 @@ Route::middleware(['auth:sanctum'])->get('/user', function (Request $request) { return $user; }); +Route::prefix('/account')->group(function () { + Route::post("/activate", [RegisteredUserController::class, 'activate']); +}); + Route::middleware(['auth:sanctum'])->prefix('/students')->group(function () { Route::get('/', [StudentDataController::class, 'all']); Route::get('/{id}', [StudentDataController::class, 'get']); From 8ffac4975afb1b36501ce47bf947a65733e236cf Mon Sep 17 00:00:00 2001 From: Andrej Date: Tue, 4 Nov 2025 17:28:42 +0100 Subject: [PATCH 100/205] pridanie stranky na aktivaciu uctu --- .../app/pages/account/activation/[token].vue | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 frontend/app/pages/account/activation/[token].vue diff --git a/frontend/app/pages/account/activation/[token].vue b/frontend/app/pages/account/activation/[token].vue new file mode 100644 index 0000000..b7e09c5 --- /dev/null +++ b/frontend/app/pages/account/activation/[token].vue @@ -0,0 +1,138 @@ + + + + + From eeac19799dd906ea5b538dd0e22a870957e4ced2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sofia=20Reh=C3=A1kov=C3=A1?= <125893846+sofiarehakova@users.noreply.github.com> Date: Tue, 4 Nov 2025 18:09:48 +0100 Subject: [PATCH 101/205] pridanie tlacitka "moj profil" pre admina --- frontend/app/pages/dashboard/admin/index.vue | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/app/pages/dashboard/admin/index.vue b/frontend/app/pages/dashboard/admin/index.vue index 1fbfcc2..f8019d0 100644 --- a/frontend/app/pages/dashboard/admin/index.vue +++ b/frontend/app/pages/dashboard/admin/index.vue @@ -32,6 +32,9 @@ const user = useSanctumUser(); Praxe + + Môj profil +
From fedbb047280ddf8b82699546f106cff7c598a5f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sofia=20Reh=C3=A1kov=C3=A1?= <125893846+sofiarehakova@users.noreply.github.com> Date: Tue, 4 Nov 2025 18:10:11 +0100 Subject: [PATCH 102/205] presunutie tlacitka "moj profil" pre firmu --- frontend/app/pages/dashboard/company/index.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/app/pages/dashboard/company/index.vue b/frontend/app/pages/dashboard/company/index.vue index 56af26a..cb064d3 100644 --- a/frontend/app/pages/dashboard/company/index.vue +++ b/frontend/app/pages/dashboard/company/index.vue @@ -24,12 +24,12 @@ const user = useSanctumUser();
- - Môj profil - Praxe + + Môj profil +
From d41df106e4d58b0dd3760a8a60cffe92deecf507 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sofia=20Reh=C3=A1kov=C3=A1?= <125893846+sofiarehakova@users.noreply.github.com> Date: Tue, 4 Nov 2025 18:10:39 +0100 Subject: [PATCH 103/205] upravenie cesty tlacitka "moj profil" pre studenta --- frontend/app/pages/dashboard/student/index.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app/pages/dashboard/student/index.vue b/frontend/app/pages/dashboard/student/index.vue index 7388f91..b4f36f9 100644 --- a/frontend/app/pages/dashboard/student/index.vue +++ b/frontend/app/pages/dashboard/student/index.vue @@ -44,7 +44,7 @@ const { data, error } = await useSanctumFetch('/api/internships/my Firmy - + Môj profil From d5ccbd2df9602587bb3ed07e0abc9156e3d272ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sofia=20Reh=C3=A1kov=C3=A1?= <125893846+sofiarehakova@users.noreply.github.com> Date: Tue, 4 Nov 2025 18:10:56 +0100 Subject: [PATCH 104/205] presunutie profilovej stranky --- frontend/app/pages/account/index.vue | 118 +++++++++++++++++ .../pages/dashboard/student/profile/index.vue | 124 ------------------ 2 files changed, 118 insertions(+), 124 deletions(-) create mode 100644 frontend/app/pages/account/index.vue delete mode 100644 frontend/app/pages/dashboard/student/profile/index.vue diff --git a/frontend/app/pages/account/index.vue b/frontend/app/pages/account/index.vue new file mode 100644 index 0000000..1da7911 --- /dev/null +++ b/frontend/app/pages/account/index.vue @@ -0,0 +1,118 @@ + + + + + diff --git a/frontend/app/pages/dashboard/student/profile/index.vue b/frontend/app/pages/dashboard/student/profile/index.vue deleted file mode 100644 index 136a707..0000000 --- a/frontend/app/pages/dashboard/student/profile/index.vue +++ /dev/null @@ -1,124 +0,0 @@ - - - - - From 897bd0ff930bccaad8e316d5a146c81971d97bd7 Mon Sep 17 00:00:00 2001 From: dkecskes Date: Tue, 4 Nov 2025 18:43:20 +0100 Subject: [PATCH 105/205] Refactor password change method --- .../Controllers/Auth/RegisteredUserController.php | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/backend/app/Http/Controllers/Auth/RegisteredUserController.php b/backend/app/Http/Controllers/Auth/RegisteredUserController.php index f551047..13c9e3c 100644 --- a/backend/app/Http/Controllers/Auth/RegisteredUserController.php +++ b/backend/app/Http/Controllers/Auth/RegisteredUserController.php @@ -92,14 +92,15 @@ class RegisteredUserController extends Controller return response()->noContent(); } - public function activate(Request $request) { + public function activate(Request $request) + { $request->validate([ 'token' => ['required', 'string', 'exists:users,activation_token'], 'password' => ['required', 'string', 'min:8'], ]); $user = User::where('activation_token', '=', $request->token)->first(); - + if (!$user) { return response()->json(['message' => 'Invalid activation token'], 400); } @@ -155,17 +156,13 @@ class RegisteredUserController extends Controller { $user = auth()->user(); - if ($user->role !== 'STUDENT') { - return response()->json(['message' => 'Only students...'], 403); - } - $request->validate([ - 'password' => ['required', 'string', 'min:8', 'confirmed'], + 'password' => ['required', 'string', 'min:8'], ]); $user->password = Hash::make($request->password); $user->save(); - return response()->json(['message' => 'Password successfully changed.']); + return response()->noContent(); } } \ No newline at end of file From 66599f65f89e490f079d7f0efb9835c7f8175498 Mon Sep 17 00:00:00 2001 From: dkecskes Date: Tue, 4 Nov 2025 18:43:31 +0100 Subject: [PATCH 106/205] Move change-password route to account prefix --- backend/routes/api.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/routes/api.php b/backend/routes/api.php index 486c72c..7d6d64b 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -24,10 +24,10 @@ Route::middleware(['auth:sanctum'])->get('/user', function (Request $request) { Route::prefix('/account')->group(function () { Route::post("/activate", [RegisteredUserController::class, 'activate']); + Route::post('/change-password', [RegisteredUserController::class, 'change_password']); }); Route::middleware(['auth:sanctum'])->prefix('/students')->group(function () { - Route::post('/change-password', [RegisteredUserController::class, 'change_password']); Route::get('/', [StudentDataController::class, 'all']); Route::get('/{id}', [StudentDataController::class, 'get']); Route::post('/{id}', [StudentDataController::class, 'update_all']); From 0bbc65803477fb86def584a556b1d408495eb96b Mon Sep 17 00:00:00 2001 From: dkecskes Date: Tue, 4 Nov 2025 18:43:41 +0100 Subject: [PATCH 107/205] Refactor password change functionality: move to separate change-password component and update navigation link --- frontend/app/pages/account.vue | 115 ------------------ .../app/pages/account/change-password.vue | 95 +++++++++++++++ frontend/app/pages/account/index.vue | 2 +- 3 files changed, 96 insertions(+), 116 deletions(-) delete mode 100644 frontend/app/pages/account.vue create mode 100644 frontend/app/pages/account/change-password.vue diff --git a/frontend/app/pages/account.vue b/frontend/app/pages/account.vue deleted file mode 100644 index 8932e48..0000000 --- a/frontend/app/pages/account.vue +++ /dev/null @@ -1,115 +0,0 @@ - - - \ No newline at end of file diff --git a/frontend/app/pages/account/change-password.vue b/frontend/app/pages/account/change-password.vue new file mode 100644 index 0000000..b5a6c79 --- /dev/null +++ b/frontend/app/pages/account/change-password.vue @@ -0,0 +1,95 @@ + + + + + \ No newline at end of file diff --git a/frontend/app/pages/account/index.vue b/frontend/app/pages/account/index.vue index 1da7911..d438249 100644 --- a/frontend/app/pages/account/index.vue +++ b/frontend/app/pages/account/index.vue @@ -80,7 +80,7 @@ const user = useSanctumUser(); - + Zmeniť heslo From fc0ce811c53e324d6fb8ec027a623cf3dd59cf66 Mon Sep 17 00:00:00 2001 From: br0kenpixel <23280129+br0kenpixel@users.noreply.github.com> Date: Wed, 5 Nov 2025 09:10:42 +0100 Subject: [PATCH 108/205] refactor: put alerts into separate components --- frontend/app/components/ErrorAlert.vue | 21 +++++++++++++++++++ frontend/app/components/InfoAlert.vue | 20 ++++++++++++++++++ .../components/InternshipDocumentEditor.vue | 16 ++++++-------- .../components/InternshipDocumentViewer.vue | 13 ++++++------ frontend/app/components/InternshipEditor.vue | 6 ++---- .../app/components/InternshipStatusEditor.vue | 10 ++++----- .../InternshipStatusHistoryView.vue | 6 ++---- frontend/app/components/LoadingAlert.vue | 5 +++++ frontend/app/components/SuccessAlert.vue | 20 ++++++++++++++++++ frontend/app/components/WarningAlert.vue | 21 +++++++++++++++++++ .../app/pages/account/activation/[token].vue | 11 ++++------ .../app/pages/account/change-password.vue | 8 ++----- .../dashboard/admin/companies/edit/[id].vue | 8 ++----- .../pages/dashboard/admin/companies/index.vue | 7 ++----- .../dashboard/admin/internships/edit/[id].vue | 9 +++----- .../dashboard/admin/internships/index.vue | 5 ++--- .../dashboard/admin/students/edit/[id].vue | 8 ++----- .../pages/dashboard/admin/students/index.vue | 10 +++------ .../company/internships/edit/[id].vue | 15 ++++++------- .../dashboard/company/internships/index.vue | 3 +-- .../app/pages/dashboard/student/companies.vue | 3 +-- .../app/pages/dashboard/student/index.vue | 3 +-- .../dashboard/student/internship/create.vue | 6 ++---- .../student/internship/edit/[id].vue | 15 ++++++------- frontend/app/pages/login.vue | 6 ++---- frontend/app/pages/register/company.vue | 6 ++---- frontend/app/pages/register/student.vue | 6 ++---- frontend/app/pages/reset_psw/index.vue | 6 ++---- frontend/app/pages/reset_psw/request_sent.vue | 3 +-- 29 files changed, 153 insertions(+), 123 deletions(-) create mode 100644 frontend/app/components/ErrorAlert.vue create mode 100644 frontend/app/components/InfoAlert.vue create mode 100644 frontend/app/components/LoadingAlert.vue create mode 100644 frontend/app/components/SuccessAlert.vue create mode 100644 frontend/app/components/WarningAlert.vue diff --git a/frontend/app/components/ErrorAlert.vue b/frontend/app/components/ErrorAlert.vue new file mode 100644 index 0000000..140fa48 --- /dev/null +++ b/frontend/app/components/ErrorAlert.vue @@ -0,0 +1,21 @@ + + + \ No newline at end of file diff --git a/frontend/app/components/InfoAlert.vue b/frontend/app/components/InfoAlert.vue new file mode 100644 index 0000000..ae6ef21 --- /dev/null +++ b/frontend/app/components/InfoAlert.vue @@ -0,0 +1,20 @@ + + + \ No newline at end of file diff --git a/frontend/app/components/InternshipDocumentEditor.vue b/frontend/app/components/InternshipDocumentEditor.vue index 903a9ce..6f383ed 100644 --- a/frontend/app/components/InternshipDocumentEditor.vue +++ b/frontend/app/components/InternshipDocumentEditor.vue @@ -80,26 +80,23 @@ async function onSubmit() {