employer ticket creation

This commit is contained in:
mohanmd 2026-06-15 11:59:05 +05:30
parent 95bb24ab1e
commit 3ecd12ed10
54 changed files with 938 additions and 436 deletions

View File

@ -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'];

View File

@ -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.',

View File

@ -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',

View File

@ -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;

View File

@ -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) {

View File

@ -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.',
@ -131,6 +141,7 @@ public function login(Request $request)
$validator = Validator::make($request->all(), [
'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,

View File

@ -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(),

View File

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

View File

@ -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) {

View File

@ -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'),

View File

@ -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,

View File

@ -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,

View File

@ -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',

View File

@ -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,

View File

@ -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

View File

@ -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,

View File

@ -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()
{

View File

@ -34,6 +34,7 @@ class Sponsor extends Authenticatable
'license_file',
'emirates_id_file',
'license_expiry',
'fcm_token',
];
protected $hidden = [

View File

@ -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()
{

View File

@ -1,18 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class WorkerCategory extends Model
{
use HasFactory;
protected $fillable = ['name'];
public function workers()
{
return $this->hasMany(Worker::class, 'category_id');
}
}

View File

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

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('sponsors', function (Blueprint $table) {
$table->string('fcm_token')->nullable()->after('api_token');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('sponsors', function (Blueprint $table) {
$table->dropColumn('fcm_token');
});
}
};

View File

@ -29,7 +29,6 @@ public function run(): void
// Call Seeders
$this->call([
WorkerCategorySeeder::class,
SkillSeeder::class,
WorkerSeeder::class,
EmployerProfileSeeder::class,

View File

@ -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',

View File

@ -27,7 +27,6 @@ public function run(): void
// 2. Run static reference seeders
$this->call([
WorkerCategorySeeder::class,
SkillSeeder::class,
]);
}

View File

@ -1,28 +0,0 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class WorkerCategorySeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$categories = [
'Electrician', 'Mason', 'Plumber', 'Cleaner', 'Site Supervisor', 'Driver', 'General Helper',
'Childcare', 'Housekeeping', 'Cooking', 'Elderly Care'
];
foreach ($categories as $category) {
\Illuminate\Support\Facades\DB::table('worker_categories')->insertOrIgnore([
'name' => $category,
'created_at' => now(),
'updated_at' => now(),
]);
}
}
}

View File

@ -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(),
]));

View File

@ -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

View File

@ -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,

View File

@ -272,7 +272,7 @@ export default function Dashboard({
<div className="space-y-2.5 flex-1 py-2">
<Link
href="/employer/workers?category=Childcare"
href="/employer/workers"
className="w-full p-3.5 bg-slate-50 hover:bg-blue-50/50 border border-slate-200 hover:border-[#185FA5]/40 rounded-2xl transition-all flex items-center space-x-3 text-left group"
>
<div className="w-9 h-9 rounded-xl bg-purple-100/50 text-purple-600 flex items-center justify-center flex-shrink-0 group-hover:scale-105 transition-transform">
@ -345,13 +345,6 @@ export default function Dashboard({
</div>
</div>
<div className="space-y-1">
<div className="text-[10px] font-semibold text-slate-400 uppercase tracking-tighter">{t('category', 'Category')}:</div>
<div className="inline-block px-2 py-0.5 bg-white border border-slate-200 rounded-md text-xs font-semibold text-slate-700">
{worker.category || 'General Helper'}
</div>
</div>
<Link
href={`/employer/workers/${worker.id}`}
className="w-full mt-2 bg-white hover:bg-slate-50 text-[#185FA5] border border-slate-200 text-center rounded-lg py-1.5 text-[10px] font-black flex items-center justify-center transition-colors"
@ -395,8 +388,13 @@ export default function Dashboard({
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between">
<div className="font-bold text-xs text-slate-800 group-hover:text-[#185FA5] transition-colors truncate">
{msg.worker_name}
<div className="font-bold text-xs text-slate-800 group-hover:text-[#185FA5] transition-colors truncate flex items-center gap-1.5 flex-wrap">
<span>{msg.worker_name}</span>
{msg.worker_nationality && (
<span className="text-[9px] font-normal text-slate-500 bg-slate-100/80 px-1.5 py-0.5 rounded-md border border-slate-200/50">
{msg.worker_nationality}
</span>
)}
</div>
<div className="flex items-center space-x-2">
<span className="text-[9px] font-semibold text-slate-400">{msg.sent_at}</span>
@ -405,7 +403,12 @@ export default function Dashboard({
)}
</div>
</div>
<p className="text-xs text-slate-600 truncate mt-0.5 font-medium">
<p className="text-xs text-slate-600 truncate mt-1 font-medium">
{msg.sender_type === 'employer' ? (
<span className="text-slate-400 font-semibold">{t('you', 'You')}: </span>
) : (
<span className="text-[#185FA5] font-semibold">{msg.worker_name}: </span>
)}
{msg.last_message}
</p>
</div>

View File

@ -50,7 +50,7 @@ export default function Confirm({ worker }) {
{worker.name.charAt(0)}
</div>
<h3 className="font-extrabold text-slate-900 tracking-tight">{worker.name}</h3>
<p className="text-xs font-bold text-[#185FA5] uppercase tracking-widest mt-1">{worker.category}</p>
</div>
<div className="p-6 space-y-4">
<div className="flex items-center justify-between text-xs font-medium">

View File

@ -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 <span className="text-[#185FA5]">{job?.title || 'Senior Mason Project'}</span>
</h1>
<div className="flex items-center space-x-3 text-[10px] font-bold text-slate-400 uppercase tracking-widest">
<span className="flex items-center"><Briefcase className="w-3 h-3 mr-1" /> {job?.category || 'Mason'}</span>
<span className="w-1 h-1 bg-slate-200 rounded-full" />
<span className="flex items-center"><DollarSign className="w-3 h-3 mr-1" /> {job?.salary || '2800'} AED</span>
</div>
</div>

View File

@ -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 }) {
</div>
</div>
<div className="space-y-2">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Worker Category</label>
<div className="relative">
<LayoutGrid className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<select
className="w-full pl-11 pr-4 py-3 bg-slate-50 border border-slate-100 rounded-2xl text-sm font-bold text-slate-900 focus:ring-4 focus:ring-blue-500/10 focus:border-[#185FA5] outline-none transition-all appearance-none"
value={data.category}
onChange={e => setData('category', e.target.value)}
required
>
<option value="">Select Category</option>
{categories.map(cat => <option key={cat} value={cat}>{cat}</option>)}
</select>
</div>
</div>
<div className="space-y-2">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Workers Needed</label>

View File

@ -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 }) {
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<input
type="text"
placeholder="Search by job title or category..."
placeholder="Search by job title..."
className="w-full pl-11 pr-4 py-3 bg-slate-50 border border-slate-100 rounded-2xl text-sm font-bold text-slate-900 focus:ring-4 focus:ring-blue-500/10 focus:border-[#185FA5] outline-none transition-all"
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
@ -120,8 +119,6 @@ export default function Index({ initialJobs }) {
<div>
<h3 className="font-black text-slate-900 tracking-tight group-hover:text-[#185FA5] transition-colors line-clamp-1">{job.title}</h3>
<div className="flex items-center space-x-2 mt-1">
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">{job.category}</span>
<span className="w-1 h-1 bg-slate-300 rounded-full" />
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">{job.posted_at}</span>
</div>
</div>

View File

@ -65,9 +65,6 @@ export default function Index({ conversations }) {
<div className="flex items-center justify-between mb-1">
<div className="flex items-center space-x-2 font-bold text-base text-slate-900 group-hover:text-[#185FA5] transition-colors truncate">
<span>{thread.worker_name}</span>
<span className="px-2 py-0.5 bg-slate-100 text-slate-500 rounded-md text-[10px] font-bold uppercase tracking-wider hidden sm:inline-block">
{thread.category}
</span>
</div>
<span className={`text-xs font-semibold whitespace-nowrap ${thread.unread ? 'text-[#185FA5]' : 'text-slate-400'}`}>
{thread.sent_at}

View File

@ -498,9 +498,6 @@ export default function Show({ conversation, initialMessages, conversations = []
</div>
<div>
<h4 className="font-extrabold text-base text-slate-900 tracking-tight">{conversation.worker_name}</h4>
<span className="inline-block bg-slate-50 px-2 py-0.5 border border-slate-200 rounded text-[9px] uppercase font-black tracking-widest mt-1">
{conversation.category}
</span>
</div>
</div>

View File

@ -418,7 +418,6 @@ export default function SelectedCandidates({
<tr className="border-b border-slate-100">
<th className="px-6 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest">{t('candidate_header', 'Candidate')}</th>
<th className="px-6 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest">{t('selected_header', 'Selected')}</th>
<th className="px-6 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest">{t('category_header', 'Category')}</th>
<th className="px-6 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest">{t('salary_header', 'Salary')}</th>
<th className="px-6 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest">{t('status_header', 'Status')}</th>
<th className="px-6 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest text-right">{t('actions_header', 'Actions')}</th>
@ -443,9 +442,6 @@ export default function SelectedCandidates({
</div>
</td>
<td className="px-6 py-5 text-[11px] font-bold text-slate-500">{worker.applied_at || '—'}</td>
<td className="px-6 py-5">
<span className="px-2 py-1 bg-slate-100 text-slate-600 text-[10px] font-bold rounded-lg">{worker.category}</span>
</td>
<td className="px-6 py-5">
<span className="text-sm font-bold text-slate-800">{worker.salary} <span className="text-[10px] text-slate-400">AED</span></span>
</td>
@ -510,3 +506,4 @@ export default function SelectedCandidates({
</EmployerLayout>
);
}

View File

@ -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 }) {
<div className="p-6 space-y-4 flex-1 flex flex-col justify-between">
<div className="space-y-4">
{/* Category Pill, Ratings & Cost */}
<div className="flex items-center justify-between text-xs">
<span className="px-2.5 py-1 bg-blue-50 text-[#185FA5] font-black rounded-lg border border-blue-100 uppercase tracking-wider text-[9px]">
{worker.category}
</span>
{/* Ratings & Cost */}
<div className="flex items-center justify-end text-xs">
<div className="flex items-center space-x-1 font-bold text-slate-800">
<DollarSign className="w-3.5 h-3.5 text-emerald-600" />
<span className="text-sm font-black">{worker.salary} AED</span>
@ -199,9 +197,13 @@ export default function Shortlist({ shortlistedWorkers }) {
<Sparkles className="w-3.5 h-3.5 text-slate-400 flex-shrink-0" />
<span className="truncate uppercase text-[10px]">{worker.preferred_job_type}</span>
</div>
<div className="flex items-center space-x-1.5 truncate col-span-2">
<div className="flex items-center space-x-1.5 truncate">
<MapPin className="w-3.5 h-3.5 text-[#185FA5] flex-shrink-0" />
<span className="truncate text-[#185FA5]">{worker.preferred_location || 'Not Specified'}</span>
</div>
<div className="flex items-center space-x-1.5 truncate">
<Star className="w-3.5 h-3.5 text-amber-500 flex-shrink-0 fill-amber-500" />
<span className="truncate">{worker.rating || '4.5'} ({worker.reviews_count || '12'} reviews)</span>
<span className="truncate">{worker.rating || '4.5'} ({worker.reviews_count || '12'})</span>
</div>
</div>
@ -330,7 +332,7 @@ export default function Shortlist({ shortlistedWorkers }) {
</div>
<div>
<div className="font-bold text-xs text-slate-900 truncate max-w-[120px]">{w.name}</div>
<div className="text-[10px] text-slate-500 font-bold">{w.nationality} {w.category}</div>
<div className="text-[10px] text-slate-500 font-bold">{w.nationality}</div>
</div>
</div>
@ -338,7 +340,7 @@ export default function Shortlist({ shortlistedWorkers }) {
<div>Salary: <span className="text-slate-800">{w.salary} AED</span></div>
<div>Age: <span className="text-slate-800">{w.age} yrs</span></div>
<div>Job Type: <span className="text-slate-800 uppercase">{w.preferred_job_type}</span></div>
<div>Status: <span className="text-slate-800 font-black">{w.availability_status}</span></div>
<div className="truncate">Location: <span className="text-slate-800 font-medium" title={w.preferred_location}>{w.preferred_location || 'Not Specified'}</span></div>
</div>
{/* Skills Display in Comparison */}
@ -395,29 +397,34 @@ export default function Shortlist({ shortlistedWorkers }) {
}`} />
<span>{previewWorker.passport_status}</span>
</div>
<div className="text-xs text-slate-500 font-bold">{previewWorker.nationality} {previewWorker.category}</div>
<div className="text-xs text-slate-500 font-bold">{previewWorker.nationality}</div>
</div>
</div>
{previewWorker.bio ? (
<p className="text-xs text-slate-600 italic bg-slate-50 p-4 rounded-xl leading-relaxed">
"{previewWorker.bio}"
</p>
) : (
<p className="text-xs text-slate-400 italic bg-slate-50 p-4 rounded-xl leading-relaxed">
{t('no_bio_provided', 'No bio details provided.')}
</p>
)}
<div className="grid grid-cols-2 gap-4 text-xs font-bold text-slate-600">
<div className="p-3 bg-slate-50/50 rounded-xl">
<div className="text-[10px] text-slate-400 uppercase tracking-widest mb-0.5">{t('expectations', 'Expectations')}</div>
<div className="text-slate-800">{previewWorker.salary} AED / mo</div>
</div>
<div className="p-3 bg-slate-50/50 rounded-xl">
<div className="text-[10px] text-slate-400 uppercase tracking-widest mb-0.5">{t('job_preference', 'Job Preference')}</div>
<div className="text-[10px] text-slate-400 uppercase tracking-widest mb-0.5">{t('job_type', 'Job Type')}</div>
<div className="text-slate-800 uppercase text-[10px] mt-1">{previewWorker.preferred_job_type}</div>
</div>
<div className="p-3 bg-slate-50/50 rounded-xl">
<div className="text-[10px] text-slate-400 uppercase tracking-widest mb-0.5">{t('gender', 'Gender')}</div>
<div className="text-slate-800 capitalize mt-1">{previewWorker.gender || 'Female'}</div>
</div>
<div className="p-3 bg-slate-50/50 rounded-xl">
<div className="text-[10px] text-slate-400 uppercase tracking-widest mb-0.5">{t('visa_status', 'Visa Status')}</div>
<div className="text-slate-800 capitalize mt-1">{previewWorker.visa_status || 'Residence Visa'}</div>
</div>
<div className="p-3 bg-slate-50/50 rounded-xl col-span-2">
<div className="text-[10px] text-slate-400 uppercase tracking-widest mb-0.5">{t('preferred_location', 'Preferred Location')}</div>
<div className="text-slate-800 flex items-center space-x-1.5 mt-1">
<MapPin className="w-3.5 h-3.5 text-[#185FA5]" />
<span>{previewWorker.preferred_location || 'Not Specified'}</span>
</div>
</div>
<div className="p-3 bg-slate-50/50 rounded-xl col-span-2">
<div className="text-[10px] text-slate-400 uppercase tracking-widest mb-0.5">{t('languages', 'Languages')}</div>
<div className="flex flex-wrap gap-1.5 mt-1">

View File

@ -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 = [] }) {
)}
</div>
{/* Voice Note Attachment */}
<div className="space-y-1.5">
<label className="block text-slate-400 uppercase tracking-widest text-[9px]">{t('voice_note_attachment', 'Voice Note Attachment (Optional)')}</label>
{!data.voice_note ? (
<div className="border-2 border-dashed border-slate-200 rounded-2xl p-6 text-center space-y-2 relative hover:border-[#185FA5] transition-all bg-slate-50/50">
<UploadCloud className="w-8 h-8 text-slate-400 mx-auto" />
<div className="text-xs font-bold text-slate-600">{t('upload_voice_note', 'Upload a voice note or audio file')}</div>
<div className="text-[9px] text-slate-400">MP3, WAV, M4A, WEBM (Max 10MB)</div>
<input
type="file"
accept="audio/*"
onChange={e => setData('voice_note', e.target.files[0])}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
/>
</div>
) : (
<div className="border border-slate-200 rounded-2xl p-4 bg-slate-50/70 space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3 min-w-0">
<div className="w-8 h-8 rounded-lg bg-blue-50 text-[#185FA5] flex items-center justify-center border border-blue-100 flex-shrink-0">
<Volume2 className="w-4 h-4 animate-pulse" />
</div>
<div className="min-w-0">
<p className="text-xs font-extrabold text-slate-800 truncate">{data.voice_note.name}</p>
<p className="text-[10px] text-slate-400 font-bold">{(data.voice_note.size / 1024 / 1024).toFixed(2)} MB</p>
</div>
</div>
<button
type="button"
onClick={() => setData('voice_note', null)}
className="p-1.5 bg-slate-105 bg-slate-100 hover:bg-slate-200 rounded-full text-slate-500 hover:text-slate-700 transition-colors"
>
<X className="w-4 h-4" />
</button>
</div>
{/* Audio preview player */}
<div className="pt-1 border-t border-slate-100">
<audio
src={URL.createObjectURL(data.voice_note)}
controls
className="w-full h-8 outline-none"
/>
</div>
</div>
)}
{errors.voice_note && (
<p className="text-[10px] text-red-500 font-semibold mt-1">{errors.voice_note}</p>
)}
</div>
{/* Support notice */}
{data.priority === 'high' && (
<div className="bg-amber-50 border border-amber-200 rounded-2xl p-4 flex items-start space-x-3 text-amber-800 animate-pulse">

View File

@ -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,12 +137,27 @@ export default function Show({ ticket, replies }) {
</h2>
</div>
<div className="p-6 md:p-8 space-y-1">
<div className="p-6 md:p-8 space-y-4">
<div className="space-y-1">
<label className="block text-slate-400 uppercase tracking-widest text-[9px] font-black mb-2">{t('original_message', 'Original Message')}</label>
<div className="text-xs text-slate-600 font-medium leading-relaxed whitespace-pre-wrap bg-slate-50/50 p-4 rounded-2xl border border-slate-100">
{ticket.description}
</div>
</div>
{ticket.voice_note_url && (
<div className="pt-2">
<label className="block text-slate-400 uppercase tracking-widest text-[9px] font-black mb-2">{t('voice_note_attachment', 'Voice Note Attachment')}</label>
<div className="flex items-center space-x-3 bg-slate-50 border border-slate-200 p-3 rounded-2xl max-w-md">
<audio
src={ticket.voice_note_url}
controls
className="w-full h-8 outline-none"
/>
</div>
</div>
)}
</div>
</div>
{/* Replies header */}
@ -184,11 +203,25 @@ export default function Show({ ticket, replies }) {
</div>
<span className="opacity-60">{reply.created_at}</span>
</div>
{reply.message && (
<div className={`text-xs font-semibold leading-relaxed whitespace-pre-wrap ${
isSystemDev ? 'text-slate-300 font-mono font-semibold' : 'text-slate-600'
}`}>
{reply.message}
</div>
)}
{reply.voice_note_url && (
<div className="mt-2.5">
<div className="flex items-center space-x-3 bg-slate-50 border border-slate-100 p-2.5 rounded-2xl max-w-xs">
<audio
src={reply.voice_note_url}
controls
className="w-full h-8 outline-none"
/>
</div>
</div>
)}
</div>
);
})}
@ -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 && (
<p className="text-[10px] text-red-500 font-semibold mt-1">{errors.message}</p>
)}
</div>
{/* Voice Note Attachment */}
<div className="space-y-1.5">
<label className="block text-slate-400 uppercase tracking-widest text-[9px] font-black">{t('voice_note_reply', 'Voice Note Attachment (Optional)')}</label>
{!data.voice_note ? (
<div className="border-2 border-dashed border-slate-200 rounded-2xl p-4 text-center space-y-1 relative hover:border-[#185FA5] transition-all bg-slate-50/50">
<UploadCloud className="w-6 h-6 text-slate-400 mx-auto" />
<div className="text-[11px] font-bold text-slate-600">{t('upload_voice_note', 'Upload a voice note or audio file')}</div>
<input
type="file"
accept="audio/*"
onChange={e => setData('voice_note', e.target.files[0])}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
/>
</div>
) : (
<div className="border border-slate-200 rounded-2xl p-3 bg-slate-50/70 space-y-2">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2.5 min-w-0">
<Volume2 className="w-4 h-4 text-[#185FA5] flex-shrink-0" />
<div className="min-w-0">
<p className="text-[11px] font-extrabold text-slate-800 truncate">{data.voice_note.name}</p>
</div>
</div>
<button
type="button"
onClick={() => setData('voice_note', null)}
className="p-1 bg-slate-100 hover:bg-slate-200 rounded-full text-slate-500 transition-colors"
>
<X className="w-3.5 h-3.5" />
</button>
</div>
<audio
src={URL.createObjectURL(data.voice_note)}
controls
className="w-full h-8 outline-none"
/>
</div>
)}
{errors.voice_note && (
<p className="text-[10px] text-red-500 font-semibold mt-1">{errors.voice_note}</p>
)}
</div>
<div className="flex justify-end">
<button
type="submit"

View File

@ -47,7 +47,6 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
const { t } = useTranslation();
// Basic Filters
const [searchQuery, setSearchQuery] = useState('');
const [selectedCategory, setSelectedCategory] = useState('All Categories');
const [selectedNationalities, setSelectedNationalities] = useState([]);
const [selectedGender, setSelectedGender] = useState('All Genders');
const [selectedExperience, setSelectedExperience] = useState('All Experience');
@ -83,8 +82,8 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
// Saved Presets
const [savedPresets, setSavedPresets] = useState([
{ id: 1, name: 'Live-in Baby Care', category: 'Childcare', workType: 'Live-in', minSalary: 1200 },
{ id: 2, name: 'Experienced Driver', category: 'Driver', experience: '5+ Years' }
{ id: 1, name: 'Live-in Baby Care', workType: 'Live-in', minSalary: 1200 },
{ id: 2, name: 'Experienced Driver', experience: '5+ Years' }
]);
const [presetNameInput, setPresetNameInput] = useState('');
const [showPresetModal, setShowPresetModal] = useState(false);
@ -112,7 +111,6 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
let hasAdvanced = false;
if (cat) setSelectedCategory(cat);
if (nat) {
setSelectedNationalities(nat.split(',').map(x => x.trim()).filter(Boolean));
}
@ -176,7 +174,6 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
const applyPreset = (preset) => {
resetFilters();
if (preset.category) setSelectedCategory(preset.category);
if (preset.workType) setSelectedWorkTypes([preset.workType]);
if (preset.experience) setSelectedExperience(preset.experience);
};
@ -188,7 +185,6 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
const newPreset = {
id: Date.now(),
name: presetNameInput,
category: selectedCategory !== 'All Categories' ? selectedCategory : null,
workType: selectedWorkTypes.length > 0 ? selectedWorkTypes[0] : null,
experience: selectedExperience !== 'All Experience' ? selectedExperience : null,
};
@ -200,7 +196,6 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
const resetFilters = () => {
setSearchQuery('');
setSelectedCategory('All Categories');
setSelectedNationalities([]);
setSelectedGender('All Genders');
setSelectedExperience('All Experience');
@ -262,7 +257,6 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
}
// Dropdown filters
if (selectedCategory !== 'All Categories' && worker.category !== selectedCategory) return false;
if (selectedNationalities.length > 0 && (!worker.nationality || !selectedNationalities.map(n => n.toLowerCase()).includes(worker.nationality.toLowerCase()))) return false;
if (selectedGender !== 'All Genders' && worker.gender && worker.gender.toLowerCase() !== selectedGender.toLowerCase()) return false;
if (selectedExperience !== 'All Experience' && worker.experience !== selectedExperience) return false;
@ -338,7 +332,6 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
}, [
initialWorkers,
searchQuery,
selectedCategory,
selectedNationalities,
selectedGender,
selectedExperience,
@ -705,6 +698,7 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
<div className="flex items-center space-x-1 text-[9px] font-black uppercase text-blue-700 bg-blue-50 px-1.5 py-0.5 rounded border border-blue-100">
<span>{worker.visa_status}</span>
</div>
)}
{worker.document_expiry_status && (
<div className={`flex items-center space-x-1 text-[9px] font-black uppercase px-1.5 py-0.5 rounded border ${
worker.document_expiry_days !== null && worker.document_expiry_days <= 30
@ -747,11 +741,8 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
<div className="p-6 space-y-4 flex-1 flex flex-col justify-between">
<div className="space-y-4">
{/* Category Pill, Ratings & Cost */}
<div className="flex items-center justify-between text-xs">
<span className="px-2.5 py-1 bg-blue-50 text-[#185FA5] font-black rounded-lg border border-blue-100 uppercase tracking-wider text-[9px]">
{worker.category}
</span>
{/* Ratings & Cost */}
<div className="flex items-center justify-end text-xs">
<div className="flex items-center space-x-1 font-bold text-slate-800">
<DollarSign className="w-3.5 h-3.5 text-emerald-600" />
<span className="text-sm font-black">{worker.salary} {t('aed', 'AED')}</span>
@ -770,9 +761,13 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
<Sparkles className="w-3.5 h-3.5 text-slate-400 flex-shrink-0" />
<span className="truncate uppercase text-[10px]">{worker.preferred_job_type}</span>
</div>
<div className="flex items-center space-x-1.5 truncate col-span-2">
<div className="flex items-center space-x-1.5 truncate">
<MapPin className="w-3.5 h-3.5 text-[#185FA5] flex-shrink-0" />
<span className="truncate text-[#185FA5]">{worker.preferred_location || 'Not Specified'}</span>
</div>
<div className="flex items-center space-x-1.5 truncate">
<Star className="w-3.5 h-3.5 text-amber-500 flex-shrink-0 fill-amber-500" />
<span className="truncate">{worker.rating} ({worker.reviews_count} {t('reviews', 'reviews')})</span>
<span className="truncate">{worker.rating} ({worker.reviews_count})</span>
</div>
</div>
@ -908,7 +903,7 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
</div>
<div>
<div className="font-bold text-xs text-slate-900 truncate max-w-[120px]">{w.name}</div>
<div className="text-[10px] text-slate-500 font-bold">{w.nationality} {w.category}</div>
<div className="text-[10px] text-slate-500 font-bold">{w.nationality}</div>
</div>
</div>
@ -916,7 +911,7 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
<div>{t('salary', 'Salary')}: <span className="text-slate-800">{w.salary} {t('aed', 'AED')}</span></div>
<div>{t('age', 'Age')}: <span className="text-slate-800">{w.age} {t('yrs', 'yrs')}</span></div>
<div>{t('job_type', 'Job Type')}: <span className="text-slate-800 uppercase">{w.preferred_job_type}</span></div>
<div>{t('status', 'Status')}: <span className="text-slate-800 font-black">{w.availability_status}</span></div>
<div className="truncate">{t('location', 'Location')}: <span className="text-slate-800 font-medium" title={w.preferred_location}>{w.preferred_location || 'Not Specified'}</span></div>
</div>
<div className="space-y-1">
@ -972,23 +967,34 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
}`} />
<span>{previewWorker.passport_status}</span>
</div>
<div className="text-xs text-slate-500 font-bold">{previewWorker.nationality} {previewWorker.category}</div>
<div className="text-xs text-slate-500 font-bold">{previewWorker.nationality}</div>
</div>
</div>
<p className="text-xs text-slate-600 italic bg-slate-50 p-4 rounded-xl leading-relaxed">
"{previewWorker.bio}"
</p>
<div className="grid grid-cols-2 gap-4 text-xs font-bold text-slate-600">
<div className="p-3 bg-slate-50/50 rounded-xl">
<div className="text-[10px] text-slate-400 uppercase tracking-widest mb-0.5">{t('expectations', 'Expectations')}</div>
<div className="text-slate-800">{previewWorker.salary} {t('aed', 'AED')} / {t('mo', 'mo')}</div>
</div>
<div className="p-3 bg-slate-50/50 rounded-xl">
<div className="text-[10px] text-slate-400 uppercase tracking-widest mb-0.5">{t('job_preference', 'Job Preference')}</div>
<div className="text-[10px] text-slate-400 uppercase tracking-widest mb-0.5">{t('job_type', 'Job Type')}</div>
<div className="text-slate-800 uppercase text-[10px] mt-1">{previewWorker.preferred_job_type}</div>
</div>
<div className="p-3 bg-slate-50/50 rounded-xl">
<div className="text-[10px] text-slate-400 uppercase tracking-widest mb-0.5">{t('gender', 'Gender')}</div>
<div className="text-slate-800 capitalize mt-1">{previewWorker.gender || 'Female'}</div>
</div>
<div className="p-3 bg-slate-50/50 rounded-xl">
<div className="text-[10px] text-slate-400 uppercase tracking-widest mb-0.5">{t('visa_status', 'Visa Status')}</div>
<div className="text-slate-800 capitalize mt-1">{previewWorker.visa_status || 'Residence Visa'}</div>
</div>
<div className="p-3 bg-slate-50/50 rounded-xl col-span-2">
<div className="text-[10px] text-slate-400 uppercase tracking-widest mb-0.5">{t('preferred_location', 'Preferred Location')}</div>
<div className="text-slate-800 flex items-center space-x-1.5 mt-1">
<MapPin className="w-3.5 h-3.5 text-[#185FA5]" />
<span>{previewWorker.preferred_location || 'Not Specified'}</span>
</div>
</div>
<div className="p-3 bg-slate-50/50 rounded-xl col-span-2">
<div className="text-[10px] text-slate-400 uppercase tracking-widest mb-0.5">{t('languages_spoken', 'Languages')}</div>
<div className="flex flex-wrap gap-1.5 mt-1">

View File

@ -131,7 +131,7 @@ export default function Show({ worker }) {
return (
<EmployerLayout title={t('candidate_profile_detail', 'Candidate Profile Detail')}>
<Head title={`${worker.name} - ${worker.category} Profile`} />
<Head title={`${worker.name} Profile`} />
<div className="space-y-6 select-none w-full pb-16">
@ -229,8 +229,6 @@ export default function Show({ worker }) {
</span>
<span></span>
<span>{worker.age} {t('years_old', 'Years Old')}</span>
<span></span>
<span className="bg-blue-50 text-[#185FA5] border border-blue-100 px-2 py-0.5 rounded text-[10px] uppercase font-black tracking-widest">{worker.category}</span>
</div>
{workerReviews.length > 0 && (
<div className="flex items-center space-x-1 text-amber-500 font-bold text-xs">

View File

@ -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

View File

@ -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',
],

View File

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

View File

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

View File

@ -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',

View File

@ -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',

View File

@ -0,0 +1,200 @@
<?php
namespace Tests\Feature;
use App\Models\Sponsor;
use App\Models\User;
use App\Models\Worker;
use App\Models\EmployerProfile;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
class FcmAuthenticationTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
// Write a temporary fake credential file so FCMService doesn't fail on file check
$credentialsPath = base_path('migrant-fe5a7-firebase-adminsdk-fbsvc-f8b365ecd0.json');
if (!file_exists($credentialsPath)) {
file_put_contents($credentialsPath, json_encode([
'private_key' => "-----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);
}
}

View File

@ -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,

View File

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

View File

@ -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',

View File

@ -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

View File

@ -24,10 +24,6 @@ protected function setUp(): void
$this->token = 'worker-test-token-789';
$category = \App\Models\WorkerCategory::create([
'name' => 'Housemaid',
]);
$this->worker = Worker::create([
'name' => 'John Worker',
'email' => 'worker@example.com',
@ -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,