diff --git a/app/Console/Commands/ClearDatabaseData.php b/app/Console/Commands/ClearDatabaseData.php new file mode 100644 index 0000000..2943206 --- /dev/null +++ b/app/Console/Commands/ClearDatabaseData.php @@ -0,0 +1,53 @@ +info('Clearing database tables...'); + + Schema::disableForeignKeyConstraints(); + + // Truncate recruitment, dynamic, and worker tables + DB::table('shortlists')->truncate(); + DB::table('job_applications')->truncate(); + DB::table('job_posts')->truncate(); + DB::table('messages')->truncate(); + DB::table('conversations')->truncate(); + DB::table('announcements')->truncate(); + DB::table('worker_documents')->truncate(); + DB::table('worker_skills')->truncate(); + DB::table('workers')->truncate(); + + // Also delete non-seeded users if any (keep Admin and Employer) + DB::table('users')->whereNotIn('email', ['admin@example.com', 'employer1@example.com'])->delete(); + + Schema::enableForeignKeyConstraints(); + + $this->info('Database cleared successfully! You can now register, post jobs, and apply manually.'); + return Command::SUCCESS; + } +} diff --git a/app/Http/Controllers/Employer/AnnouncementController.php b/app/Http/Controllers/Employer/AnnouncementController.php new file mode 100644 index 0000000..60eb1bc --- /dev/null +++ b/app/Http/Controllers/Employer/AnnouncementController.php @@ -0,0 +1,47 @@ +get(); + + $announcements = $dbAnnouncements->map(function ($ann) { + return [ + 'id' => $ann->id, + 'title' => $ann->title, + 'content' => $ann->body, + 'audience' => in_array($ann->type, ['Shortlisted', 'Selected Candidates']) ? $ann->type : 'Selected Candidates', + 'created_at' => $ann->created_at->diffForHumans(), + ]; + })->toArray(); + + return Inertia::render('Employer/Announcements', [ + 'initialAnnouncements' => $announcements, + ]); + } + + public function store(Request $request) + { + $request->validate([ + 'title' => 'required|string|max:255', + 'content' => 'required|string', + 'audience' => 'required|string|in:Shortlisted,Selected Candidates', + ]); + + Announcement::create([ + 'title' => $request->title, + 'body' => $request->content, + 'type' => $request->audience, + ]); + + return back()->with('success', 'Announcement posted successfully.'); + } +} diff --git a/app/Http/Controllers/Employer/CandidateController.php b/app/Http/Controllers/Employer/CandidateController.php new file mode 100644 index 0000000..a0d3627 --- /dev/null +++ b/app/Http/Controllers/Employer/CandidateController.php @@ -0,0 +1,101 @@ +id ?? null); + + if (!$sessId) { + $user = User::where('role', 'employer')->first(); + if ($user) { + session(['user' => (object)[ + 'id' => $user->id, + 'name' => $user->name, + 'email' => $user->email, + 'role' => 'employer', + 'subscription_status' => $user->subscription_status ?? 'active', + ]]); + return $user; + } + } else { + return User::find($sessId); + } + + return null; + } + + public function index(Request $request) + { + $user = $this->resolveCurrentUser(); + $employerId = $user ? $user->id : 2; + + // Get job posts created by this employer + $jobIds = JobPost::where('employer_id', $employerId)->pluck('id'); + + // Fetch applications for those jobs + $applications = JobApplication::whereIn('job_id', $jobIds) + ->with(['worker.category', 'jobPost']) + ->get(); + + $selectedWorkers = $applications->map(function ($app) { + $w = $app->worker; + if (!$w) return null; + + // Map DB status to UI friendly status + $status = 'Reviewing'; + if ($app->status === 'hired') $status = 'Hired'; + elseif ($app->status === 'rejected') $status = 'Rejected'; + elseif ($app->status === 'shortlisted' || $app->status === 'offer_sent') $status = 'Offer Sent'; + elseif ($app->status === 'applied') $status = 'Reviewing'; + else $status = ucfirst($app->status); + + return [ + 'id' => $app->id, // application id + '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'), + ]; + })->filter()->values()->toArray(); + + return Inertia::render('Employer/SelectedCandidates', [ + 'selectedWorkers' => $selectedWorkers, + ]); + } + + public function updateStatus(Request $request, $id) + { + $request->validate([ + 'status' => 'required|string|in:Reviewing,Offer Sent,Hired,Rejected', + ]); + + $app = JobApplication::findOrFail($id); + + // Map UI status back to DB status + $dbStatus = 'applied'; + if ($request->status === 'Hired') $dbStatus = 'hired'; + elseif ($request->status === 'Rejected') $dbStatus = 'rejected'; + elseif ($request->status === 'Offer Sent') $dbStatus = 'shortlisted'; + elseif ($request->status === 'Reviewing') $dbStatus = 'applied'; + + $app->update([ + 'status' => $dbStatus, + ]); + + return back()->with('success', 'Candidate status updated successfully to ' . $request->status); + } +} diff --git a/app/Http/Controllers/Employer/DashboardController.php b/app/Http/Controllers/Employer/DashboardController.php index a5b12ea..a1225be 100644 --- a/app/Http/Controllers/Employer/DashboardController.php +++ b/app/Http/Controllers/Employer/DashboardController.php @@ -5,106 +5,126 @@ use App\Http\Controllers\Controller; use Illuminate\Http\Request; use Inertia\Inertia; +use App\Models\User; +use App\Models\Shortlist; +use App\Models\Conversation; +use App\Models\Message; +use App\Models\Announcement; +use Carbon\Carbon; class DashboardController extends Controller { public function index(Request $request) { + // Resolve current employer user $sess = session('user'); - $user = is_array($sess) ? (object)$sess : ($sess ?? (object)[ - 'name' => 'John Doe', - 'subscription_status' => 'active', - 'subscription_expires_at' => now()->addDays(24)->format('Y-m-d'), - ]); + $sessId = is_array($sess) ? ($sess['id'] ?? null) : ($sess->id ?? null); + + if (!$sessId) { + $user = User::where('role', 'employer')->first(); + if ($user) { + session(['user' => (object)[ + 'id' => $user->id, + 'name' => $user->name, + 'email' => $user->email, + 'role' => 'employer', + 'subscription_status' => $user->subscription_status ?? 'active', + ]]); + } + } else { + $user = User::find($sessId); + } + + // Fallback user if DB has no users (precaution) + if (!$user) { + $user = new User([ + 'id' => 2, + 'name' => 'John Doe', + 'subscription_status' => 'active', + 'subscription_expires_at' => now()->addDays(24), + ]); + } + + // Get subscription expires at date + $sub = $user->subscription()->where('status', 'active')->latest()->first(); + $expiresAt = $sub ? $sub->expires_at : now()->addDays(24); + $daysRemaining = $expiresAt ? max(0, now()->diffInDays($expiresAt, false)) : 30; + + // 1. Stats + $shortlistedCount = Shortlist::where('employer_id', $user->id)->count(); + $messagesSent = Message::where('sender_id', $user->id)->where('sender_type', 'employer')->count(); + + $stats = [ + 'shortlisted_count' => $shortlistedCount, + 'messages_sent' => $messagesSent, + 'profile_views_given' => 45, // Static/mock analytic metric for page visits + 'days_remaining' => (int)$daysRemaining, + ]; + + // 2. Shortlisted workers + $shortlists = Shortlist::where('employer_id', $user->id) + ->with(['worker.category', 'worker.skills']) + ->get(); + + $shortlistedWorkers = $shortlists->map(function ($s) { + $w = $s->worker; + if (!$w) return null; + return [ + 'id' => $w->id, + 'name' => $w->name, + 'nationality' => $w->nationality, + 'category' => $w->category ? $w->category->name : 'General Helper', + 'skills' => $w->skills->pluck('name')->toArray(), + 'availability' => $w->availability, + 'photo_url' => null, + 'verified' => (bool)$w->verified, + ]; + })->filter()->values()->toArray(); + + // 3. Recent messages + $dbConversations = Conversation::where('employer_id', $user->id) + ->with(['worker', 'messages' => function ($q) { + $q->latest(); + }]) + ->get(); + + $recentMessages = $dbConversations->map(function ($conv) { + $lastMsg = $conv->messages->first(); + $worker = $conv->worker; + if (!$worker || !$lastMsg) return null; + + return [ + 'id' => $conv->id, + 'worker_name' => $worker->name, + 'last_message' => $lastMsg->text, + 'unread' => $lastMsg->sender_type === 'worker' && is_null($lastMsg->read_at), + 'sent_at' => $lastMsg->created_at->diffForHumans(), + 'timestamp' => $lastMsg->created_at, + ]; + })->filter()->sortByDesc('timestamp')->values()->toArray(); + + // 4. Announcements + $dbAnnouncements = Announcement::latest()->limit(5)->get(); + $announcements = $dbAnnouncements->map(function ($ann) { + return [ + 'id' => $ann->id, + 'title' => $ann->title, + 'body' => $ann->body, + 'created_at' => $ann->created_at->format('M d, Y'), + ]; + })->toArray(); return Inertia::render('Employer/Dashboard', [ 'employer' => [ - 'name' => $user->name ?? 'John Doe', - 'subscription_status' => $user->subscription_status ?? 'active', - 'subscription_expires_at' => $user->subscription_expires_at ?? '2026-12-31', - 'plan_name' => 'Premium Employer Pass', - ], - 'stats' => [ - 'shortlisted_count' => 4, - 'messages_sent' => 18, - 'profile_views_given' => 45, - 'days_remaining' => 24, - ], - 'shortlisted_workers' => [ - [ - 'id' => 101, - 'name' => 'Maria Santos', - 'nationality' => 'Philippines', - 'skills' => ['Childcare', 'Cooking', 'Housekeeping'], - 'availability' => 'Immediate', - 'photo_url' => null, - 'verified' => true, - ], - [ - 'id' => 102, - 'name' => 'Lakshmi Sharma', - 'nationality' => 'India', - 'skills' => ['Elderly Care', 'Cooking'], - 'availability' => '2 Weeks', - 'photo_url' => null, - 'verified' => true, - ], - [ - 'id' => 103, - 'name' => 'Siti Aminah', - 'nationality' => 'Indonesia', - 'skills' => ['Housekeeping', 'Ironing'], - 'availability' => 'Immediate', - 'photo_url' => null, - 'verified' => true, - ], - [ - 'id' => 104, - 'name' => 'Grace Osei', - 'nationality' => 'Ghana', - 'skills' => ['Childcare', 'English Tutoring'], - 'availability' => '1 Month', - 'photo_url' => null, - 'verified' => false, - ], - ], - 'recent_messages' => [ - [ - 'id' => 201, - 'worker_name' => 'Maria Santos', - 'last_message' => 'Yes ma\'am, I am available for a video interview tomorrow at 10 AM.', - 'unread' => true, - 'sent_at' => '10 mins ago', - ], - [ - 'id' => 202, - 'worker_name' => 'Lakshmi Sharma', - 'last_message' => 'I have 5 years of experience in Dubai taking care of elderly patients.', - 'unread' => true, - 'sent_at' => '2 hours ago', - ], - [ - 'id' => 203, - 'worker_name' => 'Siti Aminah', - 'last_message' => 'Thank ma\'am for shortlisting my profile. Please let me know your offer.', - 'unread' => false, - 'sent_at' => '1 day ago', - ], - ], - 'announcements' => [ - [ - 'id' => 301, - 'title' => 'New Visa Regulations Update', - 'body' => 'The Ministry of Human Resources and Emiratisation has announced updated guidelines for domestic worker sponsorship starting this month.', - 'created_at' => 'May 10, 2026', - ], - [ - 'id' => 302, - 'title' => 'Enhanced OCR Verification Active', - 'body' => 'We have deployed upgraded OCR passport verification to ensure 100% legal compliance for all worker profiles on the platform.', - 'created_at' => 'May 1, 2026', - ], + 'name' => $user->name, + 'subscription_status' => $sub ? $sub->status : 'active', + 'subscription_expires_at' => $expiresAt ? $expiresAt->format('Y-m-d') : '2026-12-31', + 'plan_name' => $sub ? (ucfirst($sub->plan_id) . ' Pass') : 'Premium Employer Pass', ], + 'stats' => $stats, + 'shortlisted_workers' => $shortlistedWorkers, + 'recent_messages' => array_slice($recentMessages, 0, 5), + 'announcements' => $announcements, ]); } } diff --git a/app/Http/Controllers/Employer/EmployerAuthController.php b/app/Http/Controllers/Employer/EmployerAuthController.php index 53870d1..005ebe3 100644 --- a/app/Http/Controllers/Employer/EmployerAuthController.php +++ b/app/Http/Controllers/Employer/EmployerAuthController.php @@ -5,6 +5,11 @@ use App\Http\Controllers\Controller; use Illuminate\Http\Request; use Inertia\Inertia; +use App\Models\User; +use App\Models\EmployerProfile; +use App\Mail\EmployerOtpMail; +use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Facades\Mail; class EmployerAuthController extends Controller { @@ -35,6 +40,7 @@ public function login(Request $request) return back()->with('status', 'subscription_expired'); } + // Attempt logging in via standard Auth or fallback credentials if ($credentials['email'] === 'employer@example.com' && $credentials['password'] === 'password') { session(['user' => (object)[ 'id' => 2, @@ -54,6 +60,29 @@ public function login(Request $request) return redirect()->intended('/employer/dashboard'); } + // Attempt actual database login + $user = User::where('email', $credentials['email'])->first(); + if ($user && Hash::check($credentials['password'], $user->password) && $user->role === 'employer') { + $profile = EmployerProfile::where('user_id', $user->id)->first(); + + session(['user' => (object)[ + 'id' => $user->id, + 'name' => $user->name, + 'email' => $user->email, + 'role' => $user->role, + 'subscription_status' => $user->subscription_status ?? 'active', + 'verification_status' => $profile ? $profile->verification_status : 'approved', + ]]); + + $request->session()->regenerate(); + + if ($request->source === 'mobile') { + return redirect('/mobile/employer/home'); + } + + return redirect()->intended('/employer/dashboard'); + } + return back()->withErrors([ 'email' => 'Invalid credentials. Use employer@example.com (or test emails: pending@, rejected@, expired@ / password)', ]); @@ -66,52 +95,270 @@ public function showRegister() public function register(Request $request) { - $validated = $request->validate([ + // 1. Validation + $request->validate([ + 'company_name' => 'required|string|max:255', 'name' => 'required|string|max:255', 'email' => 'required|string|email|max:255', - 'phone' => 'required|string|max:25', - 'password' => 'required|string|min:8', - 'emirates_id_front' => 'required|file|mimes:jpg,png,pdf|max:5120', - 'emirates_id_back' => 'required|file|mimes:jpg,png,pdf|max:5120', + 'phone' => 'required|string|regex:/^\+?[0-9\s\-()]{7,20}$/', + 'country' => 'required|string|max:100', + ], [ + 'phone.regex' => 'The phone number must be a valid mobile number format.', ]); - $frontPath = null; - $backPath = null; - - if ($request->hasFile('emirates_id_front')) { - $frontPath = $request->file('emirates_id_front')->store('private/emirates-id', 'local'); + // 2. Email uniqueness check (Check DB, return 409 if duplicate) + if (User::where('email', $request->email)->exists()) { + return response()->json([ + 'errors' => [ + 'email' => 'This email address is already registered. Please login or use a different email.' + ] + ], 409); } - if ($request->hasFile('emirates_id_back')) { - $backPath = $request->file('emirates_id_back')->store('private/emirates-id', 'local'); + // 3. Generate 6-digit OTP, hash & store with 10-min expiry + $otp = (string) mt_rand(100000, 999999); + $hashedOtp = Hash::make($otp); + + session([ + 'pending_employer_registration' => [ + 'company_name' => $request->company_name, + 'name' => $request->name, + 'email' => $request->email, + 'phone' => $request->phone, + 'country' => $request->country, + ], + 'employer_otp' => [ + 'hash' => $hashedOtp, + 'expires_at' => now()->addMinutes(10), + 'attempts' => 0, + 'last_sent_at' => now(), + ] + ]); + + // 4. Send professional OTP email + try { + Mail::to($request->email)->send(new EmployerOtpMail( + $otp, + $request->company_name, + $request->name + )); + } catch (\Exception $e) { + // Log error but proceed for demonstration/local testing if mail server is not fully configured + logger()->error('Failed to send registration OTP email: ' . $e->getMessage()); } - // Auto-approve registration since proof is provided + return redirect()->route('employer.verify-email') + ->with('success', 'Verification code sent to your email.'); + } + + public function showVerifyEmail() + { + if (!session()->has('pending_employer_registration')) { + return redirect()->route('employer.register') + ->with('error', 'Please submit the registration form first.'); + } + + return Inertia::render('Employer/Auth/VerifyEmail', [ + 'email' => session('pending_employer_registration.email'), + ]); + } + + public function verifyEmail(Request $request) + { + $request->validate([ + 'otp' => 'required|string|size:6', + ]); + + if (!session()->has('pending_employer_registration') || !session()->has('employer_otp')) { + return redirect()->route('employer.register') + ->with('error', 'Registration session expired. Please start over.'); + } + + $otpSession = session('employer_otp'); + + // Check attempts (max 3) + if ($otpSession['attempts'] >= 3) { + session()->forget(['pending_employer_registration', 'employer_otp']); + return redirect()->route('employer.register') + ->with('error', 'Too many failed verification attempts. Please register again.'); + } + + // Check expiry (10 mins) + if (now()->greaterThan($otpSession['expires_at'])) { + return back()->withErrors([ + 'otp' => 'The verification code has expired. Please request a new one.' + ]); + } + + // Hash comparison (allow 000000 in local environment for automated testing) + $isLocalDebug = app()->environment('local') && $request->otp === '000000'; + if (!$isLocalDebug && !Hash::check($request->otp, $otpSession['hash'])) { + $otpSession['attempts']++; + session(['employer_otp' => $otpSession]); + + $attemptsLeft = 3 - $otpSession['attempts']; + if ($attemptsLeft <= 0) { + session()->forget(['pending_employer_registration', 'employer_otp']); + return redirect()->route('employer.register') + ->with('error', 'Too many failed verification attempts. Please register again.'); + } + + return back()->withErrors([ + 'otp' => "Invalid verification code. You have {$attemptsLeft} attempts remaining." + ]); + } + + // Success - mark verified + session(['employer_email_verified' => true]); + + return redirect()->route('employer.create-password') + ->with('success', 'Email verified successfully. Please set your password.'); + } + + public function resendOtp(Request $request) + { + if (!session()->has('pending_employer_registration')) { + return response()->json([ + 'message' => 'No pending registration session found.' + ], 400); + } + + $pending = session('pending_employer_registration'); + $otpSession = session('employer_otp'); + + // Cooldown timer check (60s) + if ($otpSession && now()->diffInSeconds($otpSession['last_sent_at']) < 60) { + $secondsRemaining = 60 - now()->diffInSeconds($otpSession['last_sent_at']); + return response()->json([ + 'message' => "Please wait {$secondsRemaining} seconds before requesting a new code." + ], 429); + } + + $otp = (string) mt_rand(100000, 999999); + $hashedOtp = Hash::make($otp); + + session([ + 'employer_otp' => [ + 'hash' => $hashedOtp, + 'expires_at' => now()->addMinutes(10), + 'attempts' => 0, + 'last_sent_at' => now(), + ] + ]); + + try { + Mail::to($pending['email'])->send(new EmployerOtpMail( + $otp, + $pending['company_name'], + $pending['name'] + )); + } catch (\Exception $e) { + logger()->error('Failed to resend registration OTP email: ' . $e->getMessage()); + } + + return response()->json([ + 'message' => 'A fresh verification code has been sent to your email.' + ]); + } + + public function showCreatePassword() + { + if (!session()->has('pending_employer_registration') || !session('employer_email_verified')) { + return redirect()->route('employer.register') + ->with('error', 'Please complete email verification first.'); + } + + return Inertia::render('Employer/Auth/CreatePassword'); + } + + public function createPassword(Request $request) + { + $request->validate([ + 'password' => [ + 'required', + 'string', + 'min:8', + 'confirmed', + 'regex:/[a-z]/', // 1 lowercase + 'regex:/[A-Z]/', // 1 uppercase + 'regex:/[0-9]/', // 1 number + 'regex:/[^a-zA-Z0-9]/', // 1 special character + ] + ], [ + 'password.regex' => 'The password must contain at least 8 characters, including 1 uppercase, 1 lowercase, 1 number, and 1 special character.', + ]); + + if (!session()->has('pending_employer_registration') || !session('employer_email_verified')) { + return redirect()->route('employer.register') + ->with('error', 'Registration session expired. Please start over.'); + } + + $pending = session('pending_employer_registration'); + + // Create user + $user = User::create([ + 'name' => $pending['name'], + 'email' => $pending['email'], + 'password' => Hash::make($request->password), + 'role' => 'employer', + 'subscription_status' => 'active', // Auto-active for direct entry + 'subscription_expires_at' => now()->addDays(30), + ]); + + // Create profile + EmployerProfile::create([ + 'user_id' => $user->id, + 'company_name' => $pending['company_name'], + 'phone' => $pending['phone'], + 'country' => $pending['country'], + 'verification_status' => 'approved', + 'language' => 'English', + 'notifications' => true, + ]); + + // Create a default active premium subscription record + \Illuminate\Support\Facades\DB::table('subscriptions')->insert([ + 'user_id' => $user->id, + 'plan_id' => 'premium', + 'amount_aed' => 499.00, + 'starts_at' => now(), + 'expires_at' => now()->addDays(30), + 'paytabs_transaction_id' => 'TXN-' . strtoupper(bin2hex(random_bytes(6))), + 'status' => 'active', + 'created_at' => now(), + 'updated_at' => now(), + ]); + + // Auto-login (Laravel + Session) + auth()->login($user); + session(['user' => (object)[ - 'id' => 3, - 'name' => $validated['name'], - 'email' => $validated['email'], - 'phone' => $validated['phone'], + 'id' => $user->id, + 'name' => $user->name, + 'email' => $user->email, 'role' => 'employer', 'subscription_status' => 'active', 'verification_status' => 'approved', ]]); - if ($request->source === 'mobile') { - return redirect('/mobile/employer/home'); - } + // Flash first_login toast flag + session()->flash('first_login', true); - return redirect()->route('employer.dashboard')->with('success', 'Registration successful! Your account is verified and ready.'); - } + // Clear registration session keys + session()->forget([ + 'pending_employer_registration', + 'employer_otp', + 'employer_email_verified' + ]); - public function verifyEmail($token) - { - return redirect()->route('employer.dashboard')->with('success', 'Email verified successfully.'); + return redirect()->route('employer.dashboard') + ->with('success', 'Welcome aboard! Your registration is complete.'); } public function logout(Request $request) { session()->forget('user'); + auth()->logout(); $request->session()->invalidate(); $request->session()->regenerateToken(); diff --git a/app/Http/Controllers/Employer/JobController.php b/app/Http/Controllers/Employer/JobController.php new file mode 100644 index 0000000..799f0e9 --- /dev/null +++ b/app/Http/Controllers/Employer/JobController.php @@ -0,0 +1,160 @@ +id ?? null); + + if (!$sessId) { + $user = User::where('role', 'employer')->first(); + if ($user) { + session(['user' => (object)[ + 'id' => $user->id, + 'name' => $user->name, + 'email' => $user->email, + 'role' => 'employer', + 'subscription_status' => $user->subscription_status ?? 'active', + ]]); + return $user; + } + } else { + return User::find($sessId); + } + + return null; + } + + public function index(Request $request) + { + $user = $this->resolveCurrentUser(); + if (!$user) { + return redirect()->route('employer.login'); + } + + $dbJobs = JobPost::where('employer_id', $user->id) + ->with(['category', 'applications']) + ->latest() + ->get(); + + $jobs = $dbJobs->map(function ($job) { + return [ + 'id' => $job->id, + 'title' => $job->title, + 'category' => $job->category->name ?? 'General', + 'location' => $job->location, + 'salary' => (int) $job->salary, + 'workers_needed' => $job->workers_needed, + 'applied_count' => $job->applications->count(), + 'posted_at' => $job->created_at->format('M d, Y'), + 'status' => ucfirst($job->status), // Active, Closed, Draft + ]; + })->toArray(); + + return Inertia::render('Employer/Jobs/Index', [ + 'initialJobs' => $jobs, + ]); + } + + 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, + ]); + } + + public function store(Request $request) + { + $user = $this->resolveCurrentUser(); + if (!$user) { + return back()->withErrors(['general' => 'User session not found.']); + } + + $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', + 'job_type' => 'required|string|in:Full Time,Part Time,Contract', + 'start_date' => 'required|date', + 'description' => 'required|string|max:1000', + '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, + 'salary' => $request->salary, + 'start_date' => $request->start_date, + 'description' => $request->description, + 'requirements' => $request->requirements, + 'status' => 'active', + ]); + + return redirect()->route('employer.jobs')->with('success', 'Job posted successfully.'); + } + + public function applicants($id) + { + $user = $this->resolveCurrentUser(); + if (!$user) { + return redirect()->route('employer.login'); + } + + $job = JobPost::where('employer_id', $user->id)->where('id', $id)->firstOrFail(); + + $dbApplications = JobApplication::where('job_id', $id) + ->with(['worker.category', 'worker.skills']) + ->get(); + + $applicants = $dbApplications->map(function ($app) { + $worker = $app->worker; + return [ + '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 + 'match_score' => $worker->match_score ?? 90, + ]; + })->toArray(); + + return Inertia::render('Employer/Jobs/Applicants', [ + 'job' => [ + 'id' => $job->id, + 'title' => $job->title, + 'category' => $job->category->name ?? 'General', + 'salary' => (int) $job->salary, + ], + 'applicants' => $applicants, + ]); + } +} diff --git a/app/Http/Controllers/Employer/MessageController.php b/app/Http/Controllers/Employer/MessageController.php new file mode 100644 index 0000000..430b3cc --- /dev/null +++ b/app/Http/Controllers/Employer/MessageController.php @@ -0,0 +1,155 @@ +id ?? null); + + if (!$sessId) { + $user = User::where('role', 'employer')->first(); + if ($user) { + session(['user' => (object)[ + 'id' => $user->id, + 'name' => $user->name, + 'email' => $user->email, + 'role' => 'employer', + 'subscription_status' => $user->subscription_status ?? 'active', + ]]); + return $user; + } + } else { + return User::find($sessId); + } + + return null; + } + + public function index(Request $request) + { + $user = $this->resolveCurrentUser(); + if (!$user) { + return redirect()->route('employer.login'); + } + + $dbConversations = Conversation::where('employer_id', $user->id) + ->with(['worker.category', 'messages']) + ->latest('updated_at') + ->get(); + + $conversations = $dbConversations->map(function ($conv) { + $lastMsg = $conv->messages->last(); + return [ + 'id' => $conv->id, + 'worker_name' => $conv->worker->name ?? 'Candidate', + 'category' => $conv->worker->category->name ?? 'General Helper', + 'last_message' => $lastMsg->text ?? 'No messages yet.', + 'unread' => $lastMsg ? ($lastMsg->sender_type === 'worker' && is_null($lastMsg->read_at)) : false, + 'online' => true, + 'sent_at' => $lastMsg ? $lastMsg->created_at->diffForHumans() : 'Just now', + ]; + })->toArray(); + + return Inertia::render('Employer/Messages/Index', [ + 'conversations' => $conversations, + ]); + } + + public function show($id) + { + $user = $this->resolveCurrentUser(); + if (!$user) { + return redirect()->route('employer.login'); + } + + $dbConversations = Conversation::where('employer_id', $user->id) + ->with(['worker.category', 'messages']) + ->latest('updated_at') + ->get(); + + $conversations = $dbConversations->map(function ($conv) { + $lastMsg = $conv->messages->last(); + return [ + 'id' => $conv->id, + 'worker_name' => $conv->worker->name ?? 'Candidate', + 'category' => $conv->worker->category->name ?? 'General Helper', + 'last_message' => $lastMsg->text ?? 'No messages yet.', + 'unread' => $lastMsg ? ($lastMsg->sender_type === 'worker' && is_null($lastMsg->read_at)) : false, + 'online' => true, + 'sent_at' => $lastMsg ? $lastMsg->created_at->diffForHumans() : 'Just now', + ]; + })->toArray(); + + $activeConv = Conversation::where('employer_id', $user->id) + ->where('id', $id) + ->with(['worker.category', 'messages']) + ->firstOrFail(); + + // Mark incoming messages as read + Message::where('conversation_id', $activeConv->id) + ->where('sender_type', 'worker') + ->whereNull('read_at') + ->update(['read_at' => now()]); + + $conversationData = [ + 'id' => $activeConv->id, + 'worker_name' => $activeConv->worker->name ?? 'Candidate', + 'category' => $activeConv->worker->category->name ?? 'General Helper', + 'online' => true, + 'salary' => ($activeConv->worker->expected_salary ?? 2000) . ' AED', + 'nationality' => $activeConv->worker->nationality ?? 'Unknown', + ]; + + $initialMessages = $activeConv->messages->map(function ($msg) { + return [ + 'id' => $msg->id, + 'sender' => $msg->sender_type, // employer or worker + 'text' => $msg->text, + 'time' => $msg->created_at->format('g:i A') . ($msg->created_at->isToday() ? ' Today' : ' ' . $msg->created_at->format('M d')), + ]; + })->toArray(); + + return Inertia::render('Employer/Messages/Show', [ + 'conversations' => $conversations, + 'conversation' => $conversationData, + 'initialMessages' => $initialMessages, + ]); + } + + public function send(Request $request, $id) + { + $user = $this->resolveCurrentUser(); + if (!$user) { + return back()->withErrors(['general' => 'User session not found.']); + } + + $request->validate([ + 'text' => 'required|string|max:1000', + ]); + + $conv = Conversation::where('employer_id', $user->id)->where('id', $id)->firstOrFail(); + + $message = Message::create([ + 'conversation_id' => $conv->id, + 'sender_type' => 'employer', + 'sender_id' => $user->id, + 'text' => $request->text, + ]); + + // Touch conversation updated_at for sorting + $conv->touch(); + + return back(); + } +} diff --git a/app/Http/Controllers/Employer/ProfileController.php b/app/Http/Controllers/Employer/ProfileController.php new file mode 100644 index 0000000..7763641 --- /dev/null +++ b/app/Http/Controllers/Employer/ProfileController.php @@ -0,0 +1,127 @@ +id ?? null); + + if (!$sessId) { + $user = User::where('role', 'employer')->first(); + if ($user) { + session(['user' => (object)[ + 'id' => $user->id, + 'name' => $user->name, + 'email' => $user->email, + 'role' => 'employer', + 'subscription_status' => $user->subscription_status ?? 'active', + ]]); + return $user; + } + } else { + return User::find($sessId); + } + + return null; + } + + public function index(Request $request) + { + $user = $this->resolveCurrentUser(); + if (!$user) { + return redirect()->route('employer.dashboard'); + } + + // Fetch or create profile + $profile = $user->employerProfile; + if (!$profile) { + $profile = EmployerProfile::create([ + 'user_id' => $user->id, + 'company_name' => 'Al Mansoor Household', + 'phone' => '+971 50 123 4567', + 'emirates_id_status' => 'approved', + ]); + } + + $employerProfile = [ + 'name' => $user->name, + 'email' => $user->email, + 'company_name' => $profile->company_name, + 'phone' => $profile->phone, + 'language' => $profile->language ?? 'English', + 'notifications' => (bool)($profile->notifications ?? true), + ]; + + return Inertia::render('Employer/Profile', [ + 'employerProfile' => $employerProfile, + ]); + } + + public function update(Request $request) + { + $user = $this->resolveCurrentUser(); + if (!$user) { + return back()->withErrors(['general' => 'User session not found.']); + } + + $request->validate([ + 'name' => 'required|string|max:255', + 'email' => 'required|string|email|max:255|unique:users,email,' . $user->id, + 'phone' => 'required|string|max:255', + 'company_name' => 'required|string|max:255', + 'language' => 'required|string|in:English,Arabic', + 'notifications' => 'required|boolean', + 'current_password' => 'nullable|required_with:new_password|string', + 'new_password' => 'nullable|string|min:8|confirmed', + ]); + + // Update User Model + $user->update([ + 'name' => $request->name, + 'email' => $request->email, + ]); + + // Update EmployerProfile + $profile = $user->employerProfile; + if (!$profile) { + $profile = new EmployerProfile(['user_id' => $user->id]); + } + $profile->company_name = $request->company_name; + $profile->phone = $request->phone; + $profile->language = $request->language; + $profile->notifications = $request->notifications; + $profile->save(); + + // Update Password if provided + if ($request->filled('new_password')) { + if (!Hash::check($request->current_password, $user->password)) { + return back()->withErrors(['current_password' => 'The provided password does not match your current password.']); + } + $user->update([ + 'password' => Hash::make($request->new_password), + ]); + } + + // Update session user name/email + session(['user' => (object)[ + 'id' => $user->id, + 'name' => $user->name, + 'email' => $user->email, + 'role' => 'employer', + 'subscription_status' => $user->subscription_status ?? 'active', + ]]); + + return back()->with('success', 'Profile updated successfully.'); + } +} diff --git a/app/Http/Controllers/Employer/ShortlistController.php b/app/Http/Controllers/Employer/ShortlistController.php new file mode 100644 index 0000000..24fcb1d --- /dev/null +++ b/app/Http/Controllers/Employer/ShortlistController.php @@ -0,0 +1,116 @@ +id ?? null); + + if (!$sessId) { + $user = User::where('role', 'employer')->first(); + if ($user) { + session(['user' => (object)[ + 'id' => $user->id, + 'name' => $user->name, + 'email' => $user->email, + 'role' => 'employer', + 'subscription_status' => $user->subscription_status ?? 'active', + ]]); + return $user; + } + } else { + return User::find($sessId); + } + + return null; + } + + public function index(Request $request) + { + $user = $this->resolveCurrentUser(); + $employerId = $user ? $user->id : 2; + + $shortlists = Shortlist::where('employer_id', $employerId) + ->with(['worker.category', 'worker.skills']) + ->get(); + + $shortlistedWorkers = $shortlists->map(function ($s) { + $w = $s->worker; + if (!$w) return null; + return [ + 'id' => $w->id, + 'name' => $w->name, + 'nationality' => $w->nationality, + 'category' => $w->category ? $w->category->name : 'General Helper', + 'skills' => $w->skills->pluck('name')->toArray(), + 'availability' => $w->availability, + 'experience' => $w->experience, + 'salary' => (int)$w->salary, + 'verified' => (bool)$w->verified, + ]; + })->filter()->values()->toArray(); + + return Inertia::render('Employer/Shortlist', [ + 'shortlistedWorkers' => $shortlistedWorkers, + ]); + } + + public function toggle(Request $request) + { + $user = $this->resolveCurrentUser(); + $employerId = $user ? $user->id : 2; + + $request->validate([ + 'worker_id' => 'required|exists:workers,id', + ]); + + $workerId = $request->worker_id; + + $existing = Shortlist::where('employer_id', $employerId) + ->where('worker_id', $workerId) + ->first(); + + if ($existing) { + $existing->delete(); + $status = 'removed'; + } else { + Shortlist::create([ + 'employer_id' => $employerId, + 'worker_id' => $workerId, + ]); + $status = 'added'; + } + + if ($request->wantsJson()) { + return response()->json([ + 'status' => 'success', + 'action' => $status, + 'shortlistedIds' => Shortlist::where('employer_id', $employerId)->pluck('worker_id')->toArray() + ]); + } + + return back(); + } + + public function remove(Request $request, $id) + { + $user = $this->resolveCurrentUser(); + $employerId = $user ? $user->id : 2; + + Shortlist::where('employer_id', $employerId) + ->where('worker_id', $id) + ->delete(); + + return back()->with('success', 'Worker removed from shortlist.'); + } +} diff --git a/app/Http/Controllers/Employer/WorkerController.php b/app/Http/Controllers/Employer/WorkerController.php index ed2ba0a..b3b27f6 100644 --- a/app/Http/Controllers/Employer/WorkerController.php +++ b/app/Http/Controllers/Employer/WorkerController.php @@ -5,161 +5,109 @@ use App\Http\Controllers\Controller; use Illuminate\Http\Request; use Inertia\Inertia; +use App\Models\User; +use App\Models\Worker; +use App\Models\WorkerCategory; +use App\Models\Shortlist; class WorkerController extends Controller { - private function getWorkersList() + private function resolveCurrentUser() { - return [ - [ - 'id' => 101, - 'name' => 'Maria Santos', - 'nationality' => 'Philippines', - 'category' => 'Childcare', - 'skills' => ['Childcare', 'Cooking', 'Housekeeping', 'Ironing'], - 'availability' => 'Immediate', - 'experience' => '5+ Years', - 'experience_years' => 6, - 'salary' => 1800, - 'religion' => 'Christian', - 'languages' => ['English', 'Tagalog'], - 'age' => 32, - 'verified' => true, - 'bio' => 'Experienced nanny with 6 years working with expatriate families in Dubai. Certified in pediatric first aid.', - ], - [ - 'id' => 102, - 'name' => 'Lakshmi Sharma', - 'nationality' => 'India', - 'category' => 'Elderly Care', - 'skills' => ['Elderly Care', 'Cooking', 'Medication Management'], - 'availability' => '2 Weeks', - 'experience' => '3-5 Years', - 'experience_years' => 4, - 'salary' => 1600, - 'religion' => 'Hindu', - 'languages' => ['English', 'Hindi'], - 'age' => 38, - 'verified' => true, - 'bio' => 'Patient caregiver specializing in elderly assistance, mobility support, and vegetarian cooking.', - ], - [ - 'id' => 103, - 'name' => 'Siti Aminah', - 'nationality' => 'Indonesia', - 'category' => 'Housekeeping', - 'skills' => ['Housekeeping', 'Ironing', 'Deep Cleaning'], - 'availability' => 'Immediate', - 'experience' => '3-5 Years', - 'experience_years' => 3, - 'salary' => 1400, - 'religion' => 'Muslim', - 'languages' => ['English', 'Arabic'], - 'age' => 29, - 'verified' => true, - 'bio' => 'Hardworking and meticulous housekeeper with excellent references from Abu Dhabi households.', - ], - [ - 'id' => 104, - 'name' => 'Grace Osei', - 'nationality' => 'Ghana', - 'category' => 'Childcare', - 'skills' => ['Childcare', 'English Tutoring', 'Cooking'], - 'availability' => '1 Month', - 'experience' => '1-2 Years', - 'experience_years' => 2, - 'salary' => 1500, - 'religion' => 'Christian', - 'languages' => ['English'], - 'age' => 26, - 'verified' => false, - 'bio' => 'Energetic and educated nanny. Fluent in English, great with toddlers and assisting with homework.', - ], - [ - 'id' => 105, - 'name' => 'Anoma Perera', - 'nationality' => 'Sri Lanka', - 'category' => 'Cooking', - 'skills' => ['Cooking', 'Baking', 'Housekeeping'], - 'availability' => 'Immediate', - 'experience' => '5+ Years', - 'experience_years' => 8, - 'salary' => 2200, - 'religion' => 'Buddhist', - 'languages' => ['English', 'Arabic'], - 'age' => 41, - 'verified' => true, - 'bio' => 'Professional domestic cook skilled in Arabic, Continental, and Asian cuisine. Highly organized.', - ], - [ - 'id' => 106, - 'name' => 'Mary Wanjiku', - 'nationality' => 'Kenya', - 'category' => 'Housekeeping', - 'skills' => ['Housekeeping', 'Laundry', 'Pet Care'], - 'availability' => '1 Week', - 'experience' => '1-2 Years', - 'experience_years' => 2, - 'salary' => 1300, - 'religion' => 'Christian', - 'languages' => ['English'], - 'age' => 25, - 'verified' => true, - 'bio' => 'Enthusiastic and animal-loving housekeeper. Extremely reliable and quick learner.', - ], - [ - 'id' => 107, - 'name' => 'Fatima Zahra', - 'nationality' => 'Ethiopia', - 'category' => 'Housekeeping', - 'skills' => ['Housekeeping', 'Arabic Cooking', 'Baby Care'], - 'availability' => 'Immediate', - 'experience' => '3-5 Years', - 'experience_years' => 5, - 'salary' => 1500, - 'religion' => 'Muslim', - 'languages' => ['Arabic', 'English'], - 'age' => 31, - 'verified' => true, - 'bio' => 'Fluent Arabic speaker with 5 years experience in Al Ain. Excellent at traditional Arabic dishes.', - ], - [ - 'id' => 108, - 'name' => 'Ramesh Thapa', - 'nationality' => 'Nepal', - 'category' => 'Driver', - 'skills' => ['Family Driver', 'Garden Maintenance', 'Heavy Lifting'], - 'availability' => '2 Weeks', - 'experience' => '5+ Years', - 'experience_years' => 7, - 'salary' => 2500, - 'religion' => 'Hindu', - 'languages' => ['English', 'Hindi'], - 'age' => 36, - 'verified' => true, - 'bio' => 'Valid UAE driving license with clean record. Familiar with all Dubai and Sharjah school routes.', - ], - ]; + $sess = session('user'); + $sessId = is_array($sess) ? ($sess['id'] ?? null) : ($sess->id ?? null); + + if (!$sessId) { + $user = User::where('role', 'employer')->first(); + if ($user) { + session(['user' => (object)[ + 'id' => $user->id, + 'name' => $user->name, + 'email' => $user->email, + 'role' => 'employer', + 'subscription_status' => $user->subscription_status ?? 'active', + ]]); + return $user; + } + } else { + return User::find($sessId); + } + + return null; } public function index(Request $request) { + $user = $this->resolveCurrentUser(); + $employerId = $user ? $user->id : 2; + + // Fetch workers from DB + $dbWorkers = Worker::with(['category', 'skills']) + ->where('status', 'active') + ->get(); + + $workers = $dbWorkers->map(function ($w) { + return [ + 'id' => $w->id, + 'name' => $w->name, + 'nationality' => $w->nationality, + 'category' => $w->category ? $w->category->name : 'General Helper', + 'skills' => $w->skills->pluck('name')->toArray(), + 'availability' => $w->availability, + 'experience' => $w->experience, + 'salary' => (int)$w->salary, + 'religion' => $w->religion, + 'languages' => ['English', 'Arabic'], // Custom language field or mock + 'age' => $w->age, + 'verified' => (bool)$w->verified, + 'bio' => $w->bio, + ]; + })->toArray(); + + // Get saved shortlist for current employer + $shortlistedIds = Shortlist::where('employer_id', $employerId)->pluck('worker_id')->toArray(); + + // Dynamically build filter metadata from DB values + $dbCategories = WorkerCategory::pluck('name')->toArray(); + $dbNationalities = Worker::distinct()->where('status', 'active')->pluck('nationality')->toArray(); + + $filtersMetadata = [ + 'categories' => array_merge(['All Categories'], $dbCategories), + 'nationalities' => array_merge(['All Nationalities'], $dbNationalities), + 'availabilities' => ['All Availabilities', 'Immediate', '1 Week', '2 Weeks', '1 Month'], + 'experienceLevels' => ['All Experience', '1-2 Years', '3-5 Years', '5+ Years'], + 'religions' => ['All Religions', 'Christian', 'Muslim', 'Hindu', 'Buddhist'], + ]; + return Inertia::render('Employer/Workers/Index', [ - 'initialWorkers' => $this->getWorkersList(), - 'filtersMetadata' => [ - 'categories' => ['All Categories', 'Childcare', 'Housekeeping', 'Cooking', 'Elderly Care', 'Driver'], - 'nationalities' => ['All Nationalities', 'Philippines', 'India', 'Indonesia', 'Sri Lanka', 'Nepal', 'Ethiopia', 'Kenya', 'Ghana'], - 'availabilities' => ['All Availabilities', 'Immediate', '1 Week', '2 Weeks', '1 Month'], - 'experienceLevels' => ['All Experience', '1-2 Years', '3-5 Years', '5+ Years'], - 'religions' => ['All Religions', 'Christian', 'Muslim', 'Hindu', 'Buddhist'], - ] + 'initialWorkers' => $workers, + 'initialShortlistedIds' => $shortlistedIds, + 'filtersMetadata' => $filtersMetadata, ]); } public function show($id) { - $workers = $this->getWorkersList(); - $worker = collect($workers)->firstWhere('id', (int)$id) ?? $workers[0]; + $w = Worker::with(['category', 'skills', 'documents'])->findOrFail($id); + + $worker = [ + 'id' => $w->id, + 'name' => $w->name, + 'nationality' => $w->nationality, + 'category' => $w->category ? $w->category->name : 'General Helper', + 'skills' => $w->skills->pluck('name')->toArray(), + 'availability' => $w->availability, + 'experience' => $w->experience, + 'experience_years' => 5, // Can parse or default + 'salary' => (int)$w->salary, + 'religion' => $w->religion, + 'languages' => ['English', 'Arabic'], + 'age' => $w->age, + 'verified' => (bool)$w->verified, + 'bio' => $w->bio, + 'passport_status' => 'OCR Verified', + 'visa_status' => 'Transferable', + ]; return Inertia::render('Employer/Workers/Show', [ 'worker' => $worker, diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index 5214475..2a1408b 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -35,17 +35,57 @@ public function version(Request $request): ?string */ public function share(Request $request): array { + $sess = session('user'); + $userId = is_array($sess) ? ($sess['id'] ?? null) : ($sess->id ?? null); + $user = $userId ? \App\Models\User::find($userId) : \App\Models\User::where('role', 'employer')->first(); + + $userData = null; + $unreadCount = 0; + + if ($user) { + $sub = \Illuminate\Support\Facades\DB::table('subscriptions') + ->where('user_id', $user->id) + ->where('status', 'active') + ->latest('id') + ->first(); + + $profile = \Illuminate\Support\Facades\DB::table('employer_profiles') + ->where('user_id', $user->id) + ->first(); + + $unreadCount = \Illuminate\Support\Facades\DB::table('messages') + ->join('conversations', 'messages.conversation_id', '=', 'conversations.id') + ->where('conversations.employer_id', $user->id) + ->where('messages.sender_type', 'worker') + ->whereNull('messages.read_at') + ->count(); + + $userData = [ + 'id' => $user->id, + 'name' => $user->name, + 'email' => $user->email, + 'role' => $user->role, + 'company_name' => $profile ? $profile->company_name : 'Al Mansoor Household', + 'subscription_status' => $sub ? 'active' : 'none', + 'subscription_expires_at' => $sub && $sub->expires_at ? date('M d', strtotime($sub->expires_at)) : null, + ]; + + // Sync with session so that controllers have access to the exact user + session(['user' => (object) $userData]); + } + return [ ...parent::share($request), 'auth' => [ - 'user' => session('user'), + 'user' => $userData, ], - 'unread_messages_count' => 3, + 'unread_messages_count' => $unreadCount, 'flash' => [ 'status' => session('status'), 'reason' => session('reason'), 'success' => session('success'), 'error' => session('error'), + 'first_login' => session('first_login'), ], ]; } diff --git a/app/Mail/EmployerOtpMail.php b/app/Mail/EmployerOtpMail.php new file mode 100644 index 0000000..54efc84 --- /dev/null +++ b/app/Mail/EmployerOtpMail.php @@ -0,0 +1,48 @@ +otp = $otp; + $this->companyName = $companyName; + $this->employerName = $employerName; + } + + /** + * Get the message envelope. + */ + public function envelope(): Envelope + { + return new Envelope( + subject: 'Verification Code for Marketplace Registration: ' . $this->otp, + ); + } + + /** + * Get the message content definition. + */ + public function content(): Content + { + return new Content( + view: 'emails.employer-otp', + ); + } +} diff --git a/app/Models/Announcement.php b/app/Models/Announcement.php new file mode 100644 index 0000000..13326f8 --- /dev/null +++ b/app/Models/Announcement.php @@ -0,0 +1,17 @@ +belongsTo(User::class, 'employer_id'); + } + + public function worker() + { + return $this->belongsTo(Worker::class, 'worker_id'); + } + + public function messages() + { + return $this->hasMany(Message::class); + } +} diff --git a/app/Models/EmployerProfile.php b/app/Models/EmployerProfile.php index aea69d4..fc5327b 100644 --- a/app/Models/EmployerProfile.php +++ b/app/Models/EmployerProfile.php @@ -9,10 +9,13 @@ class EmployerProfile extends Model protected $fillable = [ 'user_id', 'company_name', - 'phone', + 'phone', + 'country', 'emirates_id_front', 'emirates_id_back', 'verification_status', - 'rejection_reason' + 'rejection_reason', + 'language', + 'notifications' ]; } diff --git a/app/Models/JobApplication.php b/app/Models/JobApplication.php new file mode 100644 index 0000000..77cd3e1 --- /dev/null +++ b/app/Models/JobApplication.php @@ -0,0 +1,27 @@ +belongsTo(JobPost::class, 'job_id'); + } + + public function worker() + { + return $this->belongsTo(Worker::class, 'worker_id'); + } +} diff --git a/app/Models/JobPost.php b/app/Models/JobPost.php new file mode 100644 index 0000000..4a443ef --- /dev/null +++ b/app/Models/JobPost.php @@ -0,0 +1,47 @@ + 'date', + 'salary' => 'decimal:2', + 'workers_needed' => 'integer', + ]; + + public function employer() + { + return $this->belongsTo(User::class, 'employer_id'); + } + + public function category() + { + return $this->belongsTo(WorkerCategory::class, 'category_id'); + } + + public function applications() + { + return $this->hasMany(JobApplication::class, 'job_id'); + } +} diff --git a/app/Models/Message.php b/app/Models/Message.php new file mode 100644 index 0000000..e58a1d1 --- /dev/null +++ b/app/Models/Message.php @@ -0,0 +1,28 @@ + 'boolean', + ]; + + public function conversation() + { + return $this->belongsTo(Conversation::class); + } +} diff --git a/app/Models/Shortlist.php b/app/Models/Shortlist.php new file mode 100644 index 0000000..de732cb --- /dev/null +++ b/app/Models/Shortlist.php @@ -0,0 +1,26 @@ +belongsTo(User::class, 'employer_id'); + } + + public function worker() + { + return $this->belongsTo(Worker::class, 'worker_id'); + } +} diff --git a/app/Models/Skill.php b/app/Models/Skill.php new file mode 100644 index 0000000..2928332 --- /dev/null +++ b/app/Models/Skill.php @@ -0,0 +1,18 @@ +belongsToMany(Worker::class, 'worker_skills'); + } +} diff --git a/app/Models/Worker.php b/app/Models/Worker.php new file mode 100644 index 0000000..6031cac --- /dev/null +++ b/app/Models/Worker.php @@ -0,0 +1,48 @@ + 'boolean', + 'salary' => 'decimal:2', + ]; + + public function category() + { + return $this->belongsTo(WorkerCategory::class, 'category_id'); + } + + public function skills() + { + return $this->belongsToMany(Skill::class, 'worker_skills'); + } + + public function documents() + { + return $this->hasMany(WorkerDocument::class); + } +} diff --git a/app/Models/WorkerCategory.php b/app/Models/WorkerCategory.php new file mode 100644 index 0000000..3675e28 --- /dev/null +++ b/app/Models/WorkerCategory.php @@ -0,0 +1,18 @@ +hasMany(Worker::class, 'category_id'); + } +} diff --git a/app/Models/WorkerDocument.php b/app/Models/WorkerDocument.php new file mode 100644 index 0000000..ee4862f --- /dev/null +++ b/app/Models/WorkerDocument.php @@ -0,0 +1,25 @@ +belongsTo(Worker::class); + } +} diff --git a/database/migrations/2026_05_18_104329_create_worker_categories_table.php b/database/migrations/2026_05_18_104329_create_worker_categories_table.php new file mode 100644 index 0000000..2463d44 --- /dev/null +++ b/database/migrations/2026_05_18_104329_create_worker_categories_table.php @@ -0,0 +1,28 @@ +id(); + $table->string('name')->unique(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('worker_categories'); + } +}; diff --git a/database/migrations/2026_05_18_104336_create_workers_table.php b/database/migrations/2026_05_18_104336_create_workers_table.php new file mode 100644 index 0000000..0d97914 --- /dev/null +++ b/database/migrations/2026_05_18_104336_create_workers_table.php @@ -0,0 +1,41 @@ +id(); + $table->string('name'); + $table->string('email')->unique(); + $table->string('phone'); + $table->string('nationality'); + $table->integer('age'); + $table->decimal('salary', 10, 2); + $table->string('availability'); + $table->string('experience'); + $table->string('religion'); + $table->text('bio'); + $table->foreignId('category_id')->constrained('worker_categories'); + $table->boolean('verified')->default(false); + $table->string('status')->default('active'); // active, inactive + $table->timestamps(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('workers'); + } +}; diff --git a/database/migrations/2026_05_18_104342_create_skills_table.php b/database/migrations/2026_05_18_104342_create_skills_table.php new file mode 100644 index 0000000..1ad6713 --- /dev/null +++ b/database/migrations/2026_05_18_104342_create_skills_table.php @@ -0,0 +1,28 @@ +id(); + $table->string('name')->unique(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('skills'); + } +}; diff --git a/database/migrations/2026_05_18_104349_create_worker_skills_table.php b/database/migrations/2026_05_18_104349_create_worker_skills_table.php new file mode 100644 index 0000000..86eb2d6 --- /dev/null +++ b/database/migrations/2026_05_18_104349_create_worker_skills_table.php @@ -0,0 +1,29 @@ +id(); + $table->foreignId('worker_id')->constrained('workers')->onDelete('cascade'); + $table->foreignId('skill_id')->constrained('skills')->onDelete('cascade'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('worker_skills'); + } +}; diff --git a/database/migrations/2026_05_18_104354_create_worker_documents_table.php b/database/migrations/2026_05_18_104354_create_worker_documents_table.php new file mode 100644 index 0000000..0c630bc --- /dev/null +++ b/database/migrations/2026_05_18_104354_create_worker_documents_table.php @@ -0,0 +1,34 @@ +id(); + $table->foreignId('worker_id')->constrained('workers')->onDelete('cascade'); + $table->string('type'); // passport, visa + $table->string('number'); + $table->date('issue_date')->nullable(); + $table->date('expiry_date')->nullable(); + $table->decimal('ocr_accuracy', 5, 2)->nullable(); + $table->string('file_path')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('worker_documents'); + } +}; diff --git a/database/migrations/2026_05_18_104400_create_job_posts_table.php b/database/migrations/2026_05_18_104400_create_job_posts_table.php new file mode 100644 index 0000000..69dfb4c --- /dev/null +++ b/database/migrations/2026_05_18_104400_create_job_posts_table.php @@ -0,0 +1,39 @@ +id(); + $table->foreignId('employer_id')->constrained('users')->onDelete('cascade'); + $table->string('title'); + $table->foreignId('category_id')->constrained('worker_categories'); + $table->integer('workers_needed')->default(1); + $table->string('job_type'); // Full Time, Part Time, Contract + $table->string('location'); + $table->decimal('salary', 10, 2); + $table->date('start_date'); + $table->text('description'); + $table->text('requirements')->nullable(); + $table->string('status')->default('active'); // active, pending, closed + $table->timestamps(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('job_posts'); + } +}; diff --git a/database/migrations/2026_05_18_104406_create_job_applications_table.php b/database/migrations/2026_05_18_104406_create_job_applications_table.php new file mode 100644 index 0000000..f3f42ab --- /dev/null +++ b/database/migrations/2026_05_18_104406_create_job_applications_table.php @@ -0,0 +1,30 @@ +id(); + $table->foreignId('job_id')->constrained('job_posts')->onDelete('cascade'); + $table->foreignId('worker_id')->constrained('workers')->onDelete('cascade'); + $table->string('status')->default('applied'); // applied, shortlisted, hired, rejected + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('job_applications'); + } +}; diff --git a/database/migrations/2026_05_18_104412_create_conversations_table.php b/database/migrations/2026_05_18_104412_create_conversations_table.php new file mode 100644 index 0000000..35198c5 --- /dev/null +++ b/database/migrations/2026_05_18_104412_create_conversations_table.php @@ -0,0 +1,29 @@ +id(); + $table->foreignId('employer_id')->constrained('users')->onDelete('cascade'); + $table->foreignId('worker_id')->constrained('workers')->onDelete('cascade'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('conversations'); + } +}; diff --git a/database/migrations/2026_05_18_104419_create_messages_table.php b/database/migrations/2026_05_18_104419_create_messages_table.php new file mode 100644 index 0000000..f8a80ff --- /dev/null +++ b/database/migrations/2026_05_18_104419_create_messages_table.php @@ -0,0 +1,32 @@ +id(); + $table->foreignId('conversation_id')->constrained('conversations')->onDelete('cascade'); + $table->string('sender_type'); // employer, worker + $table->unsignedBigInteger('sender_id'); // links to users (employer) or workers + $table->text('text'); + $table->timestamp('read_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('messages'); + } +}; diff --git a/database/migrations/2026_05_18_104432_create_payments_table.php b/database/migrations/2026_05_18_104432_create_payments_table.php new file mode 100644 index 0000000..87a6a23 --- /dev/null +++ b/database/migrations/2026_05_18_104432_create_payments_table.php @@ -0,0 +1,32 @@ +id(); + $table->foreignId('user_id')->constrained('users')->onDelete('cascade'); + $table->decimal('amount', 10, 2); + $table->string('currency')->default('AED'); + $table->string('description'); + $table->string('status')->default('success'); // success, failed, pending + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('payments'); + } +}; diff --git a/database/migrations/2026_05_19_120000_create_announcements_table.php b/database/migrations/2026_05_19_120000_create_announcements_table.php new file mode 100644 index 0000000..63bdabc --- /dev/null +++ b/database/migrations/2026_05_19_120000_create_announcements_table.php @@ -0,0 +1,30 @@ +id(); + $table->string('title'); + $table->text('body'); + $table->string('type')->default('info'); // info, warning, success + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('announcements'); + } +}; diff --git a/database/migrations/2026_05_19_130000_create_shortlists_table.php b/database/migrations/2026_05_19_130000_create_shortlists_table.php new file mode 100644 index 0000000..cf00b79 --- /dev/null +++ b/database/migrations/2026_05_19_130000_create_shortlists_table.php @@ -0,0 +1,31 @@ +id(); + $table->foreignId('employer_id')->constrained('users')->onDelete('cascade'); + $table->foreignId('worker_id')->constrained('workers')->onDelete('cascade'); + $table->timestamps(); + + $table->unique(['employer_id', 'worker_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('shortlists'); + } +}; diff --git a/database/migrations/2026_05_19_140000_add_language_and_notifications_to_employer_profiles.php b/database/migrations/2026_05_19_140000_add_language_and_notifications_to_employer_profiles.php new file mode 100644 index 0000000..5c9778e --- /dev/null +++ b/database/migrations/2026_05_19_140000_add_language_and_notifications_to_employer_profiles.php @@ -0,0 +1,29 @@ +string('language')->default('English'); + $table->boolean('notifications')->default(true); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('employer_profiles', function (Blueprint $table) { + $table->dropColumn(['language', 'notifications']); + }); + } +}; diff --git a/database/migrations/2026_05_20_052419_add_country_to_employer_profiles_table.php b/database/migrations/2026_05_20_052419_add_country_to_employer_profiles_table.php new file mode 100644 index 0000000..a0f7198 --- /dev/null +++ b/database/migrations/2026_05_20_052419_add_country_to_employer_profiles_table.php @@ -0,0 +1,28 @@ +string('country')->nullable()->after('company_name'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('employer_profiles', function (Blueprint $table) { + $table->dropColumn('country'); + }); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 6b901f8..5fa88a4 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -15,11 +15,91 @@ class DatabaseSeeder extends Seeder */ public function run(): void { - // User::factory(10)->create(); + // Create Admin User + \Illuminate\Support\Facades\DB::table('users')->insert([ + 'name' => 'Admin User', + 'email' => 'admin@example.com', + 'password' => \Illuminate\Support\Facades\Hash::make('password'), + 'role' => 'admin', + 'created_at' => now(), + 'updated_at' => now(), + ]); - User::factory()->create([ - 'name' => 'Test User', - 'email' => 'test@example.com', + // Call Seeders + $this->call([ + WorkerCategorySeeder::class, + SkillSeeder::class, + EmployerProfileSeeder::class, + WorkerSeeder::class, + JobPostSeeder::class, + ]); + + // Seed Conversations & Messages (assuming IDs exist) + $conversationId = \Illuminate\Support\Facades\DB::table('conversations')->insertGetId([ + 'employer_id' => 2, // From EmployerProfileSeeder + 'worker_id' => 1, // From WorkerSeeder + 'created_at' => now(), + 'updated_at' => now(), + ]); + + \Illuminate\Support\Facades\DB::table('messages')->insert([ + [ + 'conversation_id' => $conversationId, + 'sender_type' => 'employer', + 'sender_id' => 2, + 'text' => 'Hello, are you available for an interview?', + 'created_at' => now(), + 'updated_at' => now(), + ], + [ + 'conversation_id' => $conversationId, + 'sender_type' => 'worker', + 'sender_id' => 1, + 'text' => 'Yes, I am available.', + 'created_at' => now()->addMinutes(5), + 'updated_at' => now()->addMinutes(5), + ] + ]); + + // Seed Subscriptions + \Illuminate\Support\Facades\DB::table('subscriptions')->insert([ + 'user_id' => 2, // Employer created in EmployerProfileSeeder + 'plan_id' => 'premium', + 'amount_aed' => 499.00, + 'starts_at' => now(), + 'expires_at' => now()->addMonth(), + 'status' => 'active', + 'created_at' => now(), + 'updated_at' => now(), + ]); + + // Seed Payments + \Illuminate\Support\Facades\DB::table('payments')->insert([ + 'user_id' => 2, // Employer + 'amount' => 499.00, + 'currency' => 'AED', + 'description' => 'Premium Monthly Subscription', + 'status' => 'success', + 'created_at' => now(), + 'updated_at' => now(), + ]); + + // Seed Announcements + \Illuminate\Support\Facades\DB::table('announcements')->insert([ + [ + 'title' => 'New Visa Regulations Update', + 'body' => 'The Ministry of Human Resources and Emiratisation has announced updated guidelines for domestic worker sponsorship starting this month.', + 'type' => 'info', + 'created_at' => now(), + 'updated_at' => now(), + ], + [ + 'title' => 'Enhanced OCR Verification Active', + 'body' => 'We have deployed upgraded OCR passport verification to ensure 100% legal compliance for all worker profiles on the platform.', + 'type' => 'info', + 'created_at' => now()->subDays(5), + 'updated_at' => now()->subDays(5), + ], ]); } } diff --git a/database/seeders/EmployerProfileSeeder.php b/database/seeders/EmployerProfileSeeder.php new file mode 100644 index 0000000..ebf497a --- /dev/null +++ b/database/seeders/EmployerProfileSeeder.php @@ -0,0 +1,35 @@ +insertGetId([ + 'name' => 'Employer One', + 'email' => 'employer1@example.com', + 'password' => \Illuminate\Support\Facades\Hash::make('password'), + 'role' => 'employer', + 'created_at' => now(), + 'updated_at' => now(), + ]); + + // Create Employer Profile + \Illuminate\Support\Facades\DB::table('employer_profiles')->insert([ + 'user_id' => $userId, + 'company_name' => 'Al Mansoor Household', + 'phone' => '+971 50 123 4567', + 'verification_status' => 'approved', + 'created_at' => now(), + 'updated_at' => now(), + ]); + } +} diff --git a/database/seeders/JobPostSeeder.php b/database/seeders/JobPostSeeder.php new file mode 100644 index 0000000..3753ec8 --- /dev/null +++ b/database/seeders/JobPostSeeder.php @@ -0,0 +1,70 @@ +first()->id ?? 2; + + $jobs = [ + [ + 'employer_id' => $employerId, + 'title' => 'Senior Electrician for Villa Project', + 'category_id' => 1, // Electrician + 'workers_needed' => 2, + 'job_type' => 'Full Time', + 'location' => 'Dubai Marina', + 'salary' => 3000.00, + 'start_date' => '2026-06-01', + 'description' => 'Need an experienced electrician for a luxury villa project.', + 'status' => 'active', + ], + [ + 'employer_id' => $employerId, + 'title' => 'Mason for Commercial Building', + 'category_id' => 2, // Mason + 'workers_needed' => 5, + 'job_type' => 'Contract', + 'location' => 'Business Bay', + 'salary' => 2500.00, + 'start_date' => '2026-06-15', + 'description' => 'Looking for skilled masons for a commercial project.', + 'status' => 'active', + ], + ]; + + foreach ($jobs as $jobData) { + $jobId = \Illuminate\Support\Facades\DB::table('job_posts')->insertGetId(array_merge($jobData, [ + 'created_at' => now(), + 'updated_at' => now(), + ])); + + // Seed Applications (assuming Worker ID 1 and 2 exist) + \Illuminate\Support\Facades\DB::table('job_applications')->insert([ + [ + 'job_id' => $jobId, + 'worker_id' => 1, + 'status' => 'applied', + 'created_at' => now(), + 'updated_at' => now(), + ], + [ + 'job_id' => $jobId, + 'worker_id' => 2, + 'status' => 'shortlisted', + 'created_at' => now(), + 'updated_at' => now(), + ] + ]); + } + } +} diff --git a/database/seeders/SkillSeeder.php b/database/seeders/SkillSeeder.php new file mode 100644 index 0000000..b06ea0c --- /dev/null +++ b/database/seeders/SkillSeeder.php @@ -0,0 +1,27 @@ +insert([ + 'name' => $skill, + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + } +} diff --git a/database/seeders/WorkerCategorySeeder.php b/database/seeders/WorkerCategorySeeder.php new file mode 100644 index 0000000..05ad3eb --- /dev/null +++ b/database/seeders/WorkerCategorySeeder.php @@ -0,0 +1,28 @@ +insert([ + 'name' => $category, + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + } +} diff --git a/database/seeders/WorkerSeeder.php b/database/seeders/WorkerSeeder.php new file mode 100644 index 0000000..32fcd2e --- /dev/null +++ b/database/seeders/WorkerSeeder.php @@ -0,0 +1,185 @@ + 'John Doe', + 'email' => 'john.doe@example.com', + 'phone' => '+971 50 111 2222', + 'nationality' => 'India', + 'age' => 30, + 'salary' => 2500.00, + 'availability' => 'Immediate', + 'experience' => '5+ Years', + 'religion' => 'Christian', + 'bio' => 'Experienced electrician with a strong background in residential wiring.', + 'category_id' => 1, // Electrician + 'verified' => true, + 'status' => 'active', + ], + [ + 'name' => 'Ali Hassan', + 'email' => 'ali.hassan@example.com', + 'phone' => '+971 50 333 4444', + 'nationality' => 'Pakistan', + 'age' => 28, + 'salary' => 2000.00, + 'availability' => '1 Month Notice', + 'experience' => '3 Years', + 'religion' => 'Muslim', + 'bio' => 'Skilled mason with experience in block work and plastering.', + 'category_id' => 2, // Mason + 'verified' => true, + 'status' => 'active', + ], + [ + 'name' => 'Maria Santos', + 'email' => 'maria.santos@example.com', + 'phone' => '+971 50 555 6666', + 'nationality' => 'Philippines', + 'age' => 32, + 'salary' => 1800.00, + 'availability' => 'Immediate', + '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', + ], + [ + 'name' => 'Lakshmi Sharma', + 'email' => 'lakshmi.sharma@example.com', + 'phone' => '+971 50 777 8888', + 'nationality' => 'India', + 'age' => 38, + 'salary' => 1600.00, + 'availability' => '2 Weeks', + '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', + ], + [ + 'name' => 'Siti Aminah', + 'email' => 'siti.aminah@example.com', + 'phone' => '+971 50 999 0000', + 'nationality' => 'Indonesia', + 'age' => 29, + 'salary' => 1400.00, + 'availability' => 'Immediate', + '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', + ], + [ + 'name' => 'Grace Osei', + 'email' => 'grace.osei@example.com', + 'phone' => '+971 50 222 3333', + 'nationality' => 'Ghana', + 'age' => 26, + 'salary' => 1500.00, + 'availability' => '1 Month', + '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', + ], + [ + 'name' => 'Anoma Perera', + 'email' => 'anoma.perera@example.com', + 'phone' => '+971 50 444 5555', + 'nationality' => 'Sri Lanka', + 'age' => 41, + 'salary' => 2200.00, + 'availability' => 'Immediate', + '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', + ], + [ + 'name' => 'Ramesh Thapa', + 'email' => 'ramesh.thapa@example.com', + 'phone' => '+971 50 666 7777', + 'nationality' => 'Nepal', + 'age' => 36, + 'salary' => 2500.00, + 'availability' => '2 Weeks', + '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', + ], + ]; + + foreach ($workers as $workerData) { + $workerId = \Illuminate\Support\Facades\DB::table('workers')->insertGetId(array_merge($workerData, [ + 'created_at' => now(), + 'updated_at' => now(), + ])); + + // Seed Documents + \Illuminate\Support\Facades\DB::table('worker_documents')->insert([ + [ + 'worker_id' => $workerId, + 'type' => 'passport', + 'number' => 'P' . rand(100000, 999999), + 'issue_date' => '2020-01-01', + 'expiry_date' => '2030-01-01', + 'ocr_accuracy' => 95.5, + 'created_at' => now(), + 'updated_at' => now(), + ], + [ + 'worker_id' => $workerId, + 'type' => 'visa', + 'number' => 'V' . rand(100000, 999999), + 'issue_date' => '2022-01-01', + 'expiry_date' => '2025-01-01', + 'ocr_accuracy' => 98.0, + 'created_at' => now(), + 'updated_at' => now(), + ] + ]); + + // Seed Skills (assuming skill IDs 1 and 2 exist) + \Illuminate\Support\Facades\DB::table('worker_skills')->insert([ + [ + 'worker_id' => $workerId, + 'skill_id' => 1, + 'created_at' => now(), + 'updated_at' => now(), + ], + [ + 'worker_id' => $workerId, + 'skill_id' => 2, + 'created_at' => now(), + 'updated_at' => now(), + ] + ]); + } + } +} diff --git a/resources/js/Layouts/EmployerLayout.jsx b/resources/js/Layouts/EmployerLayout.jsx index 3967a9f..31f9ab7 100644 --- a/resources/js/Layouts/EmployerLayout.jsx +++ b/resources/js/Layouts/EmployerLayout.jsx @@ -1,5 +1,6 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { Link, usePage } from '@inertiajs/react'; +import { toast } from 'sonner'; import { LayoutDashboard, Search, @@ -27,7 +28,23 @@ import { export default function EmployerLayout({ children, title, fullPage = false }) { const { url, props } = usePage(); - const { auth, unread_messages_count } = props; + const { auth, unread_messages_count, flash } = props; + + useEffect(() => { + if (flash?.success) { + toast.success(flash.success); + } + if (flash?.error) { + toast.error(flash.error); + } + if (flash?.first_login) { + // Elegant first login celebratory alert + toast.success('🎉 Welcome to Marketplace!', { + description: 'Your employer profile is verified and active with 30 days of Premium Access.', + duration: 6000, + }); + } + }, [flash]); const user = auth?.user || { name: 'John Doe', diff --git a/resources/js/Pages/Admin/Announcements/Index.jsx b/resources/js/Pages/Admin/Announcements/Index.jsx index 55ab872..d11b4b4 100644 --- a/resources/js/Pages/Admin/Announcements/Index.jsx +++ b/resources/js/Pages/Admin/Announcements/Index.jsx @@ -23,6 +23,7 @@ export default function Announcements() { const [isFormOpen, setIsFormOpen] = useState(false); const [newAnnouncement, setNewAnnouncement] = useState({ title: '', content: '', audience: 'Both' }); + const [errors, setErrors] = useState({}); const [toastMessage, setToastMessage] = useState(null); const showToast = (message) => { @@ -32,6 +33,15 @@ export default function Announcements() { const handleCreate = (e) => { e.preventDefault(); + let newErrors = {}; + if (!newAnnouncement.title.trim()) newErrors.title = 'Title is required'; + if (!newAnnouncement.content.trim()) newErrors.content = 'Content is required'; + + if (Object.keys(newErrors).length > 0) { + setErrors(newErrors); + return; + } + const newEntry = { id: announcements.length + 1, title: newAnnouncement.title, @@ -41,6 +51,7 @@ export default function Announcements() { }; setAnnouncements([newEntry, ...announcements]); setNewAnnouncement({ title: '', content: '', audience: 'Both' }); + setErrors({}); setIsFormOpen(false); showToast('Announcement sent successfully'); }; @@ -70,7 +81,7 @@ export default function Announcements() { + + + -
-
- - setNewAnnouncement({ ...newAnnouncement, title: e.target.value })} - className="w-full px-4 py-2.5 rounded-xl border border-slate-300 text-sm focus:ring-2 focus:ring-[#185FA5]/20 focus:border-[#185FA5] outline-none transition-all" - placeholder="Enter announcement title..." - /> -
-
- - -
-
- - -
-
- - -
-
)} @@ -165,7 +178,7 @@ export default function Announcements() { + + {errors.password && ( +

{errors.password[0] || errors.password}

+ )} + + + {/* Confirm Password input */} +
+ +
+ + handleInputChange('password_confirmation', e.target.value)} + placeholder="••••••••" + className={`w-full pl-12 pr-4 py-4 bg-slate-50 border rounded-2xl text-sm font-bold focus:ring-4 focus:ring-blue-100 focus:bg-white transition-all outline-none ${ + errors.password_confirmation ? 'border-rose-400 bg-rose-50/20' : 'border-transparent' + }`} + required + /> +
+ {errors.password_confirmation && ( +

{errors.password_confirmation[0] || errors.password_confirmation}

+ )} +
+ + {/* Password Rules Checklist */} +
+

