1418 lines
60 KiB
PHP
1418 lines
60 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Worker;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Illuminate\Support\Facades\Mail;
|
|
use Illuminate\Support\Facades\Validator;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\Validation\Rule;
|
|
|
|
class WorkerAuthController extends Controller
|
|
{
|
|
/**
|
|
* Get system configuration and supported languages for worker onboarding.
|
|
* Aligns with Step 2 "Select Language (HI / SW / TL / TA)".
|
|
*/
|
|
public function config()
|
|
{
|
|
return response()->json([
|
|
'success' => true,
|
|
'languages' => [
|
|
['code' => 'HI', 'name' => 'Hindi (हिन्दी)'],
|
|
['code' => 'SW', 'name' => 'Swahili (Kiswahili)'],
|
|
['code' => 'TL', 'name' => 'Tagalog (Wikang Tagalog)'],
|
|
['code' => 'TA', 'name' => 'Tamil (தமிழ்)'],
|
|
]
|
|
], 200);
|
|
}
|
|
|
|
/**
|
|
* Get all master skills for worker registration/onboarding.
|
|
*/
|
|
public function skills()
|
|
{
|
|
$skills = \App\Models\Skill::all()->map(function ($skill) {
|
|
return [
|
|
'id' => $skill->id,
|
|
'name' => $skill->name,
|
|
'image_url' => $skill->image_path ? asset('storage/' . $skill->image_path) : null,
|
|
];
|
|
});
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'skills' => $skills
|
|
], 200);
|
|
}
|
|
|
|
/**
|
|
* Step 1: Send OTP to worker's mobile number or email.
|
|
* Accepts: phone OR email (one is required).
|
|
*/
|
|
public function sendOtp(Request $request)
|
|
{
|
|
$validator = Validator::make($request->all(), [
|
|
'phone' => 'required_without:email|nullable|string|max:50|unique:workers,phone',
|
|
'email' => 'required_without:phone|nullable|email|max:255|unique:workers,email',
|
|
], [
|
|
'phone.required_without' => 'Please provide a mobile number or email address.',
|
|
'phone.unique' => 'This mobile number is already registered.',
|
|
'email.required_without' => 'Please provide a mobile number or email address.',
|
|
'email.unique' => 'This email address is already registered.',
|
|
]);
|
|
|
|
if ($validator->fails()) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Validation error.',
|
|
'errors' => $validator->errors()
|
|
], 422);
|
|
}
|
|
|
|
// Determine identifier (phone takes priority)
|
|
$identifier = $request->phone ?? $request->email;
|
|
$isPhone = !empty($request->phone);
|
|
|
|
// Generate OTP (always 111111 in local/dev, random in production)
|
|
$otp = app()->environment('production') ? strval(rand(100000, 999999)) : '111111';
|
|
|
|
// Cache OTP for 10 minutes
|
|
Cache::put('worker_otp_' . $identifier, [
|
|
'code' => $otp,
|
|
'expires_at' => now()->addMinutes(10)->timestamp,
|
|
], 600);
|
|
|
|
// Send OTP via email if email provided (phone SMS would be handled by SMS gateway)
|
|
if (!$isPhone && $request->email) {
|
|
try {
|
|
Mail::raw("Your Migrant Worker verification code is: {$otp}\n\nThis code expires in 10 minutes.", function ($m) use ($request, $otp) {
|
|
$m->to($request->email)
|
|
->subject('Your Migrant Worker Verification Code');
|
|
});
|
|
} catch (\Exception $mailEx) {
|
|
logger()->error('Worker OTP Mail Failure: ' . $mailEx->getMessage());
|
|
}
|
|
}
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => $isPhone
|
|
? 'OTP sent to your mobile number.'
|
|
: 'OTP sent to your email address.',
|
|
'identifier' => $identifier,
|
|
'otp' => app()->environment('production') ? null : $otp, // Return OTP in response in non-prod for testing convenience
|
|
], 200);
|
|
}
|
|
|
|
/**
|
|
* Step 2: Verify OTP sent to mobile/email.
|
|
* Accepts: phone OR email, otp.
|
|
*/
|
|
public function verifyOtp(Request $request)
|
|
{
|
|
$validator = Validator::make($request->all(), [
|
|
'phone' => 'required_without:email|nullable|string|max:50',
|
|
'email' => 'required_without:phone|nullable|email|max:255',
|
|
'otp' => 'required|string|size:6',
|
|
]);
|
|
|
|
if ($validator->fails()) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'errors' => $validator->errors()
|
|
], 422);
|
|
}
|
|
|
|
$identifier = $request->phone ?? $request->email;
|
|
$cachedData = Cache::get('worker_otp_' . $identifier);
|
|
|
|
if (!$cachedData || !isset($cachedData['code']) || !isset($cachedData['expires_at'])) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Invalid or expired verification code.'
|
|
], 400);
|
|
}
|
|
|
|
// Handle any dirty/legacy cache objects gracefully
|
|
if (is_object($cachedData['expires_at']) || $cachedData['expires_at'] instanceof \__PHP_Incomplete_Class) {
|
|
Cache::forget('worker_otp_' . $identifier);
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Session expired. Please request a new 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);
|
|
}
|
|
|
|
// Mark OTP as verified in cache (used as gate for profile setup step)
|
|
Cache::put('worker_otp_verified_' . $identifier, true, 3600); // 1 hour grace
|
|
Cache::forget('worker_otp_' . $identifier);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => 'Mobile number verified successfully. Please complete your profile.',
|
|
'identifier' => $identifier,
|
|
], 200);
|
|
}
|
|
|
|
/**
|
|
* Step 3: Complete basic profile setup after OTP verification.
|
|
* Accepts: phone OR email, name, nationality, skills (array of skill IDs), language.
|
|
*/
|
|
public function setupProfile(Request $request)
|
|
{
|
|
$validator = Validator::make($request->all(), [
|
|
'phone' => 'required_without:email|nullable|string|max:50',
|
|
'email' => 'required_without:phone|nullable|email|max:255',
|
|
'name' => 'required|string|max:255',
|
|
'nationality' => 'required|string|max:100',
|
|
'language' => 'nullable|string',
|
|
'gender' => 'nullable|string|in:male,female,other',
|
|
'live_in_out' => 'nullable|string|in:live_in,live_out',
|
|
'preferred_location' => 'nullable|string|max:255',
|
|
'main_profession' => 'nullable|string|max:100',
|
|
'skills' => 'nullable|array',
|
|
'skills.*' => 'integer|exists:skills,id',
|
|
'fcm_token' => 'nullable|string|max:255',
|
|
]);
|
|
|
|
if ($validator->fails()) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'errors' => $validator->errors()
|
|
], 422);
|
|
}
|
|
|
|
$identifier = $request->phone ?? $request->email;
|
|
|
|
// Ensure OTP was verified in this session
|
|
if (!Cache::get('worker_otp_verified_' . $identifier)) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'OTP verification is required before profile setup.'
|
|
], 403);
|
|
}
|
|
|
|
// Check if already registered
|
|
if ($request->phone && Worker::where('phone', $request->phone)->exists()) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'This mobile number is already registered.'
|
|
], 409);
|
|
}
|
|
if ($request->email && Worker::where('email', $request->email)->exists()) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'This email address is already registered.'
|
|
], 409);
|
|
}
|
|
|
|
try {
|
|
// Build email for workers who registered by phone
|
|
$email = $request->email;
|
|
if (!$email && $request->phone) {
|
|
$phoneClean = preg_replace('/[^0-9]/', '', $request->phone);
|
|
$email = "worker.{$phoneClean}@migrant.ae";
|
|
if (Worker::where('email', $email)->exists()) {
|
|
$email = "worker.{$phoneClean}." . rand(10, 99) . "@migrant.ae";
|
|
}
|
|
}
|
|
|
|
$apiToken = Str::random(80);
|
|
|
|
$worker = DB::transaction(function () use ($request, $email, $apiToken) {
|
|
$worker = Worker::create([
|
|
'name' => $request->name,
|
|
'email' => $email,
|
|
'phone' => $request->phone ?? null,
|
|
'language' => $request->language ?? 'HI', // Default to Hindi if not selected
|
|
'password' => Hash::make(Str::random(16)), // No password at this stage
|
|
'nationality' => $request->nationality,
|
|
'gender' => $request->gender,
|
|
'live_in_out' => $request->live_in_out,
|
|
'preferred_location' => $request->preferred_location,
|
|
'main_profession' => $request->main_profession,
|
|
'age' => 25, // Default — updated later in full profile
|
|
'salary' => 1500, // Default AED — updated later
|
|
'availability' => 'Immediate',
|
|
'experience' => 'Not Specified',
|
|
'religion' => 'Not Specified',
|
|
'bio' => 'New worker on Migrant platform.',
|
|
'verified' => false,
|
|
'status' => 'active',
|
|
'api_token' => $apiToken,
|
|
'fcm_token' => $request->fcm_token,
|
|
]);
|
|
|
|
// Sync skills if provided
|
|
if (!empty($request->skills)) {
|
|
$worker->skills()->sync($request->skills);
|
|
}
|
|
|
|
return $worker;
|
|
});
|
|
|
|
// Clear the OTP verification gate
|
|
Cache::forget('worker_otp_verified_' . $identifier);
|
|
|
|
$worker->load(['skills']);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => 'Registration complete! Welcome to Migrant.',
|
|
'data' => [
|
|
'worker' => $worker,
|
|
'token' => $apiToken,
|
|
]
|
|
], 201);
|
|
|
|
} catch (\Exception $e) {
|
|
logger()->error('Worker Profile Setup Failure: ' . $e->getMessage());
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Registration failed. Please try again.',
|
|
'error' => app()->environment('local', 'testing') ? $e->getMessage() : 'Internal Server Error'
|
|
], 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Unified registration — supports direct JSON payloads for passport and visa.
|
|
*/
|
|
public function register(Request $request)
|
|
{
|
|
$data = $request->all();
|
|
if (isset($data['passport']) && is_string($data['passport'])) {
|
|
$decoded = json_decode($data['passport'], true);
|
|
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
|
|
$data['passport'] = $decoded;
|
|
}
|
|
}
|
|
if (isset($data['visa']) && is_string($data['visa'])) {
|
|
$decoded = json_decode($data['visa'], true);
|
|
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
|
|
$data['visa'] = $decoded;
|
|
}
|
|
}
|
|
$request->merge($data);
|
|
|
|
$validator = Validator::make($request->all(), [
|
|
'name' => 'required|string|max:255',
|
|
'phone' => 'required|string|max:50|unique:workers,phone',
|
|
'salary' => 'required|numeric|min:0',
|
|
'password' => 'required|string|min:6',
|
|
'skills' => 'nullable',
|
|
'language' => 'nullable|string',
|
|
'nationality' => 'nullable|string|max:100',
|
|
'age' => 'nullable|integer',
|
|
'experience' => 'nullable|string|max:100',
|
|
'in_country' => 'nullable',
|
|
'visa_status' => 'nullable|string|max:100',
|
|
'preferred_job_type' => 'nullable|string|max:100',
|
|
'main_profession' => 'nullable|string|max:100',
|
|
'gender' => 'nullable|string|in:male,female,other',
|
|
'live_in_out' => 'nullable|string|in:live_in,live_out',
|
|
'preferred_location' => 'nullable|string|max:255',
|
|
'fcm_token' => 'nullable|string|max:255',
|
|
'passport' => 'nullable|array',
|
|
'passport.passport_number' => [
|
|
'nullable',
|
|
'string',
|
|
Rule::unique('worker_documents', 'number')->where(function ($query) {
|
|
return $query->where('type', 'passport');
|
|
}),
|
|
],
|
|
'passport.document_type' => 'nullable|string|max:10',
|
|
'passport.issuing_country' => 'nullable|string|max:10',
|
|
'passport.surname' => 'nullable|string|max:255',
|
|
'passport.given_names' => 'nullable|string|max:255',
|
|
'passport.nationality' => 'nullable|string|max:100',
|
|
'passport.date_of_birth' => 'nullable|string',
|
|
'passport.date_of_expiry' => 'nullable|string',
|
|
'passport.place_of_birth' => 'nullable|string|max:255',
|
|
'passport.authority' => 'nullable|string|max:255',
|
|
'passport.sex' => 'nullable|string|max:10',
|
|
'passport.date_of_issue' => 'nullable|string',
|
|
'passport.ocr_accuracy' => 'nullable|numeric',
|
|
'visa' => 'nullable|array',
|
|
'visa.document_type' => 'nullable|string|max:100',
|
|
'visa.country' => 'nullable|string|max:100',
|
|
'visa.visa_type' => 'nullable|string|max:100',
|
|
'visa.entry_permit_no' => 'nullable|string|max:100',
|
|
'visa.issue_date' => 'nullable|string',
|
|
'visa.valid_until' => 'nullable|string',
|
|
'visa.uid_no' => 'nullable|string|max:100',
|
|
'visa.full_name' => 'nullable|string|max:255',
|
|
'visa.nationality' => 'nullable|string|max:100',
|
|
'visa.place_of_birth' => 'nullable|string|max:255',
|
|
'visa.date_of_birth' => 'nullable|string',
|
|
'visa.passport_no' => 'nullable|string|max:100',
|
|
'visa.profession' => 'nullable|string|max:255',
|
|
'visa.sponsor_name' => 'nullable|string|max:255',
|
|
'visa.ocr_accuracy' => 'nullable|numeric',
|
|
], [
|
|
'phone.unique' => 'This mobile number is already registered.',
|
|
'passport.passport_number.unique' => 'This passport number is already registered.',
|
|
]);
|
|
|
|
if ($validator->fails()) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Validation error.',
|
|
'errors' => $validator->errors()
|
|
], 422);
|
|
}
|
|
|
|
try {
|
|
$phoneClean = preg_replace('/[^0-9]/', '', $request->phone);
|
|
$email = "worker.{$phoneClean}@migrant.ae";
|
|
if (Worker::where('email', $email)->exists()) {
|
|
$email = "worker.{$phoneClean}." . rand(10, 99) . "@migrant.ae";
|
|
}
|
|
|
|
$skillsArray = [];
|
|
if ($request->has('skills')) {
|
|
$skillsInput = $request->skills;
|
|
if (is_array($skillsInput)) {
|
|
$skillsArray = $skillsInput;
|
|
} elseif (is_string($skillsInput)) {
|
|
$decoded = json_decode($skillsInput, true);
|
|
$skillsArray = (json_last_error() === JSON_ERROR_NONE && is_array($decoded))
|
|
? $decoded
|
|
: array_filter(array_map('trim', explode(',', $skillsInput)));
|
|
}
|
|
}
|
|
|
|
$inCountry = filter_var($request->input('in_country', true), FILTER_VALIDATE_BOOLEAN);
|
|
$apiToken = Str::random(80);
|
|
|
|
$result = DB::transaction(function () use ($request, $email, $skillsArray, $apiToken, $inCountry) {
|
|
// Initialize fields
|
|
$name = $request->name;
|
|
$nationality = $request->nationality ?? 'Not Specified';
|
|
$age = $request->age ?? 25;
|
|
$visaStatus = $inCountry ? $request->visa_status : null;
|
|
|
|
$passportDataInput = $request->input('passport');
|
|
$visaDataInput = $request->input('visa');
|
|
|
|
// Process passport if provided
|
|
if ($passportDataInput) {
|
|
$givenNames = trim($passportDataInput['given_names'] ?? '');
|
|
$surname = trim($passportDataInput['surname'] ?? '');
|
|
$fullName = trim($givenNames . ' ' . $surname);
|
|
if (empty($fullName)) {
|
|
$fullName = $passportDataInput['name'] ?? null;
|
|
}
|
|
if ($fullName) {
|
|
$name = $fullName;
|
|
}
|
|
|
|
if (!empty($passportDataInput['nationality'])) {
|
|
$codeUpper = strtoupper($passportDataInput['nationality']);
|
|
$matchedNationality = \App\Models\Nationality::where('iso3', $codeUpper)
|
|
->orWhere('code', $codeUpper)
|
|
->first();
|
|
$nationality = $matchedNationality ? $matchedNationality->name : $passportDataInput['nationality'];
|
|
}
|
|
|
|
if (!empty($passportDataInput['date_of_birth'])) {
|
|
try {
|
|
$dob = new \DateTime($this->normaliseDateForController($passportDataInput['date_of_birth']));
|
|
$age = (new \DateTime())->diff($dob)->y;
|
|
} catch (\Exception $dateEx) {
|
|
logger()->error('Passport DOB parse error in register: ' . $dateEx->getMessage());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process visa if provided
|
|
if ($visaDataInput) {
|
|
if (!empty($visaDataInput['profession'])) {
|
|
if (empty($visaStatus)) {
|
|
$visaStatus = $visaDataInput['profession'];
|
|
}
|
|
}
|
|
}
|
|
|
|
$worker = Worker::create([
|
|
'name' => $name,
|
|
'email' => $email,
|
|
'phone' => $request->phone,
|
|
'language' => $request->language ?? 'HI',
|
|
'password' => Hash::make($request->password),
|
|
'nationality' => $nationality,
|
|
'gender' => $request->gender,
|
|
'live_in_out' => $request->live_in_out,
|
|
'preferred_location' => $request->preferred_location,
|
|
'main_profession' => $request->main_profession,
|
|
'age' => $age,
|
|
'salary' => $request->salary,
|
|
'availability' => 'Immediate',
|
|
'experience' => $request->experience ?? 'Not Specified',
|
|
'religion' => 'Not Specified',
|
|
'bio' => 'Hardworking and reliable General Helper available for immediate hire.',
|
|
'verified' => ($passportDataInput || $visaDataInput) ? true : false,
|
|
'status' => 'active',
|
|
'api_token' => $apiToken,
|
|
'in_country' => $inCountry,
|
|
'visa_status' => $visaStatus,
|
|
'preferred_job_type' => $request->preferred_job_type,
|
|
'fcm_token' => $request->fcm_token,
|
|
]);
|
|
|
|
if (!empty($skillsArray)) {
|
|
$validSkillIds = DB::table('skills')->whereIn('id', $skillsArray)->pluck('id')->toArray();
|
|
if (!empty($validSkillIds)) {
|
|
$worker->skills()->sync($validSkillIds);
|
|
}
|
|
}
|
|
|
|
// Create passport document if provided
|
|
if ($passportDataInput) {
|
|
$passportNum = $passportDataInput['passport_number'] ?? ('P' . rand(1000000, 9999999));
|
|
$expiryDate = $passportDataInput['date_of_expiry'] ?? null;
|
|
$issueDate = $passportDataInput['date_of_issue'] ?? null;
|
|
|
|
$expiryDateFormatted = $expiryDate ? $this->normaliseDateForController($expiryDate) : now()->addYears(8)->toDateString();
|
|
$issueDateFormatted = $issueDate ? $this->normaliseDateForController($issueDate) : now()->subYears(3)->toDateString();
|
|
|
|
$worker->documents()->create([
|
|
'type' => 'passport',
|
|
'number' => $passportNum,
|
|
'issue_date' => $issueDateFormatted,
|
|
'expiry_date' => $expiryDateFormatted,
|
|
'ocr_accuracy' => isset($passportDataInput['ocr_accuracy']) ? (float)$passportDataInput['ocr_accuracy'] : 99.0,
|
|
'file_path' => null,
|
|
'ocr_data' => $passportDataInput,
|
|
]);
|
|
}
|
|
|
|
// Create visa document if provided
|
|
if ($visaDataInput) {
|
|
$cleanedVisaOcr = $this->cleanVisaData($visaDataInput);
|
|
$visaNum = $cleanedVisaOcr['entry_permit_no'] ?: ('V' . rand(1000000, 9999999));
|
|
$expiryDate = $cleanedVisaOcr['valid_until'] ?? null;
|
|
$issueDate = $cleanedVisaOcr['issue_date'] ?? null;
|
|
|
|
$expiryDateFormatted = $expiryDate ? $this->normaliseDateForController($expiryDate) : now()->addYears(2)->toDateString();
|
|
$issueDateFormatted = $issueDate ? $this->normaliseDateForController($issueDate) : now()->subYears(2)->toDateString();
|
|
|
|
$worker->documents()->create([
|
|
'type' => 'visa',
|
|
'number' => $visaNum,
|
|
'issue_date' => $issueDateFormatted,
|
|
'expiry_date' => $expiryDateFormatted,
|
|
'ocr_accuracy' => isset($visaDataInput['ocr_accuracy']) ? (float)$visaDataInput['ocr_accuracy'] : 99.0,
|
|
'file_path' => null,
|
|
'ocr_data' => $cleanedVisaOcr,
|
|
]);
|
|
}
|
|
|
|
return $worker;
|
|
});
|
|
|
|
$result->load(['skills', 'documents']);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => 'Worker registered successfully.',
|
|
'data' => ['worker' => $result, 'token' => $apiToken]
|
|
], 201);
|
|
|
|
} catch (\Exception $e) {
|
|
logger()->error('Worker Registration Failure: ' . $e->getMessage());
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Registration failed.',
|
|
'error' => app()->environment('local', 'testing') ? $e->getMessage() : 'Internal Server Error'
|
|
], 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Authenticate a worker and return a secure Bearer token.
|
|
*/public function login(Request $request)
|
|
{
|
|
$validator = Validator::make($request->all(), [
|
|
'phone' => 'required_without:email|nullable|string',
|
|
'email' => 'required_without:phone|nullable|email',
|
|
'password' => 'required|string',
|
|
'fcm_token' => 'nullable|string|max:255',
|
|
]);
|
|
|
|
if ($validator->fails()) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Validation error.',
|
|
'errors' => $validator->errors()
|
|
], 422);
|
|
}
|
|
|
|
try {
|
|
$worker = $request->phone
|
|
? Worker::where('phone', $request->phone)->first()
|
|
: Worker::where('email', $request->email)->first();
|
|
|
|
if (!$worker || !Hash::check($request->password, $worker->password)) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Invalid credentials.'
|
|
], 401);
|
|
}
|
|
|
|
$apiToken = Str::random(80);
|
|
$updateData = ['api_token' => $apiToken];
|
|
if ($request->has('fcm_token')) {
|
|
$updateData['fcm_token'] = $request->fcm_token;
|
|
}
|
|
$worker->update($updateData);
|
|
|
|
if ($request->filled('fcm_token')) {
|
|
\App\Services\FCMService::sendPushNotification(
|
|
$request->fcm_token,
|
|
'Successful Login',
|
|
'Worker logged in successfully.'
|
|
);
|
|
}
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => 'Worker logged in successfully.',
|
|
'data' => [
|
|
'worker' => $worker->load(['skills', 'documents']),
|
|
'token' => $apiToken
|
|
]
|
|
], 200);
|
|
|
|
} catch (\Exception $e) {
|
|
logger()->error('Worker Login Failure: ' . $e->getMessage());
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'An error occurred during login. Please try again.',
|
|
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
|
], 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get list of nationalities translated to requested language.
|
|
* Supports en, hindi (hi), sawahi (sw), taglo (tl), tamil (ta).
|
|
*/
|
|
public function nationalities(Request $request)
|
|
{
|
|
$lang = strtolower($request->input('lang') ?? $request->header('Accept-Language') ?? 'en');
|
|
if (str_contains($lang, ',')) {
|
|
$lang = explode(',', $lang)[0];
|
|
}
|
|
if (str_contains($lang, '-')) {
|
|
$lang = explode('-', $lang)[0];
|
|
}
|
|
|
|
$langMap = [
|
|
'hindi' => 'hi',
|
|
'sawahi' => 'sw',
|
|
'swahili' => 'sw',
|
|
'taglo' => 'tl',
|
|
'tagalog' => 'tl',
|
|
'tamil' => 'ta',
|
|
];
|
|
|
|
if (isset($langMap[$lang])) {
|
|
$lang = $langMap[$lang];
|
|
}
|
|
|
|
if (!in_array($lang, ['en', 'hi', 'sw', 'tl', 'ta'])) {
|
|
$lang = 'en';
|
|
}
|
|
|
|
$rawNationalities = \App\Models\Nationality::all();
|
|
|
|
$callingCodes = [
|
|
'AF' => '+93',
|
|
'AL' => '+355',
|
|
'DZ' => '+213',
|
|
'AD' => '+376',
|
|
'AO' => '+244',
|
|
'AG' => '+1-268',
|
|
'AR' => '+54',
|
|
'AM' => '+374',
|
|
'AU' => '+61',
|
|
'AT' => '+43',
|
|
'AZ' => '+994',
|
|
'BS' => '+1-242',
|
|
'BH' => '+973',
|
|
'BD' => '+880',
|
|
'BB' => '+1-246',
|
|
'BY' => '+375',
|
|
'BE' => '+32',
|
|
'BZ' => '+501',
|
|
'BJ' => '+229',
|
|
'BT' => '+975',
|
|
'BO' => '+591',
|
|
'BA' => '+387',
|
|
'BW' => '+267',
|
|
'BR' => '+55',
|
|
'BN' => '+673',
|
|
'BG' => '+359',
|
|
'BF' => '+226',
|
|
'BI' => '+257',
|
|
'KH' => '+855',
|
|
'CM' => '+237',
|
|
'CA' => '+1',
|
|
'CV' => '+238',
|
|
'CF' => '+236',
|
|
'TD' => '+235',
|
|
'CL' => '+56',
|
|
'CN' => '+86',
|
|
'CO' => '+57',
|
|
'KM' => '+269',
|
|
'CG' => '+242',
|
|
'CD' => '+243',
|
|
'CR' => '+506',
|
|
'HR' => '+385',
|
|
'CU' => '+53',
|
|
'CY' => '+357',
|
|
'CZ' => '+420',
|
|
'DK' => '+45',
|
|
'DJ' => '+253',
|
|
'DM' => '+1-767',
|
|
'DO' => '+1-809',
|
|
'TL' => '+670',
|
|
'EC' => '+593',
|
|
'EG' => '+20',
|
|
'SV' => '+503',
|
|
'GQ' => '+240',
|
|
'ER' => '+291',
|
|
'EE' => '+372',
|
|
'ET' => '+251',
|
|
'FJ' => '+679',
|
|
'FI' => '+358',
|
|
'FR' => '+33',
|
|
'GA' => '+241',
|
|
'GM' => '+220',
|
|
'GE' => '+995',
|
|
'DE' => '+49',
|
|
'GH' => '+233',
|
|
'GR' => '+30',
|
|
'GD' => '+1-473',
|
|
'GT' => '+502',
|
|
'GN' => '+224',
|
|
'GW' => '+245',
|
|
'GY' => '+592',
|
|
'HT' => '+509',
|
|
'HN' => '+504',
|
|
'HU' => '+36',
|
|
'IS' => '+354',
|
|
'IN' => '+91',
|
|
'ID' => '+62',
|
|
'IR' => '+98',
|
|
'IQ' => '+964',
|
|
'IE' => '+353',
|
|
'IL' => '+972',
|
|
'IT' => '+39',
|
|
'JM' => '+1-876',
|
|
'JP' => '+81',
|
|
'JO' => '+962',
|
|
'KZ' => '+7',
|
|
'KE' => '+254',
|
|
'KI' => '+686',
|
|
'KP' => '+850',
|
|
'KR' => '+82',
|
|
'KW' => '+965',
|
|
'KG' => '+996',
|
|
'LA' => '+856',
|
|
'LV' => '+371',
|
|
'LB' => '+961',
|
|
'LS' => '+266',
|
|
'LR' => '+231',
|
|
'LY' => '+218',
|
|
'LI' => '+423',
|
|
'LT' => '+370',
|
|
'LU' => '+352',
|
|
'MK' => '+389',
|
|
'MG' => '+261',
|
|
'MW' => '+265',
|
|
'MY' => '+60',
|
|
'MV' => '+960',
|
|
'ML' => '+223',
|
|
'MT' => '+356',
|
|
'MH' => '+692',
|
|
'MR' => '+222',
|
|
'MU' => '+230',
|
|
'MX' => '+52',
|
|
'FM' => '+691',
|
|
'MD' => '+373',
|
|
'MC' => '+377',
|
|
'MN' => '+976',
|
|
'ME' => '+382',
|
|
'MA' => '+212',
|
|
'MZ' => '+258',
|
|
'MM' => '+95',
|
|
'NA' => '+264',
|
|
'NR' => '+674',
|
|
'NP' => '+977',
|
|
'NL' => '+31',
|
|
'NZ' => '+64',
|
|
'NI' => '+505',
|
|
'NE' => '+227',
|
|
'NG' => '+234',
|
|
'NO' => '+47',
|
|
'OM' => '+968',
|
|
'PK' => '+92',
|
|
'PW' => '+680',
|
|
'PA' => '+507',
|
|
'PG' => '+675',
|
|
'PY' => '+595',
|
|
'PE' => '+51',
|
|
'PH' => '+63',
|
|
'PL' => '+48',
|
|
'PT' => '+351',
|
|
'QA' => '+974',
|
|
'RO' => '+40',
|
|
'RU' => '+7',
|
|
'RW' => '+250',
|
|
'KN' => '+1-869',
|
|
'LC' => '+1-758',
|
|
'VC' => '+1-784',
|
|
'WS' => '+685',
|
|
'SM' => '+378',
|
|
'ST' => '+239',
|
|
'SA' => '+966',
|
|
'SN' => '+221',
|
|
'RS' => '+381',
|
|
'SC' => '+248',
|
|
'SL' => '+232',
|
|
'SG' => '+65',
|
|
'SK' => '+421',
|
|
'SI' => '+386',
|
|
'SB' => '+677',
|
|
'SO' => '+252',
|
|
'ZA' => '+27',
|
|
'SS' => '+211',
|
|
'ES' => '+34',
|
|
'LK' => '+94',
|
|
'SD' => '+249',
|
|
'SR' => '+597',
|
|
'SZ' => '+268',
|
|
'SE' => '+46',
|
|
'CH' => '+41',
|
|
'SY' => '+963',
|
|
'TJ' => '+992',
|
|
'TZ' => '+255',
|
|
'TH' => '+66',
|
|
'TG' => '+228',
|
|
'TO' => '+676',
|
|
'TT' => '+1-868',
|
|
'TN' => '+216',
|
|
'TR' => '+90',
|
|
'TM' => '+993',
|
|
'TV' => '+688',
|
|
'UG' => '+256',
|
|
'UA' => '+380',
|
|
'AE' => '+971',
|
|
'GB' => '+44',
|
|
'US' => '+1',
|
|
'UY' => '+598',
|
|
'UZ' => '+998',
|
|
'VU' => '+678',
|
|
'VE' => '+58',
|
|
'VN' => '+84',
|
|
'YE' => '+967',
|
|
'ZM' => '+260',
|
|
'ZW' => '+263',
|
|
'AI' => '+1-264',
|
|
'VG' => '+1-284',
|
|
'BM' => '+1-441',
|
|
'KY' => '+1-345',
|
|
'CK' => '+682',
|
|
'CY-W' => '+44',
|
|
'CY-M' => '+44',
|
|
'FO' => '+298',
|
|
'GI' => '+350',
|
|
'GL' => '+299',
|
|
'GU' => '+1-671',
|
|
'HK' => '+852',
|
|
'XK' => '+383',
|
|
'MO' => '+853',
|
|
'MQ' => '+596',
|
|
'MS' => '+1-664',
|
|
'NU' => '+683',
|
|
'GB-NIR' => '+44',
|
|
'PS' => '+970',
|
|
'PN' => '+64',
|
|
'PR' => '+1-787',
|
|
'GB-SCT' => '+44',
|
|
'SH' => '+290',
|
|
'ST-L' => '',
|
|
'TA-T' => '+290',
|
|
'TC' => '+1-649',
|
|
'VA' => '+379',
|
|
'WF' => '+681',
|
|
'GB-WLS' => '+44',
|
|
];
|
|
|
|
$list = [];
|
|
foreach ($rawNationalities as $item) {
|
|
$list[] = [
|
|
'code' => $item->code,
|
|
'country_code' => $item->code,
|
|
'phone_code' => $callingCodes[$item->code] ?? '',
|
|
'dial_code' => $callingCodes[$item->code] ?? '',
|
|
'calling_code' => $callingCodes[$item->code] ?? '',
|
|
'name' => $item->{$lang} ?? $item->name,
|
|
];
|
|
}
|
|
|
|
$search = strtolower($request->input('search') ?? $request->input('q') ?? '');
|
|
if ($search !== '') {
|
|
$list = array_filter($list, function ($item) use ($search) {
|
|
return stripos($item['name'], $search) !== false || stripos($item['code'], $search) !== false;
|
|
});
|
|
$list = array_values($list);
|
|
}
|
|
|
|
// Apply Pagination
|
|
$page = (int) $request->input('page', 1);
|
|
$perPage = (int) $request->input('per_page', 500);
|
|
if ($perPage === 15) {
|
|
$perPage = 500;
|
|
}
|
|
$total = count($list);
|
|
$offset = ($page - 1) * $perPage;
|
|
$paginatedList = array_slice($list, $offset, $perPage);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => [
|
|
'nationalities' => $paginatedList,
|
|
'pagination' => [
|
|
'total' => $total,
|
|
'per_page' => $perPage,
|
|
'current_page' => $page,
|
|
'last_page' => max(1, (int) ceil($total / $perPage)),
|
|
]
|
|
],
|
|
], 200);
|
|
}
|
|
|
|
/**
|
|
* Get list of preferred locations translated to requested language.
|
|
* Supports en, hindi (hi), sawahi (sw), taglo (tl), tamil (ta).
|
|
*/
|
|
public function preferredLocations(Request $request)
|
|
{
|
|
$lang = strtolower($request->input('lang') ?? $request->header('Accept-Language') ?? 'en');
|
|
if (str_contains($lang, ',')) {
|
|
$lang = explode(',', $lang)[0];
|
|
}
|
|
if (str_contains($lang, '-')) {
|
|
$lang = explode('-', $lang)[0];
|
|
}
|
|
|
|
$langMap = [
|
|
'hindi' => 'hi',
|
|
'sawahi' => 'sw',
|
|
'swahili' => 'sw',
|
|
'taglo' => 'tl',
|
|
'tagalog' => 'tl',
|
|
'tamil' => 'ta',
|
|
];
|
|
|
|
if (isset($langMap[$lang])) {
|
|
$lang = $langMap[$lang];
|
|
}
|
|
|
|
if (!in_array($lang, ['en', 'hi', 'sw', 'tl', 'ta'])) {
|
|
$lang = 'en';
|
|
}
|
|
|
|
$rawLocations = [
|
|
['code' => 'dubai', 'name' => 'Dubai', 'hi' => 'दुबई', 'sw' => 'Dubai', 'tl' => 'Dubai', 'ta' => 'துபாய்'],
|
|
['code' => 'abu dhabi', 'name' => 'Abu Dhabi', 'hi' => 'अबू धाबी', 'sw' => 'Abu Dhabi', 'tl' => 'Abu Dhabi', 'ta' => 'அபுதாபி'],
|
|
['code' => 'oman', 'name' => 'Oman', 'hi' => 'ओमान', 'sw' => 'Omani', 'tl' => 'Oman', 'ta' => 'ஓமன்']
|
|
];
|
|
|
|
$list = [];
|
|
foreach ($rawLocations as $item) {
|
|
$list[] = [
|
|
'code' => $item['code'],
|
|
'name' => $item[$lang] ?? $item['name'],
|
|
];
|
|
}
|
|
|
|
$search = strtolower($request->input('search') ?? $request->input('q') ?? '');
|
|
if ($search !== '') {
|
|
$list = array_filter($list, function ($item) use ($search) {
|
|
return stripos($item['name'], $search) !== false || stripos($item['code'], $search) !== false;
|
|
});
|
|
$list = array_values($list);
|
|
}
|
|
|
|
// Apply Pagination
|
|
$page = (int) $request->input('page', 1);
|
|
$perPage = (int) $request->input('per_page', 15);
|
|
$total = count($list);
|
|
$offset = ($page - 1) * $perPage;
|
|
$paginatedList = array_slice($list, $offset, $perPage);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => [
|
|
'preferred_locations' => $paginatedList,
|
|
'pagination' => [
|
|
'total' => $total,
|
|
'per_page' => $perPage,
|
|
'current_page' => $page,
|
|
'last_page' => max(1, (int) ceil($total / $perPage)),
|
|
]
|
|
],
|
|
], 200);
|
|
}
|
|
|
|
/**
|
|
* Get list of areas for a specific preferred location translated to requested language.
|
|
* Supports en, hindi (hi), sawahi (sw), taglo (tl), tamil (ta).
|
|
*/
|
|
public function preferredLocationAreas(Request $request, $location = null)
|
|
{
|
|
// Get location from path or query parameter
|
|
$loc = strtolower(trim($location ?? $request->input('location') ?? $request->input('city') ?? ''));
|
|
|
|
// Handle URL-friendly formats like abu-dhabi, abu_dhabi, abu dhabi
|
|
$loc = str_replace(['_', '-'], ' ', $loc);
|
|
|
|
if (empty($loc)) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Please provide a valid location (e.g., dubai, abu dhabi, oman).'
|
|
], 400);
|
|
}
|
|
|
|
$lang = strtolower($request->input('lang') ?? $request->header('Accept-Language') ?? 'en');
|
|
if (str_contains($lang, ',')) {
|
|
$lang = explode(',', $lang)[0];
|
|
}
|
|
if (str_contains($lang, '-')) {
|
|
$lang = explode('-', $lang)[0];
|
|
}
|
|
|
|
$langMap = [
|
|
'hindi' => 'hi',
|
|
'sawahi' => 'sw',
|
|
'swahili' => 'sw',
|
|
'taglo' => 'tl',
|
|
'tagalog' => 'tl',
|
|
'tamil' => 'ta',
|
|
];
|
|
|
|
if (isset($langMap[$lang])) {
|
|
$lang = $langMap[$lang];
|
|
}
|
|
|
|
if (!in_array($lang, ['en', 'hi', 'sw', 'tl', 'ta'])) {
|
|
$lang = 'en';
|
|
}
|
|
|
|
$areasMapping = [
|
|
'dubai' => [
|
|
['code' => 'dubai', 'name' => 'Dubai', 'hi' => 'दुबई', 'sw' => 'Dubai', 'tl' => 'Dubai', 'ta' => 'துபாய்'],
|
|
['code' => 'marina', 'name' => 'Dubai Marina', 'hi' => 'दुबई मरीना', 'sw' => 'Dubai Marina', 'tl' => 'Dubai Marina', 'ta' => 'துபாய் மெரினா'],
|
|
['code' => 'barsha', 'name' => 'Al Barsha', 'hi' => 'अल बरशा', 'sw' => 'Al Barsha', 'tl' => 'Al Barsha', 'ta' => 'அல் பர்ஷா'],
|
|
['code' => 'nahda', 'name' => 'Al Nahda', 'hi' => 'अल नहदा', 'sw' => 'Al Nahda', 'tl' => 'Al Nahda', 'ta' => 'அல் நஹ்தா'],
|
|
['code' => 'jumeirah', 'name' => 'Jumeirah', 'hi' => 'जुमेराह', 'sw' => 'Jumeirah', 'tl' => 'Jumeirah', 'ta' => 'ஜுமேரா'],
|
|
['code' => 'deira', 'name' => 'Deira', 'hi' => 'देइरा', 'sw' => 'Deira', 'tl' => 'Deira', 'ta' => 'தேரா'],
|
|
['code' => 'downtown', 'name' => 'Downtown Dubai', 'hi' => 'डाउनटाउन दुबई', 'sw' => 'Downtown Dubai', 'tl' => 'Downtown Dubai', 'ta' => 'டவுன்டவுன் துபாய்'],
|
|
['code' => 'silicon', 'name' => 'Silicon Oasis', 'hi' => 'सिलिकॉन ओएसिस', 'sw' => 'Silicon Oasis', 'tl' => 'Silicon Oasis', 'ta' => 'சிலிக்கான் ஓசிஸ்'],
|
|
['code' => 'sports', 'name' => 'Sports City', 'hi' => 'स्पोर्ट्स सिटी', 'sw' => 'Sports City', 'tl' => 'Sports City', 'ta' => 'ஸ்போர்ட்ஸ் சிட்டி'],
|
|
['code' => 'motor', 'name' => 'Motor City', 'hi' => 'मोटर सिटी', 'sw' => 'Motor City', 'tl' => 'Motor City', 'ta' => 'மோட்டார் சிட்டி'],
|
|
['code' => 'jlt', 'name' => 'JLT', 'hi' => 'JLT', 'sw' => 'JLT', 'tl' => 'JLT', 'ta' => 'ஜே.எல்.டி'],
|
|
['code' => 'jbr', 'name' => 'JBR', 'hi' => 'JBR', 'sw' => 'JBR', 'tl' => 'JBR', 'ta' => 'ஜே.பி.ஆர்'],
|
|
['code' => 'meydan', 'name' => 'Meydan', 'hi' => 'मेदान', 'sw' => 'Meydan', 'tl' => 'Meydan', 'ta' => 'மெய்தான்'],
|
|
['code' => 'ranches', 'name' => 'Arabian Ranches', 'hi' => 'अरेबियन रैंचेस', 'sw' => 'Arabian Ranches', 'tl' => 'Arabian Ranches', 'ta' => 'அரேபியன் ரான்சஸ்'],
|
|
['code' => 'bay', 'name' => 'Business Bay', 'hi' => 'बिजनेस बे', 'sw' => 'Business Bay', 'tl' => 'Business Bay', 'ta' => 'பிசினஸ் பே'],
|
|
['code' => 'mirdif', 'name' => 'Mirdif', 'hi' => 'मिर्डिफ', 'sw' => 'Mirdif', 'tl' => 'Mirdif', 'ta' => 'மிர்திஃப்'],
|
|
['code' => 'quoz', 'name' => 'Al Quoz', 'hi' => 'अल क्वोज़', 'sw' => 'Al Quoz', 'tl' => 'Al Quoz', 'ta' => 'அல் கூஸ்'],
|
|
],
|
|
'abu dhabi' => [
|
|
['code' => 'abu dhabi', 'name' => 'Abu Dhabi City', 'hi' => 'अबू धाबी शहर', 'sw' => 'Abu Dhabi City', 'tl' => 'Abu Dhabi City', 'ta' => 'அபுதாபி நகரம்'],
|
|
['code' => 'yas', 'name' => 'Yas Island', 'hi' => 'यास द्वीप', 'sw' => 'Yas Island', 'tl' => 'Yas Island', 'ta' => 'யாஸ் தீவு'],
|
|
['code' => 'khalifa', 'name' => 'Khalifa City', 'hi' => 'खलीफा शहर', 'sw' => 'Khalifa City', 'tl' => 'Khalifa City', 'ta' => 'கலீஃபா நகரம்'],
|
|
['code' => 'reem', 'name' => 'Reem Island', 'hi' => 'रीम द्वीप', 'sw' => 'Reem Island', 'tl' => 'Reem Island', 'ta' => 'ரீம் தீவு'],
|
|
['code' => 'saadiyat', 'name' => 'Saadiyat Island', 'hi' => 'सादियात द्वीप', 'sw' => 'Saadiyat Island', 'tl' => 'Saadiyat Island', 'ta' => 'சாதியாத் தீவு'],
|
|
['code' => 'raha', 'name' => 'Al Raha', 'hi' => 'अल राहा', 'sw' => 'Al Raha', 'tl' => 'Al Raha', 'ta' => 'அல் ராஹா'],
|
|
['code' => 'mussafah', 'name' => 'Mussafah', 'hi' => 'मुसफ्फह', 'sw' => 'Mussafah', 'tl' => 'Mussafah', 'ta' => 'முஸாஃபா'],
|
|
['code' => 'zahiyah', 'name' => 'Al Zahiyah', 'hi' => 'अल ज़ाहिया', 'sw' => 'Al Zahiyah', 'tl' => 'Al Zahiyah', 'ta' => 'அல் ஜாஹியா'],
|
|
['code' => 'karamah', 'name' => 'Al Karamah', 'hi' => 'अल करमा', 'sw' => 'Al Karamah', 'tl' => 'Al Karamah', 'ta' => 'அல் கராமா'],
|
|
],
|
|
'oman' => [
|
|
['code' => 'oman', 'name' => 'Oman', 'hi' => 'ओमान', 'sw' => 'Omani', 'tl' => 'Oman', 'ta' => 'ஓமன்'],
|
|
['code' => 'muscat', 'name' => 'Muscat', 'hi' => 'मस्कट', 'sw' => 'Muscat', 'tl' => 'Muscat', 'ta' => 'மस्कट'],
|
|
['code' => 'salalah', 'name' => 'Salalah', 'hi' => 'सलालाह', 'sw' => 'Salalah', 'tl' => 'Salalah', 'ta' => 'சலாலா'],
|
|
['code' => 'sohar', 'name' => 'Sohar', 'hi' => 'सोहर', 'sw' => 'Sohar', 'tl' => 'Sohar', 'ta' => 'சோஹார்'],
|
|
['code' => 'nizwa', 'name' => 'Nizwa', 'hi' => 'निज़वा', 'sw' => 'Nizwa', 'tl' => 'Nizwa', 'ta' => 'நிஸ்வா'],
|
|
['code' => 'sur', 'name' => 'Sur', 'hi' => 'सुर', 'sw' => 'Sur', 'tl' => 'Sur', 'ta' => 'சூர்'],
|
|
['code' => 'ibri', 'name' => 'Ibri', 'hi' => 'इबरी', 'sw' => 'Ibri', 'tl' => 'Ibri', 'ta' => 'இப்ரி'],
|
|
['code' => 'rustaq', 'name' => 'Rustaq', 'hi' => 'रुस्तक़', 'sw' => 'Rustaq', 'tl' => 'Rustaq', 'ta' => 'रुस्तक़'],
|
|
]
|
|
];
|
|
|
|
if (!isset($areasMapping[$loc])) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Areas for the specified location were not found. Supported locations: dubai, abu dhabi, oman.'
|
|
], 404);
|
|
}
|
|
|
|
$rawAreas = $areasMapping[$loc];
|
|
$list = [];
|
|
foreach ($rawAreas as $item) {
|
|
$list[] = [
|
|
'code' => $item['code'],
|
|
'name' => $item[$lang] ?? $item['name'],
|
|
];
|
|
}
|
|
|
|
$search = strtolower($request->input('search') ?? $request->input('q') ?? '');
|
|
if ($search !== '') {
|
|
$list = array_filter($list, function ($item) use ($search) {
|
|
return stripos($item['name'], $search) !== false || stripos($item['code'], $search) !== false;
|
|
});
|
|
$list = array_values($list);
|
|
}
|
|
|
|
// Apply Pagination
|
|
$page = (int) $request->input('page', 1);
|
|
$perPage = (int) $request->input('per_page', 15);
|
|
$total = count($list);
|
|
$offset = ($page - 1) * $perPage;
|
|
$paginatedList = array_slice($list, $offset, $perPage);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => [
|
|
'location' => $loc,
|
|
'areas' => $paginatedList,
|
|
'pagination' => [
|
|
'total' => $total,
|
|
'per_page' => $perPage,
|
|
'current_page' => $page,
|
|
'last_page' => max(1, (int) ceil($total / $perPage)),
|
|
]
|
|
],
|
|
], 200);
|
|
}
|
|
|
|
/**
|
|
* Get list of languages translated to requested language.
|
|
* Supports en, hindi (hi), sawahi (sw), taglo (tl), tamil (ta).
|
|
*/
|
|
public function languages(Request $request)
|
|
{
|
|
$lang = strtolower($request->input('lang') ?? $request->header('Accept-Language') ?? 'en');
|
|
if (str_contains($lang, ',')) {
|
|
$lang = explode(',', $lang)[0];
|
|
}
|
|
if (str_contains($lang, '-')) {
|
|
$lang = explode('-', $lang)[0];
|
|
}
|
|
|
|
$langMap = [
|
|
'hindi' => 'hi',
|
|
'sawahi' => 'sw',
|
|
'swahili' => 'sw',
|
|
'taglo' => 'tl',
|
|
'tagalog' => 'tl',
|
|
'tamil' => 'ta',
|
|
];
|
|
|
|
if (isset($langMap[$lang])) {
|
|
$lang = $langMap[$lang];
|
|
}
|
|
|
|
if (!in_array($lang, ['en', 'hi', 'sw', 'tl', 'ta'])) {
|
|
$lang = 'en';
|
|
}
|
|
|
|
$rawLanguages = [
|
|
['code' => 'en', 'name' => 'English', 'hi' => 'अंग्रेज़ी', 'sw' => 'Kiingereza', 'tl' => 'Ingles', 'ta' => 'ஆங்கிலம்'],
|
|
['code' => 'ar', 'name' => 'Arabic', 'hi' => 'अरबी', 'sw' => 'Kiarabu', 'tl' => 'Arabe', 'ta' => 'அரபு'],
|
|
['code' => 'hi', 'name' => 'Hindi', 'hi' => 'हिन्दी', 'sw' => 'Kihindi', 'tl' => 'Hindi', 'ta' => 'இந்தி'],
|
|
['code' => 'tl', 'name' => 'Tagalog', 'hi' => 'तागालोग', 'sw' => 'Kitagalog', 'tl' => 'Tagalog', 'ta' => 'தகலாக்'],
|
|
['code' => 'sw', 'name' => 'Swahili', 'hi' => 'स्वाहिली', 'sw' => 'Kiswahili', 'tl' => 'Swahili', 'ta' => 'சுவாஹிலி'],
|
|
['code' => 'ta', 'name' => 'Tamil', 'hi' => 'तमिल', 'sw' => 'Kitamil', 'tl' => 'Tamil', 'ta' => 'தமிழ்'],
|
|
['code' => 'ur', 'name' => 'Urdu', 'hi' => 'उर्दू', 'sw' => 'Kiurdu', 'tl' => 'Urdu', 'ta' => 'உருது'],
|
|
['code' => 'bn', 'name' => 'Bengali', 'hi' => 'बंगाली', 'sw' => 'Kibengali', 'tl' => 'Bengali', 'ta' => 'பெங்காலி'],
|
|
['code' => 'ml', 'name' => 'Malayalam', 'hi' => 'मलयालम', 'sw' => 'Kimalayalam', 'tl' => 'Malayalam', 'ta' => 'மலையாளம்'],
|
|
['code' => 'ne', 'name' => 'Nepali', 'hi' => 'नेपाली', 'sw' => 'Kinepali', 'tl' => 'Nepali', 'ta' => 'நேபாளி'],
|
|
['code' => 'si', 'name' => 'Sinhala', 'hi' => 'सिंहली', 'sw' => 'Kisinhala', 'tl' => 'Sinhala', 'ta' => 'சிங்களம்'],
|
|
['code' => 'te', 'name' => 'Telugu', 'hi' => 'तेलुगु', 'sw' => 'Kitelugu', 'tl' => 'Telugu', 'ta' => 'தெலுங்கு'],
|
|
['code' => 'pa', 'name' => 'Punjabi', 'hi' => 'पंजाबी', 'sw' => 'Kipunjabi', 'tl' => 'Punjabi', 'ta' => 'பஞ்சாபி'],
|
|
['code' => 'ps', 'name' => 'Pashto', 'hi' => 'पश्तो', 'sw' => 'Kipashto', 'tl' => 'Pashto', 'ta' => 'பஷ்தூ'],
|
|
['code' => 'kn', 'name' => 'Kannada', 'hi' => 'कन्नड़', 'sw' => 'Kikannada', 'tl' => 'Kannada', 'ta' => 'கன்னடம்'],
|
|
['code' => 'mr', 'name' => 'Marathi', 'hi' => 'मराठी', 'sw' => 'Kimarathi', 'tl' => 'Marathi', 'ta' => 'மராத்தி'],
|
|
['code' => 'gu', 'name' => 'Gujarati', 'hi' => 'गुजराती', 'sw' => 'Kigujarati', 'tl' => 'Gujarati', 'ta' => 'குஜராत्ति'],
|
|
['code' => 'fr', 'name' => 'French', 'hi' => 'फ़्रांसीसी', 'sw' => 'Kifaransa', 'tl' => 'Pranses', 'ta' => 'பிரெஞ்சு'],
|
|
['code' => 'es', 'name' => 'Spanish', 'hi' => 'स्पैनिश', 'sw' => 'Kihispania', 'tl' => 'Espanyol', 'ta' => 'ஸ்பானிஷ்'],
|
|
['code' => 'zh', 'name' => 'Chinese', 'hi' => 'चीनी', 'sw' => 'Kichina', 'tl' => 'Intsik', 'ta' => 'சீனம்'],
|
|
['code' => 'ru', 'name' => 'Russian', 'hi' => 'रूसी', 'sw' => 'Kirusi', 'tl' => 'Ruso', 'ta' => 'ரஷ்யன்'],
|
|
['code' => 'id', 'name' => 'Indonesian', 'hi' => 'इंडोनेशियाई', 'sw' => 'Kiindonesia', 'tl' => 'Indonesian', 'ta' => 'இந்தோனேசியன்'],
|
|
];
|
|
|
|
$list = [];
|
|
foreach ($rawLanguages as $item) {
|
|
$list[] = [
|
|
'code' => $item['code'],
|
|
'name' => $item[$lang] ?? $item['name'],
|
|
];
|
|
}
|
|
|
|
$search = strtolower($request->input('search') ?? $request->input('q') ?? '');
|
|
if ($search !== '') {
|
|
$list = array_filter($list, function ($item) use ($search) {
|
|
return stripos($item['name'], $search) !== false || stripos($item['code'], $search) !== false;
|
|
});
|
|
$list = array_values($list);
|
|
}
|
|
|
|
// Apply Pagination
|
|
$page = (int) $request->input('page', 1);
|
|
$perPage = (int) $request->input('per_page', 500);
|
|
if ($perPage === 15) {
|
|
$perPage = 500;
|
|
}
|
|
$total = count($list);
|
|
$offset = ($page - 1) * $perPage;
|
|
$paginatedList = array_slice($list, $offset, $perPage);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => [
|
|
'languages' => $paginatedList,
|
|
'pagination' => [
|
|
'total' => $total,
|
|
'per_page' => $perPage,
|
|
'current_page' => $page,
|
|
'last_page' => max(1, (int) ceil($total / $perPage)),
|
|
]
|
|
],
|
|
], 200);
|
|
}
|
|
|
|
/**
|
|
* E.g. "14/05/1990" -> "1990-05-14"
|
|
*/
|
|
private function normaliseDateForController(?string $raw): ?string
|
|
{
|
|
if (empty($raw)) {
|
|
return null;
|
|
}
|
|
$raw = trim($raw);
|
|
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $raw)) {
|
|
return $raw;
|
|
}
|
|
try {
|
|
if (preg_match('/^(\d{1,2})[\/\- ]([A-Za-z0-9]+)[\/\- ](\d{4})$/', $raw, $m)) {
|
|
$day = str_pad($m[1], 2, '0', STR_PAD_LEFT);
|
|
$month = $m[2];
|
|
$year = $m[3];
|
|
if (is_numeric($month)) {
|
|
$month = str_pad($month, 2, '0', STR_PAD_LEFT);
|
|
return "{$year}-{$month}-{$day}";
|
|
}
|
|
$ts = strtotime("{$day} {$month} {$year}");
|
|
return $ts ? date('Y-m-d', $ts) : $raw;
|
|
}
|
|
$dt = new \DateTime($raw);
|
|
return $dt->format('Y-m-d');
|
|
} catch (\Exception $e) {
|
|
return $raw;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Standardize and clean Visa OCR data structure to exactly the 15 required fields.
|
|
*/
|
|
private function cleanVisaData(array $visaDataInput): array
|
|
{
|
|
$rawVisaType = $visaDataInput['visa_type'] ?? null;
|
|
$rawEntryPermitNo = $visaDataInput['entry_permit_no'] ?? $visaDataInput['file_number'] ?? $visaDataInput['id_number'] ?? $visaDataInput['number'] ?? null;
|
|
$rawIssueDate = $visaDataInput['issue_date'] ?? null;
|
|
$rawValidUntil = $visaDataInput['valid_until'] ?? $visaDataInput['expiry_date'] ?? null;
|
|
$rawUidNo = $visaDataInput['uid_no'] ?? $visaDataInput['id_number'] ?? null;
|
|
$rawFullName = $visaDataInput['full_name'] ?? $visaDataInput['name'] ?? null;
|
|
$rawNationality = $visaDataInput['nationality'] ?? null;
|
|
$rawPlaceOfBirth = $visaDataInput['place_of_birth'] ?? null;
|
|
$rawDateOfBirth = $visaDataInput['date_of_birth'] ?? $visaDataInput['dob'] ?? null;
|
|
$rawPassportNo = $visaDataInput['passport_no'] ?? $visaDataInput['passport_number'] ?? null;
|
|
$rawProfession = $visaDataInput['profession'] ?? null;
|
|
$rawSponsorName = $visaDataInput['sponsor_name'] ?? null;
|
|
|
|
// Clean sponsor
|
|
$sponsorData = $visaDataInput['sponsor'] ?? [];
|
|
if (is_string($sponsorData)) {
|
|
$sponsorNameFromObj = $sponsorData;
|
|
$sponsorAddress = '';
|
|
$sponsorPhone = '';
|
|
} else {
|
|
$sponsorNameFromObj = $sponsorData['name'] ?? $sponsorData['sponsor_name'] ?? $sponsorData['full_name'] ?? '';
|
|
$sponsorAddress = $sponsorData['address'] ?? '';
|
|
$sponsorPhone = $sponsorData['phone'] ?? $sponsorData['mobile'] ?? $sponsorData['mobile_number'] ?? $sponsorData['phone_number'] ?? '';
|
|
}
|
|
if (empty($rawSponsorName)) {
|
|
$rawSponsorName = $sponsorNameFromObj;
|
|
}
|
|
|
|
return [
|
|
'document_type' => 'uae_visa',
|
|
'country' => 'United Arab Emirates',
|
|
'visa_type' => $rawVisaType ?: 'ENTRY PERMIT',
|
|
'entry_permit_no' => $rawEntryPermitNo ?: '',
|
|
'issue_date' => $rawIssueDate ?: '',
|
|
'valid_until' => $rawValidUntil ?: '',
|
|
'uid_no' => $rawUidNo ?: '',
|
|
'full_name' => $rawFullName ?: '',
|
|
'nationality' => $rawNationality ?: '',
|
|
'place_of_birth' => $rawPlaceOfBirth ?: '',
|
|
'date_of_birth' => $rawDateOfBirth ?: '',
|
|
'passport_no' => $rawPassportNo ?: '',
|
|
'profession' => $rawProfession ?: '',
|
|
'sponsor_name' => $rawSponsorName ?: '',
|
|
'sponsor' => [
|
|
'name' => $sponsorNameFromObj ?: '',
|
|
'address' => $sponsorAddress ?: '',
|
|
'phone' => $sponsorPhone ?: '',
|
|
]
|
|
];
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Forgot / Reset Password (via Mobile Number)
|
|
// -------------------------------------------------------------------------
|
|
|
|
/**
|
|
* POST /api/workers/forgot-password
|
|
* Send a 6-digit OTP to verify the worker's identity by phone number.
|
|
* Workers do not use email — phone is the primary identifier.
|
|
*/
|
|
public function forgotPassword(Request $request)
|
|
{
|
|
$validator = Validator::make($request->all(), [
|
|
'phone' => 'required|string',
|
|
]);
|
|
|
|
if ($validator->fails()) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Validation error.',
|
|
'errors' => $validator->errors(),
|
|
], 422);
|
|
}
|
|
|
|
$worker = Worker::where('phone', $request->phone)->first();
|
|
|
|
if (!$worker) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'user not found this mobile number',
|
|
], 404);
|
|
}
|
|
|
|
$otp = (string) mt_rand(100000, 999999);
|
|
$cacheKey = 'worker_pwd_reset_' . md5($request->phone);
|
|
|
|
Cache::put($cacheKey, [
|
|
'otp' => Hash::make($otp),
|
|
'expires_at' => now()->addMinutes(10)->timestamp,
|
|
], now()->addMinutes(10));
|
|
|
|
// Send via email if available, otherwise log for dev
|
|
if ($worker->email) {
|
|
try {
|
|
Mail::to($worker->email)->send(
|
|
new \App\Mail\ForgotPasswordOtpMail($otp, $worker->name, 'Worker')
|
|
);
|
|
} catch (\Exception $e) {
|
|
logger()->error('Worker forgot-password mail failed: ' . $e->getMessage());
|
|
}
|
|
} else {
|
|
// Log OTP for debugging when no email configured
|
|
logger()->info("Worker password reset OTP for {$request->phone}: {$otp}");
|
|
}
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => 'If an account with that mobile number exists, a reset OTP has been sent.',
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* POST /api/workers/reset-password
|
|
* Verify OTP sent to phone and set a new password.
|
|
*/
|
|
public function resetPassword(Request $request)
|
|
{
|
|
$validator = Validator::make($request->all(), [
|
|
'phone' => 'required|string',
|
|
'otp' => 'required|string|size:6',
|
|
'password' => 'required|string|min:8|confirmed',
|
|
'password_confirmation' => 'required|string',
|
|
]);
|
|
|
|
if ($validator->fails()) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Validation error.',
|
|
'errors' => $validator->errors(),
|
|
], 422);
|
|
}
|
|
|
|
$cacheKey = 'worker_pwd_reset_' . md5($request->phone);
|
|
$cached = Cache::get($cacheKey);
|
|
|
|
if (!$cached || $cached['expires_at'] < now()->timestamp) {
|
|
Cache::forget($cacheKey);
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'OTP has expired or is invalid. Please request a new one.',
|
|
], 422);
|
|
}
|
|
|
|
if (!Hash::check($request->otp, $cached['otp'])) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Invalid OTP. Please check and try again.',
|
|
], 422);
|
|
}
|
|
|
|
$worker = Worker::where('phone', $request->phone)->first();
|
|
|
|
if (!$worker) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Account not found.',
|
|
], 404);
|
|
}
|
|
|
|
$worker->update(['password' => Hash::make($request->password)]);
|
|
Cache::forget($cacheKey);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => 'Password reset successfully. You can now log in with your new password.',
|
|
]);
|
|
}
|
|
}
|
|
|