get() ->map(fn ($letter) => $this->formatLetterResponse($letter)); return response()->json([ 'success' => true, 'data' => $letters ]); } catch (Exception $e) { return response()->json([ 'success' => false, 'message' => 'Error fetching letters', 'error' => $e->getMessage() ], 500); } } /** * Store a newly created letter * * Validates input data and handles file upload if present * Creates new letter with UUID as primary key * * @param Request $request Contains letter data and optional file attachment * @return JsonResponse Created letter with its file upload info */ public function store(Request $request): JsonResponse { try { // Validate incoming request data $validated = $request->validate([ 'nama_mahasiswa' => 'required|string', 'nomor_induk_mahasiswa' => 'required|string', 'program_studi_mahasiswa' => 'required|string', 'tahun_ajaran' => 'required|string', 'start_date' => 'required|date', 'completion_date' => 'required|date', 'lampiran' => 'nullable|file|mimes:pdf,jpg,png|max:2048', // Max 2MB ]); // Create letter with UUID and optional fields $letter = Letter::create([ 'id' => Str::uuid()->toString(), ...$validated, 'is_acceptance' => $request->is_acceptance ?? false, 'nomor_surat_acceptance' => $request->nomor_surat_acceptance, 'is_completion' => $request->is_completion ?? false, 'nomor_surat_completion' => $request->nomor_surat_completion, ]); // Handle file upload if present if ($request->hasFile('lampiran')) { $this->handleFileUpload($request->file('lampiran'), $letter); } return response()->json([ 'success' => true, 'message' => 'Surat berhasil disimpan', 'data' => $letter->load('fileUpload') ], 201); } catch (ValidationException $e) { return response()->json([ 'success' => false, 'message' => 'Validation failed', 'errors' => $e->errors() ], 422); } catch (Exception $e) { return response()->json([ 'success' => false, 'message' => 'Error creating letter', 'error' => $e->getMessage() ], 500); } } /** * Display the specified letter * * @param string $id Letter UUID * @return JsonResponse Letter details with file attachment * @throws ModelNotFoundException If letter not found */ public function show(string $id): JsonResponse { try { $letter = Letter::with('fileUpload')->findOrFail($id); return response()->json([ 'success' => true, 'data' => $this->formatLetterResponse($letter) ]); } catch (ModelNotFoundException $e) { return response()->json([ 'success' => false, 'message' => 'Letter not found' ], 404); } catch (Exception $e) { return response()->json([ 'success' => false, 'message' => 'Error retrieving letter', 'error' => $e->getMessage() ], 500); } } /** * Update the specified letter * * Handles partial updates with 'sometimes' validation rules * Updates file attachment if new file is uploaded * * @param Request $request Update data and optional new file * @param string $id Letter UUID * @return JsonResponse Updated letter data * @throws ModelNotFoundException If letter not found */ public function update(Request $request, string $id): JsonResponse { try { $letter = Letter::findOrFail($id); $validated = $request->validate([ 'nama_mahasiswa' => 'sometimes|string', 'nomor_induk_mahasiswa' => 'sometimes|string', 'program_studi_mahasiswa' => 'sometimes|string', 'tahun_ajaran' => 'sometimes|string', 'start_date' => 'sometimes|date', 'completion_date' => 'sometimes|date', 'is_acceptance' => 'sometimes|boolean', 'nomor_surat_acceptance' => 'nullable|string', 'is_completion' => 'sometimes|boolean', 'nomor_surat_completion' => 'nullable|string', 'lampiran' => 'nullable|file|mimes:pdf,jpg,png|max:2048', ]); $letter->update($validated); if ($request->hasFile('lampiran')) { $this->handleFileUpload($request->file('lampiran'), $letter, true); } return response()->json([ 'success' => true, 'message' => 'Surat berhasil diperbarui', 'data' => $letter->load('fileUpload') ]); } catch (ModelNotFoundException $e) { return response()->json([ 'success' => false, 'message' => 'Letter not found' ], 404); } catch (ValidationException $e) { return response()->json([ 'success' => false, 'message' => 'Validation failed', 'errors' => $e->errors() ], 422); } catch (Exception $e) { return response()->json([ 'success' => false, 'message' => 'Error updating letter', 'error' => $e->getMessage() ], 500); } } /** * Delete letter and its attachment * DELETE /api/letters/{id} */ public function destroy(string $id): JsonResponse { try { $letter = Letter::findOrFail($id); $this->deleteFileUpload($letter->id); $letter->delete(); return response()->json([ 'success' => true, 'message' => 'Surat berhasil dihapus' ]); } catch (ModelNotFoundException $e) { return response()->json([ 'success' => false, 'message' => 'Letter not found' ], 404); } catch (Exception $e) { return response()->json([ 'success' => false, 'message' => 'Error deleting letter', 'error' => $e->getMessage() ], 500); } } /** * Generate signed URL for file attachment * * Creates temporary signed URL valid for 5 minutes * Used for secure file access without exposing actual file path * * @param string $id FileUpload UUID * @return JsonResponse Signed URL and file metadata */ public function letterLampiran(string $id): JsonResponse { try { $file = FileUpload::findOrFail($id); $filePath = storage_path('app/public/' . $file->file_path); if (!file_exists($filePath)) { return response()->json([ 'success' => false, 'message' => 'File not found' ], 404); } // Generate signed URL valid for 5 minutes return response()->json([ 'success' => true, 'data' => [ 'file_name' => $file->file_name, 'mime_type' => $file->mime_type, 'url' => URL::signedRoute('file.view', ['id' => $id], now()->addMinutes(5)) ] ]); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => 'Error retrieving file', 'error' => $e->getMessage() ], 500); } } /** * Format letter data for consistent API response * * Includes file URL if attachment exists * Uses Laravel's asset() helper for public storage URLs * * @param Letter $letter Letter model instance * @return array Formatted letter data */ private function formatLetterResponse(Letter $letter): array { return [ 'id' => $letter->id, 'nama_mahasiswa' => $letter->nama_mahasiswa, 'nomor_induk_mahasiswa' => $letter->nomor_induk_mahasiswa, 'program_studi_mahasiswa' => $letter->program_studi_mahasiswa, 'tahun_ajaran' => $letter->tahun_ajaran, 'start_date' => $letter->start_date, 'completion_date' => $letter->completion_date, 'is_acceptance' => $letter->is_acceptance, 'nomor_surat_acceptance' => $letter->nomor_surat_acceptance, 'is_completion' => $letter->is_completion, 'nomor_surat_completion' => $letter->nomor_surat_completion, 'lampiran' => $letter->fileUpload ? asset('storage/lampiran/' . basename($letter->fileUpload->file_path)) : null, 'created_at' => $letter->created_at, ]; } /** * Handle file upload process * * Stores file in public storage * Creates FileUpload record * Updates letter with file reference * * @param UploadedFile $file The uploaded file * @param Letter $letter Associated letter * @param bool $isUpdate Whether this is an update operation */ private function handleFileUpload($file, Letter $letter, bool $isUpdate = false): void { // Delete existing file if updating if ($isUpdate) { $this->deleteFileUpload($letter->id); } // Store file and create record $path = $file->store('lampiran', 'public'); $fileId = Str::uuid()->toString(); FileUpload::create([ 'id' => $fileId, 'letter_id' => $letter->id, 'file_name' => $file->getClientOriginalName(), 'file_path' => $path, 'mime_type' => $file->getMimeType(), ]); $letter->update(['lampiran' => $fileId]); } /** * Helper method to delete file uploads */ private function deleteFileUpload(string $letterId): void { $file = FileUpload::where('letter_id', $letterId)->first(); if ($file) { Storage::delete($file->file_path); $file->delete(); } } }