Password Complexity Checklist

+
+ {rules.map((rule) => { + const isMet = strength[rule.key]; + return ( +
+
+ {isMet ? : null} +
+ {rule.text} +
+ ); + })} +
+
+ + + + + + + +
+ + + Cancel Registration + +
+ + + + ); +} diff --git a/resources/js/Pages/Employer/Auth/Register.jsx b/resources/js/Pages/Employer/Auth/Register.jsx index f085e68..01c69c8 100644 --- a/resources/js/Pages/Employer/Auth/Register.jsx +++ b/resources/js/Pages/Employer/Auth/Register.jsx @@ -1,505 +1,290 @@ import React, { useState } from 'react'; -import { useForm, Head, Link } from '@inertiajs/react'; +import { Head, Link, router } from '@inertiajs/react'; +import axios from 'axios'; +import { toast } from 'sonner'; import { - CheckCircle, - Camera, - Eye, - EyeOff, - X, - Loader2, - FileText, - AlertCircle, - CreditCard, - ShieldCheck, - ChevronRight, - Lock, - User, - Mail, - Phone as PhoneIcon, - ArrowRight, - ArrowLeft, - Check, - Briefcase + Loader2, + Users, + DollarSign, + MessageSquare } from 'lucide-react'; +const countries = [ + 'United Arab Emirates', + 'Saudi Arabia', + 'Qatar', + 'Oman', + 'Kuwait', + 'Bahrain', + 'United Kingdom', + 'United States', + 'Canada', + 'Australia', + 'Singapore' +]; + export default function Register() { - const [step, setStep] = useState(1); - const { data, setData, post, processing, errors } = useForm({ + const [data, setData] = useState({ + company_name: '', name: '', email: '', phone: '', - password: '', - password_confirmation: '', - emirates_id_front: null, - emirates_id_back: null, - selected_plan: 'premium', - card_number: '', - expiry: '', - cvc: '', + country: 'United Arab Emirates', }); - const [showPassword, setShowPassword] = useState(false); - const [frontFileName, setFrontFileName] = useState(null); - const [backFileName, setBackFileName] = useState(null); + const [loading, setLoading] = useState(false); + const [errors, setErrors] = useState({}); - const plans = [ - { id: 'basic', name: 'Basic Search', price: '99', features: ['Browse 500+ workers', 'Shortlist up to 10', 'Standard vetting'] }, - { id: 'premium', name: 'Premium Pass', price: '199', features: ['Unlimited shortlisting', 'Direct messaging', 'Priority scheduling'], popular: true }, - { id: 'vip', name: 'VIP Concierge', price: '499', features: ['Assigned manager', 'Medical guarantee', 'Free replacements'] }, - ]; - - const nextStep = () => setStep(prev => prev + 1); - const prevStep = () => setStep(prev => prev - 1); - - const handleFileChange = (e, field) => { - const file = e.target.files[0]; - if (file) { - setData(field, file); - const sizeMb = (file.size / (1024 * 1024)).toFixed(1); - if (field === 'emirates_id_front') { - setFrontFileName(`${file.name} (${sizeMb} MB)`); - } else { - setBackFileName(`${file.name} (${sizeMb} MB)`); - } + const handleInputChange = (field, value) => { + setData(prev => ({ ...prev, [field]: value })); + if (errors[field]) { + setErrors(prev => ({ ...prev, [field]: null })); } }; - const removeFile = (field) => { - setData(field, null); - if (field === 'emirates_id_front') { - setFrontFileName(null); - } else { - setBackFileName(null); - } - }; - - const handleSubmit = (e) => { + const handleSubmit = async (e) => { e.preventDefault(); - post('/employer/register'); + setLoading(true); + setErrors({}); + + try { + await axios.post('/employer/register', data); + toast.success('Registration details saved! A 6-digit verification code has been sent to your email.'); + router.visit('/employer/verify-email'); + } catch (err) { + if (err.response) { + if (err.response.status === 409) { + const errMsg = err.response.data?.errors?.email || 'This email address is already registered.'; + setErrors({ email: errMsg }); + toast.error(errMsg); + } else if (err.response.status === 422) { + setErrors(err.response.data.errors || {}); + toast.error('Validation failed. Please correct the fields in red.'); + } else { + toast.error('Something went wrong. Please try again.'); + } + } else { + toast.error('Network error. Please check your internet connection.'); + } + } finally { + setLoading(false); + } }; const renderStepIndicator = () => ( -
+
-
- {[1, 2, 3, 4].map((s) => ( -
-
= s ? 'bg-[#185FA5] text-white border-blue-100 scale-110 shadow-lg' : 'bg-white text-slate-400 border-slate-50' + {[ + { step: 1, label: 'Register' }, + { step: 2, label: 'Verify' }, + { step: 3, label: 'Password' } + ].map((s) => ( +
+
- {step > s ? : s} + {s.step}
+ {s.label}
))}
); return ( -
- +
+ - {/* Left Brand Panel */} -
-
-
+ {/* Left Brand Panel (Desktop Only) */} +
+
-
-
-
+
+
+
M
- Marketplace + Marketplace Portal
-
-

- The Secure Hub for Direct Hiring. +
+ + Employer Registration + +

+ Find verified domestic workers — no agents, no fees.

-

- Join 12,000+ employers hiring domestic workers directly with verified documentation. +

+ Register to review your shortlisted candidates, manage interview schedules, and communicate directly.

-
- {[ - { title: 'Verified Profiles', desc: 'Every worker is OCR vetted and legally checked.', icon: ShieldCheck }, - { title: 'Instant Access', desc: 'Connect with candidates in seconds after verification.', icon: ArrowRight }, - { title: 'Zero Fees', desc: 'No recruitment agency commissions, ever.', icon: CheckCircle } - ].map((feat, i) => ( -
-
- -
-
-

{feat.title}

-

{feat.desc}

-
-
- ))} + {/* Stat Row */} +
+
+ +
500+
+
Verified Workers
+
+ +
+ +
0 AED
+
Agent Fees
+
+ +
+ +
Direct
+
Messaging
+
-
- Empowering Household Recruitment Since 2024 +
+ © 2026 Domestic Worker Marketplace. All rights reserved.
- {/* Right Registration Content */} -
-
-
-

Employer Enrollment

-

Step {step} of 4: { - step === 1 ? 'Account Security' : - step === 2 ? 'Identity Verification' : - step === 3 ? 'Choose Your Plan' : 'Secure Payment' - }

+ {/* Right Registration Form */} +
+
+
+
+

Create Account

+

Register your employer portal to start searching

+
+ {renderStepIndicator()}
- {renderStepIndicator()} - -
-
- - {/* Step 1: Account Creation */} - {step === 1 && ( -
-
-
- -
- - setData('name', e.target.value)} - placeholder="e.g. Abdullah Bin Ahmed" - className="w-full pl-12 pr-4 py-4 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-4 focus:ring-blue-100 transition-all outline-none" - required - /> -
-
- -
- -
- - setData('email', e.target.value)} - placeholder="employer@domain.com" - className="w-full pl-12 pr-4 py-4 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-4 focus:ring-blue-100 transition-all outline-none" - required - /> -
-
- -
- -
- - setData('phone', e.target.value)} - placeholder="+971 XX XXX XXXX" - className="w-full pl-12 pr-4 py-4 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-4 focus:ring-blue-100 transition-all outline-none" - required - /> -
-
- -
- -
- - setData('password', e.target.value)} - placeholder="••••••••" - className="w-full pl-12 pr-12 py-4 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-4 focus:ring-blue-100 transition-all outline-none" - required - /> - -
-
- -
- - setData('password_confirmation', e.target.value)} - placeholder="••••••••" - className="w-full px-5 py-4 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-4 focus:ring-blue-100 transition-all outline-none" - required - /> -
-
- -
+ + {/* Company Name */} +
+ + handleInputChange('company_name', e.target.value)} + placeholder="e.g. Al Mansoor Household" + className={`w-full px-3.5 py-2.5 rounded-xl border text-sm focus:outline-none focus:ring-2 ${ + errors.company_name + ? 'border-red-500 focus:ring-red-500/20 focus:border-red-500' + : 'border-slate-300 focus:ring-[#185FA5]/20 focus:border-[#185FA5]' + }`} + required + /> + {errors.company_name && ( +

{errors.company_name[0] || errors.company_name}

)} +
- {/* Step 2: Emirates ID */} - {step === 2 && ( -
-
- -

- In compliance with UAE Labor Law, all employers must verify their identity using a valid Emirates ID before accessing the marketplace. -

-
+
+ {/* Contact Name */} +
+ + handleInputChange('name', e.target.value)} + placeholder="e.g. Abdullah Bin Ahmed" + className={`w-full px-3.5 py-2.5 rounded-xl border text-sm focus:outline-none focus:ring-2 ${ + errors.name + ? 'border-red-500 focus:ring-red-500/20 focus:border-red-500' + : 'border-slate-300 focus:ring-[#185FA5]/20 focus:border-[#185FA5]' + }`} + required + /> + {errors.name && ( +

{errors.name[0] || errors.name}

+ )} +
-
- {[ - { field: 'emirates_id_front', label: 'Emirates ID Front', file: frontFileName }, - { field: 'emirates_id_back', label: 'Emirates ID Back', file: backFileName } - ].map((slot) => ( -
- - {slot.file ? ( -
- - {slot.file} - -
- ) : ( - - )} -
- ))} -
+ {/* Work Email */} +
+ + handleInputChange('email', e.target.value)} + placeholder="employer@domain.com" + className={`w-full px-3.5 py-2.5 rounded-xl border text-sm focus:outline-none focus:ring-2 ${ + errors.email + ? 'border-red-500 focus:ring-red-500/20 focus:border-red-500' + : 'border-slate-300 focus:ring-[#185FA5]/20 focus:border-[#185FA5]' + }`} + required + /> + {errors.email && ( +

{errors.email[0] || errors.email}

+ )} +
+
-
- - -
-
+
+ {/* Mobile Number */} +
+ + handleInputChange('phone', e.target.value)} + placeholder="e.g. +971501234567" + className={`w-full px-3.5 py-2.5 rounded-xl border text-sm focus:outline-none focus:ring-2 ${ + errors.phone + ? 'border-red-500 focus:ring-red-500/20 focus:border-red-500' + : 'border-slate-300 focus:ring-[#185FA5]/20 focus:border-[#185FA5]' + }`} + required + /> + {errors.phone && ( +

{errors.phone[0] || errors.phone}

+ )} +
+ + {/* Country Dropdown */} +
+ + + {errors.country && ( +

{errors.country[0] || errors.country}

+ )} +
+
+ + +
- {/* Step 3: Plan Selection */} - {step === 3 && ( -
-
- {plans.map((p) => ( - - ))} -
- -
- - -
-
- )} - - {/* Step 4: Secure Payment */} - {step === 4 && ( -
-
-
-
-
-
-
- -
-
-
- {data.card_number || '•••• •••• •••• ••••'} -
-
-
-
Card Holder
-
{data.name || 'Your Name'}
-
-
-
Expires
-
{data.expiry || 'MM/YY'}
-
-
-
-
- -
-
- -
- - setData('card_number', e.target.value.replace(/\W/gi, '').replace(/(.{4})/g, '$1 ').trim())} - placeholder="4242 4242 4242 4242" - className="w-full pl-12 pr-4 py-4 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-4 focus:ring-blue-100 transition-all outline-none" - required - /> -
-
- -
-
- - setData('expiry', e.target.value.replace(/^(\d{2})/, '$1/'))} - placeholder="MM/YY" - className="w-full px-5 py-4 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-4 focus:ring-blue-100 transition-all outline-none text-center" - required - /> -
-
- - setData('cvc', e.target.value)} - placeholder="CVC" - className="w-full px-5 py-4 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-4 focus:ring-blue-100 transition-all outline-none text-center" - required - /> -
-
-
- -
-
-
Total Amount
-
{plans.find(p => p.id === data.selected_plan)?.price} AED
-
- -
- -
- - -
-

- Secured by 256-bit AES encryption -

-
- )} - - -
- -
-

- Already part of the network?{' '} - - Authorize Account +

+

+ Already have an account?{' '} + + Sign in

diff --git a/resources/js/Pages/Employer/Auth/VerifyEmail.jsx b/resources/js/Pages/Employer/Auth/VerifyEmail.jsx new file mode 100644 index 0000000..0ed8fca --- /dev/null +++ b/resources/js/Pages/Employer/Auth/VerifyEmail.jsx @@ -0,0 +1,263 @@ +import React, { useState, useEffect } from 'react'; +import { Head, Link, router } from '@inertiajs/react'; +import axios from 'axios'; +import { toast } from 'sonner'; +import { + ShieldCheck, + CheckCircle, + ArrowLeft, + MailCheck, + Loader2, + RefreshCw, + KeyRound +} from 'lucide-react'; + +export default function VerifyEmail({ email }) { + const [otp, setOtp] = useState(''); + const [loading, setLoading] = useState(false); + const [resending, setResending] = useState(false); + const [errors, setErrors] = useState({}); + + // Cooldown timer state for resending OTP (60s) + const [cooldown, setCooldown] = useState(60); + + useEffect(() => { + let timer; + if (cooldown > 0) { + timer = setTimeout(() => setCooldown(cooldown - 1), 1000); + } + return () => clearTimeout(timer); + }, [cooldown]); + + const handleOtpChange = (e) => { + const val = e.target.value.replace(/[^0-9]/g, '').slice(0, 6); + setOtp(val); + if (errors.otp) { + setErrors({}); + } + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + if (otp.length !== 6) { + setErrors({ otp: 'Please enter all 6 digits of the code.' }); + return; + } + + setLoading(true); + setErrors({}); + + try { + const response = await axios.post('/employer/verify-email', { otp }); + toast.success('Email verified successfully! Let\'s secure your account.'); + router.visit('/employer/create-password'); + } catch (err) { + if (err.response) { + if (err.response.status === 422 || err.response.status === 400) { + const otpErr = err.response.data?.errors?.otp || err.response.data?.message || 'Verification failed.'; + setErrors({ otp: otpErr }); + toast.error(otpErr); + } else if (err.response.status === 429) { + toast.error('Too many requests. Please slow down.'); + } else { + toast.error('Failed to verify OTP. Please try again.'); + } + } else { + toast.error('Network error. Please check your connection.'); + } + } finally { + setLoading(false); + } + }; + + const handleResend = async () => { + if (cooldown > 0 || resending) return; + + setResending(true); + try { + const response = await axios.post('/employer/resend-otp'); + toast.success('A new verification code has been dispatched to your email.'); + setCooldown(60); // Reset timer + setOtp(''); + } catch (err) { + if (err.response) { + toast.error(err.response.data?.message || 'Failed to dispatch verification code.'); + if (err.response.status === 429) { + setCooldown(60); // Reset cooldown on too many requests + } + } else { + toast.error('Network error. Please check your connection.'); + } + } finally { + setResending(false); + } + }; + + const renderStepIndicator = () => ( +
+
+
+ + {[ + { step: 1, label: 'Register' }, + { step: 2, label: 'Verify Code' }, + { step: 3, label: 'Set Password' } + ].map((s) => ( +
+
+ {s.step} +
+ {s.label} +
+ ))} +
+ ); + + return ( +
+ + + {/* Left Brand Panel */} +
+
+ +
+
+
+ M +
+ Marketplace +
+ +
+

+ Verify Your Business Email. +

+

+ We take platform trust seriously. Verify your email to ensure secure account setup. +

+
+
+ +
+ Empowering Household Recruitment Since 2024 +
+
+ + {/* Right Form Content */} +
+
+
+

Email Verification

+

+ Step 2 of 3: Enter Hashed Passcode +

+
+ + {renderStepIndicator()} + +
+ +
+
+ +
+

Check Your Email

+

+ We dispatched a 6-digit confirmation code to: +
+ {email} +

+
+ +
+ +
+ +
+ + +
+ {errors.otp && ( +

{errors.otp}

+ )} +
+ + + +
+ {cooldown > 0 ? ( +

+ Resend Code available in {cooldown}s +

+ ) : ( + + )} +
+ +
+ +
+ +
+ + + Change Email / Back + +
+
+
+
+ ); +} diff --git a/resources/js/Pages/Employer/Jobs/Create.jsx b/resources/js/Pages/Employer/Jobs/Create.jsx index 68d9ee4..77493a8 100644 --- a/resources/js/Pages/Employer/Jobs/Create.jsx +++ b/resources/js/Pages/Employer/Jobs/Create.jsx @@ -34,10 +34,7 @@ export default function Create({ categories: initialCategories }) { const handleSubmit = (e) => { e.preventDefault(); - // In a real app, we'd post to /employer/jobs - // post('/employer/jobs'); - alert('Job posted successfully! Redirecting to Job List...'); - window.location.href = '/employer/jobs'; + post('/employer/jobs/create'); }; return ( diff --git a/resources/js/Pages/Employer/Messages/Show.jsx b/resources/js/Pages/Employer/Messages/Show.jsx index 8f2bbf7..a739296 100644 --- a/resources/js/Pages/Employer/Messages/Show.jsx +++ b/resources/js/Pages/Employer/Messages/Show.jsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useRef } from 'react'; -import { Head, Link } from '@inertiajs/react'; +import { Head, Link, router } from '@inertiajs/react'; import EmployerLayout from '../../../Layouts/EmployerLayout'; import { Send, @@ -38,6 +38,10 @@ export default function Show({ conversation, initialMessages, conversations = [] scrollToBottom(); }, [messages, isTyping]); + useEffect(() => { + setMessages(initialMessages || []); + }, [initialMessages]); + const filteredConversations = (conversations || []).filter(c => c.worker_name.toLowerCase().includes(searchTerm.toLowerCase()) ); @@ -45,26 +49,14 @@ export default function Show({ conversation, initialMessages, conversations = [] const sendMsg = (text) => { if (!text.trim()) return; - const newMsg = { - id: Date.now(), - sender: 'employer', - text: text, - time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), - }; - - setMessages(prev => [...prev, newMsg]); - setInput(''); - setIsTyping(true); - - setTimeout(() => { - setIsTyping(false); - setMessages(prev => [...prev, { - id: Date.now() + 1, - sender: 'worker', - text: "Thank you for the message ma'am! I confirm I have received your request and look forward to speaking with you.", - time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), - }]); - }, 1500 + Math.random() * 1000); + router.post(`/employer/messages/${conversation.id}/send`, { + text: text + }, { + preserveScroll: true, + onSuccess: () => { + setInput(''); + } + }); }; const handleFormSubmit = (e) => { diff --git a/resources/js/Pages/Employer/Profile.jsx b/resources/js/Pages/Employer/Profile.jsx index 4c3666b..e536c9a 100644 --- a/resources/js/Pages/Employer/Profile.jsx +++ b/resources/js/Pages/Employer/Profile.jsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { Head } from '@inertiajs/react'; +import { Head, useForm } from '@inertiajs/react'; import EmployerLayout from '../../Layouts/EmployerLayout'; import { User, @@ -18,21 +18,30 @@ import { } from 'lucide-react'; export default function Profile({ employerProfile }) { - const [profile, setProfile] = useState(employerProfile || { - name: 'John Doe', - company_name: 'Al Mansoor Household', - email: 'employer@example.com', - phone: '+971 50 123 4567', - language: 'English', - notifications: true + const { data: profile, setData: setProfile, post, processing, errors } = useForm({ + name: employerProfile?.name || '', + company_name: employerProfile?.company_name || '', + email: employerProfile?.email || '', + phone: employerProfile?.phone || '', + language: employerProfile?.language || 'English', + notifications: employerProfile?.notifications ?? true, + current_password: '', + new_password: '', + new_password_confirmation: '', }); + const [activeTab, setActiveTab] = useState('personal'); const [saved, setSaved] = useState(false); const handleSave = (e) => { e.preventDefault(); - setSaved(true); - setTimeout(() => setSaved(false), 3000); + post('/employer/profile/update', { + preserveScroll: true, + onSuccess: () => { + setSaved(true); + setTimeout(() => setSaved(false), 3000); + } + }); }; const tabs = [ @@ -128,6 +137,7 @@ export default function Profile({ employerProfile }) { className="w-full pl-12 pr-4 py-3 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-2 focus:ring-blue-100 transition-all" />
+ {errors.name &&

{errors.name}

}
@@ -139,8 +149,9 @@ export default function Profile({ employerProfile }) { value={profile.email} onChange={(e) => setProfile({ ...profile, email: e.target.value })} className="w-full pl-12 pr-4 py-3 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-2 focus:ring-blue-100 transition-all" - /> -
+ /> +
+ {errors.email &&

{errors.email}

}
@@ -154,6 +165,7 @@ export default function Profile({ employerProfile }) { className="w-full pl-12 pr-4 py-3 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-2 focus:ring-blue-100 transition-all" />
+ {errors.phone &&

{errors.phone}

}
@@ -169,6 +181,7 @@ export default function Profile({ employerProfile }) {
+ {errors.language &&

{errors.language}

}
@@ -192,6 +205,7 @@ export default function Profile({ employerProfile }) { className="w-full pl-12 pr-4 py-3 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-2 focus:ring-blue-100 transition-all" />
+ {errors.company_name &&

{errors.company_name}

}
@@ -223,15 +237,36 @@ export default function Profile({ employerProfile }) {
- + setProfile({ ...profile, current_password: e.target.value })} + className="w-full px-4 py-3 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-2 focus:ring-blue-100 transition-all" + /> + {errors.current_password &&

{errors.current_password}

}
- + setProfile({ ...profile, new_password: e.target.value })} + className="w-full px-4 py-3 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-2 focus:ring-blue-100 transition-all" + /> + {errors.new_password &&

{errors.new_password}

}
- + setProfile({ ...profile, new_password_confirmation: e.target.value })} + className="w-full px-4 py-3 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-2 focus:ring-blue-100 transition-all" + /> + {errors.new_password_confirmation &&

{errors.new_password_confirmation}

}
diff --git a/resources/js/Pages/Employer/SelectedCandidates.jsx b/resources/js/Pages/Employer/SelectedCandidates.jsx index ccf88a4..78c5f7a 100644 --- a/resources/js/Pages/Employer/SelectedCandidates.jsx +++ b/resources/js/Pages/Employer/SelectedCandidates.jsx @@ -1,5 +1,5 @@ -import React, { useState } from 'react'; -import { Head, Link } from '@inertiajs/react'; +import React, { useState, useEffect } from 'react'; +import { Head, Link, router } from '@inertiajs/react'; import EmployerLayout from '../../Layouts/EmployerLayout'; import { CheckCircle2, @@ -34,6 +34,20 @@ export default function SelectedCandidates({ selectedWorkers }) { const [workers, setWorkers] = useState((selectedWorkers || []).filter(w => w.status !== 'Searching')); const [searchTerm, setSearchTerm] = useState(''); + useEffect(() => { + setWorkers((selectedWorkers || []).filter(w => w.status !== 'Searching')); + }, [selectedWorkers]); + + const changeStatus = (id, newStatus) => { + // Optimistic UI state update + setWorkers(workers.map(w => w.id === id ? { ...w, status: newStatus } : w)); + + // Persist change to database + router.post(`/employer/candidates/${id}/status`, { status: newStatus }, { + preserveScroll: true + }); + }; + const stats = [ { label: 'Total Candidates', @@ -184,17 +198,17 @@ export default function SelectedCandidates({ selectedWorkers }) { Change Status - + changeStatus(worker.id, 'Reviewing')} className="text-xs font-bold text-slate-600 focus:text-amber-700 focus:bg-amber-50 rounded-lg cursor-pointer"> Reviewing - + changeStatus(worker.id, 'Offer Sent')} className="text-xs font-bold text-slate-600 focus:text-blue-700 focus:bg-blue-50 rounded-lg cursor-pointer"> Offer Sent - + changeStatus(worker.id, 'Hired')} className="text-xs font-bold text-slate-600 focus:text-emerald-700 focus:bg-emerald-50 rounded-lg cursor-pointer"> Hired - + changeStatus(worker.id, 'Rejected')} className="text-xs font-bold text-rose-600 focus:text-rose-700 focus:bg-rose-50 rounded-lg cursor-pointer"> Rejected diff --git a/resources/js/Pages/Employer/Shortlist.jsx b/resources/js/Pages/Employer/Shortlist.jsx index 484e251..457de66 100644 --- a/resources/js/Pages/Employer/Shortlist.jsx +++ b/resources/js/Pages/Employer/Shortlist.jsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { Head, Link } from '@inertiajs/react'; +import { Head, Link, router } from '@inertiajs/react'; import EmployerLayout from '../../Layouts/EmployerLayout'; import { Bookmark, @@ -16,7 +16,13 @@ export default function Shortlist({ shortlistedWorkers }) { const [workers, setWorkers] = useState(shortlistedWorkers || []); const removeWorker = (id) => { + // Optimistic UI state update setWorkers(workers.filter(w => w.id !== id)); + + // Persist change to database + router.post(`/employer/shortlist/${id}/remove`, {}, { + preserveScroll: true + }); }; return ( diff --git a/resources/js/Pages/Employer/Workers/Index.jsx b/resources/js/Pages/Employer/Workers/Index.jsx index 739904d..a5cbbb7 100644 --- a/resources/js/Pages/Employer/Workers/Index.jsx +++ b/resources/js/Pages/Employer/Workers/Index.jsx @@ -1,5 +1,5 @@ import React, { useState, useMemo } from 'react'; -import { Head, Link } from '@inertiajs/react'; +import { Head, Link, router } from '@inertiajs/react'; import EmployerLayout from '../../../Layouts/EmployerLayout'; import { Search, @@ -17,7 +17,7 @@ import { Filter } from 'lucide-react'; -export default function Index({ initialWorkers, filtersMetadata }) { +export default function Index({ initialWorkers, initialShortlistedIds, filtersMetadata }) { const [searchQuery, setSearchQuery] = useState(''); const [selectedCategory, setSelectedCategory] = useState('All Categories'); const [selectedNationality, setSelectedNationality] = useState('All Nationalities'); @@ -25,14 +25,25 @@ export default function Index({ initialWorkers, filtersMetadata }) { const [selectedExperience, setSelectedExperience] = useState('All Experience'); const [selectedReligion, setSelectedReligion] = useState('All Religions'); const [maxSalary, setMaxSalary] = useState(3000); - const [shortlistedIds, setShortlistedIds] = useState([101, 103]); + const [shortlistedIds, setShortlistedIds] = useState(initialShortlistedIds || []); const toggleShortlist = (id) => { + // Optimistic UI state update if (shortlistedIds.includes(id)) { setShortlistedIds(shortlistedIds.filter(i => i !== id)); } else { setShortlistedIds([...shortlistedIds, id]); } + + // Persist change to database + router.post('/employer/shortlist/toggle', { worker_id: id }, { + preserveScroll: true, + onSuccess: (page) => { + if (page.props.initialShortlistedIds) { + setShortlistedIds(page.props.initialShortlistedIds); + } + } + }); }; const resetFilters = () => { diff --git a/resources/js/Pages/Mobile/EmployerAnnouncements.jsx b/resources/js/Pages/Mobile/EmployerAnnouncements.jsx deleted file mode 100644 index 588e9a5..0000000 --- a/resources/js/Pages/Mobile/EmployerAnnouncements.jsx +++ /dev/null @@ -1,90 +0,0 @@ -import React, { useState } from 'react'; -import { Head, Link } from '@inertiajs/react'; -import EmployerMobileLayout from './Layouts/EmployerMobileLayout'; -import { - Megaphone, - Calendar, - ArrowRight, - Search, - Plus, - X -} from 'lucide-react'; - -export default function EmployerAnnouncements() { - const [searchTerm, setSearchTerm] = useState(''); - - const announcements = [ - { id: 1, title: 'Ramadan Working Hours Update', date: 'May 12, 2026', content: 'Please note the revised working hours for all domestic staff during the upcoming Holy Month of Ramadan...', priority: 'High' }, - { id: 2, title: 'New Visa Regulations 2026', date: 'May 08, 2026', content: 'The Ministry of Human Resources has announced new updates regarding domestic worker visa renewals...', priority: 'Normal' }, - { id: 3, title: 'Marketplace Service Maintenance', date: 'May 05, 2026', content: 'Our platform will undergo scheduled maintenance on Sunday from 2:00 AM to 4:00 AM...', priority: 'Low' }, - ]; - - return ( - - - -
- {/* Header */} -
-
-

Broadcasts

-

Important updates & announcements

-
- -
- - {/* Search */} -
- - setSearchTerm(e.target.value)} - className="w-full pl-12 pr-4 py-4 bg-white border border-slate-200 rounded-2xl text-sm focus:ring-4 focus:ring-slate-100 focus:border-slate-300 outline-none transition-all" - /> -
- - {/* Announcement List */} -
- {announcements.map((item) => ( -
-
-
-
- -
-
-

{item.title}

-
- - {item.date} -
-
-
-
- -

- {item.content} -

- -
-
- {item.priority} Priority -
- -
-
- ))} -
-
-
- ); -} diff --git a/resources/js/Pages/Mobile/EmployerCandidates.jsx b/resources/js/Pages/Mobile/EmployerCandidates.jsx deleted file mode 100644 index 9555fc4..0000000 --- a/resources/js/Pages/Mobile/EmployerCandidates.jsx +++ /dev/null @@ -1,135 +0,0 @@ -import React, { useState } from 'react'; -import { Head, Link } from '@inertiajs/react'; -import EmployerMobileLayout from './Layouts/EmployerMobileLayout'; -import { - Users, - Clock, - CheckCircle2, - XCircle, - Search, - Filter, - MessageSquare, - MoreHorizontal -} from 'lucide-react'; - -export default function EmployerCandidates() { - const [searchQuery, setSearchQuery] = useState(''); - - const stats = [ - { label: 'Total Candidates', value: 5, icon: Users, color: 'bg-blue-50 text-blue-600' }, - { label: 'Reviewing', value: 3, icon: Clock, color: 'bg-orange-50 text-orange-500' }, - { label: 'Hired', value: 1, icon: CheckCircle2, color: 'bg-emerald-50 text-emerald-600' }, - { label: 'Rejected', value: 1, icon: XCircle, color: 'bg-rose-50 text-rose-500' }, - ]; - - const candidates = [ - { id: 1, name: 'Sarah Connor', country: 'Philippines', role: 'Site Supervisor', salary: '4,500 AED', status: 'Reviewing' }, - { id: 2, name: 'Maria Garcia', country: 'Kenya', role: 'Housemaid', salary: '2,500 AED', status: 'Hired' }, - { id: 3, name: 'Elena Gilbert', country: 'Indonesia', role: 'Nanny', salary: '3,000 AED', status: 'Rejected' }, - ]; - - const getStatusStyles = (status) => { - switch (status) { - case 'Reviewing': return 'bg-orange-50 text-orange-600 border-orange-100'; - case 'Hired': return 'bg-emerald-50 text-emerald-600 border-emerald-100'; - case 'Rejected': return 'bg-rose-50 text-rose-600 border-rose-100'; - default: return 'bg-slate-50 text-slate-600 border-slate-100'; - } - }; - - const getStatusDot = (status) => { - switch (status) { - case 'Reviewing': return 'bg-orange-500'; - case 'Hired': return 'bg-emerald-500'; - case 'Rejected': return 'bg-rose-500'; - default: return 'bg-slate-500'; - } - }; - - return ( - - - -
- {/* Page Title */} -
-

Candidates

-

Manage your workforce recruitment pipeline

-
- - {/* Stats Grid 2x2 */} -
- {stats.map((stat, i) => ( -
-
- -
-
-
{stat.value}
-
{stat.label}
-
-
- ))} -
- - {/* Search & Filter */} -
-
- - setSearchQuery(e.target.value)} - className="w-full pl-12 pr-4 py-4 bg-white border border-slate-200 rounded-2xl text-sm focus:ring-4 focus:ring-slate-100 focus:border-slate-300 outline-none transition-all" - /> -
- -
- - {/* Candidate List */} -
- {candidates.map((candidate) => ( -
-
-
-
- {candidate.name.charAt(0)} -
-
-

{candidate.name}

-
{candidate.country}
-
-
-
- - -
-
- -
-
- - {candidate.role} - -
{candidate.salary}
-
- -
-
- {candidate.status} -
-
-
- ))} -
-
- - ); -} diff --git a/resources/js/Pages/Mobile/EmployerChatDetail.jsx b/resources/js/Pages/Mobile/EmployerChatDetail.jsx deleted file mode 100644 index 7af6684..0000000 --- a/resources/js/Pages/Mobile/EmployerChatDetail.jsx +++ /dev/null @@ -1,147 +0,0 @@ -import React, { useState, useRef, useEffect } from 'react'; -import { Head, Link } from '@inertiajs/react'; -import { - ChevronLeft, - MoreVertical, - Send, - Paperclip, - Image as ImageIcon, - Smile, - CheckCheck, - Mic -} from 'lucide-react'; - -export default function EmployerChatDetail({ id }) { - const [message, setMessage] = useState(''); - const [messages, setMessages] = useState([ - { id: 1, sender: 'them', text: 'Hello! Is the site supervisor position still available?', time: '10:30 AM' }, - { id: 2, sender: 'me', text: 'Hi Sarah! Yes, it is. We are currently reviewing applications.', time: '10:32 AM' }, - { id: 3, sender: 'them', text: 'Great! I have over 8 years of experience in Dubai. Would you like me to send my CV?', time: '10:35 AM' }, - { id: 4, sender: 'me', text: 'That would be excellent. Please send it over here.', time: '10:36 AM' }, - { id: 5, sender: 'them', text: 'Sure, here it is. I also have pediatric first aid certification.', time: '10:40 AM' }, - ]); - - const messagesEndRef = useRef(null); - - const scrollToBottom = () => { - messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); - }; - - useEffect(() => { - scrollToBottom(); - }, [messages]); - - const handleSend = (e) => { - if (e) e.preventDefault(); - if (!message.trim()) return; - - const newMessage = { - id: messages.length + 1, - sender: 'me', - text: message, - time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) - }; - - setMessages([...messages, newMessage]); - setMessage(''); - }; - - // Mock data for the specific chat - const chatInfo = { - id: id, - name: 'Sarah Connor', - initial: 'S', - online: true, - role: 'Site Supervisor' - }; - - return ( -
- - - {/* Chat Header */} -
-
- -
-
-
- {chatInfo.initial} -
- {chatInfo.online && ( -
- )} -
-
-

{chatInfo.name}

-

Online Now

-
-
-
-
- -
-
- - {/* Messages Area */} -
-
- Today -
- - {messages.map((msg) => ( -
-
-
- {msg.text} -
-
- {msg.time} - {msg.sender === 'me' && } -
-
-
- ))} -
-
- - {/* Input Area */} -
-
- - setMessage(e.target.value)} - className="flex-1 bg-transparent border-none text-sm font-medium focus:ring-0 placeholder-slate-300 text-slate-700 h-12" - /> -
- - {message.trim() ? ( - - ) : ( - - )} -
-
-
-
- ); -} diff --git a/resources/js/Pages/Mobile/EmployerForgotPassword.jsx b/resources/js/Pages/Mobile/EmployerForgotPassword.jsx deleted file mode 100644 index a06a6ca..0000000 --- a/resources/js/Pages/Mobile/EmployerForgotPassword.jsx +++ /dev/null @@ -1,100 +0,0 @@ -import React, { useState } from 'react'; -import { Head, Link } from '@inertiajs/react'; -import { - ArrowLeft, - Loader2, - Mail, - ArrowRight, - CheckCircle2 -} from 'lucide-react'; - -export default function EmployerForgotPassword() { - const [email, setEmail] = useState(''); - const [isSent, setIsSent] = useState(false); - const [processing, setProcessing] = useState(false); - - const handleSubmit = (e) => { - e.preventDefault(); - setProcessing(true); - setTimeout(() => { - setIsSent(true); - setProcessing(false); - }, 1500); - }; - - return ( -
- - -
- -
- -
- {!isSent ? ( -
-
-

Forgot password

-

No worries, we'll send you reset instructions

-
- -
-
- -
- - setEmail(e.target.value)} - placeholder="Enter your email" - className="w-full pl-12 pr-4 py-4 bg-slate-50 border border-slate-200 rounded-2xl text-sm focus:bg-white focus:ring-2 focus:ring-[#185FA5]/10 focus:border-[#185FA5] outline-none transition-all" - required - /> -
-
- - -
-
- ) : ( -
-
-
- -
-
-
-

Check your email

-

- We've sent a password reset link to
- {email} -

-
-
- - Back to login - -
-
- )} -
-
- ); -} diff --git a/resources/js/Pages/Mobile/EmployerHome.jsx b/resources/js/Pages/Mobile/EmployerHome.jsx deleted file mode 100644 index 97a805d..0000000 --- a/resources/js/Pages/Mobile/EmployerHome.jsx +++ /dev/null @@ -1,89 +0,0 @@ -import React from 'react'; -import { Head, Link } from '@inertiajs/react'; -import EmployerMobileLayout from './Layouts/EmployerMobileLayout'; -import { - Bookmark, - MessageSquare, - Calendar, - CreditCard, - ArrowRight, - TrendingUp, - Users -} from 'lucide-react'; - -export default function EmployerHome() { - const stats = [ - { label: 'Shortlisted', value: 4, icon: Bookmark, color: 'bg-blue-50 text-blue-600' }, - { label: 'Messages Sent', value: 18, icon: MessageSquare, color: 'bg-emerald-50 text-emerald-600' }, - { label: 'Days Remaining', value: 24, icon: Calendar, color: 'bg-amber-50 text-amber-500' }, - { label: 'Total Hires', value: 12, icon: Users, color: 'bg-indigo-50 text-indigo-600' }, - ]; - - return ( - - - -
- {/* Welcome Section */} -
-

Dashboard

-

Welcome back, John Doe

-
- - {/* Plan Status Card */} -
-
-
-
-
- Premium Employer Pass -
-
-

24 Days

-

Remaining in your plan

-
- - Upgrade Plan - - -
-
- - {/* Stats Stacked */} -
-

Key Statistics

-
- {stats.map((stat, i) => ( -
-
-
- -
-
-
{stat.label}
-
{stat.value}
-
-
- -
- ))} -
-
- - {/* Placeholder for Analytics Chart (Full Width) */} -
-

Recruitment Analytics

-
-
- -
-
-

Activity Chart

-

Visualizing your hiring trends over 30 days

-
-
-
-
- - ); -} diff --git a/resources/js/Pages/Mobile/EmployerLogin.jsx b/resources/js/Pages/Mobile/EmployerLogin.jsx deleted file mode 100644 index 70bdd5d..0000000 --- a/resources/js/Pages/Mobile/EmployerLogin.jsx +++ /dev/null @@ -1,111 +0,0 @@ -import React, { useState } from 'react'; -import { useForm, Head, Link } from '@inertiajs/react'; -import { - ArrowLeft, - Eye, - EyeOff, - Loader2, - Mail, - Lock -} from 'lucide-react'; - -export default function EmployerLogin() { - const { data, setData, post, processing, errors } = useForm({ - email: '', - password: '', - remember: false, - source: 'mobile', - }); - - const [showPassword, setShowPassword] = useState(false); - - const handleSubmit = (e) => { - e.preventDefault(); - post('/employer/login'); - }; - - return ( -
- - - {/* Header */} -
- -
- -
- {/* Title */} -
-

Welcome back

-

Please enter your details to sign in

-
- - {/* Login Form */} -
-
-
- -
- - setData('email', e.target.value)} - placeholder="Enter your email" - className="w-full pl-12 pr-4 py-4 bg-slate-50 border border-slate-200 rounded-2xl text-sm focus:bg-white focus:ring-2 focus:ring-[#185FA5]/10 focus:border-[#185FA5] transition-all outline-none" - required - /> -
- {errors.email &&

{errors.email}

} -
- -
-
- - Forgot? -
-
- - setData('password', e.target.value)} - placeholder="••••••••" - className="w-full pl-12 pr-12 py-4 bg-slate-50 border border-slate-200 rounded-2xl text-sm focus:bg-white focus:ring-2 focus:ring-[#185FA5]/10 focus:border-[#185FA5] transition-all outline-none" - required - /> - -
-
-
- - -
- - {/* Footer */} -
-

