You've already forked isop-mirror
Merge branch 'develop' into feature/docker
This commit is contained in:
116
.github/workflows/cypress.yml
vendored
Normal file
116
.github/workflows/cypress.yml
vendored
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
name: Cypress E2E Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
- feature/ci-cypress-tests-08-11-2025
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cypress-tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
services:
|
||||||
|
mariadb:
|
||||||
|
image: mariadb:latest
|
||||||
|
env:
|
||||||
|
MYSQL_ROOT_PASSWORD: password
|
||||||
|
MYSQL_DATABASE: isop_test
|
||||||
|
ports:
|
||||||
|
- 3306:3306
|
||||||
|
options: --health-cmd="healthcheck.sh --connect --innodb_initialized" --health-interval=10s --health-timeout=5s --health-retries=3
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
cache: 'npm'
|
||||||
|
cache-dependency-path: frontend/package-lock.json
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: '8.2'
|
||||||
|
extensions: mbstring, xml, ctype, json, mysql, pdo, pdo_mysql
|
||||||
|
coverage: none
|
||||||
|
|
||||||
|
- name: Install Composer dependencies
|
||||||
|
working-directory: ./backend
|
||||||
|
run: composer install --prefer-dist --no-progress --no-suggest
|
||||||
|
|
||||||
|
- name: Copy .env file
|
||||||
|
working-directory: ./backend
|
||||||
|
run: |
|
||||||
|
cp .env.example .env
|
||||||
|
php artisan key:generate
|
||||||
|
|
||||||
|
- name: Configure database
|
||||||
|
working-directory: ./backend
|
||||||
|
run: |
|
||||||
|
DB_CONNECTION=mariadb >> .env
|
||||||
|
echo "DB_HOST=127.0.0.1" >> .env
|
||||||
|
echo "DB_PORT=3306" >> .env
|
||||||
|
echo "DB_DATABASE=isop_test" >> .env
|
||||||
|
echo "DB_USERNAME=root" >> .env
|
||||||
|
echo "DB_PASSWORD=password" >> .env
|
||||||
|
|
||||||
|
- name: Print backend .env file
|
||||||
|
working-directory: ./backend
|
||||||
|
run: cat .env
|
||||||
|
|
||||||
|
- name: Run migrations
|
||||||
|
working-directory: ./backend
|
||||||
|
run: php artisan migrate --force
|
||||||
|
|
||||||
|
- name: Run seeders
|
||||||
|
working-directory: ./backend
|
||||||
|
run: php artisan db:seed
|
||||||
|
|
||||||
|
- name: Start Laravel server
|
||||||
|
working-directory: ./backend
|
||||||
|
run: php artisan serve --host=0.0.0.0 --port=8000 &
|
||||||
|
|
||||||
|
- name: Install frontend dependencies
|
||||||
|
working-directory: ./frontend
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Build frontend
|
||||||
|
working-directory: ./frontend
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Start frontend server
|
||||||
|
working-directory: ./frontend
|
||||||
|
run: npm run preview &
|
||||||
|
env:
|
||||||
|
PORT: 3000
|
||||||
|
|
||||||
|
- name: Wait for servers to be ready
|
||||||
|
run: |
|
||||||
|
timeout 15 bash -c 'until curl -f http://localhost:8000/sanctum/csrf-cookie; do sleep 2; done'
|
||||||
|
timeout 15 bash -c 'until curl -f http://localhost:3000; do sleep 2; done'
|
||||||
|
|
||||||
|
- name: Run Cypress tests
|
||||||
|
working-directory: ./frontend
|
||||||
|
run: npx cypress run
|
||||||
|
env:
|
||||||
|
CYPRESS_BASE_URL: http://localhost:3000
|
||||||
|
|
||||||
|
- name: Upload Cypress screenshots
|
||||||
|
if: failure()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: cypress-screenshots
|
||||||
|
path: frontend/cypress/screenshots
|
||||||
|
if-no-files-found: ignore
|
||||||
|
|
||||||
|
- name: Upload Cypress videos
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: cypress-videos
|
||||||
|
path: frontend/cypress/videos
|
||||||
|
if-no-files-found: ignore
|
||||||
8
README.md
Normal file
8
README.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Informačný systém odbornej praxe - ISOP
|
||||||
|
|
||||||
|
## CI Status
|
||||||
|
|
||||||
|
| Badge | Poznámka |
|
||||||
|
|-------|-------|
|
||||||
|
| [](https://github.com/isop-ukf/isop-app/actions/workflows/cypress.yml) | Kompletný test aplikácie |
|
||||||
|
|
||||||
61
backend/app/Console/Commands/CreateAdmin.php
Normal file
61
backend/app/Console/Commands/CreateAdmin.php
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use App\Models\User;
|
||||||
|
use Hash;
|
||||||
|
|
||||||
|
class CreateAdmin extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'app:create-admin';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Create a new admin user';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$this->info('=== Create Admin ===');
|
||||||
|
|
||||||
|
// Načítanie údajov interaktívne
|
||||||
|
$firstName = $this->ask('First name');
|
||||||
|
$lastName = $this->ask('Last name');
|
||||||
|
$email = $this->ask('E-mail');
|
||||||
|
|
||||||
|
// Kontrola duplicity emailu
|
||||||
|
if (User::where('email', $email)->exists()) {
|
||||||
|
$this->error('A user with the same email already exists.');
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$password = $this->secret('Enter password');
|
||||||
|
$phone = $this->ask('Enter phone number');
|
||||||
|
|
||||||
|
// Vytvorenie používateľa
|
||||||
|
$user = User::create([
|
||||||
|
'name' => $firstName . ' ' . $lastName,
|
||||||
|
'first_name' => $firstName,
|
||||||
|
'last_name' => $lastName,
|
||||||
|
'email' => $email,
|
||||||
|
'password' => Hash::make($password),
|
||||||
|
'active' => true,
|
||||||
|
'role' => 'ADMIN',
|
||||||
|
'phone' => $phone,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->info("\n Admin {$user->first_name} {$user->last_name} created.");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
22
backend/app/Enums/InternshipStatus.php
Normal file
22
backend/app/Enums/InternshipStatus.php
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
enum InternshipStatus: string
|
||||||
|
{
|
||||||
|
case SUBMITTED = 'SUBMITTED';
|
||||||
|
|
||||||
|
case CONFIRMED_BY_COMPANY = 'CONFIRMED_BY_COMPANY';
|
||||||
|
case CONFIRMED_BY_ADMIN = 'CONFIRMED_BY_ADMIN';
|
||||||
|
|
||||||
|
case DENIED_BY_COMPANY = 'DENIED_BY_COMPANY';
|
||||||
|
case DENIED_BY_ADMIN = 'DENIED_BY_ADMIN';
|
||||||
|
|
||||||
|
case DEFENDED = 'DEFENDED';
|
||||||
|
case NOT_DEFENDED = 'NOT_DEFENDED';
|
||||||
|
|
||||||
|
public static function all(): array
|
||||||
|
{
|
||||||
|
return array_map(fn($case) => $case->value, self::cases());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,11 +3,13 @@
|
|||||||
namespace App\Http\Controllers\Auth;
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Mail\UserAccountActivated;
|
||||||
use App\Mail\UserPasswordReset;
|
use App\Mail\UserPasswordReset;
|
||||||
use App\Mail\UserRegistrationCompleted;
|
use App\Mail\UserRegistrationCompleted;
|
||||||
use App\Models\Company;
|
use App\Models\Company;
|
||||||
use App\Models\StudentData;
|
use App\Models\StudentData;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use DB;
|
||||||
use Illuminate\Auth\Events\Registered;
|
use Illuminate\Auth\Events\Registered;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
@@ -24,9 +26,10 @@ class RegisteredUserController extends Controller
|
|||||||
public function store(Request $request): Response
|
public function store(Request $request): Response
|
||||||
{
|
{
|
||||||
$password = bin2hex(random_bytes(16));
|
$password = bin2hex(random_bytes(16));
|
||||||
|
$activation_token = bin2hex(random_bytes(16));
|
||||||
|
|
||||||
$request->validate([
|
$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'],
|
'first_name' => ['required', 'string', 'max:64'],
|
||||||
'last_name' => ['required', 'string', 'max:64'],
|
'last_name' => ['required', 'string', 'max:64'],
|
||||||
'phone' => ['required', 'string', 'max:13'],
|
'phone' => ['required', 'string', 'max:13'],
|
||||||
@@ -46,47 +49,82 @@ class RegisteredUserController extends Controller
|
|||||||
'company_data.hiring' => ['required_if:role,EMPLOYER', 'boolean'],
|
'company_data.hiring' => ['required_if:role,EMPLOYER', 'boolean'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$user = User::create([
|
DB::beginTransaction();
|
||||||
'email' => $request->email,
|
|
||||||
'first_name' => $request->first_name,
|
|
||||||
'last_name' => $request->last_name,
|
|
||||||
'name' => "{$request->first_name} {$request->last_name}",
|
|
||||||
'phone' => $request->phone,
|
|
||||||
'role' => $request->role,
|
|
||||||
'password' => Hash::make($password),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if($user->role === "STUDENT") {
|
try {
|
||||||
StudentData::create([
|
$user = User::create([
|
||||||
'user_id' => $user->id,
|
'email' => $request->email,
|
||||||
'address' => $request->student_data['address'],
|
'first_name' => $request->first_name,
|
||||||
'personal_email' => $request->student_data['personal_email'],
|
'last_name' => $request->last_name,
|
||||||
'study_field' => $request->student_data['study_field'],
|
'name' => "{$request->first_name} {$request->last_name}",
|
||||||
]);
|
'phone' => $request->phone,
|
||||||
} else if($user->role === "EMPLOYER") {
|
'role' => $request->role,
|
||||||
Company::create([
|
'password' => Hash::make($password),
|
||||||
'name' => $request->company_data['name'],
|
'activation_token' => $activation_token
|
||||||
'address' => $request->company_data['address'],
|
|
||||||
'ico' => $request->company_data['ico'],
|
|
||||||
'contact' => $user->id,
|
|
||||||
'hiring' => $request->company_data['hiring'],
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
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") {
|
||||||
|
Company::create([
|
||||||
|
'name' => $request->company_data['name'],
|
||||||
|
'address' => $request->company_data['address'],
|
||||||
|
'ico' => $request->company_data['ico'],
|
||||||
|
'contact' => $user->id,
|
||||||
|
'hiring' => $request->company_data['hiring'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
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));
|
event(new Registered($user));
|
||||||
|
|
||||||
return response()->noContent();
|
return response()->noContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function reset_password(Request $request): Response {
|
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)
|
||||||
|
{
|
||||||
$request->validate([
|
$request->validate([
|
||||||
'email' => ['required', 'string', 'lowercase', 'email', 'max:255'],
|
'email' => ['required', 'string', 'lowercase', 'email', 'max:255'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$user = User::whereEmail($request->email)->first();
|
$user = User::whereEmail($request->email)->first();
|
||||||
if (!$user) {
|
if (!$user) {
|
||||||
return response(status: 400);
|
return response()->json([
|
||||||
|
'message' => 'No such user exists.'
|
||||||
|
], 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
$newPassword = bin2hex(random_bytes(16));
|
$newPassword = bin2hex(random_bytes(16));
|
||||||
@@ -97,4 +135,38 @@ class RegisteredUserController extends Controller
|
|||||||
|
|
||||||
return response()->noContent();
|
return response()->noContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function reset_password_2(Request $request)
|
||||||
|
{
|
||||||
|
$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()->json([
|
||||||
|
'message' => 'No such user exists.'
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->password = Hash::make($request->password);
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
return response()->noContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function change_password(Request $request)
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
$request->validate([
|
||||||
|
'password' => ['required', 'string', 'min:8'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user->password = Hash::make($request->password);
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
return response()->noContent();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,10 +3,106 @@
|
|||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Models\Company;
|
use App\Models\Company;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Internship;
|
||||||
|
use App\Models\InternshipStatusData;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
class CompanyController extends Controller
|
class CompanyController extends Controller
|
||||||
{
|
{
|
||||||
|
public function all_simple()
|
||||||
|
{
|
||||||
|
$companies = Company::all();
|
||||||
|
|
||||||
|
$companies->each(function ($company) {
|
||||||
|
$company->contact = User::find($company->contact);
|
||||||
|
});
|
||||||
|
|
||||||
|
return response()->json($companies);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific company with contact details.
|
||||||
|
*/
|
||||||
|
public function get(int $id)
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
if ($user->role !== 'ADMIN') {
|
||||||
|
abort(403, 'Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
$company = Company::find($id);
|
||||||
|
|
||||||
|
if (!$company) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'No such company exists.'
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$company->contact = User::find($company->contact);
|
||||||
|
|
||||||
|
return response()->json($company);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update company information and contact person.
|
||||||
|
*/
|
||||||
|
public function update_all(int $id, Request $request)
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
if ($user->role !== 'ADMIN') {
|
||||||
|
abort(403, 'Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
$company = Company::find($id);
|
||||||
|
|
||||||
|
if (!$company) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'No such company exists.'
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validácia dát
|
||||||
|
$request->validate([
|
||||||
|
'name' => ['required', 'string', 'max:255'],
|
||||||
|
'address' => ['required', 'string', 'max:500'],
|
||||||
|
'ico' => ['required', 'integer'],
|
||||||
|
'hiring' => ['required', 'boolean'],
|
||||||
|
'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'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Aktualizácia Company údajov
|
||||||
|
$company->update([
|
||||||
|
'name' => $request->name,
|
||||||
|
'address' => $request->address,
|
||||||
|
'ico' => $request->ico,
|
||||||
|
'hiring' => $request->hiring,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Aktualizácia kontaktnej osoby
|
||||||
|
if ($request->has('contact')) {
|
||||||
|
$contactPerson = User::find($company->contact);
|
||||||
|
|
||||||
|
if ($contactPerson) {
|
||||||
|
$contactPerson->update([
|
||||||
|
'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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->noContent();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display a listing of the resource.
|
* Display a listing of the resource.
|
||||||
*/
|
*/
|
||||||
@@ -62,4 +158,48 @@ class CompanyController extends Controller
|
|||||||
{
|
{
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a company, its contact person and all related data.
|
||||||
|
*/
|
||||||
|
public function delete(int $id)
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
// Admin kontrola
|
||||||
|
if ($user->role !== 'ADMIN') {
|
||||||
|
abort(403, 'Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
$company = Company::find($id);
|
||||||
|
$company_contact = User::find($company->contact);
|
||||||
|
|
||||||
|
if (!$company) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'No such company exists.'
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
$internships = Internship::whereCompanyId($company->id);
|
||||||
|
|
||||||
|
// mazanie statusov
|
||||||
|
$internships->each(function ($internship) {
|
||||||
|
InternshipStatusData::whereInternshipId($internship->id)->delete();
|
||||||
|
});
|
||||||
|
|
||||||
|
// mazanie praxov
|
||||||
|
$internships->delete();
|
||||||
|
|
||||||
|
// mazanie firmy
|
||||||
|
Company::whereContact($company_contact->id);
|
||||||
|
|
||||||
|
// mazanie účtu firmy
|
||||||
|
$company_contact->delete();
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
return response()->noContent();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
102
backend/app/Http/Controllers/ExternalApiController.php
Normal file
102
backend/app/Http/Controllers/ExternalApiController.php
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Enums\InternshipStatus;
|
||||||
|
use App\Mail\InternshipStatusUpdated;
|
||||||
|
use App\Models\Internship;
|
||||||
|
use App\Models\InternshipStatusData;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Laravel\Sanctum\Sanctum;
|
||||||
|
use Mail;
|
||||||
|
|
||||||
|
class ExternalApiController extends Controller
|
||||||
|
{
|
||||||
|
public function all_keys(Request $request)
|
||||||
|
{
|
||||||
|
$tokens = Sanctum::$personalAccessTokenModel::with('tokenable')->get();
|
||||||
|
|
||||||
|
$tokens = $tokens->map(fn($token) => [
|
||||||
|
"id" => $token->id,
|
||||||
|
"name" => $token->name,
|
||||||
|
"created_at" => $token->created_at,
|
||||||
|
"last_used_at" => $token->last_used_at,
|
||||||
|
"owner" => User::find($token->tokenable_id)->name,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json($tokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create_key(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'name' => 'required|string|min:3|max:64',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (Sanctum::$personalAccessTokenModel::where('name', $request->name)->exists()) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'A token with this name already exists.'
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = $request->user()->createToken($request->name)->plainTextToken;
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
"key" => $token,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy_key(Request $request, int $id)
|
||||||
|
{
|
||||||
|
$request->user()->tokens()->where('id', $id)->delete();
|
||||||
|
return response()->noContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update_internship_status(Request $request, $id)
|
||||||
|
{
|
||||||
|
$user = $request->user();
|
||||||
|
$internship = Internship::find($id);
|
||||||
|
|
||||||
|
if (!$internship) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'No such internship exists.'
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentStatus = $internship->status->status;
|
||||||
|
|
||||||
|
$request->validate([
|
||||||
|
'status' => ['required', 'string', 'uppercase', 'in:DEFENDED,NOT_DEFENDED'],
|
||||||
|
'note' => ['required', 'string', 'min:1']
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($currentStatus !== InternshipStatus::CONFIRMED_BY_ADMIN) {
|
||||||
|
return response()->json([
|
||||||
|
"error" => "Expected current status to be 'CONFIRMED_BY_ADMIN', but it was '$currentStatus->value' instead",
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$newStatus = InternshipStatusData::make([
|
||||||
|
'internship_id' => $id,
|
||||||
|
'status' => $request->status,
|
||||||
|
'note' => $request->note,
|
||||||
|
'changed' => now(),
|
||||||
|
'modified_by' => $user->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Mail::to($internship->student)
|
||||||
|
->sendNow(new InternshipStatusUpdated(
|
||||||
|
$internship,
|
||||||
|
$user->name,
|
||||||
|
$internship->student->name,
|
||||||
|
$internship->company->name,
|
||||||
|
$currentStatus,
|
||||||
|
$request->enum('status', InternshipStatus::class),
|
||||||
|
$request->note
|
||||||
|
));
|
||||||
|
|
||||||
|
$newStatus->save();
|
||||||
|
return response()->noContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,10 +3,163 @@
|
|||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Models\Internship;
|
use App\Models\Internship;
|
||||||
|
use App\Models\InternshipStatusData;
|
||||||
|
use App\Models\User;
|
||||||
|
use Carbon\Carbon;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Mpdf\Mpdf;
|
||||||
|
|
||||||
class InternshipController extends Controller
|
class InternshipController extends Controller
|
||||||
{
|
{
|
||||||
|
public function all(Request $request)
|
||||||
|
{
|
||||||
|
$internships = $this->filterSearch($request);
|
||||||
|
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 && $user->id !== $internship->company->contact) {
|
||||||
|
abort(403, 'Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json($internship);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_default_proof(Request $request, 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 && $user->id !== $internship->company->contact) {
|
||||||
|
abort(403, 'Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
$contact = User::find($internship->company->contact);
|
||||||
|
|
||||||
|
$html = view('proof.default', [
|
||||||
|
'company' => $internship->company,
|
||||||
|
'companyContact' => $contact,
|
||||||
|
'internship' => $internship,
|
||||||
|
'student' => $internship->student,
|
||||||
|
'student_address' => $internship->student->studentData->address,
|
||||||
|
])->render();
|
||||||
|
|
||||||
|
$pdf = new Mpdf([
|
||||||
|
'orientation' => 'P'
|
||||||
|
]);
|
||||||
|
$pdf->WriteHTML($html);
|
||||||
|
|
||||||
|
return response($pdf->Output('', 'S'), 200)
|
||||||
|
->header('Content-Type', 'application/pdf')
|
||||||
|
->header('Content-Disposition', 'attachment; filename="proof_' . $id . '.pdf"');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_proof(int $id)
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
$internship = Internship::find($id);
|
||||||
|
|
||||||
|
if (!$internship) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'No such internship exists.'
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$internship->proof) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'No proof file exists for this internship.'
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user->role !== 'ADMIN' && $internship->user_id !== $user->id && $user->id !== $internship->company->contact) {
|
||||||
|
abort(403, 'Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response($internship->proof, 200)
|
||||||
|
->header('Content-Type', 'application/pdf')
|
||||||
|
->header('Content-Disposition', 'attachment; filename="proof_' . $id . '.pdf"');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_report(int $id)
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
$internship = Internship::find($id);
|
||||||
|
|
||||||
|
if (!$internship) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'No such internship exists.'
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$internship->report) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'No report file exists for this internship.'
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user->role !== 'ADMIN' && $internship->user_id !== $user->id && $user->id !== $internship->company->contact) {
|
||||||
|
abort(403, 'Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response($internship->report, 200)
|
||||||
|
->header('Content-Type', 'application/pdf')
|
||||||
|
->header('Content-Disposition', 'attachment; filename="report_' . $id . '.pdf"');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function export(Request $request)
|
||||||
|
{
|
||||||
|
$internships = $this->filterSearch($request, true);
|
||||||
|
|
||||||
|
$csv_header = [
|
||||||
|
'ID',
|
||||||
|
'Student',
|
||||||
|
'Company',
|
||||||
|
'Start',
|
||||||
|
'End',
|
||||||
|
'YearOfStudy',
|
||||||
|
'Semester',
|
||||||
|
'StudyField',
|
||||||
|
'Status'
|
||||||
|
];
|
||||||
|
|
||||||
|
$csv_content = implode(',', $csv_header) . "\n";
|
||||||
|
foreach ($internships as $internship) {
|
||||||
|
$data = [
|
||||||
|
$internship->id,
|
||||||
|
'"' . $internship->student->name . '"',
|
||||||
|
'"' . $internship->company->name . '"',
|
||||||
|
Carbon::parse($internship->start)->format('d.m.Y'),
|
||||||
|
Carbon::parse($internship->end)->format('d.m.Y'),
|
||||||
|
$internship->year_of_study,
|
||||||
|
$internship->semester,
|
||||||
|
$internship->student->studentData->study_field,
|
||||||
|
$internship->status->status->value
|
||||||
|
];
|
||||||
|
$csv_content .= implode(',', $data) . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
return response($csv_content, 200)
|
||||||
|
->header('Content-Type', 'application/csv')
|
||||||
|
->header('Content-Disposition', 'attachment; filename="internships.csv"');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display a listing of the resource.
|
* Display a listing of the resource.
|
||||||
*/
|
*/
|
||||||
@@ -28,7 +181,31 @@ class InternshipController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function store(Request $request)
|
public function store(Request $request)
|
||||||
{
|
{
|
||||||
//
|
$user = auth()->user();
|
||||||
|
|
||||||
|
$this->validateNewInternship($request);
|
||||||
|
$this->checkOverlap($user->id, $request->start, $request->end);
|
||||||
|
|
||||||
|
$Internship = Internship::create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'company_id' => $request->company_id,
|
||||||
|
'start' => $request->start,
|
||||||
|
'end' => $request->end,
|
||||||
|
'year_of_study' => $request->year_of_study,
|
||||||
|
'semester' => $request->semester,
|
||||||
|
'position_description' => $request->position_description,
|
||||||
|
'proof' => null
|
||||||
|
]);
|
||||||
|
|
||||||
|
InternshipStatusData::create([
|
||||||
|
'internship_id' => $Internship->id,
|
||||||
|
'status' => 'SUBMITTED',
|
||||||
|
'changed' => now(),
|
||||||
|
'note' => null,
|
||||||
|
'modified_by' => $user->id
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->noContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -47,6 +224,71 @@ 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 && $user->id !== $internship->company->contact) {
|
||||||
|
abort(403, 'Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->validateNewInternship($request);
|
||||||
|
$this->checkOverlap($internship->user_id, $request->start, $request->end, $internship->id);
|
||||||
|
|
||||||
|
$internship->update($request->except(['user_id']));
|
||||||
|
return response()->noContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update_documents(int $id, Request $request)
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
$internship = Internship::find($id);
|
||||||
|
|
||||||
|
if (!$internship) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'No such internship exists.'
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($internship->user_id !== $user->id && $user->id !== $internship->company->contact) {
|
||||||
|
abort(403, 'Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
$request->validate([
|
||||||
|
'proof' => ['nullable', 'file', 'mimes:pdf', 'max:10240'],
|
||||||
|
'report' => ['nullable', 'file', 'mimes:pdf', 'max:10240'],
|
||||||
|
'report_confirmed' => ['required', 'boolean'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($request->hasFile('proof')) {
|
||||||
|
$internship->proof = file_get_contents($request->file('proof')->getRealPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->hasFile('report')) {
|
||||||
|
$internship->report = file_get_contents($request->file('report')->getRealPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user->role === 'EMPLOYER') {
|
||||||
|
if ($request->report_confirmed && (!$internship->proof || !$internship->report)) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Report cannot be confirmed without an proof and report.'
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$internship->report_confirmed = $request->report_confirmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
$internship->save();
|
||||||
|
return response()->noContent();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the specified resource in storage.
|
* Update the specified resource in storage.
|
||||||
*/
|
*/
|
||||||
@@ -58,8 +300,116 @@ class InternshipController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Remove the specified resource from storage.
|
* Remove the specified resource from storage.
|
||||||
*/
|
*/
|
||||||
public function destroy(Internship $internship)
|
public function destroy(int $id)
|
||||||
{
|
{
|
||||||
//
|
$internship = Internship::find($id);
|
||||||
|
|
||||||
|
if (!$internship) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'No such internship exists.'
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
$internship->delete();
|
||||||
|
|
||||||
|
return response()->noContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 00:00:00', strtotime($request->start)),
|
||||||
|
'end' => date('Y-m-d 00:00:00', strtotime($request->end))
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
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])
|
||||||
|
->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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function filterSearch(Request $request, bool $ignorePage = false)
|
||||||
|
{
|
||||||
|
$user = $request->user();
|
||||||
|
|
||||||
|
$request->validate([
|
||||||
|
'year' => 'nullable|integer',
|
||||||
|
'company' => 'nullable|string|min:3|max:32',
|
||||||
|
'study_programe' => 'nullable|string|min:3|max:32',
|
||||||
|
'student' => 'nullable|string|min:3|max:32',
|
||||||
|
'page' => 'nullable|integer|min:1',
|
||||||
|
'per_page' => 'nullable|integer|min:-1|max:100',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($ignorePage) {
|
||||||
|
$request->merge(['per_page' => -1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$perPage = $request->input('per_page', 15);
|
||||||
|
|
||||||
|
// Handle "All" items (-1)
|
||||||
|
if ($perPage == -1) {
|
||||||
|
$perPage = Internship::count();
|
||||||
|
}
|
||||||
|
|
||||||
|
$internships = Internship::query()
|
||||||
|
->with(['student.studentData'])
|
||||||
|
->when($request->year, function ($query, $year) {
|
||||||
|
$query->whereYear('start', $year);
|
||||||
|
})
|
||||||
|
->when($request->company, function ($query, $company) {
|
||||||
|
$query->whereHas('company', function ($q) use ($company) {
|
||||||
|
$q->where('name', 'like', "%$company%");
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->when($request->study_programe, function ($query, $studyPrograme) {
|
||||||
|
$query->whereHas('student.studentData', function ($q) use ($studyPrograme) {
|
||||||
|
$q->where('study_field', 'like', "%$studyPrograme%");
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->when($request->student, function ($query, $student) {
|
||||||
|
$query->whereHas('student', function ($q) use ($student) {
|
||||||
|
$q->where('name', 'like', "%$student%");
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->when($user->role === 'STUDENT', function ($query) use ($user) {
|
||||||
|
$query->where('user_id', '=', $user->id);
|
||||||
|
})
|
||||||
|
->when($user->role === 'EMPLOYER', function ($query) use ($user) {
|
||||||
|
$query->whereHas('company', function ($q) use ($user) {
|
||||||
|
$q->where('contact', 'like', $user->id);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->paginate($perPage);
|
||||||
|
|
||||||
|
return $internships;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
|
||||||
|
|
||||||
use App\Models\InternshipStatus;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
|
|
||||||
class InternshipStatusController extends Controller
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Display a listing of the resource.
|
|
||||||
*/
|
|
||||||
public function index()
|
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the form for creating a new resource.
|
|
||||||
*/
|
|
||||||
public function create()
|
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Store a newly created resource in storage.
|
|
||||||
*/
|
|
||||||
public function store(Request $request)
|
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display the specified resource.
|
|
||||||
*/
|
|
||||||
public function show(InternshipStatus $internshipStatus)
|
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the form for editing the specified resource.
|
|
||||||
*/
|
|
||||||
public function edit(InternshipStatus $internshipStatus)
|
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the specified resource in storage.
|
|
||||||
*/
|
|
||||||
public function update(Request $request, InternshipStatus $internshipStatus)
|
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove the specified resource from storage.
|
|
||||||
*/
|
|
||||||
public function destroy(InternshipStatus $internshipStatus)
|
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
||||||
}
|
|
||||||
161
backend/app/Http/Controllers/InternshipStatusDataController.php
Normal file
161
backend/app/Http/Controllers/InternshipStatusDataController.php
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Enums\InternshipStatus;
|
||||||
|
use App\Mail\InternshipStatusUpdated;
|
||||||
|
use App\Models\Internship;
|
||||||
|
use App\Models\InternshipStatusData;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Mail;
|
||||||
|
|
||||||
|
class InternshipStatusDataController extends Controller
|
||||||
|
{
|
||||||
|
public function get(int $id)
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
$internship_statuses = InternshipStatusData::whereInternshipId($id)->orderByDesc('changed')->get();
|
||||||
|
|
||||||
|
if (!$internship_statuses) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'No such internship exists.'
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$internship = Internship::find($id);
|
||||||
|
if ($user->role !== 'ADMIN' && $internship->user_id !== $user->id && $user->id !== $internship->company->contact) {
|
||||||
|
abort(403, 'Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json($internship_statuses);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_next_states(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 && $user->id !== $internship->company->contact) {
|
||||||
|
abort(403, 'Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
$nextPossibleStatuses = $internship->nextStates($user->role);
|
||||||
|
return response()->json($nextPossibleStatuses);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display a listing of the resource.
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the form for creating a new resource.
|
||||||
|
*/
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a newly created resource in storage.
|
||||||
|
*/
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the specified resource.
|
||||||
|
*/
|
||||||
|
public function show(InternshipStatusData $internshipStatus)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the form for editing the specified resource.
|
||||||
|
*/
|
||||||
|
public function edit(InternshipStatusData $internshipStatus)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the specified resource in storage.
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user->role !== 'ADMIN' && $user->id !== $internship->company->contact) {
|
||||||
|
abort(403, 'Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
$internshipStatus = $internship->status;
|
||||||
|
$newStatusValidator = 'in:' . implode(',', $internship->nextStates($user->role));
|
||||||
|
|
||||||
|
$request->validate([
|
||||||
|
'status' => ['required', 'string', 'uppercase', $newStatusValidator],
|
||||||
|
'note' => ['required', 'string', 'min:1']
|
||||||
|
]);
|
||||||
|
|
||||||
|
$newStatus = InternshipStatusData::make([
|
||||||
|
'internship_id' => $id,
|
||||||
|
'status' => $request->status,
|
||||||
|
'note' => $request->note,
|
||||||
|
'changed' => now(),
|
||||||
|
'modified_by' => $user->id
|
||||||
|
]);
|
||||||
|
|
||||||
|
// mail študentovi
|
||||||
|
Mail::to($internship->student)
|
||||||
|
->sendNow(new InternshipStatusUpdated(
|
||||||
|
$internship,
|
||||||
|
$internshipStatus->status,
|
||||||
|
$newStatus->status,
|
||||||
|
$request->note,
|
||||||
|
$user,
|
||||||
|
recipiantIsStudent: true,
|
||||||
|
));
|
||||||
|
|
||||||
|
// ak zmenu nevykonala firma, posleme mail aj firme
|
||||||
|
if ($user->id !== $internship->company->contactPerson->id) {
|
||||||
|
Mail::to($internship->company->contactPerson->email)
|
||||||
|
->sendNow(new InternshipStatusUpdated(
|
||||||
|
$internship,
|
||||||
|
$internshipStatus->status,
|
||||||
|
$newStatus->status,
|
||||||
|
$request->note,
|
||||||
|
$user,
|
||||||
|
recipiantIsStudent: false,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
$newStatus->save();
|
||||||
|
return response()->noContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the specified resource from storage.
|
||||||
|
*/
|
||||||
|
public function destroy(InternshipStatusData $internshipStatus)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,11 +2,123 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Internship;
|
||||||
use App\Models\StudentData;
|
use App\Models\StudentData;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\InternshipStatusData;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
class StudentDataController extends Controller
|
class StudentDataController extends Controller
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* Display a listing of all students with their data.
|
||||||
|
*/
|
||||||
|
public function all()
|
||||||
|
{
|
||||||
|
// Iba admin môže vidieť zoznam študentov
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
if ($user->role !== 'ADMIN') {
|
||||||
|
abort(403, 'Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
$students = User::where('role', 'STUDENT')
|
||||||
|
->with('studentData')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return response()->json($students);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific student with their data.
|
||||||
|
*/
|
||||||
|
public function get(int $id)
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
$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);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user->role !== 'ADMIN') {
|
||||||
|
abort(403, 'Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
$student->load('studentData');
|
||||||
|
|
||||||
|
return response()->json($student);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update student's basic information and student data.
|
||||||
|
*/
|
||||||
|
public function update_all(int $id, Request $request)
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
$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);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user->role !== 'ADMIN') {
|
||||||
|
abort(403, 'Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validácia dát
|
||||||
|
$request->validate([
|
||||||
|
'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'],
|
||||||
|
'student_data.personal_email' => ['nullable', 'email', 'max:255'],
|
||||||
|
'student_data.address' => ['nullable', 'string', 'max:500'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Aktualizácia User údajov
|
||||||
|
$student->update([
|
||||||
|
'name' => $request->first_name . ' ' . $request->last_name,
|
||||||
|
'first_name' => $request->first_name,
|
||||||
|
'last_name' => $request->last_name,
|
||||||
|
'email' => $request->email,
|
||||||
|
'phone' => $request->phone,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Aktualizácia alebo vytvorenie StudentData
|
||||||
|
if ($request->has('student_data')) {
|
||||||
|
$studentData = $student->studentData;
|
||||||
|
|
||||||
|
if ($studentData) {
|
||||||
|
$studentData->update($request->student_data);
|
||||||
|
} else {
|
||||||
|
$student->studentData()->create($request->student_data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->noContent();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display a listing of the resource.
|
* Display a listing of the resource.
|
||||||
*/
|
*/
|
||||||
@@ -62,4 +174,54 @@ 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
// mazanie praxov
|
||||||
|
$internships = Internship::whereUserId($student->id);
|
||||||
|
|
||||||
|
// mazanie statusov
|
||||||
|
$internships->each(function ($internship) {
|
||||||
|
InternshipStatusData::whereInternshipId($internship->id)->delete();
|
||||||
|
});
|
||||||
|
|
||||||
|
// mazanie praxov
|
||||||
|
$internships->delete();
|
||||||
|
|
||||||
|
// mazanie firmy
|
||||||
|
StudentData::whereUserId($student->id);
|
||||||
|
|
||||||
|
// mazanie účtu firmy
|
||||||
|
$student->delete();
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
return response()->noContent();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
30
backend/app/Http/Middleware/AdministratorOnly.php
Normal file
30
backend/app/Http/Middleware/AdministratorOnly.php
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class AdministratorOnly
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
*
|
||||||
|
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||||
|
*/
|
||||||
|
public function handle(Request $request, Closure $next): Response
|
||||||
|
{
|
||||||
|
$user = $request->user();
|
||||||
|
|
||||||
|
if ($user === null) {
|
||||||
|
abort(403, 'Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user->role !== 'ADMIN') {
|
||||||
|
abort(403, 'Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
RateLimiter::clear($this->throttleKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
89
backend/app/Mail/InternshipStatusUpdated.php
Normal file
89
backend/app/Mail/InternshipStatusUpdated.php
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Mail;
|
||||||
|
|
||||||
|
use App\Enums\InternshipStatus;
|
||||||
|
use App\Models\Internship;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Mail\Mailable;
|
||||||
|
use Illuminate\Mail\Mailables\Content;
|
||||||
|
use Illuminate\Mail\Mailables\Envelope;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
class InternshipStatusUpdated extends Mailable
|
||||||
|
{
|
||||||
|
use Queueable, SerializesModels;
|
||||||
|
|
||||||
|
private Internship $internship;
|
||||||
|
private InternshipStatus $oldStatus;
|
||||||
|
private InternshipStatus $newStatus;
|
||||||
|
private string $note;
|
||||||
|
private User $changedBy;
|
||||||
|
private bool $recipiantIsStudent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new message instance.
|
||||||
|
*/
|
||||||
|
public function __construct(Internship $internship, InternshipStatus $oldStatus, InternshipStatus $newStatus, string $note, User $changedBy, bool $recipiantIsStudent)
|
||||||
|
{
|
||||||
|
$this->internship = $internship;
|
||||||
|
$this->oldStatus = $oldStatus;
|
||||||
|
$this->newStatus = $newStatus;
|
||||||
|
$this->note = $note;
|
||||||
|
$this->changedBy = $changedBy;
|
||||||
|
$this->recipiantIsStudent = $recipiantIsStudent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the message envelope.
|
||||||
|
*/
|
||||||
|
public function envelope(): Envelope
|
||||||
|
{
|
||||||
|
return new Envelope(
|
||||||
|
subject: 'Internship Status Updated',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the message content definition.
|
||||||
|
*/
|
||||||
|
public function content(): Content
|
||||||
|
{
|
||||||
|
return new Content(
|
||||||
|
view: 'mail.internship.status_updated',
|
||||||
|
with: [
|
||||||
|
"internship" => $this->internship,
|
||||||
|
"oldStatus" => $this->prettyStatus($this->oldStatus->value),
|
||||||
|
"newStatus" => $this->prettyStatus($this->newStatus->value),
|
||||||
|
"note" => $this->note,
|
||||||
|
"recipiantIsStudent" => $this->recipiantIsStudent,
|
||||||
|
"changedBy" => $this->changedBy,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the attachments for the message.
|
||||||
|
*
|
||||||
|
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
|
||||||
|
*/
|
||||||
|
public function attachments(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function prettyStatus(string $status): string
|
||||||
|
{
|
||||||
|
return match ($status) {
|
||||||
|
'SUBMITTED' => 'Zadané',
|
||||||
|
'CONFIRMED_BY_COMPANY' => 'Potvrdená firmou',
|
||||||
|
'CONFIRMED_BY_ADMIN' => 'Potvrdená garantom',
|
||||||
|
'DENIED_BY_COMPANY' => 'Zamietnutá firmou',
|
||||||
|
'DENIED_BY_ADMIN' => 'Zamietnutá garantom',
|
||||||
|
'DEFENDED' => 'Obhájená',
|
||||||
|
'NOT_DEFENDED' => 'Neobhájená',
|
||||||
|
default => throw new \Exception("Invalid status: $status")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
58
backend/app/Mail/UserAccountActivated.php
Normal file
58
backend/app/Mail/UserAccountActivated.php
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Mail;
|
||||||
|
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Mail\Mailable;
|
||||||
|
use Illuminate\Mail\Mailables\Content;
|
||||||
|
use Illuminate\Mail\Mailables\Envelope;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
class UserAccountActivated extends Mailable
|
||||||
|
{
|
||||||
|
use Queueable, SerializesModels;
|
||||||
|
|
||||||
|
private string $name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new message instance.
|
||||||
|
*/
|
||||||
|
public function __construct(string $name)
|
||||||
|
{
|
||||||
|
$this->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<int, \Illuminate\Mail\Mailables\Attachment>
|
||||||
|
*/
|
||||||
|
public function attachments(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,15 +14,15 @@ class UserRegistrationCompleted extends Mailable
|
|||||||
use Queueable, SerializesModels;
|
use Queueable, SerializesModels;
|
||||||
|
|
||||||
private string $name;
|
private string $name;
|
||||||
private string $password;
|
private string $activation_token;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new message instance.
|
* Create a new message instance.
|
||||||
*/
|
*/
|
||||||
public function __construct(string $name, string $password)
|
public function __construct(string $name, string $activation_token)
|
||||||
{
|
{
|
||||||
$this->name = $name;
|
$this->name = $name;
|
||||||
$this->password = $password;
|
$this->activation_token = $activation_token;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,7 +44,7 @@ class UserRegistrationCompleted extends Mailable
|
|||||||
view: 'mail.registration.completed',
|
view: 'mail.registration.completed',
|
||||||
with: [
|
with: [
|
||||||
"name" => $this->name,
|
"name" => $this->name,
|
||||||
"password" => $this->password
|
"activation_token" => $this->activation_token
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,4 +22,30 @@ class Company extends Model
|
|||||||
'contact',
|
'contact',
|
||||||
'hiring'
|
'hiring'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that should be hidden for serialization.
|
||||||
|
*
|
||||||
|
* @var list<string>
|
||||||
|
*/
|
||||||
|
protected $hidden = [
|
||||||
|
'created_at',
|
||||||
|
'updated_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the internships for the company.
|
||||||
|
*/
|
||||||
|
public function internships()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Internship::class, 'company_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the contact person (user) for the company.
|
||||||
|
*/
|
||||||
|
public function contactPerson()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'contact');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Enums\InternshipStatus;
|
||||||
|
use Carbon\Carbon;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
@@ -23,6 +25,124 @@ class Internship extends Model
|
|||||||
'year_of_study',
|
'year_of_study',
|
||||||
'semester',
|
'semester',
|
||||||
'position_description',
|
'position_description',
|
||||||
'agreement',
|
'proof',
|
||||||
|
'report',
|
||||||
|
'report_confirmed',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that should be hidden for serialization.
|
||||||
|
*
|
||||||
|
* @var list<string>
|
||||||
|
*/
|
||||||
|
protected $hidden = [
|
||||||
|
'created_at',
|
||||||
|
'updated_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the attributes that should be cast.
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'report_confirmed' => 'boolean',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function student()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'user_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function company()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Company::class, 'company_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function status()
|
||||||
|
{
|
||||||
|
return $this->hasOne(InternshipStatusData::class, 'internship_id')->latestOfMany();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function nextStates(string $userRole)
|
||||||
|
{
|
||||||
|
$current_status = $this->status->status;
|
||||||
|
$report_confirmed = $this->report_confirmed;
|
||||||
|
|
||||||
|
// študent nemôže meniť stav
|
||||||
|
if ($userRole === 'STUDENT')
|
||||||
|
return [];
|
||||||
|
|
||||||
|
/*
|
||||||
|
nasledujúci platný stav je určený podľa:
|
||||||
|
- aktuálneho stavu
|
||||||
|
- roly používateľa, ktorý ide meniť stav
|
||||||
|
- či bol výkaz potvrdený firmou
|
||||||
|
*/
|
||||||
|
return match (true) {
|
||||||
|
// prax bola iba vytvorená a ide ju meniť admin alebo firma
|
||||||
|
$current_status === InternshipStatus::SUBMITTED
|
||||||
|
=> ['CONFIRMED_BY_COMPANY', 'DENIED_BY_COMPANY'],
|
||||||
|
|
||||||
|
// prax bola potvrdená firmou a ide ju meniť admin
|
||||||
|
$current_status === InternshipStatus::CONFIRMED_BY_COMPANY && $userRole === "ADMIN"
|
||||||
|
=> ['CONFIRMED_BY_ADMIN', 'DENIED_BY_ADMIN'],
|
||||||
|
|
||||||
|
// prax bola potvrdená firmou a ide ju meniť firma
|
||||||
|
$current_status === InternshipStatus::CONFIRMED_BY_COMPANY && $userRole === "EMPLOYER"
|
||||||
|
=> ['DENIED_BY_COMPANY'],
|
||||||
|
|
||||||
|
// prax bola zamietnutá firmou a ide ju meniť admin
|
||||||
|
$current_status === InternshipStatus::DENIED_BY_COMPANY && $userRole === "ADMIN"
|
||||||
|
=> ['CONFIRMED_BY_COMPANY', 'SUBMITTED'],
|
||||||
|
|
||||||
|
// prax bola potvrdená garantom, ide ju meniť admin a výkaz bol potvrdený firmou
|
||||||
|
$current_status === InternshipStatus::CONFIRMED_BY_ADMIN && $userRole === "ADMIN" && $report_confirmed
|
||||||
|
=> ['DENIED_BY_ADMIN', 'CONFIRMED_BY_COMPANY', 'DENIED_BY_COMPANY', 'DEFENDED', 'NOT_DEFENDED'],
|
||||||
|
|
||||||
|
// prax bola potvrdená garantom, ide ju meniť admin a výkaz nebol potvrdený firmou
|
||||||
|
$current_status === InternshipStatus::CONFIRMED_BY_ADMIN && $userRole === "ADMIN" && !$report_confirmed
|
||||||
|
=> ['DENIED_BY_ADMIN', 'CONFIRMED_BY_COMPANY', 'DENIED_BY_COMPANY'],
|
||||||
|
|
||||||
|
// prax bola zamietnutá garantom a ide ju meniť garant
|
||||||
|
$current_status === InternshipStatus::DENIED_BY_ADMIN && $userRole === "ADMIN"
|
||||||
|
=> ['CONFIRMED_BY_COMPANY', 'CONFIRMED_BY_ADMIN', 'DENIED_BY_COMPANY'],
|
||||||
|
|
||||||
|
// prax bola obhájená a ide ju meniť admin
|
||||||
|
$current_status === InternshipStatus::DEFENDED && $userRole === "ADMIN"
|
||||||
|
=> ['NOT_DEFENDED'],
|
||||||
|
|
||||||
|
// prax nebola obhájená a ide ju meniť admin
|
||||||
|
$current_status === InternshipStatus::NOT_DEFENDED && $userRole === "ADMIN"
|
||||||
|
=> ['DEFENDED'],
|
||||||
|
|
||||||
|
default => []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare the model for JSON serialization.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function toArray(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'student' => $this->student,
|
||||||
|
'company' => $this->company,
|
||||||
|
'start' => Carbon::parse($this->start)->format('d.m.Y'),
|
||||||
|
'end' => Carbon::parse($this->end)->format('d.m.Y'),
|
||||||
|
'year_of_study' => $this->year_of_study,
|
||||||
|
'semester' => $this->semester,
|
||||||
|
'position_description' => $this->position_description,
|
||||||
|
'proof' => $this->proof !== null,
|
||||||
|
'report' => $this->report !== null,
|
||||||
|
'report_confirmed' => $this->report_confirmed,
|
||||||
|
'status' => $this->status,
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
|
|
||||||
class InternshipStatus extends Model
|
|
||||||
{
|
|
||||||
/** @use HasFactory<\Database\Factories\InternshipStatusFactory> */
|
|
||||||
use HasFactory;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The attributes that are mass assignable.
|
|
||||||
*
|
|
||||||
* @var list<string>
|
|
||||||
*/
|
|
||||||
protected $fillable = [
|
|
||||||
'internship_id',
|
|
||||||
'status',
|
|
||||||
'changed',
|
|
||||||
'note',
|
|
||||||
'modified_by'
|
|
||||||
];
|
|
||||||
}
|
|
||||||
66
backend/app/Models/InternshipStatusData.php
Normal file
66
backend/app/Models/InternshipStatusData.php
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class InternshipStatusData extends Model
|
||||||
|
{
|
||||||
|
/** @use HasFactory<\Database\Factories\InternshipStatusFactory> */
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that are mass assignable.
|
||||||
|
*
|
||||||
|
* @var list<string>
|
||||||
|
*/
|
||||||
|
protected $fillable = [
|
||||||
|
'internship_id',
|
||||||
|
'status',
|
||||||
|
'changed',
|
||||||
|
'note',
|
||||||
|
'modified_by'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that should be hidden for serialization.
|
||||||
|
*
|
||||||
|
* @var list<string>
|
||||||
|
*/
|
||||||
|
protected $hidden = [
|
||||||
|
'created_at',
|
||||||
|
'updated_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $table = 'internship_statuses';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the attributes that should be cast.
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'status' => '\App\Enums\InternshipStatus',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function modifiedBy()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'modified_by');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toArray()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'internship_id' => $this->internship_id,
|
||||||
|
'status' => $this->status->value,
|
||||||
|
'changed' => $this->changed,
|
||||||
|
'note' => $this->note,
|
||||||
|
'modified_by' => $this->modifiedBy,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,4 +21,14 @@ class StudentData extends Model
|
|||||||
'personal_email',
|
'personal_email',
|
||||||
'study_field',
|
'study_field',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that should be hidden for serialization.
|
||||||
|
*
|
||||||
|
* @var list<string>
|
||||||
|
*/
|
||||||
|
protected $hidden = [
|
||||||
|
'created_at',
|
||||||
|
'updated_at',
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,12 @@ namespace App\Models;
|
|||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
|
use Laravel\Sanctum\HasApiTokens;
|
||||||
|
|
||||||
class User extends Authenticatable
|
class User extends Authenticatable
|
||||||
{
|
{
|
||||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||||
use HasFactory, Notifiable;
|
use HasApiTokens, HasFactory, Notifiable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The attributes that are mass assignable.
|
* The attributes that are mass assignable.
|
||||||
@@ -25,6 +26,9 @@ class User extends Authenticatable
|
|||||||
'email',
|
'email',
|
||||||
'role',
|
'role',
|
||||||
'password',
|
'password',
|
||||||
|
'active',
|
||||||
|
'needs_password_change',
|
||||||
|
'activation_token',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,6 +39,12 @@ class User extends Authenticatable
|
|||||||
protected $hidden = [
|
protected $hidden = [
|
||||||
'password',
|
'password',
|
||||||
'remember_token',
|
'remember_token',
|
||||||
|
'activation_token',
|
||||||
|
'created_at',
|
||||||
|
'updated_at',
|
||||||
|
'email_verified_at',
|
||||||
|
'active',
|
||||||
|
'needs_password_change'
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -47,6 +57,24 @@ class User extends Authenticatable
|
|||||||
return [
|
return [
|
||||||
'email_verified_at' => 'datetime',
|
'email_verified_at' => 'datetime',
|
||||||
'password' => 'hashed',
|
'password' => 'hashed',
|
||||||
|
'active' => 'boolean',
|
||||||
|
'needs_password_change' => 'boolean'
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the student data associated with the user.
|
||||||
|
*/
|
||||||
|
public function studentData()
|
||||||
|
{
|
||||||
|
return $this->hasOne(StudentData::class, 'user_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the internships for the user.
|
||||||
|
*/
|
||||||
|
public function internships()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Internship::class, 'user_id');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,8 @@
|
|||||||
"php": "^8.2",
|
"php": "^8.2",
|
||||||
"laravel/framework": "^12.0",
|
"laravel/framework": "^12.0",
|
||||||
"laravel/sanctum": "^4.0",
|
"laravel/sanctum": "^4.0",
|
||||||
"laravel/tinker": "^2.10.1"
|
"laravel/tinker": "^2.10",
|
||||||
|
"mpdf/mpdf": "^8.2"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"fakerphp/faker": "^1.23",
|
"fakerphp/faker": "^1.23",
|
||||||
@@ -80,4 +81,4 @@
|
|||||||
},
|
},
|
||||||
"minimum-stability": "stable",
|
"minimum-stability": "stable",
|
||||||
"prefer-stable": true
|
"prefer-stable": true
|
||||||
}
|
}
|
||||||
|
|||||||
1262
backend/composer.lock
generated
1262
backend/composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,7 @@ class CompanyFactory extends Factory
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'name' => fake()->company(),
|
'name' => fake()->company(),
|
||||||
'address' => fake()->address(),
|
'address' => fake()->streetAddress() . ", " . fake()->city() . ", " . fake()->postcode(),
|
||||||
'ico' => fake()->numberBetween(111111, 999999),
|
'ico' => fake()->numberBetween(111111, 999999),
|
||||||
'contact' => 0,
|
'contact' => 0,
|
||||||
'hiring' => fake()->boolean(),
|
'hiring' => fake()->boolean(),
|
||||||
|
|||||||
@@ -16,15 +16,20 @@ class InternshipFactory extends Factory
|
|||||||
*/
|
*/
|
||||||
public function definition(): array
|
public function definition(): array
|
||||||
{
|
{
|
||||||
|
$start = fake()->dateTime()->setTime(0, 0, 0, 0);
|
||||||
|
$end = (clone $start)->modify('+' . fake()->numberBetween(150, 160) . ' hours')->setTime(0, 0, 0, 0);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'user_id' => 0,
|
'user_id' => 0,
|
||||||
'company_id' => 0,
|
'company_id' => 0,
|
||||||
'start' => fake()->dateTime(),
|
'start' => $start,
|
||||||
'end' => fake()->dateTime("+30 days"),
|
'end' => $end,
|
||||||
'year_of_study' => fake()->randomElement([1, 2, 3, 4, 5]),
|
'year_of_study' => fake()->randomElement([1, 2, 3, 4, 5]),
|
||||||
'semester' => fake()->randomElement(["WINTER", "SUMMER"]),
|
'semester' => fake()->randomElement(["WINTER", "SUMMER"]),
|
||||||
'position_description' => fake()->jobTitle(),
|
'position_description' => fake()->jobTitle(),
|
||||||
'agreement' => null,
|
'proof' => null,
|
||||||
|
'report' => null,
|
||||||
|
'report_confirmed' => false,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,13 @@
|
|||||||
|
|
||||||
namespace Database\Factories;
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Enums\InternshipStatus;
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\InternshipStatus>
|
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\InternshipStatusData>
|
||||||
*/
|
*/
|
||||||
class InternshipStatusFactory extends Factory
|
class InternshipStatusDataFactory extends Factory
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Define the model's default state.
|
* Define the model's default state.
|
||||||
@@ -18,7 +19,7 @@ class InternshipStatusFactory extends Factory
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'internship_id' => 0,
|
'internship_id' => 0,
|
||||||
'status' => fake()->randomElement(["SUBMITTED", "CONFIRMED", "DENIED", "DEFENDED", "NOT_DEFENDED"]),
|
'status' => fake()->randomElement(InternshipStatus::all()),
|
||||||
'changed' => fake()->dateTime(),
|
'changed' => fake()->dateTime(),
|
||||||
'note' => null,
|
'note' => null,
|
||||||
'modified_by' => 0,
|
'modified_by' => 0,
|
||||||
@@ -18,7 +18,7 @@ class StudentDataFactory extends Factory
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'user_id' => 0,
|
'user_id' => 0,
|
||||||
'address' => fake()->address(),
|
'address' => fake()->streetAddress() . ", " . fake()->city() . ", " . fake()->postcode(),
|
||||||
'personal_email' => fake()->safeEmail(),
|
'personal_email' => fake()->safeEmail(),
|
||||||
'study_field' => fake()->randomElement(["AI22m", "AI22b"]),
|
'study_field' => fake()->randomElement(["AI22m", "AI22b"]),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ class UserFactory extends Factory
|
|||||||
'email_verified_at' => now(),
|
'email_verified_at' => now(),
|
||||||
'password' => static::$password ??= Hash::make('password'),
|
'password' => static::$password ??= Hash::make('password'),
|
||||||
'remember_token' => Str::random(10),
|
'remember_token' => Str::random(10),
|
||||||
|
'active' => true,
|
||||||
|
'needs_password_change' => false,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ return new class extends Migration
|
|||||||
$table->string('email')->unique();
|
$table->string('email')->unique();
|
||||||
$table->timestamp('email_verified_at')->nullable();
|
$table->timestamp('email_verified_at')->nullable();
|
||||||
$table->string('password');
|
$table->string('password');
|
||||||
|
$table->boolean('active')->default(false);
|
||||||
|
$table->boolean('needs_password_change')->default(false);
|
||||||
|
$table->string('activation_token')->nullable();
|
||||||
$table->rememberToken();
|
$table->rememberToken();
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ use Illuminate\Database\Migrations\Migration;
|
|||||||
use Illuminate\Database\Schema\Blueprint;
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
return new class extends Migration
|
return new class extends Migration {
|
||||||
{
|
|
||||||
/**
|
/**
|
||||||
* Run the migrations.
|
* Run the migrations.
|
||||||
*/
|
*/
|
||||||
@@ -20,7 +19,9 @@ return new class extends Migration
|
|||||||
$table->unsignedSmallInteger("year_of_study")->nullable(false);
|
$table->unsignedSmallInteger("year_of_study")->nullable(false);
|
||||||
$table->enum("semester", ["WINTER", "SUMMER"])->nullable(false);
|
$table->enum("semester", ["WINTER", "SUMMER"])->nullable(false);
|
||||||
$table->string("position_description")->nullable(false);
|
$table->string("position_description")->nullable(false);
|
||||||
$table->binary("agreement")->nullable(true);
|
$table->binary("proof")->nullable(true);
|
||||||
|
$table->binary("report")->nullable(true);
|
||||||
|
$table->boolean("report_confirmed")->nullable(false)->default(false);
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Enums\InternshipStatus;
|
||||||
use Illuminate\Database\Migrations\Migration;
|
use Illuminate\Database\Migrations\Migration;
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
return new class extends Migration
|
return new class extends Migration {
|
||||||
{
|
|
||||||
/**
|
/**
|
||||||
* Run the migrations.
|
* Run the migrations.
|
||||||
*/
|
*/
|
||||||
@@ -14,7 +14,7 @@ return new class extends Migration
|
|||||||
Schema::create('internship_statuses', function (Blueprint $table) {
|
Schema::create('internship_statuses', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->foreignId("internship_id")->nullable(false)->constrained("internships")->onDelete("cascade");
|
$table->foreignId("internship_id")->nullable(false)->constrained("internships")->onDelete("cascade");
|
||||||
$table->enum("status", ["SUBMITTED", "CONFIRMED", "DENIED", "DEFENDED", "NOT_DEFENDED"])->nullable(false)->default("SUBMITTED");
|
$table->enum("status", InternshipStatus::all())->nullable(false)->default(InternshipStatus::SUBMITTED);
|
||||||
$table->dateTimeTz("changed")->nullable(false);
|
$table->dateTimeTz("changed")->nullable(false);
|
||||||
$table->string("note")->nullable(true)->default(null);
|
$table->string("note")->nullable(true)->default(null);
|
||||||
$table->foreignId("modified_by")->nullable(false)->constrained("users")->onDelete("cascade");
|
$table->foreignId("modified_by")->nullable(false)->constrained("users")->onDelete("cascade");
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ namespace Database\Seeders;
|
|||||||
|
|
||||||
use App\Models\Company;
|
use App\Models\Company;
|
||||||
use App\Models\Internship;
|
use App\Models\Internship;
|
||||||
use App\Models\InternshipStatus;
|
use App\Models\InternshipStatusData;
|
||||||
use App\Models\StudentData;
|
use App\Models\StudentData;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||||
@@ -28,7 +28,7 @@ class DatabaseSeeder extends Seeder
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// create employers and companies
|
// create employers and companies
|
||||||
User::factory(10)
|
User::factory(20)
|
||||||
->create([
|
->create([
|
||||||
'role' => 'EMPLOYER'
|
'role' => 'EMPLOYER'
|
||||||
])
|
])
|
||||||
@@ -39,26 +39,35 @@ class DatabaseSeeder extends Seeder
|
|||||||
});
|
});
|
||||||
|
|
||||||
// create students
|
// create students
|
||||||
User::factory(10)
|
User::factory(20)
|
||||||
->create([
|
->create([
|
||||||
'role' => 'STUDENT'
|
'role' => 'STUDENT'
|
||||||
])
|
])
|
||||||
->each(function ($user) use ($admin) {
|
->each(function ($user) use ($admin) {
|
||||||
|
$user->update([
|
||||||
|
'email' => fake()->unique()->userName() . '@student.ukf.sk',
|
||||||
|
]);
|
||||||
|
|
||||||
StudentData::factory()->create([
|
StudentData::factory()->create([
|
||||||
'user_id' => $user->id
|
'user_id' => $user->id
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$internship = Internship::factory()->create([
|
$internship = Internship::factory()->create([
|
||||||
'user_id' => $user->id,
|
'user_id' => $user->id,
|
||||||
'company_id' => Company::inRandomOrder()->value('id'),
|
'company_id' => Company::inRandomOrder()->value('id'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
InternshipStatus::factory()->create([
|
InternshipStatusData::factory()->create([
|
||||||
'internship_id' => $internship->id,
|
'internship_id' => $internship->id,
|
||||||
'status' => "SUBMITTED",
|
'status' => "SUBMITTED",
|
||||||
'note' => 'made by seeder',
|
'note' => 'made by seeder',
|
||||||
'modified_by' => $admin->id,
|
'modified_by' => $admin->id,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// create some random external API keys
|
||||||
|
for ($i = 0; $i < 4; $i++) {
|
||||||
|
$admin->createToken(fake()->userName());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
@include("parts.header")
|
||||||
|
<p>Vážená/ý {{ $name }},</p>
|
||||||
|
<p>úspešne ste aktivovali váš účet!</p>
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<p>s pozdravom</p>
|
||||||
|
<p>Systém ISOP UKF</p>
|
||||||
|
@include("parts.footer")
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
@include("parts.header")
|
||||||
|
|
||||||
|
@if($recipiantIsStudent)
|
||||||
|
<p>Vážená/ý {{ $internship->student->name }},</p>
|
||||||
|
@else
|
||||||
|
<p>Vážená/ý {{ $internship->company->contactPerson->name }},</p>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<p>oznamujeme Vás o zmene praxe:</p>
|
||||||
|
@if(!$recipiantIsStudent)
|
||||||
|
<p>Študent: <em>{{ $internship->student->name }} ({{ $internship->student->email }},
|
||||||
|
{{ $internship->student->phone }})</em></p>
|
||||||
|
@endif
|
||||||
|
<p>Stav: <em>{{ $oldStatus }}</em> <strong>-></strong> <em>{{ $newStatus }}</em></p>
|
||||||
|
<p>Poznámka: <em>{{ $note }}</em>.</p>
|
||||||
|
<p>Zmenu vykonal <em>{{ $changedBy->name }}</em>.</p>
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<p>S pozdravom.</p>
|
||||||
|
<p>Systém ISOP UKF</p>
|
||||||
|
@include("parts.footer")
|
||||||
@@ -3,7 +3,12 @@
|
|||||||
<p>vaša registrácia do systému ISOP UKF prebehla úspešne!</p>
|
<p>vaša registrácia do systému ISOP UKF prebehla úspešne!</p>
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<p>Vaše heslo je: <em>{{ $password }}</em></p>
|
<p>Aktivujte účet pomocou nasledujúceho linku:</p>
|
||||||
|
<br />
|
||||||
|
<p>
|
||||||
|
<a
|
||||||
|
href="{{ config('app.frontend_url') }}/account/activation/{{ $activation_token }}">{{ config('app.frontend_url') }}/account/activation/{{ $activation_token }}</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
|
|||||||
287
backend/resources/views/proof/default.blade.php
Normal file
287
backend/resources/views/proof/default.blade.php
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="sk">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Dohoda o odbornej praxi študenta</title>
|
||||||
|
<style>
|
||||||
|
@page {
|
||||||
|
margin: 2cm;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: "Calibri", Times, serif;
|
||||||
|
font-size: 11pt;
|
||||||
|
line-height: 1.1;
|
||||||
|
color: #000;
|
||||||
|
max-width: 210mm;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20mm;
|
||||||
|
background: #fff;
|
||||||
|
text-align: justify;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
text-align: right;
|
||||||
|
font-style: italic;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 11pt;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.university-info,
|
||||||
|
.provider-info,
|
||||||
|
.student-info {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 11pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indented {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-line {
|
||||||
|
border-bottom: 1px dotted #000;
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.roman-numeral {
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 25px 0 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-list {
|
||||||
|
margin-left: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#signature-section {
|
||||||
|
margin-top: 50px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-block {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-line {
|
||||||
|
border-top: 1px dotted #000;
|
||||||
|
margin-top: 40px;
|
||||||
|
padding-top: 5px;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-line {
|
||||||
|
display: inline-block;
|
||||||
|
border-bottom: 1px dotted #000;
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="header-right">
|
||||||
|
Platnosť tlačiva od 1.10.2024 (aplikovaná informatika)
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="title">
|
||||||
|
DOHODA O ODBORNEJ PRAXI ŠTUDENTA
|
||||||
|
</div>
|
||||||
|
<div class="subtitle">
|
||||||
|
uzatvorená v zmysle § 51 Občianskeho zákonníka a Zákona č. 131/2002 Z.z. o vysokých školách
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="university-info">
|
||||||
|
<strong>Univerzita Konštantína Filozofa v Nitre</strong><br>
|
||||||
|
<div class="indented">
|
||||||
|
Fakulta prírodných vied a informatiky<br>
|
||||||
|
Trieda A. Hlinku 1, 949 01 Nitra<br>
|
||||||
|
v zastúpení prof. RNDr. František Petrovič, PhD., MBA - dekan fakulty<br>
|
||||||
|
e-mail: dfpvai@ukf.sk tel. 037/6408 555
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="provider-info">
|
||||||
|
<strong>Poskytovateľ odbornej praxe (organizácia, resp. inštitúcia)</strong><br>
|
||||||
|
<div class="indented">
|
||||||
|
<em>{{ $company->name }}</em> v zastúpení <em>{{ $companyContact->name }}</em>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="student-info">
|
||||||
|
<strong>Študent:</strong><br>
|
||||||
|
<div class="indented">
|
||||||
|
<table style="border: none; width: 100%; line-height: 1;">
|
||||||
|
<tr>
|
||||||
|
<td style="width: 260px;">Meno a priezvisko:</td>
|
||||||
|
<td><em>{{ $student->name }}</em></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Adresa trvalého bydliska:</td>
|
||||||
|
<td><em>{{ $student_address }}</em></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Kontakt študenta FPVaI UKF v Nitre:</td>
|
||||||
|
<td><em>{{ $student->email }}</em></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Študijný program:</td>
|
||||||
|
<td>aplikovaná informatika</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin: 20px 0;">
|
||||||
|
uzatvárajú túto dohodu o odbornej praxi študenta.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="roman-numeral">I. Predmet dohody</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
Predmetom tejto dohody je vykonanie odbornej praxe študenta v rozsahu 150 hodín, v termíne od
|
||||||
|
<em>{{ \Carbon\Carbon::parse($internship->start)->format('d.m.Y') }}</em> do
|
||||||
|
<em>{{ \Carbon\Carbon::parse($internship->end)->format('d.m.Y') }}</em> bezodplatne.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="roman-numeral">II. Práva a povinnosti účastníkov dohody</div>
|
||||||
|
|
||||||
|
<div class="section-header">1. Fakulta prírodných vied a informatiky Univerzity Konštantína Filozofa v Nitre:
|
||||||
|
</div>
|
||||||
|
<div class="list-item">
|
||||||
|
1.1 Poverí svojho zamestnanca: Mgr. Dominik Halvoník, PhD. (ďalej garant odbornej praxe) garanciou odbornej
|
||||||
|
praxe.
|
||||||
|
</div>
|
||||||
|
<div class="list-item">
|
||||||
|
1.2 Prostredníctvom garanta odbornej praxe:
|
||||||
|
<div class="sub-list">
|
||||||
|
a) poskytne študentovi:
|
||||||
|
<div style="margin-left: 20px;">
|
||||||
|
- informácie o organizácii praxe, o podmienkach dojednania dohody o odbornej praxi, o obsahovom
|
||||||
|
zameraní odbornej praxe a o požiadavkách na obsahovú náplň správy z odbornej praxe,<br>
|
||||||
|
- návrh dohody o odbornej praxi študenta,
|
||||||
|
</div>
|
||||||
|
b) rozhodne o udelení hodnotenia „ABS" (absolvoval) študentovi na základe dokladu „Výkaz o vykonanej
|
||||||
|
odbornej praxi", vydaného poskytovateľom odbornej praxe a na základe študentom vypracovanej správy o
|
||||||
|
odbornej praxi, ktorej súčasťou je verejná obhajoba výsledkov odbornej praxe,<br><br>
|
||||||
|
c) spravuje vyplnenú a účastníkmi podpísanú dohodu o odbornej praxi.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-header">2. Poskytovateľ odbornej praxe:</div>
|
||||||
|
<div class="list-item">
|
||||||
|
2.1 poverí svojho zamestnanca (tútor - zodpovedný za odbornú prax v organizácii)
|
||||||
|
<em>{{ $companyContact->name }}</em>, ktorý bude dohliadať na dodržiavanie dohody o
|
||||||
|
odbornej praxi, plnenie obsahovej náplne odbornej praxe a bude nápomocný pri získavaní potrebných údajov pre
|
||||||
|
vypracovanie správy z odbornej praxe,
|
||||||
|
</div>
|
||||||
|
<div class="list-item">
|
||||||
|
2.2 na začiatku praxe vykoná poučenie o bezpečnosti a ochrane zdravia pri práci v zmysle platných predpisov,
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="page-break-before: always;"></div>
|
||||||
|
|
||||||
|
<div class="list-item">
|
||||||
|
2.3 vzniknuté organizačné problémy súvisiace s plnením dohody rieši spolu s garantom odbornej praxe,
|
||||||
|
</div>
|
||||||
|
<div class="list-item">
|
||||||
|
2.4 po ukončení odbornej praxe vydá študentovi „Výkaz o vykonanej odbornej praxi", ktorý obsahuje popis
|
||||||
|
vykonávaných činností a stručné hodnotenie študenta a je jedným z predpokladov úspešného ukončenia predmetu
|
||||||
|
Odborná prax,
|
||||||
|
</div>
|
||||||
|
<div class="list-item">
|
||||||
|
2.5 umožní garantovi odbornej praxe a garantovi študijného predmetu kontrolu študentom plnených úloh.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-header">3. Študent FPVaI UKF v Nitre:</div>
|
||||||
|
<div class="list-item">
|
||||||
|
3.1 osobne zabezpečí podpísanie tejto dohody o odbornej praxi študenta,
|
||||||
|
</div>
|
||||||
|
<div class="list-item">
|
||||||
|
3.2 zodpovedne vykonáva činnosti pridelené tútorom odbornej praxe,
|
||||||
|
</div>
|
||||||
|
<div class="list-item">
|
||||||
|
3.3 zabezpečí doručenie dokladu „Výkaz o vykonanej odbornej praxi" najneskôr v termínoch predpísaných
|
||||||
|
garantom pre daný semester,
|
||||||
|
</div>
|
||||||
|
<div class="list-item">
|
||||||
|
3.4 okamžite, bez zbytočného odkladu informuje garanta odbornej praxe o problémoch, ktoré bránia plneniu
|
||||||
|
odbornej praxe.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="roman-numeral">III. Všeobecné a záverečné ustanovenia</div>
|
||||||
|
|
||||||
|
<div class="list-item">
|
||||||
|
1. Dohoda sa uzatvára na dobu určitú. Dohoda nadobúda platnosť a účinnosť dňom podpísania obidvomi zmluvnými
|
||||||
|
stranami. Obsah dohody sa môže meniť písomne len po súhlase jej zmluvných strán.
|
||||||
|
</div>
|
||||||
|
<div class="list-item">
|
||||||
|
2. Diela vytvorené študentom sa spravujú režimom zamestnaneckého diela podľa § 90 zákona č. 185/2015 Z. z.
|
||||||
|
(Autorský zákon). V prípade, že sa dielo stane školským dielom podľa § 93 citovaného zákona, Fakulta
|
||||||
|
prírodných vied a informatiky Univerzity Konštantína Filozofa v Nitre týmto udeľuje Poskytovateľovi odbornej
|
||||||
|
praxe výhradnú, časovo a teritoriálne neobmedzenú, bezodplatnú licenciu na akékoľvek použitie alebo
|
||||||
|
sublicenciu diel vytvorených študentom počas trvania odbornej praxe.
|
||||||
|
</div>
|
||||||
|
<div class="list-item">
|
||||||
|
3. Dohoda sa uzatvára v 3 vyhotoveniach, každá zmluvná strana obdrží jedno vyhotovenie dohody.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="signature-section" style="margin-top: 40px;">
|
||||||
|
<div>V Nitre, dňa <em>{{ \Carbon\Carbon::parse(now())->format('d.m.Y') }}</em>.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; justify-content: space-between; margin-top: 60px;">
|
||||||
|
<div class="signature-block">
|
||||||
|
<div class="signature-line">
|
||||||
|
prof. RNDr. František Petrovič, PhD., MBA<br>
|
||||||
|
dekan FPVaI UKF v Nitre
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<div class="signature-block">
|
||||||
|
<div class="signature-line">
|
||||||
|
{{ $companyContact->name }}<br>
|
||||||
|
štatutárny zástupca pracoviska odb. praxe
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="text-align: center; margin-top: 60px;">
|
||||||
|
<div class="signature-line" style="display: inline-block;">
|
||||||
|
{{ $student->name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Http\Controllers\Auth\RegisteredUserController;
|
use App\Http\Controllers\Auth\RegisteredUserController;
|
||||||
|
use App\Http\Controllers\CompanyController;
|
||||||
|
use App\Http\Controllers\ExternalApiController;
|
||||||
|
use App\Http\Controllers\InternshipController;
|
||||||
|
use App\Http\Controllers\StudentDataController;
|
||||||
|
use App\Http\Controllers\InternshipStatusDataController;
|
||||||
|
use App\Http\Middleware\AdministratorOnly;
|
||||||
use App\Models\Company;
|
use App\Models\Company;
|
||||||
use App\Models\StudentData;
|
use App\Models\StudentData;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@@ -18,6 +24,61 @@ Route::middleware(['auth:sanctum'])->get('/user', function (Request $request) {
|
|||||||
return $user;
|
return $user;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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::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'])
|
Route::post('/password-reset', [RegisteredUserController::class, 'reset_password'])
|
||||||
->middleware(['guest', 'throttle:6,1'])
|
->middleware(['guest', 'throttle:6,1'])
|
||||||
->name('password.reset');
|
->name('password.reset');
|
||||||
|
|
||||||
|
Route::prefix('/internships')->group(function () {
|
||||||
|
Route::get("/", [InternshipController::class, 'all'])->middleware(['auth:sanctum'])->name("api.internships");
|
||||||
|
Route::get("/export", [InternshipController::class, 'export'])->middleware(AdministratorOnly::class)->name("api.internships.export");
|
||||||
|
|
||||||
|
Route::prefix('/{id}')->middleware("auth:sanctum")->group(function () {
|
||||||
|
Route::get("/", [InternshipController::class, 'get'])->name("api.internships.get");
|
||||||
|
Route::delete("/", [InternshipController::class, 'destroy'])->middleware(AdministratorOnly::class)->name("api.internships.delete");
|
||||||
|
Route::put("/status", [InternshipStatusDataController::class, 'update'])->name("api.internships.status.update");
|
||||||
|
Route::get("/statuses", [InternshipStatusDataController::class, 'get'])->name("api.internships.get");
|
||||||
|
Route::get("/next-statuses", [InternshipStatusDataController::class, 'get_next_states'])->name("api.internships.status.next.get");
|
||||||
|
Route::get("/default-proof", [InternshipController::class, 'get_default_proof'])->name("api.internships.proof.default.get");
|
||||||
|
Route::get("/proof", [InternshipController::class, 'get_proof'])->name("api.internships.proof.get");
|
||||||
|
Route::get("/report", [InternshipController::class, 'get_report'])->name("api.internships.report.get");
|
||||||
|
Route::post("/documents", [InternshipController::class, 'update_documents'])->name("api.internships.documents.set");
|
||||||
|
Route::post("/basic", [InternshipController::class, 'update_basic'])->name("api.internships.update.basic");
|
||||||
|
});
|
||||||
|
|
||||||
|
Route::put("/new", [InternshipController::class, 'store'])->name("api.internships.create");
|
||||||
|
});
|
||||||
|
|
||||||
|
Route::prefix('/companies')->middleware("auth:sanctum")->group(function () {
|
||||||
|
Route::get("/simple", [CompanyController::class, 'all_simple']);
|
||||||
|
Route::get("/{id}", [CompanyController::class, 'get']);
|
||||||
|
Route::post("/{id}", [CompanyController::class, 'update_all']);
|
||||||
|
Route::delete("/{id}", [CompanyController::class, 'delete']);
|
||||||
|
});
|
||||||
|
|
||||||
|
Route::prefix('/external')->group(function () {
|
||||||
|
Route::prefix('/keys')->middleware(['auth:sanctum', AdministratorOnly::class])->group(function () {
|
||||||
|
Route::get("/", [ExternalApiController::class, 'all_keys'])->name("api.external.keys.create");
|
||||||
|
Route::put("/", [ExternalApiController::class, 'create_key'])->name("api.external.keys.list");
|
||||||
|
Route::delete("/{id}", [ExternalApiController::class, 'destroy_key'])->name("api.external.keys.delete");
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
Route::prefix('/internships')->group(function () {
|
||||||
|
Route::prefix('/{id}')->middleware("auth:sanctum")->group(function () {
|
||||||
|
Route::put("/status", [ExternalApiController::class, 'update_internship_status'])->name("api.external.internships.status.update");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
5
frontend/.gitignore
vendored
5
frontend/.gitignore
vendored
@@ -22,3 +22,8 @@ logs
|
|||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
|
||||||
|
# Cypress
|
||||||
|
cypress/screenshots
|
||||||
|
cypress/videos
|
||||||
|
cypress/downloads
|
||||||
21
frontend/app/components/ErrorAlert.vue
Normal file
21
frontend/app/components/ErrorAlert.vue
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<v-alert density="compact" :text="error" :title="title || 'Chyba'" type="error" class="mb-2 mt-2"></v-alert>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: "Chyba"
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
20
frontend/app/components/InfoAlert.vue
Normal file
20
frontend/app/components/InfoAlert.vue
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<v-alert density="compact" :text="text" :title="title" type="info" class="mb-2 mt-2"></v-alert>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
39
frontend/app/components/InternshipAgreementDownloader.vue
Normal file
39
frontend/app/components/InternshipAgreementDownloader.vue
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { FetchError } from 'ofetch';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
internship_id: number,
|
||||||
|
block?: boolean,
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const client = useSanctumClient();
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
async function requestDownload() {
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const proof = await client<Blob>(`/api/internships/${props.internship_id}/default-proof`);
|
||||||
|
triggerDownload(proof, `default-proof-${props.internship_id}`, 'pdf');
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof FetchError) {
|
||||||
|
alert(`Nepodarilo sa vygenerovať zmluvu: ${e.statusMessage}`);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<v-btn prepend-icon="mdi-download" color="blue" class="mr-2 mt-2 mb-2" :disabled="loading"
|
||||||
|
:block="block ?? undefined" @click="requestDownload">
|
||||||
|
<span v-show="!loading">Stiahnuť originálnu zmluvu</span>
|
||||||
|
<span v-show="loading">Prosím čakajte...</span>
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
149
frontend/app/components/InternshipDocumentEditor.vue
Normal file
149
frontend/app/components/InternshipDocumentEditor.vue
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { Internship } from '~/types/internships';
|
||||||
|
import { FetchError } from 'ofetch';
|
||||||
|
import type { User } from '~/types/user';
|
||||||
|
import { Role } from '~/types/role';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
internship: Internship
|
||||||
|
}>();
|
||||||
|
const emit = defineEmits(['successfulSubmit']);
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
isPdf: (v: File | null) =>
|
||||||
|
!v || v.type === "application/pdf" || 'Povolený je iba PDF súbor.',
|
||||||
|
maxSize: (v: File | null) =>
|
||||||
|
!v || v.size <= (10 * 1024 * 1024 /* 10 MB */) || 'Maximálna veľkosť súboru je 10 MB.',
|
||||||
|
};
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const error = ref<string | null>(null);
|
||||||
|
const proof = ref<File | null>(null);
|
||||||
|
const report = ref<File | null>(null);
|
||||||
|
const report_confirmed = ref(props.internship.report_confirmed);
|
||||||
|
|
||||||
|
const client = useSanctumClient();
|
||||||
|
const user = useSanctumUser<User>();
|
||||||
|
|
||||||
|
function triggerDownload(file: Blob, file_name: string) {
|
||||||
|
const url = window.URL.createObjectURL(file);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = `${file_name}.pdf`;
|
||||||
|
link.target = "_blank";
|
||||||
|
link.click();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadProof() {
|
||||||
|
const proof: Blob = await client(`/api/internships/${props.internship.id}/proof`);
|
||||||
|
triggerDownload(proof, `proof-${props.internship.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadReport() {
|
||||||
|
const report: Blob = await client(`/api/internships/${props.internship.id}/report`);
|
||||||
|
triggerDownload(report, `report-${props.internship.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSubmit() {
|
||||||
|
error.value = null;
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('report_confirmed', report_confirmed.value ? '1' : '0');
|
||||||
|
if (proof.value) {
|
||||||
|
formData.append('proof', proof.value);
|
||||||
|
}
|
||||||
|
if (report.value) {
|
||||||
|
formData.append('report', report.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client(`/api/internships/${props.internship.id}/documents`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
proof.value = null;
|
||||||
|
report.value = null;
|
||||||
|
emit('successfulSubmit');
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof FetchError) {
|
||||||
|
error.value = e.response?._data.message;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- Čakajúca hláška -->
|
||||||
|
<LoadingAlert v-if="loading" />
|
||||||
|
|
||||||
|
<!-- Chybová hláška -->
|
||||||
|
<ErrorAlert v-if="error" :error="error" />
|
||||||
|
|
||||||
|
<v-form @submit.prevent="onSubmit" :disabled="loading">
|
||||||
|
<div>
|
||||||
|
<h4 class="mb-2">Dokument o vykonaní praxe</h4>
|
||||||
|
|
||||||
|
<p>Zmluva/dohoda o brigádnickej praxi alebo 3 faktúry v pre živnostníkov.</p>
|
||||||
|
<InternshipAgreementDownloader :internship_id="internship.id" />
|
||||||
|
|
||||||
|
<WarningAlert v-if="props.internship.proof" title="Existujúci dokument"
|
||||||
|
text="V systéme je už nahratý dokument. Ak chcete nahradiť existujúcu verziu, vyberte súbor, alebo v opačnom prípade nechajte toto pole nevyplnené.">
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<v-btn prepend-icon="mdi-download" color="blue" class="mr-2 mt-2" @click="downloadProof">
|
||||||
|
Stiahnuť
|
||||||
|
</v-btn>
|
||||||
|
</WarningAlert>
|
||||||
|
|
||||||
|
<v-file-input v-model="proof" :rules="[rules.isPdf, rules.maxSize]" accept=".pdf,application/pdf"
|
||||||
|
prepend-icon="mdi-handshake" label="Nahrať PDF dokument" variant="outlined" show-size clearable
|
||||||
|
hint="Povolené: PDF, max 10 MB" persistent-hint />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 class="mb-2">Výkaz</h4>
|
||||||
|
|
||||||
|
<p>Dokument o hodnotení praxe.</p>
|
||||||
|
<v-btn prepend-icon="mdi-download" color="blue" class="mr-2 mt-2 mb-2" target="_blank"
|
||||||
|
href="https://www.fpvai.ukf.sk/images/Organizacia_studia/odborna_prax/aplikovana_informatika/Priloha_Vykaz_o_vykonanej_odbornej_praxi-AI.docx">
|
||||||
|
<span>Stiahnuť šablónu na výkaz</span>
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<WarningAlert v-if="props.internship.report" title="Existujúci dokument"
|
||||||
|
text="V systéme je už nahratý výkaz. Ak chcete nahradiť existujúcu verziu, vyberte súbor, alebo v opačnom prípade nechajte toto pole nevyplnené.">
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<v-btn prepend-icon="mdi-download" color="blue" class="mr-2 mt-2" @click="downloadReport">
|
||||||
|
Stiahnuť
|
||||||
|
</v-btn>
|
||||||
|
</WarningAlert>
|
||||||
|
|
||||||
|
<v-file-input v-model="report" :rules="[rules.isPdf, rules.maxSize]" accept=".pdf,application/pdf"
|
||||||
|
prepend-icon="mdi-chart-box-outline" label="Nahrať PDF výkaz" variant="outlined" show-size clearable
|
||||||
|
hint="Povolené: PDF, max 10 MB" persistent-hint />
|
||||||
|
|
||||||
|
<v-checkbox v-if="user?.role === Role.EMPLOYER"
|
||||||
|
:disabled="!props.internship.proof || !props.internship.report" v-model="report_confirmed"
|
||||||
|
label="Výkaz je správny"></v-checkbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<v-btn type="submit" color="success" size="large" block
|
||||||
|
:disabled="!proof && !report && (!props.internship.proof || !props.internship.report)">
|
||||||
|
Uloziť
|
||||||
|
</v-btn>
|
||||||
|
</v-form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
83
frontend/app/components/InternshipDocumentViewer.vue
Normal file
83
frontend/app/components/InternshipDocumentViewer.vue
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { Internship } from '~/types/internships';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
internship: Internship
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const client = useSanctumClient();
|
||||||
|
|
||||||
|
async function downloadAgreement() {
|
||||||
|
const proof: Blob = await client(`/api/internships/${props.internship.id}/proof`);
|
||||||
|
triggerDownload(proof, `proof-${props.internship.id}`, 'pdf');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadReport() {
|
||||||
|
const report: Blob = await client(`/api/internships/${props.internship.id}/report`);
|
||||||
|
triggerDownload(report, `report-${props.internship.id}`, 'pdf');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<v-row class="d-flex">
|
||||||
|
<!-- Podpísaný dokument k praxe -->
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-card variant="outlined" class="h-100">
|
||||||
|
<v-card-title class="d-flex align-center ga-2">
|
||||||
|
<v-icon icon="mdi mdi-file-document-outline" />
|
||||||
|
Dokument o vykonaní praxe
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text>
|
||||||
|
<InternshipAgreementDownloader :internship_id="internship.id" block />
|
||||||
|
|
||||||
|
<WarningAlert v-if="!props.internship.proof" title="Neodovzdané"
|
||||||
|
text="Dokument zatiaľ nebol nahratý." />
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<SuccessAlert title="Odovzdané" text="Zmluva bola nahratá." />
|
||||||
|
|
||||||
|
<v-btn prepend-icon="mdi-download" color="blue" class="mr-2 mt-2" block
|
||||||
|
@click="downloadAgreement">
|
||||||
|
Stiahnuť
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<!-- Výkaz -->
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-card variant="outlined" class="h-100">
|
||||||
|
<v-card-title class="d-flex align-center ga-2">
|
||||||
|
<v-icon icon="mdi-file-clock-outline" />
|
||||||
|
Výkaz
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text>
|
||||||
|
<v-btn prepend-icon="mdi-download" color="blue" class="mr-2 mt-2" block target="_blank"
|
||||||
|
href="https://www.fpvai.ukf.sk/images/Organizacia_studia/odborna_prax/aplikovana_informatika/Priloha_Vykaz_o_vykonanej_odbornej_praxi-AI.docx">
|
||||||
|
<span>Stiahnuť šablónu na výkaz</span>
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<WarningAlert v-if="!props.internship.report" title="Neodovzdané"
|
||||||
|
text="Výkaz zatiaľ nebol nahratý." />
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<ErrorAlert v-if="!props.internship.report_confirmed" title="Nepotvrdené"
|
||||||
|
error="Výkaz bol nahratý, ale zatiaľ nebol potvrdený firmou." />
|
||||||
|
|
||||||
|
<SuccessAlert v-else title="Potvrdené" text="Výkaz bol nahratý, aj potvrdený firmou." />
|
||||||
|
|
||||||
|
<v-btn prepend-icon="mdi-download" color="blue" class="mr-2 mt-2" block
|
||||||
|
@click="downloadReport">
|
||||||
|
Stiahnuť
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
140
frontend/app/components/InternshipEditor.vue
Normal file
140
frontend/app/components/InternshipEditor.vue
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { CompanyData } from '~/types/company_data';
|
||||||
|
import { convertDate, type Internship, type NewInternship } from '~/types/internships';
|
||||||
|
import type { User } from '~/types/user';
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
required: (v: any) => (!!v && String(v).trim().length > 0) || 'Povinné pole',
|
||||||
|
mustAgree: (v: boolean) => v === true || 'Je potrebné potvrdiť',
|
||||||
|
};
|
||||||
|
|
||||||
|
const year_of_study_choices = [
|
||||||
|
{
|
||||||
|
title: '1',
|
||||||
|
subtitle: 'bakalárske',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '2',
|
||||||
|
subtitle: 'bakalárske',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '3',
|
||||||
|
subtitle: 'bakalárske',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '4',
|
||||||
|
subtitle: 'magisterské',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '5',
|
||||||
|
subtitle: 'magisterské',
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const semester_choices = [
|
||||||
|
{
|
||||||
|
title: "Zimný",
|
||||||
|
value: "WINTER"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Letný",
|
||||||
|
value: "SUMMER"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
internship?: Internship,
|
||||||
|
submit: (new_internship: NewInternship) => void,
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const isValid = ref(false);
|
||||||
|
const form = ref({
|
||||||
|
start: props.internship?.start ? convertDate(props.internship.start) : null,
|
||||||
|
end: props.internship?.end ? convertDate(props.internship.end) : null,
|
||||||
|
year_of_study: props.internship?.year_of_study || 2025,
|
||||||
|
semester: props.internship?.semester || "WINTER",
|
||||||
|
company_id: props.internship?.company?.id == undefined ? null : props.internship.company.id,
|
||||||
|
description: props.internship?.position_description || "",
|
||||||
|
consent: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = useSanctumUser<User>();
|
||||||
|
|
||||||
|
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: dateTimeFixup(form.value.start!),
|
||||||
|
end: dateTimeFixup(form.value.end!),
|
||||||
|
year_of_study: form.value.year_of_study,
|
||||||
|
semester: form.value.semester,
|
||||||
|
position_description: form.value.description
|
||||||
|
};
|
||||||
|
|
||||||
|
props.submit(new_internship);
|
||||||
|
}
|
||||||
|
|
||||||
|
function companyListProps(company: CompanyData) {
|
||||||
|
return {
|
||||||
|
title: company.name,
|
||||||
|
subtitle: `IČO: ${company.ico}, Zodpovedný: ${company.contact.name}, ${!company.hiring ? "ne" : ""}prijímajú nových študentov`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function yearOfStudyValueHandler(item: { title: string, subtitle: string }) {
|
||||||
|
return parseInt(item.title) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, pending, error } = await useLazySanctumFetch<CompanyData[]>('/api/companies/simple');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-form v-model="isValid" @submit.prevent="triggerSubmit">
|
||||||
|
<v-date-input v-model="form.start" :rules="[rules.required]" clearable label="Začiatok"></v-date-input>
|
||||||
|
<v-date-input v-model="form.end" :rules="[rules.required]" clearable label="Koniec"></v-date-input>
|
||||||
|
|
||||||
|
<v-select v-model="form.year_of_study" clearable label="Ročník" :items="year_of_study_choices"
|
||||||
|
:item-props="(item) => { return { title: item.title, subtitle: item.subtitle } }"
|
||||||
|
:item-value="yearOfStudyValueHandler" :rules="[rules.required]"></v-select>
|
||||||
|
|
||||||
|
<v-select v-model="form.semester" clearable label="Semester" :items="semester_choices"
|
||||||
|
:rules="[rules.required]"></v-select>
|
||||||
|
|
||||||
|
<!-- Výber firmy -->
|
||||||
|
|
||||||
|
<!-- Čakajúca hláška -->
|
||||||
|
<LoadingAlert v-if="pending" />
|
||||||
|
|
||||||
|
<!-- Chybová hláška -->
|
||||||
|
<ErrorAlert v-else-if="error" :error="error.message" />
|
||||||
|
|
||||||
|
<v-select v-else v-model="form.company_id" clearable label="Firma" :items="data" :item-props="companyListProps"
|
||||||
|
item-value="id" :rules="[rules.required]"></v-select>
|
||||||
|
|
||||||
|
<v-textarea v-model="form.description" clearable label="Popis práce" :rules="[rules.required]"></v-textarea>
|
||||||
|
|
||||||
|
<v-checkbox v-model="form.consent" :rules="[rules.mustAgree]" label="Potvrdzujem, že zadané údaje sú pravdivé"
|
||||||
|
density="comfortable" />
|
||||||
|
|
||||||
|
<v-btn type="submit" color="success" size="large" block :disabled="!isValid || !form.consent">
|
||||||
|
Uloziť
|
||||||
|
</v-btn>
|
||||||
|
</v-form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
form {
|
||||||
|
width: 80%;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
181
frontend/app/components/InternshipListView.vue
Normal file
181
frontend/app/components/InternshipListView.vue
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { Internship, InternshipFilter } from '~/types/internships';
|
||||||
|
import type { Paginated } from '~/types/pagination';
|
||||||
|
import { prettyInternshipStatus } from '~/types/internship_status';
|
||||||
|
import { FetchError } from 'ofetch';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
mode: 'admin' | 'company' | 'student';
|
||||||
|
}>();
|
||||||
|
const emit = defineEmits<{
|
||||||
|
filterApplied: [value: InternshipFilter],
|
||||||
|
itemsAvailable: [value: boolean]
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const filters = ref<InternshipFilter>({
|
||||||
|
year: null,
|
||||||
|
company: null,
|
||||||
|
study_programe: null,
|
||||||
|
student: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const page = ref(1);
|
||||||
|
const itemsPerPage = ref(15);
|
||||||
|
const totalItems = ref(0);
|
||||||
|
const deleteConfirmDialog = ref(false);
|
||||||
|
const internshipToDelete = ref<Internship | null>(null);
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
minFilterLen: (v: string) => (v.length >= 3) || 'Min. 3 znaky',
|
||||||
|
minYear: (v: number | null) => (v === null ? true : v >= 1000) || 'Min. 4-ciferné číslo'
|
||||||
|
};
|
||||||
|
|
||||||
|
const allHeaders = [
|
||||||
|
{ title: "Študent", key: "student.name", sortable: false },
|
||||||
|
{ title: "Firma", key: "company.name", sortable: false },
|
||||||
|
{ title: "Od", key: "start", sortable: false },
|
||||||
|
{ title: "Do", key: "end", sortable: false },
|
||||||
|
{ title: "Semester", key: "semester", sortable: false },
|
||||||
|
{ title: "Ročník", key: "year_of_study", sortable: false },
|
||||||
|
{ title: "Študijný odbor", key: "student.student_data.study_field", sortable: false },
|
||||||
|
{ title: "Stav", key: "status", sortable: false },
|
||||||
|
{ title: "Operácie", key: "operations", sortable: false }
|
||||||
|
];
|
||||||
|
|
||||||
|
const headers = props.mode === 'company'
|
||||||
|
? allHeaders.filter(header => header.key !== "company.name")
|
||||||
|
: allHeaders;
|
||||||
|
|
||||||
|
const client = useSanctumClient();
|
||||||
|
const { data, error, pending, refresh } = await useLazySanctumFetch<Paginated<Internship>>('/api/internships', () => ({
|
||||||
|
params: {
|
||||||
|
...filters.value,
|
||||||
|
page: page.value,
|
||||||
|
per_page: itemsPerPage.value,
|
||||||
|
}
|
||||||
|
}), {
|
||||||
|
watch: [filters.value, page, itemsPerPage]
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(data, (newData) => {
|
||||||
|
totalItems.value = newData?.total ?? 0;
|
||||||
|
emit('itemsAvailable', totalItems.value > 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(filters, async () => {
|
||||||
|
page.value = 1;
|
||||||
|
emit('filterApplied', filters.value);
|
||||||
|
}, { deep: true });
|
||||||
|
|
||||||
|
async function delteInternship(internship: Internship) {
|
||||||
|
pending.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client(`/api/internships/${internship.id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
await refresh();
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof FetchError) {
|
||||||
|
alert(`Chyba pri mazaní stáže: ${e.statusMessage ?? e.message}`);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
pending.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDeleteDialog(internship: Internship) {
|
||||||
|
internshipToDelete.value = internship;
|
||||||
|
deleteConfirmDialog.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmDeletion(confirm: boolean) {
|
||||||
|
if (confirm && internshipToDelete.value) {
|
||||||
|
await delteInternship(internshipToDelete.value);
|
||||||
|
}
|
||||||
|
deleteConfirmDialog.value = false;
|
||||||
|
internshipToDelete.value = null;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<ErrorAlert v-if="error" :error="error.statusMessage ?? error.message" />
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="3">
|
||||||
|
<v-text-field v-model="filters.year" label="Rok" type="number" clearable density="compact"
|
||||||
|
:rules="[rules.minYear]" />
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="3" v-if="mode !== 'company'">
|
||||||
|
<v-text-field v-model="filters.company" label="Názov firmy" clearable density="compact"
|
||||||
|
:rules="[rules.minFilterLen]" />
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="3" v-if="mode !== 'student'">
|
||||||
|
<v-text-field v-model="filters.study_programe" label="Študijný program" clearable density="compact"
|
||||||
|
:rules="[rules.minFilterLen]" />
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="3" v-if="mode !== 'student'">
|
||||||
|
<v-text-field v-model="filters.student" label="Študent" clearable density="compact"
|
||||||
|
:rules="[rules.minFilterLen]" />
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-data-table-server v-model:items-per-page="itemsPerPage" v-model:page="page" :headers="headers"
|
||||||
|
:items="data?.data" :items-length="totalItems" :loading="pending">
|
||||||
|
|
||||||
|
<template #item.status="{ item }">
|
||||||
|
{{ prettyInternshipStatus(item.status.status) }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #item.semester="{ item }">
|
||||||
|
{{ item.semester === "WINTER" ? "Zimný" : "Letný" }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #item.operations="{ item }">
|
||||||
|
<v-tooltip text="Editovať">
|
||||||
|
<template #activator="{ props }">
|
||||||
|
<v-btn icon="mdi-pencil" size="small" variant="text"
|
||||||
|
:to="`/dashboard/${mode}/internships/edit/${item.id}`" class="internship-edit-btn" />
|
||||||
|
</template>
|
||||||
|
</v-tooltip>
|
||||||
|
<v-tooltip text="Vymazať" v-if="mode === 'admin'">
|
||||||
|
<template #activator="{ props }">
|
||||||
|
<v-btn icon="mdi-delete" size="small" variant="text" color="error"
|
||||||
|
@click="() => openDeleteDialog(item)" class="internship-delete-btn" />
|
||||||
|
</template>
|
||||||
|
</v-tooltip>
|
||||||
|
</template>
|
||||||
|
</v-data-table-server>
|
||||||
|
|
||||||
|
<!-- Delete confirm dialog -->
|
||||||
|
<v-dialog v-model="deleteConfirmDialog" max-width="500px">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="text-h5">
|
||||||
|
Potvrdiť vymazanie praxe
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text>
|
||||||
|
<p>Ste si istý, že chcete vymazať tento záznam?</p>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn color="red" variant="text" @click="async () => await confirmDeletion(true)"
|
||||||
|
:loading="pending">
|
||||||
|
Áno
|
||||||
|
</v-btn>
|
||||||
|
<v-btn color="black" variant="text" @click="async () => await confirmDeletion(false)">
|
||||||
|
Zrusiť
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:deep(.v-data-table-header__content) {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
92
frontend/app/components/InternshipStatusEditor.vue
Normal file
92
frontend/app/components/InternshipStatusEditor.vue
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { Internship } from '~/types/internships';
|
||||||
|
import { InternshipStatus, prettyInternshipStatus, type NewInternshipStatusData } from '~/types/internship_status';
|
||||||
|
import type { User } from '~/types/user';
|
||||||
|
import { FetchError } from 'ofetch';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
internship: Internship
|
||||||
|
}>();
|
||||||
|
const emit = defineEmits(['successfulSubmit']);
|
||||||
|
|
||||||
|
const user = useSanctumUser<User>();
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
required: (v: any) => (!!v && String(v).trim().length > 0) || 'Povinné pole',
|
||||||
|
};
|
||||||
|
|
||||||
|
const isValid = ref(false);
|
||||||
|
const new_state = ref(null as InternshipStatus | null);
|
||||||
|
const note = ref("");
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const save_error = ref(null as null | string);
|
||||||
|
|
||||||
|
const client = useSanctumClient();
|
||||||
|
const { data, error: load_error, refresh } = await useLazySanctumFetch(`/api/internships/${props.internship.id}/next-statuses`, undefined, {
|
||||||
|
transform: (statuses: InternshipStatus[]) => statuses.map((state) => ({
|
||||||
|
title: prettyInternshipStatus(state),
|
||||||
|
value: state
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
save_error.value = null;
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
const new_status: NewInternshipStatusData = {
|
||||||
|
status: new_state.value!,
|
||||||
|
note: note.value,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client(`/api/internships/${props.internship.id}/status`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: new_status
|
||||||
|
});
|
||||||
|
|
||||||
|
new_state.value = null;
|
||||||
|
note.value = "";
|
||||||
|
await refresh();
|
||||||
|
emit('successfulSubmit');
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof FetchError) {
|
||||||
|
save_error.value = e.response?._data.message;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- Čakajúca hláška -->
|
||||||
|
<LoadingAlert v-if="loading" />
|
||||||
|
|
||||||
|
<!-- Chybová hláška -->
|
||||||
|
<ErrorAlert v-if="save_error" :error="`Nepodarilo uložiť: ${save_error}`" />
|
||||||
|
|
||||||
|
<!-- Chybová hláška -->
|
||||||
|
<ErrorAlert v-if="load_error" :error="`Nepodarilo sa načítať stavy: ${load_error}`" />
|
||||||
|
|
||||||
|
<!-- Chybová hláška -->
|
||||||
|
<WarningAlert v-else-if="data?.length === 0" title="Blokované"
|
||||||
|
text="Stav praxe už nie je možné meniť, pretože bola (ne)obhájená alebo zamietnutá. V prípade, že ste prax zamietli omylom, alebo ak máte technické problémy, prosíme kontaktovať garanta praxe." />
|
||||||
|
|
||||||
|
<v-form v-else v-model="isValid" @submit.prevent="submit" :disabled="loading">
|
||||||
|
<v-select v-model="new_state" label="Stav" :items="data" item-value="value"></v-select>
|
||||||
|
<v-text-field v-model="note" :rules="[rules.required]" label="Poznámka"></v-text-field>
|
||||||
|
|
||||||
|
<v-btn type="submit" color="success" size="large" block :disabled="!isValid">
|
||||||
|
Uloziť
|
||||||
|
</v-btn>
|
||||||
|
</v-form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.alert {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
49
frontend/app/components/InternshipStatusHistoryView.vue
Normal file
49
frontend/app/components/InternshipStatusHistoryView.vue
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { prettyInternshipStatus, type InternshipStatusData } from '~/types/internship_status';
|
||||||
|
import type { Internship } from '~/types/internships';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
internship: Internship
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const headers = [
|
||||||
|
{ title: 'Stav', key: 'status', align: 'left' },
|
||||||
|
{ title: 'Zmenené', key: 'changed', align: 'left' },
|
||||||
|
{ title: 'Poznámka', key: 'start', align: 'left' },
|
||||||
|
{ title: 'Zmenu vykonal', key: 'modified_by', align: 'left' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const { data, error, pending, refresh } = await useLazySanctumFetch<InternshipStatusData[]>(`/api/internships/${props.internship.id}/statuses`);
|
||||||
|
|
||||||
|
watch(() => props.internship, async () => {
|
||||||
|
await refresh();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- Čakajúca hláška -->
|
||||||
|
<LoadingAlert v-if="pending" />
|
||||||
|
|
||||||
|
<!-- Chybová hláška -->
|
||||||
|
<ErrorAlert v-if="error" :error="error?.message" />
|
||||||
|
|
||||||
|
<v-table v-else>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th v-for="header in headers" :class="'text-' + header.align">
|
||||||
|
<strong>{{ header.title }}</strong>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="item in data">
|
||||||
|
<td>{{ prettyInternshipStatus(item.status) }}</td>
|
||||||
|
<td>{{ item.changed }}</td>
|
||||||
|
<td>{{ item.note }}</td>
|
||||||
|
<td>{{ item.modified_by.name }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</v-table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
5
frontend/app/components/LoadingAlert.vue
Normal file
5
frontend/app/components/LoadingAlert.vue
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<v-alert density="compact" text="Prosím čakajte..." title="Spracovávam" type="info" class="mb-2 mt-2"></v-alert>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
20
frontend/app/components/SuccessAlert.vue
Normal file
20
frontend/app/components/SuccessAlert.vue
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<v-alert density="compact" :text="text" :title="title" type="success" class="mb-2 mt-2"></v-alert>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
23
frontend/app/components/WarningAlert.vue
Normal file
23
frontend/app/components/WarningAlert.vue
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<v-alert density="compact" :text="text" :title="title" type="warning" class="mb-2 mt-2">
|
||||||
|
<slot />
|
||||||
|
</v-alert>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: "Upozornenie"
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
135
frontend/app/pages/account/activation/[token].vue
Normal file
135
frontend/app/pages/account/activation/[token].vue
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { FetchError } from 'ofetch';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const client = useSanctumClient();
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: ['sanctum:guest'],
|
||||||
|
});
|
||||||
|
|
||||||
|
useSeoMeta({
|
||||||
|
title: "Aktivácia účtu | ISOP",
|
||||||
|
ogTitle: "Aktivácia účtu",
|
||||||
|
description: "Aktivácia účtu ISOP",
|
||||||
|
ogDescription: "Aktivácia účtu",
|
||||||
|
});
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
required: (v: string) => (!!v && v.trim().length > 0) || 'Povinné pole',
|
||||||
|
};
|
||||||
|
|
||||||
|
const isValid = ref(false);
|
||||||
|
const showPassword = ref(false);
|
||||||
|
const password = ref('');
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const error = ref(null as null | string);
|
||||||
|
const success = ref(false);
|
||||||
|
|
||||||
|
async function handleLogin() {
|
||||||
|
error.value = null;
|
||||||
|
loading.value = true;
|
||||||
|
success.value = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client('/api/account/activate', {
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
token: route.params.token,
|
||||||
|
password: password.value
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
success.value = true;
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof FetchError) {
|
||||||
|
error.value = e.response?._data.message;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-container fluid class="page-container form-wrap">
|
||||||
|
<v-card id="page-container-card">
|
||||||
|
<h2 class="page-title">Aktivácia účtu</h2>
|
||||||
|
|
||||||
|
<SuccessAlert v-show="success" title="Aktivácia ukončená" text="">
|
||||||
|
<p>Váš účet bol úspešne aktivovaný! Prihláste sa <NuxtLink to="/login">tu</NuxtLink>.
|
||||||
|
</p>
|
||||||
|
</SuccessAlert>
|
||||||
|
|
||||||
|
<div v-show="!success">
|
||||||
|
<!-- Chybová hláška -->
|
||||||
|
<ErrorAlert v-if="error" :error="error" />
|
||||||
|
|
||||||
|
<!-- Čakajúca hláška -->
|
||||||
|
<LoadingAlert v-if="loading" />
|
||||||
|
|
||||||
|
<v-form v-else v-model="isValid" @submit.prevent="handleLogin">
|
||||||
|
<v-text-field v-model="password" :rules="[rules.required]"
|
||||||
|
:type="showPassword ? 'text' : 'password'" label="Heslo:" variant="outlined"
|
||||||
|
density="comfortable"
|
||||||
|
:append-inner-icon="showPassword ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
|
||||||
|
@click:append-inner="showPassword = !showPassword" />
|
||||||
|
|
||||||
|
<v-btn type="submit" color="success" size="large" block :disabled="!isValid">
|
||||||
|
Aktivovať
|
||||||
|
</v-btn>
|
||||||
|
</v-form>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.alert {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-container {
|
||||||
|
max-width: 1120px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding-left: 24px;
|
||||||
|
padding-right: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#page-container-card {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-wrap {
|
||||||
|
max-width: 640px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1.2;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 24px 0 16px;
|
||||||
|
color: #1f1f1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forgot-btn {
|
||||||
|
text-transform: none;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-1 {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-2 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
91
frontend/app/pages/account/change-password.vue
Normal file
91
frontend/app/pages/account/change-password.vue
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { FetchError } from 'ofetch';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: ['sanctum:auth'],
|
||||||
|
});
|
||||||
|
|
||||||
|
useSeoMeta({
|
||||||
|
title: "Zmena hesla | ISOP",
|
||||||
|
ogTitle: "Zmena hesla",
|
||||||
|
description: "Zmena hesla študenta",
|
||||||
|
ogDescription: "Zmena hesla študenta",
|
||||||
|
});
|
||||||
|
|
||||||
|
const client = useSanctumClient();
|
||||||
|
|
||||||
|
const password = ref('');
|
||||||
|
const password_confirmation = ref('');
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const error = ref<string | null>(null);
|
||||||
|
const success = ref(false);
|
||||||
|
|
||||||
|
// Funkcia na zmenu hesla
|
||||||
|
const changePassword = async () => {
|
||||||
|
error.value = null;
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client('/api/account/change-password', {
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
password: password.value,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
success.value = true;
|
||||||
|
|
||||||
|
// Vyčisti formulár
|
||||||
|
password.value = '';
|
||||||
|
password_confirmation.value = '';
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof FetchError) {
|
||||||
|
error.value = e.data?.message;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-container fluid>
|
||||||
|
<v-card id="page-container-card">
|
||||||
|
<h1>Zmena hesla</h1>
|
||||||
|
|
||||||
|
<!-- Error alert -->
|
||||||
|
<ErrorAlert v-if="error" :error="error" />
|
||||||
|
|
||||||
|
<!-- Success alert -->
|
||||||
|
<SuccessAlert v-else-if="success" title="Heslo aktualizované" text="Heslo bolo úspešne zmenené." />
|
||||||
|
|
||||||
|
<v-form v-else :disabled="loading" @submit.prevent="changePassword">
|
||||||
|
<!-- Nové heslo -->
|
||||||
|
<v-text-field v-model="password" label="Nové heslo" type="password" variant="outlined" class="mb-3"
|
||||||
|
hint="Minimálne 8 znakov" persistent-hint></v-text-field>
|
||||||
|
|
||||||
|
<!-- Potvrdenie hesla -->
|
||||||
|
<v-text-field v-model="password_confirmation" label="Potvrďte nové heslo" type="password"
|
||||||
|
variant="outlined" class="mb-3"></v-text-field>
|
||||||
|
|
||||||
|
<!-- Submit button -->
|
||||||
|
<v-btn type="submit" color="primary" :disabled="password !== password_confirmation" class="mb-2">
|
||||||
|
Zmeniť heslo
|
||||||
|
</v-btn>
|
||||||
|
</v-form>
|
||||||
|
|
||||||
|
<!-- Zrušiť -->
|
||||||
|
<v-btn type="submit" color="yellow" to="/dashboard" class="mb-2">
|
||||||
|
Späť na dashboard
|
||||||
|
</v-btn>
|
||||||
|
</v-card>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#page-container-card {
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
118
frontend/app/pages/account/index.vue
Normal file
118
frontend/app/pages/account/index.vue
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { User } from '~/types/user';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: ['sanctum:auth'],
|
||||||
|
});
|
||||||
|
|
||||||
|
useSeoMeta({
|
||||||
|
title: 'Môj profil | ISOP',
|
||||||
|
ogTitle: 'Môj profil',
|
||||||
|
description: 'Môj profil ISOP',
|
||||||
|
ogDescription: 'Môj profil ISOP',
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = useSanctumUser<User>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-container fluid>
|
||||||
|
<v-card id="page-container-card">
|
||||||
|
<h1>Môj profil</h1>
|
||||||
|
|
||||||
|
<v-divider class="my-4" />
|
||||||
|
|
||||||
|
<!-- Osobné údaje -->
|
||||||
|
<h3>Osobné údaje</h3>
|
||||||
|
<v-list density="compact" class="readonly-list">
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-title>Meno</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ user?.first_name }}</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-title>Priezvisko</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ user?.last_name }}</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
|
||||||
|
<!-- Údaje študenta -->
|
||||||
|
<div v-if="user?.student_data">
|
||||||
|
<h3>Študentské údaje</h3>
|
||||||
|
<v-list density="compact" class="readonly-list">
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-title>Osobný e-mail</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ user?.student_data.personal_email }}</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-title>Študijný odbor</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ user?.student_data.study_field }}</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-title>Adresa</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ user?.student_data.address }}</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Údaje firmy -->
|
||||||
|
<div v-if="user?.company_data">
|
||||||
|
<h3>Firemné údaje</h3>
|
||||||
|
<v-list density="compact" class="readonly-list">
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-title>Názov</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ user?.company_data.name }}</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-title>Adresa</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ user?.company_data.address }}</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-title>IČO</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ user?.company_data.ico }}</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-list density="compact" class="readonly-list">
|
||||||
|
<v-list-item>
|
||||||
|
<v-btn prepend-icon="mdi mdi-pencil" color="orange" to="/account/change-password">
|
||||||
|
Zmeniť heslo
|
||||||
|
</v-btn>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-card>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#page-container-card {
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.readonly-list {
|
||||||
|
--v-list-padding-start: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.readonly-list :deep(.v-list-item) {
|
||||||
|
--v-list-item-padding-start: 0px;
|
||||||
|
padding-left: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.readonly-list :deep(.v-list-item__content) {
|
||||||
|
padding-left: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.readonly-list :deep(.v-list-item-subtitle) {
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
|
||||||
|
.readonly-list :deep(.v-list-item-title) {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { User } from '~/types/user';
|
|
||||||
|
|
||||||
definePageMeta({
|
|
||||||
middleware: ['sanctum:auth', 'admin-only'],
|
|
||||||
});
|
|
||||||
|
|
||||||
useSeoMeta({
|
|
||||||
title: "Portál administrátora | ISOP",
|
|
||||||
ogTitle: "Portál administrátora",
|
|
||||||
description: "Portál administrátora ISOP",
|
|
||||||
ogDescription: "Portál administrátora",
|
|
||||||
});
|
|
||||||
|
|
||||||
const user = useSanctumUser<User>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<v-container fluid>
|
|
||||||
<v-card id="footer-card">
|
|
||||||
<h1>Vitajte, {{ user?.name }}</h1>
|
|
||||||
|
|
||||||
|
|
||||||
</v-card>
|
|
||||||
</v-container>
|
|
||||||
</template>
|
|
||||||
144
frontend/app/pages/dashboard/admin/companies/edit/[id].vue
Normal file
144
frontend/app/pages/dashboard/admin/companies/edit/[id].vue
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { CompanyData } from '~/types/company_data';
|
||||||
|
import { FetchError } from 'ofetch';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: ['sanctum:auth', 'admin-only']
|
||||||
|
})
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const client = useSanctumClient();
|
||||||
|
const companyId = route.params.id;
|
||||||
|
|
||||||
|
const loading = ref(true);
|
||||||
|
const saving = ref(false);
|
||||||
|
|
||||||
|
// Delete state
|
||||||
|
const company = ref<CompanyData | null>(null);
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
name: '',
|
||||||
|
address: '',
|
||||||
|
ico: 0,
|
||||||
|
hiring: false,
|
||||||
|
contact: {
|
||||||
|
first_name: '',
|
||||||
|
last_name: '',
|
||||||
|
email: '',
|
||||||
|
phone: ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Načítanie dát firmy
|
||||||
|
const { data } = await useLazySanctumFetch<CompanyData>(`/api/companies/${companyId}`);
|
||||||
|
|
||||||
|
watch(data, (newData) => {
|
||||||
|
if (newData) {
|
||||||
|
company.value = newData;
|
||||||
|
form.value.name = newData.name;
|
||||||
|
form.value.address = newData.address;
|
||||||
|
form.value.ico = newData.ico;
|
||||||
|
form.value.hiring = !!newData.hiring;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
// Uloženie zmien
|
||||||
|
async function saveChanges() {
|
||||||
|
saving.value = true;
|
||||||
|
try {
|
||||||
|
await client(`/api/companies/${companyId}`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: form.value
|
||||||
|
});
|
||||||
|
|
||||||
|
alert('Údaje firmy boli úspešne aktualizované');
|
||||||
|
navigateTo("/dashboard/admin/companies");
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof FetchError) {
|
||||||
|
console.error('Error saving company:', e.response?._data.message);
|
||||||
|
alert('Chyba:\n' + e.response?._data.message);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
saving.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancel() {
|
||||||
|
navigateTo('/dashboard/admin/companies');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-container class="h-100">
|
||||||
|
<div v-if="loading" class="text-center">
|
||||||
|
<v-progress-circular indeterminate color="primary"></v-progress-circular>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<v-row class="mb-4">
|
||||||
|
<v-col>
|
||||||
|
<h1>Editovať firmu</h1>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="8">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>Údaje firmy</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-form>
|
||||||
|
<v-text-field v-model="form.name" label="Názov firmy" required variant="outlined"
|
||||||
|
class="mb-3"></v-text-field>
|
||||||
|
|
||||||
|
<v-textarea v-model="form.address" label="Adresa" required variant="outlined" rows="3"
|
||||||
|
class="mb-3"></v-textarea>
|
||||||
|
|
||||||
|
<v-text-field v-model.number="form.ico" label="IČO" type="number" required
|
||||||
|
variant="outlined" class="mb-3"></v-text-field>
|
||||||
|
|
||||||
|
<v-checkbox v-model="form.hiring" label="Prijíma študentov na prax"
|
||||||
|
class="mb-3"></v-checkbox>
|
||||||
|
|
||||||
|
<v-divider class="my-4"></v-divider>
|
||||||
|
|
||||||
|
<h3 class="mb-3">Kontaktná osoba</h3>
|
||||||
|
|
||||||
|
<v-text-field v-model="form.contact.first_name" label="Meno" required variant="outlined"
|
||||||
|
class="mb-3"></v-text-field>
|
||||||
|
|
||||||
|
<v-text-field v-model="form.contact.last_name" label="Priezvisko" required
|
||||||
|
variant="outlined" class="mb-3"></v-text-field>
|
||||||
|
|
||||||
|
<v-text-field v-model="form.contact.email" label="E-mail" type="email" required
|
||||||
|
variant="outlined" class="mb-3"></v-text-field>
|
||||||
|
|
||||||
|
<v-text-field v-model="form.contact.phone" label="Telefón"
|
||||||
|
variant="outlined"></v-text-field>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-actions class="px-6 pb-4">
|
||||||
|
<v-btn color="primary" @click="saveChanges" :loading="saving" :disabled="saving">
|
||||||
|
Uložiť zmeny
|
||||||
|
</v-btn>
|
||||||
|
<v-btn @click="cancel" :disabled="saving">
|
||||||
|
Zrušiť
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</div>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.h-100 {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
165
frontend/app/pages/dashboard/admin/companies/index.vue
Normal file
165
frontend/app/pages/dashboard/admin/companies/index.vue
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { CompanyData } from '~/types/company_data';
|
||||||
|
import { FetchError } from 'ofetch';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: ['sanctum:auth', 'admin-only'],
|
||||||
|
});
|
||||||
|
|
||||||
|
useSeoMeta({
|
||||||
|
title: "Partnerské firmy | ISOP",
|
||||||
|
ogTitle: "Partnerské firmy",
|
||||||
|
description: "Partnerské firmy ISOP",
|
||||||
|
ogDescription: "Partnerské firmy",
|
||||||
|
});
|
||||||
|
|
||||||
|
const headers = [
|
||||||
|
{ title: 'Názov', key: 'name', align: 'left' },
|
||||||
|
{ title: 'IČO', key: 'ico', align: 'left' },
|
||||||
|
{ title: 'Kontaktná osoba', key: 'contact_name', align: 'left' },
|
||||||
|
{ title: 'Telefón', key: 'phone', align: 'middle' },
|
||||||
|
{ title: 'E-mail', key: 'email', align: 'middle' },
|
||||||
|
{ title: 'Prijímajú študentov', key: 'hiring', align: 'middle' },
|
||||||
|
{ title: 'Operácie', key: 'ops', align: 'middle' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const client = useSanctumClient();
|
||||||
|
|
||||||
|
const { data, error, pending, refresh } = await useLazySanctumFetch<CompanyData[]>('/api/companies/simple');
|
||||||
|
|
||||||
|
// State pre delete dialog
|
||||||
|
const deleteDialog = ref(false);
|
||||||
|
const companyToDelete = ref<CompanyData | null>(null);
|
||||||
|
const deleteLoading = ref(false);
|
||||||
|
const deleteError = ref<string | null>(null);
|
||||||
|
|
||||||
|
// Funkcia na otvorenie delete dialogu
|
||||||
|
const openDeleteDialog = (company: CompanyData) => {
|
||||||
|
companyToDelete.value = company;
|
||||||
|
deleteDialog.value = true;
|
||||||
|
deleteError.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Funkcia na zatvorenie dialogu
|
||||||
|
const closeDeleteDialog = () => {
|
||||||
|
deleteDialog.value = false;
|
||||||
|
companyToDelete.value = null;
|
||||||
|
deleteError.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Funkcia na vymazanie firmy
|
||||||
|
const deleteCompany = async () => {
|
||||||
|
if (!companyToDelete.value) return;
|
||||||
|
|
||||||
|
deleteLoading.value = true;
|
||||||
|
deleteError.value = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client(`/api/companies/${companyToDelete.value.id}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
closeDeleteDialog();
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof FetchError) {
|
||||||
|
deleteError.value = err.data?.message;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
deleteLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-container fluid>
|
||||||
|
<v-card id="page-container-card">
|
||||||
|
<h1>Partnerské firmy</h1>
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<!-- spacer -->
|
||||||
|
<div style="height: 40px;"></div>
|
||||||
|
|
||||||
|
<!-- Čakajúca hláška -->
|
||||||
|
<LoadingAlert v-if="pending" />
|
||||||
|
|
||||||
|
<!-- Chybová hláška -->
|
||||||
|
<ErrorAlert v-else-if="error" :error="error?.message" />
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<p>Aktuálne spolupracujeme s {{ data?.length }} firmami.</p>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<v-table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th v-for="header in headers" :class="'text-' + header.align">
|
||||||
|
<strong>{{ header.title }}</strong>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="item in data">
|
||||||
|
<td>{{ item.name }}</td>
|
||||||
|
<td>{{ item.ico }}</td>
|
||||||
|
<td>{{ item.contact.name }}</td>
|
||||||
|
<td>{{ item.contact.phone }}</td>
|
||||||
|
<td>{{ item.contact.email }}</td>
|
||||||
|
<td>{{ item.hiring ? "Áno" : "Nie" }}</td>
|
||||||
|
<td class="text-left">
|
||||||
|
<v-btn class="m-1 op-btn" density="compact" append-icon="mdi-pencil" base-color="orange"
|
||||||
|
:to="'/dashboard/admin/companies/edit/' + item.id">Editovať</v-btn>
|
||||||
|
<v-btn class="m-1 op-btn" density="compact" append-icon="mdi-delete" base-color="red"
|
||||||
|
@click="openDeleteDialog(item)">Vymazať</v-btn>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</v-table>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<!-- Delete Confirmation Dialog -->
|
||||||
|
<v-dialog v-model="deleteDialog" max-width="500px">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="text-h5">
|
||||||
|
Potvrdiť vymazanie
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<p>
|
||||||
|
Naozaj chcete vymazať firmu <strong>{{ companyToDelete?.name }}</strong>?
|
||||||
|
</p>
|
||||||
|
<p class="text-error mt-2">
|
||||||
|
Táto akcia vymaže aj kontaktnú osobu (EMPLOYER), všetky praxe a statusy spojené s touto firmou a
|
||||||
|
<strong>nie je možné ju vrátiť späť</strong>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Error message -->
|
||||||
|
<ErrorAlert v-if="deleteError" :error="deleteError" />
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn color="grey" variant="text" @click="closeDeleteDialog" :disabled="deleteLoading">
|
||||||
|
Zrušiť
|
||||||
|
</v-btn>
|
||||||
|
<v-btn color="red" variant="text" @click="deleteCompany" :loading="deleteLoading">
|
||||||
|
Vymazať
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#page-container-card {
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.op-btn {
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
241
frontend/app/pages/dashboard/admin/external_api.vue
Normal file
241
frontend/app/pages/dashboard/admin/external_api.vue
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { FetchError } from 'ofetch';
|
||||||
|
import type { ApiKey, NewApiKey } from '~/types/api_keys';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: ['sanctum:auth', 'admin-only'],
|
||||||
|
});
|
||||||
|
|
||||||
|
useSeoMeta({
|
||||||
|
title: "API Manažment | ISOP",
|
||||||
|
ogTitle: "API Manažment",
|
||||||
|
description: "API Manažment ISOP",
|
||||||
|
ogDescription: "API Manažment",
|
||||||
|
});
|
||||||
|
|
||||||
|
const headers = [
|
||||||
|
{ title: 'Názov', key: 'name', align: 'left' },
|
||||||
|
{ title: 'Vytvorené', key: 'created_at', align: 'left' },
|
||||||
|
{ title: 'Naposledy použité', key: 'last_used_at', align: 'left' },
|
||||||
|
{ title: 'Vlastník', key: 'owner', align: 'left' },
|
||||||
|
{ title: 'Operácie', key: 'ops', align: 'middle' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const creationDialog = ref(false);
|
||||||
|
const keyDisplayDialog = ref(false);
|
||||||
|
const deletionConfirmDialog = ref<{ open: boolean, key: ApiKey | null }>({ open: false, key: null });
|
||||||
|
const newKey = ref<string>("");
|
||||||
|
const newKeyName = ref("");
|
||||||
|
const waiting = ref(false);
|
||||||
|
|
||||||
|
const { data, error, pending, refresh } = useLazySanctumFetch<ApiKey[]>('/api/external/keys');
|
||||||
|
const client = useSanctumClient();
|
||||||
|
|
||||||
|
function closeKeyDisplayDialog() {
|
||||||
|
keyDisplayDialog.value = false;
|
||||||
|
newKey.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyNewKeyToClipboard() {
|
||||||
|
await navigator.clipboard.writeText(newKey.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDeletionConfirmDialog(key: ApiKey) {
|
||||||
|
deletionConfirmDialog.value = { open: true, key: key };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmDeletion(confirm: boolean) {
|
||||||
|
const key = deletionConfirmDialog.value.key!;
|
||||||
|
|
||||||
|
if (!confirm) {
|
||||||
|
deletionConfirmDialog.value = { open: false, key: null };
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
await deleteKey(key);
|
||||||
|
deletionConfirmDialog.value = { open: false, key: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestNewKey() {
|
||||||
|
waiting.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await client<NewApiKey>('/api/external/keys', {
|
||||||
|
method: 'PUT',
|
||||||
|
body: {
|
||||||
|
name: newKeyName.value
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
newKey.value = result.key;
|
||||||
|
await refresh();
|
||||||
|
creationDialog.value = false;
|
||||||
|
|
||||||
|
keyDisplayDialog.value = true;
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof FetchError) {
|
||||||
|
alert(`Chyba: ${e.data?.message}`);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
waiting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteKey(key: ApiKey) {
|
||||||
|
waiting.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client<NewApiKey>(`/api/external/keys/${key.id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
|
||||||
|
await refresh();
|
||||||
|
|
||||||
|
deletionConfirmDialog.value = { open: false, key: null };
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof FetchError) {
|
||||||
|
alert(`Chyba: ${e.data?.message}`);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
waiting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-container fluid>
|
||||||
|
<v-card id="page-container-card">
|
||||||
|
<h1>API Manažment</h1>
|
||||||
|
|
||||||
|
<!-- spacer -->
|
||||||
|
<div style="height: 40px;"></div>
|
||||||
|
|
||||||
|
<v-btn prepend-icon="mdi-plus" color="blue" class="mr-2" @click="() => creationDialog = true"
|
||||||
|
:disabled="waiting">
|
||||||
|
Pridať
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<!-- Čakajúca hláška -->
|
||||||
|
<LoadingAlert v-if="pending" />
|
||||||
|
|
||||||
|
<!-- Chybová hláška -->
|
||||||
|
<ErrorAlert v-else-if="error" :error="error?.message" />
|
||||||
|
|
||||||
|
<v-table v-else>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th v-for="header in headers" :class="'text-' + header.align">
|
||||||
|
<strong>{{ header.title }}</strong>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="item in data">
|
||||||
|
<td>{{ item.name }}</td>
|
||||||
|
<td>{{ item.created_at }}</td>
|
||||||
|
<td>{{ item.last_used_at ?? "Nikdy" }}</td>
|
||||||
|
<td>{{ item.owner }}</td>
|
||||||
|
<td class="text-left">
|
||||||
|
<v-btn class="m-1 op-btn" density="compact" append-icon="mdi-delete" base-color="red"
|
||||||
|
@click="() => openDeletionConfirmDialog(item)">Vymazať</v-btn>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</v-table>
|
||||||
|
|
||||||
|
<!-- spacer -->
|
||||||
|
<div style="height: 40px;"></div>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<!-- New API key dialog -->
|
||||||
|
<v-dialog v-model="creationDialog" max-width="500px">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="text-h5">
|
||||||
|
Nový API kľúč
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text>
|
||||||
|
<v-text-field label="Názov kľúča" required v-model="newKeyName" id="newKeyName"></v-text-field>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn color="green" variant="text" @click="requestNewKey" :loading="waiting">
|
||||||
|
Vytvoriť
|
||||||
|
</v-btn>
|
||||||
|
<v-btn color="black" variant="text" @click="creationDialog = false">
|
||||||
|
Zavrieť
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<!-- New API key display dialog -->
|
||||||
|
<v-dialog v-model="keyDisplayDialog" max-width="500px">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="text-h5">
|
||||||
|
Nový API kľúč
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text>
|
||||||
|
<p>Nový klúč bol úspešne vytvorený!</p>
|
||||||
|
|
||||||
|
<p id="key-copy-warn">Nezabudnite skopírovať kľúč! Po zavretí tohto okna ho nebudete vedieť
|
||||||
|
zobraziť znovu!</p>
|
||||||
|
|
||||||
|
<v-text-field label="API kľúč" required readonly v-model="newKey"></v-text-field>
|
||||||
|
<v-btn prepend-icon="mdi-content-copy" color="blue" block
|
||||||
|
@click="copyNewKeyToClipboard">Copy</v-btn>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn color="black" variant="text" @click="closeKeyDisplayDialog">
|
||||||
|
Zavrieť
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<!-- Key deletion confirmation dialog -->
|
||||||
|
<v-dialog v-model="deletionConfirmDialog.open" max-width="500px">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="text-h5">
|
||||||
|
Vymazať API kľúč
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text>
|
||||||
|
<p>Ste si istý že chcete deaktivovať a vymazať vybraný API kľúč?</p>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
|
||||||
|
<v-btn color="red" variant="text" @click="async () => { confirmDeletion(true) }" :loading="waiting">
|
||||||
|
Áno
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<v-btn color="black" variant="text" @click="async () => { confirmDeletion(false) }">
|
||||||
|
Zrušiť
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#page-container-card {
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.op-btn {
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#key-copy-warn {
|
||||||
|
color: red;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
56
frontend/app/pages/dashboard/admin/index.vue
Normal file
56
frontend/app/pages/dashboard/admin/index.vue
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { User } from '~/types/user';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: ['sanctum:auth', 'admin-only'],
|
||||||
|
});
|
||||||
|
|
||||||
|
useSeoMeta({
|
||||||
|
title: "Portál administrátora | ISOP",
|
||||||
|
ogTitle: "Portál administrátora",
|
||||||
|
description: "Portál administrátora ISOP",
|
||||||
|
ogDescription: "Portál administrátora",
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = useSanctumUser<User>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-container fluid>
|
||||||
|
<v-card id="page-container-card">
|
||||||
|
<h1>Vitajte, {{ user?.name }}</h1>
|
||||||
|
|
||||||
|
<!-- spacer -->
|
||||||
|
<div style="height: 40px;"></div>
|
||||||
|
|
||||||
|
<v-btn prepend-icon="mdi-account-circle" color="blue" class="mr-2" to="/dashboard/admin/students">
|
||||||
|
Študenti
|
||||||
|
</v-btn>
|
||||||
|
<v-btn prepend-icon="mdi-domain" color="blue" class="mr-2" to="/dashboard/admin/companies">
|
||||||
|
Firmy
|
||||||
|
</v-btn>
|
||||||
|
<v-btn prepend-icon="mdi-account-hard-hat" color="blue" class="mr-2" to="/dashboard/admin/internships">
|
||||||
|
Praxe
|
||||||
|
</v-btn>
|
||||||
|
<v-btn prepend-icon="mdi-key" color="blue" class="mr-2" to="/dashboard/admin/external_api">
|
||||||
|
API Manažment
|
||||||
|
</v-btn>
|
||||||
|
<v-btn prepend-icon="mdi-pencil" color="orange" class="mr-2" to="/account">
|
||||||
|
Môj profil
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<!-- spacer -->
|
||||||
|
<div style="height: 40px;"></div>
|
||||||
|
|
||||||
|
<p>...</p>
|
||||||
|
</v-card>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#page-container-card {
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
123
frontend/app/pages/dashboard/admin/internships/edit/[id].vue
Normal file
123
frontend/app/pages/dashboard/admin/internships/edit/[id].vue
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { Internship, NewInternship } from '~/types/internships';
|
||||||
|
import { prettyInternshipStatus } from '~/types/internship_status';
|
||||||
|
import { FetchError } from 'ofetch';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: ['sanctum:auth', 'admin-only'],
|
||||||
|
});
|
||||||
|
|
||||||
|
useSeoMeta({
|
||||||
|
title: "Edit praxe | ISOP",
|
||||||
|
ogTitle: "Edit praxe",
|
||||||
|
description: "Edit praxe ISOP",
|
||||||
|
ogDescription: "Edit praxe",
|
||||||
|
});
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const client = useSanctumClient();
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const action_error = ref(null as null | string);
|
||||||
|
const refreshKey = ref(0);
|
||||||
|
|
||||||
|
const { data, error, pending, refresh } = await useLazySanctumFetch<Internship>(`/api/internships/${route.params.id}`);
|
||||||
|
|
||||||
|
async function handleUpdateOfBasicInfo(internship: NewInternship) {
|
||||||
|
action_error.value = null;
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client(`/api/internships/${route.params.id}/basic`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: internship
|
||||||
|
});
|
||||||
|
|
||||||
|
navigateTo("/dashboard/admin/internships");
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof FetchError) {
|
||||||
|
action_error.value = e.response?._data.message;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function forceRefresh() {
|
||||||
|
await refresh();
|
||||||
|
refreshKey.value++;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-container fluid>
|
||||||
|
<v-card id="page-container-card">
|
||||||
|
<h1>Edit praxe</h1>
|
||||||
|
|
||||||
|
<!-- spacer -->
|
||||||
|
<div style="height: 40px;"></div>
|
||||||
|
|
||||||
|
<!-- Čakajúca hláška -->
|
||||||
|
<LoadingAlert v-if="loading" />
|
||||||
|
|
||||||
|
<!-- Chybová hláška -->
|
||||||
|
<ErrorAlert v-if="action_error" :error="action_error" />
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<!-- Čakajúca hláška -->
|
||||||
|
<LoadingAlert v-if="pending" />
|
||||||
|
|
||||||
|
<!-- Chybová hláška -->
|
||||||
|
<ErrorAlert v-else-if="error" :error="error?.message" />
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<div>
|
||||||
|
<h2>Základné informácie</h2>
|
||||||
|
<InternshipEditor :internship="data!" :submit="handleUpdateOfBasicInfo" />
|
||||||
|
<hr />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2>Stav</h2>
|
||||||
|
<h4>Aktuálny stav</h4>
|
||||||
|
<p>{{ prettyInternshipStatus(data?.status.status!) }}</p>
|
||||||
|
<p>Poznámka: <em>{{ data?.status.note }}</em></p>
|
||||||
|
<p>Posledná zmena: <em>{{ data?.status.changed }}, {{ data?.status.modified_by.name }}</em></p>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<h4>História</h4>
|
||||||
|
<InternshipStatusHistoryView :internship="data!" />
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<h4>Zmena stavu</h4>
|
||||||
|
<InternshipStatusEditor :internship="data!" @successful-submit="forceRefresh" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<h2>Dokumenty</h2>
|
||||||
|
<InternshipDocumentViewer :internship="data!" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#page-container-card {
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
87
frontend/app/pages/dashboard/admin/internships/index.vue
Normal file
87
frontend/app/pages/dashboard/admin/internships/index.vue
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { InternshipFilter } from '~/types/internships';
|
||||||
|
import { FetchError } from 'ofetch';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: ['sanctum:auth', 'admin-only'],
|
||||||
|
});
|
||||||
|
|
||||||
|
useSeoMeta({
|
||||||
|
title: "Praxe študentov | ISOP",
|
||||||
|
ogTitle: "Praxe študentov",
|
||||||
|
description: "Praxe študentov ISOP",
|
||||||
|
ogDescription: "Praxe študentov",
|
||||||
|
});
|
||||||
|
|
||||||
|
const headers = [
|
||||||
|
{ title: 'Firma', key: 'company', align: 'left' },
|
||||||
|
{ title: 'Študent', key: 'student', align: 'left' },
|
||||||
|
{ title: 'Od', key: 'start', align: 'left' },
|
||||||
|
{ title: 'Do', key: 'end', align: 'left' },
|
||||||
|
{ title: 'Ročník', key: 'year_of_study', align: 'middle' },
|
||||||
|
{ title: 'Semester', key: 'semester', align: 'middle' },
|
||||||
|
{ title: 'Stav', key: 'status', align: 'middle' },
|
||||||
|
{ title: 'Operácie', key: 'ops', align: 'middle' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const internshipFilters = ref<InternshipFilter | null>(null);
|
||||||
|
const exportPending = ref(false);
|
||||||
|
const exportAvailable = ref(true);
|
||||||
|
|
||||||
|
const client = useSanctumClient();
|
||||||
|
|
||||||
|
async function requestExport() {
|
||||||
|
exportPending.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const file = await client<Blob>(`/api/internships/export`, {
|
||||||
|
method: 'GET',
|
||||||
|
query: internshipFilters.value ?? {},
|
||||||
|
});
|
||||||
|
triggerDownload(file, 'internships_export', 'csv');
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof FetchError) {
|
||||||
|
alert(`Chyba pri exportovaní: ${e.statusMessage ?? e.message}`);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
exportPending.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-container fluid>
|
||||||
|
<v-card id="page-container-card">
|
||||||
|
<h1>Praxe študentov</h1>
|
||||||
|
|
||||||
|
<!-- spacer -->
|
||||||
|
<div style="height: 40px;"></div>
|
||||||
|
|
||||||
|
<v-btn prepend-icon="mdi-file-export-outline" color="green" class="mr-2 mb-2" @click="requestExport"
|
||||||
|
:disabled="!exportAvailable" :loading="exportPending">
|
||||||
|
<v-tooltip activator="parent" location="top">
|
||||||
|
Exportovať aktuálne zobrazené výsledky do CSV súboru
|
||||||
|
</v-tooltip>
|
||||||
|
Export výsledkov
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<InternshipListView mode="admin" @filterApplied="(filters) => internshipFilters = filters"
|
||||||
|
@itemsAvailable="(available) => exportAvailable = available" />
|
||||||
|
</v-card>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#page-container-card {
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.op-btn {
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
223
frontend/app/pages/dashboard/admin/students/edit/[id].vue
Normal file
223
frontend/app/pages/dashboard/admin/students/edit/[id].vue
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { User } from '~/types/user';
|
||||||
|
import { FetchError } from 'ofetch';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: ['sanctum:auth', 'admin-only']
|
||||||
|
})
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const client = useSanctumClient();
|
||||||
|
const studentId = route.params.id;
|
||||||
|
|
||||||
|
const student = ref<User | null>(null);
|
||||||
|
const loading = ref(true);
|
||||||
|
const saving = ref(false);
|
||||||
|
|
||||||
|
// Delete state
|
||||||
|
const deleteDialog = ref(false);
|
||||||
|
const deleteLoading = ref(false);
|
||||||
|
const deleteError = ref<string | null>(null);
|
||||||
|
const deleteSuccess = ref(false);
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
first_name: '',
|
||||||
|
last_name: '',
|
||||||
|
email: '',
|
||||||
|
phone: '',
|
||||||
|
student_data: {
|
||||||
|
study_field: '',
|
||||||
|
personal_email: '',
|
||||||
|
address: ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data } = await useLazySanctumFetch<User>(`/api/students/${studentId}`);
|
||||||
|
|
||||||
|
// Načítanie dát študenta
|
||||||
|
watch(data, (newData) => {
|
||||||
|
if (newData) {
|
||||||
|
student.value = newData;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
// Uloženie zmien
|
||||||
|
async function saveChanges() {
|
||||||
|
saving.value = true;
|
||||||
|
try {
|
||||||
|
await client(`/api/students/${studentId}`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: form.value
|
||||||
|
});
|
||||||
|
|
||||||
|
alert('Údaje študenta boli úspešne aktualizované');
|
||||||
|
navigateTo("/dashboard/admin/students");
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof FetchError) {
|
||||||
|
alert('Chyba:\n' + e.response?._data.message);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
saving.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.';
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
deleteLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-container class="h-100">
|
||||||
|
<div v-if="loading" class="text-center">
|
||||||
|
<v-progress-circular indeterminate color="primary"></v-progress-circular>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<v-row class="mb-4">
|
||||||
|
<v-col>
|
||||||
|
<h1>Editovať študenta</h1>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="8">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>Základné údaje</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-form>
|
||||||
|
<v-text-field v-model="form.first_name" label="Meno" required variant="outlined"
|
||||||
|
class="mb-3"></v-text-field>
|
||||||
|
|
||||||
|
<v-text-field v-model="form.last_name" label="Priezvisko" required variant="outlined"
|
||||||
|
class="mb-3"></v-text-field>
|
||||||
|
|
||||||
|
<v-text-field v-model="form.email" label="E-mail (prihlasovací)" type="email" required
|
||||||
|
variant="outlined" class="mb-3"></v-text-field>
|
||||||
|
|
||||||
|
<v-text-field v-model="form.phone" label="Telefón" variant="outlined"
|
||||||
|
class="mb-3"></v-text-field>
|
||||||
|
|
||||||
|
<v-divider class="my-4"></v-divider>
|
||||||
|
|
||||||
|
<h3 class="mb-3">Študijné údaje</h3>
|
||||||
|
|
||||||
|
<v-text-field v-model="form.student_data.study_field" label="Študijný program"
|
||||||
|
variant="outlined" class="mb-3"></v-text-field>
|
||||||
|
|
||||||
|
<v-text-field v-model="form.student_data.personal_email" label="Osobný e-mail"
|
||||||
|
type="email" variant="outlined" class="mb-3"></v-text-field>
|
||||||
|
|
||||||
|
<v-textarea v-model="form.student_data.address" label="Adresa" variant="outlined"
|
||||||
|
rows="3"></v-textarea>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-actions class="px-6 pb-4">
|
||||||
|
<v-btn color="primary" @click="saveChanges" :loading="saving" :disabled="saving">
|
||||||
|
Uložiť zmeny
|
||||||
|
</v-btn>
|
||||||
|
<v-btn @click="cancel" :disabled="saving">
|
||||||
|
Zrušiť
|
||||||
|
</v-btn>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn color="red" variant="outlined" @click="openDeleteDialog" :disabled="saving">
|
||||||
|
Vymazať študenta
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Delete Confirmation Dialog -->
|
||||||
|
<v-dialog v-model="deleteDialog" max-width="500px">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="text-h5">
|
||||||
|
Potvrdiť vymazanie
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<p v-if="!deleteSuccess">
|
||||||
|
Naozaj chcete vymazať študenta <strong>{{ student?.name }}</strong>?
|
||||||
|
</p>
|
||||||
|
<p v-if="!deleteSuccess" class="text-error mt-2">
|
||||||
|
Táto akcia vymaže aj všetky súvisiace dáta (praxe, statusy, atď.) a <strong>nie je možné ju
|
||||||
|
vrátiť späť</strong>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Error message -->
|
||||||
|
<ErrorAlert v-if="deleteError" :error="deleteError" />
|
||||||
|
|
||||||
|
<!-- Success message -->
|
||||||
|
<SuccessAlert v-if="deleteSuccess" title="Úspech" text="Študent bol úspešne vymazaný." />
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn color="grey" variant="text" @click="closeDeleteDialog" :disabled="deleteLoading">
|
||||||
|
Zrušiť
|
||||||
|
</v-btn>
|
||||||
|
<v-btn color="red" variant="text" @click="deleteStudent" :loading="deleteLoading"
|
||||||
|
:disabled="deleteSuccess">
|
||||||
|
Vymazať
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.h-100 {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
170
frontend/app/pages/dashboard/admin/students/index.vue
Normal file
170
frontend/app/pages/dashboard/admin/students/index.vue
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { User } from '~/types/user';
|
||||||
|
import { FetchError } from 'ofetch';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: ['sanctum:auth', 'admin-only'],
|
||||||
|
});
|
||||||
|
|
||||||
|
useSeoMeta({
|
||||||
|
title: "Študenti | ISOP",
|
||||||
|
ogTitle: "Študenti",
|
||||||
|
description: "Študenti ISOP",
|
||||||
|
ogDescription: "Študenti",
|
||||||
|
});
|
||||||
|
|
||||||
|
const headers = [
|
||||||
|
{ title: 'Meno', key: 'name', align: 'left' },
|
||||||
|
{ title: 'E-mail', key: 'email', align: 'left' },
|
||||||
|
{ title: 'Telefón', key: 'phone', align: 'left' },
|
||||||
|
{ title: 'Študijný program', key: 'study_field', align: 'left' },
|
||||||
|
{ title: 'Osobný e-mail', key: 'personal_email', align: 'middle' },
|
||||||
|
{ title: 'Adresa', key: 'address', align: 'middle' },
|
||||||
|
{ title: 'Operácie', key: 'ops', align: 'middle' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const client = useSanctumClient();
|
||||||
|
|
||||||
|
// Načítame všetkých študentov
|
||||||
|
const { data: students, error, pending, refresh } = await useLazySanctumFetch<User[]>('/api/students');
|
||||||
|
|
||||||
|
// State pre delete dialog
|
||||||
|
const deleteDialog = ref(false);
|
||||||
|
const studentToDelete = ref<User | null>(null);
|
||||||
|
const deleteLoading = ref(false);
|
||||||
|
const deleteError = ref<string | null>(null);
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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'
|
||||||
|
});
|
||||||
|
|
||||||
|
await refresh();
|
||||||
|
closeDeleteDialog();
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof FetchError) {
|
||||||
|
deleteError.value = err.data?.message;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
deleteLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-container fluid>
|
||||||
|
<v-card id="page-container-card">
|
||||||
|
<h1>Študenti</h1>
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<!-- spacer -->
|
||||||
|
<div style="height: 40px;"></div>
|
||||||
|
|
||||||
|
<!-- Čakajúca hláška -->
|
||||||
|
<LoadingAlert v-if="pending" />
|
||||||
|
|
||||||
|
<!-- Chybová hláška -->
|
||||||
|
<ErrorAlert v-else-if="error" :error="error?.message" />
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<p>Aktuálne evidujeme {{ students?.length || 0 }} študentov.</p>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<v-table v-if="students && students.length > 0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th v-for="header in headers" :class="'text-' + header.align">
|
||||||
|
<strong>{{ header.title }}</strong>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="item in students" :key="item.id">
|
||||||
|
<td>{{ item.name }}</td>
|
||||||
|
<td>{{ item.email }}</td>
|
||||||
|
<td>{{ item.phone || '-' }}</td>
|
||||||
|
<td>{{ item.student_data?.study_field || '-' }}</td>
|
||||||
|
<td>{{ item.student_data?.personal_email || '-' }}</td>
|
||||||
|
<td>{{ item.student_data?.address || '-' }}</td>
|
||||||
|
<td class="text-left">
|
||||||
|
<v-btn class="m-1 op-btn" density="compact" append-icon="mdi-pencil" base-color="orange"
|
||||||
|
:to="'/dashboard/admin/students/edit/' + item.id">Editovať</v-btn>
|
||||||
|
<v-btn class="m-1 op-btn" density="compact" append-icon="mdi-delete" base-color="red"
|
||||||
|
@click="openDeleteDialog(item)">Vymazať</v-btn>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</v-table>
|
||||||
|
|
||||||
|
<InfoAlert v-else title="Informácia" text="Zatiaľ nie sú zaregistrovaní žiadni študenti." />
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<!-- Delete Confirmation Dialog -->
|
||||||
|
<v-dialog v-model="deleteDialog" max-width="500px">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="text-h5">
|
||||||
|
Potvrdiť vymazanie
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<p>
|
||||||
|
Naozaj chcete vymazať študenta <strong>{{ studentToDelete?.name }}</strong>?
|
||||||
|
</p>
|
||||||
|
<p class="text-error mt-2">
|
||||||
|
Táto akcia vymaže aj všetky súvisiace dáta (praxe, statusy, atď.) a <strong>nie je možné ju
|
||||||
|
vrátiť späť</strong>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Error message -->
|
||||||
|
<ErrorAlert v-if="deleteError" :error="deleteError" />
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn color="grey" variant="text" @click="closeDeleteDialog" :disabled="deleteLoading">
|
||||||
|
Zrušiť
|
||||||
|
</v-btn>
|
||||||
|
<v-btn color="red" variant="text" @click="deleteStudent" :loading="deleteLoading">
|
||||||
|
Vymazať
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#page-container-card {
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.op-btn {
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { User } from '~/types/user';
|
|
||||||
|
|
||||||
definePageMeta({
|
|
||||||
middleware: ['sanctum:auth', 'company-only'],
|
|
||||||
});
|
|
||||||
|
|
||||||
useSeoMeta({
|
|
||||||
title: "Portál firmy | ISOP",
|
|
||||||
ogTitle: "Portál firmy",
|
|
||||||
description: "Portál firmy ISOP",
|
|
||||||
ogDescription: "Portál firmy",
|
|
||||||
});
|
|
||||||
|
|
||||||
const user = useSanctumUser<User>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<v-container fluid>
|
|
||||||
<v-card id="footer-card">
|
|
||||||
<h1>Vitajte, {{ user?.name }} <em>({{ user?.company_data?.name }})</em></h1>
|
|
||||||
|
|
||||||
|
|
||||||
</v-card>
|
|
||||||
</v-container>
|
|
||||||
</template>
|
|
||||||
45
frontend/app/pages/dashboard/company/index.vue
Normal file
45
frontend/app/pages/dashboard/company/index.vue
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { User } from '~/types/user';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: ['sanctum:auth', 'company-only'],
|
||||||
|
});
|
||||||
|
|
||||||
|
useSeoMeta({
|
||||||
|
title: "Portál firmy | ISOP",
|
||||||
|
ogTitle: "Portál firmy",
|
||||||
|
description: "Portál firmy ISOP",
|
||||||
|
ogDescription: "Portál firmy",
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = useSanctumUser<User>();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-container fluid>
|
||||||
|
<v-card id="page-container-card">
|
||||||
|
<h1>Vitajte, {{ user?.name }} <em>({{ user?.company_data?.name }})</em></h1>
|
||||||
|
|
||||||
|
<!-- spacer -->
|
||||||
|
<div style="height: 40px;"></div>
|
||||||
|
|
||||||
|
<v-btn prepend-icon="mdi-briefcase" color="blue" class="mr-2" to="/dashboard/company/internships">
|
||||||
|
Praxe
|
||||||
|
</v-btn>
|
||||||
|
<v-btn prepend-icon="mdi-account-circle" color="blue" class="mr-2" to="/account">
|
||||||
|
Môj profil
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<!-- spacer -->
|
||||||
|
<div style="height: 40px;"></div>
|
||||||
|
</v-card>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#page-container-card {
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
125
frontend/app/pages/dashboard/company/internships/edit/[id].vue
Normal file
125
frontend/app/pages/dashboard/company/internships/edit/[id].vue
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { InternshipStatus, prettyInternshipStatus } from '~/types/internship_status';
|
||||||
|
import type { Internship, NewInternship } from '~/types/internships';
|
||||||
|
import { FetchError } from 'ofetch';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: ['sanctum:auth', 'company-only'],
|
||||||
|
});
|
||||||
|
|
||||||
|
useSeoMeta({
|
||||||
|
title: "Edit praxe | ISOP",
|
||||||
|
ogTitle: "Edit praxe",
|
||||||
|
description: "Edit praxe ISOP",
|
||||||
|
ogDescription: "Edit praxe",
|
||||||
|
});
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const client = useSanctumClient();
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const action_error = ref(null as null | string);
|
||||||
|
const refreshKey = ref(0);
|
||||||
|
|
||||||
|
const { data, error: load_error, pending, refresh } = await useLazySanctumFetch<Internship>(`/api/internships/${route.params.id}`);
|
||||||
|
|
||||||
|
async function handleUpdateOfBasicInfo(internship: NewInternship) {
|
||||||
|
action_error.value = null;
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client(`/api/internships/${route.params.id}/basic`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: internship
|
||||||
|
});
|
||||||
|
|
||||||
|
navigateTo("/dashboard/company/internships");
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof FetchError) {
|
||||||
|
action_error.value = e.response?._data.message;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-container fluid>
|
||||||
|
<v-card id="page-container-card">
|
||||||
|
<h1>Edit praxe</h1>
|
||||||
|
|
||||||
|
<!-- spacer -->
|
||||||
|
<div style="height: 40px;"></div>
|
||||||
|
|
||||||
|
<!-- Čakajúca hláška -->
|
||||||
|
<LoadingAlert v-if="pending" />
|
||||||
|
|
||||||
|
<!-- Chybová hláška -->
|
||||||
|
<ErrorAlert v-else-if="load_error" :error="load_error.message" />
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<!-- Čakajúca hláška -->
|
||||||
|
<LoadingAlert v-if="loading" />
|
||||||
|
|
||||||
|
<!-- Chybová hláška -->
|
||||||
|
<ErrorAlert v-else-if="action_error" :error="action_error" />
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<div>
|
||||||
|
<h2>Základné informácie</h2>
|
||||||
|
<ErrorAlert v-if="data?.status.status !== InternshipStatus.SUBMITTED" title="Blokované"
|
||||||
|
error='Vaša prax nie je v stave "Zadaná" a teda nemôžete meniť údaje' />
|
||||||
|
<InternshipEditor v-else :internship="data!" :submit="handleUpdateOfBasicInfo" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2>Stav</h2>
|
||||||
|
<h4>Aktuálny stav</h4>
|
||||||
|
<p>{{ prettyInternshipStatus(data?.status.status!) }}</p>
|
||||||
|
<p>Poznámka: <em>{{ data?.status.note }}</em></p>
|
||||||
|
<p>Posledná zmena: <em>{{ data?.status.changed }}, {{ data?.status.modified_by.name }}</em></p>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<h4>História</h4>
|
||||||
|
<InternshipStatusHistoryView :internship="data!" />
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<h4>Zmena stavu</h4>
|
||||||
|
<InternshipStatusEditor :internship="data!"
|
||||||
|
@successful-submit="() => { refresh(); refreshKey++; }" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2>Nahratie dokumentov</h2>
|
||||||
|
|
||||||
|
<ErrorAlert v-if="data?.status.status !== InternshipStatus.CONFIRMED_BY_COMPANY"
|
||||||
|
title="Blokované"
|
||||||
|
error='Vaša prax nie je v stave "Schválená" a teda nemôžete nahrať dokumenty.' />
|
||||||
|
|
||||||
|
<InternshipDocumentEditor v-else :internship="data!" @successful-submit="refresh" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#page-container-card {
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
42
frontend/app/pages/dashboard/company/internships/index.vue
Normal file
42
frontend/app/pages/dashboard/company/internships/index.vue
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
middleware: ['sanctum:auth', 'company-only'],
|
||||||
|
});
|
||||||
|
|
||||||
|
useSeoMeta({
|
||||||
|
title: "Portál firmy - praxe | ISOP",
|
||||||
|
ogTitle: "Portál firmy - praxe",
|
||||||
|
description: "Portál firmy - praxe ISOP",
|
||||||
|
ogDescription: "Portál firmy - praxe",
|
||||||
|
});
|
||||||
|
|
||||||
|
const headers = [
|
||||||
|
{ title: 'Študent', key: 'student', align: 'left' },
|
||||||
|
{ title: 'Od', key: 'start', align: 'left' },
|
||||||
|
{ title: 'Do', key: 'end', align: 'left' },
|
||||||
|
{ title: 'Ročník', key: 'year_of_study', align: 'middle' },
|
||||||
|
{ title: 'Semester', key: 'semester', align: 'middle' },
|
||||||
|
{ title: 'Stav', key: 'status', align: 'middle' },
|
||||||
|
{ title: 'Operácie', key: 'ops', align: 'middle' },
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-container fluid>
|
||||||
|
<v-card id="page-container-card">
|
||||||
|
<h1>Praxe študentov</h1>
|
||||||
|
|
||||||
|
<!-- spacer -->
|
||||||
|
<div style="height: 40px;"></div>
|
||||||
|
|
||||||
|
<InternshipListView mode="company" />
|
||||||
|
</v-card>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#page-container-card {
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
130
frontend/app/pages/dashboard/company/profile/index.vue
Normal file
130
frontend/app/pages/dashboard/company/profile/index.vue
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { User } from '~/types/user';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: ['sanctum:auth', 'company-only'],
|
||||||
|
});
|
||||||
|
|
||||||
|
useSeoMeta({
|
||||||
|
title: 'Môj profil | ISOP',
|
||||||
|
ogTitle: 'Môj profil',
|
||||||
|
description: 'Profil firmy ISOP',
|
||||||
|
ogDescription: 'Profil firmy ISOP',
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = useSanctumUser<User>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-container fluid class="page-container">
|
||||||
|
<v-card id="profile-card">
|
||||||
|
<template v-if="user">
|
||||||
|
<div class="header">
|
||||||
|
<v-avatar size="64" class="mr-4">
|
||||||
|
<v-icon size="48">mdi-account-circle</v-icon>
|
||||||
|
</v-avatar>
|
||||||
|
<div>
|
||||||
|
<h2 class="title">Môj profil</h2>
|
||||||
|
<p class="subtitle">{{ user?.company_data?.name || user?.name }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-divider class="my-4" />
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<!-- Údaje firmy -->
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<h3 class="section-title">Údaje firmy</h3>
|
||||||
|
<v-list density="compact" class="readonly-list">
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-title>Názov</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ user?.company_data?.name || '—' }}</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-title>Adresa</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ user?.company_data?.address || '—' }}</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-title>IČO</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ user?.company_data?.ico ?? '—' }}</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<!-- Kontaktná osoba -->
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<h3 class="section-title">Kontaktná osoba</h3>
|
||||||
|
<v-list density="compact" class="readonly-list">
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-title>Meno</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ user?.first_name || '—' }}</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-title>Priezvisko</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ user?.last_name || '—' }}</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-title>Email</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ user?.email || '—' }}</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-title>Telefón</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ user?.phone || '—' }}</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item class="mt-4">
|
||||||
|
<v-btn prepend-icon="mdi mdi-pencil" color="blue" class="mr-2">
|
||||||
|
Zmeniť heslo
|
||||||
|
</v-btn>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<v-skeleton-loader
|
||||||
|
type="heading, text, text, divider, list-item-two-line, list-item-two-line, list-item-two-line"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</v-card>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
.page-container {
|
||||||
|
max-width: 1120px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding-left: 24px;
|
||||||
|
padding-right: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#profile-card { padding: 16px; }
|
||||||
|
.header { display: flex; align-items: center; }
|
||||||
|
.title { font-size: 24px; font-weight: 700; margin: 0; }
|
||||||
|
.subtitle { color: #555; margin-top: 4px; }
|
||||||
|
.section-title { font-size: 16px; font-weight: 700; margin: 8px 0 8px; }
|
||||||
|
|
||||||
|
.readonly-list {
|
||||||
|
--v-list-padding-start: 0px;
|
||||||
|
}
|
||||||
|
.readonly-list :deep(.v-list-item) {
|
||||||
|
--v-list-item-padding-start: 0px;
|
||||||
|
padding-left: 0 !important;
|
||||||
|
}
|
||||||
|
.readonly-list :deep(.v-list-item__content) {
|
||||||
|
padding-left: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.readonly-list :deep(.v-list-item-subtitle) {
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
|
||||||
|
.readonly-list :deep(.v-list-item-title) { font-weight: 600; }
|
||||||
|
</style>
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { User } from '~/types/user';
|
|
||||||
|
|
||||||
definePageMeta({
|
|
||||||
middleware: ['sanctum:auth', 'student-only'],
|
|
||||||
});
|
|
||||||
|
|
||||||
useSeoMeta({
|
|
||||||
title: "Portál študenta | ISOP",
|
|
||||||
ogTitle: "Portál študenta",
|
|
||||||
description: "Portál študenta ISOP",
|
|
||||||
ogDescription: "Portál študenta",
|
|
||||||
});
|
|
||||||
|
|
||||||
const user = useSanctumUser<User>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<v-container fluid>
|
|
||||||
<v-card id="footer-card">
|
|
||||||
<h1>Vitajte, {{ user?.name }}</h1>
|
|
||||||
|
|
||||||
|
|
||||||
</v-card>
|
|
||||||
</v-container>
|
|
||||||
</template>
|
|
||||||
76
frontend/app/pages/dashboard/student/companies.vue
Normal file
76
frontend/app/pages/dashboard/student/companies.vue
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { CompanyData } from '~/types/company_data';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: ['sanctum:auth', 'student-only'],
|
||||||
|
});
|
||||||
|
|
||||||
|
useSeoMeta({
|
||||||
|
title: "Partnerské firmy | ISOP",
|
||||||
|
ogTitle: "Partnerské firmy",
|
||||||
|
description: "Partnerské firmy ISOP",
|
||||||
|
ogDescription: "Partnerské firmy",
|
||||||
|
});
|
||||||
|
|
||||||
|
const headers = [
|
||||||
|
{ title: 'Názov', key: 'name', align: 'left' },
|
||||||
|
{ title: 'IČO', key: 'ico', align: 'left' },
|
||||||
|
{ title: 'Kontaktná osoba', key: 'contact_name', align: 'left' },
|
||||||
|
{ title: 'Telefón', key: 'phone', align: 'middle' },
|
||||||
|
{ title: 'E-mail', key: 'email', align: 'middle' },
|
||||||
|
{ title: 'Prijímajú študentov', key: 'hiring', align: 'middle' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const { data, error, pending } = await useLazySanctumFetch<CompanyData[]>('/api/companies/simple');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-container fluid>
|
||||||
|
<v-card id="page-container-card">
|
||||||
|
<h1>Partnerské firmy</h1>
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<!-- spacer -->
|
||||||
|
<div style="height: 40px;"></div>
|
||||||
|
|
||||||
|
<!-- Čakajúca hláška -->
|
||||||
|
<LoadingAlert v-if="pending" />
|
||||||
|
|
||||||
|
<!-- Chybová hláška -->
|
||||||
|
<ErrorAlert v-else-if="error" :error="error?.message" />
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<p>Aktuálne spolupracujeme s {{ data?.length }} firmami.</p>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<v-table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th v-for="header in headers" :class="'text-' + header.align">
|
||||||
|
<strong>{{ header.title }}</strong>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="item in data">
|
||||||
|
<td>{{ item.name }}</td>
|
||||||
|
<td>{{ item.ico }}</td>
|
||||||
|
<td>{{ item.contact.name }}</td>
|
||||||
|
<td>{{ item.contact.phone }}</td>
|
||||||
|
<td>{{ item.contact.email }}</td>
|
||||||
|
<td>{{ item.hiring ? "Áno" : "Nie" }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</v-table>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#page-container-card {
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
53
frontend/app/pages/dashboard/student/index.vue
Normal file
53
frontend/app/pages/dashboard/student/index.vue
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { User } from '~/types/user';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: ['sanctum:auth', 'student-only'],
|
||||||
|
});
|
||||||
|
|
||||||
|
useSeoMeta({
|
||||||
|
title: "Portál študenta | ISOP",
|
||||||
|
ogTitle: "Portál študenta",
|
||||||
|
description: "Portál študenta ISOP",
|
||||||
|
ogDescription: "Portál študenta",
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = useSanctumUser<User>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-container fluid>
|
||||||
|
<v-card id="page-container-card">
|
||||||
|
<h1>Vitajte, {{ user?.name }}</h1>
|
||||||
|
<hr />
|
||||||
|
<small>{{ user?.student_data?.study_field }}, {{ user?.student_data?.personal_email }}</small>
|
||||||
|
|
||||||
|
<!-- spacer -->
|
||||||
|
<div style="height: 40px;"></div>
|
||||||
|
|
||||||
|
<v-btn prepend-icon="mdi-plus" color="blue" class="mr-2" to="/dashboard/student/internships/create">
|
||||||
|
Pridať
|
||||||
|
</v-btn>
|
||||||
|
<v-btn prepend-icon="mdi-domain" color="blue" class="mr-2" to="/dashboard/student/companies">
|
||||||
|
Firmy
|
||||||
|
</v-btn>
|
||||||
|
<v-btn prepend-icon="mdi-pencil" color="orange" class="mr-2" to="/account">
|
||||||
|
Môj profil
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<!-- spacer -->
|
||||||
|
<div style="height: 40px;"></div>
|
||||||
|
|
||||||
|
<h3>Moje praxe</h3>
|
||||||
|
|
||||||
|
<InternshipListView mode="student" />
|
||||||
|
</v-card>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#page-container-card {
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
67
frontend/app/pages/dashboard/student/internships/create.vue
Normal file
67
frontend/app/pages/dashboard/student/internships/create.vue
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { NewInternship } from '~/types/internships';
|
||||||
|
import { FetchError } from 'ofetch';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: ['sanctum:auth', 'student-only'],
|
||||||
|
});
|
||||||
|
|
||||||
|
useSeoMeta({
|
||||||
|
title: "Vytvorenie praxe | ISOP",
|
||||||
|
ogTitle: "Vytvorenie praxe",
|
||||||
|
description: "Vytvorenie praxe ISOP",
|
||||||
|
ogDescription: "Vytvorenie praxe",
|
||||||
|
});
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const error = ref(null as null | string);
|
||||||
|
|
||||||
|
const client = useSanctumClient();
|
||||||
|
|
||||||
|
async function handleInternshipRegistration(internship: NewInternship) {
|
||||||
|
try {
|
||||||
|
await client("/api/internships/new", {
|
||||||
|
method: 'PUT',
|
||||||
|
body: internship
|
||||||
|
});
|
||||||
|
|
||||||
|
navigateTo("/dashboard/student");
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof FetchError) {
|
||||||
|
error.value = e.response?._data.message;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-container fluid>
|
||||||
|
<v-card id="page-container-card">
|
||||||
|
<h1>Vytvorenie praxe</h1>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<!-- Čakajúca hláška -->
|
||||||
|
<LoadingAlert v-if="loading" />
|
||||||
|
|
||||||
|
<!-- Chybová hláška -->
|
||||||
|
<ErrorAlert v-if="error" :error="error" />
|
||||||
|
|
||||||
|
<InternshipEditor v-show="!loading" :submit="handleInternshipRegistration" />
|
||||||
|
</v-card>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#page-container-card {
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
118
frontend/app/pages/dashboard/student/internships/edit/[id].vue
Normal file
118
frontend/app/pages/dashboard/student/internships/edit/[id].vue
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { InternshipStatus, prettyInternshipStatus } from '~/types/internship_status';
|
||||||
|
import type { Internship, NewInternship } from '~/types/internships';
|
||||||
|
import { FetchError } from 'ofetch';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: ['sanctum:auth', 'student-only'],
|
||||||
|
});
|
||||||
|
|
||||||
|
useSeoMeta({
|
||||||
|
title: "Edit praxe | ISOP",
|
||||||
|
ogTitle: "Edit praxe",
|
||||||
|
description: "Edit praxe ISOP",
|
||||||
|
ogDescription: "Edit praxe",
|
||||||
|
});
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const client = useSanctumClient();
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const action_error = ref(null as null | string);
|
||||||
|
|
||||||
|
const { data, error, pending, refresh } = await useLazySanctumFetch<Internship>(`/api/internships/${route.params.id}`);
|
||||||
|
|
||||||
|
async function handleUpdateOfBasicInfo(internship: NewInternship) {
|
||||||
|
action_error.value = null;
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client(`/api/internships/${route.params.id}/basic`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: internship
|
||||||
|
});
|
||||||
|
|
||||||
|
navigateTo("/dashboard/student");
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof FetchError) {
|
||||||
|
action_error.value = e.response?._data.message;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-container fluid>
|
||||||
|
<v-card id="page-container-card">
|
||||||
|
<h1>Edit praxe</h1>
|
||||||
|
|
||||||
|
<!-- spacer -->
|
||||||
|
<div style="height: 40px;"></div>
|
||||||
|
|
||||||
|
<!-- Čakajúca hláška -->
|
||||||
|
<LoadingAlert v-if="pending" />
|
||||||
|
|
||||||
|
<!-- Chybová hláška -->
|
||||||
|
<ErrorAlert v-else-if="error" :error="error.message" />
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<!-- Čakajúca hláška -->
|
||||||
|
<LoadingAlert v-if="loading" />
|
||||||
|
|
||||||
|
<!-- Chybová hláška -->
|
||||||
|
<ErrorAlert v-else-if="action_error" :error="action_error" />
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<div>
|
||||||
|
<h2>Základné informácie</h2>
|
||||||
|
<ErrorAlert v-if="data?.status.status !== InternshipStatus.SUBMITTED"
|
||||||
|
error='Vaša prax nie je v stave "Zadaná" a teda nemôžete meniť údaje.' />
|
||||||
|
<InternshipEditor v-else :internship="data!" :submit="handleUpdateOfBasicInfo" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2>Stav</h2>
|
||||||
|
<h4>Aktuálny stav</h4>
|
||||||
|
<p>{{ prettyInternshipStatus(data?.status.status!) }}</p>
|
||||||
|
<p>Poznámka: <em>{{ data?.status.note }}</em></p>
|
||||||
|
<p>Posledná zmena: <em>{{ data?.status.changed }}, {{ data?.status.modified_by.name }}</em></p>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<h4>História</h4>
|
||||||
|
<InternshipStatusHistoryView :internship="data!" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2>Nahratie dokumentov</h2>
|
||||||
|
|
||||||
|
<ErrorAlert v-if="data?.status.status !== InternshipStatus.CONFIRMED_BY_COMPANY"
|
||||||
|
title="Blokované"
|
||||||
|
error='Vaša prax nie je v stave "Schválená" a teda nemôžete nahrať dokumenty.' />
|
||||||
|
|
||||||
|
<InternshipDocumentEditor v-else :internship="data!" @successful-submit="refresh" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#page-container-card {
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -21,7 +21,7 @@ useSeoMeta({
|
|||||||
|
|
||||||
<v-row class="pc" align="stretch" justify="start">
|
<v-row class="pc" align="stretch" justify="start">
|
||||||
<InfoCard title="Rozsah a účasť"
|
<InfoCard title="Rozsah a účasť"
|
||||||
description="Absolvovanie praxe v minimálnom rozsahu 130 hodín a povinná účasť na úvodnom stretnutí"
|
description="Absolvovanie praxe v minimálnom rozsahu 150 hodín a povinná účasť na úvodnom stretnutí"
|
||||||
icon="mdi-clock-time-five-outline" />
|
icon="mdi-clock-time-five-outline" />
|
||||||
<InfoCard title="Denník praxe"
|
<InfoCard title="Denník praxe"
|
||||||
description="Priebežné vedenie denníka praxe podľa predpísanej štruktúry a jeho odovzdanie na konci obdobia."
|
description="Priebežné vedenie denníka praxe podľa predpísanej štruktúry a jeho odovzdanie na konci obdobia."
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ async function handleLogin() {
|
|||||||
try {
|
try {
|
||||||
await login(form.value);
|
await login(form.value);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof FetchError && e.response?.status === 422) {
|
if (e instanceof FetchError) {
|
||||||
error.value = e.response?._data.message;
|
error.value = e.response?._data.message;
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -50,16 +50,14 @@ async function handleLogin() {
|
|||||||
<h2 class="page-title">Prihlásenie</h2>
|
<h2 class="page-title">Prihlásenie</h2>
|
||||||
|
|
||||||
<!-- Chybová hláška -->
|
<!-- Chybová hláška -->
|
||||||
<v-alert v-if="error !== null" density="compact" :text="error" title="Chyba" type="error"
|
<ErrorAlert v-if="error" :error="error" />
|
||||||
id="login-error-alert" class="mx-auto alert"></v-alert>
|
|
||||||
|
|
||||||
<!-- Čakajúca hláška -->
|
<!-- Čakajúca hláška -->
|
||||||
<v-alert v-if="loading" density="compact" text="Prosím čakajte..." title="Spracovávam" type="info"
|
<LoadingAlert v-if="loading" />
|
||||||
id="login-error-alert" class="mx-auto alert"></v-alert>
|
|
||||||
|
|
||||||
<v-form v-else v-model="isValid" @submit.prevent="handleLogin">
|
<v-form v-else v-model="isValid" @submit.prevent="handleLogin">
|
||||||
<v-text-field v-model="form.email" :rules="[rules.required, rules.email]" label="Email:"
|
<v-text-field v-model="form.email" :rules="[rules.required, rules.email]" label="Email:"
|
||||||
variant="outlined" density="comfortable" />
|
variant="outlined" density="comfortable" type="email" />
|
||||||
|
|
||||||
<v-text-field v-model="form.password" :rules="[rules.required]"
|
<v-text-field v-model="form.password" :rules="[rules.required]"
|
||||||
:type="showPassword ? 'text' : 'password'" label="Heslo:" variant="outlined" density="comfortable"
|
:type="showPassword ? 'text' : 'password'" label="Heslo:" variant="outlined" density="comfortable"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { NewRole } from '~/types/role';
|
import { NewRole } from '~/types/role';
|
||||||
import type { NewUser } from '~/types/user';
|
import type { NewUser } from '~/types/user';
|
||||||
|
import { FetchError } from 'ofetch';
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: ['sanctum:guest'],
|
middleware: ['sanctum:guest'],
|
||||||
@@ -18,7 +19,8 @@ useSeoMeta({
|
|||||||
const rules = {
|
const rules = {
|
||||||
required: (v: any) => (!!v && String(v).trim().length > 0) || 'Povinné pole',
|
required: (v: any) => (!!v && String(v).trim().length > 0) || 'Povinné pole',
|
||||||
email: (v: string) => /.+@.+\..+/.test(v) || 'Zadajte platný email',
|
email: (v: string) => /.+@.+\..+/.test(v) || 'Zadajte platný email',
|
||||||
phone: (v: string) => (!v || /^[0-9 +()-]{6,}$/.test(v)) || 'Zadajte platné telefónne číslo',
|
phone: (v: string) =>
|
||||||
|
(!v || /^\+[0-9]{6,13}$/.test(v)) || 'Zadajte platné telefónne číslo. Príklad: +421908123456',
|
||||||
mustAgree: (v: boolean) => v === true || 'Je potrebné súhlasiť',
|
mustAgree: (v: boolean) => v === true || 'Je potrebné súhlasiť',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -63,8 +65,10 @@ async function handleRegistration() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
navigateTo("/");
|
navigateTo("/");
|
||||||
} catch (e: any) {
|
} catch (e) {
|
||||||
error.value = e.data?.message as string;
|
if (e instanceof FetchError) {
|
||||||
|
error.value = e.response?._data.message;
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
@@ -77,12 +81,10 @@ async function handleRegistration() {
|
|||||||
<h4 class="page-title">Registrácia firmy</h4>
|
<h4 class="page-title">Registrácia firmy</h4>
|
||||||
|
|
||||||
<!-- Chybová hláška -->
|
<!-- Chybová hláška -->
|
||||||
<v-alert v-if="error !== null" density="compact" :text="error" title="Chyba" type="error"
|
<ErrorAlert v-if=error :error="error" />
|
||||||
id="login-error-alert" class="mx-auto"></v-alert>
|
|
||||||
|
|
||||||
<!-- Čakajúca hláška -->
|
<!-- Čakajúca hláška -->
|
||||||
<v-alert v-if="loading" density="compact" text="Prosím čakajte..." title="Spracovávam" type="info"
|
<LoadingAlert v-if="loading" />
|
||||||
id="login-error-alert" class="mx-auto"></v-alert>
|
|
||||||
|
|
||||||
<v-form v-else v-model="isValid" @submit.prevent="handleRegistration">
|
<v-form v-else v-model="isValid" @submit.prevent="handleRegistration">
|
||||||
<v-text-field v-model="form.name" :rules="[rules.required]" label="Názov firmy:" variant="outlined"
|
<v-text-field v-model="form.name" :rules="[rules.required]" label="Názov firmy:" variant="outlined"
|
||||||
@@ -100,8 +102,8 @@ async function handleRegistration() {
|
|||||||
density="comfortable" />
|
density="comfortable" />
|
||||||
<v-text-field v-model="form.email" :rules="[rules.required, rules.email]" label="Email:"
|
<v-text-field v-model="form.email" :rules="[rules.required, rules.email]" label="Email:"
|
||||||
variant="outlined" density="comfortable" />
|
variant="outlined" density="comfortable" />
|
||||||
<v-text-field v-model="form.phone" :rules="[rules.phone]" label="Telefón:" variant="outlined"
|
<v-text-field v-model="form.phone" :rules="[rules.phone]" label="Telefón (s predvoľbou):"
|
||||||
density="comfortable" />
|
variant="outlined" density="comfortable" />
|
||||||
|
|
||||||
<v-checkbox v-model="form.consent" :rules="[rules.mustAgree]"
|
<v-checkbox v-model="form.consent" :rules="[rules.mustAgree]"
|
||||||
label="Súhlasím s podmienkami spracúvania osobných údajov" density="comfortable" />
|
label="Súhlasím s podmienkami spracúvania osobných údajov" density="comfortable" />
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { NewRole } from '~/types/role';
|
import { NewRole } from '~/types/role';
|
||||||
import type { NewUser } from '~/types/user';
|
import type { NewUser } from '~/types/user';
|
||||||
|
import { FetchError } from 'ofetch';
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: ['sanctum:guest'],
|
middleware: ['sanctum:guest'],
|
||||||
@@ -22,11 +23,14 @@ const rules = {
|
|||||||
personal_email: (v: string) =>
|
personal_email: (v: string) =>
|
||||||
/.+@.+\..+/.test(v) || 'Zadajte platný osobný email',
|
/.+@.+\..+/.test(v) || 'Zadajte platný osobný email',
|
||||||
phone: (v: string) =>
|
phone: (v: string) =>
|
||||||
(!v || /^[0-9 +()-]{6,}$/.test(v)) || 'Zadajte platné telefónne číslo',
|
(!v || /^\+[0-9]{6,13}$/.test(v)) || 'Zadajte platné telefónne číslo. Príklad: +421908123456',
|
||||||
mustAgree: (v: boolean) => v === true || 'Je potrebné súhlasiť',
|
mustAgree: (v: boolean) => v === true || 'Je potrebné súhlasiť',
|
||||||
};
|
};
|
||||||
const programs = [
|
const programs = [
|
||||||
'Aplikovaná informatika',
|
{ title: 'Aplikovaná informatika, Bc. (AI22b)', value: 'AI22b' },
|
||||||
|
{ title: 'Aplikovaná informatika, Bc. (AI15b)', value: 'AI15b' },
|
||||||
|
{ title: 'Aplikovaná informatika, Mgr. (AI22m)', value: 'AI22m' },
|
||||||
|
{ title: 'Aplikovaná informatika, Mgr. (AI15m)', value: 'AI15m' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const isValid = ref(false);
|
const isValid = ref(false);
|
||||||
@@ -37,10 +41,11 @@ const form = ref({
|
|||||||
studentEmail: '',
|
studentEmail: '',
|
||||||
personalEmail: '',
|
personalEmail: '',
|
||||||
phone: '',
|
phone: '',
|
||||||
studyProgram: programs[0] as string,
|
studyProgram: programs[0]!.value,
|
||||||
year_of_study: 1,
|
year_of_study: 1,
|
||||||
consent: false,
|
consent: false,
|
||||||
});
|
});
|
||||||
|
const maxYearOfStudy = ref(0);
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const error = ref(null as null | string);
|
const error = ref(null as null | string);
|
||||||
@@ -71,12 +76,18 @@ async function handleRegistration() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
navigateTo("/");
|
navigateTo("/");
|
||||||
} catch (e: any) {
|
} catch (e) {
|
||||||
error.value = e.data?.message as string;
|
if (e instanceof FetchError) {
|
||||||
|
error.value = e.response?._data.message;
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(form, (newForm) => {
|
||||||
|
maxYearOfStudy.value = newForm.studyProgram.slice(-1) === 'b' ? 3 : 2;
|
||||||
|
}, { deep: true, immediate: true });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -85,12 +96,10 @@ async function handleRegistration() {
|
|||||||
<h4 class="page-title">Registrácia študenta</h4>
|
<h4 class="page-title">Registrácia študenta</h4>
|
||||||
|
|
||||||
<!-- Chybová hláška -->
|
<!-- Chybová hláška -->
|
||||||
<v-alert v-if="error !== null" density="compact" :text="error" title="Chyba" type="error"
|
<ErrorAlert v-if="error" :error="error" />
|
||||||
id="login-error-alert" class="mx-auto alert"></v-alert>
|
|
||||||
|
|
||||||
<!-- Čakajúca hláška -->
|
<!-- Čakajúca hláška -->
|
||||||
<v-alert v-if="loading" density="compact" text="Prosím čakajte..." title="Spracovávam" type="info"
|
<LoadingAlert v-if="loading" />
|
||||||
id="login-error-alert" class="mx-auto alert"></v-alert>
|
|
||||||
|
|
||||||
<v-form v-else v-model="isValid" @submit.prevent="handleRegistration">
|
<v-form v-else v-model="isValid" @submit.prevent="handleRegistration">
|
||||||
<v-text-field v-model="form.firstName" :rules="[rules.required]" label="Meno:" variant="outlined"
|
<v-text-field v-model="form.firstName" :rules="[rules.required]" label="Meno:" variant="outlined"
|
||||||
@@ -108,14 +117,14 @@ async function handleRegistration() {
|
|||||||
<v-text-field v-model="form.personalEmail" :rules="[rules.required, rules.personal_email]"
|
<v-text-field v-model="form.personalEmail" :rules="[rules.required, rules.personal_email]"
|
||||||
label="Alternatívny email:" variant="outlined" density="comfortable" />
|
label="Alternatívny email:" variant="outlined" density="comfortable" />
|
||||||
|
|
||||||
<v-text-field v-model="form.phone" :rules="[rules.required, rules.phone]" label="Telefón:"
|
<v-text-field v-model="form.phone" :rules="[rules.required, rules.phone]"
|
||||||
variant="outlined" density="comfortable" />
|
label="Telefón (s predvoľbou):" variant="outlined" density="comfortable" />
|
||||||
|
|
||||||
<v-select v-model="form.studyProgram" :items="programs" :rules="[rules.required]"
|
<v-select v-model="form.studyProgram" :items="programs" :rules="[rules.required]"
|
||||||
label="Študijný odbor:" variant="outlined" density="comfortable" />
|
label="Študijný odbor:" variant="outlined" density="comfortable" />
|
||||||
|
|
||||||
<v-number-input control-variant="split" v-model="form.year_of_study" :rules="[rules.required]"
|
<v-number-input control-variant="split" v-model="form.year_of_study" :rules="[rules.required]"
|
||||||
label="Ročník:" :min="1" :max="5"></v-number-input>
|
label="Ročník:" :min="1" :max="maxYearOfStudy"></v-number-input>
|
||||||
|
|
||||||
<v-checkbox v-model="form.consent" :rules="[rules.mustAgree]"
|
<v-checkbox v-model="form.consent" :rules="[rules.mustAgree]"
|
||||||
label="Súhlasím s podmienkami spracúvania osobných údajov" density="comfortable" />
|
label="Súhlasím s podmienkami spracúvania osobných údajov" density="comfortable" />
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { FetchError } from 'ofetch';
|
||||||
|
|
||||||
const client = useSanctumClient();
|
const client = useSanctumClient();
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
@@ -36,8 +38,10 @@ async function handleReset() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
navigateTo("/reset_psw/request_sent");
|
navigateTo("/reset_psw/request_sent");
|
||||||
} catch (e: any) {
|
} catch (e) {
|
||||||
error.value = e.data?.message as string;
|
if (e instanceof FetchError) {
|
||||||
|
error.value = e.response?._data.message;
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
@@ -50,12 +54,10 @@ async function handleReset() {
|
|||||||
<h2 class="page-title">Reset hesla</h2>
|
<h2 class="page-title">Reset hesla</h2>
|
||||||
|
|
||||||
<!-- Chybová hláška -->
|
<!-- Chybová hláška -->
|
||||||
<v-alert v-if="error !== null" density="compact" :text="error" title="Chyba" type="error"
|
<ErrorAlert v-if="error" :error="error" />
|
||||||
id="login-error-alert" class="alert mx-auto"></v-alert>
|
|
||||||
|
|
||||||
<!-- Čakajúca hláška -->
|
<!-- Čakajúca hláška -->
|
||||||
<v-alert v-if="loading" density="compact" text="Prosím čakajte..." title="Spracovávam" type="info"
|
<LoadingAlert v-if="loading" />
|
||||||
id="login-error-alert" class="alert mx-auto"></v-alert>
|
|
||||||
|
|
||||||
<v-form v-else v-model="isValid" @submit.prevent="handleReset">
|
<v-form v-else v-model="isValid" @submit.prevent="handleReset">
|
||||||
<v-text-field v-model="email" :rules="[rules.required, rules.email]" label="Email:" variant="outlined"
|
<v-text-field v-model="email" :rules="[rules.required, rules.email]" label="Email:" variant="outlined"
|
||||||
|
|||||||
@@ -16,8 +16,7 @@ useSeoMeta({
|
|||||||
<v-card id="page-container-card">
|
<v-card id="page-container-card">
|
||||||
<h2 class="page-title">Reset hesla</h2>
|
<h2 class="page-title">Reset hesla</h2>
|
||||||
|
|
||||||
<v-alert density="compact" text="Nové heslo vám bolo zaslané na e-mail" title="Reset hesla" type="info"
|
<SuccessAlert title="Reset hesla" text="Nové heslo vám bolo zaslané na e-mail" />
|
||||||
class="mx-auto"></v-alert>
|
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
11
frontend/app/types/api_keys.ts
Normal file
11
frontend/app/types/api_keys.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export type ApiKey = {
|
||||||
|
id: number,
|
||||||
|
name: string,
|
||||||
|
created_at: string,
|
||||||
|
last_used_at: string,
|
||||||
|
owner: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NewApiKey = {
|
||||||
|
key: string,
|
||||||
|
};
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import type { User } from "./user";
|
||||||
|
|
||||||
export interface CompanyData {
|
export interface CompanyData {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
address: string;
|
address: string;
|
||||||
ico: number;
|
ico: number;
|
||||||
contact: number;
|
contact: User;
|
||||||
hiring: boolean;
|
hiring: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
47
frontend/app/types/internship_status.ts
Normal file
47
frontend/app/types/internship_status.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import type { User } from "./user";
|
||||||
|
|
||||||
|
export interface InternshipStatusData {
|
||||||
|
status: InternshipStatus;
|
||||||
|
changed: string;
|
||||||
|
note: string;
|
||||||
|
modified_by: User;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface NewInternshipStatusData {
|
||||||
|
status: InternshipStatus;
|
||||||
|
note: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum InternshipStatus {
|
||||||
|
SUBMITTED = 'SUBMITTED',
|
||||||
|
|
||||||
|
CONFIRMED_BY_COMPANY = 'CONFIRMED_BY_COMPANY',
|
||||||
|
CONFIRMED_BY_ADMIN = 'CONFIRMED_BY_ADMIN',
|
||||||
|
|
||||||
|
DENIED_BY_COMPANY = 'DENIED_BY_COMPANY',
|
||||||
|
DENIED_BY_ADMIN = 'DENIED_BY_ADMIN',
|
||||||
|
|
||||||
|
DEFENDED = 'DEFENDED',
|
||||||
|
NOT_DEFENDED = 'NOT_DEFENDED',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function prettyInternshipStatus(status: InternshipStatus) {
|
||||||
|
switch (status) {
|
||||||
|
case InternshipStatus.SUBMITTED:
|
||||||
|
return "Zadané";
|
||||||
|
case InternshipStatus.CONFIRMED_BY_COMPANY:
|
||||||
|
return "Potvrdené firmou";
|
||||||
|
case InternshipStatus.CONFIRMED_BY_ADMIN:
|
||||||
|
return "Potvrdené garantom";
|
||||||
|
case InternshipStatus.DENIED_BY_COMPANY:
|
||||||
|
return "Zamietnuté firmou";
|
||||||
|
case InternshipStatus.DENIED_BY_ADMIN:
|
||||||
|
return "Zamietnuté garantom";
|
||||||
|
case InternshipStatus.DEFENDED:
|
||||||
|
return "Obhájené";
|
||||||
|
case InternshipStatus.NOT_DEFENDED:
|
||||||
|
return "Neobhájené";
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown internship status: '${status}'`);
|
||||||
|
}
|
||||||
|
}
|
||||||
59
frontend/app/types/internships.ts
Normal file
59
frontend/app/types/internships.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import type { CompanyData } from "./company_data";
|
||||||
|
import type { InternshipStatusData } from "./internship_status";
|
||||||
|
import type { User } from "./user";
|
||||||
|
|
||||||
|
export interface Internship {
|
||||||
|
id: number;
|
||||||
|
student: User;
|
||||||
|
company: CompanyData;
|
||||||
|
start: string;
|
||||||
|
end: string;
|
||||||
|
year_of_study: number;
|
||||||
|
semester: string;
|
||||||
|
position_description: string;
|
||||||
|
proof: boolean;
|
||||||
|
report: boolean;
|
||||||
|
report_confirmed: boolean;
|
||||||
|
status: InternshipStatusData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface NewInternship {
|
||||||
|
user_id: number;
|
||||||
|
company_id: number;
|
||||||
|
start: string;
|
||||||
|
end: string;
|
||||||
|
year_of_study: number;
|
||||||
|
semester: string;
|
||||||
|
position_description: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface InternshipFilter {
|
||||||
|
year: number | null;
|
||||||
|
company: string | null;
|
||||||
|
study_programe: string | null;
|
||||||
|
student: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function convertDate(date: string): Date {
|
||||||
|
const matcher = /^\d\d.\d\d.\d\d\d\d$/;
|
||||||
|
|
||||||
|
if (!matcher.test(date)) {
|
||||||
|
throw new Error(`Invalid date or format: '${date}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [day, month, year] = date.split('.').map(Number);
|
||||||
|
|
||||||
|
if (day === undefined || month === undefined || year === undefined) {
|
||||||
|
throw new Error(`Unable to parse date parts: '${date}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (month < 1 || month > 12) {
|
||||||
|
throw new Error(`Invalid month: ${month}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (day < 1 || day > 31) {
|
||||||
|
throw new Error(`Invalid day: ${day}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Date(year, month - 1, day);
|
||||||
|
}
|
||||||
4
frontend/app/types/pagination.ts
Normal file
4
frontend/app/types/pagination.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export type Paginated<T> = {
|
||||||
|
data: T[],
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
9
frontend/app/utils/index.ts
Normal file
9
frontend/app/utils/index.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export function triggerDownload(file: Blob, file_name: string, ext: string) {
|
||||||
|
const url = window.URL.createObjectURL(file);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = `${file_name}.${ext}`;
|
||||||
|
link.target = "_blank";
|
||||||
|
link.click();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
18
frontend/cypress.config.ts
Normal file
18
frontend/cypress.config.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { defineConfig } from "cypress";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
e2e: {
|
||||||
|
setupNodeEvents(on, config) {
|
||||||
|
// implement node event listeners here
|
||||||
|
},
|
||||||
|
baseUrl: 'http://localhost:3000',
|
||||||
|
video: true,
|
||||||
|
screenshotOnRunFailure: true,
|
||||||
|
},
|
||||||
|
component: {
|
||||||
|
devServer: {
|
||||||
|
framework: 'vue',
|
||||||
|
bundler: 'vite',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
86
frontend/cypress/e2e/admin/api_key_management/list.cy.ts
Normal file
86
frontend/cypress/e2e/admin/api_key_management/list.cy.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
describe('Admin API Key Management CRUD', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('/login')
|
||||||
|
|
||||||
|
cy.wait(3000)
|
||||||
|
|
||||||
|
cy.get('input[type="email"]').type('test@example.com')
|
||||||
|
cy.get('input[type="password"]').type('password')
|
||||||
|
cy.get('button[type="submit"]').click()
|
||||||
|
|
||||||
|
cy.wait(1000)
|
||||||
|
|
||||||
|
cy.contains("API Manažment").click()
|
||||||
|
cy.url().should('include', '/dashboard/admin/external_api')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should load the list of API keys', () => {
|
||||||
|
cy.get('table').within(() => {
|
||||||
|
cy.validateColumn('Názov', (name) => {
|
||||||
|
expect(name).to.not.be.empty
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.validateColumn('Vytvorené', (created_at) => {
|
||||||
|
expect(created_at).to.not.be.empty
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.validateColumn('Naposledy použité', (last_used_at) => {
|
||||||
|
expect(last_used_at).to.not.be.empty
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.validateColumn('Vlastník', (owner) => {
|
||||||
|
const nameParts = owner.trim().split(' ')
|
||||||
|
expect(nameParts).to.have.length.at.least(2)
|
||||||
|
expect(nameParts[0]).to.not.be.empty
|
||||||
|
expect(nameParts[1]).to.not.be.empty
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.contains('th', 'Operácie').parent('tr')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should be able to create an API key', () => {
|
||||||
|
// vytvorenie nového kľúča
|
||||||
|
cy.contains("Pridať").click()
|
||||||
|
cy.get('#newKeyName').type('cypress-e2e-test-key')
|
||||||
|
cy.contains("Vytvoriť").click()
|
||||||
|
|
||||||
|
cy.wait(3000)
|
||||||
|
cy.contains("Zavrieť").click()
|
||||||
|
|
||||||
|
// mali by sme mať práve 1 kľúč
|
||||||
|
cy.get('table tbody tr').its('length').then((count) => {
|
||||||
|
expect(count).to.be.greaterThan(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Kontrola názvu
|
||||||
|
cy.contains('cypress-e2e-test-key').should('be.visible')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should be able to delete an API key', () => {
|
||||||
|
let initialRowCount = 0
|
||||||
|
|
||||||
|
cy.get('table tbody tr').its('length').then((count) => {
|
||||||
|
initialRowCount = count
|
||||||
|
})
|
||||||
|
|
||||||
|
// získanie prvého riadka z tabuľky
|
||||||
|
cy.get('table tbody tr').then($rows => {
|
||||||
|
const selectedRow = [...$rows].find(row =>
|
||||||
|
Cypress.$(row).find('td').first().text().trim() === 'cypress-e2e-test-key'
|
||||||
|
);
|
||||||
|
cy.wrap(selectedRow).as('selectedRow');
|
||||||
|
})
|
||||||
|
|
||||||
|
// kliknutie na "Vymazať"
|
||||||
|
cy.get('@selectedRow').within(() => {
|
||||||
|
cy.contains('Vymazať').click()
|
||||||
|
})
|
||||||
|
|
||||||
|
// potvrdenie
|
||||||
|
cy.contains(' Áno ').click()
|
||||||
|
cy.wait(3000)
|
||||||
|
|
||||||
|
cy.contains('cypress-e2e-test-key').should('not.exist')
|
||||||
|
})
|
||||||
|
})
|
||||||
150
frontend/cypress/e2e/admin/companies/list.cy.ts
Normal file
150
frontend/cypress/e2e/admin/companies/list.cy.ts
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
describe('Admin Company CRUD', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('/login')
|
||||||
|
|
||||||
|
cy.wait(3000)
|
||||||
|
|
||||||
|
cy.get('input[type="email"]').type('test@example.com')
|
||||||
|
cy.get('input[type="password"]').type('password')
|
||||||
|
cy.get('button[type="submit"]').click()
|
||||||
|
|
||||||
|
cy.wait(1000)
|
||||||
|
|
||||||
|
cy.contains("Firmy").click()
|
||||||
|
cy.url().should('include', '/dashboard/admin/companies')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should load the list of companies in a proper format', () => {
|
||||||
|
cy.get('table').within(() => {
|
||||||
|
cy.validateColumn('Názov', (name) => {
|
||||||
|
expect(name).to.not.be.empty
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.validateColumn('IČO', (ico) => {
|
||||||
|
expect(ico.trim()).to.not.be.empty
|
||||||
|
expect(Number(ico.trim())).to.be.a('number').and.not.be.NaN
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.validateColumn('Kontaktná osoba', (contact_name) => {
|
||||||
|
const nameParts = contact_name.trim().split(' ')
|
||||||
|
expect(nameParts).to.have.length.at.least(2)
|
||||||
|
expect(nameParts[0]).to.not.be.empty
|
||||||
|
expect(nameParts[1]).to.not.be.empty
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.validateColumn('Telefón', (program) => {
|
||||||
|
expect(program.trim()).to.not.be.empty
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.validateColumn('E-mail', (email) => {
|
||||||
|
expect(email.trim()).to.not.be.empty
|
||||||
|
expect(email.trim()).to.include("@")
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.validateColumn('Prijímajú študentov', (hiring) => {
|
||||||
|
expect(hiring.trim()).to.be.oneOf(['Áno', 'Nie'])
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.contains('th', 'Operácie').parent('tr')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should be able to delete a company', () => {
|
||||||
|
let initialRowCount = 0
|
||||||
|
|
||||||
|
cy.get('table tbody tr').its('length').then((count) => {
|
||||||
|
initialRowCount = count
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.get('table tbody tr').first().within(() => {
|
||||||
|
cy.contains('Vymazať').click()
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.contains("Potvrdiť vymazanie").parent().should('be.visible')
|
||||||
|
cy.contains("Potvrdiť vymazanie").parent().contains("Vymazať").click()
|
||||||
|
cy.contains("Potvrdiť vymazanie").should('not.exist')
|
||||||
|
|
||||||
|
cy.wait(1000)
|
||||||
|
|
||||||
|
cy.get('table tbody tr').its('length').then((count) => {
|
||||||
|
expect(count).to.be.eq(initialRowCount - 1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should be able to edit a student', () => {
|
||||||
|
// Náhodné sety
|
||||||
|
const companyNames = [
|
||||||
|
'Tech Solutions s.r.o.',
|
||||||
|
'Digital Systems a.s.',
|
||||||
|
'Innovation Labs s.r.o.',
|
||||||
|
'Software House Slovakia',
|
||||||
|
'Data Analytics Group',
|
||||||
|
'Cloud Services s.r.o.',
|
||||||
|
'IT Consulting a.s.',
|
||||||
|
'Web Development Studio',
|
||||||
|
'Mobile Apps Company',
|
||||||
|
'Cyber Security s.r.o.'
|
||||||
|
]
|
||||||
|
|
||||||
|
// Výber náhodného študenta
|
||||||
|
cy.get('table tbody tr').then($rows => {
|
||||||
|
const randomIndex = Math.floor(Math.random() * $rows.length)
|
||||||
|
const randomRow = $rows.eq(randomIndex)
|
||||||
|
|
||||||
|
cy.wrap(randomIndex).as('selectedIndex')
|
||||||
|
cy.wrap(randomRow).as('selectedRow')
|
||||||
|
})
|
||||||
|
|
||||||
|
// Kliknutie na "Editovať"
|
||||||
|
cy.get('@selectedRow').within(() => {
|
||||||
|
cy.contains('Editovať').click()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Generovanie náhodného mena
|
||||||
|
|
||||||
|
const randomCompanyName = companyNames[Math.floor(Math.random() * companyNames.length)]
|
||||||
|
const randomHouseNumber = Math.floor(Math.random() * 200 + 1)
|
||||||
|
const randomAddress = `Hlavná ${randomHouseNumber}/1, Komárno, 946 01`
|
||||||
|
const randomICO = String(Math.floor(Math.random() * 90000000000) + 10000000000)
|
||||||
|
|
||||||
|
// Kontrola cesty
|
||||||
|
cy.url().should('include', '/dashboard/admin/companies/edit/')
|
||||||
|
|
||||||
|
// Zmena názvu
|
||||||
|
cy.get('#input-v-1-1').clear().type(randomCompanyName)
|
||||||
|
|
||||||
|
// Zmena adresy
|
||||||
|
cy.get('#input-v-1-4').clear().type(randomAddress)
|
||||||
|
|
||||||
|
// Zmena IČO
|
||||||
|
cy.get('#input-v-1-7').clear().type(randomICO)
|
||||||
|
|
||||||
|
// Uložiť zmeny
|
||||||
|
cy.contains('Uložiť zmeny').click()
|
||||||
|
|
||||||
|
// Počkanie na uloženie
|
||||||
|
cy.wait(2000)
|
||||||
|
cy.url().should('include', '/dashboard/admin/companies')
|
||||||
|
|
||||||
|
// Overenie zmien v tabuľke
|
||||||
|
cy.get('@selectedIndex').then((index) => {
|
||||||
|
cy.get('table tbody tr').eq(index as number).within(() => {
|
||||||
|
const expectedValues = [
|
||||||
|
`${randomCompanyName}`,
|
||||||
|
randomICO,
|
||||||
|
null, // netestuje sa
|
||||||
|
null, // netestuje sa
|
||||||
|
null, // netestuje sa
|
||||||
|
null // netestuje sa
|
||||||
|
]
|
||||||
|
|
||||||
|
cy.get('td').then($cells => {
|
||||||
|
expectedValues.forEach((expectedValue, i) => {
|
||||||
|
if (expectedValue === null) return; // skip checking
|
||||||
|
expect($cells.eq(i).text().trim()).to.equal(expectedValue)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
48
frontend/cypress/e2e/admin/dashboard.cy.ts
Normal file
48
frontend/cypress/e2e/admin/dashboard.cy.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
describe('Admin Dashboard', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('/login')
|
||||||
|
|
||||||
|
cy.wait(1000)
|
||||||
|
|
||||||
|
cy.get('input[type="email"]').type('test@example.com')
|
||||||
|
cy.get('input[type="password"]').type('password')
|
||||||
|
cy.get('button[type="submit"]').click()
|
||||||
|
|
||||||
|
cy.wait(1000)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should load the list of companies', () => {
|
||||||
|
cy.contains("Firmy").click()
|
||||||
|
cy.url().should('include', '/dashboard/admin/companies')
|
||||||
|
cy.get("table").find("tr").should("have.length.greaterThan", 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should load the list of students', () => {
|
||||||
|
cy.contains("Študenti").click()
|
||||||
|
cy.url().should('include', '/dashboard/admin/students')
|
||||||
|
cy.get("table").find("tr").should("have.length.greaterThan", 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should load the list of internships', () => {
|
||||||
|
cy.contains("Praxe").click()
|
||||||
|
cy.url().should('include', '/dashboard/admin/internships')
|
||||||
|
cy.get("table").find("tr").should("have.length.greaterThan", 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should load the my account details', () => {
|
||||||
|
cy.contains("Môj profil").click()
|
||||||
|
cy.url().should('include', '/account')
|
||||||
|
|
||||||
|
cy.contains("Môj profil")
|
||||||
|
cy.contains("Osobné údaje")
|
||||||
|
cy.contains("Meno")
|
||||||
|
cy.contains("Priezvisko")
|
||||||
|
|
||||||
|
// check if the names match the default test user
|
||||||
|
cy.get('#page-container-card > div:nth-child(5) > div:nth-child(1) > div > div.v-list-item-subtitle').should('have.text', 'Test')
|
||||||
|
cy.get("#page-container-card > div:nth-child(5) > div:nth-child(2) > div > div.v-list-item-subtitle").should('have.text', 'User')
|
||||||
|
|
||||||
|
// check if the "change my password" button is there
|
||||||
|
cy.get("#page-container-card > div:nth-child(6) > div > div > a > span.v-btn__content").invoke('text').then(text => text.trim()).should('equal', 'Zmeniť heslo')
|
||||||
|
})
|
||||||
|
})
|
||||||
37
frontend/cypress/e2e/admin/internships/downloads.cy.ts
Normal file
37
frontend/cypress/e2e/admin/internships/downloads.cy.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
describe('Admin Student Document Downloads', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('/login')
|
||||||
|
|
||||||
|
cy.wait(1000)
|
||||||
|
|
||||||
|
cy.get('input[type="email"]').type('test@example.com')
|
||||||
|
cy.get('input[type="password"]').type('password')
|
||||||
|
cy.get('button[type="submit"]').click()
|
||||||
|
|
||||||
|
cy.wait(1000)
|
||||||
|
|
||||||
|
cy.contains("Praxe").click()
|
||||||
|
cy.url().should('include', '/dashboard/admin/internships')
|
||||||
|
cy.wait(2000)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should be able to generate and download the default proof', () => {
|
||||||
|
cy.get('table').within(() => {
|
||||||
|
cy.get('tbody tr')
|
||||||
|
.then(rows => {
|
||||||
|
const count = rows.length
|
||||||
|
const randomIndex = Math.floor(Math.random() * count)
|
||||||
|
cy.wrap(rows[randomIndex]).as('randomRow')
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.get('@randomRow').within(() => {
|
||||||
|
cy.get('td').get('.internship-edit-btn').click()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.url().should('include', '/dashboard/admin/internships/edit/')
|
||||||
|
|
||||||
|
cy.contains('Stiahnuť originálnu zmluvu').click()
|
||||||
|
cy.wait(2000)
|
||||||
|
})
|
||||||
|
})
|
||||||
79
frontend/cypress/e2e/admin/internships/list.cy.ts
Normal file
79
frontend/cypress/e2e/admin/internships/list.cy.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
describe('Admin Internship CRUD', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('/login')
|
||||||
|
|
||||||
|
cy.wait(3000)
|
||||||
|
|
||||||
|
cy.get('input[type="email"]').type('test@example.com')
|
||||||
|
cy.get('input[type="password"]').type('password')
|
||||||
|
cy.get('button[type="submit"]').click()
|
||||||
|
|
||||||
|
cy.wait(1000)
|
||||||
|
|
||||||
|
cy.contains("Praxe").click()
|
||||||
|
cy.url().should('include', '/dashboard/admin/internships')
|
||||||
|
cy.wait(2000)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should load the list of internships in a proper format', () => {
|
||||||
|
cy.get('table').within(() => {
|
||||||
|
cy.validateColumn('Firma', (company) => {
|
||||||
|
expect(company).to.not.be.empty
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.validateColumn('Študent', (student) => {
|
||||||
|
const nameParts = student.trim().split(' ')
|
||||||
|
expect(nameParts).to.have.length.at.least(2)
|
||||||
|
expect(nameParts[0]).to.not.be.empty
|
||||||
|
expect(nameParts[1]).to.not.be.empty
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.validateColumn('Od', (from) => {
|
||||||
|
const datePattern = /^\d{2}\.\d{2}\.\d{4}$/
|
||||||
|
expect(from.trim()).to.match(datePattern)
|
||||||
|
const [day, month, year] = from.trim().split('.').map(Number)
|
||||||
|
const date = new Date(year, month - 1, day)
|
||||||
|
expect(date.getDate()).to.equal(day)
|
||||||
|
expect(date.getMonth()).to.equal(month - 1)
|
||||||
|
expect(date.getFullYear()).to.equal(year)
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.validateColumn('Do', (to) => {
|
||||||
|
const datePattern = /^\d{2}\.\d{2}\.\d{4}$/
|
||||||
|
expect(to.trim()).to.match(datePattern)
|
||||||
|
const [day, month, year] = to.trim().split('.').map(Number)
|
||||||
|
const date = new Date(year, month - 1, day)
|
||||||
|
expect(date.getDate()).to.equal(day)
|
||||||
|
expect(date.getMonth()).to.equal(month - 1)
|
||||||
|
expect(date.getFullYear()).to.equal(year)
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.validateColumn('Ročník', (grade) => {
|
||||||
|
const gradeNum = Number(grade.trim())
|
||||||
|
expect(gradeNum).to.be.a('number')
|
||||||
|
expect(gradeNum).to.be.at.least(1)
|
||||||
|
expect(gradeNum).to.be.at.most(5)
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.validateColumn('Semester', (semester) => {
|
||||||
|
expect(semester.trim()).to.be.oneOf(['Zimný', 'Letný'])
|
||||||
|
})
|
||||||
|
|
||||||
|
// stav netestujeme
|
||||||
|
|
||||||
|
cy.contains('th', 'Operácie').parent('tr')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should be able to delete an internship', () => {
|
||||||
|
cy.get('table tbody tr').first().within(() => {
|
||||||
|
cy.get('.internship-delete-btn').click()
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.contains("Potvrdiť vymazanie").parent().should('be.visible')
|
||||||
|
cy.contains("Potvrdiť vymazanie").parent().contains("Áno").click()
|
||||||
|
cy.contains("Potvrdiť vymazanie").should('not.exist')
|
||||||
|
|
||||||
|
cy.wait(1000)
|
||||||
|
})
|
||||||
|
})
|
||||||
149
frontend/cypress/e2e/admin/students/list.cy.ts
Normal file
149
frontend/cypress/e2e/admin/students/list.cy.ts
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
describe('Admin Student CRUD', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('/login')
|
||||||
|
|
||||||
|
cy.wait(3000)
|
||||||
|
|
||||||
|
cy.get('input[type="email"]').type('test@example.com')
|
||||||
|
cy.get('input[type="password"]').type('password')
|
||||||
|
cy.get('button[type="submit"]').click()
|
||||||
|
|
||||||
|
cy.wait(1000)
|
||||||
|
|
||||||
|
cy.contains("Študenti").click()
|
||||||
|
cy.url().should('include', '/dashboard/admin/students')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should load the list of students in a proper format', () => {
|
||||||
|
cy.get('table').within(() => {
|
||||||
|
cy.validateColumn('Meno', (name) => {
|
||||||
|
const nameParts = name.trim().split(' ')
|
||||||
|
expect(nameParts).to.have.length.at.least(2)
|
||||||
|
expect(nameParts[0]).to.not.be.empty
|
||||||
|
expect(nameParts[1]).to.not.be.empty
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.validateColumn('E-mail', (email) => {
|
||||||
|
expect(email).to.include("@student.ukf.sk")
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.validateColumn('Telefón', (phone) => {
|
||||||
|
expect(phone.trim()).to.not.be.empty
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.validateColumn('Študijný program', (program) => {
|
||||||
|
expect(program.trim()).to.not.be.empty
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.validateColumn('Osobný e-mail', (personalEmail) => {
|
||||||
|
expect(personalEmail.trim()).to.not.be.empty
|
||||||
|
expect(personalEmail.trim()).to.include("@")
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.validateColumn('Adresa', (address) => {
|
||||||
|
expect(address.trim()).to.not.be.empty
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should be able to delete a student', () => {
|
||||||
|
let initialRowCount = 0
|
||||||
|
|
||||||
|
cy.get('table tbody tr').its('length').then((count) => {
|
||||||
|
initialRowCount = count
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.get('table tbody tr').first().within(() => {
|
||||||
|
cy.contains('Vymazať').click()
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.contains("Potvrdiť vymazanie").parent().should('be.visible')
|
||||||
|
cy.contains("Potvrdiť vymazanie").parent().contains("Vymazať").click()
|
||||||
|
cy.contains("Potvrdiť vymazanie").should('not.exist')
|
||||||
|
|
||||||
|
cy.wait(1000)
|
||||||
|
|
||||||
|
cy.get('table tbody tr').its('length').then((count) => {
|
||||||
|
expect(count).to.be.eq(initialRowCount - 1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should be able to edit a student', () => {
|
||||||
|
// Náhodné sety
|
||||||
|
const firstNames = ['Ján', 'Peter', 'Martin', 'Tomáš', 'Michal', 'Anna', 'Mária', 'Eva', 'Katarína', 'Lucia', 'Adam', 'Cypress']
|
||||||
|
const lastNames = ['Novák', 'Kováč', 'Horváth', 'Varga', 'Molnár', 'Tóth', 'Nagy', 'Lukáč', 'Szabó', 'Kiss', 'Jablko', 'Tester']
|
||||||
|
|
||||||
|
// Výber náhodného študenta
|
||||||
|
cy.get('table tbody tr').then($rows => {
|
||||||
|
const randomIndex = Math.floor(Math.random() * $rows.length)
|
||||||
|
const randomRow = $rows.eq(randomIndex)
|
||||||
|
|
||||||
|
cy.wrap(randomIndex).as('selectedIndex')
|
||||||
|
cy.wrap(randomRow).as('selectedRow')
|
||||||
|
})
|
||||||
|
|
||||||
|
// Kliknutie na "Editovať"
|
||||||
|
cy.get('@selectedRow').within(() => {
|
||||||
|
cy.contains('Editovať').click()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Generovanie náhodného mena
|
||||||
|
|
||||||
|
const randomFirstName = firstNames[Math.floor(Math.random() * firstNames.length)]
|
||||||
|
const randomLastName = lastNames[Math.floor(Math.random() * lastNames.length)]
|
||||||
|
const randomStudentEmail = `cypress.test.${Date.now()}@student.ukf.sk`
|
||||||
|
const randomPhone = `+421${Math.floor(Math.random() * 900000000 + 100000000)}`
|
||||||
|
const randomPersonalEmail = `cypress.test.${Date.now()}@gmail.com`
|
||||||
|
const randomHouseNumber = Math.floor(Math.random() * 200 + 1)
|
||||||
|
const randomAddress = `Hlavná ${randomHouseNumber}/1, Komárno, 946 01`
|
||||||
|
|
||||||
|
// Kontrola cesty
|
||||||
|
cy.url().should('include', '/dashboard/admin/students/edit/')
|
||||||
|
|
||||||
|
// Zmena mena
|
||||||
|
cy.get('#input-v-1-1').clear().type(randomFirstName) // meno
|
||||||
|
cy.get('#input-v-1-4').clear().type(randomLastName) // priezvisko
|
||||||
|
|
||||||
|
// Zmena e-mailu
|
||||||
|
cy.get('#input-v-1-7').clear().type(randomStudentEmail)
|
||||||
|
|
||||||
|
// Zmena telefónu
|
||||||
|
cy.get('#input-v-1-10').clear().type(randomPhone)
|
||||||
|
|
||||||
|
// Zmena študijného programu
|
||||||
|
cy.get('#input-v-1-13').clear().type('aplikovaná informatika')
|
||||||
|
|
||||||
|
// Zmena osobného e-mailu
|
||||||
|
cy.get('#input-v-1-16').clear().type(randomPersonalEmail)
|
||||||
|
|
||||||
|
// Zmena adresy
|
||||||
|
cy.get('#input-v-1-19').clear().type(randomAddress)
|
||||||
|
|
||||||
|
// Uložiť zmeny
|
||||||
|
cy.contains('Uložiť zmeny').click()
|
||||||
|
|
||||||
|
// Počkanie na uloženie
|
||||||
|
cy.wait(2000)
|
||||||
|
cy.url().should('include', '/dashboard/admin/students')
|
||||||
|
|
||||||
|
// Overenie zmien v tabuľke
|
||||||
|
cy.get('@selectedIndex').then((index) => {
|
||||||
|
cy.get('table tbody tr').eq(index as number).within(() => {
|
||||||
|
const expectedValues = [
|
||||||
|
`${randomFirstName} ${randomLastName}`,
|
||||||
|
randomStudentEmail,
|
||||||
|
randomPhone,
|
||||||
|
'aplikovaná informatika',
|
||||||
|
randomPersonalEmail,
|
||||||
|
randomAddress
|
||||||
|
]
|
||||||
|
|
||||||
|
cy.get('td').then($cells => {
|
||||||
|
expectedValues.forEach((expectedValue, i) => {
|
||||||
|
expect($cells.eq(i).text().trim()).to.equal(expectedValue)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
17
frontend/cypress/e2e/home.cy.ts
Normal file
17
frontend/cypress/e2e/home.cy.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
describe('Home Page', () => {
|
||||||
|
it('should display the home page', () => {
|
||||||
|
cy.visit('/')
|
||||||
|
|
||||||
|
cy.wait(1000)
|
||||||
|
|
||||||
|
cy.contains("Domov")
|
||||||
|
cy.contains("Register")
|
||||||
|
cy.contains("Login")
|
||||||
|
|
||||||
|
cy.contains("Informácie o odbornej praxi pre študentov")
|
||||||
|
cy.contains("Informácie o odbornej praxi pre firmy")
|
||||||
|
cy.contains("O aplikácii")
|
||||||
|
|
||||||
|
cy.contains("(c) Fakulta prírodných vied a informatiky, UKF v Nitre")
|
||||||
|
})
|
||||||
|
})
|
||||||
21
frontend/cypress/e2e/info-pages.cy.ts
Normal file
21
frontend/cypress/e2e/info-pages.cy.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
describe('Info Pages', () => {
|
||||||
|
it('should load the info page for students', () => {
|
||||||
|
cy.visit('/')
|
||||||
|
cy.contains("Informácie o odbornej praxi pre študentov").click()
|
||||||
|
cy.location('pathname').should('eq', '/info/student')
|
||||||
|
|
||||||
|
cy.contains("Informácie o odbornej praxi pre študentov")
|
||||||
|
cy.contains("Podmienky absolvovania predmetu")
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should load the info page for companies', () => {
|
||||||
|
cy.visit('/')
|
||||||
|
cy.contains("Informácie o odbornej praxi pre firmy").click()
|
||||||
|
cy.location('pathname').should('eq', '/info/company')
|
||||||
|
|
||||||
|
cy.contains("Detaily a pravidlá odbornej praxe pre firmy")
|
||||||
|
cy.contains("Zmluvné podmienky")
|
||||||
|
cy.contains("Pravidlá a povinnost počas praxe")
|
||||||
|
cy.contains("Hodnotenie a ukončenie praxe")
|
||||||
|
})
|
||||||
|
})
|
||||||
24
frontend/cypress/e2e/login.cy.ts
Normal file
24
frontend/cypress/e2e/login.cy.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
describe('Log in as admin', () => {
|
||||||
|
it('should be able to log in as the default administrator', () => {
|
||||||
|
cy.visit('/login')
|
||||||
|
|
||||||
|
cy.wait(1500)
|
||||||
|
|
||||||
|
cy.contains("Prihlásenie")
|
||||||
|
cy.get('input[type="email"]').type('test@example.com')
|
||||||
|
cy.get('input[type="password"]').type('password')
|
||||||
|
cy.get('button[type="submit"]').click()
|
||||||
|
|
||||||
|
cy.wait(1000)
|
||||||
|
|
||||||
|
cy.contains("Vitajte")
|
||||||
|
cy.location('pathname').should('eq', '/dashboard/admin')
|
||||||
|
|
||||||
|
cy.wait(1000)
|
||||||
|
|
||||||
|
cy.contains("Logout").click()
|
||||||
|
cy.location('pathname').should('eq', '/')
|
||||||
|
|
||||||
|
cy.wait(1000)
|
||||||
|
})
|
||||||
|
})
|
||||||
39
frontend/cypress/support/commands.ts
Normal file
39
frontend/cypress/support/commands.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
// ***********************************************
|
||||||
|
// This example commands.ts shows you how to
|
||||||
|
// create various custom commands and overwrite
|
||||||
|
// existing commands.
|
||||||
|
//
|
||||||
|
// For more comprehensive examples of custom
|
||||||
|
// commands please read more here:
|
||||||
|
// https://on.cypress.io/custom-commands
|
||||||
|
// ***********************************************
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This is a parent command --
|
||||||
|
// Cypress.Commands.add('login', (email, password) => { ... })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This is a child command --
|
||||||
|
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This is a dual command --
|
||||||
|
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This will overwrite an existing command --
|
||||||
|
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
||||||
|
|
||||||
|
// Custom command to validate table columns
|
||||||
|
Cypress.Commands.add('validateColumn', (columnName: string, validator: (value: string) => void) => {
|
||||||
|
cy.contains('th', columnName).parent('tr').then(($headerRow) => {
|
||||||
|
const colIndex = $headerRow.find('th').index(cy.$$(`th:contains("${columnName}")`)[0])
|
||||||
|
|
||||||
|
cy.get('tbody tr').each(($row) => {
|
||||||
|
cy.wrap($row).find('td').eq(colIndex).invoke('text').then((cellText) => {
|
||||||
|
validator(cellText as string)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
17
frontend/cypress/support/e2e.ts
Normal file
17
frontend/cypress/support/e2e.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// ***********************************************************
|
||||||
|
// This example support/e2e.ts is processed and
|
||||||
|
// loaded automatically before your test files.
|
||||||
|
//
|
||||||
|
// This is a great place to put global configuration and
|
||||||
|
// behavior that modifies Cypress.
|
||||||
|
//
|
||||||
|
// You can change the location of this file or turn off
|
||||||
|
// automatically serving support files with the
|
||||||
|
// 'supportFile' configuration option.
|
||||||
|
//
|
||||||
|
// You can read more here:
|
||||||
|
// https://on.cypress.io/configuration
|
||||||
|
// ***********************************************************
|
||||||
|
|
||||||
|
// Import commands.js using ES2015 syntax:
|
||||||
|
import './commands'
|
||||||
13
frontend/cypress/support/index.d.ts
vendored
Normal file
13
frontend/cypress/support/index.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
declare namespace Cypress {
|
||||||
|
interface Chainable {
|
||||||
|
/**
|
||||||
|
* Custom command to validate all cells in a table column
|
||||||
|
* @param columnName - The name of the column header to validate
|
||||||
|
* @param validator - A function that receives the cell text and performs assertions
|
||||||
|
* @example cy.validateColumn('Email', (email) => { expect(email).to.include('@') })
|
||||||
|
*/
|
||||||
|
validateColumn(columnName: string, validator: (value: string) => void): Chainable<void>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,5 +39,10 @@ export default defineNuxtConfig({
|
|||||||
origin: 'http://localhost:8080', // NUXT_PUBLIC_SANCTUM_ORIGIN
|
origin: 'http://localhost:8080', // NUXT_PUBLIC_SANCTUM_ORIGIN
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
typescript: {
|
||||||
|
strict: true,
|
||||||
|
typeCheck: true,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
2231
frontend/package-lock.json
generated
2231
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user