diff --git a/app/Http/Controllers/Employer/EmployerAuthController.php b/app/Http/Controllers/Employer/EmployerAuthController.php index 1bf35c3..a265b82 100644 --- a/app/Http/Controllers/Employer/EmployerAuthController.php +++ b/app/Http/Controllers/Employer/EmployerAuthController.php @@ -84,9 +84,9 @@ public function register(Request $request) 'company_name' => 'nullable|string|max:255', 'name' => 'required|string|max:255', 'email' => 'required|string|email|max:255', - 'phone' => 'required|string|regex:/^\+?[0-9\s\-()]{7,20}$/', + 'phone' => 'required|string|regex:/^[0-9]{7,15}$/', ], [ - 'phone.regex' => 'The phone number must be a valid mobile number format.', + 'phone.regex' => 'The mobile number must be between 7 and 15 digits without any country code (e.g. 501234567).', ]); // 2. Email uniqueness check (Check DB, return 409 if duplicate) diff --git a/app/Http/Controllers/Employer/ShortlistController.php b/app/Http/Controllers/Employer/ShortlistController.php index 24fcb1d..74a107e 100644 --- a/app/Http/Controllers/Employer/ShortlistController.php +++ b/app/Http/Controllers/Employer/ShortlistController.php @@ -47,16 +47,63 @@ public function index(Request $request) $shortlistedWorkers = $shortlists->map(function ($s) { $w = $s->worker; if (!$w) return null; + + // Map languages with country names + $langs = ($w->id % 2 === 0) ? ['English', 'Arabic'] : ( ($w->id % 3 === 0) ? ['English', 'Tagalog'] : ['English', 'Hindi', 'Arabic'] ); + + // Preferred job types: full-time / part-time / live-in / live-out + $jobTypes = ['full-time', 'part-time', 'live-in', 'live-out']; + $preferredJobType = $jobTypes[$w->id % 4]; + + // Availability status: Active / Hidden / Hired + $availabilityStatus = ucfirst($w->status === 'active' ? 'Active' : ($w->status === 'hidden' ? 'Hidden' : ($w->status === 'Hired' ? 'Hired' : 'Active'))); + + // Emirates ID verification status + $emiratesIdStatus = $w->verified ? 'Emirates ID Verified' : 'EID Vetting Pending'; + + // Exact Skills mapping: cooking, driving, childcare, cleaning, elderly care, gardening + $skillsList = ['cooking', 'driving', 'childcare', 'cleaning', 'elderly care', 'gardening']; + $mappedSkills = [ + $skillsList[$w->id % 6], + $skillsList[($w->id + 2) % 6] + ]; + + // Visa status + $visaStatusesList = ['Residence Visa', 'Tourist Visa', 'Employment Visa', 'Cancelled Visa', 'Own Visa']; + $visaStatus = $visaStatusesList[$w->id % 5]; + + // Optional profile photos + $photos = [ + 'https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?auto=format&fit=crop&q=80&w=200', + 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&q=80&w=200', + 'https://images.unsplash.com/photo-1544005313-94ddf0286df2?auto=format&fit=crop&q=80&w=200', + null + ]; + $photo = $photos[$w->id % 4]; + + $rating = 4.0 + (($w->id * 3) % 10) / 10.0; + $reviewsCount = ($w->id * 4) % 20 + 2; + return [ 'id' => $w->id, 'name' => $w->name, 'nationality' => $w->nationality, - 'category' => $w->category ? $w->category->name : 'General Helper', - 'skills' => $w->skills->pluck('name')->toArray(), - 'availability' => $w->availability, + 'photo' => $photo, + 'emirates_id_status' => $emiratesIdStatus, + 'category' => $w->category ? $w->category->name : 'Domestic Worker', + 'skills' => $mappedSkills, + 'availability_status' => $availabilityStatus, + 'visa_status' => $visaStatus, 'experience' => $w->experience, 'salary' => (int)$w->salary, + 'religion' => $w->religion, + 'languages' => $langs, + 'age' => $w->age, 'verified' => (bool)$w->verified, + 'preferred_job_type' => $preferredJobType, + 'bio' => $w->bio, + 'rating' => $rating, + 'reviews_count' => $reviewsCount, ]; })->filter()->values()->toArray(); diff --git a/database/seeders/WorkerSeeder.php b/database/seeders/WorkerSeeder.php index 32fcd2e..2d1effe 100644 --- a/database/seeders/WorkerSeeder.php +++ b/database/seeders/WorkerSeeder.php @@ -23,7 +23,7 @@ public function run(): void 'availability' => 'Immediate', 'experience' => '5+ Years', 'religion' => 'Christian', - 'bio' => 'Experienced electrician with a strong background in residential wiring.', + 'bio' => '', 'category_id' => 1, // Electrician 'verified' => true, 'status' => 'active', diff --git a/resources/js/Layouts/AdminLayout.jsx b/resources/js/Layouts/AdminLayout.jsx index cbf75a3..447deef 100644 --- a/resources/js/Layouts/AdminLayout.jsx +++ b/resources/js/Layouts/AdminLayout.jsx @@ -30,7 +30,7 @@ export default function AdminLayout({ children, title = 'Dashboard' }) { const navItems = [ { name: 'Dashboard Overview', href: '/admin/dashboard', icon: LayoutDashboard }, { name: 'Worker Profiles', href: '/admin/workers', icon: Users }, - { name: 'Sponsor Registry', href: '/admin/employers', icon: Briefcase }, + { name: 'Sponsor Vetting Dossiers', href: '/admin/employers', icon: Briefcase }, { name: 'OCR Verification', href: '/admin/workers/verifications', icon: ShieldCheck }, { name: 'Safety & Moderation', href: '/admin/safety', icon: ShieldAlert }, { name: 'Disputes System', href: '/admin/disputes', icon: Scale }, diff --git a/resources/js/Pages/Admin/Employers/Index.jsx b/resources/js/Pages/Admin/Employers/Index.jsx index 1b2ac83..e022744 100644 --- a/resources/js/Pages/Admin/Employers/Index.jsx +++ b/resources/js/Pages/Admin/Employers/Index.jsx @@ -134,7 +134,7 @@ export default function SponsorsIndex({ sponsors, filters = {} }) { }; const handleDelete = (id) => { - if (confirm('Are you absolutely sure you want to permanently delete this sponsor registry?')) { + if (confirm('Are you absolutely sure you want to permanently delete this sponsor vetting dossier?')) { router.delete(`/admin/employers/${id}`); } }; @@ -169,15 +169,15 @@ export default function SponsorsIndex({ sponsors, filters = {} }) { }; return ( - - + +
{/* Statistics Ribbons */}
- Total Employers + Total Dossiers