- Don't have an account?{' '} - - Sign up - -

-
-
-
- ); -} diff --git a/resources/js/Pages/Mobile/EmployerMessages.jsx b/resources/js/Pages/Mobile/EmployerMessages.jsx deleted file mode 100644 index bc6c850..0000000 --- a/resources/js/Pages/Mobile/EmployerMessages.jsx +++ /dev/null @@ -1,79 +0,0 @@ -import React, { useState } from 'react'; -import { Head, Link } from '@inertiajs/react'; -import EmployerMobileLayout from './Layouts/EmployerMobileLayout'; -import { Search, MoreHorizontal, CheckCheck } from 'lucide-react'; - -export default function EmployerMessages() { - const [searchQuery, setSearchQuery] = useState(''); - - const chats = [ - { id: 1, name: 'Sarah Connor', preview: 'Is the site supervisor position still available?', time: '2m ago', unread: 2, online: true }, - { id: 2, name: 'Marketplace Support', preview: 'Your account verification is complete.', time: '1h ago', unread: 0, online: true }, - { id: 3, name: 'Maria Garcia', preview: 'I can start from next Monday. Looking forward to it!', time: '3h ago', unread: 0, online: false }, - { id: 4, name: 'Elena Gilbert', preview: 'Thank you for the opportunity.', time: 'Yesterday', unread: 0, online: false }, - { id: 5, name: 'John Smith', preview: 'Attached are my certificates for review.', time: 'Yesterday', unread: 0, online: false }, - ]; - - return ( - - - -
- {/* Search Bar */} -
-
- - setSearchQuery(e.target.value)} - className="w-full pl-12 pr-4 py-4 bg-white border border-slate-200 rounded-2xl text-sm focus:ring-4 focus:ring-slate-100 focus:border-slate-300 outline-none transition-all" - /> -
-
- - {/* Chat List */} -
-
- {chats.map((chat) => ( - -
-
- {chat.name.charAt(0)} -
- {chat.online && ( -
- )} -
- -
-
-

{chat.name}

- {chat.time} -
-
-

0 ? 'text-slate-900 font-bold' : 'text-slate-500 font-medium'}`}> - {chat.preview} -

- {chat.unread > 0 ? ( -
- {chat.unread} -
- ) : ( - - )} -
-
- - ))} -
-
-
- - ); -} diff --git a/resources/js/Pages/Mobile/EmployerOTP.jsx b/resources/js/Pages/Mobile/EmployerOTP.jsx deleted file mode 100644 index 6d710aa..0000000 --- a/resources/js/Pages/Mobile/EmployerOTP.jsx +++ /dev/null @@ -1,96 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { Head, Link } from '@inertiajs/react'; -import { - ArrowLeft, - Loader2, - Smartphone -} from 'lucide-react'; - -export default function EmployerOTP() { - const [otp, setOtp] = useState(['', '', '', '']); - const [timer, setTimer] = useState(59); - const [processing, setProcessing] = useState(false); - - useEffect(() => { - const interval = setInterval(() => { - if (timer > 0) setTimer(timer - 1); - }, 1000); - return () => clearInterval(interval); - }, [timer]); - - const handleChange = (index, value) => { - if (value.length <= 1 && /^\d*$/.test(value)) { - const newOtp = [...otp]; - newOtp[index] = value; - setOtp(newOtp); - - if (value && index < 3) { - document.getElementById(`otp-${index + 1}`).focus(); - } - } - }; - - const handleSubmit = (e) => { - e.preventDefault(); - setProcessing(true); - setTimeout(() => { - window.location.href = '/mobile/employer/home'; - }, 1500); - }; - - return ( -
- - -
- -
- -
-
-

Verify email

-

Enter the code sent to your email address

-
- -
-
- {otp.map((digit, i) => ( - handleChange(i, e.target.value)} - className="w-16 h-18 bg-slate-50 border border-slate-200 rounded-2xl text-center text-2xl font-bold text-slate-900 focus:bg-white focus:ring-2 focus:ring-[#185FA5]/10 focus:border-[#185FA5] outline-none transition-all" - maxLength={1} - required - /> - ))} -
- -
- - -
-

- Didn't receive code? {timer > 0 ? ( - 00:{timer < 10 ? `0${timer}` : timer} - ) : ( - - )} -

-
-
-
-
-
- ); -} diff --git a/resources/js/Pages/Mobile/EmployerProfile.jsx b/resources/js/Pages/Mobile/EmployerProfile.jsx deleted file mode 100644 index ef9dd11..0000000 --- a/resources/js/Pages/Mobile/EmployerProfile.jsx +++ /dev/null @@ -1,135 +0,0 @@ -import React from 'react'; -import { Head, Link } from '@inertiajs/react'; -import EmployerMobileLayout from './Layouts/EmployerMobileLayout'; -import { - User, - Settings, - CreditCard, - Shield, - Bell, - HelpCircle, - LogOut, - ChevronRight, - Camera, - CheckCircle2, - Briefcase, - Building2, - Smartphone, - Globe -} from 'lucide-react'; - -export default function EmployerProfile() { - const sections = [ - { - title: 'Account Settings', - items: [ - { label: 'Personal Information', icon: User, href: '/mobile/employer/profile/edit', color: 'text-blue-500 bg-blue-50' }, - { label: 'Subscription Plan', icon: CreditCard, href: '/mobile/employer/subscription', badge: 'Premium', color: 'text-indigo-500 bg-indigo-50' }, - { label: 'Security & Password', icon: Shield, href: '/mobile/employer/profile/security', color: 'text-emerald-500 bg-emerald-50' }, - ] - }, - { - title: 'Preferences', - items: [ - { label: 'Notification Settings', icon: Bell, href: '/mobile/employer/profile/notifications', color: 'text-orange-500 bg-orange-50' }, - { label: 'Language', icon: Globe, href: '#', detail: 'English (US)', color: 'text-purple-500 bg-purple-50' }, - ] - }, - { - title: 'Support', - items: [ - { label: 'Help & Support', icon: HelpCircle, href: '/mobile/employer/support', color: 'text-slate-500 bg-slate-50' }, - ] - } - ]; - - return ( - - - -
- {/* Profile Hero Card */} -
-
- -
-
- JD -
- -
- -
-
-

John Doe

- -
-
- - Al Mansoor Household -
-
- -
-
-
Plan
-
Premium
-
-
-
Verified
-
Yes
-
-
-
- - {/* Settings Menu Sections */} -
- {sections.map((section, idx) => ( -
-

{section.title}

-
- {section.items.map((item, i) => ( - -
-
- -
- {item.label} -
-
- {item.badge && ( - - {item.badge} - - )} - {item.detail && ( - {item.detail} - )} - -
- - ))} -
-
- ))} - - - -
-

Version 2.4.1 (Stable Build)

-
-
-
- - ); -} diff --git a/resources/js/Pages/Mobile/EmployerRegister.jsx b/resources/js/Pages/Mobile/EmployerRegister.jsx deleted file mode 100644 index 0ae4a62..0000000 --- a/resources/js/Pages/Mobile/EmployerRegister.jsx +++ /dev/null @@ -1,376 +0,0 @@ -import React, { useState } from 'react'; -import { useForm, Head, Link } from '@inertiajs/react'; -import { - CheckCircle, - Camera, - Eye, - EyeOff, - X, - Loader2, - FileText, - AlertCircle, - CreditCard, - ShieldCheck, - Lock, - User, - Mail, - Phone as PhoneIcon, - ArrowRight, - ArrowLeft, - Check, - Globe, - ChevronLeft, - ChevronRight, - Zap, - Shield, - Trophy -} from 'lucide-react'; - -export default function EmployerRegister() { - const [step, setStep] = useState(1); - const { data, setData, post, processing, errors } = useForm({ - name: '', - email: '', - phone: '', - password: '', - password_confirmation: '', - emirates_id_front: null, - emirates_id_back: null, - selected_plan: 'premium', - card_number: '', - expiry: '', - cvc: '', - source: 'mobile', - }); - - const [showPassword, setShowPassword] = useState(false); - const [frontFileName, setFrontFileName] = useState(null); - const [backFileName, setBackFileName] = useState(null); - - const plans = [ - { id: 'basic', name: 'Basic Search', price: '99', icon: Zap, color: 'text-blue-500 bg-blue-50', features: ['Browse 500+ workers', '10 Shortlists'] }, - { id: 'premium', name: 'Premium Pass', price: '199', icon: Shield, color: 'text-[#185FA5] bg-blue-50', popular: true, features: ['Unlimited lists', 'Direct Messaging'] }, - { id: 'vip', name: 'VIP Concierge', price: '499', icon: Trophy, color: 'text-amber-500 bg-amber-50', features: ['Assigned Manager', 'Full Warranty'] }, - ]; - - const nextStep = () => setStep(prev => prev + 1); - const prevStep = () => setStep(prev => prev - 1); - - const handleFileChange = (e, field) => { - const file = e.target.files[0]; - if (file) { - setData(field, file); - const sizeMb = (file.size / (1024 * 1024)).toFixed(1); - if (field === 'emirates_id_front') { - setFrontFileName(`${file.name} (${sizeMb}MB)`); - } else { - setBackFileName(`${file.name} (${sizeMb}MB)`); - } - } - }; - - const removeFile = (field) => { - setData(field, null); - if (field === 'emirates_id_front') { - setFrontFileName(null); - } else { - setBackFileName(null); - } - }; - - const handleSubmit = (e) => { - e.preventDefault(); - post('/employer/register'); - }; - - return ( -
- - - {/* Minimal Header */} -
- -
-
- Step {step} / 4 -
-
- -
- {/* Header Text */} -
-

- {step === 1 && "Create Account"} - {step === 2 && "Identity Vetting"} - {step === 3 && "Choose Plan"} - {step === 4 && "Checkout"} -

-

- {step === 1 && "Start your professional hiring journey."} - {step === 2 && "Verified ID is mandatory for compliance."} - {step === 3 && "Select a package that fits your needs."} - {step === 4 && "Secure payment via encrypted gateway."} -

-
- -
- {/* Step 1: Account Info */} - {step === 1 && ( -
-
- - setData('name', e.target.value)} - placeholder="Enter your full name" - className="w-full px-6 py-4 bg-slate-50 border border-slate-100 rounded-[24px] text-sm font-medium focus:bg-white focus:border-[#185FA5] focus:ring-4 focus:ring-blue-500/10 transition-all outline-none" - required - /> -
-
- - setData('email', e.target.value)} - placeholder="your@email.com" - className="w-full px-6 py-4 bg-slate-50 border border-slate-100 rounded-[24px] text-sm font-medium focus:bg-white focus:border-[#185FA5] focus:ring-4 focus:ring-blue-500/10 transition-all outline-none" - required - /> -
-
- - setData('phone', e.target.value)} - placeholder="+971 50 000 0000" - className="w-full px-6 py-4 bg-slate-50 border border-slate-100 rounded-[24px] text-sm font-medium focus:bg-white focus:border-[#185FA5] focus:ring-4 focus:ring-blue-500/10 transition-all outline-none" - required - /> -
-
- -
- setData('password', e.target.value)} - placeholder="Min. 8 characters" - className="w-full px-6 py-4 bg-slate-50 border border-slate-100 rounded-[24px] text-sm font-medium focus:bg-white focus:border-[#185FA5] focus:ring-4 focus:ring-blue-500/10 transition-all outline-none" - required - /> - -
-
- -
- )} - - {/* Step 2: Emirates ID */} - {step === 2 && ( -
-
- -

- Your ID is encrypted and handled securely as per UAE data protection laws. -

-
- -
- {[ - { field: 'emirates_id_front', label: 'EID Front Side', file: frontFileName }, - { field: 'emirates_id_back', label: 'EID Back Side', file: backFileName } - ].map((slot) => ( -
- - {slot.file ? ( -
- - {slot.file} - -
- ) : ( - - )} -
- ))} -
- - -
- )} - - {/* Step 3: Plans */} - {step === 3 && ( -
- {plans.map((p) => ( - - ))} - -
- )} - - {/* Step 4: Payment */} - {step === 4 && ( -
- {/* Card Visual */} -
-
-
-
- -
-
-
- {data.card_number || '•••• •••• •••• ••••'} -
-
-
-
Card Holder
-
{data.name || 'Your Name'}
-
-
-
Expiry
-
{data.expiry || 'MM/YY'}
-
-
-
-
- -
- setData('card_number', e.target.value.replace(/\W/gi, '').replace(/(.{4})/g, '$1 ').trim())} - placeholder="Card Number" - className="w-full px-6 py-4 bg-slate-50 border border-slate-100 rounded-[24px] text-sm font-medium focus:bg-white focus:border-[#185FA5] transition-all outline-none" - required - /> -
- setData('expiry', e.target.value.replace(/^(\d{2})/, '$1/'))} - placeholder="MM/YY" - className="w-full px-6 py-4 bg-slate-50 border border-slate-100 rounded-[24px] text-sm font-medium focus:bg-white focus:border-[#185FA5] transition-all outline-none text-center" - required - /> - setData('cvc', e.target.value)} - placeholder="CVC" - className="w-full px-6 py-4 bg-slate-50 border border-slate-100 rounded-[24px] text-sm font-medium focus:bg-white focus:border-[#185FA5] transition-all outline-none text-center" - required - /> -
-
- -
-
-
Total to Pay
-
{plans.find(p => p.id === data.selected_plan)?.price} AED
-
-
- -
-
- - -
- )} - - -
-

- Already joined? Sign In -

-
-
-
- ); -} diff --git a/resources/js/Pages/Mobile/EmployerShortlist.jsx b/resources/js/Pages/Mobile/EmployerShortlist.jsx deleted file mode 100644 index 50fffdf..0000000 --- a/resources/js/Pages/Mobile/EmployerShortlist.jsx +++ /dev/null @@ -1,109 +0,0 @@ -import React from 'react'; -import { Head, Link } from '@inertiajs/react'; -import EmployerMobileLayout from './Layouts/EmployerMobileLayout'; -import { - Bookmark, - MessageSquare, - X, - Briefcase, - DollarSign, - Star, - Globe2, - ArrowRight -} from 'lucide-react'; - -export default function EmployerShortlist() { - const savedWorkers = [ - { id: 1, name: 'Maria Santos', role: 'Housemaid', rating: '4.9', salary: '1,800 AED', nationality: 'Philippines', img: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Maria', exp: '8 Years' }, - { id: 2, name: 'Elena Gilbert', role: 'Nanny', rating: '4.8', salary: '2,500 AED', nationality: 'Kenya', img: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Elena', exp: '5 Years' }, - ]; - - return ( - - - -
- {/* Header */} -
-

Shortlist

-

Quick access to your saved candidates

-
- - {savedWorkers.length > 0 ? ( -
-
- {savedWorkers.length} Saved Profiles - -
- - {savedWorkers.map((worker) => ( -
-
-
-
- worker -
-
-

{worker.name}

-
- - {worker.nationality} -
-
-
- -
- -
-
- - {worker.exp} -
-
- - {worker.salary} -
-
- -
- - View Profile - -
- - {worker.rating} -
-
-
- ))} - -
- -
-
- ) : ( -
-
- -
-
-

Your Shortlist is Empty

-

Save profiles to compare and hire faster.

-
- - Browse Workers - -
- )} -
-
- ); -} diff --git a/resources/js/Pages/Mobile/EmployerSplash.jsx b/resources/js/Pages/Mobile/EmployerSplash.jsx deleted file mode 100644 index 97f4b83..0000000 --- a/resources/js/Pages/Mobile/EmployerSplash.jsx +++ /dev/null @@ -1,44 +0,0 @@ -import React, { useEffect } from 'react'; -import { Head, router } from '@inertiajs/react'; -import { ShieldCheck } from 'lucide-react'; - -export default function EmployerSplash() { - useEffect(() => { - const timer = setTimeout(() => { - router.get('/mobile/employer/welcome'); - }, 2000); - return () => clearTimeout(timer); - }, []); - - return ( -
- - - {/* Animated Background Elements */} -
-
- -
-
- -
- -
-

- JobSift -

-

Employer Edition

-
-
- -
-
-
-
-
-
- Version 2.0.26 -
-
- ); -} diff --git a/resources/js/Pages/Mobile/EmployerSubscription.jsx b/resources/js/Pages/Mobile/EmployerSubscription.jsx deleted file mode 100644 index 4fe4127..0000000 --- a/resources/js/Pages/Mobile/EmployerSubscription.jsx +++ /dev/null @@ -1,97 +0,0 @@ -import React from 'react'; -import { Head, Link } from '@inertiajs/react'; -import EmployerMobileLayout from './Layouts/EmployerMobileLayout'; -import { Check, ShieldCheck, Zap, Star, Trophy } from 'lucide-react'; - -export default function EmployerSubscription() { - const plans = [ - { - id: 'basic', - name: 'Basic Search', - price: '99', - icon: Zap, - color: 'bg-slate-50 text-slate-400', - features: ['Browse 500+ workers', 'Shortlist up to 10', 'Email support'] - }, - { - id: 'premium', - name: 'Premium Pass', - price: '199', - icon: Star, - color: 'bg-blue-50 text-blue-600', - popular: true, - features: ['Unlimited shortlisting', 'Direct messaging', 'Priority profile view', '24/7 Support'] - }, - { - id: 'vip', - name: 'VIP Concierge', - price: '499', - icon: Trophy, - color: 'bg-amber-50 text-amber-500', - features: ['Assigned manager', 'Medical guarantee', 'Replacement warranty', 'Legal assistance'] - }, - ]; - - return ( - - - -
- {/* Header */} -
-

Choose Your Plan

-

Select a package that fits your hiring needs.

-
- - {/* Plan Cards Stacked */} -
- {plans.map((plan) => ( -
- {plan.popular && ( -
- Most Popular -
- )} - -
-
-
- -
-
-

{plan.name}

-
- {plan.price} - AED / Mo -
-
-
- -
- {plan.features.map((feature, i) => ( -
-
- -
- {feature} -
- ))} -
- - -
-
- ))} -
-
-
- ); -} diff --git a/resources/js/Pages/Mobile/EmployerWelcome.jsx b/resources/js/Pages/Mobile/EmployerWelcome.jsx deleted file mode 100644 index 27874b1..0000000 --- a/resources/js/Pages/Mobile/EmployerWelcome.jsx +++ /dev/null @@ -1,83 +0,0 @@ -import React from 'react'; -import { Head, Link } from '@inertiajs/react'; -import { - Briefcase, - ArrowRight, - Sparkles, - ShieldCheck, - Users -} from 'lucide-react'; - -export default function EmployerWelcome() { - return ( -
- - - {/* Artistic Header Background */} -
-
-
-
- -
-
- -
-
-

- Find Top
Talent Fast -

-

The Future of Recruitment

-
-
-
- -
-
-
-
- -
-
-

Verified Workers

-

Access 10,000+ pre-vetted domestic and commercial workers.

-
-
- -
-
- -
-
-

AI Matching

-

Our smart engine matches workers based on your specific needs.

-
-
-
- -
- - Login to Portal - - - - Join as Employer - -
-
- -
-

- By continuing you agree to our
- Terms of Service -

-
-
- ); -} diff --git a/resources/js/Pages/Mobile/EmployerWorkers.jsx b/resources/js/Pages/Mobile/EmployerWorkers.jsx deleted file mode 100644 index 82fa9e0..0000000 --- a/resources/js/Pages/Mobile/EmployerWorkers.jsx +++ /dev/null @@ -1,319 +0,0 @@ -import React, { useState } from 'react'; -import { Head, Link } from '@inertiajs/react'; -import EmployerMobileLayout from './Layouts/EmployerMobileLayout'; -import { - Search, - Filter, - Globe, - Briefcase, - DollarSign, - Star, - Heart, - MapPin, - ArrowRight, - ChevronLeft, - Bookmark, - CheckCircle2, - Calendar, - Sparkles, - X, - RotateCcw -} from 'lucide-react'; - -export default function EmployerWorkers() { - const [searchQuery, setSearchQuery] = useState(''); - const [showFilters, setShowFilters] = useState(false); - const [activeCat, setActiveCat] = useState('All'); - - const [filters, setFilters] = useState({ - nationality: 'All Nationalities', - availability: 'All Availabilities', - experience: 'All Experience', - religion: 'All Religions', - maxSalary: 3000 - }); - - const categories = ['All', 'Housemaid', 'Nanny', 'Cook', 'Driver', 'Gardener']; - - const filterOptions = { - nationalities: ['All Nationalities', 'Philippines', 'India', 'Sri Lanka', 'Kenya', 'Ethiopia', 'Indonesia'], - availabilities: ['All Availabilities', 'Available Now', 'Available in 1 week', 'Available in 1 month'], - experienceLevels: ['All Experience', '0-2 Years', '2-5 Years', '5-10 Years', '10+ Years'], - religions: ['All Religions', 'Christianity', 'Islam', 'Hinduism', 'Other'] - }; - - const workers = [ - { - id: 1, - name: 'Maria Santos', - role: 'Childcare', - salary: '1800 AED / mo', - nationality: 'Philippines', - initial: 'M', - bio: '"Experienced nanny with 6 years working with expatriate families in Dubai. Certified in pediatric first aid."', - exp: '5+ Years', - avail: 'Immediate', - religion: 'Christian', - age: '32 yrs', - skills: ['Childcare', 'Cooking', 'Housekeeping', 'Ironing'] - }, - { - id: 2, - name: 'Elena Gilbert', - role: 'Housemaid', - salary: '2500 AED / mo', - nationality: 'Kenya', - initial: 'E', - bio: '"Professional housekeeper with expertise in deep cleaning and organizing. Fluent in English and Swahili."', - exp: '3+ Years', - avail: 'Immediate', - religion: 'Christian', - age: '28 yrs', - skills: ['Cleaning', 'Ironing', 'Bilingual'] - } - ]; - - const resetFilters = () => { - setFilters({ - nationality: 'All Nationalities', - availability: 'All Availabilities', - experience: 'All Experience', - religion: 'All Religions', - maxSalary: 3000 - }); - setSearchQuery(''); - }; - - return ( - - - -
- {/* Header */} -
-
- -

Find Workers

-
- - {/* Search Bar */} -
-
- - setSearchQuery(e.target.value)} - className="w-full pl-12 pr-4 py-4 bg-slate-50 border border-slate-100 rounded-2xl text-sm focus:ring-4 focus:ring-blue-500/10 focus:border-[#185FA5] outline-none transition-all" - /> -
- -
- - {/* Category Tabs */} -
- {categories.map((cat) => ( - - ))} -
-
- - {/* Worker Listing */} -
- {workers.map((worker) => ( -
- {/* Card Header Section */} -
-
-
- {worker.initial} -
-
-
-

{worker.name}

- -
-
- - {worker.nationality} -
-
-
- -
- - {/* Card Body Section */} -
-
-
- {worker.role} -
-
- - {worker.salary} -
-
- -

- {worker.bio} -

- - {/* Stats Grid 2x2 */} -
-
- -
Exp: {worker.exp}
-
-
- -
Avail: {worker.avail}
-
-
- -
{worker.religion}
-
-
- -
Age: {worker.age}
-
-
- - {/* Skills Tags */} -
- {worker.skills.map((skill) => ( - - {skill} - - ))} -
- - - View Full Profile - -
-
- ))} -
- - {/* Filter Drawer - Simple UI Style */} - {showFilters && ( -
-
setShowFilters(false)} /> -
-
-
- -

Filters

-
- -
- -
- {/* Nationality */} -
- - -
- - {/* Availability */} -
- -
- {filterOptions.availabilities.map(av => ( - - ))} -
-
- - {/* Experience */} -
- - -
- - {/* Salary Slider */} -
-
- - {filters.maxSalary} AED -
- setFilters({...filters, maxSalary: Number(e.target.value)})} - className="w-full h-2 bg-slate-100 rounded-full appearance-none cursor-pointer accent-[#185FA5]" - /> -
- 1200 AED - 3000 AED -
-
- - -
-
-
- )} -
- - ); -} diff --git a/resources/js/Pages/Mobile/Layouts/EmployerMobileLayout.jsx b/resources/js/Pages/Mobile/Layouts/EmployerMobileLayout.jsx deleted file mode 100644 index 084d906..0000000 --- a/resources/js/Pages/Mobile/Layouts/EmployerMobileLayout.jsx +++ /dev/null @@ -1,61 +0,0 @@ -import React from 'react'; -import { Link, usePage } from '@inertiajs/react'; -import { - LayoutGrid, - Search, - Users, - MessageSquare, - User, - Bell -} from 'lucide-react'; - -export default function EmployerMobileLayout({ children, title }) { - const { url } = usePage(); - - const navItems = [ - { label: 'Home', icon: LayoutGrid, href: '/mobile/employer/home' }, - { label: 'Find Workers', icon: Search, href: '/mobile/employer/workers' }, - { label: 'Candidates', icon: Users, href: '/mobile/employer/candidates' }, - { label: 'Chat', icon: MessageSquare, href: '/mobile/employer/messages', badge: 3 }, - { label: 'Profile', icon: User, href: '/mobile/employer/profile' }, - ]; - - const isActive = (href) => url.startsWith(href); - - return ( -
- {/* Content Area */} -
- {children} -
- - {/* Bottom Navigation */} -
- {navItems.map((item) => { - const active = isActive(item.href); - return ( - -
- - {item.badge && ( -
- {item.badge} -
- )} -
- {item.label} - {active && ( -
- )} - - ); - })} -
-
- ); -} diff --git a/resources/js/Pages/Mobile/Welcome.jsx b/resources/js/Pages/Mobile/Welcome.jsx deleted file mode 100644 index ad6fc1e..0000000 --- a/resources/js/Pages/Mobile/Welcome.jsx +++ /dev/null @@ -1,95 +0,0 @@ -import React from 'react'; -import { Head, Link } from '@inertiajs/react'; -import { - Briefcase, - User, - ShieldCheck, - ArrowRight, - Globe, - Zap -} from 'lucide-react'; - -export default function MobileWelcome() { - return ( -
- - - {/* Background Decor */} -
-
- - {/* Language Selector */} -
- -
- -
- {/* Hero */} -
-
-
- -
-
-
-

Marketplace

-

Direct Hiring Portal

-
-
- - {/* Selection Cards */} -
- -
-
- -
-
-

Employer

-

I want to hire

-
-
- - - - -
-
- -
-
-

Worker

-

I want a job

-
-
- - -
- -
-
- - Trusted by 10k+ users -
-
-
- - {/* Footer Links */} -
-

- Need Help?
- Contact Support -

-
-
- ); -} diff --git a/resources/js/Pages/Mobile/WorkerAnnouncements.jsx b/resources/js/Pages/Mobile/WorkerAnnouncements.jsx deleted file mode 100644 index fc77f5a..0000000 --- a/resources/js/Pages/Mobile/WorkerAnnouncements.jsx +++ /dev/null @@ -1,95 +0,0 @@ -import React from 'react'; -import { Head, Link } from '@inertiajs/react'; -import { - Home, - MessageCircle, - User, - Compass, - Bell, - Calendar, - ChevronRight, - Megaphone, - Search, - ArrowLeft, - Clock -} from 'lucide-react'; - -export default function WorkerAnnouncements() { - const announcements = [ - { id: 1, title: "NEW PLATFORM UPDATE", date: "2 HOURS AGO", text: "We have simplified the verification process for all workers. Check your profile for details.", category: "SYSTEM" }, - { id: 2, title: "SAFETY GUIDELINES", date: "YESTERDAY", text: "Please review our updated safety protocols for household interviews.", category: "SECURITY" }, - { id: 3, title: "RAMADAN HOURS", date: "2 DAYS AGO", text: "Special support hours during the holy month of Ramadan have been updated.", category: "GENERAL" }, - ]; - - return ( -
- - - {/* Header */} -
-
- - -
-
- -
- {/* Section Title */} -
- -

Latest Updates

-
- - {/* Cards */} -
- {announcements.map((item) => ( -
-
- {item.category} -
- {item.date} -
-
-
-

{item.title}

-

{item.text}

-
- -
- ))} -
-
- - {/* Bottom Navigation */} -
- - - Home - - - - Explore - - - - Updates - - - - Chat - - - - Profile - -
-
- ); -} - diff --git a/resources/js/Pages/Mobile/WorkerChat.jsx b/resources/js/Pages/Mobile/WorkerChat.jsx deleted file mode 100644 index beb43e8..0000000 --- a/resources/js/Pages/Mobile/WorkerChat.jsx +++ /dev/null @@ -1,88 +0,0 @@ -import React, { useState } from 'react'; -import { Head, Link } from '@inertiajs/react'; -import { - Home, - MessageCircle, - User, - Briefcase, - ArrowLeft, - Search, - Send, - MoreVertical, - CheckCheck, - Mic, - Image as ImageIcon, - Paperclip, - Bell -} from 'lucide-react'; - -export default function WorkerChat() { - const [messages, setMessages] = useState([ - { id: 1, text: "Hello Danial, are you available for an interview?", time: "09:41 AM", sender: "employer" }, - { id: 2, text: "Yes ma'am, I am available tomorrow morning.", time: "09:42 AM", sender: "me" }, - { id: 3, text: "Great! Let's connect at 10 AM.", time: "09:45 AM", sender: "employer" }, - ]); - - const [input, setInput] = useState(""); - - return ( -
- - - {/* Header */} -
- -
-

Al Mansoor

- Online -
- -
- - {/* Message Area */} -
- {messages.map((msg) => ( -
-
-

{msg.text}

-
- {msg.time} - {msg.sender === 'me' && } -
-
-
- ))} -
- - {/* Input Bar */} -
-
- -
- setInput(e.target.value)} - /> - -
-
-
-
- ); -} - diff --git a/resources/js/Pages/Mobile/WorkerChatList.jsx b/resources/js/Pages/Mobile/WorkerChatList.jsx deleted file mode 100644 index bcbfbfe..0000000 --- a/resources/js/Pages/Mobile/WorkerChatList.jsx +++ /dev/null @@ -1,93 +0,0 @@ -import React from 'react'; -import { Head, Link } from '@inertiajs/react'; -import { - Home, - MessageCircle, - User, - Search, - ArrowLeft, - CheckCheck, - Compass, - Bell -} from 'lucide-react'; - -export default function WorkerChatList() { - const chats = [ - { id: 1, name: 'Al Mansoor', message: "Great! Let's connect at 10 AM.", time: '10:45 AM', unread: 2, avatar: 'M' }, - { id: 2, name: 'Maktoom Family', message: 'Thank you for the update.', time: 'Yesterday', unread: 0, avatar: 'S' }, - { id: 3, name: 'Zayed Household', message: 'Are you available for a trial?', time: '2 days ago', unread: 0, avatar: 'E' }, - ]; - - return ( -
- - - {/* Header */} -
- -

Messages

- -
- -
- {chats.map((chat) => ( - -
- {chat.avatar} -
-
-
-

{chat.name}

- {chat.time} -
-
-

0 ? 'font-black text-slate-900' : 'text-slate-400'}`}> - {chat.message} -

- {chat.unread > 0 ? ( -
- {chat.unread} -
- ) : ( - - )} -
-
- - ))} -
- - {/* Bottom Navigation */} -
- - - Home - - - - Explore - - - - Updates - - - - Chat - - - - Profile - -
-
- ); -} diff --git a/resources/js/Pages/Mobile/WorkerDetail.jsx b/resources/js/Pages/Mobile/WorkerDetail.jsx deleted file mode 100644 index e9d93b6..0000000 --- a/resources/js/Pages/Mobile/WorkerDetail.jsx +++ /dev/null @@ -1,226 +0,0 @@ -import React from 'react'; -import { Head, Link } from '@inertiajs/react'; -import { - ChevronLeft, - Share2, - CheckCircle2, - Globe, - Briefcase, - Calendar, - Heart, - Sparkles, - DollarSign, - MessageSquare, - Bookmark, - Phone, - FileText, - ShieldCheck, - Languages, - Info, - ArrowRight -} from 'lucide-react'; - -export default function WorkerDetail({ id }) { - // Mock data for the worker detail - const worker = { - id: id, - name: 'Maria Santos', - role: 'Childcare / Nanny', - salary: '1800 AED / mo', - nationality: 'Philippines', - initial: 'M', - online: true, - bio: 'Experienced and dedicated nanny with over 6 years of experience working with expatriate families in Dubai. Certified in pediatric first aid and CPR. I love working with children and I am skilled in early childhood development activities, meal preparation, and maintaining a clean environment for the kids.', - exp: '6 Years', - avail: 'Immediate', - religion: 'Christian', - age: '32 Years', - languages: ['English (Fluent)', 'Tagalog (Native)'], - education: 'Bachelors in Education', - status: 'Verified', - skills: ['Pediatric First Aid', 'Newborn Care', 'Cooking', 'Housekeeping', 'Storytelling'], - documents: [ - { name: 'Emirates ID', status: 'Verified' }, - { name: 'Medical Certificate', status: 'Vetted' }, - { name: 'Criminal Record Check', status: 'Clean' } - ] - }; - - return ( -
- - - {/* Header */} -
- - - -
- -
-
- - {/* Profile Hero */} -
-
-
-
- {worker.initial} -
-
- -
-
- -
-

{worker.name}

-
- - {worker.nationality} -
-
- -
-
- {worker.role} -
-
-
- - {/* Key Stats Grid */} -
-
-
- -
-
-
Salary
-
{worker.salary}
-
-
-
-
- -
-
-
Experience
-
{worker.exp}
-
-
-
-
- - {/* Profile Content */} -
- {/* About Section */} -
-
-
-

Professional Bio

-
-
-

- {worker.bio} -

-
-
- - {/* Detailed Information */} -
-
-
-

Candidate Overview

-
-
-
-
- - Availability -
- {worker.avail} -
-
-
- - Religion -
- {worker.religion} -
-
-
- - Age -
- {worker.age} -
-
-
- - Languages -
- {worker.languages.join(' · ')} -
-
-
- - {/* Vetted Documents */} -
-
-
-

Identity & Verification

-
-
- {worker.documents.map((doc, idx) => ( -
-
-
- -
- {doc.name} -
-
- - {doc.status} -
-
- ))} -
-
- - {/* Skills Section */} -
-
-
-

Skills & Expertise

-
-
- {worker.skills.map((skill) => ( - - {skill} - - ))} -
-
-
- - {/* Floating Action Bar */} -
- - - - -
-
- ); -} diff --git a/resources/js/Pages/Mobile/WorkerEditProfile.jsx b/resources/js/Pages/Mobile/WorkerEditProfile.jsx deleted file mode 100644 index d096632..0000000 --- a/resources/js/Pages/Mobile/WorkerEditProfile.jsx +++ /dev/null @@ -1,170 +0,0 @@ -import React, { useState } from 'react'; -import { Head, Link } from '@inertiajs/react'; -import { - ArrowLeft, - Check, - Save, - MapPin, - Briefcase, - Globe -} from 'lucide-react'; - -export default function WorkerEditProfile() { - const commonSkills = [ - { name: 'Cleaning', icon: '🧼' }, - { name: 'Mason', icon: '🏗️' }, - { name: 'Electric', icon: '⚡' }, - { name: 'Plumb', icon: '🔧' }, - { name: 'Paint', icon: '🎨' }, - { name: 'Helper', icon: '📦' } - ]; - const commonAreas = ['Marina', 'Deira', 'Bur Dubai', 'Jebel Ali', 'Al Quoz', 'City']; - const commonLangs = ['English', 'Arabic', 'Hindi', 'Urdu', 'Tagalog']; - - const [selectedSkills, setSelectedSkills] = useState(['Cleaning']); - const [otherSkill, setOtherSkill] = useState(''); - const [showOtherInput, setShowOtherInput] = useState(false); - const [selectedAreas, setSelectedAreas] = useState(['Deira']); - const [selectedLangs, setSelectedLangs] = useState(['English']); - const [saved, setSaved] = useState(false); - - const toggle = (val, list, setter) => { - if (list.includes(val)) { - setter(list.filter(item => item !== val)); - } else { - setter([...list, val]); - } - }; - - const handleSave = () => { - setSaved(true); - setTimeout(() => setSaved(false), 2000); - }; - - return ( -
- - - {/* Header */} -
- -

Edit Skills

-
-
- -
- {/* Work Section */} -
-
- -

My Skills

-
-
- {commonSkills.map((skill) => ( - - ))} - - {/* Other Skill Button */} - -
- - {showOtherInput && ( -
- setOtherSkill(e.target.value)} - autoFocus - /> -
- )} -
- - {/* Locations Section */} -
-
- -

My Locations

-
-
- {commonAreas.map((area) => ( - - ))} -
-
- - {/* Languages Section */} -
-
- -

My Languages

-
-
- {commonLangs.map((lang) => ( - - ))} -
-
-
- - {/* Huge Save Button */} -
- -
-
- ); -} - diff --git a/resources/js/Pages/Mobile/WorkerExplore.jsx b/resources/js/Pages/Mobile/WorkerExplore.jsx deleted file mode 100644 index 455b249..0000000 --- a/resources/js/Pages/Mobile/WorkerExplore.jsx +++ /dev/null @@ -1,225 +0,0 @@ -import React, { useState } from 'react'; -import { Head, Link } from '@inertiajs/react'; -import { - Home, - Search, - Bell, - MapPin, - ChevronRight, - MessageCircle, - User, - Compass, - SlidersHorizontal, - Briefcase -} from 'lucide-react'; - -export default function WorkerExplore() { - const [showFilters, setShowFilters] = useState(false); - const categories = ['All', 'Cleaning', 'Driver', 'Mason', 'Electrician', 'Carpenter']; - const [activeCat, setActiveCat] = useState('All'); - - const jobs = [ - { id: 1, title: 'Concrete Mason', company: 'BuildRight Ltd', salary: 'AED 3,500', loc: 'Deira', type: 'Full-time', bg: 'bg-blue-50/50', icon: '🏗️' }, - { id: 2, title: 'Home Sanitizer', company: 'PureClean', salary: 'AED 2,200', loc: 'Marina', type: 'Part-time', bg: 'bg-emerald-50/50', icon: '🧼' }, - { id: 3, title: 'Light Driver', company: 'Zayed Logistics', salary: 'AED 4,000', loc: 'Al Quoz', type: 'Full-time', bg: 'bg-purple-50/50', icon: '🚗' }, - { id: 4, title: 'General Helper', company: 'Global Pack', salary: 'AED 1,800', loc: 'Jebel Ali', type: 'Full-time', bg: 'bg-rose-50/50', icon: '📦' }, - ]; - - return ( -
- - - {/* Header / Search Area */} -
-
-

Jobs

- -
- - {/* Search Bar */} -
- - -
- - {/* Category Chips */} -
- {categories.map((cat) => ( - - ))} -
-
- -
-
-

Found {jobs.length} Jobs

-
- - Dubai, UAE -
-
- - {/* Job List */} -
- {jobs.map((job) => ( - -
-
-
{job.icon}
-
-
- {job.salary} -
-
- -
-

{job.title}

-
-

- High street avenue
- London, UK -

-
-

From Fri, 17 Dec

-
- -
- - {job.type} - -
- - ))} -
-
- - {/* Filter Bottom Sheet */} - {showFilters && ( -
-
setShowFilters(false)} /> - -
- {/* Handle */} -
-
-
- -
-
-

Filter result

- 22 result -
- -
- {/* Sort By */} -
-

Sort by

-
- {['Default', 'Latest First', 'Oldest First', 'Price: Low to High', 'Price: High to Low'].map((opt, i) => ( - - ))} -
-
- - {/* Contract Type */} -
-

Contract type

-
- {['Part time', 'Full time', 'Contractual'].map((opt) => ( - - ))} -
-
- - {/* Skill Range */} -
-

Skill range

-
- {['Junior', 'Medium', 'Expert'].map((opt) => ( - - ))} -
-
- - {/* Salary Type */} -
-

Salary type

-
- {['Hourly Rate', 'Fix Rate'].map((opt) => ( - - ))} -
-
-
- - {/* Actions */} -
- - -
-
-
-
- )} - - {/* Bottom Navigation */} -
- - - Home - - - - Jobs - - - - Updates - - - - Chat - - - - Profile - -
-
- ); -} diff --git a/resources/js/Pages/Mobile/WorkerHome.jsx b/resources/js/Pages/Mobile/WorkerHome.jsx deleted file mode 100644 index c1674b4..0000000 --- a/resources/js/Pages/Mobile/WorkerHome.jsx +++ /dev/null @@ -1,251 +0,0 @@ -import React, { useState } from 'react'; -import { Head, Link } from '@inertiajs/react'; -import { - Home, - Search, - Bell, - Bookmark, - ChevronDown, - MessageCircle, - User, - Briefcase, - Compass, - Globe -} from 'lucide-react'; - -export default function WorkerHome() { - const [isOpenToWork, setIsOpenToWork] = useState(true); - const [showLangDropdown, setShowLangDropdown] = useState(false); - const [currentLang, setCurrentLang] = useState('EN'); - - return ( -
- - - {/* Header Area */} -
-
-
-
- avatar -
-
-

Hi, Danial

- -
-
-
-
- - - {showLangDropdown && ( -
- {['English', 'Arabic', 'Tagalog', 'Swahili', 'Hindi'].map((lang) => ( - - ))} -
- )} -
- - -
-
- - {/* Search Bar */} -
- - -
- -
-
-
- -
- {/* Open to Work Card */} - - - {/* Job Offers Horizontal */} -
-
-

Job offers

- -
- -
- {[ - { id: 1, title: 'Driver needed', salary: '$15', icon: '🚗', bg: 'bg-blue-50' }, - { id: 2, title: 'Mason needed', salary: '$25', icon: '🏗️', bg: 'bg-emerald-50' } - ].map((offer) => ( - -
-
- {offer.icon} -
-

{offer.salary}

-
-
-

{offer.title}

-

High street avenue London, UK

-

From Fri, 17 Dec

-
- Part time - - ))} -
-
- - {/* Employer Requests - Compact List */} -
-
-

Employer Requests

- 2 Pending -
-
- {[ - { id: 1, employer: 'Al Mansoori Group', role: 'Project Mason', img: 'https://api.dicebear.com/7.x/initials/svg?seed=AM' }, - { id: 2, employer: 'Marina Cleaners', role: 'Deep Cleaning', img: 'https://api.dicebear.com/7.x/initials/svg?seed=MC' } - ].map((req) => ( -
-
-
- employer -
-
-

{req.employer}

-

{req.role}

-
-
-
- - -
-
- ))} -
-
- - {/* Recent Job Post - Compact & Professional */} -
-
-

Recent job post

- -
- - {/* Category Tabs */} -
- {['Cleaner', 'Driver', 'Mason', 'Electrician'].map((cat) => ( - - ))} -
- - {/* Job Cards - Refined & Compact */} -
- {[ - { title: 'Home Cleaning', salary: '$20', loc: 'Marina, Dubai', icon: '🧼', time: '2h ago' }, - { title: 'Mason Worker', salary: '$30', loc: 'Deira, Dubai', icon: '🏗️', time: '4h ago' }, - { title: 'Plumbing Service', salary: '$45', loc: 'Al Barsha', icon: '🔧', time: '5h ago' } - ].map((job, i) => ( - -
-
- {job.icon} -
-
-

{job.title}

-
-

{job.loc}

- -

{job.time}

-
-
-
-
-

{job.salary}

-

Verified

-
- - ))} -
-
-
- - {/* Bottom Navigation */} -
- - - Home - - - - Explore - - - - Updates - - - - Chat - - - - Profile - -
-
- ); -} diff --git a/resources/js/Pages/Mobile/WorkerJobDetail.jsx b/resources/js/Pages/Mobile/WorkerJobDetail.jsx deleted file mode 100644 index a4935c2..0000000 --- a/resources/js/Pages/Mobile/WorkerJobDetail.jsx +++ /dev/null @@ -1,147 +0,0 @@ -import React from 'react'; -import { Head, Link } from '@inertiajs/react'; -import { - ArrowLeft, - MoreVertical, - Briefcase, - MapPin, - Users, - ChevronRight, - Clock, - ShieldCheck, - DollarSign, - Star, - Phone, - Calendar -} from 'lucide-react'; - -export default function WorkerJobDetail() { - return ( -
- - - {/* Header */} -
- -

Details

- -
- -
- {/* Job Icon & Title */} -
-
-
- -
-
-
-

Driver needed

-

- - High street avenue London, UK -

-
-
- - - {/* Stats Grid */} -
-
-
- -
-
-

Salary

-

$15

-
-
-
-
- -
-
-

Job type

-

Part time

-
-
-
-
- -
-
-

Skill

-

Expert

-
-
-
-
- -
-
-

Payment

-

Verified

-
-
-
- - {/* Description */} -
-

Description

-

- An outstanding executer that consistently delivers more than expected & start mentoring experience man to drive my vehicle safely and efficiently. Read More -

-
- - {/* Additional Details */} -
-

Additional details

-
-
-
- - Offer type -
- Contractual -
-
-
- - Start from -
-
- 26 Aug, 2023 - 9:00 am to 6:00 pm -
-
-
-
- - Contact -
- (702) 555-0122 -
-
-
- - Posted -
- 2 days ago -
-
-
-
- - {/* Footer Apply Button */} -
- -
-
- ); -} diff --git a/resources/js/Pages/Mobile/WorkerLogin.jsx b/resources/js/Pages/Mobile/WorkerLogin.jsx deleted file mode 100644 index 340407b..0000000 --- a/resources/js/Pages/Mobile/WorkerLogin.jsx +++ /dev/null @@ -1,110 +0,0 @@ -import React, { useState } from 'react'; -import { useForm, Head, Link } from '@inertiajs/react'; -import { - ArrowLeft, - Eye, - EyeOff, - Loader2, - Mail, - Lock, - ChevronLeft -} from 'lucide-react'; - -export default function WorkerLogin() { - const { data, setData, post, processing, errors } = useForm({ - email: '', - password: '', - remember: false, - source: 'mobile', - }); - - const [showPassword, setShowPassword] = useState(false); - - const handleSubmit = (e) => { - e.preventDefault(); - // post('/worker/login'); - }; - - return ( -
- - - {/* Minimal Header */} -
- -
- -
- {/* Title Section */} -
-

Welcome back

-

Please enter your details to sign in

-
- - {/* Login Form */} -
-
-
- -
- - setData('email', e.target.value)} - placeholder="Enter your email" - className="w-full pl-14 pr-6 py-5 bg-slate-50 border border-slate-100 rounded-[24px] text-sm font-medium focus:bg-white focus:border-[#185FA5] focus:ring-4 focus:ring-blue-500/10 transition-all outline-none" - required - /> -
- {errors.email &&

{errors.email}

} -
- -
-
- - Forgot? -
-
- - setData('password', e.target.value)} - placeholder="••••••••" - className="w-full pl-14 pr-14 py-5 bg-slate-50 border border-slate-100 rounded-[24px] text-sm font-medium focus:bg-white focus:border-[#185FA5] focus:ring-4 focus:ring-blue-500/10 transition-all outline-none" - required - /> - -
-
-
- - -
- - {/* Footer */} -
-

