diff --git a/app/Http/Controllers/Api/WorkerAuthController.php b/app/Http/Controllers/Api/WorkerAuthController.php index 384dcf6..069a0a3 100644 --- a/app/Http/Controllers/Api/WorkerAuthController.php +++ b/app/Http/Controllers/Api/WorkerAuthController.php @@ -243,7 +243,7 @@ public function setupProfile(Request $request) '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 + 'age' => '18-25', // Default — updated later in full profile 'salary' => 1500, // Default AED — updated later 'availability' => 'Immediate', 'experience' => 'Not Specified', @@ -315,7 +315,7 @@ public function register(Request $request) 'skills' => 'nullable', 'language' => 'nullable|string', 'nationality' => 'nullable|string|max:100', - 'age' => 'nullable|integer', + 'age' => 'nullable|string|in:18-25,26-35,36-45,46-55,56-60', 'experience' => 'nullable|string|max:100', 'in_country' => 'nullable', 'visa_status' => 'nullable|string', @@ -401,7 +401,7 @@ public function register(Request $request) // Initialize fields $name = $request->name; $nationality = $request->nationality ?? 'Not Specified'; - $age = $request->age ?? 25; + $age = $request->age ?? '18-25'; $visaStatus = $inCountry ? $request->visa_status : null; $passportDataInput = $request->input('passport'); @@ -430,7 +430,20 @@ public function register(Request $request) if (!empty($passportDataInput['date_of_birth'])) { try { $dob = new \DateTime($this->normaliseDateForController($passportDataInput['date_of_birth'])); - $age = (new \DateTime())->diff($dob)->y; + $ageVal = (new \DateTime())->diff($dob)->y; + if ($ageVal >= 18 && $ageVal <= 25) { + $age = '18-25'; + } elseif ($ageVal >= 26 && $ageVal <= 35) { + $age = '26-35'; + } elseif ($ageVal >= 36 && $ageVal <= 45) { + $age = '36-45'; + } elseif ($ageVal >= 46 && $ageVal <= 55) { + $age = '46-55'; + } elseif ($ageVal >= 56 && $ageVal <= 60) { + $age = '56-60'; + } else { + $age = $ageVal < 18 ? '18-25' : '56-60'; + } } catch (\Exception $dateEx) { logger()->error('Passport DOB parse error in register: ' . $dateEx->getMessage()); } diff --git a/app/Http/Controllers/Api/WorkerProfileController.php b/app/Http/Controllers/Api/WorkerProfileController.php index 80bd1d5..3927d2f 100644 --- a/app/Http/Controllers/Api/WorkerProfileController.php +++ b/app/Http/Controllers/Api/WorkerProfileController.php @@ -70,7 +70,7 @@ public function updateProfile(Request $request) $validator = Validator::make($request->all(), [ 'name' => 'nullable|string|max:255', 'phone' => 'nullable|string|max:50|unique:workers,phone,' . $worker->id, - 'age' => 'nullable|integer|min:18|max:70', + 'age' => 'nullable|string|in:18-25,26-35,36-45,46-55,56-60', 'nationality' => 'nullable|string|max:100', 'language' => 'nullable|string', 'salary' => 'nullable|numeric|min:0', diff --git a/database/migrations/2026_07_07_125455_change_age_to_string_in_workers_table.php b/database/migrations/2026_07_07_125455_change_age_to_string_in_workers_table.php new file mode 100644 index 0000000..d3ff5d3 --- /dev/null +++ b/database/migrations/2026_07_07_125455_change_age_to_string_in_workers_table.php @@ -0,0 +1,25 @@ +string('age')->nullable()->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('workers', function (Blueprint $table) { + $table->integer('age')->nullable(false)->change(); + }); + } +}; diff --git a/public/swagger.json b/public/swagger.json index ac10af7..9a9f2ad 100644 --- a/public/swagger.json +++ b/public/swagger.json @@ -1017,9 +1017,16 @@ "description": "Nationality of the worker." }, "age": { - "type": "integer", - "example": 28, - "description": "Age of the worker." + "type": "string", + "enum": [ + "18-25", + "26-35", + "36-45", + "46-55", + "56-60" + ], + "example": "26-35", + "description": "Age range of the worker." }, "experience": { "type": "string", @@ -7427,33 +7434,106 @@ "employer": { "type": "object", "properties": { - "id": { "type": "integer", "example": 1 }, - "name": { "type": "string", "example": "Jane Sponsor" }, - "email": { "type": "string", "nullable": true, "example": "sponsor@example.com" }, - "phone": { "type": "string", "nullable": true, "example": "+971501112222" }, - "company_name": { "type": "string", "example": "Sponsor Co" }, - "city": { "type": "string", "nullable": true, "example": "Dubai" }, - "district": { "type": "string", "nullable": true, "example": "Dubai Marina" }, - "nationality": { "type": "string", "nullable": true, "example": "Emirati" }, - "address": { "type": "string", "nullable": true, "example": "123 Street" }, - "accommodation": { "type": "string", "nullable": true, "example": "Provided" }, - "language": { "type": "string", "nullable": true, "example": "English" }, - "profile_photo_url": { "type": "string", "nullable": true, "example": "https://example.com/photo.jpg" }, - "rating": { "type": "number", "format": "float", "example": 4.5 }, - "reviews_count": { "type": "integer", "example": 12 }, + "id": { + "type": "integer", + "example": 1 + }, + "name": { + "type": "string", + "example": "Jane Sponsor" + }, + "email": { + "type": "string", + "nullable": true, + "example": "sponsor@example.com" + }, + "phone": { + "type": "string", + "nullable": true, + "example": "+971501112222" + }, + "company_name": { + "type": "string", + "example": "Sponsor Co" + }, + "city": { + "type": "string", + "nullable": true, + "example": "Dubai" + }, + "district": { + "type": "string", + "nullable": true, + "example": "Dubai Marina" + }, + "nationality": { + "type": "string", + "nullable": true, + "example": "Emirati" + }, + "address": { + "type": "string", + "nullable": true, + "example": "123 Street" + }, + "accommodation": { + "type": "string", + "nullable": true, + "example": "Provided" + }, + "language": { + "type": "string", + "nullable": true, + "example": "English" + }, + "profile_photo_url": { + "type": "string", + "nullable": true, + "example": "https://example.com/photo.jpg" + }, + "rating": { + "type": "number", + "format": "float", + "example": 4.5 + }, + "reviews_count": { + "type": "integer", + "example": 12 + }, "review_summary": { "type": "object", "properties": { - "total": { "type": "integer", "example": 12 }, - "average": { "type": "number", "example": 4.5 }, + "total": { + "type": "integer", + "example": 12 + }, + "average": { + "type": "number", + "example": 4.5 + }, "stars": { "type": "object", "properties": { - "5": { "type": "integer", "example": 8 }, - "4": { "type": "integer", "example": 2 }, - "3": { "type": "integer", "example": 1 }, - "2": { "type": "integer", "example": 1 }, - "1": { "type": "integer", "example": 0 } + "5": { + "type": "integer", + "example": 8 + }, + "4": { + "type": "integer", + "example": 2 + }, + "3": { + "type": "integer", + "example": 1 + }, + "2": { + "type": "integer", + "example": 1 + }, + "1": { + "type": "integer", + "example": 0 + } } } } @@ -7463,15 +7543,45 @@ "items": { "type": "object", "properties": { - "id": { "type": "integer", "example": 5 }, - "title": { "type": "string", "example": "Housekeeper Needed" }, - "location": { "type": "string", "example": "Dubai Marina" }, - "salary": { "type": "integer", "example": 2500 }, - "job_type": { "type": "string", "example": "Full Time" }, - "start_date": { "type": "string", "format": "date", "example": "2026-08-01" }, - "description": { "type": "string", "example": "Clean house" }, - "requirements": { "type": "string", "nullable": true, "example": "Experience required" }, - "posted_at": { "type": "string", "format": "date-time", "example": "2026-07-04T12:00:00Z" } + "id": { + "type": "integer", + "example": 5 + }, + "title": { + "type": "string", + "example": "Housekeeper Needed" + }, + "location": { + "type": "string", + "example": "Dubai Marina" + }, + "salary": { + "type": "integer", + "example": 2500 + }, + "job_type": { + "type": "string", + "example": "Full Time" + }, + "start_date": { + "type": "string", + "format": "date", + "example": "2026-08-01" + }, + "description": { + "type": "string", + "example": "Clean house" + }, + "requirements": { + "type": "string", + "nullable": true, + "example": "Experience required" + }, + "posted_at": { + "type": "string", + "format": "date-time", + "example": "2026-07-04T12:00:00Z" + } } } } @@ -7485,17 +7595,42 @@ "items": { "type": "object", "properties": { - "id": { "type": "integer", "example": 1 }, - "rating": { "type": "integer", "example": 5 }, - "title": { "type": "string", "example": "Great employer" }, - "comment": { "type": "string", "example": "Highly recommended!" }, - "created_at": { "type": "string", "format": "date-time", "example": "2026-07-04T12:00:00Z" }, + "id": { + "type": "integer", + "example": 1 + }, + "rating": { + "type": "integer", + "example": 5 + }, + "title": { + "type": "string", + "example": "Great employer" + }, + "comment": { + "type": "string", + "example": "Highly recommended!" + }, + "created_at": { + "type": "string", + "format": "date-time", + "example": "2026-07-04T12:00:00Z" + }, "worker": { "type": "object", "properties": { - "id": { "type": "integer", "example": 2 }, - "name": { "type": "string", "example": "Worker Name" }, - "nationality": { "type": "string", "example": "Filipino" } + "id": { + "type": "integer", + "example": 2 + }, + "name": { + "type": "string", + "example": "Worker Name" + }, + "nationality": { + "type": "string", + "example": "Filipino" + } } } } @@ -7504,10 +7639,22 @@ "pagination": { "type": "object", "properties": { - "total": { "type": "integer", "example": 1 }, - "per_page": { "type": "integer", "example": 10 }, - "current_page": { "type": "integer", "example": 1 }, - "last_page": { "type": "integer", "example": 1 } + "total": { + "type": "integer", + "example": 1 + }, + "per_page": { + "type": "integer", + "example": 10 + }, + "current_page": { + "type": "integer", + "example": 1 + }, + "last_page": { + "type": "integer", + "example": 1 + } } } } @@ -9133,8 +9280,16 @@ "example": "Egypt" }, "age": { - "type": "integer", - "example": 28 + "type": "string", + "enum": [ + "18-25", + "26-35", + "36-45", + "46-55", + "56-60" + ], + "example": "26-35", + "description": "Age range of the worker." }, "salary": { "type": "string", diff --git a/resources/js/Pages/Employer/Shortlist.jsx b/resources/js/Pages/Employer/Shortlist.jsx index 7b7c822..9241bca 100644 --- a/resources/js/Pages/Employer/Shortlist.jsx +++ b/resources/js/Pages/Employer/Shortlist.jsx @@ -238,7 +238,15 @@ export default function Shortlist({ shortlistedWorkers, filtersMetadata = {} }) } else if (sortBy === 'rating') { list.sort((a, b) => b.rating - a.rating); } else if (sortBy === 'experience') { - list.sort((a, b) => b.age - a.age); // approximate experience by age + list.sort((a, b) => { + const parseAge = (val) => { + if (!val) return 0; + if (typeof val === 'number') return val; + const num = parseInt(val, 10); + return isNaN(num) ? 0 : num; + }; + return parseAge(b.age) - parseAge(a.age); + }); } return list; diff --git a/resources/js/Pages/Employer/Workers/Index.jsx b/resources/js/Pages/Employer/Workers/Index.jsx index 62c1c97..bc92cb4 100644 --- a/resources/js/Pages/Employer/Workers/Index.jsx +++ b/resources/js/Pages/Employer/Workers/Index.jsx @@ -334,7 +334,15 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [], } else if (sortBy === 'rating') { workers.sort((a, b) => b.rating - a.rating); } else if (sortBy === 'experience') { - workers.sort((a, b) => b.age - a.age); // approximate experience by age + workers.sort((a, b) => { + const parseAge = (val) => { + if (!val) return 0; + if (typeof val === 'number') return val; + const num = parseInt(val, 10); + return isNaN(num) ? 0 : num; + }; + return parseAge(b.age) - parseAge(a.age); + }); } return workers; diff --git a/tests/Feature/WorkerJourneyApiTest.php b/tests/Feature/WorkerJourneyApiTest.php index f978614..5d1fcdf 100644 --- a/tests/Feature/WorkerJourneyApiTest.php +++ b/tests/Feature/WorkerJourneyApiTest.php @@ -383,7 +383,7 @@ public function test_register_with_new_api_fields() 'passport_file' => $fakePassport, 'language' => 'HI', 'nationality' => 'Kenyan', - 'age' => 30, + 'age' => '26-35', 'experience' => '5 Years', 'in_country' => 'true', 'visa_status' => 'Tourist Visa', @@ -399,6 +399,7 @@ public function test_register_with_new_api_fields() $this->assertDatabaseHas('workers', [ 'name' => 'John New Worker', 'phone' => '+971501111112', + 'age' => '26-35', 'in_country' => true, 'visa_status' => 'Tourist Visa', 'preferred_job_type' => 'full-time', @@ -435,14 +436,12 @@ public function test_register_with_ocr_extraction() $response->assertStatus(201); $workerId = $response->json('data.worker.id'); - $expectedAge = now()->diff(new \DateTime('1990-11-07'))->y; - $this->assertDatabaseHas('workers', [ 'id' => $workerId, 'name' => 'MATAR ALI KHARBASH ALSAEDI', 'phone' => '+971509999999', 'nationality' => 'Emirati', - 'age' => $expectedAge, + 'age' => '26-35', 'verified' => true, ]); @@ -497,15 +496,12 @@ public function test_register_passport_and_visa_with_direct_json_payload() $response->assertStatus(201); $workerId = $response->json('data.worker.id'); - - $expectedAge = now()->diff(new \DateTime('1990-05-14'))->y; - $this->assertDatabaseHas('workers', [ 'id' => $workerId, 'name' => 'Jane Doe', 'phone' => '+971508888888', 'nationality' => 'Filipino', - 'age' => $expectedAge, + 'age' => '36-45', 'verified' => true, 'visa_status' => 'Tourist Visa', ]);