initial
This commit is contained in:
parent
2917f11d12
commit
2e3697db29
53
app/Console/Commands/ClearDatabaseData.php
Normal file
53
app/Console/Commands/ClearDatabaseData.php
Normal file
@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class ClearDatabaseData extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'db:clear-data';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Clear active database data tables for manual testing, preserving authentication and categories';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
||||
47
app/Http/Controllers/Employer/AnnouncementController.php
Normal file
47
app/Http/Controllers/Employer/AnnouncementController.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Employer;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use App\Models\Announcement;
|
||||
|
||||
class AnnouncementController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$dbAnnouncements = Announcement::latest()->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.');
|
||||
}
|
||||
}
|
||||
101
app/Http/Controllers/Employer/CandidateController.php
Normal file
101
app/Http/Controllers/Employer/CandidateController.php
Normal file
@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Employer;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use App\Models\User;
|
||||
use App\Models\JobApplication;
|
||||
use App\Models\JobPost;
|
||||
|
||||
class CandidateController extends Controller
|
||||
{
|
||||
private function resolveCurrentUser()
|
||||
{
|
||||
$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;
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
@ -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)[
|
||||
$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)->format('Y-m-d'),
|
||||
'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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
160
app/Http/Controllers/Employer/JobController.php
Normal file
160
app/Http/Controllers/Employer/JobController.php
Normal file
@ -0,0 +1,160 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Employer;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use App\Models\User;
|
||||
use App\Models\JobPost;
|
||||
use App\Models\JobApplication;
|
||||
use App\Models\WorkerCategory;
|
||||
|
||||
class JobController extends Controller
|
||||
{
|
||||
private function resolveCurrentUser()
|
||||
{
|
||||
$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();
|
||||
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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
155
app/Http/Controllers/Employer/MessageController.php
Normal file
155
app/Http/Controllers/Employer/MessageController.php
Normal file
@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Employer;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use App\Models\User;
|
||||
use App\Models\Conversation;
|
||||
use App\Models\Message;
|
||||
use App\Models\Worker;
|
||||
|
||||
class MessageController extends Controller
|
||||
{
|
||||
private function resolveCurrentUser()
|
||||
{
|
||||
$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();
|
||||
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();
|
||||
}
|
||||
}
|
||||
127
app/Http/Controllers/Employer/ProfileController.php
Normal file
127
app/Http/Controllers/Employer/ProfileController.php
Normal file
@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Employer;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use App\Models\User;
|
||||
use App\Models\EmployerProfile;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
|
||||
class ProfileController extends Controller
|
||||
{
|
||||
private function resolveCurrentUser()
|
||||
{
|
||||
$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();
|
||||
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.');
|
||||
}
|
||||
}
|
||||
116
app/Http/Controllers/Employer/ShortlistController.php
Normal file
116
app/Http/Controllers/Employer/ShortlistController.php
Normal file
@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Employer;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use App\Models\User;
|
||||
use App\Models\Worker;
|
||||
use App\Models\Shortlist;
|
||||
|
||||
class ShortlistController extends Controller
|
||||
{
|
||||
private function resolveCurrentUser()
|
||||
{
|
||||
$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;
|
||||
|
||||
$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.');
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
{
|
||||
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'],
|
||||
$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' => $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,
|
||||
|
||||
@ -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'),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
48
app/Mail/EmployerOtpMail.php
Normal file
48
app/Mail/EmployerOtpMail.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class EmployerOtpMail extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public $otp;
|
||||
public $companyName;
|
||||
public $employerName;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*/
|
||||
public function __construct($otp, $companyName, $employerName)
|
||||
{
|
||||
$this->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',
|
||||
);
|
||||
}
|
||||
}
|
||||
17
app/Models/Announcement.php
Normal file
17
app/Models/Announcement.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Announcement extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'title',
|
||||
'body',
|
||||
'type',
|
||||
];
|
||||
}
|
||||
31
app/Models/Conversation.php
Normal file
31
app/Models/Conversation.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Conversation extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'employer_id',
|
||||
'worker_id',
|
||||
];
|
||||
|
||||
public function employer()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'employer_id');
|
||||
}
|
||||
|
||||
public function worker()
|
||||
{
|
||||
return $this->belongsTo(Worker::class, 'worker_id');
|
||||
}
|
||||
|
||||
public function messages()
|
||||
{
|
||||
return $this->hasMany(Message::class);
|
||||
}
|
||||
}
|
||||
@ -10,9 +10,12 @@ class EmployerProfile extends Model
|
||||
'user_id',
|
||||
'company_name',
|
||||
'phone',
|
||||
'country',
|
||||
'emirates_id_front',
|
||||
'emirates_id_back',
|
||||
'verification_status',
|
||||
'rejection_reason'
|
||||
'rejection_reason',
|
||||
'language',
|
||||
'notifications'
|
||||
];
|
||||
}
|
||||
|
||||
27
app/Models/JobApplication.php
Normal file
27
app/Models/JobApplication.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class JobApplication extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'job_id',
|
||||
'worker_id',
|
||||
'status',
|
||||
];
|
||||
|
||||
public function jobPost()
|
||||
{
|
||||
return $this->belongsTo(JobPost::class, 'job_id');
|
||||
}
|
||||
|
||||
public function worker()
|
||||
{
|
||||
return $this->belongsTo(Worker::class, 'worker_id');
|
||||
}
|
||||
}
|
||||
47
app/Models/JobPost.php
Normal file
47
app/Models/JobPost.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class JobPost extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'employer_id',
|
||||
'title',
|
||||
'category_id',
|
||||
'workers_needed',
|
||||
'job_type',
|
||||
'location',
|
||||
'salary',
|
||||
'start_date',
|
||||
'description',
|
||||
'requirements',
|
||||
'status',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'start_date' => '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');
|
||||
}
|
||||
}
|
||||
28
app/Models/Message.php
Normal file
28
app/Models/Message.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Message extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'conversation_id',
|
||||
'sender_type',
|
||||
'sender_id',
|
||||
'text',
|
||||
'is_read',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_read' => 'boolean',
|
||||
];
|
||||
|
||||
public function conversation()
|
||||
{
|
||||
return $this->belongsTo(Conversation::class);
|
||||
}
|
||||
}
|
||||
26
app/Models/Shortlist.php
Normal file
26
app/Models/Shortlist.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Shortlist extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'employer_id',
|
||||
'worker_id',
|
||||
];
|
||||
|
||||
public function employer()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'employer_id');
|
||||
}
|
||||
|
||||
public function worker()
|
||||
{
|
||||
return $this->belongsTo(Worker::class, 'worker_id');
|
||||
}
|
||||
}
|
||||
18
app/Models/Skill.php
Normal file
18
app/Models/Skill.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Skill extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = ['name'];
|
||||
|
||||
public function workers()
|
||||
{
|
||||
return $this->belongsToMany(Worker::class, 'worker_skills');
|
||||
}
|
||||
}
|
||||
48
app/Models/Worker.php
Normal file
48
app/Models/Worker.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Worker extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'email',
|
||||
'phone',
|
||||
'nationality',
|
||||
'age',
|
||||
'salary',
|
||||
'availability',
|
||||
'experience',
|
||||
'religion',
|
||||
'bio',
|
||||
'category_id',
|
||||
'verified',
|
||||
'status',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'verified' => '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);
|
||||
}
|
||||
}
|
||||
18
app/Models/WorkerCategory.php
Normal file
18
app/Models/WorkerCategory.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class WorkerCategory extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = ['name'];
|
||||
|
||||
public function workers()
|
||||
{
|
||||
return $this->hasMany(Worker::class, 'category_id');
|
||||
}
|
||||
}
|
||||
25
app/Models/WorkerDocument.php
Normal file
25
app/Models/WorkerDocument.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class WorkerDocument extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'worker_id',
|
||||
'type',
|
||||
'number',
|
||||
'issue_date',
|
||||
'expiry_date',
|
||||
'ocr_accuracy',
|
||||
];
|
||||
|
||||
public function worker()
|
||||
{
|
||||
return $this->belongsTo(Worker::class);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('worker_categories', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name')->unique();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('worker_categories');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('workers', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('skills', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name')->unique();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('skills');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('worker_skills', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('worker_documents', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('job_posts', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('job_applications', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('conversations', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('messages', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('payments', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('announcements', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('shortlists', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('employer_profiles', function (Blueprint $table) {
|
||||
$table->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']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('employer_profiles', function (Blueprint $table) {
|
||||
$table->string('country')->nullable()->after('company_name');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('employer_profiles', function (Blueprint $table) {
|
||||
$table->dropColumn('country');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -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),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
35
database/seeders/EmployerProfileSeeder.php
Normal file
35
database/seeders/EmployerProfileSeeder.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class EmployerProfileSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// Create Employer User
|
||||
$userId = \Illuminate\Support\Facades\DB::table('users')->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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
70
database/seeders/JobPostSeeder.php
Normal file
70
database/seeders/JobPostSeeder.php
Normal file
@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class JobPostSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// Resolve the first employer user dynamically
|
||||
$employerId = \App\Models\User::where('role', 'employer')->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(),
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
27
database/seeders/SkillSeeder.php
Normal file
27
database/seeders/SkillSeeder.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class SkillSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$skills = [
|
||||
'Wiring', 'Pipe Fitting', 'Tiling', 'Driving', 'Supervision', 'Cleaning', 'Masonry', 'Plumbing'
|
||||
];
|
||||
|
||||
foreach ($skills as $skill) {
|
||||
\Illuminate\Support\Facades\DB::table('skills')->insert([
|
||||
'name' => $skill,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
28
database/seeders/WorkerCategorySeeder.php
Normal file
28
database/seeders/WorkerCategorySeeder.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class WorkerCategorySeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$categories = [
|
||||
'Electrician', 'Mason', 'Plumber', 'Cleaner', 'Site Supervisor', 'Driver', 'General Helper',
|
||||
'Childcare', 'Housekeeping', 'Cooking', 'Elderly Care'
|
||||
];
|
||||
|
||||
foreach ($categories as $category) {
|
||||
\Illuminate\Support\Facades\DB::table('worker_categories')->insert([
|
||||
'name' => $category,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
185
database/seeders/WorkerSeeder.php
Normal file
185
database/seeders/WorkerSeeder.php
Normal file
@ -0,0 +1,185 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class WorkerSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$workers = [
|
||||
[
|
||||
'name' => '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(),
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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',
|
||||
|
||||
@ -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() {
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsFormOpen(true)}
|
||||
className="bg-[#185FA5] hover:bg-[#144f8a] text-white px-5 py-2.5 rounded-xl font-bold text-sm flex items-center space-x-2 transition-colors shadow-sm"
|
||||
className="bg-[#185FA5] hover:bg-[#144f8a] text-white px-5 py-2.5 rounded-xl font-bold text-sm flex items-center space-x-2 transition-colors shadow-sm cursor-pointer"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
<span>New Announcement</span>
|
||||
@ -78,7 +89,8 @@ export default function Announcements() {
|
||||
</div>
|
||||
|
||||
{isFormOpen && (
|
||||
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm animate-in fade-in slide-in-from-top-4">
|
||||
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 animate-in fade-in">
|
||||
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-2xl w-full max-w-lg animate-in slide-in-from-bottom-4">
|
||||
<div className="flex items-center space-x-2 mb-4 text-[#185FA5] font-bold">
|
||||
<Megaphone className="w-5 h-5" />
|
||||
<h2>Compose Message</h2>
|
||||
@ -88,30 +100,30 @@ export default function Announcements() {
|
||||
<label className="block text-xs font-semibold text-slate-600 mb-1">Title</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={newAnnouncement.title}
|
||||
onChange={(e) => 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"
|
||||
className={`w-full px-4 py-2.5 rounded-xl border ${errors.title ? 'border-red-500' : 'border-slate-300'} text-sm focus:ring-2 focus:ring-[#185FA5]/20 focus:border-[#185FA5] outline-none transition-all`}
|
||||
placeholder="Enter announcement title..."
|
||||
/>
|
||||
{errors.title && <p className="text-red-500 text-xs mt-1">{errors.title}</p>}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-600 mb-1">Content</label>
|
||||
<textarea
|
||||
required
|
||||
rows="4"
|
||||
value={newAnnouncement.content}
|
||||
onChange={(e) => setNewAnnouncement({ ...newAnnouncement, content: e.target.value })}
|
||||
className="w-full px-4 py-3 rounded-xl border border-slate-300 text-sm focus:ring-2 focus:ring-[#185FA5]/20 focus:border-[#185FA5] outline-none transition-all resize-none"
|
||||
className={`w-full px-4 py-3 rounded-xl border ${errors.content ? 'border-red-500' : 'border-slate-300'} text-sm focus:ring-2 focus:ring-[#185FA5]/20 focus:border-[#185FA5] outline-none transition-all resize-none`}
|
||||
placeholder="Type your message here..."
|
||||
></textarea>
|
||||
{errors.content && <p className="text-red-500 text-xs mt-1">{errors.content}</p>}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-600 mb-1">Target Audience</label>
|
||||
<select
|
||||
value={newAnnouncement.audience}
|
||||
onChange={(e) => setNewAnnouncement({ ...newAnnouncement, audience: e.target.value })}
|
||||
className="w-full sm:w-64 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 bg-white"
|
||||
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 bg-white"
|
||||
>
|
||||
<option value="Both">Both Employers & Workers</option>
|
||||
<option value="Employers">Employers Only</option>
|
||||
@ -121,14 +133,14 @@ export default function Announcements() {
|
||||
<div className="flex items-center justify-end space-x-3 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsFormOpen(false)}
|
||||
className="px-5 py-2.5 rounded-xl text-sm font-semibold text-slate-600 hover:bg-slate-100 transition-colors"
|
||||
onClick={() => { setIsFormOpen(false); setErrors({}); }}
|
||||
className="px-5 py-2.5 rounded-xl text-sm font-semibold text-slate-600 hover:bg-slate-100 transition-colors cursor-pointer"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-emerald-600 hover:bg-emerald-700 text-white px-6 py-2.5 rounded-xl text-sm font-bold flex items-center space-x-2 transition-colors shadow-sm"
|
||||
className="bg-emerald-600 hover:bg-emerald-700 text-white px-6 py-2.5 rounded-xl text-sm font-bold flex items-center space-x-2 transition-colors shadow-sm cursor-pointer"
|
||||
>
|
||||
<Send className="w-4 h-4" />
|
||||
<span>Broadcast Message</span>
|
||||
@ -136,6 +148,7 @@ export default function Announcements() {
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
@ -165,7 +178,7 @@ export default function Announcements() {
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleDelete(ann.id)}
|
||||
className="p-2 text-slate-400 hover:text-red-600 hover:bg-red-50 rounded-xl transition-colors self-start sm:opacity-0 group-hover:opacity-100 focus:opacity-100"
|
||||
className="p-2 text-slate-400 hover:text-red-600 hover:bg-red-50 rounded-xl transition-colors self-start sm:opacity-0 group-hover:opacity-100 focus:opacity-100 cursor-pointer"
|
||||
title="Delete Announcement"
|
||||
>
|
||||
<Trash2 className="w-5 h-5" />
|
||||
|
||||
@ -139,35 +139,7 @@ export default function Login() {
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Footer Portals */}
|
||||
<div className="mt-10 z-10 w-full max-w-md space-y-6 px-2">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="h-[1px] flex-1 bg-slate-200" />
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-[0.2em]">Quick Access</span>
|
||||
<div className="h-[1px] flex-1 bg-slate-200" />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Link
|
||||
href="/mobile/employer/login"
|
||||
className="flex flex-col items-center justify-center p-6 bg-white border border-slate-100 rounded-[32px] shadow-sm hover:shadow-md transition-all group space-y-3"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-2xl bg-[#185FA5]/5 flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<Smartphone className="w-6 h-6 text-[#185FA5]" />
|
||||
</div>
|
||||
<span className="text-[10px] font-black uppercase tracking-widest text-slate-900 text-center">Employer Mobile</span>
|
||||
</Link>
|
||||
<Link
|
||||
href="/mobile/worker/register"
|
||||
className="flex flex-col items-center justify-center p-6 bg-white border border-slate-100 rounded-[32px] shadow-sm hover:shadow-md transition-all group space-y-3"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-2xl bg-[#185FA5]/5 flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<Smartphone className="w-6 h-6 text-[#185FA5]" />
|
||||
</div>
|
||||
<span className="text-[10px] font-black uppercase tracking-widest text-slate-900 text-center">Employee Mobile</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Head } from '@inertiajs/react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Head, useForm } from '@inertiajs/react';
|
||||
import EmployerLayout from '../../Layouts/EmployerLayout';
|
||||
import {
|
||||
Megaphone,
|
||||
@ -24,31 +24,21 @@ import {
|
||||
DialogFooter
|
||||
} from '@/components/ui/dialog';
|
||||
|
||||
export default function Announcements() {
|
||||
export default function Announcements({ initialAnnouncements }) {
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [newAnnouncement, setNewAnnouncement] = useState({ title: '', content: '', audience: 'Shortlisted' });
|
||||
const [toastMessage, setToastMessage] = useState(null);
|
||||
const [announcements, setAnnouncements] = useState([
|
||||
{
|
||||
id: 1,
|
||||
title: 'Interview Schedule Updates',
|
||||
content: 'Hello everyone, we will be conducting the next round of interviews next week. Please keep your phones available and ensure you have a stable internet connection for the video call.',
|
||||
audience: 'Selected Candidates',
|
||||
created_at: '2 days ago',
|
||||
reads: 12,
|
||||
replies: 4
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Welcome to the Recruitment Pool',
|
||||
content: 'We have successfully added your profile to our recruitment pool. We will contact you if your skills match our requirements.',
|
||||
audience: 'Shortlisted',
|
||||
created_at: '5 days ago',
|
||||
reads: 45,
|
||||
replies: 0
|
||||
}
|
||||
]);
|
||||
const [announcements, setAnnouncements] = useState(initialAnnouncements || []);
|
||||
|
||||
useEffect(() => {
|
||||
setAnnouncements(initialAnnouncements || []);
|
||||
}, [initialAnnouncements]);
|
||||
|
||||
const { data: newAnnouncement, setData: setNewAnnouncement, post, processing, reset } = useForm({
|
||||
title: '',
|
||||
content: '',
|
||||
audience: 'Shortlisted'
|
||||
});
|
||||
|
||||
const showToast = (message) => {
|
||||
setToastMessage(message);
|
||||
@ -57,19 +47,14 @@ export default function Announcements() {
|
||||
|
||||
const handleCreate = (e) => {
|
||||
e.preventDefault();
|
||||
const newEntry = {
|
||||
id: announcements.length + 1,
|
||||
title: newAnnouncement.title,
|
||||
content: newAnnouncement.content,
|
||||
audience: newAnnouncement.audience,
|
||||
created_at: 'Just now',
|
||||
reads: 0,
|
||||
replies: 0
|
||||
};
|
||||
setAnnouncements([newEntry, ...announcements]);
|
||||
setNewAnnouncement({ title: '', content: '', audience: 'Shortlisted' });
|
||||
post('/employer/announcements/create', {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
reset();
|
||||
setIsDialogOpen(false);
|
||||
showToast('Announcement sent successfully');
|
||||
showToast('Announcement posted successfully');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const filteredAnnouncements = announcements.filter(ann =>
|
||||
|
||||
279
resources/js/Pages/Employer/Auth/CreatePassword.jsx
Normal file
279
resources/js/Pages/Employer/Auth/CreatePassword.jsx
Normal file
@ -0,0 +1,279 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Head, Link, router } from '@inertiajs/react';
|
||||
import axios from 'axios';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
ShieldCheck,
|
||||
CheckCircle,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Lock,
|
||||
Loader2,
|
||||
Check,
|
||||
X
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function CreatePassword() {
|
||||
const [data, setData] = useState({
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
});
|
||||
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
// Live password strength validation
|
||||
const [strength, setStrength] = useState({
|
||||
length: false,
|
||||
upper: false,
|
||||
lower: false,
|
||||
number: false,
|
||||
special: false
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const pass = data.password;
|
||||
setStrength({
|
||||
length: pass.length >= 8,
|
||||
upper: /[A-Z]/.test(pass),
|
||||
lower: /[a-z]/.test(pass),
|
||||
number: /[0-9]/.test(pass),
|
||||
special: /[^A-Za-z0-9]/.test(pass)
|
||||
});
|
||||
}, [data.password]);
|
||||
|
||||
const handleInputChange = (field, value) => {
|
||||
setData(prev => ({ ...prev, [field]: value }));
|
||||
if (errors[field]) {
|
||||
setErrors(prev => ({ ...prev, [field]: null }));
|
||||
}
|
||||
};
|
||||
|
||||
const isAllCriteriaMet = Object.values(strength).every(Boolean);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!isAllCriteriaMet) {
|
||||
setErrors({ password: 'Password does not meet all security guidelines.' });
|
||||
toast.error('Password does not meet all security guidelines.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.password !== data.password_confirmation) {
|
||||
setErrors({ password_confirmation: 'Passwords do not match.' });
|
||||
toast.error('Passwords do not match.');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setErrors({});
|
||||
|
||||
try {
|
||||
const response = await axios.post('/employer/create-password', data);
|
||||
toast.success('Registration finalized! Welcome to the Marketplace.');
|
||||
router.visit('/employer/dashboard');
|
||||
} catch (err) {
|
||||
if (err.response) {
|
||||
if (err.response.status === 422) {
|
||||
setErrors(err.response.data.errors || {});
|
||||
toast.error('Validation failed. Please verify password requirements.');
|
||||
} else {
|
||||
toast.error('Failed to complete enrollment. Please try again.');
|
||||
}
|
||||
} else {
|
||||
toast.error('Network error. Please check your connection.');
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderStepIndicator = () => (
|
||||
<div className="flex items-center justify-between mb-12 relative max-w-md mx-auto px-4 select-none">
|
||||
<div className="absolute top-1/2 left-0 w-full h-0.5 bg-slate-100 -translate-y-1/2 -z-10" />
|
||||
<div className="absolute top-1/2 left-0 h-0.5 bg-[#185FA5] -translate-y-1/2 -z-10 transition-all duration-500" style={{ width: '100%' }} />
|
||||
|
||||
{[
|
||||
{ step: 1, label: 'Register' },
|
||||
{ step: 2, label: 'Verify Code' },
|
||||
{ step: 3, label: 'Set Password' }
|
||||
].map((s) => (
|
||||
<div key={s.step} className="flex flex-col items-center">
|
||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center font-black text-sm border-4 transition-all duration-300 ${
|
||||
s.step <= 3
|
||||
? 'bg-[#185FA5] text-white border-blue-100 scale-110 shadow-lg'
|
||||
: 'bg-white text-slate-400 border-slate-50'
|
||||
}`}>
|
||||
{s.step === 3 && isAllCriteriaMet ? <Check className="w-5 h-5" /> : s.step}
|
||||
</div>
|
||||
<span className={`text-[9px] font-black uppercase mt-2 tracking-wider ${
|
||||
s.step <= 3 ? 'text-[#185FA5]' : 'text-slate-400'
|
||||
}`}>{s.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
const rules = [
|
||||
{ key: 'length', text: 'At least 8 characters' },
|
||||
{ key: 'upper', text: 'One uppercase letter (A-Z)' },
|
||||
{ key: 'lower', text: 'One lowercase letter (a-z)' },
|
||||
{ key: 'number', text: 'One numerical digit (0-9)' },
|
||||
{ key: 'special', text: 'One special symbol (e.g. @, #, $, %)' }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen grid grid-cols-1 lg:grid-cols-12 bg-[#F8FAFC] font-sans selection:bg-blue-100 selection:text-[#185FA5]">
|
||||
<Head title="Secure Password Setup - Step 3" />
|
||||
|
||||
{/* Left Brand Panel */}
|
||||
<div className="hidden lg:flex lg:col-span-4 bg-[#185FA5] p-16 flex-col justify-between text-white relative overflow-hidden select-none sticky top-0 h-screen">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_right,rgba(255,255,255,0.1),transparent)] pointer-events-none" />
|
||||
|
||||
<div className="relative z-10 space-y-10">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-12 h-12 bg-white text-[#185FA5] rounded-2xl flex items-center justify-center font-black text-2xl shadow-2xl">
|
||||
M
|
||||
</div>
|
||||
<span className="font-black text-2xl tracking-tighter uppercase">Marketplace</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6 pt-10">
|
||||
<h1 className="text-5xl font-black tracking-tighter leading-[1.1] drop-shadow-sm uppercase">
|
||||
Establish Secure Credentials.
|
||||
</h1>
|
||||
<p className="text-blue-100 text-lg font-medium leading-relaxed opacity-90">
|
||||
Establish a robust password to ensure protection of your domestic hiring workspace.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 text-[10px] font-black uppercase tracking-[0.3em] text-blue-300 opacity-60">
|
||||
Empowering Household Recruitment Since 2024
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Form Content */}
|
||||
<div className="col-span-1 lg:col-span-8 flex flex-col items-center py-12 px-6 sm:px-12 overflow-y-auto justify-center">
|
||||
<div className="w-full max-w-xl">
|
||||
<div className="text-center mb-10">
|
||||
<h2 className="text-3xl font-black text-slate-900 tracking-tight uppercase">Security Configuration</h2>
|
||||
<p className="text-sm font-bold text-slate-400 mt-2 uppercase tracking-widest">
|
||||
Step 3 of 3: Configure Portal Access Key
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{renderStepIndicator()}
|
||||
|
||||
<div className="bg-white rounded-[40px] shadow-[0_20px_50px_rgba(0,0,0,0.04)] border border-slate-100 p-8 sm:p-12 transition-all duration-500 animate-in fade-in slide-in-from-bottom-4">
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
|
||||
{/* Password input */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Secure Password</label>
|
||||
<div className="relative group">
|
||||
<Lock className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 group-focus-within:text-[#185FA5] transition-colors" />
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={data.password}
|
||||
onChange={(e) => handleInputChange('password', e.target.value)}
|
||||
placeholder="••••••••"
|
||||
className={`w-full pl-12 pr-12 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 ? 'border-rose-400 bg-rose-50/20' : 'border-transparent'
|
||||
}`}
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2 p-2 text-slate-400 hover:text-slate-600 cursor-pointer"
|
||||
>
|
||||
{showPassword ? <EyeOff className="w-4.5 h-4.5" /> : <Eye className="w-4.5 h-4.5" />}
|
||||
</button>
|
||||
</div>
|
||||
{errors.password && (
|
||||
<p className="text-xs text-rose-500 font-bold ml-1 uppercase tracking-wider">{errors.password[0] || errors.password}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Confirm Password input */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Confirm Access Key</label>
|
||||
<div className="relative group">
|
||||
<Lock className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 group-focus-within:text-[#185FA5] transition-colors" />
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={data.password_confirmation}
|
||||
onChange={(e) => 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
|
||||
/>
|
||||
</div>
|
||||
{errors.password_confirmation && (
|
||||
<p className="text-xs text-rose-500 font-bold ml-1 uppercase tracking-wider">{errors.password_confirmation[0] || errors.password_confirmation}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Password Rules Checklist */}
|
||||
<div className="bg-slate-50 p-6 rounded-3xl border border-slate-100 space-y-3">
|
||||
<h4 className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Password Complexity Checklist</h4>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-x-4 gap-y-2">
|
||||
{rules.map((rule) => {
|
||||
const isMet = strength[rule.key];
|
||||
return (
|
||||
<div key={rule.key} className="flex items-center space-x-2.5">
|
||||
<div className={`w-4 h-4 rounded-full flex items-center justify-center border transition-all ${
|
||||
isMet
|
||||
? 'bg-emerald-500 border-emerald-600 text-white'
|
||||
: 'bg-white border-slate-200 text-slate-300'
|
||||
}`}>
|
||||
{isMet ? <Check className="w-2.5 h-2.5" /> : null}
|
||||
</div>
|
||||
<span className={`text-[10px] font-bold ${
|
||||
isMet ? 'text-slate-600' : 'text-slate-400'
|
||||
}`}>{rule.text}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || !isAllCriteriaMet || data.password !== data.password_confirmation}
|
||||
className="w-full bg-emerald-600 hover:bg-emerald-700 disabled:opacity-50 disabled:hover:bg-emerald-600 text-white py-5 rounded-[24px] font-black text-xs uppercase tracking-[0.2em] transition-all shadow-xl shadow-emerald-500/20 flex items-center justify-center space-x-2 mt-8 select-none cursor-pointer"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="w-5 h-5 animate-spin mr-2" />
|
||||
<span>Saving Employer Profile...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>Submit & Activate Account</span>
|
||||
<ShieldCheck className="w-4 h-4" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="mt-8 text-center select-none">
|
||||
<Link href="/employer/register" className="inline-flex items-center space-x-2 text-xs font-bold text-slate-400 hover:text-slate-600 uppercase tracking-widest">
|
||||
<X className="w-4 h-4" />
|
||||
<span>Cancel Registration</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -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
|
||||
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 = () => (
|
||||
<div className="flex items-center justify-between mb-12 relative max-w-md mx-auto px-4">
|
||||
<div className="flex items-center justify-between mb-6 relative max-w-xs mx-auto px-2 select-none">
|
||||
<div className="absolute top-1/2 left-0 w-full h-0.5 bg-slate-100 -translate-y-1/2 -z-10" />
|
||||
<div className="absolute top-1/2 left-0 h-0.5 bg-[#185FA5] -translate-y-1/2 -z-10 transition-all duration-500" style={{ width: `${(step - 1) * 33.33}%` }} />
|
||||
|
||||
{[1, 2, 3, 4].map((s) => (
|
||||
<div key={s} className="flex flex-col items-center">
|
||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center font-black text-sm transition-all duration-500 border-4 ${
|
||||
step >= 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) => (
|
||||
<div key={s.step} className="flex flex-col items-center">
|
||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center font-bold text-xs border-2 transition-all duration-300 ${
|
||||
s.step === 1
|
||||
? 'bg-[#185FA5] text-white border-[#185FA5] shadow-sm'
|
||||
: 'bg-white text-slate-400 border-slate-200'
|
||||
}`}>
|
||||
{step > s ? <Check className="w-5 h-5" /> : s}
|
||||
{s.step}
|
||||
</div>
|
||||
<span className={`text-[9px] font-semibold uppercase mt-1 tracking-wider ${
|
||||
s.step === 1 ? 'text-[#185FA5]' : 'text-slate-400'
|
||||
}`}>{s.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen grid grid-cols-1 lg:grid-cols-12 bg-[#F8FAFC] font-sans selection:bg-blue-100 selection:text-[#185FA5]">
|
||||
<Head title="Employer Registration - Secure Portal" />
|
||||
<div className="min-h-screen grid grid-cols-1 lg:grid-cols-12 bg-slate-50 font-sans">
|
||||
<Head title="Employer Registration - Marketplace Portal" />
|
||||
|
||||
{/* Left Brand Panel */}
|
||||
<div className="hidden lg:flex lg:col-span-4 bg-[#185FA5] p-16 flex-col justify-between text-white relative overflow-hidden select-none sticky top-0 h-screen">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_right,rgba(255,255,255,0.1),transparent)] pointer-events-none" />
|
||||
<div className="absolute bottom-0 left-0 right-0 h-64 bg-gradient-to-t from-black/20 to-transparent pointer-events-none" />
|
||||
{/* Left Brand Panel (Desktop Only) */}
|
||||
<div className="hidden lg:flex lg:col-span-5 bg-[#185FA5] p-12 flex-col justify-between text-white relative overflow-hidden select-none">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-blue-600/20 to-black/30 pointer-events-none" />
|
||||
|
||||
<div className="relative z-10 space-y-10">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-12 h-12 bg-white text-[#185FA5] rounded-2xl flex items-center justify-center font-black text-2xl shadow-2xl">
|
||||
<div className="relative z-10 space-y-6">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-10 h-10 bg-white text-[#185FA5] rounded-xl flex items-center justify-center font-bold text-xl shadow-md">
|
||||
M
|
||||
</div>
|
||||
<span className="font-black text-2xl tracking-tighter uppercase">Marketplace</span>
|
||||
<span className="font-bold text-2xl tracking-tight">Marketplace Portal</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6 pt-10">
|
||||
<h1 className="text-5xl font-black tracking-tighter leading-[1.1] drop-shadow-sm">
|
||||
The Secure Hub for Direct Hiring.
|
||||
<div className="space-y-3 pt-6">
|
||||
<span className="inline-block px-3 py-1 bg-white/10 border border-white/20 rounded-full text-xs font-semibold uppercase tracking-wider">
|
||||
Employer Registration
|
||||
</span>
|
||||
<h1 className="text-4xl font-extrabold tracking-tight leading-tight">
|
||||
Find verified domestic workers — no agents, no fees.
|
||||
</h1>
|
||||
<p className="text-blue-100 text-lg font-medium leading-relaxed opacity-90">
|
||||
Join 12,000+ employers hiring domestic workers directly with verified documentation.
|
||||
<p className="text-blue-100 text-sm font-medium">
|
||||
Register to review your shortlisted candidates, manage interview schedules, and communicate directly.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6 pt-12 border-t border-white/10">
|
||||
{[
|
||||
{ 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) => (
|
||||
<div key={i} className="flex items-start space-x-4 group">
|
||||
<div className="p-2.5 bg-white/10 rounded-xl group-hover:bg-white/20 transition-colors">
|
||||
<feat.icon className="w-6 h-6 text-emerald-300" />
|
||||
{/* Stat Row */}
|
||||
<div className="grid grid-cols-3 gap-4 pt-8 border-t border-white/10">
|
||||
<div className="bg-white/10 backdrop-blur-sm p-4 rounded-xl border border-white/10 text-center space-y-1">
|
||||
<Users className="w-6 h-6 text-emerald-300 mx-auto" />
|
||||
<div className="font-bold text-lg">500+</div>
|
||||
<div className="text-[10px] text-blue-100 uppercase tracking-wider font-semibold">Verified Workers</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white/10 backdrop-blur-sm p-4 rounded-xl border border-white/10 text-center space-y-1">
|
||||
<DollarSign className="w-6 h-6 text-emerald-300 mx-auto" />
|
||||
<div className="font-bold text-lg">0 AED</div>
|
||||
<div className="text-[10px] text-blue-100 uppercase tracking-wider font-semibold">Agent Fees</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white/10 backdrop-blur-sm p-4 rounded-xl border border-white/10 text-center space-y-1">
|
||||
<MessageSquare className="w-6 h-6 text-emerald-300 mx-auto" />
|
||||
<div className="font-bold text-lg">Direct</div>
|
||||
<div className="text-[10px] text-blue-100 uppercase tracking-wider font-semibold">Messaging</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 text-xs text-blue-200 font-medium">
|
||||
© 2026 Domestic Worker Marketplace. All rights reserved.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Registration Form */}
|
||||
<div className="col-span-1 lg:col-span-7 flex flex-col justify-center items-center p-6 sm:p-12 overflow-y-auto">
|
||||
<div className="w-full max-w-lg bg-white rounded-2xl shadow-sm border border-slate-200 p-8 space-y-6">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div>
|
||||
<h3 className="font-black text-base uppercase tracking-wider">{feat.title}</h3>
|
||||
<p className="text-sm text-blue-100 font-medium opacity-80">{feat.desc}</p>
|
||||
<h2 className="text-2xl font-bold text-gray-900 tracking-tight">Create Account</h2>
|
||||
<p className="text-xs text-gray-500 mt-1">Register your employer portal to start searching</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 text-[10px] font-black uppercase tracking-[0.3em] text-blue-300 opacity-60">
|
||||
Empowering Household Recruitment Since 2024
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Registration Content */}
|
||||
<div className="col-span-1 lg:col-span-8 flex flex-col items-center py-12 px-6 sm:px-12 overflow-y-auto">
|
||||
<div className="w-full max-w-2xl">
|
||||
<div className="text-center mb-10">
|
||||
<h2 className="text-3xl font-black text-slate-900 tracking-tight uppercase">Employer Enrollment</h2>
|
||||
<p className="text-sm font-bold text-slate-400 mt-2 uppercase tracking-widest">Step {step} of 4: {
|
||||
step === 1 ? 'Account Security' :
|
||||
step === 2 ? 'Identity Verification' :
|
||||
step === 3 ? 'Choose Your Plan' : 'Secure Payment'
|
||||
}</p>
|
||||
</div>
|
||||
|
||||
{renderStepIndicator()}
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-[40px] shadow-[0_20px_50px_rgba(0,0,0,0.04)] border border-slate-100 p-8 sm:p-12 transition-all duration-500 animate-in fade-in slide-in-from-bottom-4">
|
||||
<form onSubmit={handleSubmit} className="space-y-8">
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{/* Company Name */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">Company / Household Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={data.company_name}
|
||||
onChange={(e) => 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 && (
|
||||
<p className="mt-1 text-xs text-red-500">{errors.company_name[0] || errors.company_name}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Step 1: Account Creation */}
|
||||
{step === 1 && (
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="md:col-span-2 space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Full Legal Name</label>
|
||||
<div className="relative group">
|
||||
<User className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 group-focus-within:text-[#185FA5] transition-colors" />
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{/* Contact Name */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">Contact Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={data.name}
|
||||
onChange={(e) => setData('name', e.target.value)}
|
||||
onChange={(e) => handleInputChange('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"
|
||||
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
|
||||
/>
|
||||
</div>
|
||||
{errors.name && (
|
||||
<p className="mt-1 text-xs text-red-500">{errors.name[0] || errors.name}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Work Email</label>
|
||||
<div className="relative group">
|
||||
<Mail className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 group-focus-within:text-[#185FA5] transition-colors" />
|
||||
{/* Work Email */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">Work Email</label>
|
||||
<input
|
||||
type="email"
|
||||
value={data.email}
|
||||
onChange={(e) => setData('email', e.target.value)}
|
||||
onChange={(e) => handleInputChange('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
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Mobile Access</label>
|
||||
<div className="relative group">
|
||||
<PhoneIcon className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 group-focus-within:text-[#185FA5] transition-colors" />
|
||||
<input
|
||||
type="text"
|
||||
value={data.phone}
|
||||
onChange={(e) => 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
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Secure Password</label>
|
||||
<div className="relative group">
|
||||
<Lock className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 group-focus-within:text-[#185FA5] transition-colors" />
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={data.password}
|
||||
onChange={(e) => 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
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2 p-2 text-slate-400 hover:text-slate-600"
|
||||
>
|
||||
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Confirm Access</label>
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={data.password_confirmation}
|
||||
onChange={(e) => 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
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={nextStep}
|
||||
className="w-full bg-[#185FA5] hover:bg-[#144f8a] text-white py-5 rounded-[24px] font-black text-xs uppercase tracking-[0.2em] transition-all shadow-xl shadow-blue-500/20 flex items-center justify-center space-x-2 mt-8"
|
||||
>
|
||||
<span>Verify Identity</span>
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 2: Emirates ID */}
|
||||
{step === 2 && (
|
||||
<div className="space-y-8">
|
||||
<div className="bg-amber-50 border border-amber-100 p-6 rounded-3xl flex items-start space-x-4">
|
||||
<AlertCircle className="w-6 h-6 text-amber-600 shrink-0" />
|
||||
<p className="text-[11px] font-bold text-amber-800 leading-relaxed uppercase tracking-wider">
|
||||
In compliance with UAE Labor Law, all employers must verify their identity using a valid Emirates ID before accessing the marketplace.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
{[
|
||||
{ field: 'emirates_id_front', label: 'Emirates ID Front', file: frontFileName },
|
||||
{ field: 'emirates_id_back', label: 'Emirates ID Back', file: backFileName }
|
||||
].map((slot) => (
|
||||
<div key={slot.field} className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">{slot.label}</label>
|
||||
{slot.file ? (
|
||||
<div className="relative aspect-[1.6/1] bg-slate-50 rounded-[32px] border-2 border-[#185FA5] flex flex-col items-center justify-center p-6 text-center shadow-lg shadow-blue-500/5">
|
||||
<FileText className="w-10 h-10 text-[#185FA5] mb-2" />
|
||||
<span className="text-[10px] font-black text-slate-900 uppercase truncate max-w-full px-4">{slot.file}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeFile(slot.field)}
|
||||
className="absolute -top-2 -right-2 bg-rose-500 text-white p-2 rounded-full shadow-lg hover:bg-rose-600 transition-colors"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<label className="relative aspect-[1.6/1] bg-slate-50 rounded-[32px] border-4 border-dashed border-slate-200 flex flex-col items-center justify-center p-8 text-center cursor-pointer hover:border-[#185FA5] hover:bg-blue-50 transition-all group">
|
||||
<div className="w-16 h-16 bg-white rounded-2xl flex items-center justify-center shadow-md mb-4 group-hover:scale-110 transition-transform">
|
||||
<Camera className="w-8 h-8 text-slate-300 group-hover:text-[#185FA5]" />
|
||||
</div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest group-hover:text-[#185FA5]">Tap to upload</span>
|
||||
<input
|
||||
type="file"
|
||||
accept=".jpg,.jpeg,.png,.pdf"
|
||||
className="hidden"
|
||||
onChange={(e) => handleFileChange(e, slot.field)}
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 pt-6">
|
||||
<button
|
||||
type="button"
|
||||
onClick={prevStep}
|
||||
className="flex-1 bg-slate-100 hover:bg-slate-200 text-slate-600 py-5 rounded-[24px] font-black text-xs uppercase tracking-[0.2em] transition-all flex items-center justify-center space-x-2"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
<span>Back</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={nextStep}
|
||||
disabled={!frontFileName || !backFileName}
|
||||
className="flex-[2] bg-[#185FA5] hover:bg-[#144f8a] disabled:opacity-50 text-white py-5 rounded-[24px] font-black text-xs uppercase tracking-[0.2em] transition-all shadow-xl shadow-blue-500/20 flex items-center justify-center space-x-2"
|
||||
>
|
||||
<span>Select Plan</span>
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 3: Plan Selection */}
|
||||
{step === 3 && (
|
||||
<div className="space-y-8">
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{plans.map((p) => (
|
||||
<button
|
||||
key={p.id}
|
||||
type="button"
|
||||
onClick={() => setData('selected_plan', p.id)}
|
||||
className={`p-6 rounded-[32px] border-4 text-left transition-all relative overflow-hidden flex items-center justify-between group ${
|
||||
data.selected_plan === p.id
|
||||
? 'bg-blue-50 border-[#185FA5] shadow-xl shadow-blue-500/10'
|
||||
: 'bg-white border-slate-50 hover:border-slate-200'
|
||||
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]'
|
||||
}`}
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center space-x-3">
|
||||
<h4 className="text-lg font-black text-slate-900 uppercase tracking-tight">{p.name}</h4>
|
||||
{p.popular && <span className="bg-emerald-500 text-white px-2 py-0.5 rounded-full text-[9px] font-black uppercase">Most Popular</span>}
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
{p.features.slice(0, 2).map((f, i) => (
|
||||
<div key={i} className="flex items-center space-x-1.5 text-[10px] font-bold text-slate-500">
|
||||
<CheckCircle className="w-3.5 h-3.5 text-emerald-500" />
|
||||
<span>{f}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-2xl font-black text-[#185FA5]">{p.price} <span className="text-[10px] text-slate-400 font-bold uppercase tracking-widest">AED</span></div>
|
||||
<div className="text-[9px] font-black text-slate-400 uppercase tracking-widest mt-1">One Time Access</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 pt-6">
|
||||
<button
|
||||
type="button"
|
||||
onClick={prevStep}
|
||||
className="flex-1 bg-slate-100 hover:bg-slate-200 text-slate-600 py-5 rounded-[24px] font-black text-xs uppercase tracking-[0.2em] transition-all flex items-center justify-center space-x-2"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
<span>Back</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={nextStep}
|
||||
className="flex-[2] bg-[#185FA5] hover:bg-[#144f8a] text-white py-5 rounded-[24px] font-black text-xs uppercase tracking-[0.2em] transition-all shadow-xl shadow-blue-500/20 flex items-center justify-center space-x-2"
|
||||
>
|
||||
<span>Pay & Register</span>
|
||||
<CreditCard className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
required
|
||||
/>
|
||||
{errors.email && (
|
||||
<p className="mt-1 text-xs text-red-500">{errors.email[0] || errors.email}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Step 4: Secure Payment */}
|
||||
{step === 4 && (
|
||||
<div className="space-y-8 animate-in zoom-in-95 duration-500">
|
||||
<div className="bg-slate-900 rounded-[32px] p-8 text-white relative overflow-hidden shadow-2xl">
|
||||
<div className="absolute top-0 right-0 w-64 h-64 bg-blue-500/20 rounded-full blur-3xl -mr-32 -mt-32" />
|
||||
<div className="flex justify-between items-start mb-12 relative z-10">
|
||||
<div className="w-12 h-10 bg-slate-800 rounded-lg border border-slate-700 flex items-center justify-center">
|
||||
<div className="w-8 h-6 bg-gradient-to-br from-amber-200 to-amber-500 rounded" />
|
||||
</div>
|
||||
<CreditCard className="w-10 h-10 text-white/20" />
|
||||
</div>
|
||||
<div className="space-y-4 relative z-10">
|
||||
<div className="text-xl font-medium tracking-[0.2em] h-8 font-mono">
|
||||
{data.card_number || '•••• •••• •••• ••••'}
|
||||
</div>
|
||||
<div className="flex justify-between items-end">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{/* Mobile Number */}
|
||||
<div>
|
||||
<div className="text-[10px] font-black text-white/40 uppercase tracking-widest mb-1">Card Holder</div>
|
||||
<div className="text-sm font-bold uppercase tracking-widest">{data.name || 'Your Name'}</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-[10px] font-black text-white/40 uppercase tracking-widest mb-1">Expires</div>
|
||||
<div className="text-sm font-bold uppercase tracking-widest">{data.expiry || 'MM/YY'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Card Number</label>
|
||||
<div className="relative group">
|
||||
<CreditCard className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 group-focus-within:text-[#185FA5] transition-colors" />
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">Mobile Number</label>
|
||||
<input
|
||||
type="text"
|
||||
maxLength="19"
|
||||
value={data.card_number}
|
||||
onChange={(e) => 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"
|
||||
type="tel"
|
||||
value={data.phone}
|
||||
onChange={(e) => 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
|
||||
/>
|
||||
</div>
|
||||
{errors.phone && (
|
||||
<p className="mt-1 text-xs text-red-500">{errors.phone[0] || errors.phone}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Expiry Date</label>
|
||||
<input
|
||||
type="text"
|
||||
maxLength="5"
|
||||
value={data.expiry}
|
||||
onChange={(e) => 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
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Security Code</label>
|
||||
<input
|
||||
type="password"
|
||||
maxLength="4"
|
||||
value={data.cvc}
|
||||
onChange={(e) => 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
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 bg-blue-50 rounded-3xl border border-blue-100 flex items-center justify-between">
|
||||
{/* Country Dropdown */}
|
||||
<div>
|
||||
<div className="text-[10px] font-black text-blue-600 uppercase tracking-widest">Total Amount</div>
|
||||
<div className="text-xl font-black text-slate-900">{plans.find(p => p.id === data.selected_plan)?.price} AED</div>
|
||||
</div>
|
||||
<ShieldCheck className="w-8 h-8 text-[#185FA5] opacity-20" />
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={prevStep}
|
||||
className="flex-1 bg-slate-100 hover:bg-slate-200 text-slate-600 py-5 rounded-[24px] font-black text-xs uppercase tracking-[0.2em] transition-all flex items-center justify-center space-x-2"
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">Country of Residence</label>
|
||||
<select
|
||||
value={data.country}
|
||||
onChange={(e) => handleInputChange('country', e.target.value)}
|
||||
className="w-full px-3.5 py-2.5 rounded-xl border border-slate-300 text-sm focus:outline-none focus:ring-2 focus:ring-[#185FA5]/20 focus:border-[#185FA5] bg-white cursor-pointer appearance-none"
|
||||
required
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
<span>Back</span>
|
||||
</button>
|
||||
{countries.map((c) => (
|
||||
<option key={c} value={c}>
|
||||
{c}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{errors.country && (
|
||||
<p className="mt-1 text-xs text-red-500">{errors.country[0] || errors.country}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={processing}
|
||||
className="flex-[2] bg-emerald-600 hover:bg-emerald-700 active:bg-emerald-800 text-white py-5 rounded-[24px] font-black text-xs uppercase tracking-[0.2em] transition-all shadow-xl shadow-emerald-500/20 flex items-center justify-center space-x-2"
|
||||
disabled={loading}
|
||||
className="w-full bg-[#185FA5] hover:bg-[#0C447C] active:bg-[#08305c] text-white rounded-xl h-11 font-semibold text-sm transition-colors shadow-sm flex items-center justify-center disabled:opacity-70 disabled:cursor-not-allowed mt-2"
|
||||
>
|
||||
{processing ? <Loader2 className="w-5 h-5 animate-spin mr-2" /> : (
|
||||
<>
|
||||
<span>Complete & Pay</span>
|
||||
<Lock className="w-4 h-4" />
|
||||
</>
|
||||
{loading ? (
|
||||
<Loader2 className="w-5 h-5 animate-spin mr-2" />
|
||||
) : (
|
||||
'Send Verification Code'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-[10px] text-slate-400 text-center font-bold uppercase tracking-widest">
|
||||
Secured by 256-bit AES encryption
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="mt-12 text-center">
|
||||
<p className="text-xs font-bold text-slate-400 uppercase tracking-widest">
|
||||
Already part of the network?{' '}
|
||||
<Link href="/employer/login" className="text-[#185FA5] hover:underline">
|
||||
Authorize Account
|
||||
<div className="border-t border-slate-100 pt-6 text-center">
|
||||
<p className="text-xs text-gray-600 font-medium">
|
||||
Already have an account?{' '}
|
||||
<Link href="/employer/login" className="text-[#185FA5] font-bold hover:underline">
|
||||
Sign in
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
263
resources/js/Pages/Employer/Auth/VerifyEmail.jsx
Normal file
263
resources/js/Pages/Employer/Auth/VerifyEmail.jsx
Normal file
@ -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 = () => (
|
||||
<div className="flex items-center justify-between mb-12 relative max-w-md mx-auto px-4 select-none">
|
||||
<div className="absolute top-1/2 left-0 w-full h-0.5 bg-slate-100 -translate-y-1/2 -z-10" />
|
||||
<div className="absolute top-1/2 left-0 h-0.5 bg-[#185FA5] -translate-y-1/2 -z-10 transition-all duration-500" style={{ width: '50%' }} />
|
||||
|
||||
{[
|
||||
{ step: 1, label: 'Register' },
|
||||
{ step: 2, label: 'Verify Code' },
|
||||
{ step: 3, label: 'Set Password' }
|
||||
].map((s) => (
|
||||
<div key={s.step} className="flex flex-col items-center">
|
||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center font-black text-sm border-4 transition-all duration-300 ${
|
||||
s.step <= 2
|
||||
? 'bg-[#185FA5] text-white border-blue-100 scale-110 shadow-lg'
|
||||
: 'bg-white text-slate-400 border-slate-50'
|
||||
}`}>
|
||||
{s.step}
|
||||
</div>
|
||||
<span className={`text-[9px] font-black uppercase mt-2 tracking-wider ${
|
||||
s.step <= 2 ? 'text-[#185FA5]' : 'text-slate-400'
|
||||
}`}>{s.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen grid grid-cols-1 lg:grid-cols-12 bg-[#F8FAFC] font-sans selection:bg-blue-100 selection:text-[#185FA5]">
|
||||
<Head title="Verify Email - Step 2" />
|
||||
|
||||
{/* Left Brand Panel */}
|
||||
<div className="hidden lg:flex lg:col-span-4 bg-[#185FA5] p-16 flex-col justify-between text-white relative overflow-hidden select-none sticky top-0 h-screen">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_right,rgba(255,255,255,0.1),transparent)] pointer-events-none" />
|
||||
|
||||
<div className="relative z-10 space-y-10">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-12 h-12 bg-white text-[#185FA5] rounded-2xl flex items-center justify-center font-black text-2xl shadow-2xl">
|
||||
M
|
||||
</div>
|
||||
<span className="font-black text-2xl tracking-tighter uppercase">Marketplace</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6 pt-10">
|
||||
<h1 className="text-5xl font-black tracking-tighter leading-[1.1] drop-shadow-sm uppercase">
|
||||
Verify Your Business Email.
|
||||
</h1>
|
||||
<p className="text-blue-100 text-lg font-medium leading-relaxed opacity-90">
|
||||
We take platform trust seriously. Verify your email to ensure secure account setup.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 text-[10px] font-black uppercase tracking-[0.3em] text-blue-300 opacity-60">
|
||||
Empowering Household Recruitment Since 2024
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Form Content */}
|
||||
<div className="col-span-1 lg:col-span-8 flex flex-col items-center py-12 px-6 sm:px-12 overflow-y-auto justify-center">
|
||||
<div className="w-full max-w-xl">
|
||||
<div className="text-center mb-10">
|
||||
<h2 className="text-3xl font-black text-slate-900 tracking-tight uppercase">Email Verification</h2>
|
||||
<p className="text-sm font-bold text-slate-400 mt-2 uppercase tracking-widest">
|
||||
Step 2 of 3: Enter Hashed Passcode
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{renderStepIndicator()}
|
||||
|
||||
<div className="bg-white rounded-[40px] shadow-[0_20px_50px_rgba(0,0,0,0.04)] border border-slate-100 p-8 sm:p-12 transition-all duration-500 animate-in fade-in slide-in-from-bottom-4">
|
||||
|
||||
<div className="text-center mb-8">
|
||||
<div className="w-16 h-16 bg-blue-50 text-[#185FA5] rounded-3xl flex items-center justify-center mx-auto mb-4 border border-blue-100 shadow-sm animate-pulse">
|
||||
<MailCheck className="w-8 h-8" />
|
||||
</div>
|
||||
<h3 className="text-lg font-black text-slate-900 uppercase">Check Your Email</h3>
|
||||
<p className="text-xs font-bold text-slate-400 mt-1 leading-relaxed">
|
||||
We dispatched a 6-digit confirmation code to:
|
||||
<br />
|
||||
<span className="text-slate-800 font-extrabold lowercase text-sm mt-1 inline-block">{email}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
|
||||
<div className="space-y-3">
|
||||
<label className="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1 text-center block">6-Digit Passcode</label>
|
||||
<div className="relative max-w-xs mx-auto group">
|
||||
<KeyRound className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 group-focus-within:text-[#185FA5] transition-colors" />
|
||||
<input
|
||||
type="text"
|
||||
pattern="[0-9]*"
|
||||
inputMode="numeric"
|
||||
maxLength="6"
|
||||
value={otp}
|
||||
onChange={handleOtpChange}
|
||||
placeholder="••••••"
|
||||
className={`w-full pl-12 pr-4 py-5 bg-slate-50 border rounded-2xl text-center text-2xl font-black tracking-[0.25em] focus:ring-4 focus:ring-blue-100 focus:bg-white transition-all outline-none ${
|
||||
errors.otp ? 'border-rose-400 bg-rose-50/20' : 'border-transparent'
|
||||
}`}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{errors.otp && (
|
||||
<p className="text-xs text-rose-500 font-bold text-center uppercase tracking-wider">{errors.otp}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || otp.length !== 6}
|
||||
className="w-full bg-[#185FA5] hover:bg-[#144f8a] disabled:opacity-70 text-white py-5 rounded-[24px] font-black text-xs uppercase tracking-[0.2em] transition-all shadow-xl shadow-blue-500/20 flex items-center justify-center space-x-2 mt-8 select-none cursor-pointer"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="w-5 h-5 animate-spin mr-2" />
|
||||
<span>Verifying Code...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>Confirm Verification</span>
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div className="text-center pt-4 border-t border-slate-100">
|
||||
{cooldown > 0 ? (
|
||||
<p className="text-[10px] font-black text-slate-400 uppercase tracking-wider">
|
||||
Resend Code available in <span className="text-[#185FA5] font-extrabold">{cooldown}s</span>
|
||||
</p>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleResend}
|
||||
disabled={resending}
|
||||
className="inline-flex items-center space-x-1.5 text-xs font-black text-[#185FA5] hover:text-[#144f8a] uppercase tracking-wider cursor-pointer"
|
||||
>
|
||||
{resending ? (
|
||||
<>
|
||||
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
||||
<span>Requesting...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RefreshCw className="w-3.5 h-3.5" />
|
||||
<span>Resend Passcode</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="mt-8 text-center select-none">
|
||||
<Link href="/employer/register" className="inline-flex items-center space-x-2 text-xs font-bold text-slate-400 hover:text-slate-600 uppercase tracking-widest">
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
<span>Change Email / Back</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -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 (
|
||||
|
||||
@ -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]);
|
||||
router.post(`/employer/messages/${conversation.id}/send`, {
|
||||
text: text
|
||||
}, {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
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);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleFormSubmit = (e) => {
|
||||
|
||||
@ -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();
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
{errors.name && <p className="text-rose-500 text-[10px] font-bold mt-1 ml-1">{errors.name}</p>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
@ -141,6 +151,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"
|
||||
/>
|
||||
</div>
|
||||
{errors.email && <p className="text-rose-500 text-[10px] font-bold mt-1 ml-1">{errors.email}</p>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
@ -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"
|
||||
/>
|
||||
</div>
|
||||
{errors.phone && <p className="text-rose-500 text-[10px] font-bold mt-1 ml-1">{errors.phone}</p>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
@ -169,6 +181,7 @@ export default function Profile({ employerProfile }) {
|
||||
<option>Arabic</option>
|
||||
</select>
|
||||
</div>
|
||||
{errors.language && <p className="text-rose-500 text-[10px] font-bold mt-1 ml-1">{errors.language}</p>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -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"
|
||||
/>
|
||||
</div>
|
||||
{errors.company_name && <p className="text-rose-500 text-[10px] font-bold mt-1 ml-1">{errors.company_name}</p>}
|
||||
</div>
|
||||
|
||||
<div className="p-6 bg-slate-50 rounded-3xl border border-slate-100 flex items-start space-x-4">
|
||||
@ -223,15 +237,36 @@ export default function Profile({ employerProfile }) {
|
||||
<div className="space-y-4 max-w-md">
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-black text-slate-500 uppercase tracking-tighter ml-1">Current Password</label>
|
||||
<input type="password" placeholder="••••••••" 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" />
|
||||
<input
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
value={profile.current_password}
|
||||
onChange={(e) => 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 && <p className="text-rose-500 text-[10px] font-bold mt-1 ml-1">{errors.current_password}</p>}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-black text-slate-500 uppercase tracking-tighter ml-1">New Password</label>
|
||||
<input type="password" placeholder="••••••••" 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" />
|
||||
<input
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
value={profile.new_password}
|
||||
onChange={(e) => 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 && <p className="text-rose-500 text-[10px] font-bold mt-1 ml-1">{errors.new_password}</p>}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-black text-slate-500 uppercase tracking-tighter ml-1">Confirm New Password</label>
|
||||
<input type="password" placeholder="••••••••" 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" />
|
||||
<input
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
value={profile.new_password_confirmation}
|
||||
onChange={(e) => 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 && <p className="text-rose-500 text-[10px] font-bold mt-1 ml-1">{errors.new_password_confirmation}</p>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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 }) {
|
||||
<DropdownMenuContent align="start" className="w-44 p-2 rounded-xl">
|
||||
<DropdownMenuLabel className="text-[10px] font-black text-slate-400 uppercase tracking-widest px-2 py-1.5">Change Status</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="text-xs font-bold text-slate-600 focus:text-amber-700 focus:bg-amber-50 rounded-lg">
|
||||
<DropdownMenuItem onClick={() => 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
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="text-xs font-bold text-slate-600 focus:text-blue-700 focus:bg-blue-50 rounded-lg">
|
||||
<DropdownMenuItem onClick={() => 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
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="text-xs font-bold text-slate-600 focus:text-emerald-700 focus:bg-emerald-50 rounded-lg">
|
||||
<DropdownMenuItem onClick={() => 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
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="text-xs font-bold text-rose-600 focus:text-rose-700 focus:bg-rose-50 rounded-lg">
|
||||
<DropdownMenuItem onClick={() => 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
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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 = () => {
|
||||
|
||||
@ -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 (
|
||||
<EmployerMobileLayout>
|
||||
<Head title="Announcements" />
|
||||
|
||||
<div className="p-6 space-y-8">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-2xl font-bold tracking-tight">Broadcasts</h1>
|
||||
<p className="text-sm text-slate-500 font-medium">Important updates & announcements</p>
|
||||
</div>
|
||||
<button className="w-12 h-12 bg-[#141B34] text-white rounded-2xl flex items-center justify-center shadow-lg active:scale-95 transition-all">
|
||||
<Plus className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="relative group">
|
||||
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-300 group-focus-within:text-[#141B34] transition-colors" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search announcements..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Announcement List */}
|
||||
<div className="space-y-4 pb-10">
|
||||
{announcements.map((item) => (
|
||||
<div key={item.id} className="bg-white p-6 rounded-3xl border border-slate-100 shadow-sm space-y-4 active:scale-[0.98] transition-all group">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-blue-50 flex items-center justify-center text-blue-600">
|
||||
<Megaphone className="w-5 h-5" />
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
<h4 className="font-bold text-slate-900 text-sm leading-tight">{item.title}</h4>
|
||||
<div className="flex items-center space-x-1 text-[10px] font-bold text-slate-400 uppercase tracking-widest">
|
||||
<Calendar className="w-3 h-3" />
|
||||
<span>{item.date}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-slate-500 leading-relaxed font-medium line-clamp-2">
|
||||
{item.content}
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-between pt-2 border-t border-slate-50">
|
||||
<div className={`px-2.5 py-1 rounded-lg text-[9px] font-bold uppercase tracking-widest ${
|
||||
item.priority === 'High' ? 'bg-rose-50 text-rose-500 border border-rose-100' : 'bg-slate-50 text-slate-400 border border-slate-100'
|
||||
}`}>
|
||||
{item.priority} Priority
|
||||
</div>
|
||||
<button className="text-xs font-bold text-[#141B34] flex items-center space-x-1 group-hover:translate-x-1 transition-transform">
|
||||
<span>Read More</span>
|
||||
<ArrowRight className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</EmployerMobileLayout>
|
||||
);
|
||||
}
|
||||
@ -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 (
|
||||
<EmployerMobileLayout>
|
||||
<Head title="Candidates" />
|
||||
|
||||
<div className="p-6 space-y-8">
|
||||
{/* Page Title */}
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-2xl font-bold tracking-tight">Candidates</h1>
|
||||
<p className="text-sm text-slate-500 font-medium">Manage your workforce recruitment pipeline</p>
|
||||
</div>
|
||||
|
||||
{/* Stats Grid 2x2 */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{stats.map((stat, i) => (
|
||||
<div key={i} className="bg-white p-5 rounded-2xl border border-slate-100 shadow-sm space-y-3">
|
||||
<div className={`w-10 h-10 rounded-xl flex items-center justify-center ${stat.color}`}>
|
||||
<stat.icon className="w-5 h-5" />
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
<div className="text-xl font-bold">{stat.value}</div>
|
||||
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-widest leading-none">{stat.label}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Search & Filter */}
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="relative flex-1 group">
|
||||
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-300 group-focus-within:text-[#141B34] transition-colors" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search candidates..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
<button className="w-14 h-14 bg-white rounded-2xl flex items-center justify-center text-slate-400 border border-slate-200 active:scale-95 transition-all">
|
||||
<Filter className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Candidate List */}
|
||||
<div className="space-y-4 pb-10">
|
||||
{candidates.map((candidate) => (
|
||||
<div key={candidate.id} className="bg-white p-5 rounded-3xl border border-slate-100 shadow-sm space-y-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-14 h-14 rounded-full bg-slate-100 border border-slate-200 flex items-center justify-center text-[#141B34] font-bold text-xl">
|
||||
{candidate.name.charAt(0)}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<h4 className="font-bold text-slate-900 text-base">{candidate.name}</h4>
|
||||
<div className="text-[11px] font-semibold text-slate-400 uppercase tracking-wider">{candidate.country}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<button className="w-9 h-9 rounded-xl bg-slate-50 flex items-center justify-center text-slate-400 border border-slate-100">
|
||||
<MessageSquare className="w-4 h-4" />
|
||||
</button>
|
||||
<button className="w-9 h-9 rounded-xl bg-slate-50 flex items-center justify-center text-slate-400 border border-slate-100">
|
||||
<MoreHorizontal className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-2">
|
||||
<span className="px-3 py-1.5 bg-slate-50 text-slate-600 text-[10px] font-bold uppercase tracking-widest rounded-lg border border-slate-100">
|
||||
{candidate.role}
|
||||
</span>
|
||||
<div className="text-xs font-bold text-slate-900 ml-1">{candidate.salary}</div>
|
||||
</div>
|
||||
|
||||
<div className={`flex items-center space-x-2 px-3 py-1.5 rounded-full border text-[10px] font-bold uppercase tracking-widest ${getStatusStyles(candidate.status)}`}>
|
||||
<div className={`w-1.5 h-1.5 rounded-full ${getStatusDot(candidate.status)}`} />
|
||||
<span>{candidate.status}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</EmployerMobileLayout>
|
||||
);
|
||||
}
|
||||
@ -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 (
|
||||
<div className="min-h-screen bg-[#F8FAFC] flex flex-col max-w-md mx-auto relative font-sans text-slate-900">
|
||||
<Head title={`Chat with ${chatInfo.name}`} />
|
||||
|
||||
{/* Chat Header */}
|
||||
<div className="bg-white px-6 pt-12 pb-6 flex items-center justify-between border-b border-slate-100 sticky top-0 z-50">
|
||||
<div className="flex items-center space-x-4">
|
||||
<button onClick={() => window.history.back()} className="p-2 bg-slate-50 rounded-xl text-slate-400 border border-slate-100">
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
</button>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="relative">
|
||||
<div className="w-10 h-10 rounded-full bg-[#D8F3FF] flex items-center justify-center text-[#185FA5] font-bold text-sm">
|
||||
{chatInfo.initial}
|
||||
</div>
|
||||
{chatInfo.online && (
|
||||
<div className="absolute bottom-0 right-0 w-3 h-3 bg-emerald-500 border-2 border-white rounded-full shadow-sm" />
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
<h4 className="font-bold text-sm leading-none">{chatInfo.name}</h4>
|
||||
<p className="text-[10px] font-bold text-emerald-500 uppercase tracking-widest">Online Now</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<button className="p-2.5 text-slate-400 hover:text-[#185FA5] transition-colors">
|
||||
<MoreVertical className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Messages Area */}
|
||||
<div className="flex-1 overflow-y-auto p-6 space-y-6 no-scrollbar pb-32">
|
||||
<div className="flex justify-center">
|
||||
<span className="px-4 py-1.5 bg-slate-100 rounded-full text-[10px] font-bold text-slate-400 uppercase tracking-widest">Today</span>
|
||||
</div>
|
||||
|
||||
{messages.map((msg) => (
|
||||
<div key={msg.id} className={`flex ${msg.sender === 'me' ? 'justify-end' : 'justify-start'}`}>
|
||||
<div className={`max-w-[80%] space-y-1.5`}>
|
||||
<div className={`px-5 py-4 rounded-[28px] text-[13px] font-medium leading-relaxed shadow-sm ${
|
||||
msg.sender === 'me'
|
||||
? 'bg-[#185FA5] text-white rounded-tr-none'
|
||||
: 'bg-white text-slate-600 rounded-tl-none border border-slate-100'
|
||||
}`}>
|
||||
{msg.text}
|
||||
</div>
|
||||
<div className={`flex items-center space-x-1.5 px-2 ${msg.sender === 'me' ? 'justify-end' : 'justify-start'}`}>
|
||||
<span className="text-[9px] font-bold text-slate-300 uppercase tracking-widest">{msg.time}</span>
|
||||
{msg.sender === 'me' && <CheckCheck className="w-3 h-3 text-emerald-500" />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{/* Input Area */}
|
||||
<form onSubmit={handleSend} className="fixed bottom-0 left-0 right-0 max-w-md mx-auto p-6 bg-white/80 backdrop-blur-xl border-t border-slate-100">
|
||||
<div className="flex items-center space-x-3 bg-slate-50 border border-slate-100 rounded-[28px] px-4 py-2">
|
||||
<button type="button" className="p-2 text-slate-300 hover:text-[#185FA5] transition-colors">
|
||||
<Paperclip className="w-5 h-5" />
|
||||
</button>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Type a message..."
|
||||
value={message}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<div className="flex items-center space-x-1">
|
||||
<button type="button" className="p-2 text-slate-300">
|
||||
<Smile className="w-5 h-5" />
|
||||
</button>
|
||||
{message.trim() ? (
|
||||
<button type="submit" className="w-10 h-10 bg-[#185FA5] text-white rounded-full flex items-center justify-center shadow-lg shadow-blue-500/30 active:scale-90 transition-transform">
|
||||
<Send className="w-4 h-4 ml-0.5" />
|
||||
</button>
|
||||
) : (
|
||||
<button type="button" className="w-10 h-10 bg-slate-100 text-[#185FA5] rounded-full flex items-center justify-center active:scale-90 transition-transform">
|
||||
<Mic className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -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 (
|
||||
<div className="min-h-screen bg-white font-sans text-slate-900 flex flex-col max-w-md mx-auto">
|
||||
<Head title="Forgot Password" />
|
||||
|
||||
<div className="px-6 pt-12 pb-6">
|
||||
<button onClick={() => window.history.back()} className="p-2 -ml-2 text-slate-600">
|
||||
<ArrowLeft className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 px-8 pt-4">
|
||||
{!isSent ? (
|
||||
<div className="space-y-10">
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-3xl font-bold tracking-tight">Forgot password</h1>
|
||||
<p className="text-slate-500 text-sm">No worries, we'll send you reset instructions</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-8">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-semibold text-slate-700 ml-1">Email</label>
|
||||
<div className="relative">
|
||||
<Mail className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-400" />
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => 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
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={processing}
|
||||
className="w-full h-16 bg-[#185FA5] text-white rounded-2xl font-bold text-base shadow-lg shadow-blue-500/20 flex items-center justify-center space-x-2 active:scale-[0.98] transition-all"
|
||||
>
|
||||
{processing ? <Loader2 className="w-6 h-6 animate-spin" /> : (
|
||||
<>
|
||||
<span>Reset password</span>
|
||||
<ArrowRight className="w-5 h-5" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-8 text-center py-10">
|
||||
<div className="flex justify-center">
|
||||
<div className="w-20 h-20 bg-blue-50 rounded-full flex items-center justify-center text-[#185FA5]">
|
||||
<CheckCircle2 className="w-10 h-10" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-2xl font-bold tracking-tight">Check your email</h2>
|
||||
<p className="text-sm text-slate-500 leading-relaxed">
|
||||
We've sent a password reset link to <br />
|
||||
<span className="font-bold text-slate-900">{email}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="pt-4">
|
||||
<Link
|
||||
href="/mobile/employer/login"
|
||||
className="font-bold text-[#185FA5] hover:text-[#144f8a] underline underline-offset-4 text-sm"
|
||||
>
|
||||
Back to login
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -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 (
|
||||
<EmployerMobileLayout>
|
||||
<Head title="Dashboard" />
|
||||
|
||||
<div className="p-6 space-y-8">
|
||||
{/* Welcome Section */}
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-2xl font-bold tracking-tight text-slate-900">Dashboard</h1>
|
||||
<p className="text-sm text-slate-500 font-medium">Welcome back, John Doe</p>
|
||||
</div>
|
||||
|
||||
{/* Plan Status Card */}
|
||||
<div className="bg-[#141B34] rounded-[32px] p-8 text-white relative overflow-hidden shadow-2xl">
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-white/10 rounded-full -mr-16 -mt-16 blur-2xl" />
|
||||
<div className="space-y-6 relative z-10">
|
||||
<div className="inline-flex items-center space-x-2 bg-white/10 px-3 py-1.5 rounded-full border border-white/10 text-[10px] font-bold uppercase tracking-widest">
|
||||
<div className="w-1.5 h-1.5 bg-emerald-400 rounded-full" />
|
||||
<span>Premium Employer Pass</span>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<h2 className="text-3xl font-bold">24 Days</h2>
|
||||
<p className="text-slate-400 text-xs font-medium uppercase tracking-widest">Remaining in your plan</p>
|
||||
</div>
|
||||
<Link href="/mobile/employer/subscription" className="w-full h-14 bg-white text-[#141B34] rounded-2xl font-bold flex items-center justify-center space-x-2 shadow-xl active:scale-[0.98] transition-all text-xs uppercase tracking-widest">
|
||||
<span>Upgrade Plan</span>
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Stacked */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-[10px] font-bold text-slate-400 uppercase tracking-widest ml-1">Key Statistics</h3>
|
||||
<div className="space-y-3">
|
||||
{stats.map((stat, i) => (
|
||||
<div key={i} className="bg-white p-5 rounded-2xl border border-slate-100 shadow-sm flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className={`w-12 h-12 rounded-xl flex items-center justify-center ${stat.color}`}>
|
||||
<stat.icon className="w-6 h-6" />
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">{stat.label}</div>
|
||||
<div className="text-xl font-bold text-slate-900">{stat.value}</div>
|
||||
</div>
|
||||
</div>
|
||||
<TrendingUp className="w-5 h-5 text-emerald-500 opacity-20" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Placeholder for Analytics Chart (Full Width) */}
|
||||
<div className="space-y-4 pb-10">
|
||||
<h3 className="text-[10px] font-bold text-slate-400 uppercase tracking-widest ml-1">Recruitment Analytics</h3>
|
||||
<div className="bg-white p-6 rounded-3xl border border-slate-100 shadow-sm h-64 flex flex-col items-center justify-center text-center space-y-4">
|
||||
<div className="w-16 h-16 bg-slate-50 rounded-full flex items-center justify-center text-slate-200">
|
||||
<TrendingUp className="w-8 h-8" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-bold text-slate-900">Activity Chart</p>
|
||||
<p className="text-xs text-slate-400 font-medium">Visualizing your hiring trends over 30 days</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</EmployerMobileLayout>
|
||||
);
|
||||
}
|
||||
@ -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 (
|
||||
<div className="min-h-screen bg-white font-sans text-slate-900 flex flex-col max-w-md mx-auto">
|
||||
<Head title="Employer Login" />
|
||||
|
||||
{/* Header */}
|
||||
<div className="px-6 pt-12 pb-6 flex items-center">
|
||||
<button onClick={() => window.history.back()} className="p-2 -ml-2 text-slate-600 hover:text-slate-900">
|
||||
<ArrowLeft className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 px-8 pt-4 space-y-10">
|
||||
{/* Title */}
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-3xl font-bold tracking-tight">Welcome back</h1>
|
||||
<p className="text-slate-500 text-sm">Please enter your details to sign in</p>
|
||||
</div>
|
||||
|
||||
{/* Login Form */}
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-semibold text-slate-700 ml-1">Email</label>
|
||||
<div className="relative">
|
||||
<Mail className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-400" />
|
||||
<input
|
||||
type="email"
|
||||
value={data.email}
|
||||
onChange={(e) => 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
|
||||
/>
|
||||
</div>
|
||||
{errors.email && <p className="text-xs text-red-500 mt-1 ml-1">{errors.email}</p>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center justify-between ml-1">
|
||||
<label className="text-sm font-semibold text-slate-700">Password</label>
|
||||
<Link href="/mobile/employer/forgot-password" size="sm" className="text-sm font-medium text-[#185FA5] hover:text-[#144f8a]">Forgot?</Link>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Lock className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-400" />
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={data.password}
|
||||
onChange={(e) => 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
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600"
|
||||
>
|
||||
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={processing}
|
||||
className="w-full h-16 bg-[#185FA5] text-white rounded-2xl font-bold text-base shadow-lg shadow-blue-500/20 flex items-center justify-center space-x-2 active:scale-[0.98] transition-all disabled:opacity-70 mt-8"
|
||||
>
|
||||
{processing ? <Loader2 className="w-6 h-6 animate-spin" /> : <span>Sign in</span>}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="text-center pt-4 pb-10">
|
||||
<p className="text-sm text-slate-500">
|
||||
Don't have an account?{' '}
|
||||
<Link href="/mobile/employer/register" className="font-bold text-[#185FA5] hover:text-[#144f8a] underline underline-offset-4">
|
||||
Sign up
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -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 (
|
||||
<EmployerMobileLayout>
|
||||
<Head title="Messages" />
|
||||
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Search Bar */}
|
||||
<div className="p-6 pb-2">
|
||||
<div className="relative group">
|
||||
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-300 group-focus-within:text-[#141B34] transition-colors" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search conversations..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Chat List */}
|
||||
<div className="flex-1 overflow-y-auto no-scrollbar pb-10">
|
||||
<div className="divide-y divide-slate-100">
|
||||
{chats.map((chat) => (
|
||||
<Link
|
||||
key={chat.id}
|
||||
href={`/mobile/employer/chat/${chat.id}`}
|
||||
className="flex items-center p-6 bg-white active:bg-slate-50 transition-all space-x-4"
|
||||
>
|
||||
<div className="relative shrink-0">
|
||||
<div className="w-14 h-14 rounded-full bg-slate-100 border border-slate-200 flex items-center justify-center text-[#141B34] font-bold text-xl">
|
||||
{chat.name.charAt(0)}
|
||||
</div>
|
||||
{chat.online && (
|
||||
<div className="absolute bottom-0.5 right-0.5 w-3.5 h-3.5 bg-emerald-500 border-2 border-white rounded-full" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0 space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="font-bold text-slate-900 text-[15px] truncate">{chat.name}</h4>
|
||||
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">{chat.time}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className={`text-xs truncate max-w-[200px] ${chat.unread > 0 ? 'text-slate-900 font-bold' : 'text-slate-500 font-medium'}`}>
|
||||
{chat.preview}
|
||||
</p>
|
||||
{chat.unread > 0 ? (
|
||||
<div className="w-5 h-5 bg-[#185FA5] text-white text-[10px] font-bold rounded-full flex items-center justify-center border border-white">
|
||||
{chat.unread}
|
||||
</div>
|
||||
) : (
|
||||
<CheckCheck className="w-4 h-4 text-slate-300" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</EmployerMobileLayout>
|
||||
);
|
||||
}
|
||||
@ -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 (
|
||||
<div className="min-h-screen bg-white font-sans text-slate-900 flex flex-col max-w-md mx-auto">
|
||||
<Head title="OTP Verification" />
|
||||
|
||||
<div className="px-6 pt-12 pb-6">
|
||||
<button onClick={() => window.history.back()} className="p-2 -ml-2 text-slate-600">
|
||||
<ArrowLeft className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 px-8 pt-4 space-y-10">
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-3xl font-bold tracking-tight">Verify email</h1>
|
||||
<p className="text-slate-500 text-sm">Enter the code sent to your email address</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-10">
|
||||
<div className="flex justify-between space-x-4">
|
||||
{otp.map((digit, i) => (
|
||||
<input
|
||||
key={i}
|
||||
id={`otp-${i}`}
|
||||
type="text"
|
||||
value={digit}
|
||||
onChange={(e) => 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
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={processing || otp.join('').length < 4}
|
||||
className="w-full h-16 bg-[#185FA5] text-white rounded-2xl font-bold text-base shadow-lg shadow-blue-500/20 flex items-center justify-center active:scale-[0.98] transition-all disabled:opacity-50"
|
||||
>
|
||||
{processing ? <Loader2 className="w-6 h-6 animate-spin" /> : <span>Verify</span>}
|
||||
</button>
|
||||
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-slate-500">
|
||||
Didn't receive code? {timer > 0 ? (
|
||||
<span className="text-[#185FA5] font-medium ml-1">00:{timer < 10 ? `0${timer}` : timer}</span>
|
||||
) : (
|
||||
<button type="button" onClick={() => setTimer(59)} className="text-[#185FA5] font-bold hover:underline ml-1">Resend now</button>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -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 (
|
||||
<EmployerMobileLayout>
|
||||
<Head title="My Profile" />
|
||||
|
||||
<div className="flex flex-col h-full bg-[#F8F9FA]">
|
||||
{/* Profile Hero Card */}
|
||||
<div className="bg-white px-6 pt-10 pb-8 flex flex-col items-center text-center space-y-5 border-b border-slate-100 relative shadow-sm">
|
||||
<div className="absolute top-0 left-0 w-full h-32 bg-[#141B34] -z-10" />
|
||||
|
||||
<div className="relative">
|
||||
<div className="w-32 h-32 rounded-full bg-slate-100 border-4 border-white flex items-center justify-center text-[#141B34] font-black text-4xl shadow-xl overflow-hidden">
|
||||
JD
|
||||
</div>
|
||||
<button className="absolute bottom-1 right-1 w-10 h-10 bg-[#141B34] text-white rounded-full flex items-center justify-center border-4 border-white shadow-lg active:scale-95 transition-all">
|
||||
<Camera className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center justify-center space-x-1.5">
|
||||
<h2 className="text-2xl font-black text-slate-900 uppercase tracking-tight">John Doe</h2>
|
||||
<CheckCircle2 className="w-5 h-5 text-blue-500 fill-blue-50" />
|
||||
</div>
|
||||
<div className="flex items-center justify-center space-x-2 text-slate-400 font-bold text-xs uppercase tracking-widest">
|
||||
<Building2 className="w-3.5 h-3.5" />
|
||||
<span>Al Mansoor Household</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3 w-full max-w-[300px] pt-2">
|
||||
<div className="flex-1 bg-slate-50 p-3 rounded-2xl border border-slate-100">
|
||||
<div className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Plan</div>
|
||||
<div className="text-[11px] font-black text-[#141B34] uppercase tracking-widest">Premium</div>
|
||||
</div>
|
||||
<div className="flex-1 bg-slate-50 p-3 rounded-2xl border border-slate-100">
|
||||
<div className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Verified</div>
|
||||
<div className="text-[11px] font-black text-emerald-500 uppercase tracking-widest">Yes</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Settings Menu Sections */}
|
||||
<div className="p-6 space-y-8 pb-32">
|
||||
{sections.map((section, idx) => (
|
||||
<div key={idx} className="space-y-4">
|
||||
<h3 className="text-[10px] font-black text-slate-400 uppercase tracking-[0.2em] ml-2">{section.title}</h3>
|
||||
<div className="bg-white rounded-[32px] border border-slate-100 shadow-sm overflow-hidden">
|
||||
{section.items.map((item, i) => (
|
||||
<Link
|
||||
key={i}
|
||||
href={item.href}
|
||||
className={`flex items-center justify-between p-5 hover:bg-slate-50 transition-all ${i !== section.items.length - 1 ? 'border-b border-slate-50' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className={`w-11 h-11 rounded-2xl flex items-center justify-center ${item.color} shadow-sm`}>
|
||||
<item.icon className="w-5 h-5" />
|
||||
</div>
|
||||
<span className="text-[13px] font-bold text-slate-900 uppercase tracking-tight">{item.label}</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
{item.badge && (
|
||||
<span className="px-3 py-1 bg-blue-50 text-blue-600 text-[9px] font-black uppercase tracking-widest rounded-lg border border-blue-100">
|
||||
{item.badge}
|
||||
</span>
|
||||
)}
|
||||
{item.detail && (
|
||||
<span className="text-[11px] font-bold text-slate-400">{item.detail}</span>
|
||||
)}
|
||||
<ChevronRight className="w-4 h-4 text-slate-300" />
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<button className="w-full p-6 bg-white text-rose-500 rounded-[32px] font-black text-xs uppercase tracking-[0.2em] flex items-center justify-center space-x-3 active:scale-[0.98] transition-all border border-rose-100 shadow-sm mt-4">
|
||||
<LogOut className="w-5 h-5" />
|
||||
<span>Sign Out Account</span>
|
||||
</button>
|
||||
|
||||
<div className="text-center pt-4">
|
||||
<p className="text-[9px] font-black text-slate-300 uppercase tracking-[0.3em]">Version 2.4.1 (Stable Build)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</EmployerMobileLayout>
|
||||
);
|
||||
}
|
||||
@ -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 (
|
||||
<div className="min-h-screen bg-white font-sans text-slate-900 flex flex-col max-w-md mx-auto relative overflow-hidden">
|
||||
<Head title="Enrollment" />
|
||||
|
||||
{/* Minimal Header */}
|
||||
<div className="px-6 pt-12 pb-4 flex items-center justify-between sticky top-0 bg-white/80 backdrop-blur-xl z-50">
|
||||
<button onClick={() => step > 1 ? prevStep() : window.history.back()} className="p-3 bg-slate-50 rounded-2xl text-slate-400 border border-slate-100 active:scale-95 transition-all">
|
||||
<ChevronLeft className="w-6 h-6" />
|
||||
</button>
|
||||
<div className="flex items-center space-x-1.5 px-4 py-2 bg-slate-50 rounded-full border border-slate-100">
|
||||
<div className="w-1.5 h-1.5 bg-[#185FA5] rounded-full animate-pulse" />
|
||||
<span className="text-[10px] font-bold uppercase tracking-widest text-slate-400">Step {step} / 4</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 px-8 pt-8 space-y-8 overflow-y-auto no-scrollbar pb-32">
|
||||
{/* Header Text */}
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-3xl font-black tracking-tight text-slate-900 leading-none">
|
||||
{step === 1 && "Create Account"}
|
||||
{step === 2 && "Identity Vetting"}
|
||||
{step === 3 && "Choose Plan"}
|
||||
{step === 4 && "Checkout"}
|
||||
</h1>
|
||||
<p className="text-sm font-medium text-slate-400">
|
||||
{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."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Step 1: Account Info */}
|
||||
{step === 1 && (
|
||||
<div className="space-y-5 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
<div className="space-y-2">
|
||||
<label className="text-[11px] font-bold text-slate-400 uppercase tracking-widest ml-2">Full Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={data.name}
|
||||
onChange={(e) => 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
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[11px] font-bold text-slate-400 uppercase tracking-widest ml-2">Email Address</label>
|
||||
<input
|
||||
type="email"
|
||||
value={data.email}
|
||||
onChange={(e) => 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
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[11px] font-bold text-slate-400 uppercase tracking-widest ml-2">Phone Number</label>
|
||||
<input
|
||||
type="text"
|
||||
value={data.phone}
|
||||
onChange={(e) => 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
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[11px] font-bold text-slate-400 uppercase tracking-widest ml-2">Password</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={data.password}
|
||||
onChange={(e) => 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
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-6 top-1/2 -translate-y-1/2 text-slate-300 hover:text-slate-500"
|
||||
>
|
||||
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={nextStep}
|
||||
className="w-full py-5 bg-[#185FA5] text-white rounded-[24px] font-bold text-sm uppercase tracking-widest shadow-xl shadow-blue-500/20 flex items-center justify-center space-x-3 active:scale-[0.98] transition-all mt-4"
|
||||
>
|
||||
<span>Continue</span>
|
||||
<ArrowRight className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 2: Emirates ID */}
|
||||
{step === 2 && (
|
||||
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
<div className="bg-emerald-50 p-6 rounded-[32px] flex items-center space-x-4 border border-emerald-100">
|
||||
<ShieldCheck className="w-8 h-8 text-emerald-500 shrink-0" />
|
||||
<p className="text-[11px] font-bold text-emerald-800 leading-relaxed uppercase tracking-wider">
|
||||
Your ID is encrypted and handled securely as per UAE data protection laws.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{[
|
||||
{ field: 'emirates_id_front', label: 'EID Front Side', file: frontFileName },
|
||||
{ field: 'emirates_id_back', label: 'EID Back Side', file: backFileName }
|
||||
].map((slot) => (
|
||||
<div key={slot.field} className="space-y-2">
|
||||
<label className="text-[11px] font-bold text-slate-400 uppercase tracking-widest ml-2">{slot.label}</label>
|
||||
{slot.file ? (
|
||||
<div className="relative h-40 bg-slate-50 rounded-[32px] border-2 border-[#185FA5] flex flex-col items-center justify-center p-6 shadow-xl shadow-blue-500/5">
|
||||
<FileText className="w-10 h-10 text-[#185FA5] mb-2" />
|
||||
<span className="text-[10px] font-bold text-slate-900 uppercase truncate max-w-full px-4">{slot.file}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeFile(slot.field)}
|
||||
className="absolute -top-2 -right-2 bg-rose-500 text-white p-3 rounded-full shadow-lg active:scale-90 transition-transform"
|
||||
>
|
||||
<X className="w-4 h-4" strokeWidth={3} />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<label className="relative h-40 bg-slate-50 rounded-[32px] border-2 border-dashed border-slate-200 flex flex-col items-center justify-center p-8 cursor-pointer hover:border-[#185FA5] transition-all group">
|
||||
<div className="w-14 h-14 bg-white rounded-2xl flex items-center justify-center shadow-sm mb-3 group-hover:scale-110 transition-transform">
|
||||
<Camera className="w-6 h-6 text-slate-300 group-hover:text-[#185FA5]" />
|
||||
</div>
|
||||
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-widest group-hover:text-[#185FA5]">Tap to Upload</span>
|
||||
<input
|
||||
type="file"
|
||||
className="hidden"
|
||||
onChange={(e) => handleFileChange(e, slot.field)}
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={nextStep}
|
||||
disabled={!frontFileName || !backFileName}
|
||||
className="w-full py-5 bg-[#185FA5] text-white rounded-[24px] font-bold text-sm uppercase tracking-widest shadow-xl shadow-blue-500/20 flex items-center justify-center space-x-3 active:scale-[0.98] transition-all disabled:opacity-50 mt-4"
|
||||
>
|
||||
<span>Verify Identity</span>
|
||||
<ShieldCheck className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 3: Plans */}
|
||||
{step === 3 && (
|
||||
<div className="space-y-4 animate-in fade-in slide-in-from-bottom-4 duration-500 pb-10">
|
||||
{plans.map((p) => (
|
||||
<button
|
||||
key={p.id}
|
||||
type="button"
|
||||
onClick={() => setData('selected_plan', p.id)}
|
||||
className={`w-full p-6 rounded-[32px] border-2 text-left transition-all relative overflow-hidden flex items-center justify-between ${
|
||||
data.selected_plan === p.id
|
||||
? 'bg-blue-50/50 border-[#185FA5] shadow-lg shadow-blue-500/5'
|
||||
: 'bg-white border-slate-50 hover:border-slate-100'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className={`w-12 h-12 rounded-2xl flex items-center justify-center ${p.color}`}>
|
||||
<p.icon className="w-6 h-6" />
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
<div className="flex items-center space-x-2">
|
||||
<h4 className="text-sm font-bold text-slate-900 uppercase tracking-tight">{p.name}</h4>
|
||||
{p.popular && <span className="bg-emerald-500 text-white px-2 py-0.5 rounded-full text-[8px] font-bold uppercase tracking-widest">Best</span>}
|
||||
</div>
|
||||
<div className="text-[10px] font-medium text-slate-400">{p.features[0]}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-xl font-black text-[#185FA5]">{p.price} <span className="text-[10px] font-bold">AED</span></div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
onClick={nextStep}
|
||||
className="w-full py-5 bg-[#185FA5] text-white rounded-[24px] font-bold text-sm uppercase tracking-widest shadow-xl shadow-blue-500/20 flex items-center justify-center space-x-3 active:scale-[0.98] transition-all mt-4"
|
||||
>
|
||||
<span>Continue to Payment</span>
|
||||
<ArrowRight className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 4: Payment */}
|
||||
{step === 4 && (
|
||||
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
{/* Card Visual */}
|
||||
<div className="bg-[#141B34] rounded-[32px] p-8 text-white relative overflow-hidden shadow-2xl">
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-[#185FA5]/20 rounded-full blur-3xl -mr-16 -mt-16" />
|
||||
<div className="flex justify-between items-start mb-10 relative z-10">
|
||||
<div className="w-12 h-8 bg-amber-400 rounded-lg" />
|
||||
<CreditCard className="w-8 h-8 text-white/20" />
|
||||
</div>
|
||||
<div className="space-y-4 relative z-10">
|
||||
<div className="text-lg font-bold tracking-[0.2em] h-8 flex items-center">
|
||||
{data.card_number || '•••• •••• •••• ••••'}
|
||||
</div>
|
||||
<div className="flex justify-between border-t border-white/10 pt-4">
|
||||
<div className="space-y-0.5 text-left">
|
||||
<div className="text-[9px] font-bold uppercase opacity-40 tracking-widest">Card Holder</div>
|
||||
<div className="text-[11px] font-bold uppercase tracking-widest truncate max-w-[150px]">{data.name || 'Your Name'}</div>
|
||||
</div>
|
||||
<div className="text-right space-y-0.5">
|
||||
<div className="text-[9px] font-bold uppercase opacity-40 tracking-widest">Expiry</div>
|
||||
<div className="text-[11px] font-bold uppercase tracking-widest">{data.expiry || 'MM/YY'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<input
|
||||
type="text"
|
||||
value={data.card_number}
|
||||
onChange={(e) => 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
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<input
|
||||
type="text"
|
||||
value={data.expiry}
|
||||
onChange={(e) => 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
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
value={data.cvc}
|
||||
onChange={(e) => 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
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 bg-slate-50 rounded-[32px] flex items-center justify-between border border-slate-100">
|
||||
<div className="space-y-0.5">
|
||||
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Total to Pay</div>
|
||||
<div className="text-xl font-black text-slate-900">{plans.find(p => p.id === data.selected_plan)?.price} AED</div>
|
||||
</div>
|
||||
<div className="p-3 bg-emerald-50 rounded-2xl">
|
||||
<Lock className="w-6 h-6 text-emerald-500" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={processing}
|
||||
className="w-full py-5 bg-emerald-500 text-white rounded-[24px] font-bold text-sm uppercase tracking-widest shadow-xl shadow-emerald-500/20 flex items-center justify-center space-x-3 active:scale-[0.98] transition-all disabled:opacity-70 mt-4"
|
||||
>
|
||||
{processing ? <Loader2 className="w-6 h-6 animate-spin" /> : (
|
||||
<>
|
||||
<span>Confirm & Pay</span>
|
||||
<ChevronRight className="w-5 h-5" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
|
||||
<div className="text-center pt-8 pb-10">
|
||||
<p className="text-xs font-bold text-slate-400 uppercase tracking-widest">
|
||||
Already joined? <Link href="/mobile/employer/login" className="text-[#185FA5]">Sign In</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -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 (
|
||||
<EmployerMobileLayout>
|
||||
<Head title="Shortlist" />
|
||||
|
||||
<div className="p-6 space-y-8">
|
||||
{/* Header */}
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-2xl font-bold tracking-tight">Shortlist</h1>
|
||||
<p className="text-sm text-slate-500 font-medium">Quick access to your saved candidates</p>
|
||||
</div>
|
||||
|
||||
{savedWorkers.length > 0 ? (
|
||||
<div className="space-y-4 pb-10">
|
||||
<div className="flex items-center justify-between px-1">
|
||||
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">{savedWorkers.length} Saved Profiles</span>
|
||||
<button className="text-[10px] font-bold text-rose-500 uppercase tracking-widest">Clear All</button>
|
||||
</div>
|
||||
|
||||
{savedWorkers.map((worker) => (
|
||||
<div key={worker.id} className="bg-white p-5 rounded-3xl border border-slate-100 shadow-sm space-y-5">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-16 h-16 rounded-2xl bg-slate-50 overflow-hidden border border-slate-200">
|
||||
<img src={worker.img} alt="worker" className="w-full h-full object-cover" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<h4 className="font-bold text-slate-900 text-base">{worker.name}</h4>
|
||||
<div className="flex items-center space-x-2 text-[11px] font-semibold text-slate-400 uppercase tracking-wider">
|
||||
<Globe2 className="w-3 h-3" />
|
||||
<span>{worker.nationality}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button className="w-9 h-9 rounded-xl bg-slate-50 flex items-center justify-center text-slate-300 hover:text-rose-500 transition-colors border border-slate-100">
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="flex items-center space-x-2 text-[11px] font-bold text-slate-600 bg-slate-50 px-3 py-2.5 rounded-xl border border-slate-100">
|
||||
<Briefcase className="w-3.5 h-3.5 text-slate-400" />
|
||||
<span>{worker.exp}</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 text-[11px] font-bold text-slate-600 bg-slate-50 px-3 py-2.5 rounded-xl border border-slate-100">
|
||||
<DollarSign className="w-3.5 h-3.5 text-emerald-500" />
|
||||
<span>{worker.salary}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3 pt-2">
|
||||
<Link
|
||||
href={`/mobile/employer/workers/${worker.id}`}
|
||||
className="flex-1 py-4 bg-[#141B34] text-white rounded-2xl font-bold text-sm shadow-xl shadow-slate-900/10 active:scale-95 transition-all text-center"
|
||||
>
|
||||
View Profile
|
||||
</Link>
|
||||
<div className="flex items-center space-x-1 px-4 py-4 bg-amber-50 rounded-2xl border border-amber-100">
|
||||
<Star className="w-4 h-4 text-amber-500 fill-amber-500" />
|
||||
<span className="text-sm font-bold text-amber-700">{worker.rating}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="pt-4">
|
||||
<button className="w-full h-16 bg-white border-2 border-slate-100 text-[#141B34] rounded-2xl font-bold text-sm flex items-center justify-center space-x-2 shadow-sm active:scale-95 transition-all">
|
||||
<MessageSquare className="w-5 h-5" />
|
||||
<span>Message All</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 flex flex-col items-center justify-center space-y-6 pt-20">
|
||||
<div className="w-24 h-24 bg-white border border-slate-100 rounded-[40px] flex items-center justify-center shadow-sm">
|
||||
<Bookmark className="w-10 h-10 text-slate-100" />
|
||||
</div>
|
||||
<div className="text-center space-y-2 px-10">
|
||||
<h3 className="text-lg font-bold tracking-tight">Your Shortlist is Empty</h3>
|
||||
<p className="text-sm text-slate-400 font-medium">Save profiles to compare and hire faster.</p>
|
||||
</div>
|
||||
<Link href="/mobile/employer/workers" className="px-10 py-4 bg-[#141B34] text-white rounded-2xl font-bold text-sm shadow-lg active:scale-95 transition-all">
|
||||
Browse Workers
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</EmployerMobileLayout>
|
||||
);
|
||||
}
|
||||
@ -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 (
|
||||
<div className="min-h-screen bg-[#D8F3FF] flex flex-col items-center justify-center p-8 max-w-md mx-auto relative overflow-hidden">
|
||||
<Head title="Welcome to Marketplace" />
|
||||
|
||||
{/* Animated Background Elements */}
|
||||
<div className="absolute top-[-10%] right-[-10%] w-64 h-64 bg-white/40 rounded-full blur-3xl animate-pulse" />
|
||||
<div className="absolute bottom-[-10%] left-[-10%] w-80 h-80 bg-[#185FA5]/10 rounded-full blur-3xl animate-bounce duration-[3000ms]" />
|
||||
|
||||
<div className="relative flex flex-col items-center space-y-8 animate-in fade-in zoom-in duration-1000">
|
||||
<div className="w-24 h-24 bg-gradient-to-br from-[#185FA5] to-[#2573c0] rounded-[32px] flex items-center justify-center shadow-[0_20px_40px_rgba(24,95,165,0.3)] border-4 border-white/20">
|
||||
<ShieldCheck className="w-12 h-12 text-white" strokeWidth={2.5} />
|
||||
</div>
|
||||
|
||||
<div className="text-center space-y-2">
|
||||
<h1 className="text-4xl font-black text-[#141B34] tracking-tighter uppercase leading-none">
|
||||
Job<span className="text-[#185FA5]">Sift</span>
|
||||
</h1>
|
||||
<p className="text-[10px] font-black text-[#185FA5]/60 uppercase tracking-[0.4em] ml-1">Employer Edition</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-12 flex flex-col items-center space-y-4">
|
||||
<div className="flex space-x-1.5">
|
||||
<div className="w-1.5 h-1.5 bg-[#185FA5] rounded-full animate-bounce" />
|
||||
<div className="w-1.5 h-1.5 bg-[#185FA5] rounded-full animate-bounce [animation-delay:0.2s]" />
|
||||
<div className="w-1.5 h-1.5 bg-[#185FA5] rounded-full animate-bounce [animation-delay:0.4s]" />
|
||||
</div>
|
||||
<span className="text-[10px] font-black text-[#3B46D1]/40 uppercase tracking-widest">Version 2.0.26</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -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 (
|
||||
<EmployerMobileLayout>
|
||||
<Head title="Subscription" />
|
||||
|
||||
<div className="p-6 space-y-8">
|
||||
{/* Header */}
|
||||
<div className="text-center space-y-2 py-4">
|
||||
<h1 className="text-2xl font-bold tracking-tight">Choose Your Plan</h1>
|
||||
<p className="text-sm text-slate-500 font-medium max-w-[250px] mx-auto">Select a package that fits your hiring needs.</p>
|
||||
</div>
|
||||
|
||||
{/* Plan Cards Stacked */}
|
||||
<div className="space-y-6 pb-20">
|
||||
{plans.map((plan) => (
|
||||
<div key={plan.id} className={`bg-white rounded-[32px] p-8 border shadow-sm relative overflow-hidden transition-all active:scale-[0.98] ${
|
||||
plan.popular ? 'border-[#141B34] border-2 shadow-xl' : 'border-slate-100'
|
||||
}`}>
|
||||
{plan.popular && (
|
||||
<div className="absolute top-0 right-0 bg-[#141B34] text-white px-6 py-1.5 rounded-bl-2xl text-[9px] font-black uppercase tracking-widest">
|
||||
Most Popular
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className={`w-14 h-14 rounded-2xl flex items-center justify-center ${plan.color}`}>
|
||||
<plan.icon className="w-7 h-7" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-lg font-bold">{plan.name}</h3>
|
||||
<div className="flex items-baseline space-x-1">
|
||||
<span className="text-2xl font-black text-slate-900">{plan.price}</span>
|
||||
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">AED / Mo</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{plan.features.map((feature, i) => (
|
||||
<div key={i} className="flex items-center space-x-3">
|
||||
<div className="w-5 h-5 rounded-full bg-emerald-50 flex items-center justify-center">
|
||||
<Check className="w-3 h-3 text-emerald-500" strokeWidth={3} />
|
||||
</div>
|
||||
<span className="text-xs text-slate-600 font-medium">{feature}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button className={`w-full py-4 rounded-2xl font-bold text-sm uppercase tracking-widest transition-all ${
|
||||
plan.popular
|
||||
? 'bg-[#141B34] text-white shadow-lg'
|
||||
: 'bg-slate-50 text-slate-900 hover:bg-slate-100'
|
||||
}`}>
|
||||
Select {plan.name}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</EmployerMobileLayout>
|
||||
);
|
||||
}
|
||||
@ -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 (
|
||||
<div className="min-h-screen bg-[#D8F3FF] flex flex-col max-w-md mx-auto relative overflow-hidden">
|
||||
<Head title="Welcome" />
|
||||
|
||||
{/* Artistic Header Background */}
|
||||
<div className="h-[45vh] relative overflow-hidden">
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-[#185FA5] to-[#2573c0] rounded-b-[60px] shadow-2xl" />
|
||||
<div className="absolute top-[10%] left-[10%] w-32 h-32 bg-white/10 rounded-full blur-2xl" />
|
||||
<div className="absolute bottom-[20%] right-[-10%] w-64 h-64 bg-emerald-400/20 rounded-full blur-3xl" />
|
||||
|
||||
<div className="relative h-full flex flex-col items-center justify-center p-8 space-y-6 text-center animate-in slide-in-from-top duration-700">
|
||||
<div className="w-20 h-20 bg-white/20 backdrop-blur-xl rounded-[28px] border border-white/30 flex items-center justify-center shadow-2xl">
|
||||
<ShieldCheck className="w-10 h-10 text-white" strokeWidth={2.5} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-3xl font-black text-white tracking-tighter uppercase leading-none">
|
||||
Find Top <br /> Talent Fast
|
||||
</h1>
|
||||
<p className="text-blue-100/70 text-[11px] font-bold uppercase tracking-[0.2em]">The Future of Recruitment</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 px-8 pt-10 space-y-10 animate-in slide-in-from-bottom duration-700">
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-start space-x-5">
|
||||
<div className="w-12 h-12 bg-white rounded-2xl flex items-center justify-center shadow-lg shadow-blue-500/5 shrink-0">
|
||||
<Users className="w-6 h-6 text-[#185FA5]" strokeWidth={2.5} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<h4 className="font-black text-[#141B34] text-sm uppercase tracking-tight">Verified Workers</h4>
|
||||
<p className="text-[11px] text-slate-400 font-bold uppercase tracking-tight leading-relaxed">Access 10,000+ pre-vetted domestic and commercial workers.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-5">
|
||||
<div className="w-12 h-12 bg-white rounded-2xl flex items-center justify-center shadow-lg shadow-blue-500/5 shrink-0">
|
||||
<Sparkles className="w-6 h-6 text-[#22D39B]" strokeWidth={2.5} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<h4 className="font-black text-[#141B34] text-sm uppercase tracking-tight">AI Matching</h4>
|
||||
<p className="text-[11px] text-slate-400 font-bold uppercase tracking-tight leading-relaxed">Our smart engine matches workers based on your specific needs.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 pt-4">
|
||||
<Link
|
||||
href="/mobile/employer/login"
|
||||
className="w-full h-18 bg-[#185FA5] text-white rounded-[28px] font-black text-sm uppercase tracking-[0.2em] shadow-[0_15px_35px_rgba(24,95,165,0.3)] flex items-center justify-center space-x-3 active:scale-95 transition-all"
|
||||
>
|
||||
<span>Login to Portal</span>
|
||||
<ArrowRight className="w-5 h-5" strokeWidth={3} />
|
||||
</Link>
|
||||
<Link
|
||||
href="/mobile/employer/register"
|
||||
className="w-full h-18 bg-white text-[#185FA5] rounded-[28px] font-black text-sm uppercase tracking-[0.2em] shadow-lg flex items-center justify-center active:scale-95 transition-all border-2 border-white"
|
||||
>
|
||||
<span>Join as Employer</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-8 pb-10 text-center">
|
||||
<p className="text-[10px] font-black text-slate-400 uppercase tracking-widest leading-loose">
|
||||
By continuing you agree to our <br />
|
||||
<span className="text-[#185FA5] underline underline-offset-4">Terms of Service</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -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 (
|
||||
<EmployerMobileLayout>
|
||||
<Head title="Find Workers" />
|
||||
|
||||
<div className="flex flex-col h-full bg-[#F8FAFC]">
|
||||
{/* Header */}
|
||||
<div className="pt-12 pb-6 px-6 space-y-6 bg-white sticky top-0 z-40 shadow-sm">
|
||||
<div className="flex items-center space-x-4">
|
||||
<button onClick={() => window.history.back()} className="p-2 bg-slate-50 rounded-xl text-slate-400 border border-slate-100">
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
</button>
|
||||
<h1 className="text-2xl font-bold text-slate-900 tracking-tight">Find Workers</h1>
|
||||
</div>
|
||||
|
||||
{/* Search Bar */}
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex-1 relative group">
|
||||
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-300 group-focus-within:text-[#185FA5] transition-colors" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by name or skill..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowFilters(true)}
|
||||
className={`w-14 h-14 rounded-2xl flex items-center justify-center border transition-all ${
|
||||
showFilters ? 'bg-[#185FA5] text-white border-[#185FA5]' : 'bg-white text-slate-400 border-slate-100'
|
||||
}`}
|
||||
>
|
||||
<Filter className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Category Tabs */}
|
||||
<div className="flex space-x-2 overflow-x-auto no-scrollbar pb-1">
|
||||
{categories.map((cat) => (
|
||||
<button
|
||||
key={cat}
|
||||
onClick={() => setActiveCat(cat)}
|
||||
className={`shrink-0 px-5 py-2.5 rounded-full text-[10px] font-bold uppercase tracking-widest transition-all ${
|
||||
activeCat === cat
|
||||
? 'bg-[#185FA5] text-white shadow-lg'
|
||||
: 'bg-white text-slate-400 border border-slate-100 hover:bg-slate-50'
|
||||
}`}
|
||||
>
|
||||
{cat}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Worker Listing */}
|
||||
<div className="px-6 py-8 space-y-8 pb-32">
|
||||
{workers.map((worker) => (
|
||||
<div key={worker.id} className="bg-white rounded-[32px] border border-slate-100 shadow-sm overflow-hidden flex flex-col">
|
||||
{/* Card Header Section */}
|
||||
<div className="p-6 bg-[#F8FAFC] flex items-center justify-between border-b border-slate-50">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-16 h-16 rounded-full bg-[#D8F3FF] flex items-center justify-center text-[#185FA5] font-black text-2xl shadow-inner">
|
||||
{worker.initial}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center space-x-1.5">
|
||||
<h4 className="font-bold text-slate-900 text-lg tracking-tight">{worker.name}</h4>
|
||||
<CheckCircle2 className="w-4 h-4 text-emerald-500" />
|
||||
</div>
|
||||
<div className="flex items-center space-x-1.5 text-slate-400 font-semibold text-xs tracking-wide">
|
||||
<Globe className="w-3.5 h-3.5" />
|
||||
<span>{worker.nationality}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button className="w-10 h-10 rounded-full bg-[#EBF5FF] flex items-center justify-center text-[#185FA5]">
|
||||
<Bookmark className="w-5 h-5 fill-current" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Card Body Section */}
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="px-4 py-2 bg-[#F0F7FF] text-[#185FA5] rounded-xl text-xs font-bold border border-[#E1EFFF]">
|
||||
{worker.role}
|
||||
</div>
|
||||
<div className="flex items-center space-x-1.5">
|
||||
<DollarSign className="w-4 h-4 text-emerald-500" />
|
||||
<span className="text-sm font-bold text-slate-900">{worker.salary}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-[13px] text-slate-500 font-medium leading-relaxed italic">
|
||||
{worker.bio}
|
||||
</p>
|
||||
|
||||
{/* Stats Grid 2x2 */}
|
||||
<div className="bg-[#F8FAFC] p-4 rounded-2xl grid grid-cols-2 gap-4 border border-slate-50">
|
||||
<div className="flex items-center space-x-2.5">
|
||||
<Briefcase className="w-4 h-4 text-slate-400" />
|
||||
<div className="text-[11px] font-bold text-slate-600">Exp: {worker.exp}</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2.5">
|
||||
<Calendar className="w-4 h-4 text-slate-400" />
|
||||
<div className="text-[11px] font-bold text-slate-600">Avail: {worker.avail}</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2.5">
|
||||
<Heart className="w-4 h-4 text-slate-400" />
|
||||
<div className="text-[11px] font-bold text-slate-600">{worker.religion}</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2.5">
|
||||
<Sparkles className="w-4 h-4 text-slate-400" />
|
||||
<div className="text-[11px] font-bold text-slate-600">Age: {worker.age}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Skills Tags */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{worker.skills.map((skill) => (
|
||||
<span key={skill} className="px-3 py-1.5 bg-slate-50 text-slate-500 text-[10px] font-bold rounded-lg border border-slate-100">
|
||||
{skill}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href={`/mobile/employer/workers/${worker.id}`}
|
||||
className="w-full py-4 bg-[#185FA5] text-white rounded-2xl font-bold text-sm tracking-tight active:scale-[0.98] transition-all flex items-center justify-center shadow-lg shadow-blue-500/20"
|
||||
>
|
||||
View Full Profile
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Filter Drawer - Simple UI Style */}
|
||||
{showFilters && (
|
||||
<div className="fixed inset-0 z-[100] flex items-end justify-center px-4 sm:px-0">
|
||||
<div className="absolute inset-0 bg-slate-900/40 backdrop-blur-sm" onClick={() => setShowFilters(false)} />
|
||||
<div className="relative w-full max-w-md bg-white rounded-t-[40px] shadow-2xl p-8 pb-12 animate-in slide-in-from-bottom duration-300 overflow-y-auto max-h-[85vh] no-scrollbar">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div className="flex items-center space-x-3">
|
||||
<button onClick={resetFilters} className="p-2 text-slate-400 hover:text-[#185FA5] transition-colors">
|
||||
<RotateCcw className="w-5 h-5" />
|
||||
</button>
|
||||
<h2 className="text-xl font-bold">Filters</h2>
|
||||
</div>
|
||||
<button onClick={() => setShowFilters(false)} className="w-10 h-10 bg-slate-100 rounded-full flex items-center justify-center text-slate-500 hover:text-rose-500 transition-all">
|
||||
<X className="w-5 h-5" strokeWidth={3} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-8">
|
||||
{/* Nationality */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-[11px] font-bold text-slate-400 uppercase tracking-widest ml-1">Nationality</label>
|
||||
<select
|
||||
value={filters.nationality}
|
||||
onChange={(e) => setFilters({...filters, nationality: e.target.value})}
|
||||
className="w-full px-5 py-4 bg-slate-50 border border-slate-100 rounded-2xl text-sm font-bold appearance-none focus:bg-white focus:border-[#185FA5] outline-none transition-all"
|
||||
>
|
||||
{filterOptions.nationalities.map(n => <option key={n} value={n}>{n}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Availability */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-[11px] font-bold text-slate-400 uppercase tracking-widest ml-1">Availability</label>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{filterOptions.availabilities.map(av => (
|
||||
<button
|
||||
key={av}
|
||||
onClick={() => setFilters({...filters, availability: av})}
|
||||
className={`px-3 py-3 rounded-xl text-[10px] font-bold text-center border transition-all ${
|
||||
filters.availability === av
|
||||
? 'bg-[#185FA5] border-[#185FA5] text-white shadow-lg shadow-blue-500/20'
|
||||
: 'bg-white border-slate-100 text-slate-400'
|
||||
}`}
|
||||
>
|
||||
{av === 'All Availabilities' ? 'Any' : av.replace('Available ', '')}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Experience */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-[11px] font-bold text-slate-400 uppercase tracking-widest ml-1">Experience</label>
|
||||
<select
|
||||
value={filters.experience}
|
||||
onChange={(e) => setFilters({...filters, experience: e.target.value})}
|
||||
className="w-full px-5 py-4 bg-slate-50 border border-slate-100 rounded-2xl text-sm font-bold appearance-none focus:bg-white focus:border-[#185FA5] outline-none transition-all"
|
||||
>
|
||||
{filterOptions.experienceLevels.map(ex => <option key={ex} value={ex}>{ex}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Salary Slider */}
|
||||
<div className="space-y-4 pt-2">
|
||||
<div className="flex items-center justify-between px-1">
|
||||
<label className="text-[11px] font-bold text-slate-400 uppercase tracking-widest">Max Salary</label>
|
||||
<span className="text-sm font-bold text-[#185FA5]">{filters.maxSalary} AED</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min="1200"
|
||||
max="3000"
|
||||
step="100"
|
||||
value={filters.maxSalary}
|
||||
onChange={(e) => setFilters({...filters, maxSalary: Number(e.target.value)})}
|
||||
className="w-full h-2 bg-slate-100 rounded-full appearance-none cursor-pointer accent-[#185FA5]"
|
||||
/>
|
||||
<div className="flex justify-between text-[10px] font-bold text-slate-300 uppercase px-1">
|
||||
<span>1200 AED</span>
|
||||
<span>3000 AED</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setShowFilters(false)}
|
||||
className="w-full py-5 bg-[#185FA5] text-white rounded-3xl font-bold text-base shadow-xl shadow-blue-500/30 active:scale-[0.98] transition-all mt-4"
|
||||
>
|
||||
Apply Filters
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</EmployerMobileLayout>
|
||||
);
|
||||
}
|
||||
@ -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 (
|
||||
<div className="min-h-screen bg-[#F8F9FA] font-sans text-[#141B34] flex flex-col max-w-md mx-auto relative">
|
||||
{/* Content Area */}
|
||||
<main className="flex-1 overflow-y-auto pb-24">
|
||||
{children}
|
||||
</main>
|
||||
|
||||
{/* Bottom Navigation */}
|
||||
<div className="fixed bottom-0 left-0 right-0 max-w-md mx-auto bg-white border-t border-[#E5E7EB] px-2 py-2 flex items-center justify-between z-50 h-[70px]">
|
||||
{navItems.map((item) => {
|
||||
const active = isActive(item.href);
|
||||
return (
|
||||
<Link
|
||||
key={item.label}
|
||||
href={item.href}
|
||||
className={`flex flex-col items-center justify-center flex-1 space-y-1 transition-all relative ${active ? 'text-[#141B34]' : 'text-slate-400'
|
||||
}`}
|
||||
>
|
||||
<div className="relative">
|
||||
<item.icon className={`w-6 h-6 ${active ? 'fill-[#141B34]' : ''}`} />
|
||||
{item.badge && (
|
||||
<div className="absolute -top-1.5 -right-1.5 min-w-[16px] h-4 bg-[#185FA5] text-white text-[8px] font-bold rounded-full flex items-center justify-center border border-white px-1">
|
||||
{item.badge}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-[9px] font-bold tracking-tight">{item.label}</span>
|
||||
{active && (
|
||||
<div className="absolute bottom-[-8px] w-1.5 h-1.5 bg-[#141B34] rounded-full" />
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -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 (
|
||||
<div className="min-h-screen bg-white font-sans text-slate-900 flex flex-col max-w-md mx-auto shadow-2xl relative overflow-hidden">
|
||||
<Head title="Welcome" />
|
||||
|
||||
{/* Background Decor */}
|
||||
<div className="absolute top-[-10%] right-[-10%] w-64 h-64 bg-blue-50 rounded-full blur-3xl opacity-50" />
|
||||
<div className="absolute bottom-[-10%] left-[-10%] w-64 h-64 bg-[#185FA5]/5 rounded-full blur-3xl opacity-50" />
|
||||
|
||||
{/* Language Selector */}
|
||||
<div className="px-6 pt-12 flex justify-end relative z-10">
|
||||
<button className="flex items-center space-x-2 bg-slate-50 px-4 py-2 rounded-2xl border border-slate-100 active:scale-95 transition-all">
|
||||
<Globe className="w-4 h-4 text-[#185FA5]" />
|
||||
<span className="text-[10px] font-black uppercase tracking-widest">English</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 px-8 flex flex-col justify-center space-y-12 relative z-10">
|
||||
{/* Hero */}
|
||||
<div className="space-y-6 text-center">
|
||||
<div className="flex justify-center">
|
||||
<div className="w-20 h-20 bg-[#185FA5] rounded-[32px] flex items-center justify-center shadow-2xl shadow-blue-500/30">
|
||||
<ShieldCheck className="w-10 h-10 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-4xl font-black tracking-tighter uppercase leading-none">Marketplace</h1>
|
||||
<p className="text-[11px] font-black text-slate-300 uppercase tracking-[0.3em]">Direct Hiring Portal</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Selection Cards */}
|
||||
<div className="space-y-4">
|
||||
<Link
|
||||
href="/mobile/employer/login"
|
||||
className="group bg-white p-8 rounded-[40px] border-2 border-slate-50 shadow-sm flex items-center justify-between active:scale-95 transition-all hover:border-[#185FA5] hover:shadow-xl hover:shadow-blue-500/10"
|
||||
>
|
||||
<div className="flex items-center space-x-6">
|
||||
<div className="w-16 h-16 bg-slate-50 rounded-[24px] flex items-center justify-center group-hover:bg-blue-50 transition-colors">
|
||||
<Briefcase className="w-8 h-8 text-[#185FA5]" />
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<h3 className="text-xl font-black tracking-tight uppercase leading-none mb-2">Employer</h3>
|
||||
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">I want to hire</p>
|
||||
</div>
|
||||
</div>
|
||||
<ArrowRight className="w-6 h-6 text-slate-200 group-hover:text-[#185FA5] transition-colors" />
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/mobile/worker/register"
|
||||
className="group bg-white p-8 rounded-[40px] border-2 border-slate-50 shadow-sm flex items-center justify-between active:scale-95 transition-all hover:border-[#185FA5] hover:shadow-xl hover:shadow-blue-500/10"
|
||||
>
|
||||
<div className="flex items-center space-x-6">
|
||||
<div className="w-16 h-16 bg-slate-50 rounded-[24px] flex items-center justify-center group-hover:bg-blue-50 transition-colors">
|
||||
<User className="w-8 h-8 text-[#185FA5]" />
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<h3 className="text-xl font-black tracking-tight uppercase leading-none mb-2">Worker</h3>
|
||||
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">I want a job</p>
|
||||
</div>
|
||||
</div>
|
||||
<ArrowRight className="w-6 h-6 text-slate-200 group-hover:text-[#185FA5] transition-colors" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="text-center pt-4">
|
||||
<div className="flex items-center justify-center space-x-2 text-[10px] font-black text-slate-300 uppercase tracking-widest">
|
||||
<Zap className="w-3 h-3 text-amber-500 fill-amber-500" />
|
||||
<span>Trusted by 10k+ users</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer Links */}
|
||||
<div className="px-8 pb-12 text-center relative z-10">
|
||||
<p className="text-[11px] font-black text-slate-400 uppercase tracking-widest leading-loose">
|
||||
Need Help? <br />
|
||||
<Link href="/support" className="text-[#185FA5] underline underline-offset-4 decoration-2">Contact Support</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -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 (
|
||||
<div className="min-h-screen bg-white font-sans text-slate-900 flex flex-col max-w-md mx-auto shadow-2xl relative overflow-hidden pb-24">
|
||||
<Head title="Updates" />
|
||||
|
||||
{/* Header */}
|
||||
<div className="px-6 pt-12 pb-6 bg-white sticky top-0 z-50">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-300" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search updates..."
|
||||
className="w-full pl-12 pr-6 py-4 bg-[#F8FAFC] border-none rounded-2xl text-[11px] font-black uppercase tracking-tight shadow-sm focus:ring-4 focus:ring-blue-100 outline-none transition-all"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 px-6 space-y-6 overflow-y-auto">
|
||||
{/* Section Title */}
|
||||
<div className="flex items-center space-x-2 pt-4 px-1">
|
||||
<Megaphone className="w-4 h-4 text-[#185FA5]" />
|
||||
<h2 className="text-[10px] font-black text-slate-400 uppercase tracking-[0.2em]">Latest Updates</h2>
|
||||
</div>
|
||||
|
||||
{/* Cards */}
|
||||
<div className="space-y-6 pb-10">
|
||||
{announcements.map((item) => (
|
||||
<div key={item.id} className="bg-white p-8 rounded-[40px] border border-slate-100 shadow-xl shadow-slate-200/40 space-y-4 active:scale-[0.98] transition-all">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="bg-blue-50 text-[#185FA5] px-4 py-1.5 rounded-full text-[9px] font-black uppercase tracking-[0.1em]">{item.category}</span>
|
||||
<div className="flex items-center text-[9px] font-black text-slate-300 uppercase tracking-widest">
|
||||
<Calendar className="w-3 h-3 mr-1.5" /> {item.date}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-black text-slate-900 text-[13px] uppercase tracking-tight leading-tight">{item.title}</h3>
|
||||
<p className="text-xs text-slate-500 leading-relaxed font-medium">{item.text}</p>
|
||||
</div>
|
||||
<button className="flex items-center text-[10px] font-black text-[#185FA5] uppercase tracking-[0.1em] pt-2">
|
||||
Read More <ChevronRight className="w-3 h-3 ml-1" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom Navigation */}
|
||||
<div className="fixed bottom-0 left-0 right-0 max-w-md mx-auto bg-white/90 backdrop-blur-xl border-t border-slate-100 px-6 py-4 flex items-center justify-between z-50">
|
||||
<Link href="/mobile/worker/home" className="flex flex-col items-center space-y-1 text-slate-300">
|
||||
<Home className="w-5 h-5" />
|
||||
<span className="text-[8px] font-black uppercase tracking-tight">Home</span>
|
||||
</Link>
|
||||
<Link href="/mobile/worker/explore" className="flex flex-col items-center space-y-1 text-slate-300">
|
||||
<Compass className="w-5 h-5" />
|
||||
<span className="text-[8px] font-black uppercase tracking-tight">Explore</span>
|
||||
</Link>
|
||||
<Link href="/mobile/worker/announcements" className="flex flex-col items-center space-y-1 text-[#185FA5]">
|
||||
<Bell className="w-5 h-5 fill-current" />
|
||||
<span className="text-[8px] font-black uppercase tracking-tight">Updates</span>
|
||||
</Link>
|
||||
<Link href="/mobile/worker/chat" className="flex flex-col items-center space-y-1 text-slate-300">
|
||||
<MessageCircle className="w-5 h-5" />
|
||||
<span className="text-[8px] font-black uppercase tracking-tight">Chat</span>
|
||||
</Link>
|
||||
<Link href="/mobile/worker/profile" className="flex flex-col items-center space-y-1 text-slate-300">
|
||||
<User className="w-5 h-5" />
|
||||
<span className="text-[8px] font-black uppercase tracking-tight">Profile</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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 (
|
||||
<div className="min-h-screen bg-[#F9F9F9] font-sans text-slate-900 flex flex-col max-w-md mx-auto shadow-2xl relative overflow-hidden">
|
||||
<Head title="Chat" />
|
||||
|
||||
{/* Header */}
|
||||
<div className="px-6 pt-12 pb-4 bg-white flex items-center justify-between border-b border-slate-50 sticky top-0 z-50">
|
||||
<button onClick={() => window.history.back()} className="w-10 h-10 flex items-center justify-center text-slate-900">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</button>
|
||||
<div className="text-center">
|
||||
<h1 className="text-xs font-black uppercase tracking-widest">Al Mansoor</h1>
|
||||
<span className="text-[8px] text-emerald-500 font-black uppercase tracking-widest">Online</span>
|
||||
</div>
|
||||
<button className="w-10 h-10 flex items-center justify-center text-slate-900">
|
||||
<MoreVertical className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Message Area */}
|
||||
<div className="flex-1 overflow-y-auto px-6 py-8 space-y-6">
|
||||
{messages.map((msg) => (
|
||||
<div key={msg.id} className={`flex ${msg.sender === 'me' ? 'justify-end' : 'justify-start'}`}>
|
||||
<div className={`max-w-[80%] p-4 rounded-[28px] ${
|
||||
msg.sender === 'me'
|
||||
? 'bg-[#185FA5] text-white rounded-tr-none shadow-lg shadow-blue-500/10'
|
||||
: 'bg-white text-slate-700 rounded-tl-none border border-slate-50 shadow-sm'
|
||||
}`}>
|
||||
<p className="text-xs font-medium leading-relaxed">{msg.text}</p>
|
||||
<div className={`flex items-center justify-end space-x-1 mt-2 ${msg.sender === 'me' ? 'text-blue-100/60' : 'text-slate-300'}`}>
|
||||
<span className="text-[8px] font-black uppercase">{msg.time}</span>
|
||||
{msg.sender === 'me' && <CheckCheck className="w-3 h-3" />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Input Bar */}
|
||||
<div className="p-6 bg-white border-t border-slate-50">
|
||||
<div className="flex items-center space-x-3">
|
||||
<button className="w-12 h-12 rounded-2xl bg-slate-50 text-slate-400 flex items-center justify-center active:scale-95 transition-all">
|
||||
<Paperclip className="w-5 h-5" />
|
||||
</button>
|
||||
<div className="flex-1 relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Write your message..."
|
||||
className="w-full pl-6 pr-14 py-4 bg-slate-50 border-none rounded-2xl text-[11px] font-black uppercase tracking-tight focus:ring-4 focus:ring-blue-100 transition-all outline-none"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
/>
|
||||
<button className="absolute right-2 top-1/2 -translate-y-1/2 w-10 h-10 bg-[#185FA5] text-white rounded-xl flex items-center justify-center shadow-lg active:scale-95 transition-all">
|
||||
<Send className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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 (
|
||||
<div className="min-h-screen bg-white font-sans text-slate-900 flex flex-col max-w-md mx-auto shadow-2xl relative overflow-hidden pb-24">
|
||||
<Head title="Messages" />
|
||||
|
||||
{/* Header */}
|
||||
<div className="px-6 pt-12 pb-4 bg-white flex items-center justify-between border-b border-slate-50 sticky top-0 z-50">
|
||||
<button onClick={() => window.history.back()} className="w-10 h-10 flex items-center justify-center text-slate-900">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</button>
|
||||
<h1 className="text-sm font-black uppercase tracking-widest">Messages</h1>
|
||||
<button className="w-10 h-10 flex items-center justify-center text-slate-900">
|
||||
<Search className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto px-6 py-6 space-y-2">
|
||||
{chats.map((chat) => (
|
||||
<Link
|
||||
key={chat.id}
|
||||
href="/mobile/worker/chat/detail"
|
||||
className="flex items-center space-x-4 p-4 rounded-[24px] hover:bg-slate-50 transition-all border border-transparent active:border-slate-100 active:scale-[0.98]"
|
||||
>
|
||||
<div className="w-14 h-14 rounded-full bg-[#E0E7FF] flex items-center justify-center text-[#185FA5] font-black text-lg shadow-sm border-2 border-white">
|
||||
{chat.avatar}
|
||||
</div>
|
||||
<div className="flex-1 space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-black text-sm text-slate-900 uppercase tracking-tight">{chat.name}</h3>
|
||||
<span className="text-[9px] font-bold text-slate-300 uppercase">{chat.time}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className={`text-xs line-clamp-1 ${chat.unread > 0 ? 'font-black text-slate-900' : 'text-slate-400'}`}>
|
||||
{chat.message}
|
||||
</p>
|
||||
{chat.unread > 0 ? (
|
||||
<div className="w-5 h-5 bg-[#185FA5] rounded-full flex items-center justify-center text-[10px] font-black text-white shadow-lg shadow-blue-500/20">
|
||||
{chat.unread}
|
||||
</div>
|
||||
) : (
|
||||
<CheckCheck className="w-4 h-4 text-[#185FA5]" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Bottom Navigation */}
|
||||
<div className="fixed bottom-0 left-0 right-0 max-w-md mx-auto bg-white/90 backdrop-blur-xl border-t border-slate-100 px-6 py-4 flex items-center justify-between z-50">
|
||||
<Link href="/mobile/worker/home" className="flex flex-col items-center space-y-1 text-slate-300">
|
||||
<Home className="w-5 h-5" />
|
||||
<span className="text-[8px] font-black uppercase tracking-tight">Home</span>
|
||||
</Link>
|
||||
<Link href="#" className="flex flex-col items-center space-y-1 text-slate-300">
|
||||
<Compass className="w-5 h-5" />
|
||||
<span className="text-[8px] font-black uppercase tracking-tight">Explore</span>
|
||||
</Link>
|
||||
<Link href="/mobile/worker/announcements" className="flex flex-col items-center space-y-1 text-slate-300">
|
||||
<Bell className="w-5 h-5" />
|
||||
<span className="text-[8px] font-black uppercase tracking-tight">Updates</span>
|
||||
</Link>
|
||||
<Link href="/mobile/worker/chat" className="flex flex-col items-center space-y-1 text-[#185FA5]">
|
||||
<MessageCircle className="w-5 h-5 fill-current" />
|
||||
<span className="text-[8px] font-black uppercase tracking-tight">Chat</span>
|
||||
</Link>
|
||||
<Link href="/mobile/worker/profile" className="flex flex-col items-center space-y-1 text-slate-300">
|
||||
<User className="w-5 h-5" />
|
||||
<span className="text-[8px] font-black uppercase tracking-tight">Profile</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -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 (
|
||||
<div className="min-h-screen bg-[#F8FAFC] flex flex-col max-w-md mx-auto relative font-sans text-slate-900 overflow-x-hidden">
|
||||
<Head title={`${worker.name} - Profile`} />
|
||||
|
||||
{/* Header */}
|
||||
<div className="fixed top-0 left-0 right-0 max-w-md mx-auto z-50 px-6 py-12 flex items-center justify-between pointer-events-none">
|
||||
<Link
|
||||
href="/mobile/employer/workers"
|
||||
className="p-3 bg-white/80 backdrop-blur-xl border border-slate-100 rounded-2xl text-slate-400 pointer-events-auto shadow-sm active:scale-95 transition-all"
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
</Link>
|
||||
<div className="flex space-x-2 pointer-events-auto">
|
||||
<button className="p-3 bg-white/80 backdrop-blur-xl border border-slate-100 rounded-2xl text-slate-400 shadow-sm active:scale-95 transition-all">
|
||||
<Bookmark className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Profile Hero */}
|
||||
<div className="bg-white pt-32 pb-10 px-6 rounded-b-[48px] shadow-sm relative">
|
||||
<div className="flex flex-col items-center text-center space-y-4">
|
||||
<div className="relative">
|
||||
<div className="w-32 h-32 rounded-full bg-[#D8F3FF] flex items-center justify-center text-[#185FA5] font-black text-4xl shadow-inner border-4 border-white">
|
||||
{worker.initial}
|
||||
</div>
|
||||
<div className="absolute bottom-1 right-1 w-8 h-8 bg-emerald-500 border-4 border-white rounded-full flex items-center justify-center text-white">
|
||||
<CheckCircle2 className="w-4 h-4" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-2xl font-bold tracking-tight">{worker.name}</h1>
|
||||
<div className="flex items-center justify-center space-x-2 text-slate-400 font-bold text-xs uppercase tracking-widest">
|
||||
<Globe className="w-3.5 h-3.5 text-[#185FA5]" />
|
||||
<span>{worker.nationality}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2 pt-2">
|
||||
<div className="px-4 py-2 bg-[#F0F7FF] text-[#185FA5] rounded-xl text-[10px] font-black uppercase tracking-widest border border-[#E1EFFF]">
|
||||
{worker.role}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Key Stats Grid */}
|
||||
<div className="grid grid-cols-2 gap-3 mt-10">
|
||||
<div className="bg-slate-50/50 p-4 rounded-3xl border border-slate-50 flex items-center space-x-3">
|
||||
<div className="w-10 h-10 bg-white rounded-xl flex items-center justify-center text-[#185FA5] shadow-sm">
|
||||
<DollarSign className="w-5 h-5" />
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
<div className="text-[9px] font-bold text-slate-400 uppercase tracking-widest">Salary</div>
|
||||
<div className="text-[13px] font-black">{worker.salary}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-slate-50/50 p-4 rounded-3xl border border-slate-50 flex items-center space-x-3">
|
||||
<div className="w-10 h-10 bg-white rounded-xl flex items-center justify-center text-emerald-500 shadow-sm">
|
||||
<Briefcase className="w-5 h-5" />
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
<div className="text-[9px] font-bold text-slate-400 uppercase tracking-widest">Experience</div>
|
||||
<div className="text-[13px] font-black">{worker.exp}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Profile Content */}
|
||||
<div className="p-6 space-y-10 pb-32">
|
||||
{/* About Section */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-1.5 h-1.5 bg-[#185FA5] rounded-full" />
|
||||
<h3 className="text-[11px] font-bold text-slate-400 uppercase tracking-widest">Professional Bio</h3>
|
||||
</div>
|
||||
<div className="bg-white p-6 rounded-[32px] border border-slate-50 shadow-sm">
|
||||
<p className="text-[14px] text-slate-600 font-medium leading-relaxed italic">
|
||||
{worker.bio}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Detailed Information */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-1.5 h-1.5 bg-[#185FA5] rounded-full" />
|
||||
<h3 className="text-[11px] font-bold text-slate-400 uppercase tracking-widest">Candidate Overview</h3>
|
||||
</div>
|
||||
<div className="bg-white rounded-[32px] border border-slate-50 shadow-sm overflow-hidden divide-y divide-slate-50">
|
||||
<div className="flex items-center justify-between p-5">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Calendar className="w-5 h-5 text-slate-300" />
|
||||
<span className="text-xs font-bold text-slate-500">Availability</span>
|
||||
</div>
|
||||
<span className="text-xs font-black text-[#185FA5] uppercase tracking-widest">{worker.avail}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-5">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Heart className="w-5 h-5 text-slate-300" />
|
||||
<span className="text-xs font-bold text-slate-500">Religion</span>
|
||||
</div>
|
||||
<span className="text-xs font-black text-slate-700 uppercase tracking-widest">{worker.religion}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-5">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Sparkles className="w-5 h-5 text-slate-300" />
|
||||
<span className="text-xs font-bold text-slate-500">Age</span>
|
||||
</div>
|
||||
<span className="text-xs font-black text-slate-700 uppercase tracking-widest">{worker.age}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-5">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Languages className="w-5 h-5 text-slate-300" />
|
||||
<span className="text-xs font-bold text-slate-500">Languages</span>
|
||||
</div>
|
||||
<span className="text-[10px] font-black text-slate-700 uppercase tracking-widest">{worker.languages.join(' · ')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Vetted Documents */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-1.5 h-1.5 bg-[#185FA5] rounded-full" />
|
||||
<h3 className="text-[11px] font-bold text-slate-400 uppercase tracking-widest">Identity & Verification</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-3">
|
||||
{worker.documents.map((doc, idx) => (
|
||||
<div key={idx} className="bg-white p-5 rounded-[24px] border border-slate-50 shadow-sm flex items-center justify-between group active:bg-slate-50 transition-colors">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-10 h-10 bg-slate-50 rounded-xl flex items-center justify-center text-slate-300 group-active:text-[#185FA5] transition-colors">
|
||||
<FileText className="w-5 h-5" />
|
||||
</div>
|
||||
<span className="text-xs font-bold text-slate-700">{doc.name}</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1.5 text-[9px] font-black text-emerald-500 uppercase tracking-widest bg-emerald-50 px-2.5 py-1 rounded-lg">
|
||||
<ShieldCheck className="w-3 h-3" />
|
||||
<span>{doc.status}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Skills Section */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-1.5 h-1.5 bg-[#185FA5] rounded-full" />
|
||||
<h3 className="text-[11px] font-bold text-slate-400 uppercase tracking-widest">Skills & Expertise</h3>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2.5">
|
||||
{worker.skills.map((skill) => (
|
||||
<span key={skill} className="px-4 py-2 bg-white text-slate-500 text-[11px] font-bold rounded-xl border border-slate-50 shadow-sm">
|
||||
{skill}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Floating Action Bar */}
|
||||
<div className="fixed bottom-0 left-0 right-0 max-w-md mx-auto p-6 bg-white/80 backdrop-blur-xl border-t border-slate-100 flex items-center space-x-4 z-50">
|
||||
<Link
|
||||
href={`/mobile/employer/chat/${worker.id}`}
|
||||
className="w-16 h-16 bg-slate-50 text-[#185FA5] rounded-[24px] flex items-center justify-center border border-slate-100 shadow-sm active:scale-95 transition-all"
|
||||
>
|
||||
<MessageSquare className="w-6 h-6" />
|
||||
</Link>
|
||||
<button className="flex-1 h-16 bg-[#185FA5] text-white rounded-[24px] font-black text-xs uppercase tracking-[0.2em] shadow-xl shadow-blue-500/30 active:scale-[0.98] transition-all flex items-center justify-center space-x-3">
|
||||
<span>Initiate Hiring</span>
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -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 (
|
||||
<div className="min-h-screen bg-white font-sans text-slate-900 flex flex-col max-w-md mx-auto shadow-2xl relative overflow-hidden">
|
||||
<Head title="Quick Edit" />
|
||||
|
||||
{/* Header */}
|
||||
<div className="px-6 pt-12 pb-6 bg-white sticky top-0 z-50 flex items-center justify-between border-b border-slate-50">
|
||||
<button onClick={() => window.history.back()} className="w-12 h-12 rounded-2xl bg-slate-50 flex items-center justify-center text-slate-900 border border-slate-100 active:scale-95 transition-all">
|
||||
<ArrowLeft className="w-6 h-6" />
|
||||
</button>
|
||||
<h1 className="text-sm font-black uppercase tracking-tight">Edit Skills</h1>
|
||||
<div className="w-12" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 px-8 py-8 space-y-12 overflow-y-auto pb-32">
|
||||
{/* Work Section */}
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Briefcase className="w-5 h-5 text-[#185FA5]" />
|
||||
<h2 className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400">My Skills</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{commonSkills.map((skill) => (
|
||||
<button
|
||||
key={skill.name}
|
||||
onClick={() => toggle(skill.name, selectedSkills, setSelectedSkills)}
|
||||
className={`p-6 rounded-[32px] border-2 text-center transition-all active:scale-95 flex flex-col items-center space-y-3 ${
|
||||
selectedSkills.includes(skill.name)
|
||||
? 'bg-[#185FA5] border-[#185FA5] text-white shadow-xl shadow-blue-500/20'
|
||||
: 'bg-slate-50 border-slate-50 text-slate-400'
|
||||
}`}
|
||||
>
|
||||
<span className="text-2xl">{skill.icon}</span>
|
||||
<span className="text-[10px] font-black uppercase tracking-tight">{skill.name}</span>
|
||||
</button>
|
||||
))}
|
||||
|
||||
{/* Other Skill Button */}
|
||||
<button
|
||||
onClick={() => setShowOtherInput(!showOtherInput)}
|
||||
className={`p-6 rounded-[32px] border-2 text-center transition-all active:scale-95 flex flex-col items-center justify-center space-y-3 ${
|
||||
showOtherInput || otherSkill
|
||||
? 'bg-[#185FA5] border-[#185FA5] text-white shadow-xl shadow-blue-500/20'
|
||||
: 'bg-slate-50 border-slate-50 text-slate-400'
|
||||
}`}
|
||||
>
|
||||
<span className="text-2xl">➕</span>
|
||||
<span className="text-[10px] font-black uppercase tracking-tight">Other</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showOtherInput && (
|
||||
<div className="animate-in fade-in slide-in-from-top-2 duration-300">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Type your skill..."
|
||||
className="w-full px-6 py-4 bg-slate-50 border-none rounded-2xl text-[11px] font-black uppercase tracking-tight focus:ring-4 focus:ring-blue-100 transition-all outline-none"
|
||||
value={otherSkill}
|
||||
onChange={(e) => setOtherSkill(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Locations Section */}
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center space-x-3">
|
||||
<MapPin className="w-5 h-5 text-[#185FA5]" />
|
||||
<h2 className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400">My Locations</h2>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{commonAreas.map((area) => (
|
||||
<button
|
||||
key={area}
|
||||
onClick={() => toggle(area, selectedAreas, setSelectedAreas)}
|
||||
className={`px-6 py-4 rounded-2xl border-2 transition-all active:scale-95 ${
|
||||
selectedAreas.includes(area)
|
||||
? 'bg-[#185FA5] border-[#185FA5] text-white'
|
||||
: 'bg-slate-50 border-slate-50 text-slate-400'
|
||||
}`}
|
||||
>
|
||||
<span className="text-[10px] font-black uppercase tracking-tight">{area}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Languages Section */}
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Globe className="w-5 h-5 text-[#185FA5]" />
|
||||
<h2 className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400">My Languages</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{commonLangs.map((lang) => (
|
||||
<button
|
||||
key={lang}
|
||||
onClick={() => toggle(lang, selectedLangs, setSelectedLangs)}
|
||||
className={`py-4 rounded-2xl border-2 transition-all active:scale-95 ${
|
||||
selectedLangs.includes(lang)
|
||||
? 'bg-[#185FA5] border-[#185FA5] text-white'
|
||||
: 'bg-slate-50 border-slate-50 text-slate-400'
|
||||
}`}
|
||||
>
|
||||
<span className="text-[10px] font-black uppercase tracking-tight">{lang}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Huge Save Button */}
|
||||
<div className="fixed bottom-0 left-0 right-0 max-w-md mx-auto p-8 bg-white/90 backdrop-blur-xl border-t border-slate-50 z-50">
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className={`w-full py-5 rounded-[28px] font-black text-[12px] uppercase tracking-[0.2em] shadow-2xl transition-all flex items-center justify-center space-x-3 active:scale-95 ${
|
||||
saved ? 'bg-emerald-500 text-white shadow-emerald-500/20' : 'bg-[#185FA5] text-white shadow-blue-500/20'
|
||||
}`}
|
||||
>
|
||||
{saved ? <Check className="w-6 h-6" /> : <Save className="w-6 h-6" />}
|
||||
<span>{saved ? 'Changes Saved!' : 'Update Profile'}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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 (
|
||||
<div className="min-h-screen bg-[#F9F9F9] font-sans text-slate-900 flex flex-col max-w-md mx-auto shadow-2xl relative overflow-hidden pb-24">
|
||||
<Head title="Jobs" />
|
||||
|
||||
{/* Header / Search Area */}
|
||||
<div className="px-6 pt-12 pb-6 bg-white space-y-6 sticky top-0 z-50 border-b border-slate-50">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-xl font-black uppercase tracking-tight">Jobs</h1>
|
||||
<button
|
||||
onClick={() => setShowFilters(true)}
|
||||
className="w-10 h-10 rounded-full bg-slate-50 flex items-center justify-center text-slate-900 border border-slate-100 active:scale-95 transition-all"
|
||||
>
|
||||
<SlidersHorizontal className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Search Bar */}
|
||||
<div className="relative">
|
||||
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-300" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search jobs, companies..."
|
||||
className="w-full pl-12 pr-6 py-4 bg-slate-50 border-none rounded-2xl text-[11px] font-black uppercase tracking-tight shadow-sm focus:ring-4 focus:ring-blue-100 outline-none transition-all"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Category Chips */}
|
||||
<div className="flex space-x-3 overflow-x-auto pb-1 no-scrollbar">
|
||||
{categories.map((cat) => (
|
||||
<button
|
||||
key={cat}
|
||||
onClick={() => setActiveCat(cat)}
|
||||
className={`shrink-0 px-5 py-3 rounded-2xl font-black text-[10px] uppercase tracking-widest transition-all active:scale-95 ${
|
||||
activeCat === cat
|
||||
? 'bg-[#185FA5] text-white shadow-lg shadow-blue-500/20'
|
||||
: 'bg-white text-slate-400 border border-slate-50'
|
||||
}`}
|
||||
>
|
||||
{cat}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto px-6 py-8 space-y-6 no-scrollbar">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-black text-[10px] uppercase tracking-[0.2em] text-slate-300">Found {jobs.length} Jobs</h3>
|
||||
<div className="flex items-center space-x-2 text-[10px] font-black text-[#185FA5] uppercase">
|
||||
<MapPin className="w-3 h-3" />
|
||||
<span>Dubai, UAE</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Job List */}
|
||||
<div className="space-y-4 pb-10">
|
||||
{jobs.map((job) => (
|
||||
<Link
|
||||
key={job.id}
|
||||
href="/mobile/worker/job/detail"
|
||||
className="bg-white p-6 rounded-[32px] border border-slate-100 shadow-sm space-y-5 active:scale-[0.98] transition-all block"
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="w-12 h-12 rounded-full bg-slate-50 flex items-center justify-center text-slate-900 border border-slate-100/50">
|
||||
<div className="text-xl">{job.icon}</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span className="text-lg font-black text-[#185FA5]">{job.salary}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-black text-slate-900 text-[17px] tracking-tight leading-tight">{job.title}</h3>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium text-slate-400 leading-tight">
|
||||
High street avenue<br />
|
||||
London, UK
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm font-black text-slate-900">From Fri, 17 Dec</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<span className="bg-blue-50/50 text-[#185FA5] px-5 py-2 rounded-full text-xs font-black uppercase tracking-tight">
|
||||
{job.type}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filter Bottom Sheet */}
|
||||
{showFilters && (
|
||||
<div className="fixed inset-0 z-[100] flex items-end justify-center px-4 pb-4">
|
||||
<div className="absolute inset-0 bg-slate-900/60 backdrop-blur-sm" onClick={() => setShowFilters(false)} />
|
||||
|
||||
<div className="relative w-full max-w-md bg-white rounded-[40px] shadow-2xl animate-in slide-in-from-bottom duration-300 overflow-hidden">
|
||||
{/* Handle */}
|
||||
<div className="w-full flex justify-center py-4">
|
||||
<div className="w-12 h-1.5 bg-slate-100 rounded-full" />
|
||||
</div>
|
||||
|
||||
<div className="px-8 pb-10 space-y-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xl font-black text-slate-900 tracking-tight">Filter result</h2>
|
||||
<span className="text-xs font-bold text-slate-400 uppercase tracking-widest">22 result</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-10 max-h-[60vh] overflow-y-auto no-scrollbar py-2">
|
||||
{/* Sort By */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-black text-slate-900">Sort by</h3>
|
||||
<div className="flex flex-wrap gap-2.5">
|
||||
{['Default', 'Latest First', 'Oldest First', 'Price: Low to High', 'Price: High to Low'].map((opt, i) => (
|
||||
<button key={opt} className={`px-6 py-3 rounded-2xl text-[10px] font-black uppercase tracking-widest transition-all border ${i === 0 ? 'bg-blue-50 border-blue-100 text-[#185FA5]' : 'bg-white border-slate-100 text-slate-400'}`}>
|
||||
{opt}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contract Type */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-black text-slate-900">Contract type</h3>
|
||||
<div className="flex flex-wrap gap-2.5">
|
||||
{['Part time', 'Full time', 'Contractual'].map((opt) => (
|
||||
<button key={opt} className="px-6 py-3 rounded-2xl bg-white border border-slate-100 text-slate-400 text-[10px] font-black uppercase tracking-widest transition-all">
|
||||
{opt}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Skill Range */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-black text-slate-900">Skill range</h3>
|
||||
<div className="flex flex-wrap gap-2.5">
|
||||
{['Junior', 'Medium', 'Expert'].map((opt) => (
|
||||
<button key={opt} className="px-6 py-3 rounded-2xl bg-white border border-slate-100 text-slate-400 text-[10px] font-black uppercase tracking-widest transition-all">
|
||||
{opt}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Salary Type */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-black text-slate-900">Salary type</h3>
|
||||
<div className="flex flex-wrap gap-2.5">
|
||||
{['Hourly Rate', 'Fix Rate'].map((opt) => (
|
||||
<button key={opt} className="px-6 py-3 rounded-2xl bg-white border border-slate-100 text-slate-400 text-[10px] font-black uppercase tracking-widest transition-all">
|
||||
{opt}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center space-x-4 pt-4 border-t border-slate-50">
|
||||
<button onClick={() => setShowFilters(false)} className="flex-1 py-5 bg-blue-50 text-[#185FA5] rounded-[24px] font-black text-[11px] uppercase tracking-[0.2em] active:scale-95 transition-all">
|
||||
Reset
|
||||
</button>
|
||||
<button onClick={() => setShowFilters(false)} className="flex-1 py-5 bg-[#185FA5] text-white rounded-[24px] font-black text-[11px] uppercase tracking-[0.2em] shadow-xl shadow-blue-500/20 active:scale-95 transition-all">
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Bottom Navigation */}
|
||||
<div className="fixed bottom-0 left-0 right-0 max-w-md mx-auto bg-white/90 backdrop-blur-xl border-t border-slate-100 px-6 py-4 flex items-center justify-between z-50">
|
||||
<Link href="/mobile/worker/home" className="flex flex-col items-center space-y-1 text-slate-300">
|
||||
<Home className="w-5 h-5" />
|
||||
<span className="text-[8px] font-black uppercase tracking-tight">Home</span>
|
||||
</Link>
|
||||
<Link href="/mobile/worker/explore" className="flex flex-col items-center space-y-1 text-[#185FA5]">
|
||||
<Briefcase className="w-5 h-5 fill-current" />
|
||||
<span className="text-[8px] font-black uppercase tracking-tight">Jobs</span>
|
||||
</Link>
|
||||
<Link href="/mobile/worker/announcements" className="flex flex-col items-center space-y-1 text-slate-300">
|
||||
<Bell className="w-5 h-5" />
|
||||
<span className="text-[8px] font-black uppercase tracking-tight">Updates</span>
|
||||
</Link>
|
||||
<Link href="/mobile/worker/chat" className="flex flex-col items-center space-y-1 text-slate-300">
|
||||
<MessageCircle className="w-5 h-5" />
|
||||
<span className="text-[8px] font-black uppercase tracking-tight">Chat</span>
|
||||
</Link>
|
||||
<Link href="/mobile/worker/profile" className="flex flex-col items-center space-y-1 text-slate-300">
|
||||
<User className="w-5 h-5" />
|
||||
<span className="text-[8px] font-black uppercase tracking-tight">Profile</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -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 (
|
||||
<div className="min-h-screen bg-[#F9F9F9] font-sans text-slate-900 flex flex-col max-w-md mx-auto shadow-2xl relative overflow-hidden pb-24">
|
||||
<Head title="Home" />
|
||||
|
||||
{/* Header Area */}
|
||||
<div className="px-4 pt-12 pb-4 space-y-6 flex-shrink-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-10 h-10 rounded-full bg-slate-200 border-2 border-white shadow-sm overflow-hidden">
|
||||
<img src="https://api.dicebear.com/7.x/avataaars/svg?seed=Danial" alt="avatar" />
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<p className="text-[10px] text-slate-400 font-bold">Hi, Danial</p>
|
||||
<button className="flex items-center space-x-1">
|
||||
<span className="text-xs font-black text-slate-900">92 High street, London</span>
|
||||
<ChevronDown className="w-3 h-3 text-slate-400" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setShowLangDropdown(!showLangDropdown)}
|
||||
className="flex items-center space-x-2 bg-white px-3 py-2 rounded-2xl shadow-sm border border-slate-50 active:scale-95 transition-all"
|
||||
>
|
||||
<Globe className="w-4 h-4 text-[#185FA5]" />
|
||||
<span className="text-[9px] font-black uppercase tracking-widest text-slate-900">{currentLang}</span>
|
||||
</button>
|
||||
|
||||
{showLangDropdown && (
|
||||
<div className="absolute top-12 right-0 w-40 bg-white rounded-[24px] shadow-2xl border border-slate-100 py-3 z-[100] animate-in fade-in zoom-in duration-200">
|
||||
{['English', 'Arabic', 'Tagalog', 'Swahili', 'Hindi'].map((lang) => (
|
||||
<button
|
||||
key={lang}
|
||||
onClick={() => {
|
||||
setCurrentLang(lang === 'English' ? 'EN' : lang.substring(0, 2).toUpperCase());
|
||||
setShowLangDropdown(false);
|
||||
}}
|
||||
className="w-full px-6 py-3 text-left hover:bg-slate-50 flex items-center justify-between group"
|
||||
>
|
||||
<span className="text-[10px] font-black uppercase tracking-widest text-slate-400 group-hover:text-[#185FA5] transition-colors">{lang}</span>
|
||||
{currentLang === (lang === 'English' ? 'EN' : lang.substring(0, 2).toUpperCase()) && (
|
||||
<div className="w-1.5 h-1.5 bg-[#185FA5] rounded-full" />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button className="w-10 h-10 rounded-full bg-white flex items-center justify-center text-slate-400 shadow-sm border border-slate-50 relative">
|
||||
<Bell className="w-4 h-4" />
|
||||
<div className="absolute top-2.5 right-2.5 w-2 h-2 bg-red-500 rounded-full border-2 border-white" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search Bar */}
|
||||
<div className="relative">
|
||||
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-300" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search for Job, Recruiter..."
|
||||
className="w-full pl-12 pr-12 py-3.5 bg-white border-none rounded-2xl text-[11px] font-medium shadow-sm focus:ring-2 focus:ring-blue-100 outline-none"
|
||||
/>
|
||||
<div className="absolute right-4 top-1/2 -translate-y-1/2">
|
||||
<span className="text-sm">⚡</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto px-4 space-y-8">
|
||||
{/* Open to Work Card */}
|
||||
<button
|
||||
onClick={() => setIsOpenToWork(!isOpenToWork)}
|
||||
className={`w-full p-6 rounded-[32px] flex items-center justify-between transition-all duration-500 active:scale-[0.98] ${
|
||||
isOpenToWork ? 'bg-[#185FA5] text-white shadow-xl shadow-blue-500/20' : 'bg-white text-slate-400 border border-slate-100'
|
||||
}`}
|
||||
>
|
||||
<div className="text-left space-y-1">
|
||||
<p className={`text-[9px] font-black uppercase tracking-widest ${isOpenToWork ? 'text-blue-100' : 'text-slate-300'}`}>Availability</p>
|
||||
<h3 className="text-base font-black tracking-tight uppercase">Open to Work</h3>
|
||||
</div>
|
||||
<div className={`w-12 h-7 rounded-full relative transition-colors duration-300 ${isOpenToWork ? 'bg-white/20' : 'bg-slate-100'}`}>
|
||||
<div className={`absolute top-1 w-5 h-5 rounded-full transition-all duration-300 shadow-md ${
|
||||
isOpenToWork ? 'right-1 bg-white' : 'left-1 bg-white'
|
||||
}`} />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Job Offers Horizontal */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between px-1">
|
||||
<h3 className="font-black text-sm uppercase tracking-tight text-slate-900">Job offers</h3>
|
||||
<button className="text-[10px] font-black text-[#185FA5] uppercase tracking-widest">See all</button>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-4 overflow-x-auto pb-4 no-scrollbar -mx-4 px-4">
|
||||
{[
|
||||
{ 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) => (
|
||||
<Link key={offer.id} href="/mobile/worker/job/detail" className={`${offer.bg} p-6 rounded-[32px] w-[240px] shrink-0 space-y-4 shadow-sm active:scale-95 transition-all block border border-white/50`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="w-10 h-10 rounded-full bg-white flex items-center justify-center text-xl shadow-sm">
|
||||
{offer.icon}
|
||||
</div>
|
||||
<p className="text-sm font-black text-slate-900">{offer.salary}</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<h4 className="font-black text-slate-900 text-xs uppercase tracking-tight">{offer.title}</h4>
|
||||
<p className="text-[9px] text-slate-500 font-bold uppercase tracking-tight">High street avenue London, UK</p>
|
||||
<p className="text-[9px] text-[#185FA5] font-black uppercase">From Fri, 17 Dec</p>
|
||||
</div>
|
||||
<span className="inline-block bg-black text-white px-4 py-1.5 rounded-full text-[8px] font-black uppercase tracking-tight">Part time</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Employer Requests - Compact List */}
|
||||
<div className="space-y-4">
|
||||
<div className="px-1 flex items-center justify-between">
|
||||
<h2 className="text-[12px] font-black uppercase tracking-[0.15em] text-slate-400">Employer Requests</h2>
|
||||
<span className="bg-[#185FA5]/10 text-[#185FA5] px-2.5 py-1 rounded-lg text-[9px] font-black uppercase">2 Pending</span>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{ 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) => (
|
||||
<div key={req.id} className="bg-white p-4 rounded-3xl border border-slate-50 shadow-sm flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-slate-50 flex items-center justify-center overflow-hidden border border-slate-100 shrink-0">
|
||||
<img src={req.img} alt="employer" className="w-full h-full object-cover" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-black text-slate-900 text-xs uppercase tracking-tight leading-none mb-1.5">{req.employer}</h4>
|
||||
<p className="text-[9px] font-bold text-[#185FA5] uppercase tracking-widest">{req.role}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<button className="w-8 h-8 bg-emerald-50 text-emerald-600 rounded-lg flex items-center justify-center active:scale-90 transition-all">
|
||||
<span className="text-[10px] font-black">✓</span>
|
||||
</button>
|
||||
<button className="w-8 h-8 bg-rose-50 text-rose-400 rounded-lg flex items-center justify-center active:scale-90 transition-all">
|
||||
<span className="text-[10px] font-black text-lg">×</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent Job Post - Compact & Professional */}
|
||||
<div className="space-y-6 pb-12">
|
||||
<div className="flex items-center justify-between px-1">
|
||||
<h2 className="text-[12px] font-black uppercase tracking-[0.15em] text-slate-400">Recent job post</h2>
|
||||
<button className="text-[10px] font-black uppercase tracking-widest text-[#185FA5]">See All</button>
|
||||
</div>
|
||||
|
||||
{/* Category Tabs */}
|
||||
<div className="flex overflow-x-auto space-x-2 no-scrollbar pb-2 -mx-4 px-4">
|
||||
{['Cleaner', 'Driver', 'Mason', 'Electrician'].map((cat) => (
|
||||
<button key={cat} className="bg-white px-5 py-3 rounded-2xl flex items-center space-x-2 shrink-0 border border-slate-100 active:scale-95 transition-all">
|
||||
<span className="text-[10px] font-black uppercase tracking-widest text-slate-400">{cat}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Job Cards - Refined & Compact */}
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{ 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) => (
|
||||
<Link
|
||||
key={i}
|
||||
href="/mobile/worker/job/detail"
|
||||
className="bg-white p-5 rounded-[28px] border border-slate-50 shadow-sm flex items-center justify-between active:scale-[0.98] transition-all group"
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-11 h-11 bg-slate-50 rounded-2xl flex items-center justify-center text-xl group-hover:bg-blue-50 transition-colors">
|
||||
{job.icon}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<h4 className="font-black text-slate-900 text-sm uppercase tracking-tight leading-none">{job.title}</h4>
|
||||
<div className="flex items-center space-x-2">
|
||||
<p className="text-[9px] font-bold text-slate-300 uppercase tracking-widest">{job.loc}</p>
|
||||
<span className="w-0.5 h-0.5 bg-slate-200 rounded-full" />
|
||||
<p className="text-[9px] font-bold text-slate-300 uppercase tracking-widest">{job.time}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm font-black text-[#185FA5] leading-none mb-1">{job.salary}</p>
|
||||
<p className="text-[8px] font-black text-emerald-500 uppercase tracking-[0.2em] leading-none">Verified</p>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom Navigation */}
|
||||
<div className="fixed bottom-0 left-0 right-0 max-w-md mx-auto bg-white/90 backdrop-blur-xl border-t border-slate-100 px-6 py-4 flex items-center justify-between z-50">
|
||||
<Link href="/mobile/worker/home" className="flex flex-col items-center space-y-1 text-[#185FA5]">
|
||||
<Home className="w-5 h-5 fill-current" />
|
||||
<span className="text-[8px] font-black uppercase tracking-tight">Home</span>
|
||||
</Link>
|
||||
<Link href="/mobile/worker/explore" className="flex flex-col items-center space-y-1 text-slate-300">
|
||||
<Compass className="w-5 h-5" />
|
||||
<span className="text-[8px] font-black uppercase tracking-tight">Explore</span>
|
||||
</Link>
|
||||
<Link href="/mobile/worker/announcements" className="flex flex-col items-center space-y-1 text-slate-300">
|
||||
<Bell className="w-5 h-5" />
|
||||
<span className="text-[8px] font-black uppercase tracking-tight">Updates</span>
|
||||
</Link>
|
||||
<Link href="/mobile/worker/chat" className="flex flex-col items-center space-y-1 text-slate-300">
|
||||
<MessageCircle className="w-5 h-5" />
|
||||
<span className="text-[8px] font-black uppercase tracking-tight">Chat</span>
|
||||
</Link>
|
||||
<Link href="/mobile/worker/profile" className="flex flex-col items-center space-y-1 text-slate-300">
|
||||
<User className="w-5 h-5" />
|
||||
<span className="text-[8px] font-black uppercase tracking-tight">Profile</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -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 (
|
||||
<div className="min-h-screen bg-white font-sans text-slate-900 flex flex-col max-w-md mx-auto shadow-2xl relative overflow-hidden">
|
||||
<Head title="Job Details" />
|
||||
|
||||
{/* Header */}
|
||||
<div className="px-6 pt-12 pb-4 bg-white flex items-center justify-between border-b border-slate-50 sticky top-0 z-50">
|
||||
<button onClick={() => window.history.back()} className="w-10 h-10 flex items-center justify-center text-slate-900">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</button>
|
||||
<h1 className="text-sm font-black uppercase tracking-widest">Details</h1>
|
||||
<button className="w-10 h-10 flex items-center justify-center text-slate-900">
|
||||
<MoreVertical className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto pb-32">
|
||||
{/* Job Icon & Title */}
|
||||
<div className="flex flex-col items-center py-10 space-y-6">
|
||||
<div className="w-20 h-20 rounded-full bg-slate-50 border border-slate-100 flex items-center justify-center shadow-sm">
|
||||
<div className="w-14 h-14 rounded-full bg-white shadow-md flex items-center justify-center">
|
||||
<Briefcase className="w-8 h-8 text-slate-900" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center space-y-2">
|
||||
<h2 className="text-2xl font-black tracking-tight">Driver needed</h2>
|
||||
<p className="text-[10px] text-slate-400 font-bold uppercase tracking-widest flex items-center justify-center space-x-1">
|
||||
<MapPin className="w-3 h-3" />
|
||||
<span>High street avenue London, UK</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Stats Grid */}
|
||||
<div className="px-6 grid grid-cols-2 gap-4 mb-10">
|
||||
<div className="bg-[#F9F9F9] p-5 rounded-3xl border border-slate-50 flex items-center space-x-4">
|
||||
<div className="w-10 h-10 rounded-2xl bg-blue-50 text-[#185FA5] flex items-center justify-center">
|
||||
<DollarSign className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[9px] text-slate-400 font-black uppercase tracking-widest leading-none mb-1">Salary</p>
|
||||
<p className="text-xs font-black text-slate-900">$15</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-[#F9F9F9] p-5 rounded-3xl border border-slate-50 flex items-center space-x-4">
|
||||
<div className="w-10 h-10 rounded-2xl bg-orange-50 text-orange-500 flex items-center justify-center">
|
||||
<Clock className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[9px] text-slate-400 font-black uppercase tracking-widest leading-none mb-1">Job type</p>
|
||||
<p className="text-xs font-black text-slate-900">Part time</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-[#F9F9F9] p-5 rounded-3xl border border-slate-50 flex items-center space-x-4">
|
||||
<div className="w-10 h-10 rounded-2xl bg-emerald-50 text-emerald-500 flex items-center justify-center">
|
||||
<Star className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[9px] text-slate-400 font-black uppercase tracking-widest leading-none mb-1">Skill</p>
|
||||
<p className="text-xs font-black text-slate-900">Expert</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-[#F9F9F9] p-5 rounded-3xl border border-slate-50 flex items-center space-x-4">
|
||||
<div className="w-10 h-10 rounded-2xl bg-purple-50 text-purple-500 flex items-center justify-center">
|
||||
<ShieldCheck className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[9px] text-slate-400 font-black uppercase tracking-widest leading-none mb-1">Payment</p>
|
||||
<p className="text-xs font-black text-slate-900">Verified</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="px-6 space-y-4 mb-10">
|
||||
<h3 className="font-black text-sm uppercase tracking-tight">Description</h3>
|
||||
<p className="text-xs text-slate-500 leading-relaxed font-medium">
|
||||
An outstanding executer that consistently delivers more than expected & start mentoring experience man to drive my vehicle safely and efficiently. <span className="text-[#185FA5] font-black uppercase tracking-widest cursor-pointer">Read More</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Additional Details */}
|
||||
<div className="px-6 space-y-6">
|
||||
<h3 className="font-black text-sm uppercase tracking-tight">Additional details</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between py-2 border-b border-slate-50">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Briefcase className="w-4 h-4 text-slate-300" />
|
||||
<span className="text-[11px] font-bold text-slate-400 uppercase tracking-widest">Offer type</span>
|
||||
</div>
|
||||
<span className="text-[11px] font-black text-slate-900 uppercase">Contractual</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between py-2 border-b border-slate-50">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Calendar className="w-4 h-4 text-slate-300" />
|
||||
<span className="text-[11px] font-bold text-slate-400 uppercase tracking-widest">Start from</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span className="text-[11px] font-black text-slate-900 uppercase block">26 Aug, 2023</span>
|
||||
<span className="text-[9px] text-slate-400 font-bold uppercase">9:00 am to 6:00 pm</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between py-2 border-b border-slate-50">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Phone className="w-4 h-4 text-slate-300" />
|
||||
<span className="text-[11px] font-bold text-slate-400 uppercase tracking-widest">Contact</span>
|
||||
</div>
|
||||
<span className="text-[11px] font-black text-slate-900">(702) 555-0122</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between py-2 border-b border-slate-50">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Clock className="w-4 h-4 text-slate-300" />
|
||||
<span className="text-[11px] font-bold text-slate-400 uppercase tracking-widest">Posted</span>
|
||||
</div>
|
||||
<span className="text-[11px] font-black text-slate-900 uppercase">2 days ago</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer Apply Button */}
|
||||
<div className="fixed bottom-0 left-0 right-0 max-w-md mx-auto bg-white/90 backdrop-blur-xl border-t border-slate-100 p-6 z-50">
|
||||
<button className="w-full bg-[#185FA5] text-white py-5 rounded-[28px] font-black text-[12px] uppercase tracking-[0.2em] shadow-2xl shadow-blue-500/20 active:scale-95 transition-all">
|
||||
Apply Now
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -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 (
|
||||
<div className="min-h-screen bg-white font-sans text-slate-900 flex flex-col max-w-md mx-auto">
|
||||
<Head title="Worker Login" />
|
||||
|
||||
{/* Minimal Header */}
|
||||
<div className="px-6 pt-12 pb-4 flex items-center">
|
||||
<button onClick={() => window.history.back()} className="p-3 bg-slate-50 rounded-2xl text-slate-400 border border-slate-100 active:scale-95 transition-all">
|
||||
<ChevronLeft className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 px-8 pt-8 space-y-12">
|
||||
{/* Title Section */}
|
||||
<div className="space-y-3">
|
||||
<h1 className="text-4xl font-black tracking-tight text-slate-900 leading-none">Welcome back</h1>
|
||||
<p className="text-sm font-medium text-slate-400">Please enter your details to sign in</p>
|
||||
</div>
|
||||
|
||||
{/* Login Form */}
|
||||
<form onSubmit={handleSubmit} className="space-y-8">
|
||||
<div className="space-y-5">
|
||||
<div className="space-y-2">
|
||||
<label className="text-[11px] font-bold text-slate-400 uppercase tracking-widest ml-2">Email Address</label>
|
||||
<div className="relative">
|
||||
<Mail className="absolute left-6 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-300" />
|
||||
<input
|
||||
type="email"
|
||||
value={data.email}
|
||||
onChange={(e) => 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
|
||||
/>
|
||||
</div>
|
||||
{errors.email && <p className="text-xs text-red-500 mt-1 ml-2">{errors.email}</p>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between ml-2">
|
||||
<label className="text-[11px] font-bold text-slate-400 uppercase tracking-widest">Password</label>
|
||||
<Link href="#" className="text-[11px] font-bold text-[#185FA5] uppercase tracking-widest">Forgot?</label>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Lock className="absolute left-6 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-300" />
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={data.password}
|
||||
onChange={(e) => 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
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-6 top-1/2 -translate-y-1/2 text-slate-300 hover:text-slate-500"
|
||||
>
|
||||
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={processing}
|
||||
className="w-full py-5 bg-[#185FA5] text-white rounded-[24px] font-bold text-sm uppercase tracking-widest shadow-xl shadow-blue-500/20 flex items-center justify-center space-x-3 active:scale-[0.98] transition-all disabled:opacity-70 mt-8"
|
||||
>
|
||||
{processing ? <Loader2 className="w-6 h-6 animate-spin" /> : <span>Sign in</span>}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="text-center pt-4 pb-10">
|
||||
<p className="text-xs font-bold text-slate-400 uppercase tracking-widest leading-loose">
|
||||
Don't have an account? <br />
|
||||
<Link href="/mobile/worker/register" className="text-[#185FA5] underline underline-offset-4 decoration-4">Sign up here</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -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 (
|
||||
<div className="min-h-screen bg-white font-sans text-slate-900 flex flex-col max-w-md mx-auto shadow-2xl relative overflow-hidden pb-24">
|
||||
<Head title="Profile" />
|
||||
|
||||
{/* Header */}
|
||||
<div className="px-6 pt-12 pb-4 bg-white flex items-center justify-between border-b border-slate-50 sticky top-0 z-50">
|
||||
<button onClick={() => window.history.back()} className="w-10 h-10 flex items-center justify-center text-slate-900">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</button>
|
||||
<h1 className="text-sm font-black uppercase tracking-widest">Profile</h1>
|
||||
<Link href="/mobile/worker/profile/edit" className="w-10 h-10 flex items-center justify-center text-slate-900">
|
||||
<Pencil className="w-5 h-5" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{/* Profile Header */}
|
||||
<div className="flex flex-col items-center py-8 space-y-4">
|
||||
<div className="w-28 h-28 rounded-full bg-slate-100 overflow-hidden border-4 border-slate-50 shadow-sm">
|
||||
<img src="https://api.dicebear.com/7.x/avataaars/svg?seed=Danial" alt="avatar" className="w-full h-full object-cover" />
|
||||
</div>
|
||||
<div className="text-center space-y-1 px-10">
|
||||
<h2 className="text-2xl font-black tracking-tight">Danial Donald</h2>
|
||||
<p className="text-xs text-slate-400 font-bold leading-relaxed">
|
||||
4901 Thornridge Cir. Shiloh, Hawaii 81063
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Bar */}
|
||||
<div className="px-6 grid grid-cols-3 gap-3 mb-10">
|
||||
<div className="bg-[#F9F9F9] p-5 rounded-3xl flex flex-col items-center justify-center space-y-1">
|
||||
<p className="text-lg font-black">12</p>
|
||||
<p className="text-[10px] text-slate-400 font-black uppercase tracking-widest">Complete</p>
|
||||
</div>
|
||||
<div className="bg-[#F9F9F9] p-5 rounded-3xl flex flex-col items-center justify-center space-y-1">
|
||||
<div className="flex items-center space-x-1">
|
||||
<p className="text-lg font-black">4.8</p>
|
||||
<Star className="w-4 h-4 text-amber-400 fill-current" />
|
||||
</div>
|
||||
<p className="text-[10px] text-slate-400 font-black uppercase tracking-widest">Ratings</p>
|
||||
</div>
|
||||
<div className="bg-[#F9F9F9] p-5 rounded-3xl flex flex-col items-center justify-center space-y-1">
|
||||
<p className="text-lg font-black">$ 50</p>
|
||||
<p className="text-[10px] text-slate-400 font-black uppercase tracking-widest">Salary</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content Sections */}
|
||||
<div className="px-6 space-y-10 pb-10">
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-black text-base uppercase tracking-tight">About Danial</h3>
|
||||
<p className="text-sm text-slate-500 leading-relaxed font-medium">
|
||||
I am Danial Donald, a passionate professional with over 10 years of experience. Specialized in high-quality service and customer satisfaction.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-6">
|
||||
<div className="space-y-2">
|
||||
<p className="text-[10px] text-slate-300 font-black uppercase tracking-widest leading-none">Gender:</p>
|
||||
<p className="text-sm font-black text-slate-900 uppercase">Male</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="text-[10px] text-slate-300 font-black uppercase tracking-widest leading-none">Age:</p>
|
||||
<p className="text-sm font-black text-slate-900 uppercase">32 years</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="text-[10px] text-slate-300 font-black uppercase tracking-widest leading-none">Experience:</p>
|
||||
<p className="text-sm font-black text-slate-900 uppercase">Expert (10y)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<p className="text-[10px] text-slate-300 font-black uppercase tracking-widest leading-none">Expertise</p>
|
||||
<p className="text-sm font-black text-slate-900 uppercase tracking-tight">Plumbing, Cleaning, Electrician</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<p className="text-[10px] text-slate-300 font-black uppercase tracking-widest leading-none">Languages</p>
|
||||
<div className="flex flex-wrap gap-2.5 pt-1">
|
||||
{['English', 'Arabic', 'Tagalog', 'Swahili'].map((lang) => (
|
||||
<span key={lang} className="px-4 py-2 bg-slate-50 text-slate-500 rounded-full text-[10px] font-black uppercase tracking-widest border border-slate-100 shadow-sm">
|
||||
{lang}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-black text-base uppercase tracking-tight">My Documents</h3>
|
||||
<div className="bg-[#F9F9F9] p-8 rounded-[40px] border border-slate-100 shadow-sm space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-12 h-12 bg-blue-50 text-[#185FA5] rounded-2xl flex items-center justify-center">
|
||||
<FileText className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-black text-slate-900 uppercase">Passport.JPG</p>
|
||||
<p className="text-[10px] text-emerald-500 font-black uppercase tracking-widest">Verified</p>
|
||||
</div>
|
||||
</div>
|
||||
<ShieldCheck className="w-6 h-6 text-emerald-500" />
|
||||
</div>
|
||||
|
||||
<div className="pt-6 border-t border-white grid grid-cols-2 gap-6">
|
||||
<div>
|
||||
<p className="text-[9px] text-slate-300 font-black uppercase tracking-widest leading-none mb-1.5">Passport No</p>
|
||||
<p className="text-xs font-black text-slate-700">Z12345678</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[9px] text-slate-300 font-black uppercase tracking-widest leading-none mb-1.5">Expiry Date</p>
|
||||
<p className="text-xs font-black text-slate-700">12/12/2028</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom Navigation */}
|
||||
<div className="fixed bottom-0 left-0 right-0 max-w-md mx-auto bg-white/90 backdrop-blur-xl border-t border-slate-100 px-6 py-4 flex items-center justify-between z-50">
|
||||
<Link href="/mobile/worker/home" className="flex flex-col items-center space-y-1 text-slate-300">
|
||||
<Home className="w-5 h-5" />
|
||||
<span className="text-[8px] font-black uppercase tracking-tight">Home</span>
|
||||
</Link>
|
||||
<Link href="/mobile/worker/explore" className="flex flex-col items-center space-y-1 text-slate-300">
|
||||
<Compass className="w-5 h-5" />
|
||||
<span className="text-[8px] font-black uppercase tracking-tight">Explore</span>
|
||||
</Link>
|
||||
<Link href="/mobile/worker/announcements" className="flex flex-col items-center space-y-1 text-slate-300">
|
||||
<Bell className="w-5 h-5" />
|
||||
<span className="text-[8px] font-black uppercase tracking-tight">Updates</span>
|
||||
</Link>
|
||||
<Link href="/mobile/worker/chat" className="flex flex-col items-center space-y-1 text-slate-300">
|
||||
<MessageCircle className="w-5 h-5" />
|
||||
<span className="text-[8px] font-black uppercase tracking-tight">Chat</span>
|
||||
</Link>
|
||||
<Link href="/mobile/worker/profile" className="flex flex-col items-center space-y-1 text-[#185FA5]">
|
||||
<User className="w-5 h-5 fill-current" />
|
||||
<span className="text-[8px] font-black uppercase tracking-tight">Profile</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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 (
|
||||
<div className="min-h-screen bg-white font-sans text-slate-900 flex flex-col max-w-md mx-auto shadow-2xl relative overflow-hidden">
|
||||
<Head title="Join as Worker" />
|
||||
|
||||
{/* Top Bar */}
|
||||
<div className="px-6 pt-12 pb-6 flex items-center justify-between sticky top-0 bg-white z-50">
|
||||
<button onClick={() => step > 1 ? setStep(step - 1) : window.history.back()} className="w-12 h-12 rounded-2xl bg-slate-50 flex items-center justify-center text-slate-900 border border-slate-100 active:scale-95 transition-all">
|
||||
<ArrowLeft className="w-6 h-6" />
|
||||
</button>
|
||||
<div className="flex items-center space-x-1.5">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className={`h-1.5 rounded-full transition-all duration-500 ${step === i ? 'w-8 bg-[#185FA5]' : 'w-2 bg-slate-100'}`} />
|
||||
))}
|
||||
</div>
|
||||
<div className="w-12" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 px-8 py-4 overflow-y-auto pb-32">
|
||||
{step === 1 && (
|
||||
<div className="space-y-8 animate-in fade-in slide-in-from-right-6 duration-500">
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-3xl font-black tracking-tight leading-tight">Your basic info.</h1>
|
||||
<p className="text-slate-400 font-bold text-xs uppercase tracking-[0.2em]">Step 1 of 3</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-3">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Full Name</label>
|
||||
<div className="relative">
|
||||
<User className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-300" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Enter your name"
|
||||
className="w-full pl-12 pr-6 py-5 bg-slate-50 border-none rounded-3xl text-sm font-black uppercase tracking-tight focus:ring-4 focus:ring-blue-100 transition-all outline-none"
|
||||
value={formData.name}
|
||||
onChange={e => setFormData({...formData, name: e.target.value})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Phone Number</label>
|
||||
<div className="relative">
|
||||
<Phone className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-300" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="+971 XX XXX XXXX"
|
||||
className="w-full pl-12 pr-6 py-5 bg-slate-50 border-none rounded-3xl text-sm font-black uppercase tracking-tight focus:ring-4 focus:ring-blue-100 transition-all outline-none"
|
||||
value={formData.phone}
|
||||
onChange={e => setFormData({...formData, phone: e.target.value})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Where are you from?</label>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{nationalities.map((nat) => (
|
||||
<button
|
||||
key={nat.name}
|
||||
onClick={() => setFormData({...formData, nationality: nat.name})}
|
||||
className={`p-4 rounded-3xl border-2 flex flex-col items-center justify-center space-y-2 transition-all active:scale-95 ${
|
||||
formData.nationality === nat.name ? 'border-[#185FA5] bg-blue-50' : 'border-slate-50 bg-slate-50 text-slate-400'
|
||||
}`}
|
||||
>
|
||||
<span className="text-2xl">{nat.flag}</span>
|
||||
<span className="text-[9px] font-black uppercase tracking-tight">{nat.name}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 2 && (
|
||||
<div className="space-y-8 animate-in fade-in slide-in-from-right-6 duration-500">
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-3xl font-black tracking-tight leading-tight">What do you do?</h1>
|
||||
<p className="text-slate-400 font-bold text-xs uppercase tracking-[0.2em]">Step 2 of 3</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{roles.map(role => (
|
||||
<button
|
||||
key={role.name}
|
||||
onClick={() => setFormData({...formData, role: role.name})}
|
||||
className={`p-6 rounded-[32px] border-2 text-center transition-all flex flex-col items-center space-y-4 active:scale-95 ${
|
||||
formData.role === role.name ? 'border-[#185FA5] bg-blue-50 shadow-xl shadow-blue-500/5' : 'border-slate-50 bg-slate-50 text-slate-400'
|
||||
}`}
|
||||
>
|
||||
<div className={`w-14 h-14 rounded-2xl flex items-center justify-center text-3xl ${formData.role === role.name ? 'bg-white shadow-md' : 'bg-white/50'}`}>
|
||||
{role.icon}
|
||||
</div>
|
||||
<span className={`font-black text-[11px] uppercase tracking-[0.1em] ${formData.role === role.name ? 'text-slate-900' : ''}`}>{role.name}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 3 && (
|
||||
<div className="space-y-8 animate-in fade-in slide-in-from-right-6 duration-500">
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-3xl font-black tracking-tight leading-tight">Verify Identity.</h1>
|
||||
<p className="text-slate-400 font-bold text-xs uppercase tracking-[0.2em]">Final Step</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="p-10 border-4 border-dashed border-slate-100 rounded-[40px] bg-slate-50 flex flex-col items-center justify-center text-center space-y-6 active:scale-95 transition-all cursor-pointer hover:border-blue-200">
|
||||
<div className="w-24 h-24 bg-white rounded-[32px] flex items-center justify-center shadow-xl">
|
||||
<Camera className="w-12 h-12 text-[#185FA5]" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-black text-sm uppercase tracking-tight">Upload Passport</h3>
|
||||
<p className="text-[10px] font-black text-slate-300 uppercase tracking-widest">TAP TO OPEN CAMERA</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 bg-emerald-50 rounded-3xl border border-emerald-100 flex items-start space-x-4">
|
||||
<ShieldCheck className="w-6 h-6 text-emerald-600 shrink-0" />
|
||||
<p className="text-[9px] font-black text-emerald-800 uppercase tracking-widest leading-relaxed">
|
||||
Your data is 100% secure. We only show documents to verified employers.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Bottom Button */}
|
||||
<div className="fixed bottom-0 left-0 right-0 max-w-md mx-auto p-8 bg-white/90 backdrop-blur-xl border-t border-slate-50 z-50">
|
||||
{step < 3 ? (
|
||||
<button
|
||||
onClick={nextStep}
|
||||
className="w-full bg-[#185FA5] text-white py-5 rounded-[28px] font-black text-[11px] uppercase tracking-[0.2em] shadow-2xl shadow-blue-500/20 active:scale-95 transition-all"
|
||||
>
|
||||
Next Step
|
||||
</button>
|
||||
) : (
|
||||
<Link
|
||||
href="/mobile/worker/home"
|
||||
className="w-full bg-[#185FA5] text-white py-5 rounded-[28px] font-black text-[11px] uppercase tracking-[0.2em] shadow-2xl shadow-blue-500/20 active:scale-95 transition-all flex items-center justify-center"
|
||||
>
|
||||
Join Marketplace
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -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(<App {...props} />);
|
||||
root.render(
|
||||
<>
|
||||
<App {...props} />
|
||||
<Toaster richColors position="top-right" closeButton />
|
||||
</>
|
||||
);
|
||||
},
|
||||
progress: {
|
||||
color: '#4B5563',
|
||||
color: '#185FA5',
|
||||
showSpinner: true,
|
||||
},
|
||||
});
|
||||
|
||||
128
resources/views/emails/employer-otp.blade.php
Normal file
128
resources/views/emails/employer-otp.blade.php
Normal file
@ -0,0 +1,128 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Verify Your Email</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Geist', 'Segoe UI', Helvetica, Arial, sans-serif;
|
||||
background-color: #f8fafc;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 40px auto;
|
||||
background: #ffffff;
|
||||
border-radius: 24px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.03);
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
.header {
|
||||
background-color: #185FA5;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
}
|
||||
.logo {
|
||||
font-size: 28px;
|
||||
font-weight: 900;
|
||||
letter-spacing: -0.05em;
|
||||
margin: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.content {
|
||||
padding: 48px;
|
||||
color: #334155;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.greeting {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
color: #0f172a;
|
||||
}
|
||||
.text {
|
||||
font-size: 14px;
|
||||
color: #475569;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.otp-container {
|
||||
background-color: #f1f5f9;
|
||||
border: 2px dashed #cbd5e1;
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
margin: 32px 0;
|
||||
}
|
||||
.otp-code {
|
||||
font-size: 36px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.15em;
|
||||
color: #185FA5;
|
||||
margin: 0;
|
||||
}
|
||||
.expiry-warning {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #e11d48;
|
||||
margin-top: 8px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
.security-notice {
|
||||
background-color: #f8fafc;
|
||||
border-left: 4px solid #185FA5;
|
||||
padding: 16px;
|
||||
border-radius: 0 12px 12px 0;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.footer {
|
||||
background-color: #f8fafc;
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1 class="logo">Marketplace</h1>
|
||||
<div style="font-size: 12px; font-weight: 700; letter-spacing: 0.2em; opacity: 0.8; margin-top: 8px; text-transform: uppercase;">Employer Verification</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<h2 class="greeting">Hello {{ $employerName }},</h2>
|
||||
<p class="text">
|
||||
Thank you for starting the registration process for <strong>{{ $companyName }}</strong> 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.
|
||||
</p>
|
||||
|
||||
<div class="otp-container">
|
||||
<div class="otp-code">{{ $otp }}</div>
|
||||
<div class="expiry-warning">Valid for 10 minutes only</div>
|
||||
</div>
|
||||
|
||||
<div class="security-notice">
|
||||
<strong>Security Alert:</strong> This code is confidential. Our support team will never ask you for this code. Do not share this passcode with anyone.
|
||||
</div>
|
||||
|
||||
<p class="text" style="margin-bottom: 0;">
|
||||
If you did not initiate this registration, you can safely ignore this email.
|
||||
</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
Empowering Direct Household Hiring • Secure Portal
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
340
routes/web.php
340
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');
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user