- Don't have an account?
- Sign up here -

-
-
-
- ); -} diff --git a/resources/js/Pages/Mobile/WorkerProfile.jsx b/resources/js/Pages/Mobile/WorkerProfile.jsx deleted file mode 100644 index 8e5f997..0000000 --- a/resources/js/Pages/Mobile/WorkerProfile.jsx +++ /dev/null @@ -1,171 +0,0 @@ -import React from 'react'; -import { Head, Link } from '@inertiajs/react'; -import { - Home, - MessageCircle, - User, - Briefcase, - Bell, - ArrowLeft, - Pencil, - Star, - FileText, - Download, - MapPin, - Settings, - ChevronRight, - Compass, - Clock, - ShieldCheck -} from 'lucide-react'; - -export default function WorkerProfile() { - return ( -
- - - {/* Header */} -
- -

Profile

- - - -
- -
- {/* Profile Header */} -
-
- avatar -
-
-

Danial Donald

-

- 4901 Thornridge Cir. Shiloh, Hawaii 81063 -

-
-
- - {/* Stats Bar */} -
-
-

12

-

Complete

-
-
-
-

4.8

- -
-

Ratings

-
-
-

$ 50

-

Salary

-
-
- - {/* Content Sections */} -
-
-

About Danial

-

- I am Danial Donald, a passionate professional with over 10 years of experience. Specialized in high-quality service and customer satisfaction. -

-
- -
-
-

Gender:

-

Male

-
-
-

Age:

-

32 years

-
-
-

Experience:

-

Expert (10y)

-
-
- -
-
-

Expertise

-

Plumbing, Cleaning, Electrician

-
- -
-

Languages

-
- {['English', 'Arabic', 'Tagalog', 'Swahili'].map((lang) => ( - - {lang} - - ))} -
-
-
- -
-

My Documents

-
-
-
-
- -
-
-

Passport.JPG

-

Verified

-
-
- -
- -
-
-

Passport No

-

Z12345678

-
-
-

Expiry Date

-

12/12/2028

-
-
-
-
-
-
- - {/* Bottom Navigation */} -
- - - Home - - - - Explore - - - - Updates - - - - Chat - - - - Profile - -
-
- ); -} - - diff --git a/resources/js/Pages/Mobile/WorkerRegister.jsx b/resources/js/Pages/Mobile/WorkerRegister.jsx deleted file mode 100644 index fa70457..0000000 --- a/resources/js/Pages/Mobile/WorkerRegister.jsx +++ /dev/null @@ -1,195 +0,0 @@ -import React, { useState } from 'react'; -import { Head, Link } from '@inertiajs/react'; -import { - User, - Phone, - MapPin, - ChevronRight, - CheckCircle2, - ArrowLeft, - Camera, - Briefcase, - ShieldCheck, - Globe -} from 'lucide-react'; - -export default function WorkerRegister() { - const [step, setStep] = useState(1); - const [formData, setFormData] = useState({ - name: '', - phone: '', - nationality: 'Kenya', - role: 'Cleaner', - }); - - const nextStep = () => setStep(prev => prev + 1); - - const nationalities = [ - { name: 'Kenya', flag: '🇰🇪' }, - { name: 'Philippines', flag: '🇵🇭' }, - { name: 'India', flag: '🇮🇳' }, - { name: 'Pakistan', flag: '🇵🇰' }, - { name: 'Nepal', flag: '🇳🇵' }, - { name: 'Other', flag: '🌍' } - ]; - - const roles = [ - { name: 'Cleaner', icon: '🧼' }, - { name: 'Driver', icon: '🚗' }, - { name: 'Mason', icon: '🏗️' }, - { name: 'Electrician', icon: '⚡' }, - { name: 'Plumber', icon: '🔧' }, - { name: 'Helper', icon: '📦' } - ]; - - return ( -
- - - {/* Top Bar */} -
- -
- {[1, 2, 3].map((i) => ( -
- ))} -
-
-
- -
- {step === 1 && ( -
-
-

Your basic info.

-

Step 1 of 3

-
- -
-
- -
- - setFormData({...formData, name: e.target.value})} - /> -
-
- -
- -
- - setFormData({...formData, phone: e.target.value})} - /> -
-
- -
- -
- {nationalities.map((nat) => ( - - ))} -
-
-
-
- )} - - {step === 2 && ( -
-
-

What do you do?

-

Step 2 of 3

-
- -
- {roles.map(role => ( - - ))} -
-
- )} - - {step === 3 && ( -
-
-

Verify Identity.

-

Final Step

-
- -
-
-
- -
-
-

Upload Passport

-

TAP TO OPEN CAMERA

-
-
- -
- -

- Your data is 100% secure. We only show documents to verified employers. -

-
-
-
- )} -
- - {/* Bottom Button */} -
- {step < 3 ? ( - - ) : ( - - Join Marketplace - - )} -
-
- ); -} diff --git a/resources/js/app.jsx b/resources/js/app.jsx index b779e90..ad58829 100644 --- a/resources/js/app.jsx +++ b/resources/js/app.jsx @@ -6,6 +6,8 @@ import { createRoot } from 'react-dom/client'; import { createInertiaApp } from '@inertiajs/react'; import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'; +import { Toaster } from 'sonner'; + const appName = import.meta.env.VITE_APP_NAME || 'Laravel'; createInertiaApp({ @@ -13,10 +15,15 @@ createInertiaApp({ resolve: (name) => resolvePageComponent(`./Pages/${name}.jsx`, import.meta.glob('./Pages/**/*.jsx')), setup({ el, App, props }) { const root = createRoot(el); - root.render(); + root.render( + <> + + + + ); }, progress: { - color: '#4B5563', + color: '#185FA5', showSpinner: true, }, }); diff --git a/resources/views/emails/employer-otp.blade.php b/resources/views/emails/employer-otp.blade.php new file mode 100644 index 0000000..a42c4cf --- /dev/null +++ b/resources/views/emails/employer-otp.blade.php @@ -0,0 +1,128 @@ + + + + + + Verify Your Email + + + +
+
+

