review, dashboard api
This commit is contained in:
parent
b60b14fb9d
commit
eb75dca482
@ -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([
|
||||
|
||||
144
app/Http/Controllers/Api/EmployerReviewController.php
Normal file
144
app/Http/Controllers/Api/EmployerReviewController.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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')
|
||||
|
||||
@ -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([
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
26
app/Models/ProfileView.php
Normal file
26
app/Models/ProfileView.php
Normal 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
28
app/Models/Review.php
Normal 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');
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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');
|
||||
}
|
||||
};
|
||||
@ -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');
|
||||
}
|
||||
};
|
||||
@ -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": {
|
||||
|
||||
@ -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?")
|
||||
|
||||
@ -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']);
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user