You've already forked isop-mirror
Merge branch 'develop' into feature/docker
This commit is contained in:
5
frontend/.gitignore
vendored
5
frontend/.gitignore
vendored
@@ -22,3 +22,8 @@ logs
|
||||
.env
|
||||
.env.*
|
||||
!.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">
|
||||
<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" />
|
||||
<InfoCard title="Denník praxe"
|
||||
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 {
|
||||
await login(form.value);
|
||||
} catch (e) {
|
||||
if (e instanceof FetchError && e.response?.status === 422) {
|
||||
if (e instanceof FetchError) {
|
||||
error.value = e.response?._data.message;
|
||||
}
|
||||
} finally {
|
||||
@@ -50,16 +50,14 @@ async function handleLogin() {
|
||||
<h2 class="page-title">Prihlásenie</h2>
|
||||
|
||||
<!-- Chybová hláška -->
|
||||
<v-alert v-if="error !== null" density="compact" :text="error" title="Chyba" type="error"
|
||||
id="login-error-alert" class="mx-auto alert"></v-alert>
|
||||
<ErrorAlert v-if="error" :error="error" />
|
||||
|
||||
<!-- Čakajúca hláška -->
|
||||
<v-alert v-if="loading" density="compact" text="Prosím čakajte..." title="Spracovávam" type="info"
|
||||
id="login-error-alert" class="mx-auto alert"></v-alert>
|
||||
<LoadingAlert v-if="loading" />
|
||||
|
||||
<v-form v-else v-model="isValid" @submit.prevent="handleLogin">
|
||||
<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]"
|
||||
:type="showPassword ? 'text' : 'password'" label="Heslo:" variant="outlined" density="comfortable"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { NewRole } from '~/types/role';
|
||||
import type { NewUser } from '~/types/user';
|
||||
import { FetchError } from 'ofetch';
|
||||
|
||||
definePageMeta({
|
||||
middleware: ['sanctum:guest'],
|
||||
@@ -18,7 +19,8 @@ useSeoMeta({
|
||||
const rules = {
|
||||
required: (v: any) => (!!v && String(v).trim().length > 0) || 'Povinné pole',
|
||||
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ť',
|
||||
};
|
||||
|
||||
@@ -63,8 +65,10 @@ async function handleRegistration() {
|
||||
});
|
||||
|
||||
navigateTo("/");
|
||||
} catch (e: any) {
|
||||
error.value = e.data?.message as string;
|
||||
} catch (e) {
|
||||
if (e instanceof FetchError) {
|
||||
error.value = e.response?._data.message;
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -77,12 +81,10 @@ async function handleRegistration() {
|
||||
<h4 class="page-title">Registrácia firmy</h4>
|
||||
|
||||
<!-- Chybová hláška -->
|
||||
<v-alert v-if="error !== null" density="compact" :text="error" title="Chyba" type="error"
|
||||
id="login-error-alert" class="mx-auto"></v-alert>
|
||||
<ErrorAlert v-if=error :error="error" />
|
||||
|
||||
<!-- Čakajúca hláška -->
|
||||
<v-alert v-if="loading" density="compact" text="Prosím čakajte..." title="Spracovávam" type="info"
|
||||
id="login-error-alert" class="mx-auto"></v-alert>
|
||||
<LoadingAlert v-if="loading" />
|
||||
|
||||
<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"
|
||||
@@ -100,8 +102,8 @@ async function handleRegistration() {
|
||||
density="comfortable" />
|
||||
<v-text-field v-model="form.email" :rules="[rules.required, rules.email]" label="Email:"
|
||||
variant="outlined" density="comfortable" />
|
||||
<v-text-field v-model="form.phone" :rules="[rules.phone]" label="Telefón:" variant="outlined"
|
||||
density="comfortable" />
|
||||
<v-text-field v-model="form.phone" :rules="[rules.phone]" label="Telefón (s predvoľbou):"
|
||||
variant="outlined" density="comfortable" />
|
||||
|
||||
<v-checkbox v-model="form.consent" :rules="[rules.mustAgree]"
|
||||
label="Súhlasím s podmienkami spracúvania osobných údajov" density="comfortable" />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { NewRole } from '~/types/role';
|
||||
import type { NewUser } from '~/types/user';
|
||||
import { FetchError } from 'ofetch';
|
||||
|
||||
definePageMeta({
|
||||
middleware: ['sanctum:guest'],
|
||||
@@ -22,11 +23,14 @@ const rules = {
|
||||
personal_email: (v: string) =>
|
||||
/.+@.+\..+/.test(v) || 'Zadajte platný osobný email',
|
||||
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ť',
|
||||
};
|
||||
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);
|
||||
@@ -37,10 +41,11 @@ const form = ref({
|
||||
studentEmail: '',
|
||||
personalEmail: '',
|
||||
phone: '',
|
||||
studyProgram: programs[0] as string,
|
||||
studyProgram: programs[0]!.value,
|
||||
year_of_study: 1,
|
||||
consent: false,
|
||||
});
|
||||
const maxYearOfStudy = ref(0);
|
||||
|
||||
const loading = ref(false);
|
||||
const error = ref(null as null | string);
|
||||
@@ -71,12 +76,18 @@ async function handleRegistration() {
|
||||
});
|
||||
|
||||
navigateTo("/");
|
||||
} catch (e: any) {
|
||||
error.value = e.data?.message as string;
|
||||
} catch (e) {
|
||||
if (e instanceof FetchError) {
|
||||
error.value = e.response?._data.message;
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(form, (newForm) => {
|
||||
maxYearOfStudy.value = newForm.studyProgram.slice(-1) === 'b' ? 3 : 2;
|
||||
}, { deep: true, immediate: true });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -85,12 +96,10 @@ async function handleRegistration() {
|
||||
<h4 class="page-title">Registrácia študenta</h4>
|
||||
|
||||
<!-- Chybová hláška -->
|
||||
<v-alert v-if="error !== null" density="compact" :text="error" title="Chyba" type="error"
|
||||
id="login-error-alert" class="mx-auto alert"></v-alert>
|
||||
<ErrorAlert v-if="error" :error="error" />
|
||||
|
||||
<!-- Čakajúca hláška -->
|
||||
<v-alert v-if="loading" density="compact" text="Prosím čakajte..." title="Spracovávam" type="info"
|
||||
id="login-error-alert" class="mx-auto alert"></v-alert>
|
||||
<LoadingAlert v-if="loading" />
|
||||
|
||||
<v-form v-else v-model="isValid" @submit.prevent="handleRegistration">
|
||||
<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]"
|
||||
label="Alternatívny email:" variant="outlined" density="comfortable" />
|
||||
|
||||
<v-text-field v-model="form.phone" :rules="[rules.required, rules.phone]" label="Telefón:"
|
||||
variant="outlined" density="comfortable" />
|
||||
<v-text-field v-model="form.phone" :rules="[rules.required, rules.phone]"
|
||||
label="Telefón (s predvoľbou):" variant="outlined" density="comfortable" />
|
||||
|
||||
<v-select v-model="form.studyProgram" :items="programs" :rules="[rules.required]"
|
||||
label="Študijný odbor:" variant="outlined" density="comfortable" />
|
||||
|
||||
<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]"
|
||||
label="Súhlasím s podmienkami spracúvania osobných údajov" density="comfortable" />
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { FetchError } from 'ofetch';
|
||||
|
||||
const client = useSanctumClient();
|
||||
|
||||
definePageMeta({
|
||||
@@ -36,8 +38,10 @@ async function handleReset() {
|
||||
});
|
||||
|
||||
navigateTo("/reset_psw/request_sent");
|
||||
} catch (e: any) {
|
||||
error.value = e.data?.message as string;
|
||||
} catch (e) {
|
||||
if (e instanceof FetchError) {
|
||||
error.value = e.response?._data.message;
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -50,12 +54,10 @@ async function handleReset() {
|
||||
<h2 class="page-title">Reset hesla</h2>
|
||||
|
||||
<!-- Chybová hláška -->
|
||||
<v-alert v-if="error !== null" density="compact" :text="error" title="Chyba" type="error"
|
||||
id="login-error-alert" class="alert mx-auto"></v-alert>
|
||||
<ErrorAlert v-if="error" :error="error" />
|
||||
|
||||
<!-- Čakajúca hláška -->
|
||||
<v-alert v-if="loading" density="compact" text="Prosím čakajte..." title="Spracovávam" type="info"
|
||||
id="login-error-alert" class="alert mx-auto"></v-alert>
|
||||
<LoadingAlert v-if="loading" />
|
||||
|
||||
<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"
|
||||
|
||||
@@ -16,8 +16,7 @@ useSeoMeta({
|
||||
<v-card id="page-container-card">
|
||||
<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"
|
||||
class="mx-auto"></v-alert>
|
||||
<SuccessAlert title="Reset hesla" text="Nové heslo vám bolo zaslané na e-mail" />
|
||||
</v-card>
|
||||
</v-container>
|
||||
</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 {
|
||||
id: number;
|
||||
name: string;
|
||||
address: string;
|
||||
ico: number;
|
||||
contact: number;
|
||||
contact: User;
|
||||
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
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
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
@@ -16,5 +16,10 @@
|
||||
"vue": "^3.5.22",
|
||||
"vue-router": "^4.5.1",
|
||||
"vuetify-nuxt-module": "^0.18.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cypress": "^15.6.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vue-tsc": "^3.1.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,5 +14,9 @@
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user