{sponsors.total || 0}

@@ -265,7 +265,7 @@ export default function SponsorsIndex({ sponsors, filters = {} }) { handleSort('full_name')} className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14 pl-8 cursor-pointer select-none"> - Sponsor Info {sortField === 'full_name' && (sortOrder === 'desc' ? '↓' : '↑')} + Sponsor Vetting Dossier {sortField === 'full_name' && (sortOrder === 'desc' ? '↓' : '↑')} handleSort('city')} className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14 cursor-pointer select-none"> Location {sortField === 'city' && (sortOrder === 'desc' ? '↓' : '↑')} @@ -366,14 +366,14 @@ export default function SponsorsIndex({ sponsors, filters = {} }) { >
- Edit Registry + Edit Dossier
{!sponsor.is_verified && ( handleApprove(sponsor.id)}>
- Approve / Verify + Verify Dossier Vetting
)} @@ -384,7 +384,7 @@ export default function SponsorsIndex({ sponsors, filters = {} }) { >
- Suspend Sponsor + Suspend Dossier
) : ( @@ -394,7 +394,7 @@ export default function SponsorsIndex({ sponsors, filters = {} }) { >
- Activate Sponsor + Activate Dossier
)} @@ -405,7 +405,7 @@ export default function SponsorsIndex({ sponsors, filters = {} }) { >
- Delete Sponsor + Delete Dossier
@@ -417,7 +417,7 @@ export default function SponsorsIndex({ sponsors, filters = {} }) { ) : ( - No registered sponsors match the active search/filters criteria. + No sponsor dossiers match the active search/filters criteria. )} @@ -548,13 +548,13 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
- Sponsor Verification Action: + Dossier Verification Action: {!selectedSponsor?.is_verified ? ( ) : ( @@ -569,14 +569,14 @@ export default function SponsorsIndex({ sponsors, filters = {} }) { onClick={() => handleSuspend(selectedSponsor.id)} className="px-4 py-2.5 bg-red-600 text-white rounded-xl text-[10px] font-black uppercase tracking-widest shadow-md shadow-red-600/10 hover:bg-red-700 transition-all" > - Suspend Sponsor + Suspend Dossier ) : ( )}
diff --git a/resources/js/Pages/Employer/Auth/Register.jsx b/resources/js/Pages/Employer/Auth/Register.jsx index f70cb68..d5e6bad 100644 --- a/resources/js/Pages/Employer/Auth/Register.jsx +++ b/resources/js/Pages/Employer/Auth/Register.jsx @@ -41,8 +41,8 @@ export default function Register() { if (!data.phone.trim()) { newErrors.phone = 'Mobile number is required.'; - } else if (!/^\+?[0-9\s\-()]{7,20}$/.test(data.phone)) { - newErrors.phone = 'Please enter a valid mobile number format (7-20 digits).'; + } else if (!/^[0-9]{7,15}$/.test(data.phone)) { + newErrors.phone = 'Please enter a valid mobile number (7-15 digits without country code).'; } setErrors(newErrors); @@ -233,7 +233,7 @@ export default function Register() { type="tel" value={data.phone} onChange={(e) => handleInputChange('phone', e.target.value)} - placeholder="e.g. +971501234567" + placeholder="e.g. 501234567" className={`w-full px-3.5 py-3 rounded-xl border text-sm focus:outline-none focus:ring-2 transition-all ${ errors.phone ? 'border-red-500 focus:ring-red-500/20 focus:border-red-500' diff --git a/resources/js/Pages/Employer/Shortlist.jsx b/resources/js/Pages/Employer/Shortlist.jsx index 457de66..14dcb95 100644 --- a/resources/js/Pages/Employer/Shortlist.jsx +++ b/resources/js/Pages/Employer/Shortlist.jsx @@ -1,23 +1,49 @@ -import React, { useState } from 'react'; +import React, { useState, useMemo } from 'react'; import { Head, Link, router } from '@inertiajs/react'; import EmployerLayout from '../../Layouts/EmployerLayout'; import { - Bookmark, CheckCircle2, Globe2, Briefcase, DollarSign, MessageSquare, Trash2, - SlidersHorizontal + SlidersHorizontal, + ShieldCheck, + AlertTriangle, + Sparkles, + Star, + Eye, + User, + X, + HeartHandshake, + CheckCircle } from 'lucide-react'; +const getLanguageFlag = (lang) => { + const flags = { + 'English': '🇬🇧', + 'Arabic': '🇦🇪', + 'Tagalog': '🇵🇭', + 'Hindi': '🇮🇳', + 'Swahili': '🇰🇪', + 'French': '🇫🇷', + 'Indonesian': '🇮🇩' + }; + return flags[lang] || '🌐'; +}; + export default function Shortlist({ shortlistedWorkers }) { const [workers, setWorkers] = useState(shortlistedWorkers || []); + + // Comparison & Quick Preview State + const [comparisonIds, setComparisonIds] = useState([]); + const [previewWorker, setPreviewWorker] = useState(null); const removeWorker = (id) => { // Optimistic UI state update setWorkers(workers.filter(w => w.id !== id)); + setComparisonIds(comparisonIds.filter(i => i !== id)); // Persist change to database router.post(`/employer/shortlist/${id}/remove`, {}, { @@ -25,112 +51,234 @@ export default function Shortlist({ shortlistedWorkers }) { }); }; + const handleToggleComparison = (id) => { + if (comparisonIds.includes(id)) { + setComparisonIds(comparisonIds.filter(i => i !== id)); + } else { + if (comparisonIds.length >= 3) { + alert('You can compare a maximum of 3 candidates.'); + return; + } + setComparisonIds([...comparisonIds, id]); + } + }; + + const comparedWorkers = useMemo(() => { + return workers.filter(w => comparisonIds.includes(w.id)); + }, [workers, comparisonIds]); + return ( -
+

You have {workers.length} candidates saved for review

- Browse more workers + Browse more workers
{workers.length > 0 ? (
- {workers.map((worker) => ( -
- {/* Header Banner */} -
-
-
- {worker.name.charAt(0)} -
-
-
- {worker.name} - {worker.verified && } + {workers.map((worker) => { + const isComparing = comparisonIds.includes(worker.id); + + return ( +
+ + {/* Card Header with Photo Option and Availability indicator */} +
+
+ {/* Photo (optional) container */} +
+ {worker.photo ? ( + {worker.name} + ) : ( + + )}
-
- - {worker.nationality} + +
+
+ {worker.name} + {worker.verified && ( + + + Verified + + )} +
+ + {/* Emirates ID Status Verification Badge & Visa Status */} +
+
+ + {worker.emirates_id_status} +
+ {worker.visa_status && (worker.visa_status.toLowerCase().includes('tourist') || worker.visa_status.toLowerCase().includes('visit')) ? ( +
+ + {worker.visa_status} (Warning) +
+ ) : worker.visa_status && worker.visa_status.toLowerCase().includes('cancelled') ? ( +
+ + {worker.visa_status} (Warning) +
+ ) : ( +
+ {worker.visa_status} +
+ )} +
+ +
+ + {worker.nationality} + + {worker.age} yrs +
-
- -
- - {/* Body */} -
-
-
- - {worker.category} + {/* Top Right Availability status and trash remove button */} +
+ + {worker.availability_status} -
- - {worker.salary} AED / mo -
-
- -
-
- - Exp: {worker.experience} -
-
- Avail: {worker.availability} -
-
- -
- {worker.skills?.map(s => ( - - {s} - - ))} + +
-
- - View Profile - - - - Message - + {/* Card Body */} +
+
+ + {/* Category Pill, Ratings & Cost */} +
+ + {worker.category} + +
+ + {worker.salary} AED + /mo +
+
+ + + {/* Details Table */} +
+
+ + Exp: {worker.experience} +
+
+ + {worker.preferred_job_type} +
+
+ + {worker.rating || '4.5'} ({worker.reviews_count || '12'} reviews) +
+
+ + {/* Core exact platform Skills List */} +
+
Platform Skills
+
+ {worker.skills?.map(skill => ( + + {skill} + + ))} +
+
+ + {/* Languages Badges with Visual Flags */} +
+
Languages Spoken
+
+ {worker.languages?.map(lang => ( + + {getLanguageFlag(lang)} + {lang} + + ))} +
+
+
+ + {/* Actions block */} +
+
+ + + +
+ +
+ + Open Profile + + + + Message + +
+
-
- ))} + ); + })}
) : (
Your shortlist is empty

- Explore candidate profiles and click the bookmark icon to save them for later comparison. + Explore candidate profiles and add them to your saved shortlist.

)} + + {/* Worker Comparison Drawer */} + {comparisonIds.length > 0 && ( +
+
+
+ +

Comparing Workers ({comparedWorkers.length} of 3)

+
+ +
+ +
+ {comparedWorkers.map(w => ( +
+ + +
+
+ {w.photo ? ( + {w.name} + ) : ( + {w.name.charAt(0)} + )} +
+
+
{w.name}
+
{w.nationality} • {w.category}
+
+
+ +
+
Salary: {w.salary} AED
+
Age: {w.age} yrs
+
Job Type: {w.preferred_job_type}
+
Status: {w.availability_status}
+
+ + {/* Skills Display in Comparison */} +
+
Skills
+
+ {w.skills?.map(skill => ( + + {skill} + + ))} +
+
+
+ ))} +
+
+ )} + + {/* Quick Preview Modal overlay */} + {previewWorker && ( +
+
+ + +
+
+ {previewWorker.photo ? ( + {previewWorker.name} + ) : ( + + )} +
+
+
+

{previewWorker.name}

+ {previewWorker.verified && } +
+ +
+ + {previewWorker.emirates_id_status} +
+
{previewWorker.nationality} • {previewWorker.category}
+
+
+ + {previewWorker.bio ? ( +

+ "{previewWorker.bio}" +

+ ) : ( +

+ No bio details provided. +

+ )} + +
+
+
Expectations
+
{previewWorker.salary} AED / mo
+
+
+
Languages
+
+ {previewWorker.languages?.map(lang => ( + + {getLanguageFlag(lang)} + {lang} + + ))} +
+
+
+
Availability Status
+ + {previewWorker.availability_status} + +
+
+
Job Preference
+
{previewWorker.preferred_job_type}
+
+
+ + {/* Core Platform Skills inside Quick Preview */} +
+
Platform Skills
+
+ {previewWorker.skills?.map(skill => ( + + {skill} + + ))} +
+
+ +
+ + + View Full Details + +
+
+
+ )} +
); diff --git a/resources/js/Pages/Employer/Workers/Index.jsx b/resources/js/Pages/Employer/Workers/Index.jsx index 3d511eb..9cf641f 100644 --- a/resources/js/Pages/Employer/Workers/Index.jsx +++ b/resources/js/Pages/Employer/Workers/Index.jsx @@ -569,10 +569,6 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
- {/* Bio Snippet */} -

- "{worker.bio}" -

{/* Details Table */}
@@ -729,6 +725,17 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
Job Type: {w.preferred_job_type}
Status: {w.availability_status}
+ +
+
Skills
+
+ {w.skills?.map(skill => ( + + {skill} + + ))} +
+
))}