mohan #5

Merged
mohanmd merged 46 commits from mohan into master 2026-06-15 09:13:23 +00:00
15 changed files with 1066 additions and 48 deletions
Showing only changes of commit eb75dca482 - Show all commits

View File

@ -7,12 +7,69 @@
use App\Models\Message;
use App\Models\Worker;
use App\Models\User;
use App\Models\JobOffer;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
class EmployerMessageController extends Controller
{
public static function processWorkerResponse($conv, $worker, $text)
{
$replyText = strtolower(trim($text));
if ($replyText === 'yes' || $replyText === 'no') {
// Find the last message sent by the employer in this conversation (excluding the current worker message)
$lastEmployerMessage = Message::where('conversation_id', $conv->id)
->where('sender_type', 'employer')
->latest()
->first();
if ($lastEmployerMessage) {
$questionText = strtolower($lastEmployerMessage->text);
$isLookingJobQuestion = str_contains($questionText, 'looking job') ||
str_contains($questionText, 'looking for a job') ||
str_contains($questionText, 'looking for job') ||
str_contains($questionText, 'are you looking');
if ($isLookingJobQuestion) {
if ($replyText === 'yes') {
// S6 Outcome: Update status as Hired
$worker->update([
'status' => 'Hired',
'availability' => 'Hired'
]);
// Accept existing direct offer or application
$offer = JobOffer::where('employer_id', $conv->employer_id)
->where('worker_id', $worker->id)
->first();
if ($offer) {
$offer->update(['status' => 'accepted']);
} else {
JobOffer::create([
'employer_id' => $conv->employer_id,
'worker_id' => $worker->id,
'work_date' => now()->format('Y-m-d'),
'location' => 'Dubai',
'salary' => $worker->salary ?: 2000,
'notes' => 'Hired via chat agreement.',
'status' => 'accepted',
]);
}
} else if ($replyText === 'no') {
// S5 Outcome: Update status as Hidden (Auto-Hidden from search)
$worker->update([
'status' => 'hidden',
'availability' => 'Not Available'
]);
}
}
}
}
}
/**
* Get all conversations for the authorized employer.
*
@ -186,6 +243,32 @@ public function sendMessage(Request $request, $id)
// Touch conversation updated_at for sorting
$conv->touch();
// Check if employer sent predefined message "are you looking job?"
$textLower = strtolower($request->text);
if (
str_contains($textLower, 'looking job') ||
str_contains($textLower, 'looking for a job') ||
str_contains($textLower, 'looking for job') ||
str_contains($textLower, 'are you looking')
) {
// Automatically simulate worker response 'Yes' (triggers the S6 hired flow)
$workerResponseText = 'Yes';
// Create the message in database
Message::create([
'conversation_id' => $conv->id,
'sender_type' => 'worker',
'sender_id' => $conv->worker_id,
'text' => $workerResponseText,
]);
// Process the worker response to update status to Hired!
$worker = Worker::find($conv->worker_id);
if ($worker) {
self::processWorkerResponse($conv, $worker, $workerResponseText);
}
}
});
return response()->json([

View File

@ -0,0 +1,144 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\User;
use App\Models\Worker;
use App\Models\Review;
use Illuminate\Support\Facades\Validator;
class EmployerReviewController extends Controller
{
/**
* Add a new review for a worker.
* POST /api/employers/reviews
*/
public function addReview(Request $request)
{
/** @var User $employer */
$employer = $request->attributes->get('employer');
$validator = Validator::make($request->all(), [
'worker_id' => 'required|exists:workers,id',
'rating' => 'required|integer|min:1|max:5',
'comment' => 'nullable|string|max:1000',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'message' => 'Validation error.',
'errors' => $validator->errors()
], 422);
}
try {
// Check if employer has already reviewed this worker
$existingReview = Review::where('employer_id', $employer->id)
->where('worker_id', $request->worker_id)
->first();
if ($existingReview) {
// If they already have a review, we can update it or direct them to the edit endpoint.
// To be robust, let's update it directly and inform them it was updated (acting as an edit).
$existingReview->update([
'rating' => $request->rating,
'comment' => $request->comment,
]);
return response()->json([
'success' => true,
'message' => 'Review updated successfully (existing review updated).',
'data' => [
'review' => $existingReview
]
], 200);
}
// Create new review
$review = Review::create([
'employer_id' => $employer->id,
'worker_id' => $request->worker_id,
'rating' => $request->rating,
'comment' => $request->comment,
]);
return response()->json([
'success' => true,
'message' => 'Review added successfully.',
'data' => [
'review' => $review
]
], 201);
} catch (\Exception $e) {
logger()->error('Mobile API Add Review Failure: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'An error occurred while adding the review.',
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
], 500);
}
}
/**
* Edit/update an existing review.
* PUT /api/employers/reviews/{id}
*/
public function editReview(Request $request, $id)
{
/** @var User $employer */
$employer = $request->attributes->get('employer');
$validator = Validator::make($request->all(), [
'rating' => 'required|integer|min:1|max:5',
'comment' => 'nullable|string|max:1000',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'message' => 'Validation error.',
'errors' => $validator->errors()
], 422);
}
try {
$review = Review::where('id', $id)
->where('employer_id', $employer->id)
->first();
if (!$review) {
return response()->json([
'success' => false,
'message' => 'Review not found or unauthorized.'
], 404);
}
$review->update([
'rating' => $request->rating,
'comment' => $request->comment,
]);
return response()->json([
'success' => true,
'message' => 'Review edited successfully.',
'data' => [
'review' => $review
]
], 200);
} catch (\Exception $e) {
logger()->error('Mobile API Edit Review Failure: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'An error occurred while updating the review.',
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
], 500);
}
}
}

