diff --git a/app/Http/Controllers/Admin/WorkerController.php b/app/Http/Controllers/Admin/WorkerController.php index 1ac3ee6..f3a99f8 100644 --- a/app/Http/Controllers/Admin/WorkerController.php +++ b/app/Http/Controllers/Admin/WorkerController.php @@ -13,7 +13,7 @@ class WorkerController extends Controller */ public function index() { - $workers = \App\Models\Worker::with(['category', 'skills']) + $workers = \App\Models\Worker::with(['skills']) ->latest() ->get() ->map(function ($worker) { @@ -43,7 +43,6 @@ public function index() 'city' => $worker->city, 'area' => $worker->area, 'live_in_out' => $worker->live_in_out ?? 'Live-in', - 'category' => $worker->category ? $worker->category->name : 'N/A', 'experience' => $worker->experience, 'salary' => (int)$worker->salary, 'skills' => $mappedSkills, @@ -126,7 +125,6 @@ public function updateProfile(Request $request, $id) 'city' => 'nullable|string', 'area' => 'nullable|string', 'live_in_out' => 'nullable|string', - 'category' => 'required|string', 'experience' => 'required|string', 'salary' => 'nullable|numeric', 'bio' => 'nullable|string' @@ -134,10 +132,7 @@ public function updateProfile(Request $request, $id) $worker = \App\Models\Worker::findOrFail($id); - $category = \App\Models\WorkerCategory::where('name', $validated['category'])->first(); - if ($category) { - $worker->category_id = $category->id; - } + $worker->name = $validated['name']; $worker->phone = $validated['phone']; diff --git a/app/Http/Controllers/Api/EmployerAuthController.php b/app/Http/Controllers/Api/EmployerAuthController.php index 22a0768..7cbaf85 100644 --- a/app/Http/Controllers/Api/EmployerAuthController.php +++ b/app/Http/Controllers/Api/EmployerAuthController.php @@ -110,10 +110,22 @@ public function login(Request $request) } $user->update($userUpdateData); if ($sponsor) { - $sponsor->update([ + $sponsorUpdateData = [ 'api_token' => $apiToken, 'last_login_at' => now(), - ]); + ]; + if ($request->has('fcm_token')) { + $sponsorUpdateData['fcm_token'] = $request->fcm_token; + } + $sponsor->update($sponsorUpdateData); + } + + if ($request->filled('fcm_token')) { + \App\Services\FCMService::sendPushNotification( + $request->fcm_token, + 'Successful Login', + 'Welcome back to your Migrant employer account.' + ); } return response()->json([ @@ -127,10 +139,22 @@ public function login(Request $request) ], 200); } else { // Pure Sponsor account - $sponsor->update([ + $sponsorUpdateData = [ 'api_token' => $apiToken, 'last_login_at' => now(), - ]); + ]; + if ($request->has('fcm_token')) { + $sponsorUpdateData['fcm_token'] = $request->fcm_token; + } + $sponsor->update($sponsorUpdateData); + + if ($request->filled('fcm_token')) { + \App\Services\FCMService::sendPushNotification( + $request->fcm_token, + 'Successful Login', + 'Welcome back to your Migrant sponsor account.' + ); + } return response()->json([ 'success' => true, @@ -166,6 +190,7 @@ public function register(Request $request) 'name' => 'required|string|max:255', 'email' => 'required|string|email|max:255|unique:users,email', 'phone' => 'required|string|max:50', + 'fcm_token' => 'nullable|string|max:255', ]); if ($validator->fails()) { @@ -195,6 +220,7 @@ public function register(Request $request) 'role' => 'employer', 'subscription_status' => 'none', 'subscription_expires_at' => null, + 'fcm_token' => $request->fcm_token, ]); // Create pending Profile @@ -223,8 +249,17 @@ public function register(Request $request) 'is_verified' => false, 'otp_verified_at' => null, 'status' => 'inactive', + 'fcm_token' => $request->fcm_token, ]); + if ($request->filled('fcm_token')) { + \App\Services\FCMService::sendPushNotification( + $request->fcm_token, + 'Registration Initiated', + 'Your verification code has been sent to your email.' + ); + } + // Try sending email try { \Illuminate\Support\Facades\Mail::to($request->email)->send(new \App\Mail\EmployerOtpMail( @@ -257,6 +292,7 @@ public function verify(Request $request) $validator = Validator::make($request->all(), [ 'email' => 'required|email', 'otp' => 'required|string|size:6', + 'fcm_token' => 'nullable|string|max:255', ]); if ($validator->fails()) { @@ -293,11 +329,31 @@ public function verify(Request $request) try { // Update Sponsor verification status $sponsor = \App\Models\Sponsor::where('email', $request->email)->first(); + $user = User::where('email', $request->email)->where('role', 'employer')->first(); + $fcmToken = $request->fcm_token; + if ($sponsor) { - $sponsor->update([ + $sponsorUpdateData = [ 'is_verified' => true, 'otp_verified_at' => now(), - ]); + ]; + if ($fcmToken) { + $sponsorUpdateData['fcm_token'] = $fcmToken; + } + $sponsor->update($sponsorUpdateData); + } + + if ($user && $fcmToken) { + $user->update(['fcm_token' => $fcmToken]); + } + + $tokenToSend = $fcmToken ?? ($sponsor ? $sponsor->fcm_token : null) ?? ($user ? $user->fcm_token : null); + if ($tokenToSend) { + \App\Services\FCMService::sendPushNotification( + $tokenToSend, + 'Email Verified', + 'Proceed to payment selection.' + ); } // Clear Cache @@ -494,6 +550,7 @@ public function password(Request $request) $validator = Validator::make($request->all(), [ 'email' => 'required|email', 'password' => 'required|string|min:8|confirmed', + 'fcm_token' => 'nullable|string|max:255', ]); if ($validator->fails()) { @@ -526,16 +583,25 @@ public function password(Request $request) \Illuminate\Support\Facades\DB::transaction(function () use ($request, $user, $sponsor, $apiToken) { $hashedPassword = Hash::make($request->password); + $fcmToken = $request->fcm_token; - $user->update([ + $userUpdateData = [ 'password' => $hashedPassword, 'api_token' => $apiToken, - ]); + ]; + if ($fcmToken) { + $userUpdateData['fcm_token'] = $fcmToken; + } + $user->update($userUpdateData); - $sponsor->update([ + $sponsorUpdateData = [ 'password' => $hashedPassword, 'status' => 'active', - ]); + ]; + if ($fcmToken) { + $sponsorUpdateData['fcm_token'] = $fcmToken; + } + $sponsor->update($sponsorUpdateData); // Approve profile verification $profile = EmployerProfile::where('user_id', $user->id)->first(); @@ -546,6 +612,15 @@ public function password(Request $request) } }); + $tokenToSend = $request->fcm_token ?? ($user ? $user->fcm_token : null) ?? ($sponsor ? $sponsor->fcm_token : null); + if ($tokenToSend) { + \App\Services\FCMService::sendPushNotification( + $tokenToSend, + 'Welcome to Migrant', + 'Your employer registration has been completed successfully.' + ); + } + return response()->json([ 'success' => true, 'message' => 'Password created successfully. Registration finalized.', diff --git a/app/Http/Controllers/Api/EmployerMessageController.php b/app/Http/Controllers/Api/EmployerMessageController.php index 23fff0f..5f66ffe 100644 --- a/app/Http/Controllers/Api/EmployerMessageController.php +++ b/app/Http/Controllers/Api/EmployerMessageController.php @@ -83,7 +83,7 @@ public function getConversations(Request $request) try { $dbConversations = Conversation::where('employer_id', $employer->id) - ->with(['worker.category', 'messages']) + ->with(['worker', 'messages']) ->latest('updated_at') ->get(); @@ -99,7 +99,6 @@ public function getConversations(Request $request) 'id' => $conv->id, 'worker_id' => $conv->worker_id, 'worker_name' => $conv->worker->name ?? 'Candidate', - 'category' => $conv->worker->category->name ?? 'General Helper', 'nationality' => $conv->worker->nationality ?? 'Unknown', 'worker_status' => strtolower($conv->worker->status ?? 'active'), 'salary' => ($conv->worker->salary ?? $conv->worker->expected_salary ?? 2000) . ' AED', @@ -143,7 +142,7 @@ public function getMessages(Request $request, $id) try { $conv = Conversation::where('employer_id', $employer->id) ->where('id', $id) - ->with(['worker.category', 'messages']) + ->with(['worker', 'messages']) ->first(); if (!$conv) { @@ -175,7 +174,6 @@ public function getMessages(Request $request, $id) $conversationDetail = [ 'id' => $conv->id, 'worker_name' => $conv->worker->name ?? 'Candidate', - 'category' => $conv->worker->category->name ?? 'General Helper', 'nationality' => $conv->worker->nationality ?? 'Unknown', 'worker_status' => strtolower($conv->worker->status ?? 'active'), 'salary' => ($conv->worker->salary ?? $conv->worker->expected_salary ?? 2000) . ' AED', diff --git a/app/Http/Controllers/Api/EmployerProfileController.php b/app/Http/Controllers/Api/EmployerProfileController.php index 8a7e567..84a72d2 100644 --- a/app/Http/Controllers/Api/EmployerProfileController.php +++ b/app/Http/Controllers/Api/EmployerProfileController.php @@ -224,7 +224,7 @@ public function getDashboard(Request $request) ]; // Recent Announcements / Events - $dbAnnouncements = \App\Models\Announcement::where('status', 'approved')->latest()->limit(5)->get(); + $dbAnnouncements = \App\Models\Announcement::where('status', 'approved')->latest()->limit(2)->get(); $recentAnnouncements = $dbAnnouncements->map(function ($ann) { $body = $ann->body; $eventDate = null; diff --git a/app/Http/Controllers/Api/EmployerWorkerController.php b/app/Http/Controllers/Api/EmployerWorkerController.php index 8bd69cf..ac22b60 100644 --- a/app/Http/Controllers/Api/EmployerWorkerController.php +++ b/app/Http/Controllers/Api/EmployerWorkerController.php @@ -6,7 +6,6 @@ use Illuminate\Http\Request; use App\Models\User; use App\Models\Worker; -use App\Models\WorkerCategory; use App\Models\Shortlist; use App\Models\JobOffer; use App\Models\JobApplication; @@ -91,7 +90,7 @@ private function formatWorker(Worker $w) public function getWorkers(Request $request) { try { - $query = Worker::with(['category', 'skills', 'documents']) + $query = Worker::with(['skills', 'documents']) ->where('status', '!=', 'Hired') ->where('status', '!=', 'hidden'); @@ -355,11 +354,11 @@ public function getCandidates(Request $request) // Fetch Job Applications $jobIds = JobPost::where('employer_id', $employerId)->pluck('id'); $applicationsQuery = JobApplication::whereIn('job_id', $jobIds) - ->with(['worker.category', 'jobPost']); + ->with(['worker', 'jobPost']); // Fetch Direct Hiring Offers $directOffersQuery = JobOffer::where('employer_id', $employerId) - ->with('worker.category'); + ->with('worker'); // Apply search filter if provided if ($request->filled('search')) { @@ -841,7 +840,7 @@ public function getWorkerDetail(Request $request, $id) { /** @var User $employer */ $employer = $request->attributes->get('employer'); - $w = Worker::with(['category', 'skills', 'documents'])->find($id); + $w = Worker::with(['skills', 'documents'])->find($id); if (!$w) { return response()->json([ @@ -919,7 +918,7 @@ public function getWorkerDetail(Request $request, $id) } // Similar workers matching web view - $simDb = Worker::with('category') + $simDb = Worker::query() ->where('id', '!=', $w->id) ->where('status', '!=', 'Hired') ->where('status', '!=', 'hidden') @@ -1002,7 +1001,7 @@ public function getShortlist(Request $request) $perPage = (int)$request->input('per_page', 15); $shortlists = Shortlist::where('employer_id', $employer->id) - ->with(['worker.category', 'worker.skills']) + ->with(['worker.skills']) ->get(); $formattedWorkers = $shortlists->map(function ($s) { diff --git a/app/Http/Controllers/Api/SponsorAuthController.php b/app/Http/Controllers/Api/SponsorAuthController.php index 5660945..0efebd1 100644 --- a/app/Http/Controllers/Api/SponsorAuthController.php +++ b/app/Http/Controllers/Api/SponsorAuthController.php @@ -35,6 +35,7 @@ public function register(Request $request) 'address' => 'required|string|max:255', 'country_code' => 'required|string|max:10', 'license_expiry' => 'required|date_format:Y-m-d', + 'fcm_token' => 'nullable|string|max:255', ], [ 'mobile.unique' => 'This mobile number is already registered.', 'email.unique' => 'This email address is already registered.', @@ -98,9 +99,18 @@ public function register(Request $request) 'is_verified' => false, 'subscription_status' => 'none', 'api_token' => $apiToken, + 'fcm_token' => $request->fcm_token, ]); }); + if ($request->filled('fcm_token')) { + \App\Services\FCMService::sendPushNotification( + $request->fcm_token, + 'Welcome to Migrant', + 'Sponsor account registered successfully. Your license is pending admin review.' + ); + } + return response()->json([ 'success' => true, 'message' => 'Sponsor account registered successfully. Your license is pending admin review.', @@ -129,8 +139,9 @@ public function register(Request $request) public function login(Request $request) { $validator = Validator::make($request->all(), [ - 'mobile' => 'required|string', - 'password' => 'required|string', + 'mobile' => 'required|string', + 'password' => 'required|string', + 'fcm_token' => 'nullable|string|max:255', ]); if ($validator->fails()) { @@ -159,10 +170,22 @@ public function login(Request $request) // Rotate token on each login for security $apiToken = Str::random(80); - $sponsor->update([ + $updateData = [ 'api_token' => $apiToken, 'last_login_at' => now(), - ]); + ]; + if ($request->has('fcm_token')) { + $updateData['fcm_token'] = $request->fcm_token; + } + $sponsor->update($updateData); + + if ($request->filled('fcm_token')) { + \App\Services\FCMService::sendPushNotification( + $request->fcm_token, + 'Successful Login', + 'Welcome back to your Migrant sponsor account.' + ); + } return response()->json([ 'success' => true, diff --git a/app/Http/Controllers/Api/SupportTicketController.php b/app/Http/Controllers/Api/SupportTicketController.php index 153d476..c52aafc 100644 --- a/app/Http/Controllers/Api/SupportTicketController.php +++ b/app/Http/Controllers/Api/SupportTicketController.php @@ -175,7 +175,7 @@ public function getTicketsForEmployer(Request $request) return response()->json(['success' => false, 'message' => 'Unauthorized.'], 401); } - $tickets = SupportTicket::where('user_id', $employer->id) + $tickets = SupportTicket::with('reason')->where('user_id', $employer->id) ->orderBy('created_at', 'desc') ->get() ->map(function ($ticket) { @@ -184,6 +184,9 @@ public function getTicketsForEmployer(Request $request) 'ticket_number' => $ticket->ticket_number, 'subject' => $ticket->subject, 'description' => $ticket->description, + 'reason_id' => $ticket->reason_id, + 'reason_name' => $ticket->reason ? $ticket->reason->reason : null, + 'voice_note_url' => $ticket->voice_note_path ? asset('storage/' . $ticket->voice_note_path) : null, 'status' => $ticket->status, 'priority' => $ticket->priority, 'created_at' => $ticket->created_at->toIso8601String(), @@ -207,6 +210,8 @@ public function createTicketFromEmployer(Request $request) 'subject' => 'required|string|max:255', 'description' => 'required|string', 'priority' => 'nullable|string|in:low,medium,high', + 'reason_id' => 'nullable|exists:report_reasons,id', + 'voice_note' => 'nullable|file|mimes:mp3,wav,m4a,ogg,webm,aac,3gp,amr|max:10240', // 10MB limit ]); if ($validator->fails()) { @@ -217,15 +222,24 @@ public function createTicketFromEmployer(Request $request) ], 422); } + $voiceNotePath = null; + if ($request->hasFile('voice_note')) { + $voiceNotePath = $request->file('voice_note')->store('support_voice_notes', 'public'); + } + $ticket = SupportTicket::create([ 'ticket_number' => 'TKT-' . rand(100000, 999999), 'user_id' => $employer->id, + 'reason_id' => $request->reason_id, 'subject' => $request->subject, 'description' => $request->description, + 'voice_note_path' => $voiceNotePath, 'priority' => $request->priority ?? 'medium', 'status' => 'open', ]); + $ticket->load('reason'); + return response()->json([ 'success' => true, 'message' => 'Ticket created successfully.', @@ -234,6 +248,9 @@ public function createTicketFromEmployer(Request $request) 'ticket_number' => $ticket->ticket_number, 'subject' => $ticket->subject, 'description' => $ticket->description, + 'reason_id' => $ticket->reason_id, + 'reason_name' => $ticket->reason ? $ticket->reason->reason : null, + 'voice_note_url' => $ticket->voice_note_path ? asset('storage/' . $ticket->voice_note_path) : null, 'status' => $ticket->status, 'priority' => $ticket->priority, 'created_at' => $ticket->created_at->toIso8601String(), diff --git a/app/Http/Controllers/Api/WorkerAuthController.php b/app/Http/Controllers/Api/WorkerAuthController.php index 569b870..5725361 100644 --- a/app/Http/Controllers/Api/WorkerAuthController.php +++ b/app/Http/Controllers/Api/WorkerAuthController.php @@ -4,7 +4,6 @@ use App\Http\Controllers\Controller; use App\Models\Worker; -use App\Models\WorkerCategory; use Illuminate\Http\Request; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\DB; @@ -181,6 +180,7 @@ public function setupProfile(Request $request) 'preferred_location' => 'nullable|string|max:255', 'skills' => 'nullable|array', 'skills.*' => 'integer|exists:skills,id', + 'fcm_token' => 'nullable|string|max:255', ]); if ($validator->fails()) { @@ -244,10 +244,10 @@ public function setupProfile(Request $request) 'experience' => 'Not Specified', 'religion' => 'Not Specified', 'bio' => 'New worker on Migrant platform.', - 'category_id' => \App\Models\WorkerCategory::where('name', 'General Helper')->value('id') ?? (\App\Models\WorkerCategory::firstOrCreate(['name' => 'General Helper'])->id), 'verified' => false, 'status' => 'active', 'api_token' => $apiToken, + 'fcm_token' => $request->fcm_token, ]); // Sync skills if provided @@ -258,10 +258,18 @@ public function setupProfile(Request $request) return $worker; }); + if ($request->filled('fcm_token')) { + \App\Services\FCMService::sendPushNotification( + $request->fcm_token, + 'Welcome to Migrant', + 'Registration complete! Welcome to Migrant.' + ); + } + // Clear the OTP verification gate Cache::forget('worker_otp_verified_' . $identifier); - $worker->load(['category', 'skills']); + $worker->load(['skills']); return response()->json([ 'success' => true, @@ -305,6 +313,7 @@ public function register(Request $request) 'gender' => 'nullable|string|in:male,female,other', 'live_in_out' => 'nullable|string|in:live_in,live_out', 'preferred_location' => 'nullable|string|max:255', + 'fcm_token' => 'nullable|string|max:255', ]); if ($validator->fails()) { @@ -377,13 +386,13 @@ public function register(Request $request) 'experience' => $request->experience ?? 'Not Specified', 'religion' => 'Not Specified', 'bio' => 'Hardworking and reliable General Helper available for immediate hire.', - 'category_id' => \App\Models\WorkerCategory::where('name', 'General Helper')->value('id') ?? (\App\Models\WorkerCategory::firstOrCreate(['name' => 'General Helper'])->id), 'verified' => false, 'status' => 'active', 'api_token' => $apiToken, 'in_country' => $inCountry, 'visa_status' => $inCountry ? $request->visa_status : null, 'preferred_job_type' => $request->preferred_job_type, + 'fcm_token' => $request->fcm_token, ]); if (!empty($skillsArray)) { @@ -414,7 +423,15 @@ public function register(Request $request) return $worker; }); - $result->load(['category', 'skills', 'documents']); + if ($request->filled('fcm_token')) { + \App\Services\FCMService::sendPushNotification( + $request->fcm_token, + 'Welcome to Migrant', + 'Worker registered and authenticated successfully.' + ); + } + + $result->load(['skills', 'documents']); return response()->json([ 'success' => true, @@ -471,11 +488,19 @@ public function login(Request $request) } $worker->update($updateData); + if ($request->filled('fcm_token')) { + \App\Services\FCMService::sendPushNotification( + $request->fcm_token, + 'Successful Login', + 'Worker logged in successfully.' + ); + } + return response()->json([ 'success' => true, 'message' => 'Worker logged in successfully.', 'data' => [ - 'worker' => $worker->load(['category', 'skills', 'documents']), + 'worker' => $worker->load(['skills', 'documents']), 'token' => $apiToken ] ], 200); diff --git a/app/Http/Controllers/Api/WorkerProfileController.php b/app/Http/Controllers/Api/WorkerProfileController.php index 09ca911..916cb71 100644 --- a/app/Http/Controllers/Api/WorkerProfileController.php +++ b/app/Http/Controllers/Api/WorkerProfileController.php @@ -28,7 +28,7 @@ public function getProfile(Request $request) /** @var Worker $worker */ $worker = $request->attributes->get('worker'); - $worker->load(['category', 'skills', 'documents']); + $worker->load(['skills', 'documents']); return response()->json([ 'success' => true, @@ -59,7 +59,6 @@ public function updateProfile(Request $request) 'experience' => 'nullable|string|max:100', 'religion' => 'nullable|string|max:100', 'bio' => 'nullable|string|max:1000', - 'category_id' => 'nullable|exists:worker_categories,id', 'skills' => 'nullable|array', 'skills.*' => 'exists:skills,id', 'in_country' => 'nullable', @@ -95,7 +94,6 @@ public function updateProfile(Request $request) 'experience', 'religion', 'bio', - 'category_id', 'visa_status', 'preferred_job_type', 'gender', @@ -124,7 +122,7 @@ public function updateProfile(Request $request) } }); - $worker->load(['category', 'skills', 'documents']); + $worker->load(['skills', 'documents']); return response()->json([ 'success' => true, @@ -168,7 +166,7 @@ public function goLive(Request $request) 'success' => true, 'message' => 'Your profile is now live! Status set to Active.', 'data' => [ - 'worker' => $worker->load(['category', 'skills', 'documents']) + 'worker' => $worker->load(['skills', 'documents']) ] ], 200); } catch (\Exception $e) { @@ -221,7 +219,7 @@ public function toggleAvailability(Request $request) 'still_looking' => $stillLooking, 'status' => $newStatus, 'data' => [ - 'worker' => $worker->load(['category', 'skills', 'documents']) + 'worker' => $worker->load(['skills', 'documents']) ] ], 200); @@ -255,7 +253,7 @@ public function markHired(Request $request) 'success' => true, 'message' => 'Congratulations! You have been successfully marked as Hired. Your profile has been automatically removed from active search.', 'data' => [ - 'worker' => $worker->load(['category', 'skills', 'documents']) + 'worker' => $worker->load(['skills', 'documents']) ] ], 200); } catch (\Exception $e) { diff --git a/app/Http/Controllers/Employer/CandidateController.php b/app/Http/Controllers/Employer/CandidateController.php index 4012932..0176621 100644 --- a/app/Http/Controllers/Employer/CandidateController.php +++ b/app/Http/Controllers/Employer/CandidateController.php @@ -46,7 +46,7 @@ public function index(Request $request) // Fetch applications for those jobs $applications = JobApplication::whereIn('job_id', $jobIds) - ->with(['worker.category', 'jobPost']) + ->with(['worker', 'jobPost']) ->get(); $selectedWorkers = $applications->map(function ($app) { @@ -66,7 +66,6 @@ public function index(Request $request) 'worker_id' => $w->id, 'name' => $w->name, 'nationality' => $w->nationality, - 'category' => $w->category ? $w->category->name : 'General Helper', 'salary' => (int)$w->salary, 'status' => $status, 'applied_at' => $app->created_at->format('M d, Y'), @@ -80,7 +79,7 @@ public function index(Request $request) // 2. Fetch direct hiring offers sent by this employer $directOffers = JobOffer::where('employer_id', $employerId) - ->with('worker.category') + ->with('worker') ->get(); $directWorkers = $directOffers->map(function ($offer) { @@ -98,7 +97,6 @@ public function index(Request $request) 'worker_id' => $w->id, 'name' => $w->name, 'nationality' => $w->nationality, - 'category' => $w->category ? $w->category->name : 'General Helper', 'salary' => (int)$offer->salary, 'status' => $status, 'applied_at' => $offer->created_at->format('M d, Y'), diff --git a/app/Http/Controllers/Employer/DashboardController.php b/app/Http/Controllers/Employer/DashboardController.php index a0ea292..8bdbd42 100644 --- a/app/Http/Controllers/Employer/DashboardController.php +++ b/app/Http/Controllers/Employer/DashboardController.php @@ -84,7 +84,7 @@ public function index(Request $request) // 2. Shortlisted workers $shortlists = Shortlist::where('employer_id', $user->id) - ->with(['worker.category', 'worker.skills']) + ->with(['worker.skills']) ->get(); $shortlistedWorkers = $shortlists->map(function ($s) { @@ -100,7 +100,6 @@ public function index(Request $request) 'id' => $w->id, 'name' => $w->name, 'nationality' => $w->nationality, - 'category' => $w->category ? $w->category->name : 'General Helper', 'skills' => $w->skills->pluck('name')->toArray(), 'photo_url' => null, 'verified' => (bool)$w->verified, @@ -122,6 +121,8 @@ public function index(Request $request) return [ 'id' => $conv->id, 'worker_name' => $worker->name, + 'worker_nationality' => $worker->nationality, + 'sender_type' => $lastMsg->sender_type, 'last_message' => $lastMsg->text, 'unread' => $lastMsg->sender_type === 'worker' && is_null($lastMsg->read_at), 'sent_at' => $lastMsg->created_at->diffForHumans(), @@ -130,7 +131,7 @@ public function index(Request $request) })->filter()->sortByDesc('timestamp')->values()->toArray(); // 4. Charity Events - $dbAnnouncements = Announcement::where('status', 'approved')->latest()->limit(5)->get(); + $dbAnnouncements = Announcement::where('status', 'approved')->latest()->limit(2)->get(); $announcements = $dbAnnouncements->map(function ($ann) { $body = $ann->body; $eventDate = null; @@ -174,7 +175,7 @@ public function index(Request $request) })->toArray(); // 5. Recommended Workers - $recWorkers = \App\Models\Worker::with(['category', 'skills']) + $recWorkers = \App\Models\Worker::with(['skills']) ->where('status', 'active') ->orderBy('verified', 'desc') ->limit(3) @@ -190,7 +191,7 @@ public function index(Request $request) 'id' => $w->id, 'name' => $w->name, 'nationality' => $w->nationality, - 'category' => $w->category ? $w->category->name : 'General Helper', + 'category' => 'General Helper', 'skills' => $w->skills->pluck('name')->toArray(), 'salary' => (int)$w->salary, 'rating' => 4.8, diff --git a/app/Http/Controllers/Employer/JobController.php b/app/Http/Controllers/Employer/JobController.php index 799f0e9..822495c 100644 --- a/app/Http/Controllers/Employer/JobController.php +++ b/app/Http/Controllers/Employer/JobController.php @@ -8,7 +8,6 @@ use App\Models\User; use App\Models\JobPost; use App\Models\JobApplication; -use App\Models\WorkerCategory; class JobController extends Controller { @@ -44,7 +43,7 @@ public function index(Request $request) } $dbJobs = JobPost::where('employer_id', $user->id) - ->with(['category', 'applications']) + ->with(['applications']) ->latest() ->get(); @@ -52,7 +51,6 @@ public function index(Request $request) return [ 'id' => $job->id, 'title' => $job->title, - 'category' => $job->category->name ?? 'General', 'location' => $job->location, 'salary' => (int) $job->salary, 'workers_needed' => $job->workers_needed, @@ -69,14 +67,7 @@ public function index(Request $request) public function create() { - $categories = WorkerCategory::pluck('name')->toArray(); - if (empty($categories)) { - $categories = ['Electrician', 'Mason', 'Plumber', 'Cleaner', 'Site Supervisor', 'Driver', 'General Helper']; - } - - return Inertia::render('Employer/Jobs/Create', [ - 'categories' => $categories, - ]); + return Inertia::render('Employer/Jobs/Create'); } public function store(Request $request) @@ -88,7 +79,6 @@ public function store(Request $request) $request->validate([ 'title' => 'required|string|max:255', - 'category' => 'required|string', 'workers_needed' => 'required|integer|min:1', 'location' => 'required|string|max:255', 'salary' => 'required|numeric|min:0', @@ -98,15 +88,9 @@ public function store(Request $request) 'requirements' => 'nullable|string', ]); - $category = WorkerCategory::where('name', $request->category)->first(); - if (!$category) { - $category = WorkerCategory::create(['name' => $request->category]); - } - JobPost::create([ 'employer_id' => $user->id, 'title' => $request->title, - 'category_id' => $category->id, 'workers_needed' => $request->workers_needed, 'job_type' => $request->job_type, 'location' => $request->location, @@ -130,7 +114,7 @@ public function applicants($id) $job = JobPost::where('employer_id', $user->id)->where('id', $id)->firstOrFail(); $dbApplications = JobApplication::where('job_id', $id) - ->with(['worker.category', 'worker.skills']) + ->with(['worker.skills']) ->get(); $applicants = $dbApplications->map(function ($app) { @@ -139,7 +123,6 @@ public function applicants($id) 'id' => $worker->id, 'name' => $worker->name, 'nationality' => $worker->nationality, - 'category' => $worker->category->name ?? 'General', 'salary' => (int) $worker->expected_salary, 'experience' => ($worker->experience_years ?? 3) . ' Years', 'status' => ucfirst($app->status), // Applied, Shortlisted, Hired, Rejected @@ -151,7 +134,6 @@ public function applicants($id) 'job' => [ 'id' => $job->id, 'title' => $job->title, - 'category' => $job->category->name ?? 'General', 'salary' => (int) $job->salary, ], 'applicants' => $applicants, diff --git a/app/Http/Controllers/Employer/MessageController.php b/app/Http/Controllers/Employer/MessageController.php index 87643ea..b5418e1 100644 --- a/app/Http/Controllers/Employer/MessageController.php +++ b/app/Http/Controllers/Employer/MessageController.php @@ -101,7 +101,7 @@ public function index(Request $request) } $dbConversations = Conversation::where('employer_id', $user->id) - ->with(['worker.category', 'messages']) + ->with(['worker', 'messages']) ->latest('updated_at') ->get(); @@ -111,7 +111,6 @@ public function index(Request $request) 'id' => $conv->id, 'worker_id' => $conv->worker_id, 'worker_name' => $conv->worker->name ?? 'Candidate', - 'category' => $conv->worker->category->name ?? 'General Helper', 'worker_status' => strtolower($conv->worker->status ?? 'active'), 'last_message' => $lastMsg->text ?? 'No messages yet.', 'unread' => $lastMsg ? ($lastMsg->sender_type === 'worker' && is_null($lastMsg->read_at)) : false, @@ -133,7 +132,7 @@ public function show($id) } $dbConversations = Conversation::where('employer_id', $user->id) - ->with(['worker.category', 'messages']) + ->with(['worker', 'messages']) ->latest('updated_at') ->get(); @@ -143,7 +142,6 @@ public function show($id) 'id' => $conv->id, 'worker_id' => $conv->worker_id, 'worker_name' => $conv->worker->name ?? 'Candidate', - 'category' => $conv->worker->category->name ?? 'General Helper', 'worker_status' => strtolower($conv->worker->status ?? 'active'), 'last_message' => $lastMsg->text ?? 'No messages yet.', 'unread' => $lastMsg ? ($lastMsg->sender_type === 'worker' && is_null($lastMsg->read_at)) : false, @@ -154,7 +152,7 @@ public function show($id) $activeConv = Conversation::where('employer_id', $user->id) ->where('id', $id) - ->with(['worker.category', 'messages']) + ->with(['worker', 'messages']) ->firstOrFail(); // Mark incoming messages as read @@ -167,7 +165,6 @@ public function show($id) 'id' => $activeConv->id, 'worker_id' => $activeConv->worker_id, 'worker_name' => $activeConv->worker->name ?? 'Candidate', - 'category' => $activeConv->worker->category->name ?? 'General Helper', 'worker_status' => strtolower($activeConv->worker->status ?? 'active'), 'online' => true, 'salary' => ($activeConv->worker->expected_salary ?? 2000) . ' AED', diff --git a/app/Http/Controllers/Employer/ShortlistController.php b/app/Http/Controllers/Employer/ShortlistController.php index 79d0ded..14dff6c 100644 --- a/app/Http/Controllers/Employer/ShortlistController.php +++ b/app/Http/Controllers/Employer/ShortlistController.php @@ -43,7 +43,7 @@ public function index(Request $request) $employerId = $user ? $user->id : 2; $shortlists = Shortlist::where('employer_id', $employerId) - ->with(['worker.category', 'worker.skills', 'worker.documents']) + ->with(['worker.skills', 'worker.documents']) ->get(); $shortlistedWorkers = $shortlists->map(function ($s) { @@ -96,9 +96,9 @@ public function index(Request $request) 'photo' => $photo, 'emirates_id_status' => $emiratesIdStatus, 'passport_status' => $w->passport_status, - 'category' => $w->category ? $w->category->name : 'Domestic Worker', 'skills' => $mappedSkills, 'visa_status' => $visaStatus, + 'gender' => $w->gender ?? 'Female', 'experience' => $w->experience, 'salary' => (int) $w->salary, 'religion' => $w->religion, diff --git a/app/Http/Controllers/Employer/SupportTicketController.php b/app/Http/Controllers/Employer/SupportTicketController.php index f9af728..8be09fa 100644 --- a/app/Http/Controllers/Employer/SupportTicketController.php +++ b/app/Http/Controllers/Employer/SupportTicketController.php @@ -92,14 +92,21 @@ public function store(Request $request) 'subject' => 'required|string|max:255', 'description' => 'required|string', 'priority' => 'required|string|in:low,medium,high', + 'voice_note' => 'nullable|file|mimes:mp3,wav,m4a,ogg,webm,aac,3gp,amr|max:10240', // 10MB limit ]); + $voiceNotePath = null; + if ($request->hasFile('voice_note')) { + $voiceNotePath = $request->file('voice_note')->store('support_voice_notes', 'public'); + } + $ticket = SupportTicket::create([ 'ticket_number' => 'TKT-' . rand(100000, 999999), 'user_id' => $user->id, 'reason_id' => $request->reason_id, 'subject' => $request->subject, 'description' => $request->description, + 'voice_note_path' => $voiceNotePath, 'priority' => $request->priority, 'status' => 'open', ]); @@ -128,6 +135,7 @@ public function show(Request $request, $id) 'sender_name' => $reply->sender_name, 'is_admin' => $reply->user && $reply->user->role === 'admin', 'is_developer_response' => (bool)$reply->is_developer_response, + 'voice_note_url' => $reply->voice_note_path ? asset('storage/' . $reply->voice_note_path) : null, 'created_at' => $reply->created_at->format('Y-m-d H:i'), ]; }); @@ -139,6 +147,7 @@ public function show(Request $request, $id) 'subject' => $ticket->subject, 'reason' => $ticket->reason ? $ticket->reason->reason : null, 'description' => $ticket->description, + 'voice_note_url' => $ticket->voice_note_path ? asset('storage/' . $ticket->voice_note_path) : null, 'status' => $ticket->status, 'priority' => $ticket->priority, 'created_at' => $ticket->created_at->format('Y-m-d H:i'), @@ -161,13 +170,20 @@ public function reply(Request $request, $id) } $request->validate([ - 'message' => 'required|string', + 'message' => 'nullable|required_without:voice_note|string', + 'voice_note' => 'nullable|file|mimes:mp3,wav,m4a,ogg,webm,aac,3gp,amr|max:10240', // 10MB limit ]); + $voiceNotePath = null; + if ($request->hasFile('voice_note')) { + $voiceNotePath = $request->file('voice_note')->store('support_voice_notes', 'public'); + } + SupportTicketReply::create([ 'support_ticket_id' => $ticket->id, 'user_id' => $user->id, 'message' => $request->message, + 'voice_note_path' => $voiceNotePath, ]); // Reopen ticket if resolved/closed was updated diff --git a/app/Http/Controllers/Employer/WorkerController.php b/app/Http/Controllers/Employer/WorkerController.php index 14d03c6..bb01e0f 100644 --- a/app/Http/Controllers/Employer/WorkerController.php +++ b/app/Http/Controllers/Employer/WorkerController.php @@ -7,7 +7,6 @@ use Inertia\Inertia; use App\Models\User; use App\Models\Worker; -use App\Models\WorkerCategory; use App\Models\Shortlist; use App\Models\JobOffer; use App\Models\Review; @@ -46,7 +45,7 @@ public function index(Request $request) $user = $this->resolveCurrentUser(); $employerId = $user ? $user->id : 2; - $dbWorkers = Worker::with(['category', 'skills', 'documents']) + $dbWorkers = Worker::with(['skills', 'documents']) ->where('status', '!=', 'Hired') ->where('status', '!=', 'hidden') ->get(); @@ -98,7 +97,7 @@ public function index(Request $request) 'photo' => $photo, 'emirates_id_status' => $emiratesIdStatus, 'passport_status' => $w->passport_status, - 'category' => $w->category ? $w->category->name : 'Domestic Worker', + 'category' => 'Domestic Worker', 'skills' => $mappedSkills, 'visa_status' => $visaStatus, 'experience' => $w->experience, @@ -267,13 +266,11 @@ public function index(Request $request) $shortlistedIds = Shortlist::where('employer_id', $employerId)->pluck('worker_id')->toArray(); - $dbCategories = WorkerCategory::pluck('name')->toArray(); $nationalitiesResponse = app(\App\Http\Controllers\Api\WorkerAuthController::class)->nationalities(new \Illuminate\Http\Request(['per_page' => 500])); $nationalitiesData = json_decode($nationalitiesResponse->getContent(), true); $dbNationalities = collect($nationalitiesData['data']['nationalities'] ?? [])->pluck('name')->filter()->toArray(); $filtersMetadata = [ - 'categories' => array_merge(['All Categories'], $dbCategories), 'nationalities' => array_merge(['All Nationalities'], $dbNationalities), 'experienceLevels' => ['All Experience', '1-2 Years', '3-5 Years', '5+ Years'], 'religions' => ['All Religions', 'Christian', 'Muslim', 'Hindu', 'Buddhist'], @@ -292,7 +289,7 @@ public function index(Request $request) public function show($id) { - $w = Worker::with(['category', 'skills', 'documents'])->findOrFail($id); + $w = Worker::with(['skills', 'documents'])->findOrFail($id); $isPending = str_contains(strtolower($w->passport_status), 'pending'); if ($w->status === 'hidden' || $isPending) { @@ -345,7 +342,7 @@ public function show($id) $reviewsCount = count($reviews); $rating = $reviewsCount > 0 ? round($dbReviews->avg('rating'), 1) : 0; - $simDb = Worker::with('category') + $simDb = Worker::query() ->where('id', '!=', $w->id) ->where('status', '!=', 'Hired') ->where('status', '!=', 'hidden') @@ -361,7 +358,7 @@ public function show($id) 'id' => $sw->id, 'name' => $sw->name, 'nationality' => $sw->nationality, - 'category' => $sw->category ? $sw->category->name : 'Helper', + 'category' => 'Helper', 'salary' => (int) $sw->salary, 'rating' => 4.7, 'verified' => (bool) $sw->verified, @@ -379,7 +376,7 @@ public function show($id) 'photo' => $photo, 'emirates_id_status' => $emiratesIdStatus, 'passport_status' => $w->passport_status, - 'category' => $w->category ? $w->category->name : 'Domestic Worker', + 'category' => 'Domestic Worker', 'skills' => $mappedSkills, 'visa_status' => $visaStatus, 'experience' => $w->experience, diff --git a/app/Models/JobPost.php b/app/Models/JobPost.php index 4a443ef..444e467 100644 --- a/app/Models/JobPost.php +++ b/app/Models/JobPost.php @@ -13,7 +13,6 @@ class JobPost extends Model protected $fillable = [ 'employer_id', 'title', - 'category_id', 'workers_needed', 'job_type', 'location', @@ -35,10 +34,6 @@ public function employer() return $this->belongsTo(User::class, 'employer_id'); } - public function category() - { - return $this->belongsTo(WorkerCategory::class, 'category_id'); - } public function applications() { diff --git a/app/Models/Sponsor.php b/app/Models/Sponsor.php index e878105..71f3bf3 100644 --- a/app/Models/Sponsor.php +++ b/app/Models/Sponsor.php @@ -34,6 +34,7 @@ class Sponsor extends Authenticatable 'license_file', 'emirates_id_file', 'license_expiry', + 'fcm_token', ]; protected $hidden = [ diff --git a/app/Models/Worker.php b/app/Models/Worker.php index a6b8fec..d88a205 100644 --- a/app/Models/Worker.php +++ b/app/Models/Worker.php @@ -29,7 +29,6 @@ class Worker extends Model 'experience', 'religion', 'bio', - 'category_id', 'verified', 'status', 'api_token', @@ -128,10 +127,6 @@ public function getDocumentExpiryStatusAttribute() return 'No Documents'; } - public function category() - { - return $this->belongsTo(WorkerCategory::class, 'category_id'); - } public function skills() { diff --git a/app/Models/WorkerCategory.php b/app/Models/WorkerCategory.php deleted file mode 100644 index 3675e28..0000000 --- a/app/Models/WorkerCategory.php +++ /dev/null @@ -1,18 +0,0 @@ -hasMany(Worker::class, 'category_id'); - } -} diff --git a/database/migrations/2026_06_15_090334_drop_worker_categories_table.php b/database/migrations/2026_06_15_090334_drop_worker_categories_table.php new file mode 100644 index 0000000..5bb6407 --- /dev/null +++ b/database/migrations/2026_06_15_090334_drop_worker_categories_table.php @@ -0,0 +1,46 @@ +dropForeign(['category_id']); + $table->dropColumn('category_id'); + }); + + Schema::table('job_posts', function (Blueprint $table) { + $table->dropForeign(['category_id']); + $table->dropColumn('category_id'); + }); + + Schema::dropIfExists('worker_categories'); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::create('worker_categories', function (Blueprint $table) { + $table->id(); + $table->string('name')->unique(); + $table->timestamps(); + }); + + Schema::table('workers', function (Blueprint $table) { + $table->foreignId('category_id')->nullable()->constrained('worker_categories'); + }); + + Schema::table('job_posts', function (Blueprint $table) { + $table->foreignId('category_id')->nullable()->constrained('worker_categories'); + }); + } +}; diff --git a/database/migrations/2026_06_15_095611_add_fcm_token_to_sponsors_table.php b/database/migrations/2026_06_15_095611_add_fcm_token_to_sponsors_table.php new file mode 100644 index 0000000..bbbac56 --- /dev/null +++ b/database/migrations/2026_06_15_095611_add_fcm_token_to_sponsors_table.php @@ -0,0 +1,28 @@ +string('fcm_token')->nullable()->after('api_token'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('sponsors', function (Blueprint $table) { + $table->dropColumn('fcm_token'); + }); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 23e2ef0..8acece4 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -29,7 +29,6 @@ public function run(): void // Call Seeders $this->call([ - WorkerCategorySeeder::class, SkillSeeder::class, WorkerSeeder::class, EmployerProfileSeeder::class, diff --git a/database/seeders/JobPostSeeder.php b/database/seeders/JobPostSeeder.php index 3753ec8..b0f43f8 100644 --- a/database/seeders/JobPostSeeder.php +++ b/database/seeders/JobPostSeeder.php @@ -19,7 +19,6 @@ public function run(): void [ 'employer_id' => $employerId, 'title' => 'Senior Electrician for Villa Project', - 'category_id' => 1, // Electrician 'workers_needed' => 2, 'job_type' => 'Full Time', 'location' => 'Dubai Marina', @@ -31,7 +30,6 @@ public function run(): void [ 'employer_id' => $employerId, 'title' => 'Mason for Commercial Building', - 'category_id' => 2, // Mason 'workers_needed' => 5, 'job_type' => 'Contract', 'location' => 'Business Bay', diff --git a/database/seeders/MasterDataSeeder.php b/database/seeders/MasterDataSeeder.php index 1f45b6e..989c91f 100644 --- a/database/seeders/MasterDataSeeder.php +++ b/database/seeders/MasterDataSeeder.php @@ -27,7 +27,6 @@ public function run(): void // 2. Run static reference seeders $this->call([ - WorkerCategorySeeder::class, SkillSeeder::class, ]); } diff --git a/database/seeders/WorkerCategorySeeder.php b/database/seeders/WorkerCategorySeeder.php deleted file mode 100644 index 700a8ec..0000000 --- a/database/seeders/WorkerCategorySeeder.php +++ /dev/null @@ -1,28 +0,0 @@ -insertOrIgnore([ - 'name' => $category, - 'created_at' => now(), - 'updated_at' => now(), - ]); - } - } -} diff --git a/database/seeders/WorkerSeeder.php b/database/seeders/WorkerSeeder.php index 10f2c1a..e3cf464 100644 --- a/database/seeders/WorkerSeeder.php +++ b/database/seeders/WorkerSeeder.php @@ -31,7 +31,6 @@ public function run(): void 'experience' => '5+ Years', 'religion' => 'Christian', 'bio' => '', - 'category_id' => 1, // Electrician 'verified' => true, 'status' => 'active', ], @@ -53,7 +52,6 @@ public function run(): void 'experience' => '3 Years', 'religion' => 'Muslim', 'bio' => 'Skilled mason with experience in block work and plastering.', - 'category_id' => 2, // Mason 'verified' => true, 'status' => 'active', ], @@ -63,7 +61,7 @@ public function run(): void 'phone' => '+971 50 555 6666', 'nationality' => 'Filipino', 'country' => 'United Arab Emirates', - 'city' => 'Abu Dhabi', + 'city' => 'Yas Island', 'area' => 'Yas Island', 'preferred_location' => 'Yas Island', 'live_in_out' => 'Live-in (Stay with family)', @@ -75,7 +73,6 @@ public function run(): void 'experience' => '5+ Years', 'religion' => 'Christian', 'bio' => 'Experienced nanny with 6 years working with expatriate families in Dubai. Certified in pediatric first aid.', - 'category_id' => 8, // Childcare 'verified' => true, 'status' => 'active', ], @@ -97,7 +94,6 @@ public function run(): void 'experience' => '3-5 Years', 'religion' => 'Hindu', 'bio' => 'Patient caregiver specializing in elderly assistance, mobility support, and vegetarian cooking.', - 'category_id' => 11, // Elderly Care 'verified' => true, 'status' => 'active', ], @@ -119,7 +115,6 @@ public function run(): void 'experience' => '3-5 Years', 'religion' => 'Muslim', 'bio' => 'Hardworking and meticulous housekeeper with excellent references from Abu Dhabi households.', - 'category_id' => 9, // Housekeeping 'verified' => true, 'status' => 'active', ], @@ -141,7 +136,6 @@ public function run(): void 'experience' => '1-2 Years', 'religion' => 'Christian', 'bio' => 'Energetic and educated nanny. Fluent in English, great with toddlers and assisting with homework.', - 'category_id' => 8, // Childcare 'verified' => false, 'status' => 'active', ], @@ -163,7 +157,6 @@ public function run(): void 'experience' => '5+ Years', 'religion' => 'Buddhist', 'bio' => 'Professional domestic cook skilled in Arabic, Continental, and Asian cuisine. Highly organized.', - 'category_id' => 10, // Cooking 'verified' => true, 'status' => 'active', ], @@ -185,42 +178,11 @@ public function run(): void 'experience' => '5+ Years', 'religion' => 'Hindu', 'bio' => 'Valid UAE driving license with clean record. Familiar with all Dubai and Sharjah school routes.', - 'category_id' => 6, // Driver 'verified' => true, 'status' => 'active', ], ]; - // Dynamic Category Resolution - $categoryMapping = [ - 1 => 'Electrician', - 2 => 'Mason', - 3 => 'Plumber', - 4 => 'Cleaner', - 5 => 'Site Supervisor', - 6 => 'Driver', - 7 => 'General Helper', - 8 => 'Childcare', - 9 => 'Housekeeping', - 10 => 'Cooking', - 11 => 'Elderly Care', - ]; - - $categoryIds = []; - foreach ($categoryMapping as $oldId => $name) { - $cat = \Illuminate\Support\Facades\DB::table('worker_categories')->where('name', $name)->first(); - if (!$cat) { - $catId = \Illuminate\Support\Facades\DB::table('worker_categories')->insertGetId([ - 'name' => $name, - 'created_at' => now(), - 'updated_at' => now(), - ]); - } else { - $catId = $cat->id; - } - $categoryIds[$oldId] = $catId; - } - // Dynamic Skill Resolution $skillIds = \Illuminate\Support\Facades\DB::table('skills')->pluck('id')->toArray(); if (empty($skillIds)) { @@ -237,15 +199,12 @@ public function run(): void $sId2 = $skillIds[1] ?? 2; foreach ($workers as $workerData) { - $resolvedCategoryId = $categoryIds[$workerData['category_id']] ?? $workerData['category_id']; - // Exclude email duplicates to avoid unique constraint violations if (\Illuminate\Support\Facades\DB::table('workers')->where('email', $workerData['email'])->exists()) { continue; } $workerId = \Illuminate\Support\Facades\DB::table('workers')->insertGetId(array_merge($workerData, [ - 'category_id' => $resolvedCategoryId, 'created_at' => now(), 'updated_at' => now(), ])); diff --git a/public/swagger.json b/public/swagger.json index eef6339..d14b97e 100644 --- a/public/swagger.json +++ b/public/swagger.json @@ -1686,10 +1686,7 @@ "type": "string", "example": "Highly certified housekeeper and babysitter with cooking experience." }, - "category_id": { - "type": "integer", - "example": 8 - }, + "skills": { "type": "array", "items": { @@ -4405,7 +4402,7 @@ "requestBody": { "required": true, "content": { - "application/json": { + "multipart/form-data": { "schema": { "type": "object", "required": [ @@ -4429,6 +4426,16 @@ "high" ], "example": "medium" + }, + "reason_id": { + "type": "integer", + "example": 1, + "description": "Optional report/support reason ID." + }, + "voice_note": { + "type": "string", + "format": "binary", + "description": "Optional audio/voice note file attachment." } } } @@ -4595,10 +4602,7 @@ "type": "string", "example": "Certified infant caregiver and housekeeper with excellent cooking skills." }, - "category_id": { - "type": "integer", - "example": 7 - }, + "verified": { "type": "boolean", "example": false diff --git a/resources/js/Pages/Admin/Workers/Index.jsx b/resources/js/Pages/Admin/Workers/Index.jsx index 8f8456e..edab975 100644 --- a/resources/js/Pages/Admin/Workers/Index.jsx +++ b/resources/js/Pages/Admin/Workers/Index.jsx @@ -75,7 +75,6 @@ export default function WorkerManagement({ workers }) { name: '', phone: '', language: '', - category: '', experience: '', bio: '', country: '', @@ -128,7 +127,6 @@ export default function WorkerManagement({ workers }) { phone: editForm.phone, gender: editForm.gender, language: editForm.language, - category: editForm.category, experience: editForm.experience, salary: editForm.salary, bio: editForm.bio, @@ -170,7 +168,6 @@ export default function WorkerManagement({ workers }) { phone: fullWorker.phone, gender: fullWorker.gender || 'Female', language: fullWorker.language || 'English', - category: fullWorker.category, experience: fullWorker.experience, salary: fullWorker.salary || '', bio: fullWorker.bio, diff --git a/resources/js/Pages/Employer/Dashboard.jsx b/resources/js/Pages/Employer/Dashboard.jsx index 836f473..4a06842 100644 --- a/resources/js/Pages/Employer/Dashboard.jsx +++ b/resources/js/Pages/Employer/Dashboard.jsx @@ -272,7 +272,7 @@ export default function Dashboard({
@@ -345,13 +345,6 @@ export default function Dashboard({
-
-
{t('category', 'Category')}:
-
- {worker.category || 'General Helper'} -
-
-
-
- {msg.worker_name} +
+ {msg.worker_name} + {msg.worker_nationality && ( + + {msg.worker_nationality} + + )}
{msg.sent_at} @@ -405,7 +403,12 @@ export default function Dashboard({ )}
-

+

+ {msg.sender_type === 'employer' ? ( + {t('you', 'You')}: + ) : ( + {msg.worker_name}: + )} {msg.last_message}

diff --git a/resources/js/Pages/Employer/Hiring/Confirm.jsx b/resources/js/Pages/Employer/Hiring/Confirm.jsx index baa9fae..f6733ff 100644 --- a/resources/js/Pages/Employer/Hiring/Confirm.jsx +++ b/resources/js/Pages/Employer/Hiring/Confirm.jsx @@ -50,7 +50,7 @@ export default function Confirm({ worker }) { {worker.name.charAt(0)}

{worker.name}

-

{worker.category}

+
diff --git a/resources/js/Pages/Employer/Jobs/Applicants.jsx b/resources/js/Pages/Employer/Jobs/Applicants.jsx index 94cddd4..6114f22 100644 --- a/resources/js/Pages/Employer/Jobs/Applicants.jsx +++ b/resources/js/Pages/Employer/Jobs/Applicants.jsx @@ -20,10 +20,10 @@ import { Badge } from '@/components/ui/badge'; export default function Applicants({ job, applicants: initialApplicants }) { const [searchTerm, setSearchTerm] = useState(''); const [applicants, setApplicants] = useState(initialApplicants || [ - { id: 101, name: 'Anjali Sharma', nationality: 'Indian', category: 'Nanny', salary: 2500, experience: '5 Years', status: 'Reviewing', match_score: 95 }, - { id: 102, name: 'Siti Rahma', nationality: 'Indonesian', category: 'Housemaid', salary: 1800, experience: '3 Years', status: 'Hired', match_score: 88 }, - { id: 103, name: 'Maricel Cruz', nationality: 'Filipino', category: 'Domestic Helper', salary: 2200, experience: '7 Years', status: 'Rejected', match_score: 72 }, - { id: 104, name: 'Bebeth Santos', nationality: 'Filipino', category: 'Cook', salary: 3000, experience: '10 Years', status: 'Reviewing', match_score: 98 }, + { id: 101, name: 'Anjali Sharma', nationality: 'Indian', salary: 2500, experience: '5 Years', status: 'Reviewing', match_score: 95 }, + { id: 102, name: 'Siti Rahma', nationality: 'Indonesian', salary: 1800, experience: '3 Years', status: 'Hired', match_score: 88 }, + { id: 103, name: 'Maricel Cruz', nationality: 'Filipino', salary: 2200, experience: '7 Years', status: 'Rejected', match_score: 72 }, + { id: 104, name: 'Bebeth Santos', nationality: 'Filipino', salary: 3000, experience: '10 Years', status: 'Reviewing', match_score: 98 }, ]); const filteredApplicants = applicants.filter(a => @@ -50,8 +50,6 @@ export default function Applicants({ job, applicants: initialApplicants }) { Applicants for {job?.title || 'Senior Mason Project'}
- {job?.category || 'Mason'} - {job?.salary || '2800'} AED
diff --git a/resources/js/Pages/Employer/Jobs/Create.jsx b/resources/js/Pages/Employer/Jobs/Create.jsx index 77493a8..db048ed 100644 --- a/resources/js/Pages/Employer/Jobs/Create.jsx +++ b/resources/js/Pages/Employer/Jobs/Create.jsx @@ -22,7 +22,6 @@ export default function Create({ categories: initialCategories }) { const { data, setData, post, processing, errors } = useForm({ title: '', - category: '', workers_needed: 1, location: '', salary: '', @@ -91,21 +90,7 @@ export default function Create({ categories: initialCategories }) {
-
- -
- - -
-
+
diff --git a/resources/js/Pages/Employer/Jobs/Index.jsx b/resources/js/Pages/Employer/Jobs/Index.jsx index ec459ad..02cc27a 100644 --- a/resources/js/Pages/Employer/Jobs/Index.jsx +++ b/resources/js/Pages/Employer/Jobs/Index.jsx @@ -33,16 +33,15 @@ export default function Index({ initialJobs }) { const [statusFilter, setStatusFilter] = useState('All'); const [jobs, setJobs] = useState(initialJobs || [ - { id: 1, title: 'Experienced Mason', category: 'Mason', location: 'Dubai Marina', salary: 2800, workers_needed: 5, applied_count: 3, posted_at: 'Nov 12, 2026', status: 'Active' }, - { id: 2, title: 'Villa Cleaner', category: 'Cleaner', location: 'Al Barsha', salary: 1800, workers_needed: 2, applied_count: 12, posted_at: 'Nov 10, 2026', status: 'Active' }, - { id: 3, title: 'Site Supervisor', category: 'Site Supervisor', location: 'Sharjah', salary: 4500, workers_needed: 1, applied_count: 8, posted_at: 'Nov 05, 2026', status: 'Closed' }, - { id: 4, title: 'Plumber for Project', category: 'Plumber', location: 'Jumeirah', salary: 2600, workers_needed: 3, applied_count: 0, posted_at: 'Nov 14, 2026', status: 'Draft' }, + { id: 1, title: 'Experienced Mason', location: 'Dubai Marina', salary: 2800, workers_needed: 5, applied_count: 3, posted_at: 'Nov 12, 2026', status: 'Active' }, + { id: 2, title: 'Villa Cleaner', location: 'Al Barsha', salary: 1800, workers_needed: 2, applied_count: 12, posted_at: 'Nov 10, 2026', status: 'Active' }, + { id: 3, title: 'Site Supervisor', location: 'Sharjah', salary: 4500, workers_needed: 1, applied_count: 8, posted_at: 'Nov 05, 2026', status: 'Closed' }, + { id: 4, title: 'Plumber for Project', location: 'Jumeirah', salary: 2600, workers_needed: 3, applied_count: 0, posted_at: 'Nov 14, 2026', status: 'Draft' }, ]); const filteredJobs = useMemo(() => { return jobs.filter(job => { - const matchesSearch = job.title.toLowerCase().includes(searchTerm.toLowerCase()) || - job.category.toLowerCase().includes(searchTerm.toLowerCase()); + const matchesSearch = job.title.toLowerCase().includes(searchTerm.toLowerCase()); const matchesStatus = statusFilter === 'All' || job.status === statusFilter; return matchesSearch && matchesStatus; }); @@ -74,7 +73,7 @@ export default function Index({ initialJobs }) { setSearchTerm(e.target.value)} @@ -120,8 +119,6 @@ export default function Index({ initialJobs }) {

{job.title}

- {job.category} - {job.posted_at}
diff --git a/resources/js/Pages/Employer/Messages/Index.jsx b/resources/js/Pages/Employer/Messages/Index.jsx index 5b5826f..f4dadc5 100644 --- a/resources/js/Pages/Employer/Messages/Index.jsx +++ b/resources/js/Pages/Employer/Messages/Index.jsx @@ -65,9 +65,6 @@ export default function Index({ conversations }) {
{thread.worker_name} - - {thread.category} -
{thread.sent_at} diff --git a/resources/js/Pages/Employer/Messages/Show.jsx b/resources/js/Pages/Employer/Messages/Show.jsx index e433afe..a441acc 100644 --- a/resources/js/Pages/Employer/Messages/Show.jsx +++ b/resources/js/Pages/Employer/Messages/Show.jsx @@ -498,9 +498,6 @@ export default function Show({ conversation, initialMessages, conversations = []

{conversation.worker_name}

- - {conversation.category} -
diff --git a/resources/js/Pages/Employer/SelectedCandidates.jsx b/resources/js/Pages/Employer/SelectedCandidates.jsx index 7fd530a..43ddc4c 100644 --- a/resources/js/Pages/Employer/SelectedCandidates.jsx +++ b/resources/js/Pages/Employer/SelectedCandidates.jsx @@ -418,7 +418,6 @@ export default function SelectedCandidates({ {t('candidate_header', 'Candidate')} {t('selected_header', 'Selected')} - {t('category_header', 'Category')} {t('salary_header', 'Salary')} {t('status_header', 'Status')} {t('actions_header', 'Actions')} @@ -443,9 +442,6 @@ export default function SelectedCandidates({ {worker.applied_at || '—'} - - {worker.category} - {worker.salary} AED @@ -510,3 +506,4 @@ export default function SelectedCandidates({ ); } + diff --git a/resources/js/Pages/Employer/Shortlist.jsx b/resources/js/Pages/Employer/Shortlist.jsx index e9c18fd..65c27da 100644 --- a/resources/js/Pages/Employer/Shortlist.jsx +++ b/resources/js/Pages/Employer/Shortlist.jsx @@ -18,7 +18,8 @@ import { User, X, HeartHandshake, - CheckCircle + CheckCircle, + MapPin } from 'lucide-react'; const getLanguageFlag = (lang) => { @@ -176,11 +177,8 @@ export default function Shortlist({ shortlistedWorkers }) {
- {/* Category Pill, Ratings & Cost */} -
- - {worker.category} - + {/* Ratings & Cost */} +
{worker.salary} AED @@ -199,9 +197,13 @@ export default function Shortlist({ shortlistedWorkers }) { {worker.preferred_job_type}
-
+
+ + {worker.preferred_location || 'Not Specified'} +
+
- {worker.rating || '4.5'} ({worker.reviews_count || '12'} reviews) + {worker.rating || '4.5'} ({worker.reviews_count || '12'})
@@ -330,7 +332,7 @@ export default function Shortlist({ shortlistedWorkers }) {
{w.name}
-
{w.nationality} • {w.category}
+
{w.nationality}
@@ -338,7 +340,7 @@ export default function Shortlist({ shortlistedWorkers }) {
Salary: {w.salary} AED
Age: {w.age} yrs
Job Type: {w.preferred_job_type}
-
Status: {w.availability_status}
+
Location: {w.preferred_location || 'Not Specified'}
{/* Skills Display in Comparison */} @@ -395,41 +397,46 @@ export default function Shortlist({ shortlistedWorkers }) { }`} /> {previewWorker.passport_status}
-
{previewWorker.nationality} • {previewWorker.category}
+
{previewWorker.nationality}
- {previewWorker.bio ? ( -

- "{previewWorker.bio}" -

- ) : ( -

- {t('no_bio_provided', 'No bio details provided.')} -

- )} - -
-
-
{t('expectations', 'Expectations')}
-
{previewWorker.salary} AED / mo
-
-
-
{t('job_preference', 'Job Preference')}
-
{previewWorker.preferred_job_type}
-
-
-
{t('languages', 'Languages')}
-
- {previewWorker.languages?.map(lang => ( - - {getLanguageFlag(lang)} - {lang} - - ))} -
-
-
+
+
+
{t('expectations', 'Expectations')}
+
{previewWorker.salary} AED / mo
+
+
+
{t('job_type', 'Job Type')}
+
{previewWorker.preferred_job_type}
+
+
+
{t('gender', 'Gender')}
+
{previewWorker.gender || 'Female'}
+
+
+
{t('visa_status', 'Visa Status')}
+
{previewWorker.visa_status || 'Residence Visa'}
+
+
+
{t('preferred_location', 'Preferred Location')}
+
+ + {previewWorker.preferred_location || 'Not Specified'} +
+
+
+
{t('languages', 'Languages')}
+
+ {previewWorker.languages?.map(lang => ( + + {getLanguageFlag(lang)} + {lang} + + ))} +
+
+
{/* Core Platform Skills inside Quick Preview */}
diff --git a/resources/js/Pages/Employer/Support/Create.jsx b/resources/js/Pages/Employer/Support/Create.jsx index 9ffe383..dea74e5 100644 --- a/resources/js/Pages/Employer/Support/Create.jsx +++ b/resources/js/Pages/Employer/Support/Create.jsx @@ -6,7 +6,10 @@ import { LifeBuoy, ArrowLeft, AlertTriangle, - CheckCircle + CheckCircle, + UploadCloud, + X, + Volume2 } from 'lucide-react'; export default function Create({ reasons = [] }) { @@ -17,6 +20,7 @@ export default function Create({ reasons = [] }) { subject: '', description: '', priority: 'medium', + voice_note: null, }); const handleSubmit = (e) => { @@ -124,6 +128,58 @@ export default function Create({ reasons = [] }) { )}
+ {/* Voice Note Attachment */} +
+ + + {!data.voice_note ? ( +
+ +
{t('upload_voice_note', 'Upload a voice note or audio file')}
+
MP3, WAV, M4A, WEBM (Max 10MB)
+ setData('voice_note', e.target.files[0])} + className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" + /> +
+ ) : ( +
+
+
+
+ +
+
+

{data.voice_note.name}

+

{(data.voice_note.size / 1024 / 1024).toFixed(2)} MB

+
+
+ +
+ + {/* Audio preview player */} +
+
+
+ )} + {errors.voice_note && ( +

{errors.voice_note}

+ )} +
+ {/* Support notice */} {data.priority === 'high' && (
diff --git a/resources/js/Pages/Employer/Support/Show.jsx b/resources/js/Pages/Employer/Support/Show.jsx index 62f7975..c96a15a 100644 --- a/resources/js/Pages/Employer/Support/Show.jsx +++ b/resources/js/Pages/Employer/Support/Show.jsx @@ -11,7 +11,10 @@ import { Send, Terminal, UserCheck, - Lock + Lock, + UploadCloud, + X, + Volume2 } from 'lucide-react'; export default function Show({ ticket, replies }) { @@ -19,12 +22,13 @@ export default function Show({ ticket, replies }) { const { data, setData, post, processing, reset, errors } = useForm({ message: '', + voice_note: null, }); const handleSubmit = (e) => { e.preventDefault(); post(`/employer/support/${ticket.id}/reply`, { - onSuccess: () => reset('message'), + onSuccess: () => reset('message', 'voice_note'), }); }; @@ -133,11 +137,26 @@ export default function Show({ ticket, replies }) {
-
- -
- {ticket.description} +
+
+ +
+ {ticket.description} +
+ + {ticket.voice_note_url && ( +
+ +
+
+
+ )}
@@ -184,11 +203,25 @@ export default function Show({ ticket, replies }) {
{reply.created_at} -
- {reply.message} -
+ {reply.message && ( +
+ {reply.message} +
+ )} + + {reply.voice_note_url && ( +
+
+
+
+ )} ); })} @@ -219,13 +252,56 @@ export default function Show({ ticket, replies }) { className={`w-full p-3 border rounded-xl bg-slate-50/50 text-slate-800 outline-none focus:bg-white focus:border-[#185FA5] transition-all leading-relaxed ${ errors.message ? 'border-red-400 bg-red-50/20' : 'border-slate-200' }`} - required + required={!data.voice_note} /> {errors.message && (

{errors.message}

)} + {/* Voice Note Attachment */} +
+ + {!data.voice_note ? ( +
+ +
{t('upload_voice_note', 'Upload a voice note or audio file')}
+ setData('voice_note', e.target.files[0])} + className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" + /> +
+ ) : ( +
+
+
+ +
+

{data.voice_note.name}

+
+
+ +
+
+ )} + {errors.voice_note && ( +

{errors.voice_note}

+ )} +
+
-

- "{previewWorker.bio}" -

- -
-
-
{t('expectations', 'Expectations')}
-
{previewWorker.salary} {t('aed', 'AED')} / {t('mo', 'mo')}
-
-
-
{t('job_preference', 'Job Preference')}
-
{previewWorker.preferred_job_type}
-
-
-
{t('languages_spoken', 'Languages')}
-
- {previewWorker.languages?.map(lang => ( - - {getLanguageFlag(lang)} - {lang} - - ))} -
-
-
+
+
+
{t('expectations', 'Expectations')}
+
{previewWorker.salary} {t('aed', 'AED')} / {t('mo', 'mo')}
+
+
+
{t('job_type', 'Job Type')}
+
{previewWorker.preferred_job_type}
+
+
+
{t('gender', 'Gender')}
+
{previewWorker.gender || 'Female'}
+
+
+
{t('visa_status', 'Visa Status')}
+
{previewWorker.visa_status || 'Residence Visa'}
+
+
+
{t('preferred_location', 'Preferred Location')}
+
+ + {previewWorker.preferred_location || 'Not Specified'} +
+
+
+
{t('languages_spoken', 'Languages')}
+
+ {previewWorker.languages?.map(lang => ( + + {getLanguageFlag(lang)} + {lang} + + ))} +
+
+
{/* Core Platform Skills inside Quick Preview */}
diff --git a/resources/js/Pages/Employer/Workers/Show.jsx b/resources/js/Pages/Employer/Workers/Show.jsx index 32cb3d4..eab45bb 100644 --- a/resources/js/Pages/Employer/Workers/Show.jsx +++ b/resources/js/Pages/Employer/Workers/Show.jsx @@ -131,7 +131,7 @@ export default function Show({ worker }) { return ( - +
@@ -229,8 +229,6 @@ export default function Show({ worker }) { {worker.age} {t('years_old', 'Years Old')} - - {worker.category}
{workerReviews.length > 0 && (
diff --git a/tests/Feature/AnnouncementApiTest.php b/tests/Feature/AnnouncementApiTest.php index 947482b..bb40e4e 100644 --- a/tests/Feature/AnnouncementApiTest.php +++ b/tests/Feature/AnnouncementApiTest.php @@ -22,11 +22,6 @@ protected function setUp(): void { parent::setUp(); - // Create a category - $category = \App\Models\WorkerCategory::create([ - 'name' => 'General Helper', - ]); - // Create worker with API token $this->workerToken = 'worker-token-xyz'; $this->worker = Worker::create([ @@ -43,7 +38,6 @@ protected function setUp(): void 'experience' => '3 Years', 'religion' => 'Hindu', 'bio' => 'Experienced helper ready to work.', - 'category_id' => $category->id, ]); // Create employer with API token diff --git a/tests/Feature/EmployerMessageApiTest.php b/tests/Feature/EmployerMessageApiTest.php index 2b869c5..d1251e5 100644 --- a/tests/Feature/EmployerMessageApiTest.php +++ b/tests/Feature/EmployerMessageApiTest.php @@ -22,10 +22,6 @@ protected function setUp(): void { parent::setUp(); - // Create a category - $category = \App\Models\WorkerCategory::create([ - 'name' => 'Housemaid', - ]); // Create a test worker $this->worker = Worker::create([ @@ -41,7 +37,6 @@ protected function setUp(): void 'experience' => '2 Years', 'religion' => 'Christian', 'bio' => 'Experienced housemaid seeking employment.', - 'category_id' => $category->id, ]); // Create an employer user with token @@ -181,7 +176,6 @@ public function test_employer_can_get_conversations() 'id', 'worker_id', 'worker_name', - 'category', 'nationality', 'salary', 'last_message', @@ -229,7 +223,6 @@ public function test_employer_can_get_messages_and_mark_as_read() 'conversation' => [ 'id', 'worker_name', - 'category', 'nationality', 'salary', ], diff --git a/tests/Feature/EmployerProfileApiTest.php b/tests/Feature/EmployerProfileApiTest.php index 7daf7bb..66ac1b5 100644 --- a/tests/Feature/EmployerProfileApiTest.php +++ b/tests/Feature/EmployerProfileApiTest.php @@ -169,4 +169,53 @@ public function test_employer_password_update_fails_with_incorrect_current_passw // Verify password was NOT changed $this->assertTrue(Hash::check('Password@123', $this->employer->fresh()->password)); } + + /** + * Test employer dashboard returns only the latest 2 approved charity drives. + */ + public function test_employer_dashboard_returns_only_two_charity_drives() + { + // Create 5 approved announcements + for ($i = 1; $i <= 5; $i++) { + $ann = new \App\Models\Announcement([ + 'title' => "Charity Drive $i", + 'body' => "Details of charity drive $i", + 'type' => 'charity', + 'status' => 'approved', + ]); + $ann->created_at = now()->subMinutes(6 - $i); + $ann->save(); + } + + // Create 1 pending announcement + $pendingAnn = new \App\Models\Announcement([ + 'title' => "Pending Charity Drive", + 'body' => "Details of pending charity drive", + 'type' => 'charity', + 'status' => 'pending', + ]); + $pendingAnn->created_at = now(); + $pendingAnn->save(); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer ' . $this->token, + ])->getJson('/api/employers/dashboard'); + + $response->assertStatus(200) + ->assertJson([ + 'success' => true, + ]); + + $announcements = $response->json('data.recent_announcements'); + $this->assertCount(2, $announcements); + + // Make sure it contains the latest 2 approved ones + $titles = collect($announcements)->pluck('title'); + $this->assertTrue($titles->contains('Charity Drive 5')); + $this->assertTrue($titles->contains('Charity Drive 4')); + $this->assertFalse($titles->contains('Charity Drive 3')); + $this->assertFalse($titles->contains('Charity Drive 2')); + $this->assertFalse($titles->contains('Charity Drive 1')); + $this->assertFalse($titles->contains('Pending Charity Drive')); + } } diff --git a/tests/Feature/EmployerSupportTicketApiTest.php b/tests/Feature/EmployerSupportTicketApiTest.php index 9d7b97e..27bcb2e 100644 --- a/tests/Feature/EmployerSupportTicketApiTest.php +++ b/tests/Feature/EmployerSupportTicketApiTest.php @@ -31,6 +31,55 @@ protected function setUp(): void ]); } + public function test_employer_can_create_ticket_with_reason_and_voice_note() + { + Storage::fake('public'); + + $reason = \App\Models\ReportReason::create([ + 'reason' => 'Billing Inquiry', + 'status' => 'active', + 'type' => 'support', + ]); + + $voiceFile = UploadedFile::fake()->create('note.mp3', 500, 'audio/mpeg'); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer ' . $this->token, + ])->postJson('/api/employers/tickets', [ + 'subject' => 'Billing query', + 'description' => 'Need help with invoice.', + 'priority' => 'high', + 'reason_id' => $reason->id, + 'voice_note' => $voiceFile, + ]); + + $response->assertStatus(201) + ->assertJsonStructure([ + 'success', + 'message', + 'ticket' => [ + 'id', + 'ticket_number', + 'subject', + 'description', + 'reason_id', + 'reason_name', + 'voice_note_url', + 'status', + 'priority', + 'created_at', + ] + ]); + + $ticket = SupportTicket::first(); + $this->assertNotNull($ticket); + $this->assertEquals('Billing query', $ticket->subject); + $this->assertEquals($reason->id, $ticket->reason_id); + $this->assertNotNull($ticket->voice_note_path); + + Storage::disk('public')->assertExists($ticket->voice_note_path); + } + public function test_employer_can_reply_to_ticket_with_voice_note() { Storage::fake('public'); @@ -82,4 +131,67 @@ public function test_employer_can_reply_to_ticket_with_voice_note() 'voice_note_url' => asset('storage/' . $reply->voice_note_path) ]); } + + public function test_employer_web_can_create_ticket_with_voice_note() + { + Storage::fake('public'); + + $reason = \App\Models\ReportReason::create([ + 'reason' => 'Technical Error', + 'status' => 'active', + 'type' => 'support', + ]); + + $voiceFile = UploadedFile::fake()->create('web_note.mp3', 300, 'audio/mpeg'); + + $response = $this->actingAs($this->employer) + ->withSession(['user' => $this->employer]) + ->post('/employer/support/create', [ + 'subject' => 'Web ticket subject', + 'description' => 'Web ticket description.', + 'priority' => 'low', + 'reason_id' => $reason->id, + 'voice_note' => $voiceFile, + ]); + + $ticket = SupportTicket::first(); + $this->assertNotNull($ticket); + $this->assertEquals('Web ticket subject', $ticket->subject); + $this->assertEquals($reason->id, $ticket->reason_id); + $this->assertNotNull($ticket->voice_note_path); + + Storage::disk('public')->assertExists($ticket->voice_note_path); + + $response->assertRedirect(route('employer.support.show', $ticket->id)); + } + + public function test_employer_web_can_reply_to_ticket_with_voice_note() + { + Storage::fake('public'); + + $ticket = SupportTicket::create([ + 'ticket_number' => 'TKT-999999', + 'user_id' => $this->employer->id, + 'subject' => 'Web ticket reply test', + 'description' => 'Initial message.', + 'status' => 'open', + 'priority' => 'medium', + ]); + + $voiceFile = UploadedFile::fake()->create('web_reply_note.mp3', 200, 'audio/mpeg'); + + $response = $this->actingAs($this->employer) + ->withSession(['user' => $this->employer]) + ->post("/employer/support/{$ticket->id}/reply", [ + 'voice_note' => $voiceFile, + ]); + + $reply = SupportTicketReply::first(); + $this->assertNotNull($reply); + $this->assertNull($reply->message); + $this->assertNotNull($reply->voice_note_path); + Storage::disk('public')->assertExists($reply->voice_note_path); + + $response->assertRedirect(route('employer.support.show', $ticket->id)); + } } diff --git a/tests/Feature/EmployerWorkerFilterApiTest.php b/tests/Feature/EmployerWorkerFilterApiTest.php index 1cf712a..1ba6fc3 100644 --- a/tests/Feature/EmployerWorkerFilterApiTest.php +++ b/tests/Feature/EmployerWorkerFilterApiTest.php @@ -4,7 +4,6 @@ use App\Models\User; use App\Models\Worker; -use App\Models\WorkerCategory; use App\Models\WorkerDocument; use App\Models\JobOffer; use App\Models\JobPost; @@ -18,7 +17,6 @@ class EmployerWorkerFilterApiTest extends TestCase protected $employer; protected $token; - protected $category; protected function setUp(): void { @@ -34,10 +32,6 @@ protected function setUp(): void 'api_token' => $this->token, ]); - // Create worker category - $this->category = WorkerCategory::create([ - 'name' => 'General Helper', - ]); // Seed 3 workers with specific filter attributes // Worker 1: Dubai, full-time, live-in, Indian, in_country=true, Residence Visa, female @@ -47,7 +41,6 @@ protected function setUp(): void 'phone' => '+971501111111', 'password' => bcrypt('password'), 'nationality' => 'Indian', - 'category_id' => $this->category->id, 'verified' => true, 'status' => 'active', 'preferred_location' => 'Dubai', @@ -79,7 +72,6 @@ protected function setUp(): void 'phone' => '+971502222222', 'password' => bcrypt('password'), 'nationality' => 'Filipino', - 'category_id' => $this->category->id, 'verified' => true, 'status' => 'active', 'preferred_location' => 'Abu Dhabi', @@ -111,7 +103,6 @@ protected function setUp(): void 'phone' => '+971503333333', 'password' => bcrypt('password'), 'nationality' => 'Kenyan', - 'category_id' => $this->category->id, 'verified' => true, 'status' => 'active', 'preferred_location' => 'Dubai', @@ -408,7 +399,6 @@ public function test_worker_list_includes_document_expiry_status() 'phone' => '+971509999999', 'password' => bcrypt('password'), 'nationality' => 'Kenyan', - 'category_id' => $this->category->id, 'verified' => true, 'status' => 'active', 'preferred_location' => 'Dubai', diff --git a/tests/Feature/EmployerWorkerWebFilterTest.php b/tests/Feature/EmployerWorkerWebFilterTest.php index 6e6756c..a71e688 100644 --- a/tests/Feature/EmployerWorkerWebFilterTest.php +++ b/tests/Feature/EmployerWorkerWebFilterTest.php @@ -4,7 +4,6 @@ use App\Models\User; use App\Models\Worker; -use App\Models\WorkerCategory; use App\Models\JobOffer; use App\Models\WorkerDocument; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -15,7 +14,6 @@ class EmployerWorkerWebFilterTest extends TestCase use RefreshDatabase; protected $employer; - protected $category; protected function setUp(): void { @@ -28,9 +26,6 @@ protected function setUp(): void 'role' => 'employer', ]); - $this->category = WorkerCategory::create([ - 'name' => 'General Helper', - ]); // Seed some workers $w1 = Worker::create([ @@ -39,7 +34,6 @@ protected function setUp(): void 'phone' => '+971501111111', 'password' => bcrypt('password'), 'nationality' => 'Indian', - 'category_id' => $this->category->id, 'verified' => true, 'status' => 'active', 'preferred_location' => 'Dubai', @@ -69,7 +63,6 @@ protected function setUp(): void 'phone' => '+971502222222', 'password' => bcrypt('password'), 'nationality' => 'Filipino', - 'category_id' => $this->category->id, 'verified' => true, 'status' => 'active', 'preferred_location' => 'Abu Dhabi', @@ -165,7 +158,6 @@ public function test_employer_can_view_candidates_index_and_apply_filters() 'phone' => '+971503333333', 'password' => bcrypt('password'), 'nationality' => 'Kenyan', - 'category_id' => $this->category->id, 'verified' => true, 'status' => 'Hired', 'preferred_location' => 'Dubai', diff --git a/tests/Feature/FcmAuthenticationTest.php b/tests/Feature/FcmAuthenticationTest.php new file mode 100644 index 0000000..2d0e9df --- /dev/null +++ b/tests/Feature/FcmAuthenticationTest.php @@ -0,0 +1,200 @@ + "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC3\n-----END PRIVATE KEY-----", + 'client_email' => 'fake-fcm@migrant.iam.gserviceaccount.com', + 'project_id' => 'migrant-fe5a7' + ])); + } + + // Fake Google OAuth and FCM endpoints + Http::fake([ + 'https://oauth2.googleapis.com/token' => Http::response(['access_token' => 'fake_token'], 200), + 'https://fcm.googleapis.com/v1/projects/*' => Http::response(['name' => 'projects/migrant-fe5a7/messages/1234'], 200), + ]); + } + + protected function tearDown(): void + { + // Clean up temporary credentials file if needed, but since it's inside workspace it's fine. + parent::tearDown(); + } + + public function test_employer_registration_and_login_stores_fcm_token_and_sends_notification() + { + // Register Employer + $response = $this->postJson('/api/employers/register', [ + 'name' => 'Employer FCM Test', + 'email' => 'employer.fcm@example.com', + 'phone' => '+971500000001', + 'fcm_token' => 'employer_register_fcm_token' + ]); + + $response->assertStatus(201); + + $user = User::where('email', 'employer.fcm@example.com')->first(); + $this->assertEquals('employer_register_fcm_token', $user->fcm_token); + + $sponsor = Sponsor::where('email', 'employer.fcm@example.com')->first(); + $this->assertEquals('employer_register_fcm_token', $sponsor->fcm_token); + + // Verify OTP + $otpData = Cache::get('employer_otp_employer.fcm@example.com'); + $this->assertNotNull($otpData); + + $verifyResponse = $this->postJson('/api/employers/verify', [ + 'email' => 'employer.fcm@example.com', + 'otp' => $otpData['code'], + 'fcm_token' => 'employer_verify_fcm_token' + ]); + $verifyResponse->assertStatus(200); + + $user->refresh(); + $sponsor->refresh(); + $this->assertEquals('employer_verify_fcm_token', $user->fcm_token); + $this->assertEquals('employer_verify_fcm_token', $sponsor->fcm_token); + + // Set payment/activate status so password can be set + $sponsor->update(['payment_status' => 'paid']); + + // Set password + $passwordResponse = $this->postJson('/api/employers/password', [ + 'email' => 'employer.fcm@example.com', + 'password' => 'newpassword123', + 'password_confirmation' => 'newpassword123', + 'fcm_token' => 'employer_password_fcm_token' + ]); + $passwordResponse->assertStatus(200); + + $user->refresh(); + $sponsor->refresh(); + $this->assertEquals('employer_password_fcm_token', $user->fcm_token); + $this->assertEquals('employer_password_fcm_token', $sponsor->fcm_token); + + // Login + $loginResponse = $this->postJson('/api/employers/login', [ + 'email' => 'employer.fcm@example.com', + 'password' => 'newpassword123', + 'fcm_token' => 'employer_login_fcm_token' + ]); + $loginResponse->assertStatus(200); + + $user->refresh(); + $sponsor->refresh(); + $this->assertEquals('employer_login_fcm_token', $user->fcm_token); + $this->assertEquals('employer_login_fcm_token', $sponsor->fcm_token); + } + + public function test_worker_registration_setup_profile_and_login_stores_fcm_token_and_sends_notification() + { + // 1. Worker Setup Profile Flow (Requires OTP verified in session cache) + $phone = '+971500000002'; + Cache::put('worker_otp_verified_' . $phone, true, 60); + + $responseSetup = $this->postJson('/api/workers/setup-profile', [ + 'phone' => $phone, + 'name' => 'Worker Setup FCM', + 'nationality' => 'Indian', + 'language' => 'hi', + 'gender' => 'male', + 'fcm_token' => 'worker_setup_fcm_token' + ]); + + $responseSetup->assertStatus(201); + $worker = Worker::where('phone', $phone)->first(); + $this->assertEquals('worker_setup_fcm_token', $worker->fcm_token); + + // Set password for login + $worker->update(['password' => Hash::make('workerpassword123')]); + + // Login Setup Worker + $loginResponse = $this->postJson('/api/workers/login', [ + 'phone' => $phone, + 'password' => 'workerpassword123', + 'fcm_token' => 'worker_login_fcm_token' + ]); + $loginResponse->assertStatus(200); + + $worker->refresh(); + $this->assertEquals('worker_login_fcm_token', $worker->fcm_token); + + // 2. Worker Unified Register Flow + $passportFile = UploadedFile::fake()->create('passport.pdf', 500, 'application/pdf'); + $visaFile = UploadedFile::fake()->create('visa.pdf', 500, 'application/pdf'); + + $responseRegister = $this->postJson('/api/workers/register', [ + 'name' => 'Worker Unified FCM', + 'phone' => '+971500000003', + 'salary' => 2000, + 'password' => 'workerpassword123', + 'passport_file' => $passportFile, + 'visa_file' => $visaFile, + 'fcm_token' => 'worker_register_fcm_token' + ]); + + $responseRegister->assertStatus(201); + $workerReg = Worker::where('phone', '+971500000003')->first(); + $this->assertEquals('worker_register_fcm_token', $workerReg->fcm_token); + } + + public function test_sponsor_registration_and_login_stores_fcm_token_and_sends_notification() + { + $licenseFile = UploadedFile::fake()->create('license.pdf', 500, 'application/pdf'); + $emiratesIdFile = UploadedFile::fake()->create('emirates_id.pdf', 500, 'application/pdf'); + + $response = $this->postJson('/api/sponsors/register', [ + 'full_name' => 'Sponsor FCM Org', + 'mobile' => '+971500000004', + 'password' => 'sponsorpassword123', + 'license_file' => $licenseFile, + 'emirates_id_file' => $emiratesIdFile, + 'organization_name' => 'Sponsor FCM Co', + 'email' => 'sponsor.fcm@example.com', + 'nationality' => 'Emirati', + 'city' => 'Dubai', + 'address' => 'Test Address', + 'country_code' => '+971', + 'license_expiry' => '2027-01-01', + 'fcm_token' => 'sponsor_register_fcm_token' + ]); + + $response->assertStatus(201); + $sponsor = Sponsor::where('mobile', '+971500000004')->first(); + $this->assertEquals('sponsor_register_fcm_token', $sponsor->fcm_token); + + // Login + $loginResponse = $this->postJson('/api/sponsors/login', [ + 'mobile' => '+971500000004', + 'password' => 'sponsorpassword123', + 'fcm_token' => 'sponsor_login_fcm_token' + ]); + + $loginResponse->assertStatus(200); + $sponsor->refresh(); + $this->assertEquals('sponsor_login_fcm_token', $sponsor->fcm_token); + } +} diff --git a/tests/Feature/ReportApiTest.php b/tests/Feature/ReportApiTest.php index 3d7b51a..09e02b4 100644 --- a/tests/Feature/ReportApiTest.php +++ b/tests/Feature/ReportApiTest.php @@ -17,16 +17,11 @@ class ReportApiTest extends TestCase protected $employer; protected $workerToken; protected $employerToken; - protected $category; protected function setUp(): void { parent::setUp(); - // Create category - $this->category = \App\Models\WorkerCategory::create([ - 'name' => 'Housemaid', - ]); // Create worker $this->workerToken = 'worker-token-xyz'; @@ -44,7 +39,6 @@ protected function setUp(): void 'experience' => '2 Years', 'religion' => 'Christian', 'bio' => 'Experienced housemaid seeking employment.', - 'category_id' => $this->category->id, ]); // Create employer user @@ -148,7 +142,6 @@ public function test_worker_report_invalid_review_returns_404() 'email' => 'other@example.com', 'phone' => '+971500000009', 'password' => bcrypt('password'), - 'category_id' => $this->category->id, 'nationality' => 'Indian', 'age' => 30, 'salary' => 1800.00, diff --git a/tests/Feature/SponsorAuthApiTest.php b/tests/Feature/SponsorAuthApiTest.php index 599f775..7eaa528 100644 --- a/tests/Feature/SponsorAuthApiTest.php +++ b/tests/Feature/SponsorAuthApiTest.php @@ -356,10 +356,6 @@ public function test_sponsor_dashboard_returns_stats() 'subscription_status' => 'none', ]); - $category = \App\Models\WorkerCategory::create([ - 'name' => 'Housemaid', - ]); - // Create some workers \App\Models\Worker::create([ 'name' => 'Worker 1', @@ -375,7 +371,6 @@ public function test_sponsor_dashboard_returns_stats() 'experience' => 'None', 'religion' => 'Christian', 'bio' => 'Test', - 'category_id' => $category->id, 'availability' => 'Immediate', ]); \App\Models\Worker::create([ @@ -392,7 +387,6 @@ public function test_sponsor_dashboard_returns_stats() 'experience' => 'None', 'religion' => 'Christian', 'bio' => 'Test', - 'category_id' => $category->id, 'availability' => 'Immediate', ]); diff --git a/tests/Feature/WorkerJourneyApiTest.php b/tests/Feature/WorkerJourneyApiTest.php index effa287..2b7f45a 100644 --- a/tests/Feature/WorkerJourneyApiTest.php +++ b/tests/Feature/WorkerJourneyApiTest.php @@ -3,7 +3,6 @@ namespace Tests\Feature; use App\Models\Worker; -use App\Models\WorkerCategory; use App\Models\WorkerDocument; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Http\UploadedFile; @@ -15,21 +14,12 @@ class WorkerJourneyApiTest extends TestCase { use RefreshDatabase; - protected $category; protected function setUp(): void { parent::setUp(); - // Create standard General Helper category - \Illuminate\Support\Facades\DB::table('worker_categories')->insert([ - 'id' => 7, - 'name' => 'General Helper', - 'created_at' => now(), - 'updated_at' => now(), - ]); - $this->category = (object) ['id' => 7]; } /** @@ -183,7 +173,6 @@ public function test_s2_update_experience_and_preferences() 'experience' => 'None', 'religion' => 'Christian', 'bio' => 'New helper', - 'category_id' => $this->category->id, 'verified' => false, 'status' => 'active', 'api_token' => 'test-bearer-token-123', @@ -227,7 +216,6 @@ public function test_s3_go_live() 'experience' => 'Not Specified', 'religion' => 'Not Specified', 'bio' => 'Ready to work', - 'category_id' => $this->category->id, 'verified' => true, 'status' => 'inactive', 'api_token' => 'test-bearer-token-789', @@ -266,7 +254,6 @@ public function test_s5_toggle_availability_still_looking() 'experience' => 'Not Specified', 'religion' => 'Not Specified', 'bio' => 'Test', - 'category_id' => $this->category->id, 'verified' => true, 'status' => 'active', 'api_token' => 'test-bearer-token-abc', @@ -329,7 +316,6 @@ public function test_s6_outcome_mark_hired() 'experience' => 'Not Specified', 'religion' => 'Not Specified', 'bio' => 'Test', - 'category_id' => $this->category->id, 'verified' => true, 'status' => 'active', 'api_token' => 'test-bearer-token-abc', @@ -405,7 +391,6 @@ public function test_update_profile_with_new_api_fields() 'experience' => 'None', 'religion' => 'Christian', 'bio' => 'New helper', - 'category_id' => $this->category->id, 'verified' => false, 'status' => 'active', 'api_token' => 'bearer-update-token', @@ -630,7 +615,6 @@ public function test_worker_dashboard_stats() 'experience' => 'Not Specified', 'religion' => 'Not Specified', 'bio' => 'Test', - 'category_id' => $this->category->id, 'verified' => true, 'status' => 'active', 'api_token' => 'worker-test-token-dashboard', @@ -722,7 +706,6 @@ public function test_worker_new_announcements_flow() 'experience' => 'Not Specified', 'religion' => 'Not Specified', 'bio' => 'Test', - 'category_id' => $this->category->id, 'verified' => true, 'status' => 'active', 'api_token' => 'worker-test-token-announcements', diff --git a/tests/Feature/WorkerMessageApiTest.php b/tests/Feature/WorkerMessageApiTest.php index 9e2d04f..243f845 100644 --- a/tests/Feature/WorkerMessageApiTest.php +++ b/tests/Feature/WorkerMessageApiTest.php @@ -21,11 +21,6 @@ protected function setUp(): void { parent::setUp(); - // Create a category - $category = \App\Models\WorkerCategory::create([ - 'name' => 'Housemaid', - ]); - // Create a test worker with an API token $this->token = 'test-token-12345'; $this->worker = Worker::create([ @@ -42,7 +37,6 @@ protected function setUp(): void 'experience' => '2 Years', 'religion' => 'Christian', 'bio' => 'Experienced housemaid seeking employment.', - 'category_id' => $category->id, ]); // Create an employer user diff --git a/tests/Feature/WorkerSupportTicketApiTest.php b/tests/Feature/WorkerSupportTicketApiTest.php index dda0975..a2714a9 100644 --- a/tests/Feature/WorkerSupportTicketApiTest.php +++ b/tests/Feature/WorkerSupportTicketApiTest.php @@ -23,10 +23,6 @@ protected function setUp(): void parent::setUp(); $this->token = 'worker-test-token-789'; - - $category = \App\Models\WorkerCategory::create([ - 'name' => 'Housemaid', - ]); $this->worker = Worker::create([ 'name' => 'John Worker', @@ -35,7 +31,6 @@ protected function setUp(): void 'password' => bcrypt('password'), 'api_token' => $this->token, 'status' => 'Available', - 'category_id' => $category->id, 'nationality' => 'Ethiopian', 'age' => 28, 'salary' => 1500.00,