Compare commits
No commits in common. "c851f2e4daff1c800b5eb5202ff8eeaf77359429" and "9af2d5bc4e5bb87f19c620fbaf2693ed7ca04366" have entirely different histories.
c851f2e4da
...
9af2d5bc4e
@ -22,14 +22,9 @@ public function getAnnouncements(Request $request)
|
||||
$employer = $request->attributes->get('employer');
|
||||
|
||||
try {
|
||||
$page = (int)$request->input('page', 1);
|
||||
$perPage = (int)$request->input('per_page', 15);
|
||||
|
||||
$query = Announcement::where('employer_id', $employer->id)->latest();
|
||||
$total = $query->count();
|
||||
$offset = ($page - 1) * $perPage;
|
||||
|
||||
$announcements = $query->skip($offset)->take($perPage)->get()
|
||||
$announcements = Announcement::where('employer_id', $employer->id)
|
||||
->latest()
|
||||
->get()
|
||||
->map(function ($announcement) {
|
||||
return [
|
||||
'id' => $announcement->id,
|
||||
@ -44,13 +39,7 @@ public function getAnnouncements(Request $request)
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'announcements' => $announcements,
|
||||
'pagination' => [
|
||||
'total' => $total,
|
||||
'per_page' => $perPage,
|
||||
'current_page' => $page,
|
||||
'last_page' => max(1, (int)ceil($total / $perPage)),
|
||||
]
|
||||
'announcements' => $announcements
|
||||
]
|
||||
], 200);
|
||||
|
||||
|
||||
@ -435,142 +435,4 @@ public function plans()
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
public function forgotPassword(Request $request)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'email' => 'required|email',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Validation error.',
|
||||
'errors' => $validator->errors()
|
||||
], 422);
|
||||
}
|
||||
|
||||
try {
|
||||
$user = User::where('email', $request->email)->where('role', 'employer')->first();
|
||||
if (!$user) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'No employer account found with this email address.'
|
||||
], 404);
|
||||
}
|
||||
|
||||
$otp = '111111'; // default development OTP
|
||||
if (app()->environment('production')) {
|
||||
$otp = strval(rand(100000, 999999));
|
||||
}
|
||||
|
||||
\Illuminate\Support\Facades\Cache::put('employer_reset_otp_' . $request->email, [
|
||||
'code' => $otp,
|
||||
'expires_at' => now()->addMinutes(10)->timestamp
|
||||
], 600);
|
||||
|
||||
// Send reset OTP email
|
||||
try {
|
||||
\Illuminate\Support\Facades\Mail::to($request->email)->send(new \App\Mail\EmployerResetOtpMail(
|
||||
$otp,
|
||||
$user->name
|
||||
));
|
||||
} catch (\Exception $mailEx) {
|
||||
logger()->error('API Employer Forgot Password Mail Failure: ' . $mailEx->getMessage());
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Password reset verification code sent to email successfully.',
|
||||
'email' => $request->email
|
||||
], 200);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
logger()->error('API Employer Forgot Password Failure: ' . $e->getMessage());
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'An error occurred. Please try again.',
|
||||
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function resetPassword(Request $request)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'email' => 'required|email',
|
||||
'otp' => 'required|string|size:6',
|
||||
'password' => 'required|string|min:8|confirmed',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Validation error.',
|
||||
'errors' => $validator->errors()
|
||||
], 422);
|
||||
}
|
||||
|
||||
$cachedData = \Illuminate\Support\Facades\Cache::get('employer_reset_otp_' . $request->email);
|
||||
|
||||
if (!$cachedData || !isset($cachedData['code']) || !isset($cachedData['expires_at'])) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Invalid or expired verification code.'
|
||||
], 400);
|
||||
}
|
||||
|
||||
if ($cachedData['code'] !== $request->otp || now()->timestamp > $cachedData['expires_at']) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Invalid or expired verification code.'
|
||||
], 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$user = User::where('email', $request->email)->where('role', 'employer')->first();
|
||||
$sponsor = \App\Models\Sponsor::where('email', $request->email)->first();
|
||||
|
||||
if (!$user || !$sponsor) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'User account not found.'
|
||||
], 404);
|
||||
}
|
||||
|
||||
$apiToken = Str::random(80);
|
||||
|
||||
\Illuminate\Support\Facades\DB::transaction(function () use ($request, $user, $sponsor, $apiToken) {
|
||||
$hashedPassword = Hash::make($request->password);
|
||||
|
||||
$user->update([
|
||||
'password' => $hashedPassword,
|
||||
'api_token' => $apiToken,
|
||||
]);
|
||||
|
||||
$sponsor->update([
|
||||
'password' => $hashedPassword,
|
||||
]);
|
||||
});
|
||||
|
||||
\Illuminate\Support\Facades\Cache::forget('employer_reset_otp_' . $request->email);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Password reset successfully.',
|
||||
'data' => [
|
||||
'employer' => $user->load('employerProfile'),
|
||||
'token' => $apiToken
|
||||
]
|
||||
], 200);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
logger()->error('API Employer Reset Password Failure: ' . $e->getMessage());
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to reset password. Please try again.',
|
||||
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -101,7 +101,6 @@ public function getConversations(Request $request)
|
||||
'worker_name' => $conv->worker->name ?? 'Candidate',
|
||||
'category' => $conv->worker->category->name ?? 'General Helper',
|
||||
'nationality' => $conv->worker->nationality ?? 'Unknown',
|
||||
'worker_status' => strtolower($conv->worker->status ?? 'active'),
|
||||
'salary' => ($conv->worker->salary ?? $conv->worker->expected_salary ?? 2000) . ' AED',
|
||||
'last_message' => $lastMsg->text ?? 'No messages yet.',
|
||||
'unread_count' => $unreadCount,
|
||||
@ -177,7 +176,6 @@ public function getMessages(Request $request, $id)
|
||||
'worker_name' => $conv->worker->name ?? 'Candidate',
|
||||
'category' => $conv->worker->category->name ?? 'General Helper',
|
||||
'nationality' => $conv->worker->nationality ?? 'Unknown',
|
||||
'worker_status' => strtolower($conv->worker->status ?? 'active'),
|
||||
'salary' => ($conv->worker->salary ?? $conv->worker->expected_salary ?? 2000) . ' AED',
|
||||
];
|
||||
|
||||
|
||||
@ -51,14 +51,9 @@ public function getPayments(Request $request)
|
||||
]);
|
||||
}
|
||||
|
||||
$page = (int)$request->input('page', 1);
|
||||
$perPage = (int)$request->input('per_page', 15);
|
||||
|
||||
$query = Payment::where('user_id', $employerId)->latest();
|
||||
$total = $query->count();
|
||||
$offset = ($page - 1) * $perPage;
|
||||
|
||||
$payments = $query->skip($offset)->take($perPage)->get()
|
||||
$payments = Payment::where('user_id', $employerId)
|
||||
->latest()
|
||||
->get()
|
||||
->map(function ($payment) {
|
||||
return [
|
||||
'id' => $payment->id,
|
||||
@ -74,13 +69,7 @@ public function getPayments(Request $request)
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'payments' => $payments,
|
||||
'pagination' => [
|
||||
'total' => $total,
|
||||
'per_page' => $perPage,
|
||||
'current_page' => $page,
|
||||
'last_page' => max(1, (int)ceil($total / $perPage)),
|
||||
]
|
||||
'payments' => $payments
|
||||
]
|
||||
], 200);
|
||||
|
||||
|
||||
@ -73,21 +73,12 @@ public function updateProfile(Request $request)
|
||||
/** @var User $employer */
|
||||
$employer = $request->attributes->get('employer');
|
||||
|
||||
$sponsor = \App\Models\Sponsor::where('email', $employer->email)->first();
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
'email',
|
||||
'max:255',
|
||||
'unique:users,email,' . $employer->id,
|
||||
$sponsor ? 'unique:sponsors,email,' . $sponsor->id : 'unique:sponsors,email',
|
||||
],
|
||||
'email' => 'required|string|email|max:255|unique:users,email,' . $employer->id,
|
||||
'phone' => 'required|string|max:255',
|
||||
'company_name' => 'required|string|max:255',
|
||||
'language' => 'required|string|in:English,Arabic,english,arabic',
|
||||
'language' => 'required|string|in:English,Arabic',
|
||||
'notifications' => 'required|boolean',
|
||||
'current_password' => 'nullable|required_with:new_password|string',
|
||||
'new_password' => 'nullable|string|min:8|confirmed',
|
||||
@ -115,25 +106,12 @@ public function updateProfile(Request $request)
|
||||
}
|
||||
}
|
||||
|
||||
// Find old email before update to locate the correct sponsor record
|
||||
$oldEmail = $employer->getOriginal('email') ?? $employer->email;
|
||||
|
||||
// Update user table
|
||||
$employer->update([
|
||||
'name' => $request->name,
|
||||
'email' => $request->email,
|
||||
]);
|
||||
|
||||
// Sync with corresponding sponsor record if found
|
||||
$matchingSponsor = \App\Models\Sponsor::where('email', $oldEmail)->first();
|
||||
if ($matchingSponsor) {
|
||||
$matchingSponsor->update([
|
||||
'full_name' => $request->name,
|
||||
'email' => $request->email,
|
||||
'mobile' => $request->phone,
|
||||
]);
|
||||
}
|
||||
|
||||
// Update employer_profiles table
|
||||
$profile = $employer->employerProfile;
|
||||
if (!$profile) {
|
||||
@ -141,7 +119,7 @@ public function updateProfile(Request $request)
|
||||
}
|
||||
$profile->company_name = $request->company_name;
|
||||
$profile->phone = $request->phone;
|
||||
$profile->language = ucfirst(strtolower($request->language));
|
||||
$profile->language = $request->language;
|
||||
$profile->notifications = $request->notifications;
|
||||
$profile->save();
|
||||
|
||||
@ -180,106 +158,4 @@ public function updateProfile(Request $request)
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get dashboard analytics and summary for employer.
|
||||
*/
|
||||
public function getDashboard(Request $request)
|
||||
{
|
||||
/** @var User $employer */
|
||||
$employer = $request->attributes->get('employer');
|
||||
|
||||
try {
|
||||
$contactedWorkersCount = \App\Models\Conversation::where('employer_id', $employer->id)->count();
|
||||
|
||||
$totalHiredWorkers = \App\Models\JobOffer::where('employer_id', $employer->id)->where('status', 'accepted')->count() +
|
||||
\App\Models\JobApplication::whereHas('jobPost', function($q) use ($employer) {
|
||||
$q->where('employer_id', $employer->id);
|
||||
})->where('status', 'hired')->count();
|
||||
|
||||
$savedCandidates = \App\Models\Shortlist::where('employer_id', $employer->id)->count();
|
||||
|
||||
// Resolve plan
|
||||
$sub = \Illuminate\Support\Facades\DB::table('subscriptions')
|
||||
->where('user_id', $employer->id)
|
||||
->where('status', 'active')
|
||||
->latest('id')
|
||||
->first();
|
||||
|
||||
$currentPlan = [
|
||||
'plan_id' => $sub ? $sub->plan_id : 'premium',
|
||||
'name' => $sub ? (ucfirst($sub->plan_id) . ' Pass') : 'Premium Employer Pass',
|
||||
'status' => $sub ? $sub->status : 'active',
|
||||
'starts_at' => $sub ? date('Y-m-d', strtotime($sub->starts_at)) : now()->subDays(6)->format('Y-m-d'),
|
||||
'expires_at' => $sub ? date('Y-m-d', strtotime($sub->expires_at)) : now()->addDays(24)->format('Y-m-d'),
|
||||
];
|
||||
|
||||
// Recent Announcements / Events
|
||||
$dbAnnouncements = \App\Models\Announcement::latest()->limit(5)->get();
|
||||
$recentAnnouncements = $dbAnnouncements->map(function ($ann) {
|
||||
$body = $ann->body;
|
||||
$eventDate = null;
|
||||
$eventTime = null;
|
||||
$locationDetails = null;
|
||||
$locationPin = null;
|
||||
$providedItems = null;
|
||||
|
||||
if (strpos($ann->body, '{"type":"Charity"') === 0) {
|
||||
$decoded = json_decode($ann->body, true);
|
||||
if ($decoded) {
|
||||
$body = $decoded['content'] ?? $ann->body;
|
||||
$eventDate = $decoded['event_date'] ?? null;
|
||||
$eventTime = $decoded['event_time'] ?? null;
|
||||
$locationDetails = $decoded['location_details'] ?? null;
|
||||
$locationPin = $decoded['location_pin'] ?? null;
|
||||
$providedItems = $decoded['provided_items'] ?? null;
|
||||
}
|
||||
} else {
|
||||
$body = $ann->body;
|
||||
$eventDate = $ann->created_at->addDays(2)->format('Y-m-d');
|
||||
$eventTime = '9:00 AM - 3:00 PM';
|
||||
$locationDetails = 'Al Quoz Community Center, Dubai';
|
||||
$locationPin = 'https://maps.google.com';
|
||||
$providedItems = 'Free Medical Checks & Food Supplies';
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $ann->id,
|
||||
'title' => $ann->title,
|
||||
'body' => $body,
|
||||
'is_charity' => true,
|
||||
'event_date' => $eventDate,
|
||||
'event_time' => $eventTime,
|
||||
'location_details' => $locationDetails,
|
||||
'location_pin' => $locationPin,
|
||||
'provided_items' => $providedItems,
|
||||
'created_at' => $ann->created_at->toISOString(),
|
||||
'time_ago' => $ann->created_at->diffForHumans(),
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'stats' => [
|
||||
'contacted_workers_count' => $contactedWorkersCount,
|
||||
'total_hired_workers' => $totalHiredWorkers,
|
||||
'saved_candidates' => $savedCandidates,
|
||||
],
|
||||
'current_plan' => $currentPlan,
|
||||
'recent_announcements' => $recentAnnouncements,
|
||||
'recent_events' => $recentAnnouncements, // alias for ease
|
||||
]
|
||||
], 200);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
logger()->error('Mobile Employer Get Dashboard Failure: ' . $e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'An error occurred while fetching the dashboard statistics.',
|
||||
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -140,62 +140,5 @@ public function editReview(Request $request, $id)
|
||||
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get reviews posted by this employer.
|
||||
* GET /api/employers/reviews
|
||||
*/
|
||||
public function getReviews(Request $request)
|
||||
{
|
||||
/** @var User $employer */
|
||||
$employer = $request->attributes->get('employer');
|
||||
|
||||
try {
|
||||
$page = (int)$request->input('page', 1);
|
||||
$perPage = (int)$request->input('per_page', 15);
|
||||
|
||||
$query = Review::where('employer_id', $employer->id)
|
||||
->with('worker')
|
||||
->latest();
|
||||
|
||||
$total = $query->count();
|
||||
$offset = ($page - 1) * $perPage;
|
||||
|
||||
$reviews = $query->skip($offset)->take($perPage)->get()
|
||||
->map(function ($rev) {
|
||||
return [
|
||||
'id' => $rev->id,
|
||||
'worker_id' => $rev->worker_id,
|
||||
'worker_name' => $rev->worker->name ?? 'Worker',
|
||||
'rating' => $rev->rating,
|
||||
'comment' => $rev->comment,
|
||||
'created_at' => $rev->created_at->toISOString(),
|
||||
'time_ago' => $rev->created_at->diffForHumans(),
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'reviews' => $reviews,
|
||||
'pagination' => [
|
||||
'total' => $total,
|
||||
'per_page' => $perPage,
|
||||
'current_page' => $page,
|
||||
'last_page' => max(1, (int)ceil($total / $perPage)),
|
||||
]
|
||||
]
|
||||
], 200);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
logger()->error('Mobile API Get Employer Reviews Failure: ' . $e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'An error occurred while fetching reviews.',
|
||||
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -65,10 +65,12 @@ private function formatWorker(Worker $w)
|
||||
'nationality' => $w->nationality,
|
||||
'photo' => $photo,
|
||||
'emirates_id_status' => $emiratesIdStatus,
|
||||
'category' => $w->category ? $w->category->name : 'Domestic Worker',
|
||||
'skills' => $mappedSkills,
|
||||
'availability_status' => $availabilityStatus,
|
||||
'visa_status' => $visaStatus,
|
||||
'experience' => $w->experience,
|
||||
'salary' => (int)$w->salary,
|
||||
'religion' => $w->religion,
|
||||
'languages' => $langs,
|
||||
'age' => $w->age,
|
||||
@ -101,66 +103,34 @@ public function getWorkers(Request $request)
|
||||
});
|
||||
}
|
||||
|
||||
// Apply category filter if provided
|
||||
if ($request->filled('category_id')) {
|
||||
$query->where('category_id', $request->category_id);
|
||||
}
|
||||
|
||||
// Apply nationality filter if provided
|
||||
if ($request->filled('nationality')) {
|
||||
$query->where('nationality', $request->nationality);
|
||||
}
|
||||
|
||||
// Apply salary filters
|
||||
if ($request->filled('min_salary')) {
|
||||
$query->where('salary', '>=', $request->min_salary);
|
||||
}
|
||||
if ($request->filled('max_salary')) {
|
||||
$query->where('salary', '<=', $request->max_salary);
|
||||
}
|
||||
|
||||
$dbWorkers = $query->latest()->get();
|
||||
|
||||
$formattedWorkers = $dbWorkers->map(function ($w) {
|
||||
return $this->formatWorker($w);
|
||||
})->filter()->values();
|
||||
|
||||
$workersArray = $formattedWorkers->toArray();
|
||||
|
||||
// Apply filters: skills, language/languages, nationality, availability
|
||||
if ($request->filled('nationality')) {
|
||||
$nationality = strtolower($request->nationality);
|
||||
$workersArray = array_values(array_filter($workersArray, function ($c) use ($nationality) {
|
||||
return isset($c['nationality']) && strtolower($c['nationality']) === $nationality;
|
||||
}));
|
||||
}
|
||||
|
||||
if ($request->filled('availability')) {
|
||||
$availability = strtolower($request->availability);
|
||||
$workersArray = array_values(array_filter($workersArray, function ($c) use ($availability) {
|
||||
$val = $c['availability_status'] ?? ($c['availability'] ?? '');
|
||||
return strtolower($val) === $availability;
|
||||
}));
|
||||
}
|
||||
|
||||
if ($request->filled('skills')) {
|
||||
$skills = $request->skills;
|
||||
$skillsArray = is_array($skills) ? $skills : array_filter(array_map('trim', explode(',', $skills)));
|
||||
$skillsArray = array_map('strtolower', $skillsArray);
|
||||
|
||||
$workersArray = array_values(array_filter($workersArray, function ($c) use ($skillsArray) {
|
||||
if (!isset($c['skills']) || !is_array($c['skills'])) return false;
|
||||
foreach ($c['skills'] as $s) {
|
||||
if (in_array(strtolower($s), $skillsArray)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}));
|
||||
}
|
||||
|
||||
$langParam = $request->input('language') ?? $request->input('languages');
|
||||
if ($langParam) {
|
||||
$langsArray = is_array($langParam) ? $langParam : array_filter(array_map('trim', explode(',', $langParam)));
|
||||
$langsArray = array_map('strtolower', $langsArray);
|
||||
|
||||
$workersArray = array_values(array_filter($workersArray, function ($c) use ($langsArray) {
|
||||
if (!isset($c['languages']) || !is_array($c['languages'])) return false;
|
||||
foreach ($c['languages'] as $l) {
|
||||
if (in_array(strtolower($l), $langsArray)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'workers' => $workersArray
|
||||
'workers' => $formattedWorkers
|
||||
]
|
||||
], 200);
|
||||
|
||||
@ -189,29 +159,9 @@ public function getCandidates(Request $request)
|
||||
|
||||
// Fetch Job Applications
|
||||
$jobIds = JobPost::where('employer_id', $employerId)->pluck('id');
|
||||
$applicationsQuery = JobApplication::whereIn('job_id', $jobIds)
|
||||
->with(['worker.category', 'jobPost']);
|
||||
|
||||
// Fetch Direct Hiring Offers
|
||||
$directOffersQuery = JobOffer::where('employer_id', $employerId)
|
||||
->with('worker.category');
|
||||
|
||||
// Apply search filter if provided
|
||||
if ($request->filled('search')) {
|
||||
$search = $request->search;
|
||||
$applicationsQuery->whereHas('worker', function ($q) use ($search) {
|
||||
$q->where('name', 'like', "%{$search}%")
|
||||
->orWhere('nationality', 'like', "%{$search}%")
|
||||
->orWhere('religion', 'like', "%{$search}%");
|
||||
});
|
||||
$directOffersQuery->whereHas('worker', function ($q) use ($search) {
|
||||
$q->where('name', 'like', "%{$search}%")
|
||||
->orWhere('nationality', 'like', "%{$search}%")
|
||||
->orWhere('religion', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
$applications = $applicationsQuery->get();
|
||||
$applications = JobApplication::whereIn('job_id', $jobIds)
|
||||
->with(['worker.category', 'jobPost'])
|
||||
->get();
|
||||
|
||||
$selectedWorkers = $applications->map(function ($app) {
|
||||
$w = $app->worker;
|
||||
@ -224,29 +174,23 @@ public function getCandidates(Request $request)
|
||||
elseif ($app->status === 'applied') $status = 'Reviewing';
|
||||
else $status = ucfirst($app->status);
|
||||
|
||||
$langs = ($w->id % 2 === 0) ? ['English', 'Arabic'] : (($w->id % 3 === 0) ? ['English', 'Tagalog'] : ['English', 'Hindi', 'Arabic']);
|
||||
$skillsList = ['cooking', 'driving', 'childcare', 'cleaning', 'elderly care', 'gardening'];
|
||||
$mappedSkills = [
|
||||
$skillsList[$w->id % 6],
|
||||
$skillsList[($w->id + 2) % 6]
|
||||
];
|
||||
|
||||
return [
|
||||
'id' => $app->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('Y-m-d H:i:s'),
|
||||
'type' => 'application',
|
||||
'skills' => $mappedSkills,
|
||||
'languages' => $langs,
|
||||
'availability' => $w->availability ?? 'Immediate',
|
||||
];
|
||||
})->filter()->values()->toArray();
|
||||
|
||||
// Fetch Direct Hiring Offers
|
||||
$directOffers = $directOffersQuery->get();
|
||||
$directOffers = JobOffer::where('employer_id', $employerId)
|
||||
->with('worker.category')
|
||||
->get();
|
||||
|
||||
$directWorkers = $directOffers->map(function ($offer) {
|
||||
$w = $offer->worker;
|
||||
@ -257,24 +201,16 @@ public function getCandidates(Request $request)
|
||||
elseif ($offer->status === 'rejected') $status = 'Rejected';
|
||||
elseif ($offer->status === 'pending') $status = 'Offer Sent';
|
||||
|
||||
$langs = ($w->id % 2 === 0) ? ['English', 'Arabic'] : (($w->id % 3 === 0) ? ['English', 'Tagalog'] : ['English', 'Hindi', 'Arabic']);
|
||||
$skillsList = ['cooking', 'driving', 'childcare', 'cleaning', 'elderly care', 'gardening'];
|
||||
$mappedSkills = [
|
||||
$skillsList[$w->id % 6],
|
||||
$skillsList[($w->id + 2) % 6]
|
||||
];
|
||||
|
||||
return [
|
||||
'id' => 'offer_' . $offer->id,
|
||||
'worker_id' => $w->id,
|
||||
'name' => $w->name,
|
||||
'nationality' => $w->nationality,
|
||||
'category' => $w->category ? $w->category->name : 'General Helper',
|
||||
'salary' => (int)$offer->salary,
|
||||
'status' => $status,
|
||||
'applied_at' => $offer->created_at->format('Y-m-d H:i:s'),
|
||||
'type' => 'direct_offer',
|
||||
'skills' => $mappedSkills,
|
||||
'languages' => $langs,
|
||||
'availability' => $w->availability ?? 'Immediate',
|
||||
];
|
||||
})->filter()->values()->toArray();
|
||||
|
||||
@ -286,69 +222,10 @@ public function getCandidates(Request $request)
|
||||
return $c && $c['status'] === 'Hired';
|
||||
}));
|
||||
|
||||
// Apply filters: skills, languages, nationality, availability
|
||||
if ($request->filled('nationality')) {
|
||||
$nationality = strtolower($request->nationality);
|
||||
$mergedCandidates = array_values(array_filter($mergedCandidates, function ($c) use ($nationality) {
|
||||
return isset($c['nationality']) && strtolower($c['nationality']) === $nationality;
|
||||
}));
|
||||
}
|
||||
|
||||
if ($request->filled('availability')) {
|
||||
$availability = strtolower($request->availability);
|
||||
$mergedCandidates = array_values(array_filter($mergedCandidates, function ($c) use ($availability) {
|
||||
return isset($c['availability']) && strtolower($c['availability']) === $availability;
|
||||
}));
|
||||
}
|
||||
|
||||
if ($request->filled('skills')) {
|
||||
$skills = $request->skills;
|
||||
$skillsArray = is_array($skills) ? $skills : array_filter(array_map('trim', explode(',', $skills)));
|
||||
$skillsArray = array_map('strtolower', $skillsArray);
|
||||
|
||||
$mergedCandidates = array_values(array_filter($mergedCandidates, function ($c) use ($skillsArray) {
|
||||
if (!isset($c['skills']) || !is_array($c['skills'])) return false;
|
||||
foreach ($c['skills'] as $s) {
|
||||
if (in_array(strtolower($s), $skillsArray)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}));
|
||||
}
|
||||
|
||||
if ($request->filled('languages')) {
|
||||
$languages = $request->languages;
|
||||
$langsArray = is_array($languages) ? $languages : array_filter(array_map('trim', explode(',', $languages)));
|
||||
$langsArray = array_map('strtolower', $langsArray);
|
||||
|
||||
$mergedCandidates = array_values(array_filter($mergedCandidates, function ($c) use ($langsArray) {
|
||||
if (!isset($c['languages']) || !is_array($c['languages'])) return false;
|
||||
foreach ($c['languages'] as $l) {
|
||||
if (in_array(strtolower($l), $langsArray)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}));
|
||||
}
|
||||
|
||||
$page = (int)$request->input('page', 1);
|
||||
$perPage = (int)$request->input('per_page', 15);
|
||||
$total = count($mergedCandidates);
|
||||
$offset = ($page - 1) * $perPage;
|
||||
$paginatedCandidates = array_slice($mergedCandidates, $offset, $perPage);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'candidates' => $paginatedCandidates,
|
||||
'pagination' => [
|
||||
'total' => $total,
|
||||
'per_page' => $perPage,
|
||||
'current_page' => $page,
|
||||
'last_page' => max(1, (int)ceil($total / $perPage)),
|
||||
]
|
||||
'candidates' => $mergedCandidates
|
||||
]
|
||||
], 200);
|
||||
|
||||
@ -593,6 +470,8 @@ public function getWorkerDetail(Request $request, $id)
|
||||
'id' => $sw->id,
|
||||
'name' => $sw->name,
|
||||
'nationality' => $sw->nationality,
|
||||
'category' => $sw->category ? $sw->category->name : 'Helper',
|
||||
'salary' => (int)$sw->salary,
|
||||
'rating' => 4.7,
|
||||
'verified' => (bool)$sw->verified,
|
||||
'availability_status' => $availabilityStatus,
|
||||
@ -610,11 +489,13 @@ public function getWorkerDetail(Request $request, $id)
|
||||
'nationality' => $w->nationality,
|
||||
'photo' => $photo,
|
||||
'emirates_id_status' => $emiratesIdStatus,
|
||||
'category' => $w->category ? $w->category->name : 'Domestic Worker',
|
||||
'skills' => $mappedSkills,
|
||||
'availability_status' => $availabilityStatus,
|
||||
'visa_status' => $visaStatus,
|
||||
'experience' => $w->experience,
|
||||
'experience_years' => 5,
|
||||
'salary' => (int)$w->salary,
|
||||
'religion' => $w->religion,
|
||||
'languages' => $langs,
|
||||
'age' => $w->age,
|
||||
@ -645,119 +526,5 @@ public function getWorkerDetail(Request $request, $id)
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/employers/shortlist
|
||||
* Retrieve the list of shortlisted workers for the authenticated employer.
|
||||
*/
|
||||
public function getShortlist(Request $request)
|
||||
{
|
||||
/** @var User $employer */
|
||||
$employer = $request->attributes->get('employer');
|
||||
|
||||
try {
|
||||
$page = (int)$request->input('page', 1);
|
||||
$perPage = (int)$request->input('per_page', 15);
|
||||
|
||||
$shortlists = Shortlist::where('employer_id', $employer->id)
|
||||
->with(['worker.category', 'worker.skills'])
|
||||
->get();
|
||||
|
||||
$formattedWorkers = $shortlists->map(function ($s) {
|
||||
if ($s->worker) {
|
||||
return $this->formatWorker($s->worker);
|
||||
}
|
||||
return null;
|
||||
})->filter()->values();
|
||||
|
||||
$workersArray = $formattedWorkers->toArray();
|
||||
$total = count($workersArray);
|
||||
$offset = ($page - 1) * $perPage;
|
||||
$paginatedWorkers = array_slice($workersArray, $offset, $perPage);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'workers' => $paginatedWorkers,
|
||||
'pagination' => [
|
||||
'total' => $total,
|
||||
'per_page' => $perPage,
|
||||
'current_page' => $page,
|
||||
'last_page' => max(1, (int)ceil($total / $perPage)),
|
||||
]
|
||||
]
|
||||
], 200);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
logger()->error('Mobile API Employer Get Shortlist Failure: ' . $e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'An error occurred while fetching the shortlist.',
|
||||
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/employers/shortlist
|
||||
* Toggle a worker's shortlist status for the authenticated employer.
|
||||
*/
|
||||
public function toggleShortlist(Request $request)
|
||||
{
|
||||
/** @var User $employer */
|
||||
$employer = $request->attributes->get('employer');
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'worker_id' => 'required|exists:workers,id',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Validation error.',
|
||||
'errors' => $validator->errors()
|
||||
], 422);
|
||||
}
|
||||
|
||||
try {
|
||||
$workerId = $request->worker_id;
|
||||
|
||||
$existing = Shortlist::where('employer_id', $employer->id)
|
||||
->where('worker_id', $workerId)
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
$existing->delete();
|
||||
$action = 'removed';
|
||||
} else {
|
||||
Shortlist::create([
|
||||
'employer_id' => $employer->id,
|
||||
'worker_id' => $workerId,
|
||||
]);
|
||||
$action = 'added';
|
||||
}
|
||||
|
||||
$shortlistedIds = Shortlist::where('employer_id', $employer->id)->pluck('worker_id')->toArray();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => "Worker successfully {$action} from shortlist.",
|
||||
'data' => [
|
||||
'action' => $action,
|
||||
'shortlisted_ids' => $shortlistedIds
|
||||
]
|
||||
], 200);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
logger()->error('Mobile API Employer Toggle Shortlist Failure: ' . $e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'An error occurred while toggling the shortlist.',
|
||||
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -17,14 +17,9 @@ class WorkerAnnouncementController extends Controller
|
||||
public function getAnnouncements(Request $request)
|
||||
{
|
||||
try {
|
||||
$page = (int)$request->input('page', 1);
|
||||
$perPage = (int)$request->input('per_page', 15);
|
||||
|
||||
$query = Announcement::with('employer.employerProfile')->latest();
|
||||
$total = $query->count();
|
||||
$offset = ($page - 1) * $perPage;
|
||||
|
||||
$announcements = $query->skip($offset)->take($perPage)->get()
|
||||
$announcements = Announcement::with('employer.employerProfile')
|
||||
->latest()
|
||||
->get()
|
||||
->map(function ($announcement) {
|
||||
return [
|
||||
'id' => $announcement->id,
|
||||
@ -41,13 +36,7 @@ public function getAnnouncements(Request $request)
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'announcements' => $announcements,
|
||||
'pagination' => [
|
||||
'total' => $total,
|
||||
'per_page' => $perPage,
|
||||
'current_page' => $page,
|
||||
'last_page' => max(1, (int)ceil($total / $perPage)),
|
||||
]
|
||||
'announcements' => $announcements
|
||||
]
|
||||
], 200);
|
||||
|
||||
|
||||
@ -53,7 +53,7 @@ public function index(Request $request)
|
||||
// 1. Stats
|
||||
$shortlistedCount = Shortlist::where('employer_id', $user->id)->count();
|
||||
$messagesSent = Message::where('sender_id', $user->id)->where('sender_type', 'employer')->count();
|
||||
$contactedWorkersCount = Conversation::where('employer_id', $user->id)->count();
|
||||
$pendingOffersCount = \App\Models\JobOffer::where('employer_id', $user->id)->where('status', 'pending')->count();
|
||||
$hiredCount = \App\Models\JobOffer::where('employer_id', $user->id)->where('status', 'accepted')->count() +
|
||||
\App\Models\JobApplication::whereHas('jobPost', function($q) use ($user) {
|
||||
$q->where('employer_id', $user->id);
|
||||
@ -63,7 +63,7 @@ public function index(Request $request)
|
||||
'shortlisted_count' => $shortlistedCount,
|
||||
'messages_sent' => $messagesSent,
|
||||
'days_remaining' => (int)$daysRemaining,
|
||||
'contacted_workers_count' => $contactedWorkersCount,
|
||||
'pending_offers' => $pendingOffersCount,
|
||||
'hired_count' => $hiredCount,
|
||||
'analytics' => [
|
||||
'profile_views' => 48,
|
||||
|
||||
@ -426,105 +426,4 @@ public function logout(Request $request)
|
||||
return redirect()->route('employer.login')
|
||||
->with('success', 'Logged out successfully.');
|
||||
}
|
||||
|
||||
public function forgotPassword(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'email' => 'required|email',
|
||||
]);
|
||||
|
||||
$user = User::where('email', $request->email)->where('role', 'employer')->first();
|
||||
if (!$user) {
|
||||
return response()->json([
|
||||
'message' => 'No employer account found with this email address.'
|
||||
], 404);
|
||||
}
|
||||
|
||||
$otp = '111111'; // default development OTP
|
||||
if (app()->environment('production')) {
|
||||
$otp = strval(rand(100000, 999999));
|
||||
}
|
||||
|
||||
\Illuminate\Support\Facades\Cache::put('employer_reset_otp_' . $request->email, [
|
||||
'code' => $otp,
|
||||
'expires_at' => now()->addMinutes(10)->timestamp
|
||||
], 600);
|
||||
|
||||
// Send reset OTP email
|
||||
try {
|
||||
Mail::to($request->email)->send(new \App\Mail\EmployerResetOtpMail(
|
||||
$otp,
|
||||
$user->name
|
||||
));
|
||||
} catch (\Exception $e) {
|
||||
logger()->error('Web Employer Forgot Password Mail Failure: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Verification code sent to your email.'
|
||||
]);
|
||||
}
|
||||
|
||||
public function resetPassword(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'email' => 'required|email',
|
||||
'otp' => 'required|string|size:6',
|
||||
'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.',
|
||||
]);
|
||||
|
||||
$cachedData = \Illuminate\Support\Facades\Cache::get('employer_reset_otp_' . $request->email);
|
||||
|
||||
if (!$cachedData || !isset($cachedData['code']) || !isset($cachedData['expires_at'])) {
|
||||
return response()->json([
|
||||
'message' => 'Invalid or expired verification code.'
|
||||
], 400);
|
||||
}
|
||||
|
||||
if ($cachedData['code'] !== $request->otp || now()->timestamp > $cachedData['expires_at']) {
|
||||
return response()->json([
|
||||
'message' => 'Invalid or expired verification code.'
|
||||
], 400);
|
||||
}
|
||||
|
||||
$user = User::where('email', $request->email)->where('role', 'employer')->first();
|
||||
$sponsor = \App\Models\Sponsor::where('email', $request->email)->first();
|
||||
|
||||
if (!$user || !$sponsor) {
|
||||
return response()->json([
|
||||
'message' => 'User account not found.'
|
||||
], 404);
|
||||
}
|
||||
|
||||
\Illuminate\Support\Facades\DB::transaction(function () use ($request, $user, $sponsor) {
|
||||
$hashedPassword = Hash::make($request->password);
|
||||
|
||||
$user->update([
|
||||
'password' => $hashedPassword,
|
||||
]);
|
||||
|
||||
$sponsor->update([
|
||||
'password' => $hashedPassword,
|
||||
]);
|
||||
});
|
||||
|
||||
\Illuminate\Support\Facades\Cache::forget('employer_reset_otp_' . $request->email);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Password reset successfully.'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -109,10 +109,8 @@ public function index(Request $request)
|
||||
$lastMsg = $conv->messages->last();
|
||||
return [
|
||||
'id' => $conv->id,
|
||||
'worker_id' => $conv->worker_id,
|
||||
'worker_name' => $conv->worker->name ?? 'Candidate',
|
||||
'category' => $conv->worker->category->name ?? 'General Helper',
|
||||
'worker_status' => strtolower($conv->worker->status ?? 'active'),
|
||||
'last_message' => $lastMsg->text ?? 'No messages yet.',
|
||||
'unread' => $lastMsg ? ($lastMsg->sender_type === 'worker' && is_null($lastMsg->read_at)) : false,
|
||||
'online' => true,
|
||||
@ -141,10 +139,8 @@ public function show($id)
|
||||
$lastMsg = $conv->messages->last();
|
||||
return [
|
||||
'id' => $conv->id,
|
||||
'worker_id' => $conv->worker_id,
|
||||
'worker_name' => $conv->worker->name ?? 'Candidate',
|
||||
'category' => $conv->worker->category->name ?? 'General Helper',
|
||||
'worker_status' => strtolower($conv->worker->status ?? 'active'),
|
||||
'last_message' => $lastMsg->text ?? 'No messages yet.',
|
||||
'unread' => $lastMsg ? ($lastMsg->sender_type === 'worker' && is_null($lastMsg->read_at)) : false,
|
||||
'online' => true,
|
||||
@ -165,10 +161,8 @@ public function show($id)
|
||||
|
||||
$conversationData = [
|
||||
'id' => $activeConv->id,
|
||||
'worker_id' => $activeConv->worker_id,
|
||||
'worker_name' => $activeConv->worker->name ?? 'Candidate',
|
||||
'category' => $activeConv->worker->category->name ?? 'General Helper',
|
||||
'worker_status' => strtolower($activeConv->worker->status ?? 'active'),
|
||||
'online' => true,
|
||||
'salary' => ($activeConv->worker->expected_salary ?? 2000) . ' AED',
|
||||
'nationality' => $activeConv->worker->nationality ?? 'Unknown',
|
||||
|
||||
@ -82,21 +82,12 @@ public function update(Request $request)
|
||||
return back()->withErrors(['general' => 'User session not found.']);
|
||||
}
|
||||
|
||||
$sponsor = \App\Models\Sponsor::where('email', $user->email)->first();
|
||||
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
'email',
|
||||
'max:255',
|
||||
'unique:users,email,' . $user->id,
|
||||
$sponsor ? 'unique:sponsors,email,' . $sponsor->id : 'unique:sponsors,email',
|
||||
],
|
||||
'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,english,arabic',
|
||||
'language' => 'required|string|in:English,Arabic',
|
||||
'notifications' => 'required|boolean',
|
||||
'nationality' => 'nullable|string|max:255',
|
||||
'family_size' => 'nullable|string|max:255',
|
||||
@ -108,24 +99,12 @@ public function update(Request $request)
|
||||
'new_password' => 'nullable|string|min:8|confirmed',
|
||||
]);
|
||||
|
||||
$oldEmail = $user->getOriginal('email') ?? $user->email;
|
||||
|
||||
// Update User Model
|
||||
$user->update([
|
||||
'name' => $request->name,
|
||||
'email' => $request->email,
|
||||
]);
|
||||
|
||||
// Sync with corresponding sponsor record if found
|
||||
$matchingSponsor = \App\Models\Sponsor::where('email', $oldEmail)->first();
|
||||
if ($matchingSponsor) {
|
||||
$matchingSponsor->update([
|
||||
'full_name' => $request->name,
|
||||
'email' => $request->email,
|
||||
'mobile' => $request->phone,
|
||||
]);
|
||||
}
|
||||
|
||||
// Update EmployerProfile
|
||||
$profile = $user->employerProfile;
|
||||
if (!$profile) {
|
||||
@ -133,7 +112,7 @@ public function update(Request $request)
|
||||
}
|
||||
$profile->company_name = $request->company_name;
|
||||
$profile->phone = $request->phone;
|
||||
$profile->language = ucfirst(strtolower($request->language));
|
||||
$profile->language = $request->language;
|
||||
$profile->notifications = $request->notifications;
|
||||
|
||||
$profile->nationality = $request->nationality;
|
||||
|
||||
@ -1,46 +0,0 @@
|
||||
<?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 EmployerResetOtpMail extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public $otp;
|
||||
public $employerName;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*/
|
||||
public function __construct($otp, $employerName)
|
||||
{
|
||||
$this->otp = $otp;
|
||||
$this->employerName = $employerName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message envelope.
|
||||
*/
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
subject: 'Password Reset Verification Code: ' . $this->otp,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message content definition.
|
||||
*/
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(
|
||||
view: 'emails.employer-reset-otp',
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1478,95 +1478,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/employers/forgot-password": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Employer/Auth"
|
||||
],
|
||||
"summary": "Request Password Reset Code",
|
||||
"description": "Validates employer email and sends a 6-digit password reset verification code (OTP).",
|
||||
"security": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"email"
|
||||
],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"example": "ahmad@example.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Password reset verification code sent successfully."
|
||||
},
|
||||
"404": {
|
||||
"description": "No employer account found with this email."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/employers/reset-password": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Employer/Auth"
|
||||
],
|
||||
"summary": "Reset Password with Verification Code",
|
||||
"description": "Verifies the 6-digit OTP code and updates the password for both employer user and sponsor records.",
|
||||
"security": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"email",
|
||||
"otp",
|
||||
"password",
|
||||
"password_confirmation"
|
||||
],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"example": "ahmad@example.com"
|
||||
},
|
||||
"otp": {
|
||||
"type": "string",
|
||||
"example": "111111"
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"example": "SecureNewPassword@123"
|
||||
},
|
||||
"password_confirmation": {
|
||||
"type": "string",
|
||||
"example": "SecureNewPassword@123"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Password reset successfully."
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid or expired verification code."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/employers/conversations": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@ -1686,28 +1597,6 @@
|
||||
],
|
||||
"summary": "Get Charity Events (Worker)",
|
||||
"description": "Allows workers to retrieve the list of active charity events, food drives, and medical support programs scheduled in Dubai.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "page",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Page number.",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"default": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "per_page",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Number of items per page.",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"default": 15
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "List of charity events retrieved successfully."
|
||||
@ -1722,28 +1611,6 @@
|
||||
],
|
||||
"summary": "Get Posted Charity Events (Employer)",
|
||||
"description": "Allows employers/sponsors to retrieve the list of charity events they have published.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "page",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Page number.",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"default": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "per_page",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Number of items per page.",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"default": 15
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "List of posted charity events retrieved successfully."
|
||||
@ -1838,20 +1705,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/employers/dashboard": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Employer/Profile"
|
||||
],
|
||||
"summary": "Get Employer Dashboard stats & recent events",
|
||||
"description": "Retrieves stats including contacted workers count, total hired workers, saved candidates, current plan details, and recent announcements or events.",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Employer dashboard stats retrieved successfully."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/employers/profile": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@ -1908,9 +1761,7 @@
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"English",
|
||||
"Arabic",
|
||||
"english",
|
||||
"arabic"
|
||||
"Arabic"
|
||||
],
|
||||
"example": "English"
|
||||
},
|
||||
@ -1960,21 +1811,12 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "skills",
|
||||
"name": "category_id",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Filter by skills (comma-separated list).",
|
||||
"description": "Filter by helper category ID.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "language",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Filter by language.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -1987,32 +1829,21 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "availability",
|
||||
"name": "min_salary",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Filter by availability status.",
|
||||
"description": "Minimum desired salary range.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "page",
|
||||
"name": "max_salary",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Page number.",
|
||||
"description": "Maximum desired salary range.",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"default": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "per_page",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Number of items per page.",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"default": 15
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
@ -2088,10 +1919,7 @@
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"example": [
|
||||
"childcare",
|
||||
"cooking"
|
||||
]
|
||||
"example": ["childcare", "cooking"]
|
||||
},
|
||||
"availability_status": {
|
||||
"type": "string",
|
||||
@ -2122,10 +1950,7 @@
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"example": [
|
||||
"English",
|
||||
"Tagalog"
|
||||
]
|
||||
"example": ["English", "Tagalog"]
|
||||
},
|
||||
"age": {
|
||||
"type": "integer",
|
||||
@ -2184,79 +2009,14 @@
|
||||
}
|
||||
},
|
||||
"/employers/candidates": {
|
||||
|
||||
"get": {
|
||||
"tags": [
|
||||
"Employer/Pipeline"
|
||||
],
|
||||
"summary": "Get Candidate Applications and Offers",
|
||||
"description": "Returns all job applications and direct hiring offers connected to the authenticated sponsor account that have achieved 'Hired' status.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "search",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Search term matching helper name, nationality, or religion.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "skills",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Filter candidates by skills (comma-separated list of skill names, e.g. cooking,driving).",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "languages",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Filter candidates by languages spoken (comma-separated list of language names, e.g. English,Arabic).",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "nationality",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Filter candidates by nationality name.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "availability",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Filter candidates by availability status.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "page",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Page number.",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"default": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "per_page",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Number of items per page.",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"default": 15
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Employer candidate pipeline retrieved successfully."
|
||||
@ -2289,74 +2049,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/employers/shortlist": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Employer/Pipeline"
|
||||
],
|
||||
"summary": "Get Shortlisted Workers",
|
||||
"description": "Returns a list of all workers shortlisted by the authenticated sponsor.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "page",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Page number.",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"default": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "per_page",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Number of items per page.",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"default": 15
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Shortlisted workers retrieved successfully."
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"Employer/Pipeline"
|
||||
],
|
||||
"summary": "Toggle Worker Shortlist Status",
|
||||
"description": "Toggles (adds or removes) a worker to/from the authenticated sponsor's shortlist.",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"worker_id"
|
||||
],
|
||||
"properties": {
|
||||
"worker_id": {
|
||||
"type": "integer",
|
||||
"example": 1,
|
||||
"description": "Worker ID to shortlist/unshortlist."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Shortlist status toggled successfully."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/employers/payments": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@ -2364,28 +2056,6 @@
|
||||
],
|
||||
"summary": "Get Payment History",
|
||||
"description": "Retrieves the complete transaction history for the authenticated sponsor/employer, including subscription passes and document vetting receipts.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "page",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Page number.",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"default": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "per_page",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Number of items per page.",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"default": 15
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Employer billing history retrieved successfully."
|
||||
@ -2595,40 +2265,6 @@
|
||||
}
|
||||
},
|
||||
"/employers/reviews": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Employer/Reviews"
|
||||
],
|
||||
"summary": "Get Reviews Written by Employer",
|
||||
"description": "Retrieves a paginated list of all reviews written by the authenticated employer.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "page",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Page number.",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"default": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "per_page",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Number of items per page.",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"default": 15
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Reviews retrieved successfully."
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"Employer/Reviews"
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 371 KiB |
@ -1,295 +1,48 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Head, Link, router } from '@inertiajs/react';
|
||||
import {
|
||||
KeyRound,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Loader2,
|
||||
AlertTriangle,
|
||||
CheckCircle2,
|
||||
ArrowLeft
|
||||
} from 'lucide-react';
|
||||
import axios from 'axios';
|
||||
import React from 'react';
|
||||
import { Head, Link } from '@inertiajs/react';
|
||||
import { KeyRound } from 'lucide-react';
|
||||
|
||||
export default function ForgotPassword() {
|
||||
const [step, setStep] = useState(1); // 1 = Request Code, 2 = Verify & Reset, 3 = Success
|
||||
const [email, setEmail] = useState('');
|
||||
const [otp, setOtp] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [passwordConfirmation, setPasswordConfirmation] = useState('');
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [successMessage, setSuccessMessage] = useState('');
|
||||
|
||||
const handleSendCode = async (e) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
|
||||
if (!email.trim()) {
|
||||
setError('Please enter your email address.');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await axios.post('/employer/forgot-password', { email });
|
||||
if (response.data.success) {
|
||||
setStep(2);
|
||||
setSuccessMessage('A 6-digit verification code has been sent to your email.');
|
||||
} else {
|
||||
setError(response.data.message || 'Failed to send reset code. Please try again.');
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err.response?.data?.message || 'Failed to send reset code. Please verify your email address.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetPassword = async (e) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
|
||||
if (!otp.trim() || otp.length !== 6) {
|
||||
setError('Please enter the 6-digit verification code.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
setError('Please enter a new password.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
setError('Password must be at least 8 characters long.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (password !== passwordConfirmation) {
|
||||
setError('Passwords do not match.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate password strength: 1 uppercase, 1 lowercase, 1 number, 1 special character
|
||||
const hasUppercase = /[A-Z]/.test(password);
|
||||
const hasLowercase = /[a-z]/.test(password);
|
||||
const hasNumber = /[0-9]/.test(password);
|
||||
const hasSpecial = /[^A-Za-z0-9]/.test(password);
|
||||
|
||||
if (!hasUppercase || !hasLowercase || !hasNumber || !hasSpecial) {
|
||||
setError('Password must contain at least 1 uppercase, 1 lowercase, 1 number, and 1 special character.');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await axios.post('/employer/reset-password', {
|
||||
email,
|
||||
otp,
|
||||
password,
|
||||
password_confirmation: passwordConfirmation
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
setStep(3);
|
||||
} else {
|
||||
setError(response.data.message || 'Failed to reset password. Please check your verification code.');
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err.response?.data?.message || 'Failed to reset password. Please check your inputs and try again.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col justify-center items-center px-4 bg-slate-50 font-sans">
|
||||
<Head title="Reset Password - Employer Portal" />
|
||||
|
||||
<div className="w-full max-w-md bg-white rounded-2xl shadow-sm border border-slate-200 p-8 space-y-6">
|
||||
|
||||
{/* Header Icon & Title */}
|
||||
{step !== 3 && (
|
||||
<div className="text-center space-y-4">
|
||||
<div className="w-16 h-16 bg-blue-50 rounded-full flex items-center justify-center mx-auto border border-blue-200">
|
||||
<KeyRound className="w-8 h-8 text-[#185FA5]" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-2xl font-bold text-gray-900 tracking-tight">
|
||||
{step === 1 ? 'Forgot Password' : 'Reset Password'}
|
||||
</h1>
|
||||
<p className="text-sm text-gray-600">
|
||||
{step === 1
|
||||
? "Enter your registration email address and we'll send you a verification code."
|
||||
: `Enter the 6-digit code sent to ${email} and your new password.`
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-full max-w-md bg-white rounded-2xl shadow-sm border border-slate-200 p-8 text-center space-y-6">
|
||||
<div className="w-16 h-16 bg-blue-50 rounded-full flex items-center justify-center mx-auto border border-blue-200">
|
||||
<KeyRound className="w-8 h-8 text-[#185FA5]" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-2xl font-bold text-gray-900 tracking-tight">Forgot Password</h1>
|
||||
<p className="text-sm text-gray-600">
|
||||
Enter your registration email address and we'll send you a password reset link.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 text-left">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">Email Address</label>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="name@company.com"
|
||||
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]"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Status/Error Messages */}
|
||||
{error && (
|
||||
<div className="flex items-start space-x-3 p-4 bg-red-50 border border-red-200 rounded-xl text-red-800 text-xs font-medium">
|
||||
<AlertTriangle className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="w-full bg-[#185FA5] hover:bg-[#0C447C] text-white rounded-xl h-11 font-semibold text-sm transition-colors shadow-sm"
|
||||
>
|
||||
Send Reset Link
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{successMessage && step === 2 && (
|
||||
<div className="flex items-start space-x-3 p-4 bg-emerald-50 border border-emerald-200 rounded-xl text-emerald-800 text-xs font-medium animate-pulse">
|
||||
<CheckCircle2 className="w-5 h-5 text-emerald-600 flex-shrink-0 mt-0.5" />
|
||||
<span>{successMessage}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 1: Request Code Form */}
|
||||
{step === 1 && (
|
||||
<form onSubmit={handleSendCode} className="space-y-5">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">Email Address</label>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="name@company.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(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]"
|
||||
autoFocus
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
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"
|
||||
>
|
||||
{loading ? <Loader2 className="w-5 h-5 animate-spin mr-2" /> : 'Send Reset Code'}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{/* Step 2: Verification and Reset Form */}
|
||||
{step === 2 && (
|
||||
<form onSubmit={handleResetPassword} className="space-y-5">
|
||||
{/* OTP Input */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">6-Digit Code</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="123456"
|
||||
maxLength={6}
|
||||
value={otp}
|
||||
onChange={(e) => setOtp(e.target.value.replace(/\D/g, ''))}
|
||||
className="w-full px-3.5 py-2.5 rounded-xl border border-slate-300 text-sm font-semibold tracking-wider text-center focus:outline-none focus:ring-2 focus:ring-[#185FA5]/20 focus:border-[#185FA5]"
|
||||
autoFocus
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Password Input */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">New Password</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
placeholder="••••••••"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full pl-3.5 pr-10 py-2.5 rounded-xl border border-slate-300 text-sm focus:outline-none focus:ring-2 focus:ring-[#185FA5]/20 focus:border-[#185FA5]"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute inset-y-0 right-0 pr-3 flex items-center text-slate-400 hover:text-slate-600 focus:outline-none"
|
||||
>
|
||||
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||
</button>
|
||||
</div>
|
||||
<p className="mt-1 text-[10px] text-gray-500 leading-normal">
|
||||
Must be at least 8 characters with 1 uppercase, 1 lowercase, 1 number, and 1 special symbol.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Confirm Password Input */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">Confirm Password</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showConfirmPassword ? 'text' : 'password'}
|
||||
placeholder="••••••••"
|
||||
value={passwordConfirmation}
|
||||
onChange={(e) => setPasswordConfirmation(e.target.value)}
|
||||
className="w-full pl-3.5 pr-10 py-2.5 rounded-xl border border-slate-300 text-sm focus:outline-none focus:ring-2 focus:ring-[#185FA5]/20 focus:border-[#185FA5]"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||
className="absolute inset-y-0 right-0 pr-3 flex items-center text-slate-400 hover:text-slate-600 focus:outline-none"
|
||||
>
|
||||
{showConfirmPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
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"
|
||||
>
|
||||
{loading ? <Loader2 className="w-5 h-5 animate-spin mr-2" /> : 'Reset Password'}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setStep(1);
|
||||
setError('');
|
||||
setSuccessMessage('');
|
||||
}}
|
||||
className="w-full border border-slate-300 hover:bg-slate-50 text-gray-700 rounded-xl h-11 font-semibold text-sm transition-colors shadow-sm flex items-center justify-center"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 mr-2" /> Back
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{/* Step 3: Success Screen */}
|
||||
{step === 3 && (
|
||||
<div className="text-center space-y-6 py-4">
|
||||
<div className="w-16 h-16 bg-emerald-50 rounded-full flex items-center justify-center mx-auto border border-emerald-200">
|
||||
<CheckCircle2 className="w-8 h-8 text-emerald-600" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-2xl font-bold text-gray-900 tracking-tight">Password Reset Success</h2>
|
||||
<p className="text-sm text-gray-600">
|
||||
Your password has been successfully updated. You can now use your new password to sign in.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => router.visit('/employer/login')}
|
||||
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"
|
||||
>
|
||||
Sign In
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer Back Link */}
|
||||
{step !== 3 && (
|
||||
<div className="pt-2 text-center">
|
||||
<Link href="/employer/login" className="text-xs text-[#185FA5] font-bold hover:underline">
|
||||
Return to Sign in
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
<div className="pt-2">
|
||||
<Link href="/employer/login" className="text-xs text-[#185FA5] font-bold hover:underline">
|
||||
Return to Sign in
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -148,23 +148,23 @@ export default function Dashboard({
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Contacted Workers Stat Card */}
|
||||
{/* Pending Offers Stat Card */}
|
||||
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-xs flex flex-col justify-between space-y-4 relative group hover:border-[#185FA5] transition-all">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="w-12 h-12 rounded-xl bg-amber-50 text-amber-600 flex items-center justify-center flex-shrink-0 border border-amber-100 group-hover:scale-105 transition-transform">
|
||||
<MessageSquare className="w-5 h-5" />
|
||||
<Send className="w-5 h-5" />
|
||||
</div>
|
||||
<span className="text-[10px] font-black text-amber-600 bg-amber-50 px-2 py-0.5 rounded-md border border-amber-100">
|
||||
ENGAGED
|
||||
OUTSTANDING
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs font-semibold text-slate-400 uppercase tracking-widest">{t('contacted_workers_title', 'Contacted Workers')}</div>
|
||||
<div className="text-3xl font-black text-slate-800 mt-1">{stats.contacted_workers_count}</div>
|
||||
<div className="text-[11px] text-slate-500 font-medium mt-1">{t('active_conversations_sub', 'Active candidate chat channels')}</div>
|
||||
<div className="text-xs font-semibold text-slate-400 uppercase tracking-widest">{t('pending_job_offers', 'Pending Job Offers')}</div>
|
||||
<div className="text-3xl font-black text-slate-800 mt-1">{stats.pending_offers}</div>
|
||||
<div className="text-[11px] text-slate-500 font-medium mt-1">{t('direct_recruitment', 'Direct recruitment proposals')}</div>
|
||||
</div>
|
||||
<Link href="/employer/messages" className="text-xs font-bold text-[#185FA5] hover:underline flex items-center space-x-1 pt-2 border-t border-slate-100">
|
||||
<span>{t('view_messages', 'Open message center')}</span>
|
||||
<Link href="/employer/candidates" className="text-xs font-bold text-[#185FA5] hover:underline flex items-center space-x-1 pt-2 border-t border-slate-100">
|
||||
<span>{t('view_hiring', 'View hiring workflow')}</span>
|
||||
<ChevronRight className="w-3.5 h-3.5" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@ -420,33 +420,14 @@ export default function Show({ conversation, initialMessages, conversations = []
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-2 space-y-2">
|
||||
<div className="pt-2">
|
||||
<Link
|
||||
href={`/employer/workers/${conversation.worker_id || conversation.id}`}
|
||||
href={`/employer/workers/${conversation.id}`}
|
||||
className="w-full bg-[#185FA5] hover:bg-[#144f8a] text-white py-3 rounded-xl text-xs font-black uppercase tracking-widest flex items-center justify-center space-x-1.5 shadow-xs"
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
<span>{t('open_worker_profile', 'Open Worker Profile')}</span>
|
||||
</Link>
|
||||
|
||||
{conversation.worker_status !== 'hired' && conversation.worker_status !== 'hidden' && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
router.post(`/employer/workers/${conversation.worker_id}/mark-hired`, {}, {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
toast.success(t('marked_hired', 'Worker successfully marked as Hired!'));
|
||||
router.reload();
|
||||
}
|
||||
});
|
||||
}}
|
||||
className="w-full bg-emerald-650 bg-emerald-600 hover:bg-emerald-700 text-white py-3 rounded-xl text-xs font-black uppercase tracking-widest flex items-center justify-center space-x-1.5 shadow-md border border-emerald-700"
|
||||
>
|
||||
<CheckCircle2 className="w-4.5 h-4.5 text-white" />
|
||||
<span>{t('mark_hired_action', 'Mark Hired')}</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@ -1,124 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Reset Your Password</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;">Password Reset</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<h2 class="greeting">Hello {{ $employerName }},</h2>
|
||||
<p class="text">
|
||||
We received a request to reset your password on the Marketplace portal. To verify this request and set a new password, please enter the 6-digit verification code 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> If you did not request a password reset, you can safely ignore this email and your password will remain unchanged. Do not share this passcode with anyone.
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
Empowering Direct Household Hiring • Secure Portal
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -42,11 +42,6 @@
|
||||
Route::get('/employers/plans', [EmployerAuthController::class, 'plans']);
|
||||
Route::get('/sponsors/plans', [EmployerAuthController::class, 'plans']);
|
||||
Route::post('/employers/login', [EmployerAuthController::class, 'login']);
|
||||
Route::post('/employers/forgot-password', [EmployerAuthController::class, 'forgotPassword']);
|
||||
Route::post('/sponsors/forgot-password', [EmployerAuthController::class, 'forgotPassword']);
|
||||
Route::post('/employers/reset-password', [EmployerAuthController::class, 'resetPassword']);
|
||||
Route::post('/sponsors/reset-password', [EmployerAuthController::class, 'resetPassword']);
|
||||
|
||||
|
||||
// Protected Worker Mobile Endpoints (Token Authenticated via Bearer Token)
|
||||
Route::middleware(['auth.worker'])->group(function () {
|
||||
@ -75,10 +70,9 @@
|
||||
|
||||
// Protected Employer Mobile Endpoints (Token Authenticated via Bearer Token)
|
||||
Route::middleware(['auth.employer'])->group(function () {
|
||||
// Profile & Dashboard Management
|
||||
// Profile Management
|
||||
Route::get('/employers/profile', [EmployerProfileController::class, 'getProfile']);
|
||||
Route::post('/employers/profile/update', [EmployerProfileController::class, 'updateProfile']);
|
||||
Route::get('/employers/dashboard', [EmployerProfileController::class, 'getDashboard']);
|
||||
|
||||
// Messaging Management
|
||||
Route::get('/employers/conversations', [EmployerMessageController::class, 'getConversations']);
|
||||
@ -98,15 +92,10 @@
|
||||
Route::post('/employers/candidates/{id}/hire', [\App\Http\Controllers\Api\EmployerWorkerController::class, 'hireCandidate']);
|
||||
Route::post('/employers/candidates/hire', [\App\Http\Controllers\Api\EmployerWorkerController::class, 'hireCandidate']);
|
||||
|
||||
// Shortlist Management
|
||||
Route::get('/employers/shortlist', [\App\Http\Controllers\Api\EmployerWorkerController::class, 'getShortlist']);
|
||||
Route::post('/employers/shortlist', [\App\Http\Controllers\Api\EmployerWorkerController::class, 'toggleShortlist']);
|
||||
|
||||
// Payment History Management
|
||||
Route::get('/employers/payments', [\App\Http\Controllers\Api\EmployerPaymentController::class, 'getPayments']);
|
||||
|
||||
// Review Management
|
||||
Route::get('/employers/reviews', [EmployerReviewController::class, 'getReviews']);
|
||||
Route::post('/employers/reviews', [EmployerReviewController::class, 'addReview']);
|
||||
Route::put('/employers/reviews/{id}', [EmployerReviewController::class, 'editReview']);
|
||||
});
|
||||
|
||||
@ -92,8 +92,6 @@
|
||||
Route::get('/employer/forgot-password', function () {
|
||||
return Inertia::render('Employer/Auth/ForgotPassword');
|
||||
})->name('employer.forgot-password');
|
||||
Route::post('/employer/forgot-password', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'forgotPassword'])->name('employer.forgot-password.submit');
|
||||
Route::post('/employer/reset-password', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'resetPassword'])->name('employer.reset-password.submit');
|
||||
|
||||
// Employer Protected Routes
|
||||
Route::prefix('employer')->middleware(['employer'])->group(function () {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user