View File

@ -11,6 +11,8 @@
use App\Models\JobOffer;
use App\Models\JobApplication;
use App\Models\JobPost;
use App\Models\Review;
use App\Models\ProfileView;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
@ -376,6 +378,14 @@ public function getWorkerDetail(Request $request, $id)
], 404);
}
// Record employer profile view (Requirement 3)
if ($employer) {
ProfileView::updateOrCreate(
['employer_id' => $employer->id, 'worker_id' => $w->id],
['updated_at' => now()]
);
}
// Map languages
$langs = ($w->id % 2 === 0) ? ['English', 'Arabic'] : (($w->id % 3 === 0) ? ['English', 'Tagalog'] : ['English', 'Hindi', 'Arabic']);
@ -409,26 +419,41 @@ public function getWorkerDetail(Request $request, $id)
];
$photo = $photos[$w->id % 4];
$rating = 4.0 + (($w->id * 3) % 10) / 10.0;
$reviewsCount = ($w->id * 4) % 20 + 2;
// Fetch dynamic database reviews (Requirement 1)
$dbReviews = Review::where('worker_id', $w->id)->with('employer')->latest()->get();
$reviews = $dbReviews->map(function ($rev) {
return [
'id' => $rev->id,
'employer_name' => $rev->employer->name ?? 'Employer',
'rating' => $rev->rating,
'date' => $rev->created_at->format('M d, Y'),
'comment' => $rev->comment,
];
})->toArray();
// Hardcoded premium reviews matching web detail view
$reviews = [
[
'id' => 1,
'employer_name' => 'Fatima Al Mansoori',
'rating' => 5,
'date' => 'May 10, 2026',
'comment' => 'Extremely reliable and respectful. Professional work and great communication.',
],
[
'id' => 2,
'employer_name' => 'Michael Harrison',
'rating' => 4,
'date' => 'Mar 24, 2026',
'comment' => 'Very punctual, did exactly what was expected. Highly recommend.',
]
];
$reviewsCount = count($reviews);
if ($reviewsCount > 0) {
$rating = round($dbReviews->avg('rating'), 1);
} else {
$rating = 4.0 + (($w->id * 3) % 10) / 10.0;
$reviewsCount = ($w->id * 4) % 20 + 2;
$reviews = [
[
'id' => 1,
'employer_name' => 'Fatima Al Mansoori',
'rating' => 5,
'date' => 'May 10, 2026',
'comment' => 'Extremely reliable and respectful. Professional work and great communication.',
],
[
'id' => 2,
'employer_name' => 'Michael Harrison',
'rating' => 4,
'date' => 'Mar 24, 2026',
'comment' => 'Very punctual, did exactly what was expected. Highly recommend.',
]
];
}
// Similar workers matching web view
$simDb = Worker::with('category')

