migrant-web/app/Http/Controllers/Api/WorkerAuthController.php
2026-06-17 22:20:17 +05:30

1061 lines
47 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;
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',
'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,
'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;
});
if ($request->filled('fcm_token')) {
\App\Services\FCMService::sendPushNotification(
$request->fcm_token,
'Welcome to Migrant',
'Registration complete! Welcome to Migrant.'
);
}
// Clear the OTP verification gate
Cache::forget('worker_otp_verified_' . $identifier);
$worker->load(['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 backward compatibility and multipart file registration.
*/
public function register(Request $request)
{
$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',
'gender' => 'nullable|string|in:male,female,other',
'live_in_out' => 'nullable|string|in:live_in,live_out',
'preferred_location' => 'nullable|string|max:255',
'fcm_token' => 'nullable|string|max:255',
]);
if ($validator->fails()) {
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) {
$worker = Worker::create([
'name' => $request->name,
'email' => $email,
'phone' => $request->phone,
'language' => $request->language ?? 'HI',
'password' => Hash::make($request->password),
'nationality' => $request->nationality ?? 'Not Specified',
'gender' => $request->gender,
'live_in_out' => $request->live_in_out,
'preferred_location' => $request->preferred_location,
'age' => $request->age ?? 25,
'salary' => $request->salary,
'availability' => 'Immediate',
'experience' => $request->experience ?? 'Not Specified',
'religion' => 'Not Specified',
'bio' => 'Hardworking and reliable General Helper available for immediate hire.',
'verified' => false,
'status' => 'active',
'api_token' => $apiToken,
'in_country' => $inCountry,
'visa_status' => $inCountry ? $request->visa_status : null,
'preferred_job_type' => $request->preferred_job_type,
'fcm_token' => $request->fcm_token,
]);
if (!empty($skillsArray)) {
$validSkillIds = DB::table('skills')->whereIn('id', $skillsArray)->pluck('id')->toArray();
if (!empty($validSkillIds)) {
$worker->skills()->sync($validSkillIds);
}
}
return $worker;
});
if ($request->filled('fcm_token')) {
\App\Services\FCMService::sendPushNotification(
$request->fcm_token,
'Welcome to Migrant',
'Worker registered successfully. Please upload your passport and visa documents.'
);
}
$result->load(['skills', 'documents']);
return response()->json([
'success' => true,
'message' => 'Worker registered successfully. Please upload your passport and visa documents.',
'data' => ['worker' => $result, 'token' => $apiToken]
], 201);
} catch (\Exception $e) {
logger()->error('Worker Registration Failure: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'An error occurred during worker registration. Please try again.',
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
], 500);
}
}
// -------------------------------------------------------------------------
// Document Upload Endpoints (protected — require Bearer token)
// -------------------------------------------------------------------------
/**
* Upload & OCR-process the worker's passport document.
* POST /workers/register/passport (auth.worker)
*
* Calls the dedicated Passport OCR API (http://192.168.0.252:5000/passport).
* Creates or updates the worker's passport document record.
* Optionally syncs name, nationality, and age extracted from OCR onto the worker profile.
*/
public function registerPassport(Request $request)
{
$validator = Validator::make($request->all(), [
'passport_file' => 'required|file|mimes:jpeg,png,jpg,pdf|max:10240',
'sync_profile' => 'nullable|in:true,false,1,0,TRUE,FALSE',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'message' => 'Validation error.',
'errors' => $validator->errors()
], 422);
}
/** @var \App\Models\Worker $worker */
$worker = $request->attributes->get('worker');
try {
$file = $request->file('passport_file');
$ocrRes = \App\Services\OcrDocumentService::extractPassportData($file);
if (empty($ocrRes['success'])) {
return response()->json([
'success' => false,
'message' => 'Passport OCR extraction failed. Please upload a clear image and try again.',
'errors' => [
'passport_file' => ['Passport OCR API could not read the document. Please try again.']
]
], 422);
}
// Build document data from OCR
$number = $ocrRes['document_number'] ?? ('P' . rand(1000000, 9999999));
$expiryDate = $ocrRes['expiry_date'] ?? now()->addYears(8)->toDateString();
$issueDate = !empty($ocrRes['expiry_date'])
? date('Y-m-d', strtotime($ocrRes['expiry_date'] . ' -5 years'))
: now()->subYears(3)->toDateString();
// Upsert passport document (update if already exists, create if not)
$worker->documents()->updateOrCreate(
['type' => 'passport'],
[
'number' => $number,
'issue_date' => $issueDate,
'expiry_date' => $expiryDate,
'ocr_accuracy' => 98.5,
'file_path' => null,
]
);
// Optionally sync OCR-extracted profile fields
$profileUpdates = ['verified' => true];
$syncProfile = filter_var($request->input('sync_profile', true), FILTER_VALIDATE_BOOLEAN);
if ($syncProfile) {
if (!empty($ocrRes['name'])) {
$profileUpdates['name'] = $ocrRes['name'];
}
if (!empty($ocrRes['nationality'])) {
$codeUpper = strtoupper($ocrRes['nationality']);
$matchedNationality = \App\Models\Nationality::where('iso3', $codeUpper)
->orWhere('code', $codeUpper)
->first();
$profileUpdates['nationality'] = $matchedNationality ? $matchedNationality->name : $ocrRes['nationality'];
}
if (!empty($ocrRes['date_of_birth'])) {
try {
$dob = new \DateTime($ocrRes['date_of_birth']);
$profileUpdates['age'] = (new \DateTime())->diff($dob)->y;
} catch (\Exception $dateEx) {
logger()->error('Passport DOB parse error: ' . $dateEx->getMessage());
}
}
}
if (!empty($profileUpdates)) {
$worker->update($profileUpdates);
}
$worker->load(['skills', 'documents']);
$passport = $worker->documents->where('type', 'passport')->first();
$passportData = null;
if ($passport) {
$passportData = array_merge($passport->toArray(), [
'document_number' => $ocrRes['document_number'],
'expiry_date' => $ocrRes['expiry_date'],
'date_of_birth' => $ocrRes['date_of_birth'] ?? null,
'nationality' => $ocrRes['nationality'] ?? null,
'name' => $ocrRes['name'] ?? null,
]);
}
return response()->json([
'success' => true,
'message' => 'Passport uploaded and processed successfully.',
'data' => [
'worker' => $worker,
'passport' => $passportData,
]
], 200);
} catch (\Exception $e) {
logger()->error('Worker Passport Upload Failure: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'An error occurred while processing the passport. Please try again.',
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
], 500);
}
}
/**
* Upload & OCR-process the worker's visa document.
* POST /workers/register/visa (auth.worker)
*
* Calls the dedicated Visa OCR API (http://192.168.0.252:5000/visa).
* Creates or updates the worker's visa document record.
*/
public function registerVisa(Request $request)
{
$validator = Validator::make($request->all(), [
'visa_file' => 'required|file|mimes:jpeg,png,jpg,pdf|max:10240',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'message' => 'Validation error.',
'errors' => $validator->errors()
], 422);
}
/** @var \App\Models\Worker $worker */
$worker = $request->attributes->get('worker');
try {
$file = $request->file('visa_file');
$ocrRes = \App\Services\OcrDocumentService::extractVisaData($file);
$number = $ocrRes['document_number'] ?? ('V' . rand(1000000, 9999999));
$expiryDate = $ocrRes['expiry_date'] ?? now()->addYears(2)->toDateString();
$issueDate = $ocrRes['issue_date'] ?? date('Y-m-d', strtotime($expiryDate . ' -2 years'));
$accuracy = $ocrRes['ocr_accuracy'] ?? ($ocrRes['success'] ? 98.5 : 0.0);
// Upsert visa document
$worker->documents()->updateOrCreate(
['type' => 'visa'],
[
'number' => $number,
'issue_date' => $issueDate,
'expiry_date' => $expiryDate,
'ocr_accuracy' => $accuracy,
'file_path' => null,
]
);
$worker->update(['verified' => true]);
$worker->load(['skills', 'documents']);
$visa = $worker->documents->where('type', 'visa')->first();
$visaData = null;
if ($visa) {
$visaData = array_merge($visa->toArray(), [
'document_number' => $ocrRes['document_number'] ?? null,
'expiry_date' => $ocrRes['expiry_date'] ?? null,
'issue_date' => $ocrRes['issue_date'] ?? null,
'visa_type' => $ocrRes['visa_type'] ?? null,
]);
}
return response()->json([
'success' => true,
'message' => 'Visa uploaded and processed successfully.',
'data' => [
'worker' => $worker,
'visa' => $visaData,
]
], 200);
} catch (\Exception $e) {
logger()->error('Worker Visa Upload Failure: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'An error occurred while processing the visa. Please try again.',
'error' => app()->environment('local') ? $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();
$list = [];
foreach ($rawNationalities 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' => [
'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);
}
}