createProgressTestTablesIfMissing(); config()->set('progress.status_options', [ 'draft' => 'Draft', 'submitted' => 'Submitted', 'reviewed' => 'Reviewed', 'approved' => 'Approved', 'rejected' => 'Rejected', ]); config()->set('progress.subject_sections', [ 'quran' => ['Reading', 'Memorization'], 'arabic' => ['Reading', 'Writing'], ]); } /** @test */ public function it_lists_grouped_progress_reports(): void { $sectionId = $this->insertClassSection('Grade 5 - A'); $this->insertStudentClass($sectionId); $this->insertProgressReport([ 'class_section_id' => $sectionId, 'subject' => 'quran', 'status' => 'submitted', 'week_start' => '2026-02-09', 'week_end' => '2026-02-15', ]); $this->insertProgressReport([ 'class_section_id' => $sectionId, 'subject' => 'arabic', 'status' => 'submitted', 'week_start' => '2026-02-09', 'week_end' => '2026-02-15', ]); $response = $this->getJson('/api/admin/progress'); $response->assertOk() ->assertJsonStructure([ 'data' => [ '*' => [ 'week_start', 'week_end', 'class_section_id', 'class_section_name', 'reports', ], ], 'meta' => [ 'filters', 'class_sections', 'status_options', 'subject_sections', ], ]); $data = $response->json('data'); $this->assertCount(1, $data); $this->assertEquals('2026-02-09', $data[0]['week_start']); $this->assertEquals($sectionId, $data[0]['class_section_id']); $this->assertArrayHasKey('quran', $data[0]['reports']); $this->assertArrayHasKey('arabic', $data[0]['reports']); } /** @test */ public function it_filters_progress_reports_by_status_and_class_section(): void { $sectionA = $this->insertClassSection('Grade 4 - A'); $sectionB = $this->insertClassSection('Grade 6 - B'); $this->insertStudentClass($sectionA); $this->insertStudentClass($sectionB); $this->insertProgressReport([ 'class_section_id' => $sectionA, 'subject' => 'quran', 'status' => 'approved', 'week_start' => '2026-02-09', 'week_end' => '2026-02-15', ]); $this->insertProgressReport([ 'class_section_id' => $sectionB, 'subject' => 'arabic', 'status' => 'draft', 'week_start' => '2026-02-09', 'week_end' => '2026-02-15', ]); $response = $this->getJson('/api/admin/progress?status=approved&class_section_id=' . $sectionA); $response->assertOk(); $data = $response->json('data'); $this->assertCount(1, $data); $this->assertEquals($sectionA, $data[0]['class_section_id']); $this->assertArrayHasKey('quran', $data[0]['reports']); $this->assertArrayNotHasKey('arabic', $data[0]['reports']); } /** @test */ public function it_shows_progress_report_with_weekly_reports_and_attachments(): void { $sectionId = $this->insertClassSection('Grade 5 - A'); $report1 = $this->insertProgressReport([ 'class_section_id' => $sectionId, 'subject' => 'quran', 'status' => 'submitted', 'week_start' => '2026-02-09', 'week_end' => '2026-02-15', 'flags_json' => json_encode(['needs_followup' => true]), ]); $report2 = $this->insertProgressReport([ 'class_section_id' => $sectionId, 'subject' => 'arabic', 'status' => 'submitted', 'week_start' => '2026-02-09', 'week_end' => '2026-02-15', ]); $this->insertAttachment($report1, [ 'original_name' => 'quran-notes.pdf', 'file_path' => 'uploads/progress/quran-notes.pdf', ]); $response = $this->getJson('/api/admin/progress/reports/' . $report1); $response->assertOk() ->assertJsonStructure([ 'data' => [ 'report', 'weekly_reports', 'subject_sections', ], ]); $this->assertEquals($report1, $response->json('data.report.id')); $this->assertEquals('Submitted', $response->json('data.report.status_label')); $this->assertTrue($response->json('data.report.flags.needs_followup')); $weekly = $response->json('data.weekly_reports'); $this->assertCount(2, $weekly); $quranReport = collect($weekly)->firstWhere('subject', 'quran'); $this->assertNotNull($quranReport); $this->assertNotEmpty($quranReport['attachments']); $this->assertEquals('quran-notes.pdf', $quranReport['attachments'][0]['name']); $arabicReport = collect($weekly)->firstWhere('subject', 'arabic'); $this->assertNotNull($arabicReport); $this->assertIsArray($arabicReport['attachments']); } /** @test */ public function it_returns_404_for_missing_progress_report(): void { $this->getJson('/api/admin/progress/reports/999999') ->assertStatus(404) ->assertJson([ 'message' => 'Progress report not found.', ]); } /** @test */ public function it_downloads_legacy_attachment_from_report_attachment_path(): void { File::ensureDirectoryExists(storage_path('app/uploads/progress')); $fullPath = storage_path('app/uploads/progress/legacy-file.txt'); File::put($fullPath, 'legacy attachment content'); $reportId = $this->insertProgressReport([ 'class_section_id' => 1, 'subject' => 'quran', 'status' => 'submitted', 'week_start' => '2026-02-09', 'week_end' => '2026-02-15', 'attachment_path' => 'writable/uploads/progress/legacy-file.txt', ]); $response = $this->get('/api/admin/progress/reports/' . $reportId . '/attachment'); $response->assertOk(); $response->assertHeader('content-disposition'); $this->assertStringContainsString('legacy-file.txt', $response->headers->get('content-disposition', '')); } /** @test */ public function it_returns_404_when_legacy_attachment_is_missing(): void { $reportId = $this->insertProgressReport([ 'class_section_id' => 1, 'subject' => 'quran', 'status' => 'submitted', 'week_start' => '2026-02-09', 'week_end' => '2026-02-15', 'attachment_path' => 'writable/uploads/progress/missing.txt', ]); $this->get('/api/admin/progress/reports/' . $reportId . '/attachment') ->assertStatus(404); } /** @test */ public function it_downloads_normalized_attachment_by_attachment_id(): void { File::ensureDirectoryExists(storage_path('app/uploads/progress')); $fullPath = storage_path('app/uploads/progress/normalized-file.txt'); File::put($fullPath, 'normalized attachment content'); $reportId = $this->insertProgressReport([ 'class_section_id' => 1, 'subject' => 'arabic', 'status' => 'approved', 'week_start' => '2026-02-09', 'week_end' => '2026-02-15', ]); $attachmentId = $this->insertAttachment($reportId, [ 'original_name' => 'Teacher Notes.txt', 'file_path' => 'writable/uploads/progress/normalized-file.txt', ]); $response = $this->get('/api/admin/progress/attachments/' . $attachmentId); $response->assertOk(); $this->assertStringContainsString('Teacher Notes.txt', $response->headers->get('content-disposition', '')); } /** @test */ public function it_returns_404_for_missing_normalized_attachment(): void { $this->get('/api/admin/progress/attachments/999999') ->assertStatus(404); } // ------------------------------------------------------------------ // Helpers // ------------------------------------------------------------------ private function createProgressTestTablesIfMissing(): void { if (!Schema::hasTable('classSection')) { Schema::create('classSection', function (Blueprint $table) { $table->increments('class_section_id'); $table->string('class_section_name')->nullable(); }); } if (!Schema::hasTable('student_class')) { Schema::create('student_class', function (Blueprint $table) { $table->increments('id'); $table->unsignedInteger('class_section_id'); $table->unsignedInteger('student_id')->nullable(); }); } if (!Schema::hasTable('users')) { Schema::create('users', function (Blueprint $table) { $table->id(); $table->string('firstname')->nullable(); $table->string('lastname')->nullable(); $table->string('email')->nullable(); $table->string('password')->nullable(); $table->timestamps(); }); } else { // Add columns if test app users table exists but lacks firstname/lastname if (!Schema::hasColumn('users', 'firstname')) { Schema::table('users', function (Blueprint $table) { $table->string('firstname')->nullable(); }); } if (!Schema::hasColumn('users', 'lastname')) { Schema::table('users', function (Blueprint $table) { $table->string('lastname')->nullable(); }); } } if (!Schema::hasTable('class_progress_reports')) { Schema::create('class_progress_reports', function (Blueprint $table) { $table->id(); $table->unsignedInteger('class_section_id')->nullable(); $table->unsignedBigInteger('teacher_id')->nullable(); $table->string('subject')->nullable(); $table->date('week_start')->nullable(); $table->date('week_end')->nullable(); $table->string('status')->nullable(); $table->text('flags_json')->nullable(); $table->string('attachment_path')->nullable(); $table->text('notes')->nullable(); $table->timestamps(); }); } if (!Schema::hasTable('class_progress_attachments')) { Schema::create('class_progress_attachments', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('report_id'); $table->string('original_name')->nullable(); $table->string('file_path')->nullable(); $table->timestamps(); }); } } private function insertClassSection(string $name): int { return (int) \DB::table('classSection')->insertGetId([ 'class_section_name' => $name, ], 'class_section_id'); } private function insertStudentClass(int $classSectionId): int { return (int) \DB::table('student_class')->insertGetId([ 'class_section_id' => $classSectionId, 'student_id' => rand(1000, 9999), ]); } private function insertProgressReport(array $override = []): int { $data = array_merge([ 'class_section_id' => 1, 'teacher_id' => null, 'subject' => 'quran', 'week_start' => '2026-02-09', 'week_end' => '2026-02-15', 'status' => 'draft', 'flags_json' => null, 'attachment_path' => null, 'notes' => 'Test report', 'created_at' => now(), 'updated_at' => now(), ], $override); return (int) \DB::table('class_progress_reports')->insertGetId($data); } private function insertAttachment(int $reportId, array $override = []): int { $data = array_merge([ 'report_id' => $reportId, 'original_name' => 'Attachment.pdf', 'file_path' => 'writable/uploads/progress/attachment.pdf', 'created_at' => now(), 'updated_at' => now(), ], $override); return (int) \DB::table('class_progress_attachments')->insertGetId($data); } }