View File

@ -6,12 +6,69 @@
use App\Models\Conversation;
use App\Models\Message;
use App\Models\Worker;
use App\Models\JobOffer;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
class WorkerMessageController extends Controller
{
public static function processWorkerResponse($conv, $worker, $text)
{
$replyText = strtolower(trim($text));
if ($replyText === 'yes' || $replyText === 'no') {
// Find the last message sent by the employer in this conversation (excluding the current worker message)
$lastEmployerMessage = Message::where('conversation_id', $conv->id)
->where('sender_type', 'employer')
->latest()
->first();
if ($lastEmployerMessage) {
$questionText = strtolower($lastEmployerMessage->text);
$isLookingJobQuestion = str_contains($questionText, 'looking job') ||
str_contains($questionText, 'looking for a job') ||
str_contains($questionText, 'looking for job') ||
str_contains($questionText, 'are you looking');
if ($isLookingJobQuestion) {
if ($replyText === 'yes') {
// S6 Outcome: Update status as Hired
$worker->update([
'status' => 'Hired',
'availability' => 'Hired'
]);
// Accept existing direct offer or application
$offer = JobOffer::where('employer_id', $conv->employer_id)
->where('worker_id', $worker->id)
->first();
if ($offer) {
$offer->update(['status' => 'accepted']);
} else {
JobOffer::create([
'employer_id' => $conv->employer_id,
'worker_id' => $worker->id,
'work_date' => now()->format('Y-m-d'),
'location' => 'Dubai',
'salary' => $worker->salary ?: 2000,
'notes' => 'Hired via chat agreement.',
'status' => 'accepted',
]);
}
} else if ($replyText === 'no') {
// S5 Outcome: Update status as Hidden (Auto-Hidden from search)
$worker->update([
'status' => 'hidden',
'availability' => 'Not Available'
]);
}
}
}
}
}
/**
* Get all conversations for the authorized worker.
*
@ -181,6 +238,9 @@ public function sendMessage(Request $request, $id)
// Touch conversation updated_at for sorting
$conv->touch();
// Process the worker response to update status to Hired or Hidden
self::processWorkerResponse($conv, $worker, $request->text);
});
return response()->json([

View File

@ -6,6 +6,9 @@
use App\Models\JobOffer;
use App\Models\Worker;
use App\Models\WorkerDocument;
use App\Models\ProfileView;
use App\Models\EmployerProfile;
use App\Models\Announcement;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
@ -431,4 +434,140 @@ public function respondToOffer(Request $request, $id)
], 500);
}
}
/**
* Get profile view statistics and employer listing for the worker dashboard.
* GET /api/workers/dashboard/views
*/
public function getProfileViews(Request $request)
{
/** @var Worker $worker */
$worker = $request->attributes->get('worker');
try {
// Get views count
$viewsCount = ProfileView::where('worker_id', $worker->id)->count();
// Get list of employers/sponsors who viewed, with their details
$views = ProfileView::where('worker_id', $worker->id)
->with('employer')
->latest('updated_at')
->get();
$list = $views->map(function ($view) {
$emp = $view->employer;
$empProfile = null;
if ($emp) {
$empProfile = EmployerProfile::where('user_id', $emp->id)->first();
}
return [
'id' => $view->id,
'employer_id' => $view->employer_id,
'employer_name' => $emp->name ?? 'Employer/Sponsor',
'company_name' => $empProfile->company_name ?? 'Private Sponsor',
'nationality' => $empProfile->nationality ?? 'UAE',
'city' => $empProfile->city ?? 'Dubai',
'viewed_at' => $view->updated_at->toIso8601String(),
'viewed_at_formatted' => $view->updated_at->diffForHumans(),
];
});
return response()->json([
'success' => true,
'data' => [
'views_count' => $viewsCount,
'views_list' => $list
]
], 200);
} catch (\Exception $e) {
logger()->error('Worker Dashboard Views API Failure: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'An error occurred while fetching profile views statistics.',
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
], 500);
}
}
/**
* Get consolidated worker dashboard details (Requirement: dashboard api).
* GET /api/workers/dashboard
*/
public function getDashboard(Request $request)
{
/** @var Worker $worker */
$worker = $request->attributes->get('worker');
try {
// 1. Active status
$activeStatus = $worker->status;
// 2. Count of sponsors who viewed the profile
$viewsCount = ProfileView::where('worker_id', $worker->id)->count();
// 3. Currently working sponsor/employer
$currentSponsor = null;
$acceptedOffer = JobOffer::where('worker_id', $worker->id)
->where('status', 'accepted')
->with('employer.employerProfile')
->first();
if ($acceptedOffer && $acceptedOffer->employer) {
$emp = $acceptedOffer->employer;
$empProfile = $emp->employerProfile;
$currentSponsor = [
'id' => $emp->id,
'name' => $emp->name,
'company_name' => $empProfile->company_name ?? 'Private Sponsor',
'nationality' => $empProfile->nationality ?? 'UAE',
'city' => $empProfile->city ?? 'Dubai',
'email' => $emp->email,
'phone' => $emp->phone,
'hired_at' => $acceptedOffer->updated_at->toIso8601String(),
'hired_at_formatted' => $acceptedOffer->updated_at->format('M d, Y'),
];
}
// 4. Latest charity events / announcements list
$charityEvents = Announcement::with('employer.employerProfile')
->latest()
->limit(5)
->get()
->map(function ($event) {
return [
'id' => $event->id,
'title' => $event->title,
'body' => $event->body,
'type' => $event->type,
'employer_name' => $event->employer->name ?? 'System',
'company_name' => $event->employer->employerProfile->company_name ?? 'Migrant Support',
'created_at' => $event->created_at->toIso8601String(),
'time_ago' => $event->created_at->diffForHumans(),
];
});
return response()->json([
'success' => true,
'data' => [
'active_status' => $activeStatus,
'count_sponsors_viewed' => $viewsCount,
'currently_working_sponsor' => $currentSponsor,
'latest_charity_events_list' => $charityEvents
]
], 200);
} catch (\Exception $e) {
logger()->error('Worker Dashboard API Failure: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'An error occurred while loading the worker dashboard.',
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
], 500);
}
}
}

