diff --git a/app/Http/Controllers/FileViewController.php b/app/Http/Controllers/FileViewController.php new file mode 100644 index 0000000..57079ef --- /dev/null +++ b/app/Http/Controllers/FileViewController.php @@ -0,0 +1,14 @@ +file(storage_path('app/public/' . $file->file_path)); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/LetterController.php b/app/Http/Controllers/LetterController.php new file mode 100644 index 0000000..d6d2658 --- /dev/null +++ b/app/Http/Controllers/LetterController.php @@ -0,0 +1,346 @@ +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(); + } + } +} diff --git a/app/Models/FileUpload.php b/app/Models/FileUpload.php new file mode 100644 index 0000000..3f2b64b --- /dev/null +++ b/app/Models/FileUpload.php @@ -0,0 +1,27 @@ +id = Str::uuid()->toString(); + }); + } +} \ No newline at end of file diff --git a/app/Models/Letter.php b/app/Models/Letter.php new file mode 100644 index 0000000..8cbe277 --- /dev/null +++ b/app/Models/Letter.php @@ -0,0 +1,44 @@ +id = Str::uuid()->toString(); + }); + } + + public function fileUpload() + { + return $this->hasOne(FileUpload::class, 'letter_id', 'id'); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index a6ab88e..3eb6632 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -6,6 +6,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; +use Illuminate\Support\Str; use Laravel\Sanctum\HasApiTokens; class User extends Authenticatable @@ -46,4 +47,15 @@ class User extends Authenticatable 'password' => 'hashed', ]; } + + protected $keyType = 'string'; + public $incrementing = false; + + public static function boot() { + parent::boot(); + + static::creating(function ($model) { + $model->id = Str::uuid(); + }); + } } diff --git a/database/migrations/2025_03_08_142237_create_acceptance_letters_table.php b/database/migrations/2025_03_08_142237_create_acceptance_letters_table.php index c5df6f9..55dad7a 100644 --- a/database/migrations/2025_03_08_142237_create_acceptance_letters_table.php +++ b/database/migrations/2025_03_08_142237_create_acceptance_letters_table.php @@ -11,15 +11,20 @@ return new class extends Migration */ public function up(): void { - Schema::create('acceptance_letters', function (Blueprint $table) { - $table->id(); - $table->string('nomor_surat'); + Schema::create('letters', function (Blueprint $table) { + $table->uuid('id')->primary(); + // $table->string('nomor_surat'); $table->string('nama_mahasiswa'); $table->string('nomor_induk_mahasiswa'); $table->string('program_studi_mahasiswa'); $table->string('tahun_ajaran'); $table->date('start_date'); + $table->date('completion_date'); $table->string('lampiran')->nullable(); + $table->boolean('is_acceptance')->default(false); + $table->string('nomor_surat_acceptance')->nullable(); + $table->boolean('is_completion')->default(false); + $table->string('nomor_surat_completion')->nullable(); $table->timestamps(); }); } @@ -29,6 +34,6 @@ return new class extends Migration */ public function down(): void { - Schema::dropIfExists('acceptance_letters'); + Schema::dropIfExists('letters'); } }; diff --git a/database/migrations/2025_03_08_142250_create_completion_letters_table.php b/database/migrations/2025_03_08_142250_create_completion_letters_table.php deleted file mode 100644 index 8f11010..0000000 --- a/database/migrations/2025_03_08_142250_create_completion_letters_table.php +++ /dev/null @@ -1,34 +0,0 @@ -id(); - $table->string('nomor_surat'); - $table->string('nama_mahasiswa'); - $table->string('nomor_induk_mahasiswa'); - $table->string('program_studi_mahasiswa'); - $table->string('tahun_ajaran'); - $table->date('completion_date'); - $table->string('lampiran')->nullable(); - $table->timestamps(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('completion_letters'); - } -}; diff --git a/database/migrations/2025_03_08_142302_create_completion_templates_table.php b/database/migrations/2025_03_08_142302_create_completion_templates_table.php index c9700f7..af94461 100644 --- a/database/migrations/2025_03_08_142302_create_completion_templates_table.php +++ b/database/migrations/2025_03_08_142302_create_completion_templates_table.php @@ -12,7 +12,7 @@ return new class extends Migration public function up(): void { Schema::create('completion_templates', function (Blueprint $table) { - $table->id(); + $table->uuid('id')->primary(); $table->string('greeting'); $table->text('content'); $table->string('closing'); diff --git a/database/migrations/2025_03_23_005233_create_file_uploads_table.php b/database/migrations/2025_03_23_005233_create_file_uploads_table.php new file mode 100644 index 0000000..781ae1b --- /dev/null +++ b/database/migrations/2025_03_23_005233_create_file_uploads_table.php @@ -0,0 +1,31 @@ +uuid('id')->primary(); + $table->uuid('letter_id')->nullable(); + $table->string('file_name'); + $table->string('file_path'); + $table->string('mime_type'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('file_uploads'); + } +}; diff --git a/routes/api.php b/routes/api.php index d812db3..4a416a9 100644 --- a/routes/api.php +++ b/routes/api.php @@ -4,16 +4,70 @@ use App\Http\Controllers\AcceptanceLetterController; use App\Http\Controllers\AcceptanceTemplateController; use App\Http\Controllers\CompletionLetterController; use App\Http\Controllers\CompletionTemplateController; +use App\Http\Controllers\FileViewController; +use App\Http\Controllers\LetterController; +use App\Models\FileUpload; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Route; +/* +|-------------------------------------------------------------------------- +| API Routes +|-------------------------------------------------------------------------- +| +| Here is where you can register API routes for your application. These +| routes are loaded by the RouteServiceProvider within a group which +| is assigned the "api" middleware group. +| +*/ + +// Authentication Routes Route::get('/user', function (Request $request) { return $request->user(); })->middleware('auth:sanctum'); -// Route::middleware('auth:sanctum')->group(function () { - Route::apiResource('acceptance_letters', AcceptanceLetterController::class); - Route::apiResource('completion_letters', CompletionLetterController::class); - Route::apiResource('completion_templates', CompletionTemplateController::class); - Route::apiResource('acceptance_templates', AcceptanceTemplateController::class); -// }); +// Letter Management Routes +Route::apiResource('letters', LetterController::class); + +// File Attachment Routes +Route::get('/lampiran/{id}', [LetterController::class, 'letterLampiran']) + ->name('letters.lampiran'); + +// Template Management Routes +Route::apiResource('completion_templates', CompletionTemplateController::class); +Route::apiResource('acceptance_templates', AcceptanceTemplateController::class); + +// Letter Type Routes +Route::apiResource('acceptance_letters', AcceptanceLetterController::class); +Route::apiResource('completion_letters', CompletionLetterController::class); + +// File Viewer Route (Internal use for signed URLs) +Route::get('/file/{id}', [FileViewController::class, 'show']) + ->name('file.view') + ->middleware('signed'); + +/** + * API Endpoints Documentation: + * + * GET /api/user - Get authenticated user + * + * Letters: + * GET /api/letters - List all letters + * POST /api/letters - Create new letter + * GET /api/letters/{id} - Get letter details + * PUT /api/letters/{id} - Update letter + * DELETE /api/letters/{id} - Delete letter + * GET /api/letters/lampiran/{id} - Get file attachment URL + * + * Templates: + * GET /api/completion_templates - List completion templates + * GET /api/acceptance_templates - List acceptance templates + * + * Letter Types: + * GET /api/acceptance_letters - List acceptance letters + * GET /api/completion_letters - List completion letters + * + * File Handling: + * GET /api/file/{id} - View file (internal, signed URL) + */ diff --git a/storage/app/private/.gitignore b/storage/app/private/.gitignore deleted file mode 100644 index d6b7ef3..0000000 --- a/storage/app/private/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore