diff --git a/app/Http/Controllers/Api/WorkerAnnouncementController.php b/app/Http/Controllers/Api/WorkerAnnouncementController.php index ec1d1a7..3b183ef 100644 --- a/app/Http/Controllers/Api/WorkerAnnouncementController.php +++ b/app/Http/Controllers/Api/WorkerAnnouncementController.php @@ -4,6 +4,7 @@ use App\Http\Controllers\Controller; use App\Models\Announcement; +use App\Models\AnnouncementView; use Illuminate\Http\Request; class WorkerAnnouncementController extends Controller @@ -84,4 +85,114 @@ public function getAnnouncements(Request $request) ], 500); } } + + /** + * Get up to 3 unviewed announcements for the worker, and mark them as viewed. + */ + public function getNewAnnouncements(Request $request) + { + /** @var \App\Models\Worker $worker */ + $worker = $request->attributes->get('worker'); + + try { + $viewedIds = AnnouncementView::where('worker_id', $worker->id)->pluck('announcement_id'); + + $dbAnnouncements = Announcement::with(['employer.employerProfile', 'sponsor']) + ->where('status', 'approved') + ->whereNotIn('id', $viewedIds) + ->latest('id') + ->limit(3) + ->get(); + + // Mark them as viewed + foreach ($dbAnnouncements as $announcement) { + AnnouncementView::firstOrCreate([ + 'worker_id' => $worker->id, + 'announcement_id' => $announcement->id, + ]); + } + + $announcements = $dbAnnouncements->map(function ($announcement) { + $postedBy = 'System'; + $organization = 'Migrant Support'; + + if ($announcement->sponsor_id) { + $postedBy = $announcement->sponsor->full_name; + $organization = $announcement->sponsor->organization_name; + } elseif ($announcement->employer_id) { + $postedBy = $announcement->employer->name; + $organization = $announcement->employer->employerProfile->company_name ?? 'Employer'; + } + + // Decode charity details if they exist in json + $charityDetails = null; + $content = $announcement->body; + if (strpos($announcement->body, '{"type":"Charity"') === 0) { + $decoded = json_decode($announcement->body, true); + if ($decoded) { + $charityDetails = $decoded; + $content = $decoded['content'] ?? $announcement->body; + } + } + + return [ + 'id' => $announcement->id, + 'title' => $announcement->title, + 'body' => $content, + 'type' => $announcement->type, + 'employer_name' => $postedBy, + 'company_name' => $organization, + 'created_at' => $announcement->created_at->toISOString(), + 'time_ago' => $announcement->created_at->diffForHumans(), + 'charity_details' => $charityDetails, + ]; + }); + + return response()->json([ + 'success' => true, + 'data' => [ + 'announcements' => $announcements, + ] + ], 200); + + } catch (\Exception $e) { + logger()->error('Mobile Worker Get New Announcements Failure: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'An error occurred while fetching new announcements.', + 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' + ], 500); + } + } + + /** + * Explicitly mark an announcement as viewed by the worker. + */ + public function markAnnouncementViewed(Request $request, $id) + { + /** @var \App\Models\Worker $worker */ + $worker = $request->attributes->get('worker'); + + try { + AnnouncementView::firstOrCreate([ + 'worker_id' => $worker->id, + 'announcement_id' => $id, + ]); + + return response()->json([ + 'success' => true, + 'message' => 'Announcement marked as viewed.' + ], 200); + + } catch (\Exception $e) { + logger()->error('Mobile Worker Mark Announcement Viewed Failure: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'An error occurred.', + 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' + ], 500); + } + } } diff --git a/app/Models/AnnouncementView.php b/app/Models/AnnouncementView.php new file mode 100644 index 0000000..5b77429 --- /dev/null +++ b/app/Models/AnnouncementView.php @@ -0,0 +1,26 @@ +belongsTo(Worker::class, 'worker_id'); + } + + public function announcement() + { + return $this->belongsTo(Announcement::class, 'announcement_id'); + } +} diff --git a/database/migrations/2026_06_10_150000_create_announcement_views_table.php b/database/migrations/2026_06_10_150000_create_announcement_views_table.php new file mode 100644 index 0000000..62eeb00 --- /dev/null +++ b/database/migrations/2026_06_10_150000_create_announcement_views_table.php @@ -0,0 +1,31 @@ +id(); + $table->foreignId('worker_id')->constrained('workers')->onDelete('cascade'); + $table->foreignId('announcement_id')->constrained('announcements')->onDelete('cascade'); + $table->timestamps(); + + $table->unique(['worker_id', 'announcement_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('announcement_views'); + } +}; diff --git a/public/swagger.json b/public/swagger.json index ea39e7a..fbb192c 100644 --- a/public/swagger.json +++ b/public/swagger.json @@ -2361,6 +2361,125 @@ } } }, + "/workers/announcements/new": { + "get": { + "tags": [ + "Worker/CharityEvents" + ], + "summary": "Get New/Unviewed Announcements/Charity Events", + "description": "Retrieves up to 3 active approved charity events/announcements that the worker has not yet viewed, and automatically marks them as viewed.", + "responses": { + "200": { + "description": "Successfully retrieved new announcements.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 4 + }, + "title": { + "type": "string", + "example": "Free Dental Checkup Camp" + }, + "body": { + "type": "string", + "example": "Emirates Charity is providing free screening." + }, + "type": { + "type": "string", + "example": "charity" + }, + "employer_name": { + "type": "string", + "example": "Emirates Charity" + }, + "company_name": { + "type": "string", + "example": "Emirates Charity Foundation" + }, + "created_at": { + "type": "string", + "format": "date-time", + "example": "2026-06-01T10:00:00.000000Z" + }, + "time_ago": { + "type": "string", + "example": "5 hours ago" + } + } + } + } + } + } + } + } + }, + "401": { + "description": "Unauthenticated." + } + } + } + }, + "/workers/announcements/{id}/view": { + "post": { + "tags": [ + "Worker/CharityEvents" + ], + "summary": "Mark Specific Announcement as Viewed", + "description": "Explicitly marks a specific announcement/charity event as viewed by the authenticated worker.", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "Announcement ID", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Announcement marked as viewed successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "Announcement marked as viewed." + } + } + } + } + } + }, + "401": { + "description": "Unauthenticated." + }, + "404": { + "description": "Announcement not found." + } + } + } + }, "/employers/announcements": { "get": { "tags": [ @@ -3259,6 +3378,16 @@ "type": "integer", "example": 5 }, + "employer_contacted": { + "type": "integer", + "example": 2, + "description": "Unique count of employers who contacted this worker." + }, + "profile_viewed": { + "type": "integer", + "example": 3, + "description": "Unique count of employers who viewed this worker's profile." + }, "currently_working_sponsor": { "type": "object", "nullable": true, diff --git a/public/uploads/documents/1781088511_passport_passport.pdf b/public/uploads/documents/1781088511_passport_passport.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/documents/1781088528_passport_passport.pdf b/public/uploads/documents/1781088528_passport_passport.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/documents/1781088910_passport_passport.pdf b/public/uploads/documents/1781088910_passport_passport.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/documents/1781088943_passport_passport.pdf b/public/uploads/documents/1781088943_passport_passport.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/licenses/1781088511_license_license.pdf b/public/uploads/licenses/1781088511_license_license.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/licenses/1781088528_license_license.pdf b/public/uploads/licenses/1781088528_license_license.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/licenses/1781088910_license_license.pdf b/public/uploads/licenses/1781088910_license_license.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/licenses/1781088941_license_license.pdf b/public/uploads/licenses/1781088941_license_license.pdf new file mode 100644 index 0000000..e69de29 diff --git a/routes/api.php b/routes/api.php index d045a1f..9ffce2c 100644 --- a/routes/api.php +++ b/routes/api.php @@ -72,6 +72,8 @@ // Announcement Management Route::get('/workers/announcements', [WorkerAnnouncementController::class, 'getAnnouncements']); + Route::get('/workers/announcements/new', [WorkerAnnouncementController::class, 'getNewAnnouncements']); + Route::post('/workers/announcements/{id}/view', [WorkerAnnouncementController::class, 'markAnnouncementViewed']); // Report Management Route::post('/workers/report', [\App\Http\Controllers\Api\ReportController::class, 'reportFromWorker']); diff --git a/tests/Feature/WorkerJourneyApiTest.php b/tests/Feature/WorkerJourneyApiTest.php index df77631..eed1f3c 100644 --- a/tests/Feature/WorkerJourneyApiTest.php +++ b/tests/Feature/WorkerJourneyApiTest.php @@ -679,4 +679,124 @@ public function test_worker_dashboard_stats() ] ]); } + + /** + * Test getting new unviewed announcements and marking them viewed. + */ + public function test_worker_new_announcements_flow() + { + // 1. Create a worker + $worker = Worker::create([ + 'name' => 'Rahul Sharma', + 'email' => 'rahul@example.com', + 'phone' => '+971501234567', + 'language' => 'HI', + 'password' => bcrypt('password'), + 'nationality' => 'India', + 'age' => 25, + 'salary' => 1500, + 'availability' => 'Immediate', + 'experience' => 'Not Specified', + 'religion' => 'Not Specified', + 'bio' => 'Test', + 'category_id' => $this->category->id, + 'verified' => true, + 'status' => 'active', + 'api_token' => 'worker-test-token-announcements', + ]); + + // Create passport document to prevent 404/any checks + WorkerDocument::create([ + 'worker_id' => $worker->id, + 'type' => 'passport', + 'number' => '123456', + 'file_path' => 'passports/test.pdf', + ]); + + // 2. Create 4 approved announcements + $ann1 = \App\Models\Announcement::create([ + 'title' => 'Event 1', + 'body' => 'Details 1', + 'type' => 'charity', + 'status' => 'approved', + ]); + + $ann2 = \App\Models\Announcement::create([ + 'title' => 'Event 2', + 'body' => 'Details 2', + 'type' => 'charity', + 'status' => 'approved', + ]); + + $ann3 = \App\Models\Announcement::create([ + 'title' => 'Event 3', + 'body' => 'Details 3', + 'type' => 'charity', + 'status' => 'approved', + ]); + + $ann4 = \App\Models\Announcement::create([ + 'title' => 'Event 4', + 'body' => 'Details 4', + 'type' => 'charity', + 'status' => 'approved', + ]); + + // 3. Get new announcements (should return max 3 events, latest first: Event 4, Event 3, Event 2) + $response = $this->withHeaders([ + 'Authorization' => 'Bearer worker-test-token-announcements', + ])->getJson('/api/workers/announcements/new'); + + $response->assertStatus(200); + $data = $response->json('data.announcements'); + $this->assertCount(3, $data); + $this->assertEquals('Event 4', $data[0]['title']); + $this->assertEquals('Event 3', $data[1]['title']); + $this->assertEquals('Event 2', $data[2]['title']); + + // Assert that they are now marked as viewed in the DB + $this->assertDatabaseHas('announcement_views', ['worker_id' => $worker->id, 'announcement_id' => $ann4->id]); + $this->assertDatabaseHas('announcement_views', ['worker_id' => $worker->id, 'announcement_id' => $ann3->id]); + $this->assertDatabaseHas('announcement_views', ['worker_id' => $worker->id, 'announcement_id' => $ann2->id]); + + // 4. Get new announcements again (should return the remaining 1: Event 1) + $response = $this->withHeaders([ + 'Authorization' => 'Bearer worker-test-token-announcements', + ])->getJson('/api/workers/announcements/new'); + + $response->assertStatus(200); + $data = $response->json('data.announcements'); + $this->assertCount(1, $data); + $this->assertEquals('Event 1', $data[0]['title']); + + // 5. Get new announcements again (should return 0) + $response = $this->withHeaders([ + 'Authorization' => 'Bearer worker-test-token-announcements', + ])->getJson('/api/workers/announcements/new'); + + $response->assertStatus(200); + $this->assertCount(0, $response->json('data.announcements')); + + // 6. Create a 5th approved announcement + $ann5 = \App\Models\Announcement::create([ + 'title' => 'Event 5', + 'body' => 'Details 5', + 'type' => 'charity', + 'status' => 'approved', + ]); + + // 7. Manually mark Event 5 as viewed + $response = $this->withHeaders([ + 'Authorization' => 'Bearer worker-test-token-announcements', + ])->postJson("/api/workers/announcements/{$ann5->id}/view"); + $response->assertStatus(200); + + // 8. Get new announcements (should return 0 because Event 5 is already viewed) + $response = $this->withHeaders([ + 'Authorization' => 'Bearer worker-test-token-announcements', + ])->getJson('/api/workers/announcements/new'); + + $response->assertStatus(200); + $this->assertCount(0, $response->json('data.announcements')); + } }