View File

@ -9,6 +9,7 @@
use App\Models\Conversation;
use App\Models\Message;
use App\Models\Worker;
use App\Models\JobOffer;
class MessageController extends Controller
{
@ -36,6 +37,62 @@ private function resolveCurrentUser()
return null;
}
public static function processWorkerResponse($conv, $worker, $text)
{
$replyText = strtolower(trim($text));
if ($replyText === 'yes' || $replyText === 'no') {
// Find the last message sent by the employer in this conversation (excluding the current worker message)
$lastEmployerMessage = Message::where('conversation_id', $conv->id)
->where('sender_type', 'employer')
->latest()
->first();
if ($lastEmployerMessage) {
$questionText = strtolower($lastEmployerMessage->text);
$isLookingJobQuestion = str_contains($questionText, 'looking job') ||
str_contains($questionText, 'looking for a job') ||
str_contains($questionText, 'looking for job') ||
str_contains($questionText, 'are you looking');
if ($isLookingJobQuestion) {
if ($replyText === 'yes') {
// S6 Outcome: Update status as Hired
$worker->update([
'status' => 'Hired',
'availability' => 'Hired'
]);
// Accept existing direct offer or application
$offer = JobOffer::where('employer_id', $conv->employer_id)
->where('worker_id', $worker->id)
->first();
if ($offer) {
$offer->update(['status' => 'accepted']);
} else {
JobOffer::create([
'employer_id' => $conv->employer_id,
'worker_id' => $worker->id,
'work_date' => now()->format('Y-m-d'),
'location' => 'Dubai',
'salary' => $worker->salary ?: 2000,
'notes' => 'Hired via chat agreement.',
'status' => 'accepted',
]);
}
} else if ($replyText === 'no') {
// S5 Outcome: Update status as Hidden (Auto-Hidden from search)
$worker->update([
'status' => 'hidden',
'availability' => 'Not Available'
]);
}
}
}
}
}
public function index(Request $request)
{
$user = $this->resolveCurrentUser();
@ -150,6 +207,32 @@ public function send(Request $request, $id)
// Touch conversation updated_at for sorting
$conv->touch();
// Check if employer sent predefined message "are you looking job?"
$textLower = strtolower($request->text);
if (
str_contains($textLower, 'looking job') ||
str_contains($textLower, 'looking for a job') ||
str_contains($textLower, 'looking for job') ||
str_contains($textLower, 'are you looking')
) {
// Automatically simulate worker response 'Yes' (triggers the S6 hired flow)
$workerResponseText = 'Yes';
// Create the message in database
Message::create([
'conversation_id' => $conv->id,
'sender_type' => 'worker',
'sender_id' => $conv->worker_id,
'text' => $workerResponseText,
]);
// Process the worker response to update status to Hired!
$worker = Worker::find($conv->worker_id);
if ($worker) {
self::processWorkerResponse($conv, $worker, $workerResponseText);
}
}
return back();
}

