$student, 'documentTypes' => DocumentType::where('is_active', true)->orderBy('name')->get(), 'verificationStatuses' => $this->verificationStatuses(), 'defaultMaxFileSizeKb' => 5120, ]); } public function store(Request $request, Student $student): RedirectResponse { $data = $this->validateDocument($request); $uploadedFile = $request->file('document_file'); $path = $uploadedFile->store("student-documents/{$student->id}", 'public'); StudentDocument::create([ 'student_id' => $student->id, 'document_type_id' => $data['document_type_id'], 'file_name' => $uploadedFile->getClientOriginalName(), 'file_path' => $path, 'file_extension' => $uploadedFile->getClientOriginalExtension(), 'mime_type' => $uploadedFile->getMimeType(), 'file_size' => $uploadedFile->getSize(), 'document_number' => $data['document_number'] ?? null, 'issued_date' => $data['issued_date'] ?? null, 'expired_date' => $data['expired_date'] ?? null, 'description' => $data['description'] ?? null, 'verification_status' => $data['verification_status'], 'verification_note' => $data['verification_note'] ?? null, 'uploaded_by' => auth()->id(), 'verified_by' => in_array($data['verification_status'], ['valid', 'invalid', 'revision']) ? auth()->id() : null, 'verified_at' => in_array($data['verification_status'], ['valid', 'invalid', 'revision']) ? now() : null, ]); Alert::success('Berhasil', 'Dokumen siswa berhasil diunggah.'); return redirect()->route('students.show', $student)->with('success', 'Dokumen siswa berhasil diunggah.'); } public function edit(Student $student, StudentDocument $document): View { abort_unless($document->student_id === $student->id, 404); return view('student-documents.edit', [ 'student' => $student, 'document' => $document, 'documentTypes' => DocumentType::where('is_active', true)->orderBy('name')->get(), 'verificationStatuses' => $this->verificationStatuses(), 'defaultMaxFileSizeKb' => 5120, ]); } public function update(Request $request, Student $student, StudentDocument $document): RedirectResponse { abort_unless($document->student_id === $student->id, 404); $data = $this->validateDocument($request, false); if ($request->hasFile('document_file')) { if ($document->file_path && Storage::disk('public')->exists($document->file_path)) { Storage::disk('public')->delete($document->file_path); } $uploadedFile = $request->file('document_file'); $path = $uploadedFile->store("student-documents/{$student->id}", 'public'); $document->file_name = $uploadedFile->getClientOriginalName(); $document->file_path = $path; $document->file_extension = $uploadedFile->getClientOriginalExtension(); $document->mime_type = $uploadedFile->getMimeType(); $document->file_size = $uploadedFile->getSize(); } $document->fill([ 'document_type_id' => $data['document_type_id'], 'document_number' => $data['document_number'] ?? null, 'issued_date' => $data['issued_date'] ?? null, 'expired_date' => $data['expired_date'] ?? null, 'description' => $data['description'] ?? null, 'verification_status' => $data['verification_status'], 'verification_note' => $data['verification_note'] ?? null, ]); if (in_array($data['verification_status'], ['valid', 'invalid', 'revision'])) { $document->verified_by = auth()->id(); $document->verified_at = now(); } else { $document->verified_by = null; $document->verified_at = null; } $document->save(); Alert::success('Berhasil', 'Dokumen siswa berhasil diperbarui.'); return redirect()->route('students.show', $student)->with('success', 'Dokumen siswa berhasil diperbarui.'); } public function destroy(Student $student, StudentDocument $document): RedirectResponse { $this->ensureAdmin(); abort_unless($document->student_id === $student->id, 404); if ($document->file_path && Storage::disk('public')->exists($document->file_path)) { Storage::disk('public')->delete($document->file_path); } $document->delete(); Alert::success('Berhasil', 'Dokumen siswa berhasil dihapus.'); return redirect()->route('students.show', $student)->with('success', 'Dokumen siswa berhasil dihapus.'); } public function preview(StudentDocument $document): Response|View { abort_unless($document->file_path && Storage::disk('public')->exists($document->file_path), 404); $mimeType = $document->mime_type ?: Storage::disk('public')->mimeType($document->file_path); $url = asset('storage/' . $document->file_path); $isImage = str_starts_with((string) $mimeType, 'image/'); $isPdf = $mimeType === 'application/pdf'; return view('student-documents.preview', [ 'document' => $document, 'fileUrl' => $url, 'mimeType' => $mimeType, 'isImage' => $isImage, 'isPdf' => $isPdf, ]); } public function quickVerify(Request $request, Student $student, StudentDocument $document): RedirectResponse { $this->ensureAdmin(); abort_unless($document->student_id === $student->id, 404); $data = $request->validate([ 'verification_status' => ['required', Rule::in(['valid', 'revision', 'invalid'])], 'verification_note' => ['nullable', 'string', 'max:1000'], ]); $document->update([ 'verification_status' => $data['verification_status'], 'verification_note' => $data['verification_note'] ?? null, 'verified_by' => auth()->id(), 'verified_at' => now(), ]); Alert::success('Berhasil', 'Status verifikasi dokumen berhasil diperbarui.'); return redirect()->route('students.show', $student)->with('success', 'Status verifikasi dokumen berhasil diperbarui.'); } public function download(StudentDocument $document): StreamedResponse { abort_unless($document->file_path && Storage::disk('public')->exists($document->file_path), 404); return Storage::disk('public')->download($document->file_path, $document->file_name); } public function publicPreview(StudentDocument $document): Response|View { abort_unless($document->file_path && Storage::disk('public')->exists($document->file_path), 404); $mimeType = $document->mime_type ?: Storage::disk('public')->mimeType($document->file_path); $url = asset('storage/' . $document->file_path); $isImage = str_starts_with((string) $mimeType, 'image/'); $isPdf = $mimeType === 'application/pdf'; return view('public.document-preview', [ 'document' => $document, 'fileUrl' => $url, 'mimeType' => $mimeType, 'isImage' => $isImage, 'isPdf' => $isPdf, ]); } public function publicDownload(StudentDocument $document): StreamedResponse { return $this->download($document); } protected function validateDocument(Request $request, bool $fileRequired = true): array { $documentType = DocumentType::findOrFail($request->input('document_type_id')); $maxFileSizeKb = $documentType->max_file_size_kb ?: 5120; $allowedExtensions = $documentType->allowed_extensions ? collect(explode(',', $documentType->allowed_extensions))->map(fn ($item) => trim($item))->filter()->implode(',') : 'jpg,jpeg,png,pdf,webp'; return $request->validate([ 'document_type_id' => ['required', 'exists:document_types,id'], 'document_number' => ['nullable', 'string', 'max:100'], 'issued_date' => ['nullable', 'date'], 'expired_date' => ['nullable', 'date', 'after_or_equal:issued_date'], 'description' => ['nullable', 'string'], 'verification_status' => ['required', Rule::in(array_keys($this->verificationStatuses()))], 'verification_note' => ['nullable', 'string', 'max:1000'], 'document_file' => [$fileRequired ? 'required' : 'nullable', 'file', 'mimes:' . $allowedExtensions, 'max:' . $maxFileSizeKb], ], [ 'document_file.max' => 'Ukuran file melebihi batas maksimal untuk jenis dokumen ini (' . number_format($maxFileSizeKb / 1024, 2) . ' MB).', 'document_file.mimes' => 'Format file tidak sesuai. Format yang diizinkan untuk jenis dokumen ini: ' . strtoupper(str_replace(',', ', ', $allowedExtensions)) . '.', ]); } protected function verificationStatuses(): array { return [ 'pending' => 'Pending', 'valid' => 'Valid', 'invalid' => 'Invalid', 'revision' => 'Perlu Revisi', ]; } protected function ensureAdmin(): void { abort_unless(auth()->user()?->isAdmin(), 403, 'Aksi ini hanya boleh dilakukan administrator.'); } }