Marketplace

+
Employer Verification
+
+
+

Hello {{ $employerName }},

+

+ Thank you for starting the registration process for {{ $companyName }} on the Marketplace portal. To verify your email address and proceed with setting up your account, please enter the 6-digit one-time passcode (OTP) below. +

+ +
+
{{ $otp }}
+
Valid for 10 minutes only
+
+ +
+ Security Alert: This code is confidential. Our support team will never ask you for this code. Do not share this passcode with anyone. +
+ +

+ If you did not initiate this registration, you can safely ignore this email. +

+
+ +
+ + diff --git a/routes/web.php b/routes/web.php index 3cfe0b5..d2342a6 100644 --- a/routes/web.php +++ b/routes/web.php @@ -14,67 +14,7 @@ Route::post('/admin/login', [AdminAuthController::class, 'login'])->name('admin.login.submit'); Route::post('/admin/logout', [AdminAuthController::class, 'logout'])->name('admin.logout'); -// Temporary Mobile Mockup Routes for Client Review -Route::get('/mobile/welcome', function () { - return Inertia::render('Mobile/Welcome'); }); -Route::get('/mobile/worker/register', function () { - return Inertia::render('Mobile/WorkerRegister'); }); -Route::get('/mobile/worker/home', function () { - return Inertia::render('Mobile/WorkerHome'); }); -Route::get('/mobile/worker/explore', function () { - return Inertia::render('Mobile/WorkerExplore'); }); -Route::get('/mobile/worker/job/detail', function () { - return Inertia::render('Mobile/WorkerJobDetail'); }); -Route::get('/mobile/worker/chat', function () { - return Inertia::render('Mobile/WorkerChatList'); }); -Route::get('/mobile/worker/chat/detail', function () { - return Inertia::render('Mobile/WorkerChat'); }); -Route::get('/mobile/worker/announcements', function () { - return Inertia::render('Mobile/WorkerAnnouncements'); }); -Route::get('/mobile/worker/profile', function () { - return Inertia::render('Mobile/WorkerProfile'); }); -Route::get('/mobile/worker/profile/edit', function () { - return Inertia::render('Mobile/WorkerEditProfile'); }); -Route::get('/mobile/employer/home', function () { - return Inertia::render('Mobile/EmployerHome'); }); -Route::get('/mobile/employer/workers', function () { - return Inertia::render('Mobile/EmployerWorkers'); }); -Route::get('/mobile/employer/workers/{id}', function ($id) { - return Inertia::render('Mobile/WorkerDetail', ['id' => $id]); }); -Route::get('/mobile/employer/shortlist', function () { - return Inertia::render('Mobile/EmployerShortlist'); }); -Route::get('/mobile/employer/candidates', function () { - return Inertia::render('Mobile/EmployerCandidates'); }); -Route::get('/mobile/employer/messages', function () { - return Inertia::render('Mobile/EmployerMessages'); }); -Route::get('/mobile/employer/chat/{id}', function ($id) { - return Inertia::render('Mobile/EmployerChatDetail', ['id' => $id]); }); -Route::get('/mobile/employer/subscription', function () { - return Inertia::render('Mobile/EmployerSubscription'); }); -Route::get('/mobile/employer/profile', function () { - return Inertia::render('Mobile/EmployerProfile'); }); -Route::get('/mobile/employer/profile/edit', function () { - return Inertia::render('Mobile/EmployerProfileEdit'); }); -Route::get('/mobile/employer/profile/security', function () { - return Inertia::render('Mobile/EmployerSecurity'); }); -Route::get('/mobile/employer/profile/notifications', function () { - return Inertia::render('Mobile/EmployerNotifications'); }); -Route::get('/mobile/employer/support', function () { - return Inertia::render('Mobile/EmployerSupport'); }); -Route::get('/mobile/employer/announcements', function () { - return Inertia::render('Mobile/EmployerAnnouncements'); }); -Route::get('/mobile/employer/splash', function () { - return Inertia::render('Mobile/EmployerSplash'); }); -Route::get('/mobile/employer/welcome', function () { - return Inertia::render('Mobile/EmployerWelcome'); }); -Route::get('/mobile/employer/login', function () { - return Inertia::render('Mobile/EmployerLogin'); }); -Route::get('/mobile/employer/otp', function () { - return Inertia::render('Mobile/EmployerOTP'); }); -Route::get('/mobile/employer/forgot-password', function () { - return Inertia::render('Mobile/EmployerForgotPassword'); }); -Route::get('/mobile/employer/register', function () { - return Inertia::render('Mobile/EmployerRegister'); }); + // Admin Protected Routes @@ -114,7 +54,11 @@ Route::post('/employer/login', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'login'])->name('employer.login.submit'); Route::get('/employer/register', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'showRegister'])->name('employer.register'); Route::post('/employer/register', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'register'])->name('employer.register.submit'); -Route::get('/employer/verify-email/{token}', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'verifyEmail'])->name('employer.verify'); +Route::get('/employer/verify-email', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'showVerifyEmail'])->name('employer.verify-email'); +Route::post('/employer/verify-email', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'verifyEmail'])->name('employer.verify-email.submit')->middleware('throttle:5,1'); +Route::post('/employer/resend-otp', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'resendOtp'])->name('employer.resend-otp')->middleware('throttle:3,1'); +Route::get('/employer/create-password', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'showCreatePassword'])->name('employer.create-password'); +Route::post('/employer/create-password', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'createPassword'])->name('employer.create-password.submit'); Route::post('/employer/logout', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'logout'])->name('employer.logout'); Route::get('/employer/pending-verification', function () { return Inertia::render('Employer/Auth/PendingVerification'); @@ -129,41 +73,42 @@ Route::get('/workers', [\App\Http\Controllers\Employer\WorkerController::class, 'index'])->name('employer.workers'); Route::get('/workers/{id}', [\App\Http\Controllers\Employer\WorkerController::class, 'show'])->name('employer.workers.show'); Route::get('/workers/{id}/hire', function ($id) { - // Mock worker for now - in reality, would fetch from DB - $worker = [ - 'id' => $id, - 'name' => 'Maria Santos', - 'nationality' => 'Filipino', - 'category' => 'Domestic Helper', - 'salary' => 2500, + $worker = \App\Models\Worker::with('category')->findOrFail($id); + $workerData = [ + 'id' => $worker->id, + 'name' => $worker->name, + 'nationality' => $worker->nationality, + 'category' => $worker->category->name ?? 'General Worker', + 'salary' => (int) $worker->expected_salary, ]; - return Inertia::render('Employer/Hiring/Confirm', ['worker' => $worker]); + return Inertia::render('Employer/Hiring/Confirm', ['worker' => $workerData]); })->name('employer.hiring.confirm'); Route::get('/workers/{id}/hire/success', function ($id) { - $worker = [ - 'id' => $id, - 'name' => 'Maria Santos', + $worker = \App\Models\Worker::findOrFail($id); + $workerData = [ + 'id' => $worker->id, + 'name' => $worker->name, ]; - return Inertia::render('Employer/Hiring/Success', ['worker' => $worker]); + return Inertia::render('Employer/Hiring/Success', ['worker' => $workerData]); })->name('employer.hiring.success'); // Job Management - Route::get('/jobs', function () { - return Inertia::render('Employer/Jobs/Index'); - })->name('employer.jobs'); - - Route::get('/jobs/create', function () { - return Inertia::render('Employer/Jobs/Create'); - })->name('employer.jobs.create'); - - Route::get('/jobs/{id}/applicants', function ($id) { - $job = ['id' => $id, 'title' => 'Experienced Mason Project', 'category' => 'Mason', 'salary' => 2800]; - return Inertia::render('Employer/Jobs/Applicants', ['job' => $job]); - })->name('employer.jobs.applicants'); + Route::get('/jobs', [\App\Http\Controllers\Employer\JobController::class, 'index'])->name('employer.jobs'); + Route::get('/jobs/create', [\App\Http\Controllers\Employer\JobController::class, 'create'])->name('employer.jobs.create'); + Route::post('/jobs/create', [\App\Http\Controllers\Employer\JobController::class, 'store'])->name('employer.jobs.store'); + Route::get('/jobs/{id}/applicants', [\App\Http\Controllers\Employer\JobController::class, 'applicants'])->name('employer.jobs.applicants'); Route::get('/subscription', function () { + $sess = session('user'); + $sessId = is_array($sess) ? ($sess['id'] ?? null) : ($sess->id ?? null); + $user = $sessId ? \App\Models\User::find($sessId) : \App\Models\User::where('role', 'employer')->first(); + + $sub = $user ? \Illuminate\Support\Facades\DB::table('subscriptions')->where('user_id', $user->id)->where('status', 'active')->latest('id')->first() : null; + $expiresAt = $sub && $sub->expires_at ? date('Y-m-d', strtotime($sub->expires_at)) : '2026-12-31'; + $planName = $sub ? (ucfirst($sub->plan_id) . ' Pass') : 'Premium Employer Pass'; + return Inertia::render('Employer/Subscription', [ - 'currentPlan' => 'Premium Employer Pass', - 'expiresAt' => '2026-12-31', + 'currentPlan' => $planName, + 'expiresAt' => $expiresAt, 'plans' => [ [ 'id' => 'basic', @@ -193,215 +138,20 @@ ]); })->name('employer.subscription'); - Route::get('/messages', function () { - return Inertia::render('Employer/Messages/Index', [ - 'conversations' => [ - [ - 'id' => 201, - 'worker_name' => 'Maria Santos', - 'category' => 'Childcare', - 'last_message' => 'Yes ma\'am, I am available for a video interview tomorrow at 10 AM.', - 'unread' => true, - 'online' => true, - 'sent_at' => '10 mins ago', - ], - [ - 'id' => 202, - 'worker_name' => 'Lakshmi Sharma', - 'category' => 'Elderly Care', - 'last_message' => 'I have 5 years of experience in Dubai taking care of elderly patients.', - 'unread' => true, - 'online' => false, - 'sent_at' => '2 hours ago', - ], - [ - 'id' => 203, - 'worker_name' => 'Siti Aminah', - 'category' => 'Housekeeping', - 'last_message' => 'Thank ma\'am for shortlisting my profile. Please let me know your offer.', - 'unread' => false, - 'online' => true, - 'sent_at' => '1 day ago', - ], - ] - ]); - })->name('employer.messages'); + Route::get('/messages', [\App\Http\Controllers\Employer\MessageController::class, 'index'])->name('employer.messages'); + Route::get('/messages/{id}', [\App\Http\Controllers\Employer\MessageController::class, 'show'])->name('employer.messages.show'); + Route::post('/messages/{id}/send', [\App\Http\Controllers\Employer\MessageController::class, 'send'])->name('employer.messages.send'); - Route::get('/messages/{id}', function ($id) { - $conversations = [ - [ - 'id' => 201, - 'worker_name' => 'Maria Santos', - 'category' => 'Childcare', - 'last_message' => 'Yes ma\'am, I am available for a video interview tomorrow at 10 AM.', - 'unread' => true, - 'online' => true, - 'sent_at' => '10 mins ago', - ], - [ - 'id' => 202, - 'worker_name' => 'Lakshmi Sharma', - 'category' => 'Elderly Care', - 'last_message' => 'I have 5 years of experience in Dubai taking care of elderly patients.', - 'unread' => true, - 'online' => false, - 'sent_at' => '2 hours ago', - ], - [ - 'id' => 203, - 'worker_name' => 'Siti Aminah', - 'category' => 'Housekeeping', - 'last_message' => 'Thank ma\'am for shortlisting my profile. Please let me know your offer.', - 'unread' => false, - 'online' => true, - 'sent_at' => '1 day ago', - ], - ]; + Route::get('/shortlist', [\App\Http\Controllers\Employer\ShortlistController::class, 'index'])->name('employer.shortlist'); + Route::post('/shortlist/toggle', [\App\Http\Controllers\Employer\ShortlistController::class, 'toggle'])->name('employer.shortlist.toggle'); + Route::post('/shortlist/{id}/remove', [\App\Http\Controllers\Employer\ShortlistController::class, 'remove'])->name('employer.shortlist.remove'); - return Inertia::render('Employer/Messages/Show', [ - 'conversations' => $conversations, - 'conversation' => collect($conversations)->firstWhere('id', (int) $id) ?? [ - 'id' => (int) $id, - 'worker_name' => 'Maria Santos', - 'category' => 'Childcare', - 'online' => true, - 'salary' => '1,800 AED', - 'nationality' => 'Philippines', - ], - 'initialMessages' => [ - [ - 'id' => 1, - 'sender' => 'employer', - 'text' => 'Hello! I reviewed your profile and I am very interested in hiring you. Are you available for a phone interview?', - 'time' => 'Yesterday, 4:15 PM', - ], - [ - 'id' => 2, - 'sender' => 'worker', - 'text' => 'Good day ma\'am! Yes, thank you for reaching out. I am available anytime today or tomorrow.', - 'time' => 'Yesterday, 4:30 PM', - ], - [ - 'id' => 3, - 'sender' => 'worker', - 'text' => 'I have all my documents ready and my current visa is transferable.', - 'time' => 'Yesterday, 4:31 PM', - ], - ] - ]); - })->name('employer.messages.show'); + Route::get('/candidates', [\App\Http\Controllers\Employer\CandidateController::class, 'index'])->name('employer.candidates'); + Route::post('/candidates/{id}/status', [\App\Http\Controllers\Employer\CandidateController::class, 'updateStatus'])->name('employer.candidates.status'); - Route::get('/shortlist', function () { - return Inertia::render('Employer/Shortlist', [ - 'shortlistedWorkers' => [ - [ - 'id' => 101, - 'name' => 'Maria Santos', - 'nationality' => 'Philippines', - 'category' => 'Childcare', - 'skills' => ['Childcare', 'Cooking', 'Housekeeping'], - 'availability' => 'Immediate', - 'experience' => '5+ Years', - 'salary' => 1800, - 'verified' => true, - ], - [ - 'id' => 103, - 'name' => 'Siti Aminah', - 'nationality' => 'Indonesia', - 'category' => 'Housekeeping', - 'skills' => ['Housekeeping', 'Ironing', 'Deep Cleaning'], - 'availability' => 'Immediate', - 'experience' => '3-5 Years', - 'salary' => 1400, - 'verified' => true, - ], - [ - 'id' => 105, - 'name' => 'Anoma Perera', - 'nationality' => 'Sri Lanka', - 'category' => 'Cooking', - 'skills' => ['Cooking', 'Baking'], - 'availability' => 'Immediate', - 'experience' => '5+ Years', - 'salary' => 2200, - 'verified' => true, - ], - ] - ]); - })->name('employer.shortlist'); + Route::get('/announcements', [\App\Http\Controllers\Employer\AnnouncementController::class, 'index'])->name('employer.announcements'); + Route::post('/announcements/create', [\App\Http\Controllers\Employer\AnnouncementController::class, 'store'])->name('employer.announcements.store'); - Route::get('/candidates', function () { - return Inertia::render('Employer/SelectedCandidates', [ - 'selectedWorkers' => [ - [ - 'id' => 101, - 'name' => 'Maria Santos', - 'category' => 'Site Supervisor', - 'nationality' => 'Philippines', - 'salary' => '4,500', - 'status' => 'Interview Scheduled', - 'experience' => '8 Years' - ], - [ - 'id' => 102, - 'name' => 'Lakshmi Sharma', - 'category' => 'Electrician', - 'nationality' => 'India', - 'salary' => '3,200', - 'status' => 'Offer Extended', - 'experience' => '5 Years' - ], - [ - 'id' => 103, - 'name' => 'Siti Aminah', - 'category' => 'Project Coordinator', - 'nationality' => 'Indonesia', - 'salary' => '5,500', - 'status' => 'Hired', - 'experience' => '10 Years' - ], - [ - 'id' => 104, - 'name' => 'Ahmed Khan', - 'category' => 'Mason', - 'nationality' => 'Pakistan', - 'salary' => '2,800', - 'status' => 'Reviewing', - 'experience' => '4 Years' - ], - [ - 'id' => 105, - 'name' => 'John Doe', - 'category' => 'Carpenter', - 'nationality' => 'United Kingdom', - 'salary' => '6,000', - 'status' => 'Rejected', - 'experience' => '12 Years' - ], - ] - ]); - })->name('employer.candidates'); - - Route::get('/announcements', function () { - return Inertia::render('Employer/Announcements'); - })->name('employer.announcements'); - - Route::get('/profile', function () { - $sess = session('user'); - $user = is_array($sess) ? (object) $sess : ($sess ?? (object) [ - 'name' => 'John Doe', - 'email' => 'employer@example.com', - 'phone' => '+971 50 123 4567', - ]); - return Inertia::render('Employer/Profile', [ - 'employerProfile' => [ - 'name' => $user->name ?? 'John Doe', - 'email' => $user->email ?? 'employer@example.com', - 'phone' => $user->phone ?? '+971 50 123 4567', - 'company_name' => 'Al Mansoor Household', - 'emirates_id_status' => 'approved', - ] - ]); - })->name('employer.profile'); + Route::get('/profile', [\App\Http\Controllers\Employer\ProfileController::class, 'index'])->name('employer.profile'); + Route::post('/profile/update', [\App\Http\Controllers\Employer\ProfileController::class, 'update'])->name('employer.profile.update'); });