mohan #25
@ -69,3 +69,11 @@ EMIRATES_BACK_API_URL=
|
|||||||
SPONSOR_LICENSE_API_URL=
|
SPONSOR_LICENSE_API_URL=
|
||||||
|
|
||||||
VITE_APP_NAME="${APP_NAME}"
|
VITE_APP_NAME="${APP_NAME}"
|
||||||
|
|
||||||
|
# Notification Reminder Settings
|
||||||
|
WORKER_NO_RESPONSE_REMINDER_DAYS=14
|
||||||
|
REVIEW_ELIGIBILITY_MONTHS=3
|
||||||
|
REVIEW_EDIT_PERIOD_DAYS=7
|
||||||
|
EMPLOYER_HIRE_CONFIRM_REMINDER_DAYS=3
|
||||||
|
WORKER_JOIN_CONFIRM_REMINDER_DAYS=3
|
||||||
|
DOCUMENT_EXPIRY_REMINDER_DAYS=30,7,1
|
||||||
|
|||||||
@ -46,17 +46,22 @@ public function handle()
|
|||||||
$this->info('6. Checking Pending Direct Job Offers...');
|
$this->info('6. Checking Pending Direct Job Offers...');
|
||||||
$this->checkJobOffers();
|
$this->checkJobOffers();
|
||||||
|
|
||||||
|
$this->info('7. Checking Worker No Response...');
|
||||||
|
$this->checkWorkerNoResponse();
|
||||||
|
|
||||||
|
$this->info('8. Checking Review Reminders...');
|
||||||
|
$this->checkReviewReminders();
|
||||||
|
|
||||||
$this->info('Automated reminder system run complete!');
|
$this->info('Automated reminder system run complete!');
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getReminderLevel(int $days): ?string
|
private function getReminderLevel(int $days, string $configKey, string $default): ?string
|
||||||
{
|
{
|
||||||
if ($days === 7) {
|
$value = config($configKey, $default);
|
||||||
return '7_days';
|
$configuredDays = array_map('intval', array_filter(explode(',', $value), 'strlen'));
|
||||||
} elseif ($days === 3) {
|
|
||||||
return '3_days';
|
if (in_array($days, $configuredDays)) {
|
||||||
} elseif ($days === 1) {
|
return $days === 1 ? '1_day' : "{$days}_days";
|
||||||
return '1_day';
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -96,7 +101,7 @@ private function checkWorkerDocuments()
|
|||||||
$expiry = Carbon::parse($doc->expiry_date);
|
$expiry = Carbon::parse($doc->expiry_date);
|
||||||
$days = (int) now()->startOfDay()->diffInDays($expiry->startOfDay(), false);
|
$days = (int) now()->startOfDay()->diffInDays($expiry->startOfDay(), false);
|
||||||
|
|
||||||
$level = $this->getReminderLevel($days);
|
$level = $this->getReminderLevel($days, 'reminders.document_expiry_reminder_days', '30,7,3,1');
|
||||||
if (!$level) continue;
|
if (!$level) continue;
|
||||||
|
|
||||||
if ($this->alreadySent($worker, 'document_expiry', $doc, $level)) continue;
|
if ($this->alreadySent($worker, 'document_expiry', $doc, $level)) continue;
|
||||||
@ -123,7 +128,7 @@ private function checkEmployerProfiles()
|
|||||||
$expiry = Carbon::parse($profile->emirates_id_expiry);
|
$expiry = Carbon::parse($profile->emirates_id_expiry);
|
||||||
$days = (int) now()->startOfDay()->diffInDays($expiry->startOfDay(), false);
|
$days = (int) now()->startOfDay()->diffInDays($expiry->startOfDay(), false);
|
||||||
|
|
||||||
$level = $this->getReminderLevel($days);
|
$level = $this->getReminderLevel($days, 'reminders.document_expiry_reminder_days', '30,7,3,1');
|
||||||
if (!$level) continue;
|
if (!$level) continue;
|
||||||
|
|
||||||
if ($this->alreadySent($user, 'document_expiry', $profile, $level)) continue;
|
if ($this->alreadySent($user, 'document_expiry', $profile, $level)) continue;
|
||||||
@ -149,7 +154,7 @@ private function checkSponsors()
|
|||||||
$expiry = Carbon::parse($sponsor->emirates_id_expiry);
|
$expiry = Carbon::parse($sponsor->emirates_id_expiry);
|
||||||
$days = (int) now()->startOfDay()->diffInDays($expiry->startOfDay(), false);
|
$days = (int) now()->startOfDay()->diffInDays($expiry->startOfDay(), false);
|
||||||
|
|
||||||
$level = $this->getReminderLevel($days);
|
$level = $this->getReminderLevel($days, 'reminders.document_expiry_reminder_days', '30,7,3,1');
|
||||||
if ($level && !$this->alreadySent($sponsor, 'document_expiry_emirates', $sponsor, $level)) {
|
if ($level && !$this->alreadySent($sponsor, 'document_expiry_emirates', $sponsor, $level)) {
|
||||||
$this->info("Sending {$level} reminder to sponsor {$sponsor->full_name} for Emirates ID expiry.");
|
$this->info("Sending {$level} reminder to sponsor {$sponsor->full_name} for Emirates ID expiry.");
|
||||||
$sponsor->notify(new \App\Notifications\DocumentExpiryNotification(
|
$sponsor->notify(new \App\Notifications\DocumentExpiryNotification(
|
||||||
@ -167,7 +172,7 @@ private function checkSponsors()
|
|||||||
$expiry = Carbon::parse($sponsor->license_expiry);
|
$expiry = Carbon::parse($sponsor->license_expiry);
|
||||||
$days = (int) now()->startOfDay()->diffInDays($expiry->startOfDay(), false);
|
$days = (int) now()->startOfDay()->diffInDays($expiry->startOfDay(), false);
|
||||||
|
|
||||||
$level = $this->getReminderLevel($days);
|
$level = $this->getReminderLevel($days, 'reminders.document_expiry_reminder_days', '30,7,3,1');
|
||||||
if ($level && !$this->alreadySent($sponsor, 'document_expiry_license', $sponsor, $level)) {
|
if ($level && !$this->alreadySent($sponsor, 'document_expiry_license', $sponsor, $level)) {
|
||||||
$this->info("Sending {$level} reminder to sponsor {$sponsor->full_name} for License expiry.");
|
$this->info("Sending {$level} reminder to sponsor {$sponsor->full_name} for License expiry.");
|
||||||
$sponsor->notify(new \App\Notifications\DocumentExpiryNotification(
|
$sponsor->notify(new \App\Notifications\DocumentExpiryNotification(
|
||||||
@ -199,7 +204,7 @@ private function checkHireConfirmations()
|
|||||||
$startDate = Carbon::parse($job->start_date);
|
$startDate = Carbon::parse($job->start_date);
|
||||||
$days = (int) now()->startOfDay()->diffInDays($startDate->startOfDay(), false);
|
$days = (int) now()->startOfDay()->diffInDays($startDate->startOfDay(), false);
|
||||||
|
|
||||||
$level = $this->getReminderLevel($days);
|
$level = $this->getReminderLevel($days, 'reminders.employer_hire_confirm_reminder_days', '3,7');
|
||||||
if (!$level) continue;
|
if (!$level) continue;
|
||||||
|
|
||||||
if ($this->alreadySent($employer, 'hire_confirmation', $app, $level)) continue;
|
if ($this->alreadySent($employer, 'hire_confirmation', $app, $level)) continue;
|
||||||
@ -235,7 +240,7 @@ private function checkJoiningConfirmations()
|
|||||||
$startDate = Carbon::parse($job->start_date);
|
$startDate = Carbon::parse($job->start_date);
|
||||||
$days = (int) now()->startOfDay()->diffInDays($startDate->startOfDay(), false);
|
$days = (int) now()->startOfDay()->diffInDays($startDate->startOfDay(), false);
|
||||||
|
|
||||||
$level = $this->getReminderLevel($days);
|
$level = $this->getReminderLevel($days, 'reminders.worker_join_confirm_reminder_days', '3,7');
|
||||||
if (!$level) continue;
|
if (!$level) continue;
|
||||||
|
|
||||||
if ($this->alreadySent($worker, 'joining_confirmation', $app, $level)) continue;
|
if ($this->alreadySent($worker, 'joining_confirmation', $app, $level)) continue;
|
||||||
@ -269,7 +274,7 @@ private function checkJobOffers()
|
|||||||
$workDate = Carbon::parse($offer->work_date);
|
$workDate = Carbon::parse($offer->work_date);
|
||||||
$days = (int) now()->startOfDay()->diffInDays($workDate->startOfDay(), false);
|
$days = (int) now()->startOfDay()->diffInDays($workDate->startOfDay(), false);
|
||||||
|
|
||||||
$level = $this->getReminderLevel($days);
|
$level = $this->getReminderLevel($days, 'reminders.worker_join_confirm_reminder_days', '3,7');
|
||||||
if (!$level) continue;
|
if (!$level) continue;
|
||||||
|
|
||||||
if ($this->alreadySent($worker, 'pending_confirmation', $offer, $level)) continue;
|
if ($this->alreadySent($worker, 'pending_confirmation', $offer, $level)) continue;
|
||||||
@ -288,4 +293,166 @@ private function checkJobOffers()
|
|||||||
$this->logReminder($worker, 'pending_confirmation', $offer, $level, 'all');
|
$this->logReminder($worker, 'pending_confirmation', $offer, $level, 'all');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function checkWorkerNoResponse()
|
||||||
|
{
|
||||||
|
$noResponseDays = (int) config('reminders.worker_no_response_reminder_days', 14);
|
||||||
|
|
||||||
|
// 1. Pending direct Job Offers
|
||||||
|
$offers = \App\Models\JobOffer::where('status', 'pending')
|
||||||
|
->with(['worker', 'employer'])
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($offers as $offer) {
|
||||||
|
$worker = $offer->worker;
|
||||||
|
if (!$worker) continue;
|
||||||
|
$employer = $offer->employer;
|
||||||
|
$employerName = $employer ? $employer->name : 'Employer';
|
||||||
|
|
||||||
|
$daysSinceCreation = (int) Carbon::parse($offer->created_at)->startOfDay()->diffInDays(now()->startOfDay(), false);
|
||||||
|
|
||||||
|
if ($daysSinceCreation !== $noResponseDays) continue;
|
||||||
|
|
||||||
|
$level = $daysSinceCreation === 1 ? '1_day' : "{$daysSinceCreation}_days";
|
||||||
|
|
||||||
|
if ($this->alreadySent($worker, 'worker_no_response_offer', $offer, $level)) continue;
|
||||||
|
|
||||||
|
$this->info("Sending no-response reminder to worker {$worker->name} for pending direct offer from {$employerName}.");
|
||||||
|
$worker->notify(new \App\Notifications\WorkerNoResponseNotification(
|
||||||
|
'worker_no_response_offer',
|
||||||
|
"Action Required: Pending Job Offer",
|
||||||
|
"You have a pending job offer from {$employerName} sent on {$offer->created_at->toDateString()} that you have not responded to.",
|
||||||
|
$daysSinceCreation,
|
||||||
|
get_class($offer),
|
||||||
|
$offer->id
|
||||||
|
));
|
||||||
|
|
||||||
|
$this->logReminder($worker, 'worker_no_response_offer', $offer, $level, 'all');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Chat conversations where the last message is from the employer
|
||||||
|
$conversations = \App\Models\Conversation::with(['employer', 'worker'])->get();
|
||||||
|
|
||||||
|
foreach ($conversations as $conv) {
|
||||||
|
$worker = $conv->worker;
|
||||||
|
if (!$worker) continue;
|
||||||
|
$employer = $conv->employer;
|
||||||
|
$employerName = $employer ? $employer->name : 'Employer';
|
||||||
|
|
||||||
|
$lastMessage = $conv->messages()->latest()->first();
|
||||||
|
if (!$lastMessage) continue;
|
||||||
|
|
||||||
|
$isEmployerSender = false;
|
||||||
|
if ($lastMessage->sender_type === 'App\Models\User' || $lastMessage->sender_id == $conv->employer_id) {
|
||||||
|
$isEmployerSender = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$isEmployerSender) continue;
|
||||||
|
|
||||||
|
$daysSinceMessage = (int) Carbon::parse($lastMessage->created_at)->startOfDay()->diffInDays(now()->startOfDay(), false);
|
||||||
|
|
||||||
|
if ($daysSinceMessage !== $noResponseDays) continue;
|
||||||
|
|
||||||
|
$level = $daysSinceMessage === 1 ? '1_day' : "{$daysSinceMessage}_days";
|
||||||
|
|
||||||
|
if ($this->alreadySent($worker, 'worker_no_response_chat', $lastMessage, $level)) continue;
|
||||||
|
|
||||||
|
$this->info("Sending no-response chat reminder to worker {$worker->name} for conversation with {$employerName}.");
|
||||||
|
$worker->notify(new \App\Notifications\WorkerNoResponseNotification(
|
||||||
|
'worker_no_response_chat',
|
||||||
|
"New Messages: Reply to {$employerName}",
|
||||||
|
"You have a message from {$employerName} that you have not responded to for {$daysSinceMessage} days.",
|
||||||
|
$daysSinceMessage,
|
||||||
|
get_class($lastMessage),
|
||||||
|
$lastMessage->id
|
||||||
|
));
|
||||||
|
|
||||||
|
$this->logReminder($worker, 'worker_no_response_chat', $lastMessage, $level, 'all');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function checkReviewReminders()
|
||||||
|
{
|
||||||
|
$eligibilityMonths = (int) config('reminders.review_eligibility_months', 3);
|
||||||
|
$level = "{$eligibilityMonths}_months";
|
||||||
|
|
||||||
|
// 1. Standard Job Applications with status 'hired'
|
||||||
|
$applications = \App\Models\JobApplication::where('status', 'hired')
|
||||||
|
->whereNotNull('joining_confirmed_at')
|
||||||
|
->with(['jobPost.employer', 'worker'])
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($applications as $app) {
|
||||||
|
$job = $app->jobPost;
|
||||||
|
if (!$job) continue;
|
||||||
|
$employer = $job->employer;
|
||||||
|
$worker = $app->worker;
|
||||||
|
if (!$employer || !$worker) continue;
|
||||||
|
|
||||||
|
$joiningDate = Carbon::parse($app->joining_confirmed_at);
|
||||||
|
$months = $joiningDate->startOfDay()->diffInMonths(now()->startOfDay(), false);
|
||||||
|
|
||||||
|
if ($months >= $eligibilityMonths) {
|
||||||
|
// Remind worker to review employer
|
||||||
|
if (!$this->alreadySent($worker, 'review_employer_reminder', $app, $level)) {
|
||||||
|
$this->info("Sending review reminder to worker {$worker->name} to review employer {$employer->name}.");
|
||||||
|
$worker->notify(new \App\Notifications\WorkerReviewReminderNotification(
|
||||||
|
$employer->name,
|
||||||
|
$app->id,
|
||||||
|
$level
|
||||||
|
));
|
||||||
|
$this->logReminder($worker, 'review_employer_reminder', $app, $level, 'all');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remind employer to review worker
|
||||||
|
if (!$this->alreadySent($employer, 'review_worker_reminder', $app, $level)) {
|
||||||
|
$this->info("Sending review reminder to employer {$employer->name} to review worker {$worker->name}.");
|
||||||
|
$employer->notify(new \App\Notifications\EmployerReviewReminderNotification(
|
||||||
|
$worker->name,
|
||||||
|
$app->id,
|
||||||
|
$level
|
||||||
|
));
|
||||||
|
$this->logReminder($employer, 'review_worker_reminder', $app, $level, 'all');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Direct Job Offers with status 'accepted'
|
||||||
|
$offers = \App\Models\JobOffer::where('status', 'accepted')
|
||||||
|
->with(['worker', 'employer'])
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($offers as $offer) {
|
||||||
|
$worker = $offer->worker;
|
||||||
|
$employer = $offer->employer;
|
||||||
|
if (!$worker || !$employer) continue;
|
||||||
|
|
||||||
|
$joiningDate = $offer->work_date ? Carbon::parse($offer->work_date) : $offer->updated_at;
|
||||||
|
$months = $joiningDate->startOfDay()->diffInMonths(now()->startOfDay(), false);
|
||||||
|
|
||||||
|
if ($months >= $eligibilityMonths) {
|
||||||
|
// Remind worker to review employer
|
||||||
|
if (!$this->alreadySent($worker, 'review_employer_reminder', $offer, $level)) {
|
||||||
|
$this->info("Sending review reminder to worker {$worker->name} to review employer {$employer->name} (direct offer).");
|
||||||
|
$worker->notify(new \App\Notifications\WorkerReviewReminderNotification(
|
||||||
|
$employer->name,
|
||||||
|
$offer->id,
|
||||||
|
$level
|
||||||
|
));
|
||||||
|
$this->logReminder($worker, 'review_employer_reminder', $offer, $level, 'all');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remind employer to review worker
|
||||||
|
if (!$this->alreadySent($employer, 'review_worker_reminder', $offer, $level)) {
|
||||||
|
$this->info("Sending review reminder to employer {$employer->name} to review worker {$worker->name} (direct offer).");
|
||||||
|
$employer->notify(new \App\Notifications\EmployerReviewReminderNotification(
|
||||||
|
$worker->name,
|
||||||
|
$offer->id,
|
||||||
|
$level
|
||||||
|
));
|
||||||
|
$this->logReminder($employer, 'review_worker_reminder', $offer, $level, 'all');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -132,7 +132,7 @@ public function updateProfile(Request $request, $id)
|
|||||||
'experience' => 'required|string',
|
'experience' => 'required|string',
|
||||||
'salary' => 'nullable|numeric',
|
'salary' => 'nullable|numeric',
|
||||||
'nationality' => 'nullable|string',
|
'nationality' => 'nullable|string',
|
||||||
'visa_status' => 'nullable|string',
|
'visa_status' => 'nullable|string|in:Tourist Visa,Employment Visa,Residence Visa',
|
||||||
'age' => 'nullable|integer',
|
'age' => 'nullable|integer',
|
||||||
'in_country' => 'nullable',
|
'in_country' => 'nullable',
|
||||||
'preferred_job_type' => 'nullable|string',
|
'preferred_job_type' => 'nullable|string',
|
||||||
|
|||||||
113
app/Http/Controllers/Api/EmployerNotificationController.php
Normal file
113
app/Http/Controllers/Api/EmployerNotificationController.php
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
class EmployerNotificationController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* List all notifications for the employer.
|
||||||
|
* GET /api/employers/notifications
|
||||||
|
*/
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
/** @var User $employer */
|
||||||
|
$employer = $request->attributes->get('employer');
|
||||||
|
|
||||||
|
$filter = $request->query('filter', 'all');
|
||||||
|
$perPage = (int) $request->query('per_page', 15);
|
||||||
|
|
||||||
|
if ($filter === 'read') {
|
||||||
|
$query = $employer->readNotifications();
|
||||||
|
} elseif ($filter === 'unread') {
|
||||||
|
$query = $employer->unreadNotifications();
|
||||||
|
} else {
|
||||||
|
$query = $employer->notifications();
|
||||||
|
}
|
||||||
|
|
||||||
|
$paginator = $query->paginate($perPage);
|
||||||
|
|
||||||
|
$notifications = collect($paginator->items())->map(function ($notification) {
|
||||||
|
return [
|
||||||
|
'id' => $notification->id,
|
||||||
|
'type' => $notification->type,
|
||||||
|
'data' => $notification->data,
|
||||||
|
'read_at' => $notification->read_at ? $notification->read_at->toIso8601String() : null,
|
||||||
|
'created_at' => $notification->created_at->toIso8601String(),
|
||||||
|
'time_ago' => $notification->created_at->diffForHumans(),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
'notifications' => $notifications,
|
||||||
|
'pagination' => [
|
||||||
|
'total' => $paginator->total(),
|
||||||
|
'per_page' => $paginator->perPage(),
|
||||||
|
'current_page' => $paginator->currentPage(),
|
||||||
|
'last_page' => $paginator->lastPage(),
|
||||||
|
]
|
||||||
|
]
|
||||||
|
], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark a single notification or all unread notifications as read.
|
||||||
|
* PUT /api/employers/notifications/{id}/read or PUT /api/employers/notifications/read
|
||||||
|
*/
|
||||||
|
public function markAsRead(Request $request, $id = null)
|
||||||
|
{
|
||||||
|
/** @var User $employer */
|
||||||
|
$employer = $request->attributes->get('employer');
|
||||||
|
|
||||||
|
if ($id) {
|
||||||
|
$notification = $employer->notifications()->where('id', $id)->first();
|
||||||
|
if (!$notification) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Notification not found.'
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
$notification->markAsRead();
|
||||||
|
} else {
|
||||||
|
$employer->unreadNotifications->markAsRead();
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Notification(s) marked as read successfully.'
|
||||||
|
], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a single notification or all notifications.
|
||||||
|
* DELETE /api/employers/notifications/{id} or DELETE /api/employers/notifications
|
||||||
|
*/
|
||||||
|
public function destroy(Request $request, $id = null)
|
||||||
|
{
|
||||||
|
/** @var User $employer */
|
||||||
|
$employer = $request->attributes->get('employer');
|
||||||
|
|
||||||
|
if ($id) {
|
||||||
|
$notification = $employer->notifications()->where('id', $id)->first();
|
||||||
|
if (!$notification) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Notification not found.'
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
$notification->delete();
|
||||||
|
} else {
|
||||||
|
$employer->notifications()->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Notification(s) deleted successfully.'
|
||||||
|
], 200);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -35,12 +35,65 @@ public function addReview(Request $request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 1. Validate confirmed hire and completed joining, and review eligibility period
|
||||||
|
$hasConfirmedHire = false;
|
||||||
|
$joiningDate = null;
|
||||||
|
|
||||||
|
// Check standard JobApplication where the job belongs to this employer and worker matches
|
||||||
|
$app = \App\Models\JobApplication::where('worker_id', $request->worker_id)
|
||||||
|
->where('status', 'hired')
|
||||||
|
->whereNotNull('joining_confirmed_at')
|
||||||
|
->whereHas('jobPost', function ($query) use ($employer) {
|
||||||
|
$query->where('employer_id', $employer->id);
|
||||||
|
})
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($app) {
|
||||||
|
$hasConfirmedHire = true;
|
||||||
|
$joiningDate = \Carbon\Carbon::parse($app->joining_confirmed_at);
|
||||||
|
} else {
|
||||||
|
// Check direct JobOffer
|
||||||
|
$offer = \App\Models\JobOffer::where('worker_id', $request->worker_id)
|
||||||
|
->where('employer_id', $employer->id)
|
||||||
|
->where('status', 'accepted')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($offer) {
|
||||||
|
$hasConfirmedHire = true;
|
||||||
|
$joiningDate = $offer->work_date ? \Carbon\Carbon::parse($offer->work_date) : $offer->updated_at;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$hasConfirmedHire) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'You can only review workers with a confirmed hire and completed joining.'
|
||||||
|
], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$eligibilityMonths = (int) config('reminders.review_eligibility_months', 3);
|
||||||
|
if ($joiningDate->startOfDay()->diffInMonths(now()->startOfDay(), false) < $eligibilityMonths) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => "You can write a review only after {$eligibilityMonths} months from the joining date."
|
||||||
|
], 403);
|
||||||
|
}
|
||||||
|
|
||||||
// Check if employer has already reviewed this worker
|
// Check if employer has already reviewed this worker
|
||||||
$existingReview = Review::where('employer_id', $employer->id)
|
$existingReview = Review::where('employer_id', $employer->id)
|
||||||
->where('worker_id', $request->worker_id)
|
->where('worker_id', $request->worker_id)
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
if ($existingReview) {
|
if ($existingReview) {
|
||||||
|
// Check if the edit window has expired
|
||||||
|
$editPeriodDays = (int) config('reminders.review_edit_period_days', 7);
|
||||||
|
if ($existingReview->created_at->addDays($editPeriodDays)->isPast()) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => "The {$editPeriodDays}-day edit window for this review has expired."
|
||||||
|
], 403);
|
||||||
|
}
|
||||||
|
|
||||||
// If they already have a review, we can update it or direct them to the edit endpoint.
|
// 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).
|
// To be robust, let's update it directly and inform them it was updated (acting as an edit).
|
||||||
$existingReview->update([
|
$existingReview->update([
|
||||||
@ -118,6 +171,15 @@ public function editReview(Request $request, $id)
|
|||||||
], 404);
|
], 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the edit window has expired
|
||||||
|
$editPeriodDays = (int) config('reminders.review_edit_period_days', 7);
|
||||||
|
if ($review->created_at->addDays($editPeriodDays)->isPast()) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => "The {$editPeriodDays}-day edit window for this review has expired."
|
||||||
|
], 403);
|
||||||
|
}
|
||||||
|
|
||||||
$review->update([
|
$review->update([
|
||||||
'rating' => $request->rating,
|
'rating' => $request->rating,
|
||||||
'comment' => $request->comment,
|
'comment' => $request->comment,
|
||||||
|
|||||||
@ -944,8 +944,8 @@ public function getWorkerDetail(Request $request, $id)
|
|||||||
];
|
];
|
||||||
|
|
||||||
// Visa status
|
// Visa status
|
||||||
$visaStatusesList = ['Residence Visa', 'Tourist Visa', 'Employment Visa', 'Cancelled Visa', 'Own Visa'];
|
$visaStatusesList = ['Residence Visa', 'Tourist Visa', 'Employment Visa'];
|
||||||
$visaStatus = $visaStatusesList[$w->id % 5];
|
$visaStatus = $visaStatusesList[$w->id % 3];
|
||||||
|
|
||||||
$photo = null;
|
$photo = null;
|
||||||
|
|
||||||
|
|||||||
@ -238,6 +238,9 @@ public function postCharityEvent(Request $request)
|
|||||||
'provided_items' => 'required|string',
|
'provided_items' => 'required|string',
|
||||||
'location_details' => 'required|string',
|
'location_details' => 'required|string',
|
||||||
'location_pin' => 'required|string|url',
|
'location_pin' => 'required|string|url',
|
||||||
|
'contact_person_name' => 'required|string|max:255',
|
||||||
|
'contact_number' => 'required|string|regex:/^\d{7,15}$/',
|
||||||
|
'country_code' => 'required|string|regex:/^\+\d{1,4}$/',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($validator->fails()) {
|
if ($validator->fails()) {
|
||||||
@ -259,6 +262,9 @@ public function postCharityEvent(Request $request)
|
|||||||
'location_details' => $request->location_details,
|
'location_details' => $request->location_details,
|
||||||
'location_pin' => $request->location_pin,
|
'location_pin' => $request->location_pin,
|
||||||
'content' => $request->body,
|
'content' => $request->body,
|
||||||
|
'contact_person_name' => $request->contact_person_name,
|
||||||
|
'contact_number' => $request->contact_number,
|
||||||
|
'country_code' => $request->country_code,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$event = Announcement::create([
|
$event = Announcement::create([
|
||||||
|
|||||||
@ -318,7 +318,7 @@ public function register(Request $request)
|
|||||||
'age' => 'nullable|integer',
|
'age' => 'nullable|integer',
|
||||||
'experience' => 'nullable|string|max:100',
|
'experience' => 'nullable|string|max:100',
|
||||||
'in_country' => 'nullable',
|
'in_country' => 'nullable',
|
||||||
'visa_status' => 'nullable|string|max:100',
|
'visa_status' => 'nullable|string|in:Tourist Visa,Employment Visa,Residence Visa',
|
||||||
'preferred_job_type' => 'nullable|string|max:100',
|
'preferred_job_type' => 'nullable|string|max:100',
|
||||||
'main_profession' => 'nullable|string|max:100',
|
'main_profession' => 'nullable|string|max:100',
|
||||||
'gender' => 'nullable|string|in:male,female,other',
|
'gender' => 'nullable|string|in:male,female,other',
|
||||||
@ -1247,6 +1247,19 @@ private function normaliseDateForController(?string $raw): ?string
|
|||||||
private function cleanVisaData(array $visaDataInput): array
|
private function cleanVisaData(array $visaDataInput): array
|
||||||
{
|
{
|
||||||
$rawVisaType = $visaDataInput['visa_type'] ?? null;
|
$rawVisaType = $visaDataInput['visa_type'] ?? null;
|
||||||
|
if ($rawVisaType) {
|
||||||
|
$normalized = strtolower(trim($rawVisaType));
|
||||||
|
if (str_contains($normalized, 'residence')) {
|
||||||
|
$rawVisaType = 'Residence Visa';
|
||||||
|
} elseif (str_contains($normalized, 'employment')) {
|
||||||
|
$rawVisaType = 'Employment Visa';
|
||||||
|
} else {
|
||||||
|
$rawVisaType = 'Tourist Visa';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$rawVisaType = 'Tourist Visa';
|
||||||
|
}
|
||||||
|
|
||||||
$rawEntryPermitNo = $visaDataInput['entry_permit_no'] ?? $visaDataInput['file_number'] ?? $visaDataInput['id_number'] ?? $visaDataInput['number'] ?? null;
|
$rawEntryPermitNo = $visaDataInput['entry_permit_no'] ?? $visaDataInput['file_number'] ?? $visaDataInput['id_number'] ?? $visaDataInput['number'] ?? null;
|
||||||
$rawIssueDate = $visaDataInput['issue_date'] ?? null;
|
$rawIssueDate = $visaDataInput['issue_date'] ?? null;
|
||||||
$rawValidUntil = $visaDataInput['valid_until'] ?? $visaDataInput['expiry_date'] ?? null;
|
$rawValidUntil = $visaDataInput['valid_until'] ?? $visaDataInput['expiry_date'] ?? null;
|
||||||
@ -1277,7 +1290,7 @@ private function cleanVisaData(array $visaDataInput): array
|
|||||||
return [
|
return [
|
||||||
'document_type' => 'uae_visa',
|
'document_type' => 'uae_visa',
|
||||||
'country' => 'United Arab Emirates',
|
'country' => 'United Arab Emirates',
|
||||||
'visa_type' => $rawVisaType ?: 'ENTRY PERMIT',
|
'visa_type' => $rawVisaType,
|
||||||
'entry_permit_no' => $rawEntryPermitNo ?: '',
|
'entry_permit_no' => $rawEntryPermitNo ?: '',
|
||||||
'issue_date' => $rawIssueDate ?: '',
|
'issue_date' => $rawIssueDate ?: '',
|
||||||
'valid_until' => $rawValidUntil ?: '',
|
'valid_until' => $rawValidUntil ?: '',
|
||||||
|
|||||||
113
app/Http/Controllers/Api/WorkerNotificationController.php
Normal file
113
app/Http/Controllers/Api/WorkerNotificationController.php
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use App\Models\Worker;
|
||||||
|
|
||||||
|
class WorkerNotificationController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* List all notifications for the worker.
|
||||||
|
* GET /api/workers/notifications
|
||||||
|
*/
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
/** @var Worker $worker */
|
||||||
|
$worker = $request->attributes->get('worker');
|
||||||
|
|
||||||
|
$filter = $request->query('filter', 'all');
|
||||||
|
$perPage = (int) $request->query('per_page', 15);
|
||||||
|
|
||||||
|
if ($filter === 'read') {
|
||||||
|
$query = $worker->readNotifications();
|
||||||
|
} elseif ($filter === 'unread') {
|
||||||
|
$query = $worker->unreadNotifications();
|
||||||
|
} else {
|
||||||
|
$query = $worker->notifications();
|
||||||
|
}
|
||||||
|
|
||||||
|
$paginator = $query->paginate($perPage);
|
||||||
|
|
||||||
|
$notifications = collect($paginator->items())->map(function ($notification) {
|
||||||
|
return [
|
||||||
|
'id' => $notification->id,
|
||||||
|
'type' => $notification->type,
|
||||||
|
'data' => $notification->data,
|
||||||
|
'read_at' => $notification->read_at ? $notification->read_at->toIso8601String() : null,
|
||||||
|
'created_at' => $notification->created_at->toIso8601String(),
|
||||||
|
'time_ago' => $notification->created_at->diffForHumans(),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
'notifications' => $notifications,
|
||||||
|
'pagination' => [
|
||||||
|
'total' => $paginator->total(),
|
||||||
|
'per_page' => $paginator->perPage(),
|
||||||
|
'current_page' => $paginator->currentPage(),
|
||||||
|
'last_page' => $paginator->lastPage(),
|
||||||
|
]
|
||||||
|
]
|
||||||
|
], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark a single notification or all unread notifications as read.
|
||||||
|
* PUT /api/workers/notifications/{id}/read or PUT /api/workers/notifications/read
|
||||||
|
*/
|
||||||
|
public function markAsRead(Request $request, $id = null)
|
||||||
|
{
|
||||||
|
/** @var Worker $worker */
|
||||||
|
$worker = $request->attributes->get('worker');
|
||||||
|
|
||||||
|
if ($id) {
|
||||||
|
$notification = $worker->notifications()->where('id', $id)->first();
|
||||||
|
if (!$notification) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Notification not found.'
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
$notification->markAsRead();
|
||||||
|
} else {
|
||||||
|
$worker->unreadNotifications->markAsRead();
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Notification(s) marked as read successfully.'
|
||||||
|
], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a single notification or all notifications.
|
||||||
|
* DELETE /api/workers/notifications/{id} or DELETE /api/workers/notifications
|
||||||
|
*/
|
||||||
|
public function destroy(Request $request, $id = null)
|
||||||
|
{
|
||||||
|
/** @var Worker $worker */
|
||||||
|
$worker = $request->attributes->get('worker');
|
||||||
|
|
||||||
|
if ($id) {
|
||||||
|
$notification = $worker->notifications()->where('id', $id)->first();
|
||||||
|
if (!$notification) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Notification not found.'
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
$notification->delete();
|
||||||
|
} else {
|
||||||
|
$worker->notifications()->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Notification(s) deleted successfully.'
|
||||||
|
], 200);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -77,7 +77,7 @@ public function updateProfile(Request $request)
|
|||||||
'skills' => 'nullable|array',
|
'skills' => 'nullable|array',
|
||||||
'skills.*' => 'exists:skills,id',
|
'skills.*' => 'exists:skills,id',
|
||||||
'in_country' => 'nullable',
|
'in_country' => 'nullable',
|
||||||
'visa_status' => 'nullable|string|max:100',
|
'visa_status' => 'nullable|string|in:Tourist Visa,Employment Visa,Residence Visa',
|
||||||
'preferred_job_type' => 'nullable|string|max:100',
|
'preferred_job_type' => 'nullable|string|max:100',
|
||||||
'gender' => 'nullable|string|in:male,female,other',
|
'gender' => 'nullable|string|in:male,female,other',
|
||||||
'live_in_out' => 'nullable|string|in:live_in,live_out',
|
'live_in_out' => 'nullable|string|in:live_in,live_out',
|
||||||
@ -658,6 +658,7 @@ public function getDashboard(Request $request)
|
|||||||
'employer_contacted' => $employerContacted,
|
'employer_contacted' => $employerContacted,
|
||||||
'profile_viewed' => $profileViewed,
|
'profile_viewed' => $profileViewed,
|
||||||
'currently_working_sponsor' => $currentSponsor,
|
'currently_working_sponsor' => $currentSponsor,
|
||||||
|
'current_employer' => $currentSponsor,
|
||||||
'latest_charity_events_list' => $charityEvents
|
'latest_charity_events_list' => $charityEvents
|
||||||
]
|
]
|
||||||
], 200);
|
], 200);
|
||||||
@ -673,6 +674,105 @@ public function getDashboard(Request $request)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of employers associated with the worker.
|
||||||
|
* GET /api/workers/employers
|
||||||
|
*/
|
||||||
|
public function getEmployers(Request $request)
|
||||||
|
{
|
||||||
|
/** @var Worker $worker */
|
||||||
|
$worker = $request->attributes->get('worker');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$query = JobOffer::where('worker_id', $worker->id)
|
||||||
|
->with(['employer.employerProfile']);
|
||||||
|
|
||||||
|
// Filtering by status
|
||||||
|
if ($request->filled('status')) {
|
||||||
|
$statusInput = strtolower($request->status);
|
||||||
|
if ($statusInput === 'active') {
|
||||||
|
$query->where('status', 'accepted');
|
||||||
|
} else {
|
||||||
|
$query->where('status', $statusInput);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sorting
|
||||||
|
$sortBy = $request->input('sort_by', 'joining_date');
|
||||||
|
$sortOrder = strtolower($request->input('sort_order', 'desc')) === 'asc' ? 'asc' : 'desc';
|
||||||
|
|
||||||
|
$sortColumn = 'work_date';
|
||||||
|
if ($sortBy === 'salary') {
|
||||||
|
$sortColumn = 'salary';
|
||||||
|
} elseif ($sortBy === 'status') {
|
||||||
|
$sortColumn = 'status';
|
||||||
|
} elseif ($sortBy === 'created_at') {
|
||||||
|
$sortColumn = 'created_at';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always display the current active employer (status = accepted) at the top of the list,
|
||||||
|
// followed by other employers ordered by the chosen sort column.
|
||||||
|
$query->orderByRaw("CASE WHEN status = 'accepted' THEN 0 ELSE 1 END ASC")
|
||||||
|
->orderBy($sortColumn, $sortOrder);
|
||||||
|
|
||||||
|
$perPage = (int) $request->input('per_page', 15);
|
||||||
|
$paginator = $query->paginate($perPage);
|
||||||
|
|
||||||
|
$employers = collect($paginator->items())->map(function ($offer) {
|
||||||
|
$emp = $offer->employer;
|
||||||
|
$empProfile = $emp ? $emp->employerProfile : null;
|
||||||
|
|
||||||
|
// Map 'accepted' -> 'Active' (and others to Title Case for nice presentation)
|
||||||
|
$statusFormatted = $offer->status;
|
||||||
|
if (strtolower($offer->status) === 'accepted') {
|
||||||
|
$statusFormatted = 'Active';
|
||||||
|
} else {
|
||||||
|
$statusFormatted = ucfirst($offer->status);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'offer_id' => $offer->id,
|
||||||
|
'joining_date' => $offer->work_date ? $offer->work_date->format('Y-m-d') : null,
|
||||||
|
'location' => $offer->location,
|
||||||
|
'salary' => (int) $offer->salary,
|
||||||
|
'status' => $statusFormatted,
|
||||||
|
'notes' => $offer->notes,
|
||||||
|
'employer' => $emp ? [
|
||||||
|
'id' => $emp->id,
|
||||||
|
'name' => $emp->name,
|
||||||
|
'email' => $emp->email,
|
||||||
|
'phone' => $emp->phone,
|
||||||
|
'company_name' => $empProfile->company_name ?? 'Private Sponsor',
|
||||||
|
'city' => $empProfile->city ?? null,
|
||||||
|
'nationality' => $empProfile->nationality ?? null,
|
||||||
|
] : null
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
'employers' => $employers,
|
||||||
|
'pagination' => [
|
||||||
|
'total' => $paginator->total(),
|
||||||
|
'per_page' => $paginator->perPage(),
|
||||||
|
'current_page' => $paginator->currentPage(),
|
||||||
|
'last_page' => $paginator->lastPage(),
|
||||||
|
]
|
||||||
|
]
|
||||||
|
], 200);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
logger()->error('Worker Employers API Failure: ' . $e->getMessage());
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'An error occurred while loading the employers list.',
|
||||||
|
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Change worker's password.
|
* Change worker's password.
|
||||||
*
|
*
|
||||||
@ -765,6 +865,19 @@ private function normaliseDateForController(?string $raw): ?string
|
|||||||
private function cleanVisaData(array $visaDataInput): array
|
private function cleanVisaData(array $visaDataInput): array
|
||||||
{
|
{
|
||||||
$rawVisaType = $visaDataInput['visa_type'] ?? null;
|
$rawVisaType = $visaDataInput['visa_type'] ?? null;
|
||||||
|
if ($rawVisaType) {
|
||||||
|
$normalized = strtolower(trim($rawVisaType));
|
||||||
|
if (str_contains($normalized, 'residence')) {
|
||||||
|
$rawVisaType = 'Residence Visa';
|
||||||
|
} elseif (str_contains($normalized, 'employment')) {
|
||||||
|
$rawVisaType = 'Employment Visa';
|
||||||
|
} else {
|
||||||
|
$rawVisaType = 'Tourist Visa';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$rawVisaType = 'Tourist Visa';
|
||||||
|
}
|
||||||
|
|
||||||
$rawEntryPermitNo = $visaDataInput['entry_permit_no'] ?? $visaDataInput['file_number'] ?? $visaDataInput['id_number'] ?? $visaDataInput['number'] ?? null;
|
$rawEntryPermitNo = $visaDataInput['entry_permit_no'] ?? $visaDataInput['file_number'] ?? $visaDataInput['id_number'] ?? $visaDataInput['number'] ?? null;
|
||||||
$rawIssueDate = $visaDataInput['issue_date'] ?? null;
|
$rawIssueDate = $visaDataInput['issue_date'] ?? null;
|
||||||
$rawValidUntil = $visaDataInput['valid_until'] ?? $visaDataInput['expiry_date'] ?? null;
|
$rawValidUntil = $visaDataInput['valid_until'] ?? $visaDataInput['expiry_date'] ?? null;
|
||||||
@ -795,7 +908,7 @@ private function cleanVisaData(array $visaDataInput): array
|
|||||||
return [
|
return [
|
||||||
'document_type' => 'uae_visa',
|
'document_type' => 'uae_visa',
|
||||||
'country' => 'United Arab Emirates',
|
'country' => 'United Arab Emirates',
|
||||||
'visa_type' => $rawVisaType ?: 'ENTRY PERMIT',
|
'visa_type' => $rawVisaType,
|
||||||
'entry_permit_no' => $rawEntryPermitNo ?: '',
|
'entry_permit_no' => $rawEntryPermitNo ?: '',
|
||||||
'issue_date' => $rawIssueDate ?: '',
|
'issue_date' => $rawIssueDate ?: '',
|
||||||
'valid_until' => $rawValidUntil ?: '',
|
'valid_until' => $rawValidUntil ?: '',
|
||||||
|
|||||||
@ -84,11 +84,12 @@ public function addReview(Request $request)
|
|||||||
], 403);
|
], 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify if completed 3 months from joining date
|
// Verify if completed configured months from joining date
|
||||||
if ($joiningDate->startOfDay()->diffInMonths(now()->startOfDay(), false) < 3) {
|
$eligibilityMonths = (int) config('reminders.review_eligibility_months', 3);
|
||||||
|
if ($joiningDate->startOfDay()->diffInMonths(now()->startOfDay(), false) < $eligibilityMonths) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => false,
|
'success' => false,
|
||||||
'message' => 'You can write a review only after 3 months from the joining date.'
|
'message' => "You can write a review only after {$eligibilityMonths} months from the joining date."
|
||||||
], 403);
|
], 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,11 +170,12 @@ public function editReview(Request $request, $id)
|
|||||||
], 404);
|
], 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the 7-day edit window has expired
|
// Check if the edit window has expired
|
||||||
if ($review->created_at->addDays(7)->isPast()) {
|
$editPeriodDays = (int) config('reminders.review_edit_period_days', 7);
|
||||||
|
if ($review->created_at->addDays($editPeriodDays)->isPast()) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => false,
|
'success' => false,
|
||||||
'message' => 'The 7-day edit window for this review has expired.'
|
'message' => "The {$editPeriodDays}-day edit window for this review has expired."
|
||||||
], 403);
|
], 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -224,7 +226,8 @@ public function viewReview(Request $request, $id)
|
|||||||
], 404);
|
], 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
$editable = !$review->created_at->addDays(7)->isPast();
|
$editPeriodDays = (int) config('reminders.review_edit_period_days', 7);
|
||||||
|
$editable = !$review->created_at->addDays($editPeriodDays)->isPast();
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
@ -278,7 +281,8 @@ public function getReviews(Request $request)
|
|||||||
|
|
||||||
$reviews = $query->skip($offset)->take($perPage)->get()
|
$reviews = $query->skip($offset)->take($perPage)->get()
|
||||||
->map(function ($rev) {
|
->map(function ($rev) {
|
||||||
$editable = !$rev->created_at->addDays(7)->isPast();
|
$editPeriodDays = (int) config('reminders.review_edit_period_days', 7);
|
||||||
|
$editable = !$rev->created_at->addDays($editPeriodDays)->isPast();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => $rev->id,
|
'id' => $rev->id,
|
||||||
|
|||||||
@ -74,6 +74,7 @@ public function index(Request $request)
|
|||||||
'live_in_out' => $w->live_in_out,
|
'live_in_out' => $w->live_in_out,
|
||||||
'in_country' => (bool)$w->in_country,
|
'in_country' => (bool)$w->in_country,
|
||||||
'visa_status' => $w->visa_status,
|
'visa_status' => $w->visa_status,
|
||||||
|
'main_profession' => $w->main_profession,
|
||||||
];
|
];
|
||||||
})->filter()->values()->toArray();
|
})->filter()->values()->toArray();
|
||||||
|
|
||||||
@ -105,6 +106,7 @@ public function index(Request $request)
|
|||||||
'live_in_out' => $w->live_in_out,
|
'live_in_out' => $w->live_in_out,
|
||||||
'in_country' => (bool)$w->in_country,
|
'in_country' => (bool)$w->in_country,
|
||||||
'visa_status' => $w->visa_status,
|
'visa_status' => $w->visa_status,
|
||||||
|
'main_profession' => $w->main_profession,
|
||||||
];
|
];
|
||||||
})->filter()->values()->toArray();
|
})->filter()->values()->toArray();
|
||||||
|
|
||||||
@ -265,6 +267,13 @@ public function index(Request $request)
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($request->filled('main_profession')) {
|
||||||
|
$mainProfession = strtolower($request->main_profession);
|
||||||
|
$mergedCandidates = array_values(array_filter($mergedCandidates, function ($c) use ($mainProfession) {
|
||||||
|
return isset($c['main_profession']) && strtolower($c['main_profession']) === $mainProfession;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
$nationalitiesResponse = app(\App\Http\Controllers\Api\WorkerAuthController::class)->nationalities(new \Illuminate\Http\Request(['per_page' => 500]));
|
$nationalitiesResponse = app(\App\Http\Controllers\Api\WorkerAuthController::class)->nationalities(new \Illuminate\Http\Request(['per_page' => 500]));
|
||||||
$nationalitiesData = json_decode($nationalitiesResponse->getContent(), true);
|
$nationalitiesData = json_decode($nationalitiesResponse->getContent(), true);
|
||||||
$allNationalities = collect($nationalitiesData['data']['nationalities'] ?? [])->pluck('name')->filter()->toArray();
|
$allNationalities = collect($nationalitiesData['data']['nationalities'] ?? [])->pluck('name')->filter()->toArray();
|
||||||
|
|||||||
@ -281,7 +281,7 @@ public function index(Request $request)
|
|||||||
'languages' => ['All Languages', 'English', 'Arabic', 'Hindi', 'Tagalog'],
|
'languages' => ['All Languages', 'English', 'Arabic', 'Hindi', 'Tagalog'],
|
||||||
'workTypes' => ['All Types', 'full-time', 'part-time', 'live-in', 'live-out'],
|
'workTypes' => ['All Types', 'full-time', 'part-time', 'live-in', 'live-out'],
|
||||||
'skills' => ['All Skills', 'cooking', 'driving', 'childcare', 'cleaning', 'elderly care', 'gardening'],
|
'skills' => ['All Skills', 'cooking', 'driving', 'childcare', 'cleaning', 'elderly care', 'gardening'],
|
||||||
'visaStatuses' => ['All Visa Statuses', 'Residence Visa', 'Tourist Visa', 'Employment Visa', 'Cancelled Visa', 'Own Visa'],
|
'visaStatuses' => ['All Visa Statuses', 'Residence Visa', 'Tourist Visa', 'Employment Visa'],
|
||||||
];
|
];
|
||||||
|
|
||||||
return Inertia::render('Employer/Workers/Index', [
|
return Inertia::render('Employer/Workers/Index', [
|
||||||
|
|||||||
@ -73,17 +73,49 @@ public function getPassportStatusAttribute()
|
|||||||
public function getVisaStatusAttribute()
|
public function getVisaStatusAttribute()
|
||||||
{
|
{
|
||||||
if ($this->attributes['visa_status'] ?? null) {
|
if ($this->attributes['visa_status'] ?? null) {
|
||||||
return $this->attributes['visa_status'];
|
$val = $this->attributes['visa_status'];
|
||||||
|
$normalized = strtolower(trim($val));
|
||||||
|
if (str_contains($normalized, 'residence')) {
|
||||||
|
return 'Residence Visa';
|
||||||
|
} elseif (str_contains($normalized, 'employment')) {
|
||||||
|
return 'Employment Visa';
|
||||||
|
} else {
|
||||||
|
return 'Tourist Visa';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
$visa = $this->documents->where('type', 'visa')->first();
|
$visa = $this->documents->where('type', 'visa')->first();
|
||||||
if (!$visa) {
|
if (!$visa) {
|
||||||
return 'Visa Pending';
|
|
||||||
}
|
|
||||||
if ($visa->expiry_date && \Carbon\Carbon::parse($visa->expiry_date)->isPast()) {
|
|
||||||
return 'Cancelled Visa';
|
|
||||||
}
|
|
||||||
return 'Residence Visa';
|
return 'Residence Visa';
|
||||||
}
|
}
|
||||||
|
$ocrData = $visa->ocr_data;
|
||||||
|
$vType = $ocrData['visa_type'] ?? null;
|
||||||
|
if ($vType) {
|
||||||
|
$normalized = strtolower(trim($vType));
|
||||||
|
if (str_contains($normalized, 'residence')) {
|
||||||
|
return 'Residence Visa';
|
||||||
|
} elseif (str_contains($normalized, 'employment')) {
|
||||||
|
return 'Employment Visa';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 'Tourist Visa';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setVisaStatusAttribute($value)
|
||||||
|
{
|
||||||
|
if (empty($value)) {
|
||||||
|
$this->attributes['visa_status'] = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalized = strtolower(trim($value));
|
||||||
|
if (str_contains($normalized, 'residence')) {
|
||||||
|
$this->attributes['visa_status'] = 'Residence Visa';
|
||||||
|
} elseif (str_contains($normalized, 'employment')) {
|
||||||
|
$this->attributes['visa_status'] = 'Employment Visa';
|
||||||
|
} else {
|
||||||
|
$this->attributes['visa_status'] = 'Tourist Visa';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function getEmiratesIdStatusAttribute()
|
public function getEmiratesIdStatusAttribute()
|
||||||
{
|
{
|
||||||
|
|||||||
103
app/Notifications/EmployerReviewReminderNotification.php
Normal file
103
app/Notifications/EmployerReviewReminderNotification.php
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Notifications;
|
||||||
|
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Notifications\Messages\MailMessage;
|
||||||
|
use Illuminate\Notifications\Notification;
|
||||||
|
use App\Notifications\Channels\FcmChannel;
|
||||||
|
|
||||||
|
class EmployerReviewReminderNotification extends Notification implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Queueable;
|
||||||
|
|
||||||
|
public $workerName;
|
||||||
|
public $applicationId;
|
||||||
|
public $reviewLevel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new notification instance.
|
||||||
|
*/
|
||||||
|
public function __construct($workerName, $applicationId, $reviewLevel)
|
||||||
|
{
|
||||||
|
$this->workerName = $workerName;
|
||||||
|
$this->applicationId = $applicationId;
|
||||||
|
$this->reviewLevel = $reviewLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the notification's delivery channels.
|
||||||
|
*/
|
||||||
|
public function via($notifiable): array
|
||||||
|
{
|
||||||
|
$channels = ['database'];
|
||||||
|
|
||||||
|
// Determine if email channel should be included
|
||||||
|
$emailEnabled = true;
|
||||||
|
if (method_exists($notifiable, 'employerProfile') && $notifiable->employerProfile) {
|
||||||
|
$emailEnabled = (bool) ($notifiable->employerProfile->email_notifications ?? true);
|
||||||
|
}
|
||||||
|
if ($emailEnabled && !empty($notifiable->email)) {
|
||||||
|
$channels[] = 'mail';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if push channel should be included
|
||||||
|
$pushEnabled = true;
|
||||||
|
if (method_exists($notifiable, 'employerProfile') && $notifiable->employerProfile) {
|
||||||
|
$pushEnabled = (bool) ($notifiable->employerProfile->push_notifications ?? true);
|
||||||
|
}
|
||||||
|
if ($pushEnabled && !empty($notifiable->fcm_token)) {
|
||||||
|
$channels[] = FcmChannel::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $channels;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the mail representation of the notification.
|
||||||
|
*/
|
||||||
|
public function toMail($notifiable): MailMessage
|
||||||
|
{
|
||||||
|
$name = $notifiable->name ?? $notifiable->full_name ?? 'User';
|
||||||
|
return (new MailMessage)
|
||||||
|
->subject("Review reminder: Rate your experience with {$this->workerName}")
|
||||||
|
->greeting("Hello {$name},")
|
||||||
|
->line("It has been {$this->reviewLevel} since you hired {$this->workerName}.")
|
||||||
|
->line("Please take a moment to review and share your experience with {$this->workerName}.")
|
||||||
|
->action('Submit Review', url('/dashboard/reviews'))
|
||||||
|
->line('Thank you for using our application!');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the array representation of the notification for in-app storage.
|
||||||
|
*/
|
||||||
|
public function toArray($notifiable): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'title' => "Review reminder: {$this->workerName}",
|
||||||
|
'body' => "Please take a moment to review and share your experience with {$this->workerName}.",
|
||||||
|
'worker_name' => $this->workerName,
|
||||||
|
'application_id' => $this->applicationId,
|
||||||
|
'review_level' => $this->reviewLevel,
|
||||||
|
'type' => 'review_worker_reminder',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the push notification representation.
|
||||||
|
*/
|
||||||
|
public function toFcm($notifiable): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'token' => $notifiable->fcm_token,
|
||||||
|
'title' => "Review reminder: {$this->workerName}",
|
||||||
|
'body' => "Please take a moment to review and share your experience with {$this->workerName}.",
|
||||||
|
'data' => [
|
||||||
|
'type' => 'review_worker_reminder',
|
||||||
|
'application_id' => $this->applicationId,
|
||||||
|
'review_level' => $this->reviewLevel,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
108
app/Notifications/WorkerNoResponseNotification.php
Normal file
108
app/Notifications/WorkerNoResponseNotification.php
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Notifications;
|
||||||
|
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Notifications\Messages\MailMessage;
|
||||||
|
use Illuminate\Notifications\Notification;
|
||||||
|
use App\Notifications\Channels\FcmChannel;
|
||||||
|
|
||||||
|
class WorkerNoResponseNotification extends Notification implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Queueable;
|
||||||
|
|
||||||
|
public $type;
|
||||||
|
public $messageTitle;
|
||||||
|
public $messageBody;
|
||||||
|
public $daysElapsed;
|
||||||
|
public $entityType;
|
||||||
|
public $entityId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new notification instance.
|
||||||
|
*/
|
||||||
|
public function __construct($type, $messageTitle, $messageBody, $daysElapsed, $entityType, $entityId)
|
||||||
|
{
|
||||||
|
$this->type = $type;
|
||||||
|
$this->messageTitle = $messageTitle;
|
||||||
|
$this->messageBody = $messageBody;
|
||||||
|
$this->daysElapsed = $daysElapsed;
|
||||||
|
$this->entityType = $entityType;
|
||||||
|
$this->entityId = $entityId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the notification's delivery channels.
|
||||||
|
*/
|
||||||
|
public function via($notifiable): array
|
||||||
|
{
|
||||||
|
$channels = ['database'];
|
||||||
|
|
||||||
|
// Determine if email channel should be included
|
||||||
|
$emailEnabled = true;
|
||||||
|
if (method_exists($notifiable, 'employerProfile') && $notifiable->employerProfile) {
|
||||||
|
$emailEnabled = (bool) ($notifiable->employerProfile->email_notifications ?? true);
|
||||||
|
}
|
||||||
|
if ($emailEnabled && !empty($notifiable->email)) {
|
||||||
|
$channels[] = 'mail';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if push channel should be included
|
||||||
|
$pushEnabled = true;
|
||||||
|
if (method_exists($notifiable, 'employerProfile') && $notifiable->employerProfile) {
|
||||||
|
$pushEnabled = (bool) ($notifiable->employerProfile->push_notifications ?? true);
|
||||||
|
}
|
||||||
|
if ($pushEnabled && !empty($notifiable->fcm_token)) {
|
||||||
|
$channels[] = FcmChannel::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $channels;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the mail representation of the notification.
|
||||||
|
*/
|
||||||
|
public function toMail($notifiable): MailMessage
|
||||||
|
{
|
||||||
|
$name = $notifiable->name ?? $notifiable->full_name ?? 'User';
|
||||||
|
return (new MailMessage)
|
||||||
|
->subject($this->messageTitle)
|
||||||
|
->greeting("Hello {$name},")
|
||||||
|
->line($this->messageBody)
|
||||||
|
->action('View details', url('/dashboard'))
|
||||||
|
->line('Thank you for using our application!');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the array representation of the notification for in-app storage.
|
||||||
|
*/
|
||||||
|
public function toArray($notifiable): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'title' => $this->messageTitle,
|
||||||
|
'body' => $this->messageBody,
|
||||||
|
'type' => $this->type,
|
||||||
|
'days_elapsed' => $this->daysElapsed,
|
||||||
|
'entity_type' => $this->entityType,
|
||||||
|
'entity_id' => $this->entityId,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the push notification representation.
|
||||||
|
*/
|
||||||
|
public function toFcm($notifiable): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'token' => $notifiable->fcm_token,
|
||||||
|
'title' => $this->messageTitle,
|
||||||
|
'body' => $this->messageBody,
|
||||||
|
'data' => [
|
||||||
|
'type' => $this->type,
|
||||||
|
'entity_id' => $this->entityId,
|
||||||
|
'days_elapsed' => $this->daysElapsed,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
103
app/Notifications/WorkerReviewReminderNotification.php
Normal file
103
app/Notifications/WorkerReviewReminderNotification.php
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Notifications;
|
||||||
|
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Notifications\Messages\MailMessage;
|
||||||
|
use Illuminate\Notifications\Notification;
|
||||||
|
use App\Notifications\Channels\FcmChannel;
|
||||||
|
|
||||||
|
class WorkerReviewReminderNotification extends Notification implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Queueable;
|
||||||
|
|
||||||
|
public $employerName;
|
||||||
|
public $applicationId;
|
||||||
|
public $reviewLevel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new notification instance.
|
||||||
|
*/
|
||||||
|
public function __construct($employerName, $applicationId, $reviewLevel)
|
||||||
|
{
|
||||||
|
$this->employerName = $employerName;
|
||||||
|
$this->applicationId = $applicationId;
|
||||||
|
$this->reviewLevel = $reviewLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the notification's delivery channels.
|
||||||
|
*/
|
||||||
|
public function via($notifiable): array
|
||||||
|
{
|
||||||
|
$channels = ['database'];
|
||||||
|
|
||||||
|
// Determine if email channel should be included
|
||||||
|
$emailEnabled = true;
|
||||||
|
if (method_exists($notifiable, 'employerProfile') && $notifiable->employerProfile) {
|
||||||
|
$emailEnabled = (bool) ($notifiable->employerProfile->email_notifications ?? true);
|
||||||
|
}
|
||||||
|
if ($emailEnabled && !empty($notifiable->email)) {
|
||||||
|
$channels[] = 'mail';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if push channel should be included
|
||||||
|
$pushEnabled = true;
|
||||||
|
if (method_exists($notifiable, 'employerProfile') && $notifiable->employerProfile) {
|
||||||
|
$pushEnabled = (bool) ($notifiable->employerProfile->push_notifications ?? true);
|
||||||
|
}
|
||||||
|
if ($pushEnabled && !empty($notifiable->fcm_token)) {
|
||||||
|
$channels[] = FcmChannel::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $channels;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the mail representation of the notification.
|
||||||
|
*/
|
||||||
|
public function toMail($notifiable): MailMessage
|
||||||
|
{
|
||||||
|
$name = $notifiable->name ?? $notifiable->full_name ?? 'User';
|
||||||
|
return (new MailMessage)
|
||||||
|
->subject("Review reminder: Rate your experience with {$this->employerName}")
|
||||||
|
->greeting("Hello {$name},")
|
||||||
|
->line("It has been {$this->reviewLevel} since you started working with {$this->employerName}.")
|
||||||
|
->line("Please take a moment to review and share your experience with {$this->employerName}.")
|
||||||
|
->action('Submit Review', url('/dashboard/reviews'))
|
||||||
|
->line('Thank you for using our application!');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the array representation of the notification for in-app storage.
|
||||||
|
*/
|
||||||
|
public function toArray($notifiable): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'title' => "Review reminder: {$this->employerName}",
|
||||||
|
'body' => "Please take a moment to review and share your experience with {$this->employerName}.",
|
||||||
|
'employer_name' => $this->employerName,
|
||||||
|
'application_id' => $this->applicationId,
|
||||||
|
'review_level' => $this->reviewLevel,
|
||||||
|
'type' => 'review_employer_reminder',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the push notification representation.
|
||||||
|
*/
|
||||||
|
public function toFcm($notifiable): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'token' => $notifiable->fcm_token,
|
||||||
|
'title' => "Review reminder: {$this->employerName}",
|
||||||
|
'body' => "Please take a moment to review and share your experience with {$this->employerName}.",
|
||||||
|
'data' => [
|
||||||
|
'type' => 'review_employer_reminder',
|
||||||
|
'application_id' => $this->applicationId,
|
||||||
|
'review_level' => $this->reviewLevel,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
10
config/reminders.php
Normal file
10
config/reminders.php
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'worker_no_response_reminder_days' => env('WORKER_NO_RESPONSE_REMINDER_DAYS', 14),
|
||||||
|
'review_eligibility_months' => env('REVIEW_ELIGIBILITY_MONTHS', 3),
|
||||||
|
'review_edit_period_days' => env('REVIEW_EDIT_PERIOD_DAYS', 7),
|
||||||
|
'employer_hire_confirm_reminder_days' => env('EMPLOYER_HIRE_CONFIRM_REMINDER_DAYS', '3,7'),
|
||||||
|
'worker_join_confirm_reminder_days' => env('WORKER_JOIN_CONFIRM_REMINDER_DAYS', '3,7,1'),
|
||||||
|
'document_expiry_reminder_days' => env('DOCUMENT_EXPIRY_REMINDER_DAYS', '30,7,3,1'),
|
||||||
|
];
|
||||||
@ -32,5 +32,13 @@
|
|||||||
<env name="PULSE_ENABLED" value="false"/>
|
<env name="PULSE_ENABLED" value="false"/>
|
||||||
<env name="TELESCOPE_ENABLED" value="false"/>
|
<env name="TELESCOPE_ENABLED" value="false"/>
|
||||||
<env name="NIGHTWATCH_ENABLED" value="false"/>
|
<env name="NIGHTWATCH_ENABLED" value="false"/>
|
||||||
|
|
||||||
|
<!-- Reminder Configuration for Tests -->
|
||||||
|
<env name="WORKER_NO_RESPONSE_REMINDER_DAYS" value="14"/>
|
||||||
|
<env name="REVIEW_ELIGIBILITY_MONTHS" value="3"/>
|
||||||
|
<env name="REVIEW_EDIT_PERIOD_DAYS" value="7"/>
|
||||||
|
<env name="EMPLOYER_HIRE_CONFIRM_REMINDER_DAYS" value="3,7"/>
|
||||||
|
<env name="WORKER_JOIN_CONFIRM_REMINDER_DAYS" value="3,7,1"/>
|
||||||
|
<env name="DOCUMENT_EXPIRY_REMINDER_DAYS" value="30,7,3,1"/>
|
||||||
</php>
|
</php>
|
||||||
</phpunit>
|
</phpunit>
|
||||||
|
|||||||
@ -584,7 +584,10 @@
|
|||||||
"end_time",
|
"end_time",
|
||||||
"provided_items",
|
"provided_items",
|
||||||
"location_details",
|
"location_details",
|
||||||
"location_pin"
|
"location_pin",
|
||||||
|
"contact_person_name",
|
||||||
|
"contact_number",
|
||||||
|
"country_code"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"title": {
|
"title": {
|
||||||
@ -630,6 +633,18 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "uri",
|
"format": "uri",
|
||||||
"example": "https://maps.app.goo.gl/xyz"
|
"example": "https://maps.app.goo.gl/xyz"
|
||||||
|
},
|
||||||
|
"contact_person_name": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Jane Doe"
|
||||||
|
},
|
||||||
|
"contact_number": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "551234567"
|
||||||
|
},
|
||||||
|
"country_code": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "+971"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1019,7 +1034,12 @@
|
|||||||
"visa_status": {
|
"visa_status": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "Tourist Visa",
|
"example": "Tourist Visa",
|
||||||
"description": "Visa status (only required/applicable if in_country is true)."
|
"description": "Visa status (only required/applicable if in_country is true).",
|
||||||
|
"enum": [
|
||||||
|
"Tourist Visa",
|
||||||
|
"Employment Visa",
|
||||||
|
"Residence Visa"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"preferred_job_type": {
|
"preferred_job_type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@ -5374,7 +5394,7 @@
|
|||||||
"name": "visa_status",
|
"name": "visa_status",
|
||||||
"in": "query",
|
"in": "query",
|
||||||
"required": false,
|
"required": false,
|
||||||
"description": "Filter by next visa status (comma-separated list, e.g. Residence Visa,Tourist Visa). Only applicable if in country.",
|
"description": "Filter by next visa status (comma-separated list, e.g. Residence Visa, Tourist Visa, Employment Visa). Only applicable if in country.",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
@ -5589,6 +5609,15 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "main_profession",
|
||||||
|
"in": "query",
|
||||||
|
"required": false,
|
||||||
|
"description": "Filter candidates by main profession (e.g. Housemaid, Nanny, Cook, Driver, Caregiver).",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "preferred_location",
|
"name": "preferred_location",
|
||||||
"in": "query",
|
"in": "query",
|
||||||
@ -5638,7 +5667,7 @@
|
|||||||
"name": "visa_status",
|
"name": "visa_status",
|
||||||
"in": "query",
|
"in": "query",
|
||||||
"required": false,
|
"required": false,
|
||||||
"description": "Filter candidates by next visa status (comma-separated list, e.g. Residence Visa,Tourist Visa). Only applicable if in country.",
|
"description": "Filter candidates by next visa status (comma-separated list, e.g. Residence Visa, Tourist Visa, Employment Visa). Only applicable if in country.",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
@ -5926,6 +5955,49 @@
|
|||||||
"example": 3,
|
"example": 3,
|
||||||
"description": "Unique count of employers who viewed this worker's profile."
|
"description": "Unique count of employers who viewed this worker's profile."
|
||||||
},
|
},
|
||||||
|
"current_employer": {
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"currently_working_sponsor": {
|
"currently_working_sponsor": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
@ -6020,6 +6092,177 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/workers/employers": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Worker/Employers"
|
||||||
|
],
|
||||||
|
"summary": "Get Worker Employers List",
|
||||||
|
"description": "Allows authenticated workers to retrieve all current and past employers associated with their employment history. Supports pagination, sorting, and filtering by employment status.",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "status",
|
||||||
|
"in": "query",
|
||||||
|
"required": false,
|
||||||
|
"description": "Filter by employment status (Active, Completed, Cancelled, Pending, Rejected, etc.).",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Active"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sort_by",
|
||||||
|
"in": "query",
|
||||||
|
"required": false,
|
||||||
|
"description": "Field to sort by (joining_date, salary, status, created_at).",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "joining_date"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sort_order",
|
||||||
|
"in": "query",
|
||||||
|
"required": false,
|
||||||
|
"description": "Sort order (asc, desc).",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "desc"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "page",
|
||||||
|
"in": "query",
|
||||||
|
"required": false,
|
||||||
|
"description": "Page number for pagination.",
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"default": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "per_page",
|
||||||
|
"in": "query",
|
||||||
|
"required": false,
|
||||||
|
"description": "Number of items per page.",
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"default": 15
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Employers list retrieved successfully.",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"success": {
|
||||||
|
"type": "boolean",
|
||||||
|
"example": true
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"employers": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"offer_id": {
|
||||||
|
"type": "integer",
|
||||||
|
"example": 12
|
||||||
|
},
|
||||||
|
"joining_date": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date",
|
||||||
|
"example": "2026-06-01"
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Dubai"
|
||||||
|
},
|
||||||
|
"salary": {
|
||||||
|
"type": "integer",
|
||||||
|
"example": 2500
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Active"
|
||||||
|
},
|
||||||
|
"notes": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Hired for child care"
|
||||||
|
},
|
||||||
|
"employer": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "integer",
|
||||||
|
"example": 2
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Ahmad"
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "ahmad@example.com"
|
||||||
|
},
|
||||||
|
"phone": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "+971509990001"
|
||||||
|
},
|
||||||
|
"company_name": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Ahmad Tech Ltd"
|
||||||
|
},
|
||||||
|
"city": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Dubai"
|
||||||
|
},
|
||||||
|
"nationality": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "UAE"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pagination": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"total": {
|
||||||
|
"type": "integer",
|
||||||
|
"example": 1
|
||||||
|
},
|
||||||
|
"per_page": {
|
||||||
|
"type": "integer",
|
||||||
|
"example": 15
|
||||||
|
},
|
||||||
|
"current_page": {
|
||||||
|
"type": "integer",
|
||||||
|
"example": 1
|
||||||
|
},
|
||||||
|
"last_page": {
|
||||||
|
"type": "integer",
|
||||||
|
"example": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/employers/reviews": {
|
"/employers/reviews": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@ -6835,9 +7078,25 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "full-time"
|
"example": "full-time"
|
||||||
},
|
},
|
||||||
|
"main_profession": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Housemaid",
|
||||||
|
"enum": [
|
||||||
|
"Housemaid",
|
||||||
|
"Nanny",
|
||||||
|
"Cook",
|
||||||
|
"Driver",
|
||||||
|
"Caregiver"
|
||||||
|
]
|
||||||
|
},
|
||||||
"visa_status": {
|
"visa_status": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "Tourist Visa"
|
"example": "Tourist Visa",
|
||||||
|
"enum": [
|
||||||
|
"Tourist Visa",
|
||||||
|
"Employment Visa",
|
||||||
|
"Residence Visa"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -7891,6 +8150,508 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"/workers/notifications": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Worker/Notification"
|
||||||
|
],
|
||||||
|
"summary": "List Worker Notifications",
|
||||||
|
"description": "Returns a paginated list of notifications for the authenticated worker.",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "filter",
|
||||||
|
"in": "query",
|
||||||
|
"description": "Filter notifications by read status.",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"all",
|
||||||
|
"read",
|
||||||
|
"unread"
|
||||||
|
],
|
||||||
|
"default": "all"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "per_page",
|
||||||
|
"in": "query",
|
||||||
|
"description": "Number of items per page.",
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"default": 15
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Notifications retrieved successfully.",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"success": {
|
||||||
|
"type": "boolean",
|
||||||
|
"example": true
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"notifications": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/Notification"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pagination": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"total": {
|
||||||
|
"type": "integer",
|
||||||
|
"example": 10
|
||||||
|
},
|
||||||
|
"per_page": {
|
||||||
|
"type": "integer",
|
||||||
|
"example": 15
|
||||||
|
},
|
||||||
|
"current_page": {
|
||||||
|
"type": "integer",
|
||||||
|
"example": 1
|
||||||
|
},
|
||||||
|
"last_page": {
|
||||||
|
"type": "integer",
|
||||||
|
"example": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthenticated."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"tags": [
|
||||||
|
"Worker/Notification"
|
||||||
|
],
|
||||||
|
"summary": "Delete All Worker Notifications",
|
||||||
|
"description": "Deletes all notifications for the authenticated worker.",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Notifications deleted successfully.",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"success": {
|
||||||
|
"type": "boolean",
|
||||||
|
"example": true
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Notification(s) deleted successfully."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthenticated."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/workers/notifications/read": {
|
||||||
|
"put": {
|
||||||
|
"tags": [
|
||||||
|
"Worker/Notification"
|
||||||
|
],
|
||||||
|
"summary": "Mark All Notifications as Read",
|
||||||
|
"description": "Marks all unread notifications for the authenticated worker as read.",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Notifications marked as read successfully.",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"success": {
|
||||||
|
"type": "boolean",
|
||||||
|
"example": true
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Notification(s) marked as read successfully."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthenticated."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/workers/notifications/{id}": {
|
||||||
|
"delete": {
|
||||||
|
"tags": [
|
||||||
|
"Worker/Notification"
|
||||||
|
],
|
||||||
|
"summary": "Delete Single Notification",
|
||||||
|
"description": "Deletes a single notification for the authenticated worker.",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"description": "UUID of the notification to delete.",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Notification deleted successfully.",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"success": {
|
||||||
|
"type": "boolean",
|
||||||
|
"example": true
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Notification(s) deleted successfully."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthenticated."
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Notification not found."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/workers/notifications/{id}/read": {
|
||||||
|
"put": {
|
||||||
|
"tags": [
|
||||||
|
"Worker/Notification"
|
||||||
|
],
|
||||||
|
"summary": "Mark Single Notification as Read",
|
||||||
|
"description": "Marks a single notification for the authenticated worker as read.",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"description": "UUID of the notification to mark as read.",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Notification marked as read successfully.",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"success": {
|
||||||
|
"type": "boolean",
|
||||||
|
"example": true
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Notification(s) marked as read successfully."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthenticated."
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Notification not found."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/employers/notifications": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Employer/Notification"
|
||||||
|
],
|
||||||
|
"summary": "List Employer Notifications",
|
||||||
|
"description": "Returns a paginated list of notifications for the authenticated employer.",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "filter",
|
||||||
|
"in": "query",
|
||||||
|
"description": "Filter notifications by read status.",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"all",
|
||||||
|
"read",
|
||||||
|
"unread"
|
||||||
|
],
|
||||||
|
"default": "all"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "per_page",
|
||||||
|
"in": "query",
|
||||||
|
"description": "Number of items per page.",
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"default": 15
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Notifications retrieved successfully.",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"success": {
|
||||||
|
"type": "boolean",
|
||||||
|
"example": true
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"notifications": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/Notification"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pagination": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"total": {
|
||||||
|
"type": "integer",
|
||||||
|
"example": 10
|
||||||
|
},
|
||||||
|
"per_page": {
|
||||||
|
"type": "integer",
|
||||||
|
"example": 15
|
||||||
|
},
|
||||||
|
"current_page": {
|
||||||
|
"type": "integer",
|
||||||
|
"example": 1
|
||||||
|
},
|
||||||
|
"last_page": {
|
||||||
|
"type": "integer",
|
||||||
|
"example": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthenticated."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"tags": [
|
||||||
|
"Employer/Notification"
|
||||||
|
],
|
||||||
|
"summary": "Delete All Employer Notifications",
|
||||||
|
"description": "Deletes all notifications for the authenticated employer.",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Notifications deleted successfully.",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"success": {
|
||||||
|
"type": "boolean",
|
||||||
|
"example": true
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Notification(s) deleted successfully."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthenticated."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/employers/notifications/read": {
|
||||||
|
"put": {
|
||||||
|
"tags": [
|
||||||
|
"Employer/Notification"
|
||||||
|
],
|
||||||
|
"summary": "Mark All Notifications as Read",
|
||||||
|
"description": "Marks all unread notifications for the authenticated employer as read.",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Notifications marked as read successfully.",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"success": {
|
||||||
|
"type": "boolean",
|
||||||
|
"example": true
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Notification(s) marked as read successfully."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthenticated."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/employers/notifications/{id}": {
|
||||||
|
"delete": {
|
||||||
|
"tags": [
|
||||||
|
"Employer/Notification"
|
||||||
|
],
|
||||||
|
"summary": "Delete Single Notification",
|
||||||
|
"description": "Deletes a single notification for the authenticated employer.",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"description": "UUID of the notification to delete.",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Notification deleted successfully.",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"success": {
|
||||||
|
"type": "boolean",
|
||||||
|
"example": true
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Notification(s) deleted successfully."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthenticated."
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Notification not found."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/employers/notifications/{id}/read": {
|
||||||
|
"put": {
|
||||||
|
"tags": [
|
||||||
|
"Employer/Notification"
|
||||||
|
],
|
||||||
|
"summary": "Mark Single Notification as Read",
|
||||||
|
"description": "Marks a single notification for the authenticated employer as read.",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"description": "UUID of the notification to mark as read.",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Notification marked as read successfully.",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"success": {
|
||||||
|
"type": "boolean",
|
||||||
|
"example": true
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Notification(s) marked as read successfully."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthenticated."
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Notification not found."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
@ -8157,7 +8918,12 @@
|
|||||||
},
|
},
|
||||||
"visa_status": {
|
"visa_status": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "Tourist Visa"
|
"example": "Tourist Visa",
|
||||||
|
"enum": [
|
||||||
|
"Tourist Visa",
|
||||||
|
"Employment Visa",
|
||||||
|
"Residence Visa"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"passport_status": {
|
"passport_status": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@ -8259,7 +9025,14 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
"example": "Nanny",
|
"example": "Nanny",
|
||||||
"description": "The main profession of the worker"
|
"description": "The main profession of the worker",
|
||||||
|
"enum": [
|
||||||
|
"Housemaid",
|
||||||
|
"Nanny",
|
||||||
|
"Cook",
|
||||||
|
"Driver",
|
||||||
|
"Caregiver"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"skills": {
|
"skills": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
@ -8411,6 +9184,69 @@
|
|||||||
"example": "2026-05-20T15:23:44.000000Z"
|
"example": "2026-05-20T15:23:44.000000Z"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Notification": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"example": "d290f1ee-6c54-4b01-90e6-d701748f0851"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "App\\Notifications\\DocumentExpiryNotification"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"title": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Document Expiry Warning"
|
||||||
|
},
|
||||||
|
"body": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Your Passport will expire in 30 days on 2026-12-31."
|
||||||
|
},
|
||||||
|
"document_type": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Passport"
|
||||||
|
},
|
||||||
|
"expiry_date": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date",
|
||||||
|
"example": "2026-12-31"
|
||||||
|
},
|
||||||
|
"days_remaining": {
|
||||||
|
"type": "integer",
|
||||||
|
"example": 30
|
||||||
|
},
|
||||||
|
"reminder_level": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "30_days"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "document_expiry"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"read_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"nullable": true,
|
||||||
|
"example": "2026-07-03T12:00:00Z"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"example": "2026-07-03T11:43:30Z"
|
||||||
|
},
|
||||||
|
"time_ago": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "2 hours ago"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,6 +38,7 @@ export default function SelectedCandidates({
|
|||||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
|
|
||||||
// Filter States
|
// Filter States
|
||||||
|
const [filterProfession, setFilterProfession] = useState('All Professions');
|
||||||
const [filterLocation, setFilterLocation] = useState('All');
|
const [filterLocation, setFilterLocation] = useState('All');
|
||||||
const [filterJobType, setFilterJobType] = useState('All');
|
const [filterJobType, setFilterJobType] = useState('All');
|
||||||
const [filterAccommodation, setFilterAccommodation] = useState('All');
|
const [filterAccommodation, setFilterAccommodation] = useState('All');
|
||||||
@ -83,6 +84,9 @@ export default function SelectedCandidates({
|
|||||||
const query = searchTerm.toLowerCase();
|
const query = searchTerm.toLowerCase();
|
||||||
list = list.filter(w => w.name.toLowerCase().includes(query));
|
list = list.filter(w => w.name.toLowerCase().includes(query));
|
||||||
}
|
}
|
||||||
|
if (filterProfession !== 'All Professions') {
|
||||||
|
list = list.filter(w => w.main_profession && w.main_profession.toLowerCase() === filterProfession.toLowerCase());
|
||||||
|
}
|
||||||
if (filterLocation !== 'All' && filterLocation.trim() !== '') {
|
if (filterLocation !== 'All' && filterLocation.trim() !== '') {
|
||||||
const locKey = filterLocation.toLowerCase();
|
const locKey = filterLocation.toLowerCase();
|
||||||
const locationMapping = {
|
const locationMapping = {
|
||||||
@ -130,9 +134,10 @@ export default function SelectedCandidates({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
}, [selectedWorkers, searchTerm, filterLocation, filterJobType, filterAccommodation, filterNationalities, filterGender, filterInCountry, filterVisaType, filterSkills, filterLanguages, maxSalary]);
|
}, [selectedWorkers, searchTerm, filterProfession, filterLocation, filterJobType, filterAccommodation, filterNationalities, filterGender, filterInCountry, filterVisaType, filterSkills, filterLanguages, maxSalary]);
|
||||||
|
|
||||||
const resetFilters = () => {
|
const resetFilters = () => {
|
||||||
|
setFilterProfession('All Professions');
|
||||||
setFilterLocation('All');
|
setFilterLocation('All');
|
||||||
setFilterJobType('All');
|
setFilterJobType('All');
|
||||||
setFilterAccommodation('All');
|
setFilterAccommodation('All');
|
||||||
@ -147,6 +152,7 @@ export default function SelectedCandidates({
|
|||||||
|
|
||||||
const activeFilterCount = useMemo(() => {
|
const activeFilterCount = useMemo(() => {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
if (filterProfession !== 'All Professions') count++;
|
||||||
if (filterLocation !== 'All' && filterLocation.trim() !== '') count++;
|
if (filterLocation !== 'All' && filterLocation.trim() !== '') count++;
|
||||||
if (filterJobType !== 'All') count++;
|
if (filterJobType !== 'All') count++;
|
||||||
if (filterAccommodation !== 'All') count++;
|
if (filterAccommodation !== 'All') count++;
|
||||||
@ -158,11 +164,12 @@ export default function SelectedCandidates({
|
|||||||
if (filterLanguages.length > 0) count++;
|
if (filterLanguages.length > 0) count++;
|
||||||
if (maxSalary < 5000) count++;
|
if (maxSalary < 5000) count++;
|
||||||
return count;
|
return count;
|
||||||
}, [filterLocation, filterJobType, filterAccommodation, filterNationalities, filterGender, filterInCountry, filterVisaType, filterSkills, filterLanguages, maxSalary]);
|
}, [filterProfession, filterLocation, filterJobType, filterAccommodation, filterNationalities, filterGender, filterInCountry, filterVisaType, filterSkills, filterLanguages, maxSalary]);
|
||||||
|
|
||||||
// Active filter tags for display
|
// Active filter tags for display
|
||||||
const activeFilterTags = useMemo(() => {
|
const activeFilterTags = useMemo(() => {
|
||||||
const tags = [];
|
const tags = [];
|
||||||
|
if (filterProfession !== 'All Professions') tags.push({ key: 'profession', label: `🧑🔧 ${filterProfession}`, clear: () => setFilterProfession('All Professions') });
|
||||||
if (filterLocation !== 'All' && filterLocation.trim()) tags.push({ key: 'location', label: `📍 ${filterLocation}`, clear: () => setFilterLocation('All') });
|
if (filterLocation !== 'All' && filterLocation.trim()) tags.push({ key: 'location', label: `📍 ${filterLocation}`, clear: () => setFilterLocation('All') });
|
||||||
if (filterJobType !== 'All') tags.push({ key: 'job', label: `💼 ${filterJobType}`, clear: () => setFilterJobType('All') });
|
if (filterJobType !== 'All') tags.push({ key: 'job', label: `💼 ${filterJobType}`, clear: () => setFilterJobType('All') });
|
||||||
if (filterAccommodation !== 'All') tags.push({ key: 'acc', label: `🏠 ${filterAccommodation.replace('_', '-')}`, clear: () => setFilterAccommodation('All') });
|
if (filterAccommodation !== 'All') tags.push({ key: 'acc', label: `🏠 ${filterAccommodation.replace('_', '-')}`, clear: () => setFilterAccommodation('All') });
|
||||||
@ -174,7 +181,7 @@ export default function SelectedCandidates({
|
|||||||
if (filterLanguages.length > 0) tags.push({ key: 'lang', label: `🗣 ${filterLanguages.length} lang`, clear: () => setFilterLanguages([]) });
|
if (filterLanguages.length > 0) tags.push({ key: 'lang', label: `🗣 ${filterLanguages.length} lang`, clear: () => setFilterLanguages([]) });
|
||||||
if (maxSalary < 5000) tags.push({ key: 'salary', label: `💰 ≤${maxSalary} AED`, clear: () => setMaxSalary(5000) });
|
if (maxSalary < 5000) tags.push({ key: 'salary', label: `💰 ≤${maxSalary} AED`, clear: () => setMaxSalary(5000) });
|
||||||
return tags;
|
return tags;
|
||||||
}, [filterLocation, filterJobType, filterAccommodation, filterNationalities, filterGender, filterInCountry, filterVisaType, filterSkills, filterLanguages, maxSalary]);
|
}, [filterProfession, filterLocation, filterJobType, filterAccommodation, filterNationalities, filterGender, filterInCountry, filterVisaType, filterSkills, filterLanguages, maxSalary]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EmployerLayout title={t('candidates_pipeline', 'Hired Workers')}>
|
<EmployerLayout title={t('candidates_pipeline', 'Hired Workers')}>
|
||||||
@ -188,6 +195,22 @@ export default function SelectedCandidates({
|
|||||||
activeCount={activeFilterCount}
|
activeCount={activeFilterCount}
|
||||||
title="Filter Hired Workers"
|
title="Filter Hired Workers"
|
||||||
>
|
>
|
||||||
|
{/* Profession */}
|
||||||
|
<FilterSection label="Profession">
|
||||||
|
<FilterSelect
|
||||||
|
id="filter_main_profession"
|
||||||
|
value={filterProfession}
|
||||||
|
onChange={e => setFilterProfession(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="All Professions">All Professions</option>
|
||||||
|
<option value="Housemaid">Housemaid</option>
|
||||||
|
<option value="Nanny">Nanny</option>
|
||||||
|
<option value="Cook">Cook</option>
|
||||||
|
<option value="Driver">Driver</option>
|
||||||
|
<option value="Caregiver">Caregiver</option>
|
||||||
|
</FilterSelect>
|
||||||
|
</FilterSection>
|
||||||
|
|
||||||
{/* Preferred Location */}
|
{/* Preferred Location */}
|
||||||
<FilterSection label="Preferred Location">
|
<FilterSection label="Preferred Location">
|
||||||
<FilterSelect
|
<FilterSelect
|
||||||
@ -281,8 +304,6 @@ export default function SelectedCandidates({
|
|||||||
<option value="Residence Visa">Residence Visa</option>
|
<option value="Residence Visa">Residence Visa</option>
|
||||||
<option value="Tourist Visa">Tourist Visa</option>
|
<option value="Tourist Visa">Tourist Visa</option>
|
||||||
<option value="Employment Visa">Employment Visa</option>
|
<option value="Employment Visa">Employment Visa</option>
|
||||||
<option value="Cancelled Visa">Cancelled Visa</option>
|
|
||||||
<option value="Own Visa">Own Visa</option>
|
|
||||||
</FilterSelect>
|
</FilterSelect>
|
||||||
{filterInCountry !== 'In Country' && (
|
{filterInCountry !== 'In Country' && (
|
||||||
<p className="text-[11px] text-slate-400 mt-1.5 font-medium">
|
<p className="text-[11px] text-slate-400 mt-1.5 font-medium">
|
||||||
@ -437,6 +458,12 @@ export default function SelectedCandidates({
|
|||||||
<div className="text-[10px] text-slate-400 font-medium flex items-center space-x-1 mt-0.5">
|
<div className="text-[10px] text-slate-400 font-medium flex items-center space-x-1 mt-0.5">
|
||||||
<Globe2 className="w-3 h-3" />
|
<Globe2 className="w-3 h-3" />
|
||||||
<span>{worker.nationality || '—'}</span>
|
<span>{worker.nationality || '—'}</span>
|
||||||
|
{worker.main_profession && (
|
||||||
|
<>
|
||||||
|
<span className="text-slate-300">•</span>
|
||||||
|
<span>{worker.main_profession}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -520,8 +520,6 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
|
|||||||
<option value="Residence Visa">Residence Visa</option>
|
<option value="Residence Visa">Residence Visa</option>
|
||||||
<option value="Tourist Visa">Tourist Visa</option>
|
<option value="Tourist Visa">Tourist Visa</option>
|
||||||
<option value="Employment Visa">Employment Visa</option>
|
<option value="Employment Visa">Employment Visa</option>
|
||||||
<option value="Cancelled Visa">Cancelled Visa</option>
|
|
||||||
<option value="Own Visa">Own Visa</option>
|
|
||||||
</FilterSelect>
|
</FilterSelect>
|
||||||
{filterInCountry !== 'In Country' && (
|
{filterInCountry !== 'In Country' && (
|
||||||
<p className="text-[11px] text-slate-400 mt-1.5 font-medium">Select "In Country" to enable this filter</p>
|
<p className="text-[11px] text-slate-400 mt-1.5 font-medium">Select "In Country" to enable this filter</p>
|
||||||
|
|||||||
@ -13,6 +13,8 @@
|
|||||||
use App\Http\Controllers\Api\SponsorAuthController;
|
use App\Http\Controllers\Api\SponsorAuthController;
|
||||||
use App\Http\Controllers\Api\SponsorController;
|
use App\Http\Controllers\Api\SponsorController;
|
||||||
use App\Http\Controllers\Api\GoogleVisionOcrController;
|
use App\Http\Controllers\Api\GoogleVisionOcrController;
|
||||||
|
use App\Http\Controllers\Api\WorkerNotificationController;
|
||||||
|
use App\Http\Controllers\Api\EmployerNotificationController;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
@ -75,6 +77,7 @@
|
|||||||
Route::post('/workers/profile/mark-hired', [WorkerProfileController::class, 'markHired']);
|
Route::post('/workers/profile/mark-hired', [WorkerProfileController::class, 'markHired']);
|
||||||
Route::get('/workers/dashboard/views', [WorkerProfileController::class, 'getProfileViews']);
|
Route::get('/workers/dashboard/views', [WorkerProfileController::class, 'getProfileViews']);
|
||||||
Route::get('/workers/dashboard', [WorkerProfileController::class, 'getDashboard']);
|
Route::get('/workers/dashboard', [WorkerProfileController::class, 'getDashboard']);
|
||||||
|
Route::get('/workers/employers', [WorkerProfileController::class, 'getEmployers']);
|
||||||
Route::post('/workers/change-password', [WorkerProfileController::class, 'changePassword']);
|
Route::post('/workers/change-password', [WorkerProfileController::class, 'changePassword']);
|
||||||
|
|
||||||
|
|
||||||
@ -117,6 +120,13 @@
|
|||||||
Route::post('/workers/reviews', [\App\Http\Controllers\Api\WorkerReviewController::class, 'addReview']);
|
Route::post('/workers/reviews', [\App\Http\Controllers\Api\WorkerReviewController::class, 'addReview']);
|
||||||
Route::get('/workers/reviews/{id}', [\App\Http\Controllers\Api\WorkerReviewController::class, 'viewReview']);
|
Route::get('/workers/reviews/{id}', [\App\Http\Controllers\Api\WorkerReviewController::class, 'viewReview']);
|
||||||
Route::put('/workers/reviews/{id}', [\App\Http\Controllers\Api\WorkerReviewController::class, 'editReview']);
|
Route::put('/workers/reviews/{id}', [\App\Http\Controllers\Api\WorkerReviewController::class, 'editReview']);
|
||||||
|
|
||||||
|
// Notification Management (Worker)
|
||||||
|
Route::get('/workers/notifications', [WorkerNotificationController::class, 'index']);
|
||||||
|
Route::put('/workers/notifications/read', [WorkerNotificationController::class, 'markAsRead']);
|
||||||
|
Route::put('/workers/notifications/{id}/read', [WorkerNotificationController::class, 'markAsRead']);
|
||||||
|
Route::delete('/workers/notifications', [WorkerNotificationController::class, 'destroy']);
|
||||||
|
Route::delete('/workers/notifications/{id}', [WorkerNotificationController::class, 'destroy']);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Protected Employer Mobile Endpoints (Token Authenticated via Bearer Token)
|
// Protected Employer Mobile Endpoints (Token Authenticated via Bearer Token)
|
||||||
@ -176,6 +186,13 @@
|
|||||||
Route::delete('/employers/jobs/{id}', [\App\Http\Controllers\Api\WorkerJobController::class, 'employerDeleteJob']);
|
Route::delete('/employers/jobs/{id}', [\App\Http\Controllers\Api\WorkerJobController::class, 'employerDeleteJob']);
|
||||||
Route::get('/employers/jobs/{id}/applicants', [\App\Http\Controllers\Api\WorkerJobController::class, 'employerJobApplicants']);
|
Route::get('/employers/jobs/{id}/applicants', [\App\Http\Controllers\Api\WorkerJobController::class, 'employerJobApplicants']);
|
||||||
Route::post('/employers/applications/{id}/status', [\App\Http\Controllers\Api\WorkerJobController::class, 'employerUpdateApplicationStatus']);
|
Route::post('/employers/applications/{id}/status', [\App\Http\Controllers\Api\WorkerJobController::class, 'employerUpdateApplicationStatus']);
|
||||||
|
|
||||||
|
// Notification Management (Employer)
|
||||||
|
Route::get('/employers/notifications', [EmployerNotificationController::class, 'index']);
|
||||||
|
Route::put('/employers/notifications/read', [EmployerNotificationController::class, 'markAsRead']);
|
||||||
|
Route::put('/employers/notifications/{id}/read', [EmployerNotificationController::class, 'markAsRead']);
|
||||||
|
Route::delete('/employers/notifications', [EmployerNotificationController::class, 'destroy']);
|
||||||
|
Route::delete('/employers/notifications/{id}', [EmployerNotificationController::class, 'destroy']);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Protected Sponsor Mobile Endpoints (Token Authenticated via Bearer Token)
|
// Protected Sponsor Mobile Endpoints (Token Authenticated via Bearer Token)
|
||||||
|
|||||||
@ -49,6 +49,7 @@ protected function setUp(): void
|
|||||||
'live_in_out' => 'live_in',
|
'live_in_out' => 'live_in',
|
||||||
'in_country' => true,
|
'in_country' => true,
|
||||||
'visa_status' => 'Residence Visa',
|
'visa_status' => 'Residence Visa',
|
||||||
|
'main_profession' => 'Housemaid',
|
||||||
'age' => 25,
|
'age' => 25,
|
||||||
'gender' => 'female',
|
'gender' => 'female',
|
||||||
'salary' => 1500,
|
'salary' => 1500,
|
||||||
@ -80,6 +81,7 @@ protected function setUp(): void
|
|||||||
'live_in_out' => 'live_out',
|
'live_in_out' => 'live_out',
|
||||||
'in_country' => true,
|
'in_country' => true,
|
||||||
'visa_status' => 'Tourist Visa',
|
'visa_status' => 'Tourist Visa',
|
||||||
|
'main_profession' => 'Nanny',
|
||||||
'age' => 28,
|
'age' => 28,
|
||||||
'gender' => 'female',
|
'gender' => 'female',
|
||||||
'salary' => 1800,
|
'salary' => 1800,
|
||||||
@ -96,7 +98,7 @@ protected function setUp(): void
|
|||||||
'file_path' => 'passports/test2.jpg',
|
'file_path' => 'passports/test2.jpg',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Worker 3: Dubai, full-time, live-out, Kenyan, in_country=false, Cancelled Visa, male
|
// Worker 3: Dubai, full-time, live-out, Kenyan, in_country=false, Residence Visa, male
|
||||||
$w3 = Worker::create([
|
$w3 = Worker::create([
|
||||||
'name' => 'Worker Kenyan Out Country',
|
'name' => 'Worker Kenyan Out Country',
|
||||||
'email' => 'worker3@example.com',
|
'email' => 'worker3@example.com',
|
||||||
@ -110,7 +112,8 @@ protected function setUp(): void
|
|||||||
'preferred_job_type' => 'full-time',
|
'preferred_job_type' => 'full-time',
|
||||||
'live_in_out' => 'live_out',
|
'live_in_out' => 'live_out',
|
||||||
'in_country' => false,
|
'in_country' => false,
|
||||||
'visa_status' => 'Cancelled Visa',
|
'visa_status' => 'Residence Visa',
|
||||||
|
'main_profession' => 'Cook',
|
||||||
'age' => 30,
|
'age' => 30,
|
||||||
'gender' => 'male',
|
'gender' => 'male',
|
||||||
'salary' => 1200,
|
'salary' => 1200,
|
||||||
@ -201,17 +204,29 @@ public function test_get_workers_visa_status_if_in_country_filter()
|
|||||||
$this->assertCount(1, $workers);
|
$this->assertCount(1, $workers);
|
||||||
$this->assertEquals('Worker Filipino In Abu Dhabi', $workers[0]['name']);
|
$this->assertEquals('Worker Filipino In Abu Dhabi', $workers[0]['name']);
|
||||||
|
|
||||||
// Cancelled Visa is for Worker 3, but Worker 3 is out country, so it should not match
|
// Residence Visa is for Worker 3 (Kenyan), but Worker 3 is out country, so it should not match
|
||||||
// because of the "if in country next visa type" constraint.
|
// because of the "if in country next visa type" constraint.
|
||||||
$response2 = $this->withHeaders([
|
$response2 = $this->withHeaders([
|
||||||
'Authorization' => 'Bearer ' . $this->token,
|
'Authorization' => 'Bearer ' . $this->token,
|
||||||
])->getJson('/api/employers/workers?visa_status=Cancelled Visa');
|
])->getJson('/api/employers/workers?visa_status=Residence Visa&nationality=Kenyan');
|
||||||
|
|
||||||
$response2->assertStatus(200);
|
$response2->assertStatus(200);
|
||||||
$workers2 = $response2->json('data.workers');
|
$workers2 = $response2->json('data.workers');
|
||||||
$this->assertCount(0, $workers2);
|
$this->assertCount(0, $workers2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_get_workers_main_profession_filter()
|
||||||
|
{
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer ' . $this->token,
|
||||||
|
])->getJson('/api/employers/workers?main_profession=Housemaid');
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$workers = $response->json('data.workers');
|
||||||
|
$this->assertCount(1, $workers);
|
||||||
|
$this->assertEquals('Worker Indian In Dubai', $workers[0]['name']);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test getCandidates filtering.
|
* Test getCandidates filtering.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -41,6 +41,7 @@ protected function setUp(): void
|
|||||||
'live_in_out' => 'live_in',
|
'live_in_out' => 'live_in',
|
||||||
'in_country' => true,
|
'in_country' => true,
|
||||||
'visa_status' => 'Residence Visa',
|
'visa_status' => 'Residence Visa',
|
||||||
|
'main_profession' => 'Housemaid',
|
||||||
'age' => 25,
|
'age' => 25,
|
||||||
'salary' => 1500,
|
'salary' => 1500,
|
||||||
'experience' => '3 Years',
|
'experience' => '3 Years',
|
||||||
@ -70,6 +71,7 @@ protected function setUp(): void
|
|||||||
'live_in_out' => 'live_out',
|
'live_in_out' => 'live_out',
|
||||||
'in_country' => true,
|
'in_country' => true,
|
||||||
'visa_status' => 'Tourist Visa',
|
'visa_status' => 'Tourist Visa',
|
||||||
|
'main_profession' => 'Nanny',
|
||||||
'age' => 28,
|
'age' => 28,
|
||||||
'salary' => 1800,
|
'salary' => 1800,
|
||||||
'experience' => '2 Years',
|
'experience' => '2 Years',
|
||||||
@ -164,7 +166,8 @@ public function test_employer_can_view_candidates_index_and_apply_filters()
|
|||||||
'preferred_job_type' => 'full-time',
|
'preferred_job_type' => 'full-time',
|
||||||
'live_in_out' => 'live_out',
|
'live_in_out' => 'live_out',
|
||||||
'in_country' => false,
|
'in_country' => false,
|
||||||
'visa_status' => 'Cancelled Visa',
|
'visa_status' => 'Residence Visa',
|
||||||
|
'main_profession' => 'Cook',
|
||||||
'age' => 30,
|
'age' => 30,
|
||||||
'salary' => 1200,
|
'salary' => 1200,
|
||||||
'experience' => 'None',
|
'experience' => 'None',
|
||||||
@ -227,4 +230,59 @@ public function test_employer_workers_web_gender_filtering()
|
|||||||
$response->assertSee('Worker Indian Dubai');
|
$response->assertSee('Worker Indian Dubai');
|
||||||
$response->assertDontSee('Worker Filipino Abu Dhabi');
|
$response->assertDontSee('Worker Filipino Abu Dhabi');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test web workers index main_profession filtering.
|
||||||
|
*/
|
||||||
|
public function test_employer_workers_web_main_profession_filtering()
|
||||||
|
{
|
||||||
|
$response = $this->actingAs($this->employer)
|
||||||
|
->withSession(['user' => $this->employer])
|
||||||
|
->get(route('employer.workers', ['main_profession' => 'Housemaid']));
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertSee('Worker Indian Dubai');
|
||||||
|
$response->assertDontSee('Worker Filipino Abu Dhabi');
|
||||||
|
|
||||||
|
// Test candidate filtering by main_profession
|
||||||
|
$w = Worker::create([
|
||||||
|
'name' => 'Worker Kenyan Hired',
|
||||||
|
'email' => 'worker3@example.com',
|
||||||
|
'phone' => '+971503333333',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
'nationality' => 'Kenyan',
|
||||||
|
'verified' => true,
|
||||||
|
'status' => 'Hired',
|
||||||
|
'preferred_location' => 'Dubai',
|
||||||
|
'preferred_job_type' => 'full-time',
|
||||||
|
'live_in_out' => 'live_out',
|
||||||
|
'in_country' => false,
|
||||||
|
'visa_status' => 'Residence Visa',
|
||||||
|
'main_profession' => 'Cook',
|
||||||
|
'age' => 30,
|
||||||
|
'salary' => 1200,
|
||||||
|
'experience' => 'None',
|
||||||
|
'religion' => 'Christian',
|
||||||
|
'language' => 'SW',
|
||||||
|
'availability' => 'Immediate',
|
||||||
|
'bio' => 'New helper',
|
||||||
|
]);
|
||||||
|
|
||||||
|
JobOffer::create([
|
||||||
|
'employer_id' => $this->employer->id,
|
||||||
|
'worker_id' => $w->id,
|
||||||
|
'work_date' => now()->format('Y-m-d'),
|
||||||
|
'location' => 'Dubai',
|
||||||
|
'salary' => 1200,
|
||||||
|
'notes' => 'Hired',
|
||||||
|
'status' => 'accepted',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response2 = $this->actingAs($this->employer)
|
||||||
|
->withSession(['user' => $this->employer])
|
||||||
|
->get(route('employer.candidates', ['main_profession' => 'Cook']));
|
||||||
|
|
||||||
|
$response2->assertStatus(200);
|
||||||
|
$response2->assertSee('selectedWorkers');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
369
tests/Feature/NewRemindersAndSettingsTest.php
Normal file
369
tests/Feature/NewRemindersAndSettingsTest.php
Normal file
@ -0,0 +1,369 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Worker;
|
||||||
|
use App\Models\JobPost;
|
||||||
|
use App\Models\JobApplication;
|
||||||
|
use App\Models\JobOffer;
|
||||||
|
use App\Models\Conversation;
|
||||||
|
use App\Models\Message;
|
||||||
|
use App\Models\Review;
|
||||||
|
use App\Models\EmployerReview;
|
||||||
|
use App\Notifications\WorkerNoResponseNotification;
|
||||||
|
use App\Notifications\WorkerReviewReminderNotification;
|
||||||
|
use App\Notifications\EmployerReviewReminderNotification;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Facades\Notification;
|
||||||
|
use Illuminate\Support\Facades\Artisan;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class NewRemindersAndSettingsTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
private function createTestWorker(array $overrides = []): Worker
|
||||||
|
{
|
||||||
|
return Worker::create(array_merge([
|
||||||
|
'name' => 'John Worker',
|
||||||
|
'email' => 'worker@example.com',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
'phone' => '971500000001',
|
||||||
|
'nationality' => 'Indian',
|
||||||
|
'status' => 'active',
|
||||||
|
'api_token' => 'worker_api_token',
|
||||||
|
'age' => 25,
|
||||||
|
'salary' => 2000,
|
||||||
|
'availability' => 'Immediate',
|
||||||
|
'experience' => '2 Years',
|
||||||
|
'religion' => 'Christian',
|
||||||
|
'bio' => 'Helper info',
|
||||||
|
'passport_status' => 'valid',
|
||||||
|
], $overrides));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createTestEmployer(array $overrides = []): User
|
||||||
|
{
|
||||||
|
return User::create(array_merge([
|
||||||
|
'name' => 'Alice Employer',
|
||||||
|
'email' => 'employer@example.com',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
'role' => 'employer',
|
||||||
|
'api_token' => 'employer_api_token',
|
||||||
|
'subscription_status' => 'active',
|
||||||
|
], $overrides));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_worker_no_response_reminders()
|
||||||
|
{
|
||||||
|
Notification::fake();
|
||||||
|
|
||||||
|
$worker = $this->createTestWorker();
|
||||||
|
$employer = $this->createTestEmployer();
|
||||||
|
|
||||||
|
// 1. Create a pending direct Job Offer created exactly 14 days ago
|
||||||
|
$offer = JobOffer::create([
|
||||||
|
'employer_id' => $employer->id,
|
||||||
|
'worker_id' => $worker->id,
|
||||||
|
'work_date' => now()->addDays(5)->toDateString(),
|
||||||
|
'location' => 'Dubai Marina',
|
||||||
|
'salary' => 3000,
|
||||||
|
'status' => 'pending',
|
||||||
|
]);
|
||||||
|
// Force created_at to 14 days ago
|
||||||
|
$offer->created_at = now()->subDays(14);
|
||||||
|
$offer->save();
|
||||||
|
|
||||||
|
// 2. Create a conversation where the last message is from employer, sent 14 days ago
|
||||||
|
$conversation = Conversation::create([
|
||||||
|
'employer_id' => $employer->id,
|
||||||
|
'worker_id' => $worker->id,
|
||||||
|
]);
|
||||||
|
$message = Message::create([
|
||||||
|
'conversation_id' => $conversation->id,
|
||||||
|
'sender_type' => 'App\Models\User',
|
||||||
|
'sender_id' => $employer->id,
|
||||||
|
'text' => 'Hello worker, are you interested?',
|
||||||
|
]);
|
||||||
|
$message->created_at = now()->subDays(14);
|
||||||
|
$message->save();
|
||||||
|
|
||||||
|
// Run scheduler
|
||||||
|
Artisan::call('app:send-reminders');
|
||||||
|
|
||||||
|
// Assert Worker got WorkerNoResponseNotification for both
|
||||||
|
Notification::assertSentTo(
|
||||||
|
$worker,
|
||||||
|
WorkerNoResponseNotification::class,
|
||||||
|
function ($notification) use ($offer) {
|
||||||
|
return $notification->type === 'worker_no_response_offer' &&
|
||||||
|
$notification->daysElapsed === 14 &&
|
||||||
|
$notification->entityId === $offer->id;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Notification::assertSentTo(
|
||||||
|
$worker,
|
||||||
|
WorkerNoResponseNotification::class,
|
||||||
|
function ($notification) use ($message) {
|
||||||
|
return $notification->type === 'worker_no_response_chat' &&
|
||||||
|
$notification->daysElapsed === 14 &&
|
||||||
|
$notification->entityId === $message->id;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert reminder logs were created
|
||||||
|
$this->assertDatabaseHas('reminder_logs', [
|
||||||
|
'user_type' => get_class($worker),
|
||||||
|
'user_id' => $worker->id,
|
||||||
|
'event_type' => 'worker_no_response_offer',
|
||||||
|
'reminder_level' => '14_days',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('reminder_logs', [
|
||||||
|
'user_type' => get_class($worker),
|
||||||
|
'user_id' => $worker->id,
|
||||||
|
'event_type' => 'worker_no_response_chat',
|
||||||
|
'reminder_level' => '14_days',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Run again to verify duplicate prevention
|
||||||
|
Notification::fake();
|
||||||
|
Artisan::call('app:send-reminders');
|
||||||
|
Notification::assertNothingSent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_review_reminders_after_joining_date()
|
||||||
|
{
|
||||||
|
Notification::fake();
|
||||||
|
|
||||||
|
$worker = $this->createTestWorker();
|
||||||
|
$employer = $this->createTestEmployer();
|
||||||
|
|
||||||
|
$job = JobPost::create([
|
||||||
|
'employer_id' => $employer->id,
|
||||||
|
'title' => 'Nanny',
|
||||||
|
'location' => 'Dubai',
|
||||||
|
'salary' => 2500,
|
||||||
|
'workers_needed' => 1,
|
||||||
|
'job_type' => 'Full Time',
|
||||||
|
'start_date' => now()->subMonths(4)->toDateString(),
|
||||||
|
'description' => 'Test job',
|
||||||
|
'status' => 'active',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// JobApplication with joining_confirmed_at exactly 3 months ago
|
||||||
|
$app = JobApplication::create([
|
||||||
|
'job_id' => $job->id,
|
||||||
|
'worker_id' => $worker->id,
|
||||||
|
'status' => 'hired',
|
||||||
|
'joining_confirmed_at' => now()->subMonths(3)->toDateString(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Run scheduler
|
||||||
|
Artisan::call('app:send-reminders');
|
||||||
|
|
||||||
|
// Assert Worker got review reminder for employer
|
||||||
|
Notification::assertSentTo(
|
||||||
|
$worker,
|
||||||
|
WorkerReviewReminderNotification::class,
|
||||||
|
function ($notification) use ($app) {
|
||||||
|
return $notification->employerName === 'Alice Employer' &&
|
||||||
|
$notification->applicationId === $app->id &&
|
||||||
|
$notification->reviewLevel === '3_months';
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert Employer got review reminder for worker
|
||||||
|
Notification::assertSentTo(
|
||||||
|
$employer,
|
||||||
|
EmployerReviewReminderNotification::class,
|
||||||
|
function ($notification) use ($app) {
|
||||||
|
return $notification->workerName === 'John Worker' &&
|
||||||
|
$notification->applicationId === $app->id &&
|
||||||
|
$notification->reviewLevel === '3_months';
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert reminder logs were created
|
||||||
|
$this->assertDatabaseHas('reminder_logs', [
|
||||||
|
'user_type' => get_class($worker),
|
||||||
|
'user_id' => $worker->id,
|
||||||
|
'event_type' => 'review_employer_reminder',
|
||||||
|
'reminder_level' => '3_months',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('reminder_logs', [
|
||||||
|
'user_type' => get_class($employer),
|
||||||
|
'user_id' => $employer->id,
|
||||||
|
'event_type' => 'review_worker_reminder',
|
||||||
|
'reminder_level' => '3_months',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Run again to verify duplicate prevention
|
||||||
|
Notification::fake();
|
||||||
|
Artisan::call('app:send-reminders');
|
||||||
|
Notification::assertNothingSent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_worker_review_controller_eligibility_and_edit_window()
|
||||||
|
{
|
||||||
|
$worker = $this->createTestWorker();
|
||||||
|
$employer = $this->createTestEmployer();
|
||||||
|
|
||||||
|
$job = JobPost::create([
|
||||||
|
'employer_id' => $employer->id,
|
||||||
|
'title' => 'Test Job',
|
||||||
|
'location' => 'Dubai',
|
||||||
|
'salary' => 2500,
|
||||||
|
'workers_needed' => 1,
|
||||||
|
'job_type' => 'Full Time',
|
||||||
|
'start_date' => now()->subMonths(4)->toDateString(),
|
||||||
|
'description' => 'Test Description',
|
||||||
|
'status' => 'active',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 1. Cannot review before eligibility period (set eligibility to 3 months)
|
||||||
|
$app = JobApplication::create([
|
||||||
|
'job_id' => $job->id,
|
||||||
|
'worker_id' => $worker->id,
|
||||||
|
'status' => 'hired',
|
||||||
|
'joining_confirmed_at' => now()->subMonths(2)->toDateString(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->postJson('/api/workers/reviews', [
|
||||||
|
'employer_id' => $employer->id,
|
||||||
|
'job_id' => $job->id,
|
||||||
|
'rating' => 5,
|
||||||
|
'comment' => 'Great employer!',
|
||||||
|
], [
|
||||||
|
'Authorization' => 'Bearer worker_api_token'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(403);
|
||||||
|
$response->assertJsonPath('success', false);
|
||||||
|
$response->assertJsonPath('message', 'You can write a review only after 3 months from the joining date.');
|
||||||
|
|
||||||
|
// 2. Can review after eligibility period
|
||||||
|
$app->joining_confirmed_at = now()->subMonths(3)->subDays(5)->toDateString();
|
||||||
|
$app->save();
|
||||||
|
|
||||||
|
$response = $this->postJson('/api/workers/reviews', [
|
||||||
|
'employer_id' => $employer->id,
|
||||||
|
'job_id' => $job->id,
|
||||||
|
'rating' => 5,
|
||||||
|
'comment' => 'Great employer!',
|
||||||
|
], [
|
||||||
|
'Authorization' => 'Bearer worker_api_token'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(201);
|
||||||
|
$response->assertJsonPath('success', true);
|
||||||
|
$reviewId = $response->json('data.review.id');
|
||||||
|
|
||||||
|
// 3. Edit review within 7 days
|
||||||
|
$response = $this->putJson("/api/workers/reviews/{$reviewId}", [
|
||||||
|
'rating' => 4,
|
||||||
|
'comment' => 'Good employer indeed.',
|
||||||
|
], [
|
||||||
|
'Authorization' => 'Bearer worker_api_token'
|
||||||
|
]);
|
||||||
|
$response->assertStatus(200);
|
||||||
|
|
||||||
|
// 4. Cannot edit review after 7 days
|
||||||
|
$review = EmployerReview::find($reviewId);
|
||||||
|
$review->created_at = now()->subDays(8);
|
||||||
|
$review->save();
|
||||||
|
|
||||||
|
$response = $this->putJson("/api/workers/reviews/{$reviewId}", [
|
||||||
|
'rating' => 3,
|
||||||
|
'comment' => 'Okay employer.',
|
||||||
|
], [
|
||||||
|
'Authorization' => 'Bearer worker_api_token'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(403);
|
||||||
|
$response->assertJsonPath('success', false);
|
||||||
|
$response->assertJsonPath('message', 'The 7-day edit window for this review has expired.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_employer_review_controller_eligibility_and_edit_window()
|
||||||
|
{
|
||||||
|
$worker = $this->createTestWorker();
|
||||||
|
$employer = $this->createTestEmployer();
|
||||||
|
|
||||||
|
$job = JobPost::create([
|
||||||
|
'employer_id' => $employer->id,
|
||||||
|
'title' => 'Test Job',
|
||||||
|
'location' => 'Dubai',
|
||||||
|
'salary' => 2500,
|
||||||
|
'workers_needed' => 1,
|
||||||
|
'job_type' => 'Full Time',
|
||||||
|
'start_date' => now()->subMonths(4)->toDateString(),
|
||||||
|
'description' => 'Test Description',
|
||||||
|
'status' => 'active',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 1. Cannot review before eligibility period (set eligibility to 3 months)
|
||||||
|
$app = JobApplication::create([
|
||||||
|
'job_id' => $job->id,
|
||||||
|
'worker_id' => $worker->id,
|
||||||
|
'status' => 'hired',
|
||||||
|
'joining_confirmed_at' => now()->subMonths(2)->toDateString(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->postJson('/api/employers/reviews', [
|
||||||
|
'worker_id' => $worker->id,
|
||||||
|
'rating' => 5,
|
||||||
|
'comment' => 'Great worker!',
|
||||||
|
], [
|
||||||
|
'Authorization' => 'Bearer employer_api_token'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(403);
|
||||||
|
$response->assertJsonPath('success', false);
|
||||||
|
$response->assertJsonPath('message', 'You can write a review only after 3 months from the joining date.');
|
||||||
|
|
||||||
|
// 2. Can review after eligibility period
|
||||||
|
$app->joining_confirmed_at = now()->subMonths(3)->subDays(5)->toDateString();
|
||||||
|
$app->save();
|
||||||
|
|
||||||
|
$response = $this->postJson('/api/employers/reviews', [
|
||||||
|
'worker_id' => $worker->id,
|
||||||
|
'rating' => 5,
|
||||||
|
'comment' => 'Great worker!',
|
||||||
|
], [
|
||||||
|
'Authorization' => 'Bearer employer_api_token'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(201);
|
||||||
|
$response->assertJsonPath('success', true);
|
||||||
|
$reviewId = $response->json('data.review.id');
|
||||||
|
|
||||||
|
// 3. Edit review within 7 days
|
||||||
|
$response = $this->putJson("/api/employers/reviews/{$reviewId}", [
|
||||||
|
'rating' => 4,
|
||||||
|
'comment' => 'Good worker indeed.',
|
||||||
|
], [
|
||||||
|
'Authorization' => 'Bearer employer_api_token'
|
||||||
|
]);
|
||||||
|
$response->assertStatus(200);
|
||||||
|
|
||||||
|
// 4. Cannot edit review after 7 days
|
||||||
|
$review = Review::find($reviewId);
|
||||||
|
$review->created_at = now()->subDays(8);
|
||||||
|
$review->save();
|
||||||
|
|
||||||
|
$response = $this->putJson("/api/employers/reviews/{$reviewId}", [
|
||||||
|
'rating' => 3,
|
||||||
|
'comment' => 'Okay worker.',
|
||||||
|
], [
|
||||||
|
'Authorization' => 'Bearer employer_api_token'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(403);
|
||||||
|
$response->assertJsonPath('success', false);
|
||||||
|
$response->assertJsonPath('message', 'The 7-day edit window for this review has expired.');
|
||||||
|
}
|
||||||
|
}
|
||||||
248
tests/Feature/NotificationApiTest.php
Normal file
248
tests/Feature/NotificationApiTest.php
Normal file
@ -0,0 +1,248 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Worker;
|
||||||
|
use App\Notifications\DocumentExpiryNotification;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class NotificationApiTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
private function createTestWorker(array $overrides = []): Worker
|
||||||
|
{
|
||||||
|
return Worker::create(array_merge([
|
||||||
|
'name' => 'John Worker',
|
||||||
|
'email' => 'worker@example.com',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
'phone' => '971500000001',
|
||||||
|
'nationality' => 'Indian',
|
||||||
|
'status' => 'active',
|
||||||
|
'api_token' => 'worker_api_token',
|
||||||
|
'age' => 25,
|
||||||
|
'salary' => 2000,
|
||||||
|
'availability' => 'Immediate',
|
||||||
|
'experience' => '2 Years',
|
||||||
|
'religion' => 'Christian',
|
||||||
|
'bio' => 'Helper info',
|
||||||
|
'passport_status' => 'valid',
|
||||||
|
], $overrides));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createTestEmployer(array $overrides = []): User
|
||||||
|
{
|
||||||
|
return User::create(array_merge([
|
||||||
|
'name' => 'Alice Employer',
|
||||||
|
'email' => 'employer@example.com',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
'role' => 'employer',
|
||||||
|
'api_token' => 'employer_api_token',
|
||||||
|
'subscription_status' => 'active',
|
||||||
|
], $overrides));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_worker_can_list_notifications_with_filters()
|
||||||
|
{
|
||||||
|
$worker = $this->createTestWorker();
|
||||||
|
|
||||||
|
// Send two notifications
|
||||||
|
$worker->notify(new DocumentExpiryNotification('Passport', '2026-12-31', 30, '30_days'));
|
||||||
|
$worker->notify(new DocumentExpiryNotification('Visa', '2026-12-31', 7, '7_days'));
|
||||||
|
|
||||||
|
// Mark the Passport notification as read deterministically
|
||||||
|
$passportNotification = $worker->unreadNotifications()->where('data->document_type', 'Passport')->first();
|
||||||
|
$passportNotification->markAsRead();
|
||||||
|
|
||||||
|
// 1. Fetch all notifications
|
||||||
|
$response = $this->getJson('/api/workers/notifications', [
|
||||||
|
'Authorization' => 'Bearer worker_api_token'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJsonPath('success', true);
|
||||||
|
$response->assertJsonCount(2, 'data.notifications');
|
||||||
|
|
||||||
|
// 2. Fetch unread notifications
|
||||||
|
$response = $this->getJson('/api/workers/notifications?filter=unread', [
|
||||||
|
'Authorization' => 'Bearer worker_api_token'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJsonCount(1, 'data.notifications');
|
||||||
|
$response->assertJsonPath('data.notifications.0.data.document_type', 'Visa');
|
||||||
|
|
||||||
|
// 3. Fetch read notifications
|
||||||
|
$response = $this->getJson('/api/workers/notifications?filter=read', [
|
||||||
|
'Authorization' => 'Bearer worker_api_token'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJsonCount(1, 'data.notifications');
|
||||||
|
$response->assertJsonPath('data.notifications.0.data.document_type', 'Passport');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_worker_can_mark_single_and_all_notifications_as_read()
|
||||||
|
{
|
||||||
|
$worker = $this->createTestWorker();
|
||||||
|
|
||||||
|
// Send two notifications
|
||||||
|
$worker->notify(new DocumentExpiryNotification('Passport', '2026-12-31', 30, '30_days'));
|
||||||
|
$worker->notify(new DocumentExpiryNotification('Visa', '2026-12-31', 7, '7_days'));
|
||||||
|
|
||||||
|
$unread = $worker->unreadNotifications;
|
||||||
|
$this->assertEquals(2, $unread->count());
|
||||||
|
|
||||||
|
// 1. Mark single notification as read
|
||||||
|
$notificationId = $unread->first()->id;
|
||||||
|
$response = $this->putJson("/api/workers/notifications/{$notificationId}/read", [], [
|
||||||
|
'Authorization' => 'Bearer worker_api_token'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJsonPath('success', true);
|
||||||
|
$this->assertEquals(1, $worker->fresh()->unreadNotifications->count());
|
||||||
|
|
||||||
|
// 2. Mark all notifications as read
|
||||||
|
$response = $this->putJson('/api/workers/notifications/read', [], [
|
||||||
|
'Authorization' => 'Bearer worker_api_token'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJsonPath('success', true);
|
||||||
|
$this->assertEquals(0, $worker->fresh()->unreadNotifications->count());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_worker_can_delete_single_and_all_notifications()
|
||||||
|
{
|
||||||
|
$worker = $this->createTestWorker();
|
||||||
|
|
||||||
|
// Send two notifications
|
||||||
|
$worker->notify(new DocumentExpiryNotification('Passport', '2026-12-31', 30, '30_days'));
|
||||||
|
$worker->notify(new DocumentExpiryNotification('Visa', '2026-12-31', 7, '7_days'));
|
||||||
|
|
||||||
|
$this->assertEquals(2, $worker->notifications()->count());
|
||||||
|
|
||||||
|
// 1. Delete single notification
|
||||||
|
$notificationId = $worker->notifications->first()->id;
|
||||||
|
$response = $this->deleteJson("/api/workers/notifications/{$notificationId}", [], [
|
||||||
|
'Authorization' => 'Bearer worker_api_token'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJsonPath('success', true);
|
||||||
|
$this->assertEquals(1, $worker->fresh()->notifications()->count());
|
||||||
|
|
||||||
|
// 2. Delete all notifications
|
||||||
|
$response = $this->deleteJson('/api/workers/notifications', [], [
|
||||||
|
'Authorization' => 'Bearer worker_api_token'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJsonPath('success', true);
|
||||||
|
$this->assertEquals(0, $worker->fresh()->notifications()->count());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_employer_can_list_notifications_with_filters()
|
||||||
|
{
|
||||||
|
$employer = $this->createTestEmployer();
|
||||||
|
|
||||||
|
// Send two notifications
|
||||||
|
$employer->notify(new DocumentExpiryNotification('Emirates ID', '2026-12-31', 30, '30_days'));
|
||||||
|
$employer->notify(new DocumentExpiryNotification('Trade License', '2026-12-31', 7, '7_days'));
|
||||||
|
|
||||||
|
// Mark the Emirates ID notification as read deterministically
|
||||||
|
$emiratesIdNotification = $employer->unreadNotifications()->where('data->document_type', 'Emirates ID')->first();
|
||||||
|
$emiratesIdNotification->markAsRead();
|
||||||
|
|
||||||
|
// 1. Fetch all notifications
|
||||||
|
$response = $this->getJson('/api/employers/notifications', [
|
||||||
|
'Authorization' => 'Bearer employer_api_token'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJsonPath('success', true);
|
||||||
|
$response->assertJsonCount(2, 'data.notifications');
|
||||||
|
|
||||||
|
// 2. Fetch unread notifications
|
||||||
|
$response = $this->getJson('/api/employers/notifications?filter=unread', [
|
||||||
|
'Authorization' => 'Bearer employer_api_token'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJsonCount(1, 'data.notifications');
|
||||||
|
$response->assertJsonPath('data.notifications.0.data.document_type', 'Trade License');
|
||||||
|
|
||||||
|
// 3. Fetch read notifications
|
||||||
|
$response = $this->getJson('/api/employers/notifications?filter=read', [
|
||||||
|
'Authorization' => 'Bearer employer_api_token'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJsonCount(1, 'data.notifications');
|
||||||
|
$response->assertJsonPath('data.notifications.0.data.document_type', 'Emirates ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_employer_can_mark_single_and_all_notifications_as_read()
|
||||||
|
{
|
||||||
|
$employer = $this->createTestEmployer();
|
||||||
|
|
||||||
|
// Send two notifications
|
||||||
|
$employer->notify(new DocumentExpiryNotification('Emirates ID', '2026-12-31', 30, '30_days'));
|
||||||
|
$employer->notify(new DocumentExpiryNotification('Trade License', '2026-12-31', 7, '7_days'));
|
||||||
|
|
||||||
|
$unread = $employer->unreadNotifications;
|
||||||
|
$this->assertEquals(2, $unread->count());
|
||||||
|
|
||||||
|
// 1. Mark single notification as read
|
||||||
|
$notificationId = $unread->first()->id;
|
||||||
|
$response = $this->putJson("/api/employers/notifications/{$notificationId}/read", [], [
|
||||||
|
'Authorization' => 'Bearer employer_api_token'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJsonPath('success', true);
|
||||||
|
$this->assertEquals(1, $employer->fresh()->unreadNotifications->count());
|
||||||
|
|
||||||
|
// 2. Mark all notifications as read
|
||||||
|
$response = $this->putJson('/api/employers/notifications/read', [], [
|
||||||
|
'Authorization' => 'Bearer employer_api_token'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJsonPath('success', true);
|
||||||
|
$this->assertEquals(0, $employer->fresh()->unreadNotifications->count());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_employer_can_delete_single_and_all_notifications()
|
||||||
|
{
|
||||||
|
$employer = $this->createTestEmployer();
|
||||||
|
|
||||||
|
// Send two notifications
|
||||||
|
$employer->notify(new DocumentExpiryNotification('Emirates ID', '2026-12-31', 30, '30_days'));
|
||||||
|
$employer->notify(new DocumentExpiryNotification('Trade License', '2026-12-31', 7, '7_days'));
|
||||||
|
|
||||||
|
$this->assertEquals(2, $employer->notifications()->count());
|
||||||
|
|
||||||
|
// 1. Delete single notification
|
||||||
|
$notificationId = $employer->notifications->first()->id;
|
||||||
|
$response = $this->deleteJson("/api/employers/notifications/{$notificationId}", [], [
|
||||||
|
'Authorization' => 'Bearer employer_api_token'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJsonPath('success', true);
|
||||||
|
$this->assertEquals(1, $employer->fresh()->notifications()->count());
|
||||||
|
|
||||||
|
// 2. Delete all notifications
|
||||||
|
$response = $this->deleteJson('/api/employers/notifications', [], [
|
||||||
|
'Authorization' => 'Bearer employer_api_token'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJsonPath('success', true);
|
||||||
|
$this->assertEquals(0, $employer->fresh()->notifications()->count());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -226,6 +226,9 @@ public function test_sponsor_can_post_charity_event()
|
|||||||
'provided_items' => 'Free Iftar meals, Water, Dates',
|
'provided_items' => 'Free Iftar meals, Water, Dates',
|
||||||
'location_details' => 'Al Quoz Community Center, Dubai',
|
'location_details' => 'Al Quoz Community Center, Dubai',
|
||||||
'location_pin' => 'https://maps.app.goo.gl/xyz',
|
'location_pin' => 'https://maps.app.goo.gl/xyz',
|
||||||
|
'contact_person_name' => 'Jane Doe',
|
||||||
|
'contact_number' => '551234567',
|
||||||
|
'country_code' => '+971',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response->assertStatus(201)
|
$response->assertStatus(201)
|
||||||
@ -246,6 +249,9 @@ public function test_sponsor_can_post_charity_event()
|
|||||||
'location_details' => 'Al Quoz Community Center, Dubai',
|
'location_details' => 'Al Quoz Community Center, Dubai',
|
||||||
'location_pin' => 'https://maps.app.goo.gl/xyz',
|
'location_pin' => 'https://maps.app.goo.gl/xyz',
|
||||||
'content' => 'Join us for a free community Iftar gathering at the center.',
|
'content' => 'Join us for a free community Iftar gathering at the center.',
|
||||||
|
'contact_person_name' => 'Jane Doe',
|
||||||
|
'contact_number' => '551234567',
|
||||||
|
'country_code' => '+971',
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
])
|
])
|
||||||
@ -266,6 +272,9 @@ public function test_sponsor_can_post_charity_event()
|
|||||||
'location_details',
|
'location_details',
|
||||||
'location_pin',
|
'location_pin',
|
||||||
'content',
|
'content',
|
||||||
|
'contact_person_name',
|
||||||
|
'contact_number',
|
||||||
|
'country_code',
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
@ -277,6 +286,82 @@ public function test_sponsor_can_post_charity_event()
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test validation rules for charity event posting.
|
||||||
|
*/
|
||||||
|
public function test_sponsor_charity_event_validation()
|
||||||
|
{
|
||||||
|
$sponsor = Sponsor::create([
|
||||||
|
'full_name' => 'Test Sponsor',
|
||||||
|
'organization_name' => 'Save the Children',
|
||||||
|
'email' => 'tsponsor@example.com',
|
||||||
|
'mobile' => '+971509994444',
|
||||||
|
'password' => Hash::make('password123'),
|
||||||
|
'api_token' => 'sponsor_token_xyz',
|
||||||
|
'status' => 'active',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 1. Missing fields
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer sponsor_token_xyz',
|
||||||
|
])->postJson('/api/sponsors/charity-events', [
|
||||||
|
'title' => 'Community Ramadan Iftar',
|
||||||
|
'body' => 'Join us for a free community Iftar gathering at the center.',
|
||||||
|
'type' => 'charity',
|
||||||
|
'event_date' => '2026-06-15',
|
||||||
|
'start_time' => '09:00 AM',
|
||||||
|
'end_time' => '04:00 PM',
|
||||||
|
'provided_items' => 'Free Iftar meals, Water, Dates',
|
||||||
|
'location_details' => 'Al Quoz Community Center, Dubai',
|
||||||
|
'location_pin' => 'https://maps.app.goo.gl/xyz',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(422)
|
||||||
|
->assertJsonValidationErrors(['contact_person_name', 'contact_number', 'country_code']);
|
||||||
|
|
||||||
|
// 2. Invalid country code format (missing +)
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer sponsor_token_xyz',
|
||||||
|
])->postJson('/api/sponsors/charity-events', [
|
||||||
|
'title' => 'Community Ramadan Iftar',
|
||||||
|
'body' => 'Join us for a free community Iftar gathering at the center.',
|
||||||
|
'type' => 'charity',
|
||||||
|
'event_date' => '2026-06-15',
|
||||||
|
'start_time' => '09:00 AM',
|
||||||
|
'end_time' => '04:00 PM',
|
||||||
|
'provided_items' => 'Free Iftar meals, Water, Dates',
|
||||||
|
'location_details' => 'Al Quoz Community Center, Dubai',
|
||||||
|
'location_pin' => 'https://maps.app.goo.gl/xyz',
|
||||||
|
'contact_person_name' => 'Jane Doe',
|
||||||
|
'contact_number' => '551234567',
|
||||||
|
'country_code' => '971', // missing plus
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(422)
|
||||||
|
->assertJsonValidationErrors(['country_code']);
|
||||||
|
|
||||||
|
// 3. Invalid contact number format (non-digits)
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer sponsor_token_xyz',
|
||||||
|
])->postJson('/api/sponsors/charity-events', [
|
||||||
|
'title' => 'Community Ramadan Iftar',
|
||||||
|
'body' => 'Join us for a free community Iftar gathering at the center.',
|
||||||
|
'type' => 'charity',
|
||||||
|
'event_date' => '2026-06-15',
|
||||||
|
'start_time' => '09:00 AM',
|
||||||
|
'end_time' => '04:00 PM',
|
||||||
|
'provided_items' => 'Free Iftar meals, Water, Dates',
|
||||||
|
'location_details' => 'Al Quoz Community Center, Dubai',
|
||||||
|
'location_pin' => 'https://maps.app.goo.gl/xyz',
|
||||||
|
'contact_person_name' => 'Jane Doe',
|
||||||
|
'contact_number' => '55-123-abc', // invalid format
|
||||||
|
'country_code' => '+971',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(422)
|
||||||
|
->assertJsonValidationErrors(['contact_number']);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test that a sponsor can list charity events showing both employer and sponsor events.
|
* Test that a sponsor can list charity events showing both employer and sponsor events.
|
||||||
*/
|
*/
|
||||||
|
|||||||
239
tests/Feature/WorkerEmployersApiTest.php
Normal file
239
tests/Feature/WorkerEmployersApiTest.php
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
use App\Models\Worker;
|
||||||
|
use App\Models\WorkerDocument;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\JobOffer;
|
||||||
|
use App\Models\EmployerProfile;
|
||||||
|
|
||||||
|
class WorkerEmployersApiTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
public function test_worker_dashboard_current_employer_detail()
|
||||||
|
{
|
||||||
|
// Create worker
|
||||||
|
$worker = Worker::create([
|
||||||
|
'name' => 'Rahul Sharma',
|
||||||
|
'email' => 'rahul@example.com',
|
||||||
|
'phone' => '+971501234567',
|
||||||
|
'language' => 'HI',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
'nationality' => 'Indian',
|
||||||
|
'age' => 25,
|
||||||
|
'salary' => 1500,
|
||||||
|
'availability' => 'Immediate',
|
||||||
|
'experience' => 'Not Specified',
|
||||||
|
'religion' => 'Not Specified',
|
||||||
|
'bio' => 'Test',
|
||||||
|
'verified' => true,
|
||||||
|
'status' => 'active',
|
||||||
|
'api_token' => 'worker-test-token',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create passport document to prevent pending 404
|
||||||
|
WorkerDocument::create([
|
||||||
|
'worker_id' => $worker->id,
|
||||||
|
'type' => 'passport',
|
||||||
|
'number' => '123456',
|
||||||
|
'file_path' => 'passports/test.pdf',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create an employer
|
||||||
|
$employer = User::create([
|
||||||
|
'name' => 'Employer One',
|
||||||
|
'email' => 'emp1@example.com',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
'role' => 'employer',
|
||||||
|
]);
|
||||||
|
|
||||||
|
EmployerProfile::create([
|
||||||
|
'user_id' => $employer->id,
|
||||||
|
'company_name' => 'Ahmad Tech Ltd',
|
||||||
|
'nationality' => 'UAE',
|
||||||
|
'city' => 'Dubai',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create an accepted job offer (current employer)
|
||||||
|
JobOffer::create([
|
||||||
|
'employer_id' => $employer->id,
|
||||||
|
'worker_id' => $worker->id,
|
||||||
|
'work_date' => now()->subDays(10),
|
||||||
|
'location' => 'Dubai',
|
||||||
|
'salary' => 2500,
|
||||||
|
'status' => 'accepted',
|
||||||
|
'notes' => 'Current Active Job',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Hit the worker dashboard API
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer worker-test-token',
|
||||||
|
])->getJson('/api/workers/dashboard');
|
||||||
|
|
||||||
|
$response->assertStatus(200)
|
||||||
|
->assertJson([
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
'current_employer' => [
|
||||||
|
'id' => $employer->id,
|
||||||
|
'name' => 'Employer One',
|
||||||
|
'company_name' => 'Ahmad Tech Ltd',
|
||||||
|
'city' => 'Dubai',
|
||||||
|
'nationality' => 'UAE',
|
||||||
|
],
|
||||||
|
'currently_working_sponsor' => [
|
||||||
|
'id' => $employer->id,
|
||||||
|
'name' => 'Employer One',
|
||||||
|
'company_name' => 'Ahmad Tech Ltd',
|
||||||
|
'city' => 'Dubai',
|
||||||
|
'nationality' => 'UAE',
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_worker_employers_list_pagination_and_sorting()
|
||||||
|
{
|
||||||
|
// Create worker
|
||||||
|
$worker = Worker::create([
|
||||||
|
'name' => 'Rahul Sharma',
|
||||||
|
'email' => 'rahul@example.com',
|
||||||
|
'phone' => '+971501234567',
|
||||||
|
'language' => 'HI',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
'nationality' => 'Indian',
|
||||||
|
'age' => 25,
|
||||||
|
'salary' => 1500,
|
||||||
|
'availability' => 'Immediate',
|
||||||
|
'experience' => 'Not Specified',
|
||||||
|
'religion' => 'Not Specified',
|
||||||
|
'bio' => 'Test',
|
||||||
|
'verified' => true,
|
||||||
|
'status' => 'active',
|
||||||
|
'api_token' => 'worker-test-token',
|
||||||
|
]);
|
||||||
|
|
||||||
|
WorkerDocument::create([
|
||||||
|
'worker_id' => $worker->id,
|
||||||
|
'type' => 'passport',
|
||||||
|
'number' => '123456',
|
||||||
|
'file_path' => 'passports/test.pdf',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create 3 employers
|
||||||
|
$emp1 = User::create([
|
||||||
|
'name' => 'Employer One',
|
||||||
|
'email' => 'emp1@example.com',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
'role' => 'employer'
|
||||||
|
]);
|
||||||
|
EmployerProfile::create(['user_id' => $emp1->id, 'company_name' => 'Tech One']);
|
||||||
|
|
||||||
|
$emp2 = User::create([
|
||||||
|
'name' => 'Employer Two',
|
||||||
|
'email' => 'emp2@example.com',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
'role' => 'employer'
|
||||||
|
]);
|
||||||
|
EmployerProfile::create(['user_id' => $emp2->id, 'company_name' => 'Tech Two']);
|
||||||
|
|
||||||
|
$emp3 = User::create([
|
||||||
|
'name' => 'Employer Three',
|
||||||
|
'email' => 'emp3@example.com',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
'role' => 'employer'
|
||||||
|
]);
|
||||||
|
EmployerProfile::create(['user_id' => $emp3->id, 'company_name' => 'Tech Three']);
|
||||||
|
|
||||||
|
// Create JobOffers:
|
||||||
|
// Offer 1: Completed status, joining 20 days ago
|
||||||
|
JobOffer::create([
|
||||||
|
'employer_id' => $emp1->id,
|
||||||
|
'worker_id' => $worker->id,
|
||||||
|
'work_date' => now()->subDays(20),
|
||||||
|
'location' => 'Abu Dhabi',
|
||||||
|
'salary' => 2000,
|
||||||
|
'status' => 'completed',
|
||||||
|
'notes' => 'Past job',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Offer 2: Cancelled status, joining 30 days ago
|
||||||
|
JobOffer::create([
|
||||||
|
'employer_id' => $emp2->id,
|
||||||
|
'worker_id' => $worker->id,
|
||||||
|
'work_date' => now()->subDays(30),
|
||||||
|
'location' => 'Sharjah',
|
||||||
|
'salary' => 1800,
|
||||||
|
'status' => 'cancelled',
|
||||||
|
'notes' => 'Cancelled job',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Offer 3: Accepted (Active) status, joining 10 days ago
|
||||||
|
JobOffer::create([
|
||||||
|
'employer_id' => $emp3->id,
|
||||||
|
'worker_id' => $worker->id,
|
||||||
|
'work_date' => now()->subDays(10),
|
||||||
|
'location' => 'Dubai',
|
||||||
|
'salary' => 3000,
|
||||||
|
'status' => 'accepted',
|
||||||
|
'notes' => 'Active job',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// List all employers (should return 3)
|
||||||
|
// With Active employer at the top, then previous employers sorted by work_date desc:
|
||||||
|
// 1st: Emp 3 (Active)
|
||||||
|
// 2nd: Emp 1 (Completed, 20 days ago)
|
||||||
|
// 3rd: Emp 2 (Cancelled, 30 days ago)
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer worker-test-token',
|
||||||
|
])->getJson('/api/workers/employers');
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$data = $response->json('data.employers');
|
||||||
|
|
||||||
|
$this->assertCount(3, $data);
|
||||||
|
$this->assertEquals('Active', $data[0]['status']);
|
||||||
|
$this->assertEquals('Employer Three', $data[0]['employer']['name']);
|
||||||
|
$this->assertEquals('Completed', $data[1]['status']);
|
||||||
|
$this->assertEquals('Employer One', $data[1]['employer']['name']);
|
||||||
|
$this->assertEquals('Cancelled', $data[2]['status']);
|
||||||
|
$this->assertEquals('Employer Two', $data[2]['employer']['name']);
|
||||||
|
|
||||||
|
// Test filtering by status Completed
|
||||||
|
$responseFiltered = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer worker-test-token',
|
||||||
|
])->getJson('/api/workers/employers?status=completed');
|
||||||
|
|
||||||
|
$responseFiltered->assertStatus(200);
|
||||||
|
$filteredData = $responseFiltered->json('data.employers');
|
||||||
|
$this->assertCount(1, $filteredData);
|
||||||
|
$this->assertEquals('Completed', $filteredData[0]['status']);
|
||||||
|
$this->assertEquals('Employer One', $filteredData[0]['employer']['name']);
|
||||||
|
|
||||||
|
// Test filtering by status Active
|
||||||
|
$responseActive = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer worker-test-token',
|
||||||
|
])->getJson('/api/workers/employers?status=active');
|
||||||
|
|
||||||
|
$responseActive->assertStatus(200);
|
||||||
|
$activeData = $responseActive->json('data.employers');
|
||||||
|
$this->assertCount(1, $activeData);
|
||||||
|
$this->assertEquals('Active', $activeData[0]['status']);
|
||||||
|
$this->assertEquals('Employer Three', $activeData[0]['employer']['name']);
|
||||||
|
|
||||||
|
// Test pagination
|
||||||
|
$responsePaginated = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer worker-test-token',
|
||||||
|
])->getJson('/api/workers/employers?per_page=2');
|
||||||
|
|
||||||
|
$responsePaginated->assertStatus(200);
|
||||||
|
$paginatedData = $responsePaginated->json('data.employers');
|
||||||
|
$this->assertCount(2, $paginatedData);
|
||||||
|
$this->assertEquals(3, $responsePaginated->json('data.pagination.total'));
|
||||||
|
$this->assertEquals(2, $responsePaginated->json('data.pagination.per_page'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -507,7 +507,7 @@ public function test_register_passport_and_visa_with_direct_json_payload()
|
|||||||
'nationality' => 'Filipino',
|
'nationality' => 'Filipino',
|
||||||
'age' => $expectedAge,
|
'age' => $expectedAge,
|
||||||
'verified' => true,
|
'verified' => true,
|
||||||
'visa_status' => 'CLEANER',
|
'visa_status' => 'Tourist Visa',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseHas('worker_documents', [
|
$this->assertDatabaseHas('worker_documents', [
|
||||||
@ -636,7 +636,7 @@ public function test_register_visa_with_full_ocr_fields()
|
|||||||
$visaDoc = \App\Models\WorkerDocument::where('worker_id', $workerId)->where('type', 'visa')->first();
|
$visaDoc = \App\Models\WorkerDocument::where('worker_id', $workerId)->where('type', 'visa')->first();
|
||||||
$this->assertNotNull($visaDoc->ocr_data);
|
$this->assertNotNull($visaDoc->ocr_data);
|
||||||
$this->assertEquals('uae_visa', $visaDoc->ocr_data['document_type']);
|
$this->assertEquals('uae_visa', $visaDoc->ocr_data['document_type']);
|
||||||
$this->assertEquals('ENTRY PERMIT', $visaDoc->ocr_data['visa_type']);
|
$this->assertEquals('Tourist Visa', $visaDoc->ocr_data['visa_type']);
|
||||||
$this->assertEquals('Mr.MUHAMMAD NADEEM RASHEED', $visaDoc->ocr_data['full_name']);
|
$this->assertEquals('Mr.MUHAMMAD NADEEM RASHEED', $visaDoc->ocr_data['full_name']);
|
||||||
$this->assertEquals(97.4, $visaDoc->ocr_accuracy);
|
$this->assertEquals(97.4, $visaDoc->ocr_accuracy);
|
||||||
}
|
}
|
||||||
@ -689,7 +689,7 @@ public function test_register_passport_and_visa_with_json_encoded_strings()
|
|||||||
'name' => 'Johnny Test',
|
'name' => 'Johnny Test',
|
||||||
'phone' => '+971509999999',
|
'phone' => '+971509999999',
|
||||||
'verified' => true,
|
'verified' => true,
|
||||||
'visa_status' => 'HELPER',
|
'visa_status' => 'Tourist Visa',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseHas('worker_documents', [
|
$this->assertDatabaseHas('worker_documents', [
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user