You've already forked isop-mirror
feat: implement document uploading for students
This commit is contained in:
@@ -46,6 +46,11 @@ class InternshipController extends Controller
|
|||||||
$internship->end = Carbon::parse($internship->end)->format('d.m.Y');
|
$internship->end = Carbon::parse($internship->end)->format('d.m.Y');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$internships->each(function ($internship) {
|
||||||
|
$internship->agreement = $internship->agreement !== null;
|
||||||
|
$internship->report = $internship->report !== null;
|
||||||
|
});
|
||||||
|
|
||||||
return response()->json($internships);
|
return response()->json($internships);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,6 +78,11 @@ class InternshipController extends Controller
|
|||||||
$internship->end = Carbon::parse($internship->end)->format('d.m.Y');
|
$internship->end = Carbon::parse($internship->end)->format('d.m.Y');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$internships->each(function ($internship) {
|
||||||
|
$internship->agreement = $internship->agreement !== null;
|
||||||
|
$internship->report = $internship->report !== null;
|
||||||
|
});
|
||||||
|
|
||||||
return response()->json($internships);
|
return response()->json($internships);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,6 +110,9 @@ class InternshipController extends Controller
|
|||||||
$internship->status = InternshipStatus::whereColumn('internship_id', '=', $internship->id)->orderByDesc('changed')->get()->first()->makeHidden(['created_at', 'updated_at', 'id']);
|
$internship->status = InternshipStatus::whereColumn('internship_id', '=', $internship->id)->orderByDesc('changed')->get()->first()->makeHidden(['created_at', 'updated_at', 'id']);
|
||||||
$internship->status->modified_by = User::find($internship->status->modified_by)->makeHidden(['created_at', 'updated_at', 'email_verified_at']);
|
$internship->status->modified_by = User::find($internship->status->modified_by)->makeHidden(['created_at', 'updated_at', 'email_verified_at']);
|
||||||
|
|
||||||
|
$internship->agreement = $internship->agreement !== null;
|
||||||
|
$internship->report = $internship->report !== null;
|
||||||
|
|
||||||
return response()->json($internship);
|
return response()->json($internship);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,6 +202,43 @@ class InternshipController extends Controller
|
|||||||
return response()->noContent();
|
return response()->noContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function update_documents(int $id, Request $request) {
|
||||||
|
$user = auth()->user();
|
||||||
|
$internship = Internship::find($id);
|
||||||
|
|
||||||
|
if(!$internship) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'No such internship exists.'
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user->role !== 'ADMIN' && $internship->user_id !== $user->id && $user->id !== $internship->contact) {
|
||||||
|
abort(403, 'Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
$request->validate([
|
||||||
|
'agreement' => ['nullable', 'file', 'mimes:pdf', 'max:10240'],
|
||||||
|
'report' => ['nullable', 'file', 'mimes:pdf', 'max:10240']
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!$request->hasFile('agreement') && !$request->hasFile('report')) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'At least one document (agreement or report) must be provided.'
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->hasFile('agreement')) {
|
||||||
|
$internship->agreement = file_get_contents($request->file('agreement')->getRealPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->hasFile('report')) {
|
||||||
|
$internship->report = file_get_contents($request->file('report')->getRealPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
$internship->save();
|
||||||
|
return response()->noContent();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the specified resource in storage.
|
* Update the specified resource in storage.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ Route::prefix('/internships')->group(function () {
|
|||||||
Route::put("/status", [InternshipStatusController::class, 'update'])->name("api.internships.status.update");
|
Route::put("/status", [InternshipStatusController::class, 'update'])->name("api.internships.status.update");
|
||||||
Route::get("/statuses", [InternshipStatusController::class, 'get'])->name("api.internships.get");
|
Route::get("/statuses", [InternshipStatusController::class, 'get'])->name("api.internships.get");
|
||||||
Route::get("/next-statuses", [InternshipStatusController::class, 'get_next_states'])->name("api.internships.status.next.get");
|
Route::get("/next-statuses", [InternshipStatusController::class, 'get_next_states'])->name("api.internships.status.next.get");
|
||||||
|
Route::post("/documents", [InternshipController::class, 'update_documents'])->name("api.internships.documents.set");
|
||||||
Route::post("/basic", [InternshipController::class, 'update_basic'])->name("api.internships.update.basic");
|
Route::post("/basic", [InternshipController::class, 'update_basic'])->name("api.internships.update.basic");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
100
frontend/app/components/InternshipDocumentEditor.vue
Normal file
100
frontend/app/components/InternshipDocumentEditor.vue
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { Internship } from '~/types/internships';
|
||||||
|
import { FetchError } from 'ofetch';
|
||||||
|
|
||||||
|
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 agreement = ref<File | null>(null);
|
||||||
|
const report = ref<File | null>(null);
|
||||||
|
|
||||||
|
const client = useSanctumClient();
|
||||||
|
|
||||||
|
async function onSubmit() {
|
||||||
|
error.value = null;
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
if (agreement.value) {
|
||||||
|
formData.append('agreement', agreement.value);
|
||||||
|
}
|
||||||
|
if (report.value) {
|
||||||
|
formData.append('report', report.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client(`/api/internships/${props.internship.id}/documents`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
agreement.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 -->
|
||||||
|
<v-alert v-if="loading" density="compact" text="Prosím čakajte..." title="Spracovávam" type="info"
|
||||||
|
class="mx-auto mb-2"></v-alert>
|
||||||
|
|
||||||
|
<!-- Chybová hláška -->
|
||||||
|
<v-alert v-if="error" density="compact" :text="error" title="Chyba" type="error" class="mx-auto mb-2"></v-alert>
|
||||||
|
|
||||||
|
<v-form @submit.prevent="onSubmit" :disabled="loading">
|
||||||
|
<div>
|
||||||
|
<h4 class="mb-2">Podpísaná zmluva / dohoda</h4>
|
||||||
|
|
||||||
|
<v-alert v-if="props.internship.agreement" class="mb-2" type="warning" variant="tonal"
|
||||||
|
title="Existujúci dokument"
|
||||||
|
text="V systéme je už nahratá zmluva/dohoda. Ak chcete nahradiť existujúcu verziu, vyberte súbor, alebo v opačnom prípade nechajte toto pole nevyplnené." />
|
||||||
|
|
||||||
|
<v-file-input v-model="agreement" :rules="[rules.isPdf, rules.maxSize]" accept=".pdf,application/pdf"
|
||||||
|
prepend-icon="mdi-handshake" label="Nahrať PDF zmluvu" variant="outlined" show-size clearable
|
||||||
|
hint="Povolené: PDF, max 10 MB" persistent-hint />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 class="mb-2">Výkaz</h4>
|
||||||
|
|
||||||
|
<v-alert v-if="props.internship.report" class="mb-2" type="warning" variant="tonal"
|
||||||
|
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é." />
|
||||||
|
|
||||||
|
<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 />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<v-btn type="submit" color="success" size="large" block :disabled="!agreement && !report">
|
||||||
|
Uloziť
|
||||||
|
</v-btn>
|
||||||
|
</v-form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { User } from '~/types/user';
|
import { InternshipStatus } from '~/types/internship_status';
|
||||||
|
import type { Internship } from '~/types/internships';
|
||||||
type PracticeStatus = 'NOVA' | 'V_KONTROLE' | 'SCHVALENA' | 'ZAMIETNUTA' | 'UKONCENA'
|
|
||||||
|
|
||||||
/* sem pôjde reálny stav praxe */
|
|
||||||
const practiceStatus = ref<PracticeStatus>('SCHVALENA')
|
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: ['sanctum:auth', 'student-only'],
|
middleware: ['sanctum:auth', 'student-only'],
|
||||||
@@ -17,200 +13,29 @@ useSeoMeta({
|
|||||||
ogDescription: "Edit praxe",
|
ogDescription: "Edit praxe",
|
||||||
});
|
});
|
||||||
|
|
||||||
const user = useSanctumUser<User>();
|
const route = useRoute();
|
||||||
|
const { data, refresh } = await useSanctumFetch<Internship>(`/api/internships/${route.params.id}`);
|
||||||
const formRef = ref()
|
|
||||||
|
|
||||||
// súbory
|
|
||||||
const contractFile = ref<File | null>(null)
|
|
||||||
const reportFile = ref<File | null>(null)
|
|
||||||
|
|
||||||
// nastavenie súborov (formát a veľkosť)
|
|
||||||
const allowedMimes = ['application/pdf']
|
|
||||||
const maxSizeBytes = 10 * 1024 * 1024 // 10 MB
|
|
||||||
|
|
||||||
// helpery
|
|
||||||
const isApproved = computed(() => practiceStatus.value === 'SCHVALENA')
|
|
||||||
|
|
||||||
// validácia
|
|
||||||
const rules = {
|
|
||||||
requiredIfApproved: (v: File | null) =>
|
|
||||||
!isApproved.value || !!v || 'Zmluva je povinná pri stave Schválená.',
|
|
||||||
isPdf: (v: File | null) =>
|
|
||||||
!v || allowedMimes.includes(v.type) || 'Povolený je iba PDF súbor.',
|
|
||||||
maxSize: (v: File | null) =>
|
|
||||||
!v || v.size <= maxSizeBytes || 'Maximálna veľkosť súboru je 10 MB.',
|
|
||||||
}
|
|
||||||
|
|
||||||
// UI stav
|
|
||||||
const loading = ref(false)
|
|
||||||
const success = ref(false)
|
|
||||||
const error = ref<string | null>(null)
|
|
||||||
|
|
||||||
function resetForm() {
|
|
||||||
contractFile.value = null
|
|
||||||
reportFile.value = null
|
|
||||||
formRef.value?.resetValidation?.()
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mock submit – sem API */
|
|
||||||
async function submit() {
|
|
||||||
error.value = null
|
|
||||||
success.value = false
|
|
||||||
|
|
||||||
const ok = await formRef.value?.validate?.()
|
|
||||||
if (!ok?.valid) return
|
|
||||||
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
// tvorba payloadu
|
|
||||||
const fd = new FormData()
|
|
||||||
if (contractFile.value) fd.append('contract', contractFile.value)
|
|
||||||
if (reportFile.value) fd.append('report', reportFile.value)
|
|
||||||
|
|
||||||
// sem pôjde endpoint
|
|
||||||
await new Promise((r) => setTimeout(r, 800))
|
|
||||||
|
|
||||||
success.value = true
|
|
||||||
resetForm()
|
|
||||||
} catch (e: any) {
|
|
||||||
error.value = e?.message ?? 'Nahrávanie zlyhalo. Skúste znova.'
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<v-container fluid>
|
<v-container fluid>
|
||||||
<v-card>
|
<v-card id="page-container-card">
|
||||||
<h2 class="page-container page-title">Nahratie dokumentov</h2>
|
<h1 class>Nahratie dokumentov</h1>
|
||||||
<p class="page-container page-lead">
|
|
||||||
Nahrajte podpísanú zmluvu (povinné pri stave <strong>Schválená</strong>) a výkaz (nepovinné).
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="form-left">
|
<v-alert v-if="data?.status.status !== InternshipStatus.CONFIRMED" type="error" variant="tonal"
|
||||||
<v-alert
|
title="Blokované" text='Vaša prax nie je v stave "Schválená" a teda nemôžete nahrať dokumenty.' />
|
||||||
v-if="isApproved"
|
|
||||||
type="info"
|
|
||||||
variant="tonal"
|
|
||||||
class="mb-4"
|
|
||||||
title="Zmluva je povinná"
|
|
||||||
text="Vaša prax je v stave Schválená - nahratie podpísanej zmluvy je vyžadované pred pokračovaním."
|
|
||||||
/>
|
|
||||||
|
|
||||||
<v-chip
|
<br />
|
||||||
class="mb-4"
|
|
||||||
color="primary"
|
|
||||||
variant="flat"
|
|
||||||
:prepend-icon="isApproved ? 'mdi-check-decagram' : 'mdi-progress-clock'"
|
|
||||||
>
|
|
||||||
Stav praxe: {{ practiceStatus }}
|
|
||||||
</v-chip>
|
|
||||||
|
|
||||||
<v-form ref="formRef" @submit.prevent="submit">
|
<InternshipDocumentEditor :internship="data!" @successful-submit="refresh" />
|
||||||
<v-row align="stretch" justify="start">
|
|
||||||
<v-col cols="12" md="6">
|
|
||||||
<h3 class="section-title">Podpísaná zmluva</h3>
|
|
||||||
<v-file-input
|
|
||||||
v-model="contractFile"
|
|
||||||
:rules="[rules.requiredIfApproved, rules.isPdf, rules.maxSize]"
|
|
||||||
accept=".pdf,application/pdf"
|
|
||||||
prepend-icon=""
|
|
||||||
label="Nahrať PDF zmluvu"
|
|
||||||
variant="outlined"
|
|
||||||
show-size
|
|
||||||
clearable
|
|
||||||
:disabled="loading"
|
|
||||||
hint="Povolené: PDF, max 10 MB"
|
|
||||||
persistent-hint
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
|
|
||||||
<v-col cols="12" md="6">
|
|
||||||
<h3 class="section-title">Výkaz (nepovinné)</h3>
|
|
||||||
<v-file-input
|
|
||||||
v-model="reportFile"
|
|
||||||
:rules="[rules.isPdf, rules.maxSize]"
|
|
||||||
accept=".pdf,application/pdf"
|
|
||||||
prepend-icon=""
|
|
||||||
label="Nahrať PDF výkaz"
|
|
||||||
variant="outlined"
|
|
||||||
show-size
|
|
||||||
clearable
|
|
||||||
:disabled="loading"
|
|
||||||
hint="Povolené: PDF, max 10 MB"
|
|
||||||
persistent-hint
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
<div class="actions">
|
|
||||||
<v-btn :loading="loading" color="primary" type="submit">
|
|
||||||
Odoslať
|
|
||||||
</v-btn>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<v-alert
|
|
||||||
v-if="success"
|
|
||||||
type="success"
|
|
||||||
class="mt-4 mb-4"
|
|
||||||
title="Dokumenty nahrané"
|
|
||||||
text="Vaše dokumenty boli úspešne odoslané."
|
|
||||||
/>
|
|
||||||
<v-alert
|
|
||||||
v-if="error"
|
|
||||||
type="error"
|
|
||||||
class="mt-4 mb-4"
|
|
||||||
title="Chyba"
|
|
||||||
:text="error"
|
|
||||||
/>
|
|
||||||
</v-form>
|
|
||||||
</div>
|
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
#page-container-card {
|
||||||
.page-container {
|
padding-left: 10px;
|
||||||
max-width: 1120px;
|
padding-right: 10px;
|
||||||
margin: 0 auto;
|
padding-bottom: 10px;
|
||||||
padding-left: 24px;
|
|
||||||
padding-right: 24px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-left {
|
|
||||||
padding-left: 24px;
|
|
||||||
padding-right: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-title {
|
|
||||||
font-size: 32px;
|
|
||||||
line-height: 1.2;
|
|
||||||
font-weight: 700;
|
|
||||||
margin: 16px 0 8px;
|
|
||||||
color: #1f1f1f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-lead {
|
|
||||||
margin: 0 0 24px 0;
|
|
||||||
color: #6b6b6b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
font-size: 22px;
|
|
||||||
font-weight: 700;
|
|
||||||
margin: 16px 0 12px;
|
|
||||||
color: #1f1f1f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
margin-left: 0;
|
|
||||||
margin-top: 8px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
@@ -12,8 +12,8 @@ export interface Internship {
|
|||||||
year_of_study: number;
|
year_of_study: number;
|
||||||
semester: string;
|
semester: string;
|
||||||
position_description: string;
|
position_description: string;
|
||||||
agreement?: Uint8Array;
|
agreement: boolean;
|
||||||
report?: Uint8Array;
|
report: boolean;
|
||||||
report_confirmed: boolean;
|
report_confirmed: boolean;
|
||||||
status: InternshipStatusData;
|
status: InternshipStatusData;
|
||||||
};
|
};
|
||||||
@@ -26,5 +26,4 @@ export interface NewInternship {
|
|||||||
year_of_study: number;
|
year_of_study: number;
|
||||||
semester: string;
|
semester: string;
|
||||||
position_description: string;
|
position_description: string;
|
||||||
agreement?: Uint8Array;
|
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user