View File

@ -10,6 +10,8 @@
use App\Models\WorkerCategory;
use App\Models\Shortlist;
use App\Models\JobOffer;
use App\Models\Review;
use App\Models\ProfileView;
class WorkerController extends Controller
{
@ -134,6 +136,15 @@ public function index(Request $request)
public function show($id)
{
$w = Worker::with(['category', 'skills', 'documents'])->findOrFail($id);
// Record employer profile view (Requirement 3)
$user = $this->resolveCurrentUser();
if ($user) {
ProfileView::updateOrCreate(
['employer_id' => $user->id, 'worker_id' => $w->id],
['updated_at' => now()]
);
}
$langs = ($w->id % 2 === 0) ? ['English', 'Arabic'] : ( ($w->id % 3 === 0) ? ['English', 'Tagalog'] : ['English', 'Hindi', 'Arabic'] );
@ -158,25 +169,41 @@ public function show($id)
];
$photo = $photos[$w->id % 4];
$rating = 4.0 + (($w->id * 3) % 10) / 10.0;
$reviewsCount = ($w->id * 4) % 20 + 2;
// Fetch dynamic database reviews (Requirement 1)
$dbReviews = Review::where('worker_id', $w->id)->with('employer')->latest()->get();
$reviews = $dbReviews->map(function ($rev) {
return [
'id' => $rev->id,
'employer_name' => $rev->employer->name ?? 'Employer',
'rating' => $rev->rating,
'date' => $rev->created_at->format('M d, Y'),
'comment' => $rev->comment,
];
})->toArray();
$reviews = [
[
'id' => 1,
'employer_name' => 'Fatima Al Mansoori',
'rating' => 5,
'date' => 'May 10, 2026',
'comment' => 'Extremely reliable and respectful. Professional work and great communication.',
],
[
'id' => 2,
'employer_name' => 'Michael Harrison',
'rating' => 4,
'date' => 'Mar 24, 2026',
'comment' => 'Very punctual, did exactly what was expected. Highly recommend.',
]
];
$reviewsCount = count($reviews);
if ($reviewsCount > 0) {
$rating = round($dbReviews->avg('rating'), 1);
} else {
$rating = 4.0 + (($w->id * 3) % 10) / 10.0;
$reviewsCount = ($w->id * 4) % 20 + 2;
$reviews = [
[
'id' => 1,
'employer_name' => 'Fatima Al Mansoori',
'rating' => 5,
'date' => 'May 10, 2026',
'comment' => 'Extremely reliable and respectful. Professional work and great communication.',
],
[
'id' => 2,
'employer_name' => 'Michael Harrison',
'rating' => 4,
'date' => 'Mar 24, 2026',
'comment' => 'Very punctual, did exactly what was expected. Highly recommend.',
]
];
}
$simDb = Worker::with('category')
->where('id', '!=', $w->id)

View File

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

28
app/Models/Review.php Normal file
View File

@ -0,0 +1,28 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Review extends Model
{
use HasFactory;
protected $fillable = [
'employer_id',
'worker_id',
'rating',
'comment',
];
public function employer()
{
return $this->belongsTo(User::class, 'employer_id');
}
public function worker()
{
return $this->belongsTo(Worker::class, 'worker_id');
}
}

View File

@ -60,4 +60,14 @@ public function offers()
{
return $this->hasMany(JobOffer::class, 'worker_id');
}
public function reviews()
{
return $this->hasMany(Review::class);
}
public function profileViews()
{
return $this->hasMany(ProfileView::class);
}
}

View File

