worker dashboard api

This commit is contained in:
mohanmd 2026-06-10 16:49:33 +05:30
parent 7d0698509a
commit 30bef4e28e
14 changed files with 419 additions and 0 deletions

View File

@ -4,6 +4,7 @@
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Announcement; use App\Models\Announcement;
use App\Models\AnnouncementView;
use Illuminate\Http\Request; use Illuminate\Http\Request;
class WorkerAnnouncementController extends Controller class WorkerAnnouncementController extends Controller
@ -84,4 +85,114 @@ public function getAnnouncements(Request $request)
], 500); ], 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);
}
}
} }

View File

@ -0,0 +1,26 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class AnnouncementView extends Model
{
use HasFactory;
protected $fillable = [
'worker_id',
'announcement_id',
];
public function worker()
{
return $this->belongsTo(Worker::class, 'worker_id');
}
public function announcement()
{
return $this->belongsTo(Announcement::class, 'announcement_id');
}
}

View File

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('announcement_views', function (Blueprint $table) {
$table->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');
}
};

View File

@ -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": { "/employers/announcements": {
"get": { "get": {
"tags": [ "tags": [
@ -3259,6 +3378,16 @@
"type": "integer", "type": "integer",
"example": 5 "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": { "currently_working_sponsor": {
"type": "object", "type": "object",
"nullable": true, "nullable": true,

View File

@ -72,6 +72,8 @@
// Announcement Management // Announcement Management
Route::get('/workers/announcements', [WorkerAnnouncementController::class, 'getAnnouncements']); 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 // Report Management
Route::post('/workers/report', [\App\Http\Controllers\Api\ReportController::class, 'reportFromWorker']); Route::post('/workers/report', [\App\Http\Controllers\Api\ReportController::class, 'reportFromWorker']);

View File

@ -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'));
}
} }