@ -0,0 +1,34 @@
<?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('reviews', function (Blueprint $table) {
$table->id();
$table->foreignId('employer_id')->constrained('users')->onDelete('cascade');
$table->foreignId('worker_id')->constrained('workers')->onDelete('cascade');
$table->integer('rating');
$table->text('comment')->nullable();
$table->timestamps();
// Ensure an employer can only review a worker once
$table->unique(['employer_id', 'worker_id']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('reviews');
}
};

View File

@ -0,0 +1,32 @@
<?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('profile_views', function (Blueprint $table) {
$table->id();
$table->foreignId('employer_id')->constrained('users')->onDelete('cascade');
$table->foreignId('worker_id')->constrained('workers')->onDelete('cascade');
$table->timestamps();
// Unique combination to track unique employer-worker views and update updated_at on duplicate
$table->unique(['employer_id', 'worker_id']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('profile_views');
}
};

View File

@ -2062,6 +2062,318 @@
}
}
}
},
"/workers/dashboard/views": {
"get": {
"tags": [
"Worker/Dashboard"
],
"summary": "Get Worker Profile Views Statistics",
"description": "Allows authenticated workers to see the total count and detailed list of employers/sponsors who viewed their full profile.",
"responses": {
"200": {
"description": "Profile views statistics retrieved successfully.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": true
},
"data": {
"type": "object",
"properties": {
"views_count": {
"type": "integer",
"example": 5
},
"views_list": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"example": 1
},
"employer_id": {
"type": "integer",
"example": 2
},
"employer_name": {
"type": "string",
"example": "Ahmad"
},
"company_name": {
"type": "string",
"example": "Ahmad Tech Ltd"
},
"nationality": {
"type": "string",
"example": "UAE"
},
"city": {
"type": "string",
"example": "Dubai"
},
"viewed_at": {
"type": "string",
"format": "date-time",
"example": "2026-06-01T15:10:24.000000Z"
},
"viewed_at_formatted": {
"type": "string",
"example": "2 minutes ago"
}
}
}
}
}
}
}
}
}
}
}
}
}
},
"/workers/dashboard": {
"get": {
"tags": [
"Worker/Dashboard"
],
"summary": "Get Worker Consolidated Dashboard Data",
"description": "Allows authenticated workers to retrieve their dashboard metrics: active status, profile views count, currently working sponsor/employer details, and latest charity events/announcements.",
"responses": {
"200": {
"description": "Worker dashboard details retrieved successfully.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": true
},
"data": {
"type": "object",
"properties": {
"active_status": {
"type": "string",
"example": "active"
},
"count_sponsors_viewed": {
"type": "integer",
"example": 5
},
"currently_working_sponsor": {
"type": "object",
"nullable": true,
"properties": {
"id": {
"type": "integer",
"example": 2
},
"name": {
"type": "string",
"example": "Ahmad"
},
"company_name": {
"type": "string",
"example": "Ahmad Tech Ltd"
},
"nationality": {
"type": "string",
"example": "UAE"
},
"city": {
"type": "string",
"example": "Dubai"
},
"email": {
"type": "string",
"example": "ahmad@example.com"
},
"phone": {
"type": "string",
"example": "+971509990001"
},
"hired_at": {
"type": "string",
"format": "date-time",
"example": "2026-06-01T15:10:24.000000Z"
},
"hired_at_formatted": {
"type": "string",
"example": "Jun 01, 2026"
}
}
},
"latest_charity_events_list": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"example": 1
},
"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"
}
}
}
}
}
}
}
}
}
}
}
}
}
},
"/employers/reviews": {
"post": {
"tags": [
"Employer/Reviews"
],
"summary": "Add or Update Worker Review",
"description": "Allows employers to submit a review score and description comment for a worker. If the employer has already left a review, this will update it.",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"worker_id",
"rating"
],
"properties": {
"worker_id": {
"type": "integer",
"example": 136,
"description": "Worker ID being reviewed."
},
"rating": {
"type": "integer",
"minimum": 1,
"maximum": 5,
"example": 5,
"description": "Review score from 1 to 5."
},
"comment": {
"type": "string",
"example": "Highly professional, punctual and did a great job!",
"description": "Optional detailed comment."
}
}
}
}
}
},
"responses": {
"201": {
"description": "Review added successfully."
},
"200": {
"description": "Review updated successfully."
},
"422": {
"description": "Validation error."
}
}
}
},
"/employers/reviews/{id}": {
"put": {
"tags": [
"Employer/Reviews"
],
"summary": "Edit Worker Review",
"description": "Allows employers to edit their previously created review score and comment by review ID.",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"description": "Review ID reference.",
"schema": {
"type": "integer"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"rating"
],
"properties": {
"rating": {
"type": "integer",
"minimum": 1,
"maximum": 5,
"example": 4,
"description": "Updated rating score from 1 to 5."
},
"comment": {
"type": "string",
"example": "Updated feedback comment.",
"description": "Updated review comment description."
}
}
}
}
}
},
"responses": {
"200": {
"description": "Review edited successfully."
},
"404": {
"description": "Review not found or unauthorized."
},
"422": {
"description": "Validation error."
}
}
}
}
},
"components": {

View File

@ -77,6 +77,8 @@ export default function Show({ conversation, initialMessages, conversations = []
setMessages(prev => [...prev, newMsg]);
setInput('');
const isLookingJobQuery = text.toLowerCase().includes('looking') && text.toLowerCase().includes('job');
router.post(`/employer/messages/${conversation.id}/send`, {
text: text
}, {
@ -93,18 +95,23 @@ export default function Show({ conversation, initialMessages, conversations = []
setIsTyping(true);
setTimeout(() => {
setIsTyping(false);
const autoReply = {
id: Date.now() + 1,
sender: 'worker',
text: t('auto_reply_text', "Thank you for your response! I have reviewed your offer proposal and am looking forward to our video interview."),
time: t('just_now', 'Just Now')
};
setMessages(prev => [...prev, autoReply]);
// Pop up a clear message received notification for the sponsor
toast.info(t('new_message_from', 'New message from {name}!').replace('{name}', conversation.worker_name), {
description: autoReply.text,
duration: 5000
// Reload conversation to pull actual database messages (including automated worker 'Yes')
router.reload({
preserveScroll: true,
onSuccess: () => {
if (isLookingJobQuery) {
toast.success(t('candidate_hired_success', "Candidate response: YES! Automatically marked as HIRED!"), {
description: t('candidate_hired_desc', "{name} has been successfully added to your Hired candidate pipeline.").replace('{name}', conversation.worker_name),
duration: 6000,
});
} else {
toast.info(t('new_message_from', 'New message from {name}!').replace('{name}', conversation.worker_name), {
description: t('auto_reply_text', "Thank you for your response! I have reviewed your offer proposal and am looking forward to our video interview."),
duration: 5000
});
}
}
});
}, 2000);
}, 1500);
@ -136,6 +143,7 @@ export default function Show({ conversation, initialMessages, conversations = []
};
const chips = [
t('chip_looking_job', "Are you looking for a job?"),
t('chip_interview', "Are you available for a video interview today?"),
t('chip_salary', "What is your final expected salary?"),
t('chip_visa', "Can you transfer your visa immediately?")

View File

@ -9,6 +9,7 @@
use App\Http\Controllers\Api\EmployerMessageController;
use App\Http\Controllers\Api\EmployerAnnouncementController;
use App\Http\Controllers\Api\EmployerProfileController;
use App\Http\Controllers\Api\EmployerReviewController;
/*
|--------------------------------------------------------------------------
@ -51,6 +52,8 @@
Route::post('/workers/go-live', [WorkerProfileController::class, 'goLive']);
Route::post('/workers/profile/toggle-availability', [WorkerProfileController::class, 'toggleAvailability']);
Route::post('/workers/profile/mark-hired', [WorkerProfileController::class, 'markHired']);
Route::get('/workers/dashboard/views', [WorkerProfileController::class, 'getProfileViews']);
Route::get('/workers/dashboard', [WorkerProfileController::class, 'getDashboard']);
// Job Offers Management
Route::get('/workers/offers', [WorkerProfileController::class, 'getOffers']);
@ -91,4 +94,8 @@
// Payment History Management
Route::get('/employers/payments', [\App\Http\Controllers\Api\EmployerPaymentController::class, 'getPayments']);
// Review Management
Route::post('/employers/reviews', [EmployerReviewController::class, 'addReview']);
Route::put('/employers/reviews/{id}', [EmployerReviewController::class, 'editReview']);
});