employer login issues fixed

This commit is contained in:
mohanmd 2026-06-19 17:56:47 +05:30
parent 56d8fc49aa
commit 0a497e56ed
18 changed files with 2722 additions and 2519 deletions

View File

@ -42,14 +42,7 @@ private function formatWorker(Worker $w)
// Visa status
$visaStatus = $w->visa_status;
// 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];
$photo = null;
$dbReviews = \App\Models\Review::where('worker_id', $w->id)->get();
$reviewsCount = $dbReviews->count();
@ -887,14 +880,7 @@ public function getWorkerDetail(Request $request, $id)
$visaStatusesList = ['Residence Visa', 'Tourist Visa', 'Employment Visa', 'Cancelled Visa', 'Own Visa'];
$visaStatus = $visaStatusesList[$w->id % 5];
// Profile photo
$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];
$photo = null;
// Fetch dynamic database reviews (Requirement 1)
$dbReviews = Review::where('worker_id', $w->id)->with('employer')->latest()->get();

View File

@ -298,6 +298,21 @@ public function setupProfile(Request $request)
*/
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',

View File

@ -77,13 +77,7 @@ public function index(Request $request)
$visaStatus = $w->visa_status;
// 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];
$photo = null;
$dbReviews = \App\Models\Review::where('worker_id', $w->id)->get();
$reviewsCount = $dbReviews->count();
@ -109,6 +103,11 @@ public function index(Request $request)
'bio' => $w->bio,
'rating' => $rating,
'reviews_count' => $reviewsCount,
'document_expiry_status' => $w->document_expiry_status,
'document_expiry_days' => $w->document_expiry_days,
'visa_expiry_date' => $w->visa_expiry_date,
'in_country' => (bool) $w->in_country,
'preferred_location' => $w->preferred_location,
];
})->filter()->values()->toArray();

View File

@ -75,14 +75,7 @@ public function index(Request $request)
// Visa status
$visaStatus = $w->visa_status;
// 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 // Test optional photo placeholder
];
$photo = $photos[$w->id % 4];
$photo = null;
$dbReviews = \App\Models\Review::where('worker_id', $w->id)->get();
$reviewsCount = $dbReviews->count();
@ -113,6 +106,9 @@ public function index(Request $request)
'reviews_count' => $reviewsCount,
'preferred_location' => $w->preferred_location,
'in_country' => (bool) $w->in_country,
'document_expiry_status' => $w->document_expiry_status,
'document_expiry_days' => $w->document_expiry_days,
'visa_expiry_date' => $w->visa_expiry_date,
];
})->filter()->values()->toArray();
@ -319,13 +315,7 @@ public function show($id)
$mappedSkills = ['cooking', 'cleaning'];
}
$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];
$photo = null;
// Fetch dynamic database reviews (Requirement 1)
$dbReviews = Review::where('worker_id', $w->id)->with('employer')->latest()->get();
@ -395,6 +385,23 @@ public function show($id)
'similar_workers' => $similarWorkers,
'status' => $w->status,
'availability_status' => $w->status,
'document_expiry_status' => $w->document_expiry_status,
'document_expiry_days' => $w->document_expiry_days,
'visa_expiry_date' => $w->visa_expiry_date,
'in_country' => (bool) $w->in_country,
'preferred_location' => $w->preferred_location,
'documents' => $w->documents->map(function ($doc) {
return [
'id' => $doc->id,
'type' => $doc->type,
'number' => $doc->number,
'issue_date' => $doc->issue_date,
'expiry_date' => $doc->expiry_date,
'ocr_accuracy' => $doc->ocr_accuracy,
'file_path' => $doc->file_path ? url($doc->file_path) : null,
'ocr_data' => $doc->ocr_data,
];
})->toArray(),
];
return Inertia::render('Employer/Workers/Show', [

View File

@ -56,6 +56,7 @@ class Worker extends Model
'emirates_id_status',
'document_expiry_days',
'document_expiry_status',
'visa_expiry_date',
];
public function getPassportStatusAttribute()
@ -87,6 +88,12 @@ public function getEmiratesIdStatusAttribute()
return $this->passport_status;
}
public function getVisaExpiryDateAttribute()
{
$visa = $this->documents->where('type', 'visa')->first();
return $visa ? $visa->expiry_date : null;
}
public function getDocumentExpiryDaysAttribute()
{
$visa = $this->documents->where('type', 'visa')->first();

View File

@ -29,71 +29,23 @@ public function run(): void
// Conversation 1: with Worker 1 (John Doe)
$w1 = $workers->get(0);
if ($w1) {
$conv1 = Conversation::create([
Conversation::create([
'employer_id' => $employer->id,
'worker_id' => $w1->id,
'created_at' => now()->subHours(2),
'updated_at' => now()->subHours(2),
]);
Message::create([
'conversation_id' => $conv1->id,
'sender_type' => 'employer',
'sender_id' => $employer->id,
'text' => 'Hello John, I saw your profile and I am interested in hiring you as an electrician.',
'created_at' => now()->subMinutes(110),
]);
Message::create([
'conversation_id' => $conv1->id,
'sender_type' => 'worker',
'sender_id' => $w1->id,
'text' => 'Hello, thank you for reaching out! Yes, I am available.',
'created_at' => now()->subMinutes(100),
]);
Message::create([
'conversation_id' => $conv1->id,
'sender_type' => 'employer',
'sender_id' => $employer->id,
'text' => 'Great, what is your salary expectation?',
'created_at' => now()->subMinutes(90),
]);
Message::create([
'conversation_id' => $conv1->id,
'sender_type' => 'worker',
'sender_id' => $w1->id,
'text' => 'My expectation is around 2500 AED per month.',
'created_at' => now()->subMinutes(80),
]);
}
// Conversation 2: with Worker 2 (Ali Hassan)
$w2 = $workers->get(1);
if ($w2) {
$conv2 = Conversation::create([
Conversation::create([
'employer_id' => $employer->id,
'worker_id' => $w2->id,
'created_at' => now()->subHour(),
'updated_at' => now()->subHour(),
]);
Message::create([
'conversation_id' => $conv2->id,
'sender_type' => 'employer',
'sender_id' => $employer->id,
'text' => 'Hi Ali, are you looking for job?',
'created_at' => now()->subMinutes(50),
]);
Message::create([
'conversation_id' => $conv2->id,
'sender_type' => 'worker',
'sender_id' => $w2->id,
'text' => 'Yes, I am available.',
'created_at' => now()->subMinutes(45),
]);
}
}
}

View File

@ -111,7 +111,7 @@ export default function Announcements({ initialAnnouncements }) {
setIsDialogOpen(false);
showToast(
t('community_charity_event_posted', 'COMMUNITY CHARITY EVENT POSTED!'),
t('community_charity_event_posted_desc', 'Instant Push Notification sent to all workers in Dubai! Morning-of reminder notification scheduled successfully.')
t('community_charity_event_posted_desc', 'Instant Push Notification sent to all workers! Morning-of reminder notification scheduled successfully.')
);
}
});
@ -128,8 +128,8 @@ export default function Announcements({ initialAnnouncements }) {
});
return (
<EmployerLayout title={t('charity_events_support_drives', 'Charity Events & Support Drives')}>
<Head title={`${t('community_charity_events_sponsor_hub', 'Community Charity Events - Sponsor Hub')}`} />
<EmployerLayout title="Charity Events">
<Head title={t('charity_events', 'Charity Events')} />
{toastMessage && (
<div className="fixed bottom-8 right-8 bg-slate-900 text-white px-6 py-5 rounded-[24px] shadow-2xl border border-slate-800 flex items-start space-x-3.5 z-[100] max-w-md animate-in slide-in-from-bottom-5 duration-350">
@ -150,7 +150,7 @@ export default function Announcements({ initialAnnouncements }) {
<div className="space-y-2 max-w-2xl">
<div className="inline-flex items-center space-x-2 px-3 py-1 bg-[#185FA5]/5 border border-[#185FA5]/10 rounded-full text-[10px] font-black uppercase tracking-wider text-[#185FA5]">
<Heart className="w-3.5 h-3.5 fill-[#185FA5] text-[#185FA5] animate-pulse" />
<span>{t('dubai_community_support_drives', 'Dubai Community Support Drives')}</span>
<span>{t('dubai_community_support_drives', 'Community Support Drives')}</span>
</div>
<h2 className="text-xl font-black text-slate-900 tracking-tight">{t('free_medical_checks_title', 'Free Medical Checks, Food Drives & Support Services')}</h2>
<p className="text-xs text-slate-500 font-bold leading-relaxed">
@ -177,10 +177,6 @@ export default function Announcements({ initialAnnouncements }) {
<form onSubmit={handleCreate} className="p-8 space-y-5 bg-white max-h-[80vh] overflow-y-auto">
<div className="space-y-4 p-4 bg-[#185FA5]/5 rounded-2xl border border-[#185FA5]/10 animate-in fade-in duration-200">
<div className="text-[10px] font-black text-[#185FA5] uppercase tracking-widest flex items-center gap-1.5">
<Heart className="w-3.5 h-3.5 fill-[#185FA5] text-[#185FA5]" />
<span>{t('charity_drive_metadata', 'Charity Drive Metadata (Dubai Support)')}</span>
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
<div className="space-y-1">
@ -356,7 +352,7 @@ export default function Announcements({ initialAnnouncements }) {
<input
type="text"
required
placeholder={t('location_details_placeholder', 'e.g. Al Quoz Community Center, Dubai')}
placeholder={t('location_details_placeholder', 'e.g. Al Quoz Community Center')}
value={newAnnouncement.location_details}
onChange={(e) => setNewAnnouncement({ ...newAnnouncement, location_details: e.target.value })}
className="w-full px-3.5 py-2.5 bg-white border border-slate-200 rounded-xl text-xs font-bold focus:ring-2 focus:ring-[#185FA5]/20 outline-none mb-2"
@ -449,10 +445,10 @@ export default function Announcements({ initialAnnouncements }) {
{filteredAnnouncements.length === 0 ? (
<div className="text-center py-24 bg-white rounded-[40px] border border-slate-200 shadow-sm text-slate-500 border-dashed md:col-span-2">
<div className="w-20 h-20 bg-slate-50 rounded-full flex items-center justify-center mx-auto mb-6">
<Megaphone className="w-10 h-10 text-slate-200" />
<Heart className="w-10 h-10 text-slate-200 fill-slate-50" />
</div>
<h3 className="font-black text-slate-900 text-lg uppercase tracking-tight">{t('no_announcements_title', 'No Announcements')}</h3>
<p className="font-bold text-xs text-slate-400 mt-1 uppercase tracking-widest">{t('no_announcements_desc', "You haven't posted any announcements yet")}</p>
<h3 className="font-black text-slate-900 text-lg uppercase tracking-tight">{t('no_charity_events_title', 'No Charity Events')}</h3>
<p className="font-bold text-xs text-slate-400 mt-1 uppercase tracking-widest">{t('no_charity_events_desc', "You haven't posted any charity events yet")}</p>
</div>
) : (
filteredAnnouncements.map((ann) => {

View File

@ -339,7 +339,7 @@ export default function Dashboard({
<div className="truncate">
<div className="font-bold text-sm text-slate-800 truncate flex items-center space-x-1">
<span>{worker.name}</span>
{worker.verified && <CheckCircle2 className="w-3.5 h-3.5 text-emerald-600 flex-shrink-0" />}
</div>
<div className="text-[10px] text-slate-500 truncate font-semibold">{worker.nationality}</div>
</div>

View File

@ -37,7 +37,7 @@ export default function Show({ conversation, initialMessages, conversations = []
const [input, setInput] = useState('');
const [isTyping, setIsTyping] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [showProfile, setShowProfile] = useState(true);
const [showHireConfirmModal, setShowHireConfirmModal] = useState(false);
const [showAttachmentModal, setShowAttachmentModal] = useState(false);
const [uploadedFiles, setUploadedFiles] = useState([]);
const [isRecording, setIsRecording] = useState(false);
@ -188,6 +188,16 @@ export default function Show({ conversation, initialMessages, conversations = []
}
};
const handleMarkHired = () => {
router.post(`/employer/workers/${conversation.worker_id}/mark-hired`, {}, {
preserveScroll: true,
onSuccess: () => {
toast.success(t('marked_hired', 'Worker successfully marked as Hired!'));
router.reload();
}
});
};
const stopRecording = () => {
if (mediaRecorderRef.current && isRecording) {
mediaRecorderRef.current.stop();
@ -196,8 +206,6 @@ export default function Show({ conversation, initialMessages, conversations = []
};
const chips = [
t('chip_looking_job', "Are you looking for a job?"),
t('chip_interview', "Are you available for a video interview today?"),
t('chip_salary', "What is your final expected salary?"),
t('chip_visa', "Can you transfer your visa immediately?")
];
@ -281,7 +289,7 @@ export default function Show({ conversation, initialMessages, conversations = []
<ArrowLeft className="w-5 h-5" />
</Link>
<div className="relative group cursor-pointer" onClick={() => setShowProfile(!showProfile)}>
<div className="relative group">
<div className="w-11 h-11 rounded-2xl bg-gradient-to-br from-[#185FA5] to-blue-600 text-white flex items-center justify-center font-black text-base shadow-md">
{conversation.worker_name.charAt(0)}
</div>
@ -303,29 +311,22 @@ export default function Show({ conversation, initialMessages, conversations = []
</div>
</div>
{/* WhatsApp Bridge Widget & Dossier Button */}
<div className="flex items-center space-x-2">
<a
href={`https://wa.me/971501112222?text=Hi, I am reviewing candidate ${conversation.worker_name} (${conversation.nationality}) on the platform. Please assist with hiring workflow details.`}
target="_blank"
rel="noreferrer"
className="px-3.5 py-2 bg-emerald-50 text-emerald-800 border border-emerald-200 hover:bg-emerald-100 rounded-xl transition-all shadow-xs flex items-center space-x-1.5 text-xs font-bold"
>
<MessageSquare className="w-4 h-4 text-emerald-600 fill-emerald-600" />
<span className="hidden sm:inline">{t('whatsapp_bridge', 'WhatsApp Bridge')}</span>
</a>
{conversation.worker_status !== 'hired' && conversation.worker_status !== 'hidden' ? (
<button
onClick={() => setShowProfile(!showProfile)}
className={`p-2.5 rounded-xl transition-all shadow-xs border ${
showProfile
? 'bg-blue-50 border-blue-100 text-[#185FA5]'
: 'bg-slate-50 border-slate-200 text-slate-400 hover:text-[#185FA5]'
}`}
title="Toggle Context Sidebar"
type="button"
onClick={() => setShowHireConfirmModal(true)}
className="px-3.5 py-2 bg-emerald-600 text-white hover:bg-emerald-700 rounded-xl transition-all shadow-md flex items-center space-x-1.5 text-xs font-black uppercase tracking-wider border border-emerald-700 cursor-pointer"
>
<Info className="w-4.5 h-4.5" />
<CheckCircle2 className="w-4 h-4 text-white" />
<span>{t('mark_hired_action', 'Mark Hired')}</span>
</button>
) : conversation.worker_status === 'hired' ? (
<span className="px-3.5 py-2 bg-slate-100 text-slate-500 rounded-xl text-xs font-black uppercase tracking-wider border border-slate-200 flex items-center space-x-1.5">
<CheckCircle2 className="w-4 h-4 text-emerald-600" />
<span>{t('hired', 'Hired')}</span>
</span>
) : null}
</div>
</div>
@ -485,85 +486,7 @@ export default function Show({ conversation, initialMessages, conversations = []
</div>
</div>
{/* Candidate Context Sidebar (Right) */}
{showProfile && (
<aside className="hidden lg:flex w-72 2xl:w-80 border-l border-slate-200 flex-col flex-shrink-0 bg-white z-20 animate-in slide-in-from-right-10 duration-350 overflow-y-auto">
<div className="p-6 space-y-6">
<div className="text-center space-y-3 pb-4 border-b border-slate-100">
<div className="w-20 h-20 rounded-[30px] bg-gradient-to-br from-[#185FA5] to-blue-600 text-white flex items-center justify-center font-black text-2xl mx-auto shadow-md relative">
{conversation.worker_name.charAt(0)}
<span className="absolute -bottom-1 -right-1 bg-emerald-500 p-1 rounded-lg border-2 border-white shadow">
<ShieldCheck className="w-4 h-4 text-white" />
</span>
</div>
<div>
<h4 className="font-extrabold text-base text-slate-900 tracking-tight">{conversation.worker_name}</h4>
</div>
</div>
<div className="grid grid-cols-2 gap-2 text-center text-xs font-bold">
<div className="bg-slate-50/70 p-3 rounded-xl border border-slate-100">
<DollarSign className="w-3.5 h-3.5 text-emerald-600 mx-auto mb-1" />
<div className="text-[8px] font-black text-slate-400 uppercase tracking-widest">{t('expected', 'Expected')}</div>
<div className="text-slate-800 mt-0.5 truncate">{conversation.salary}</div>
</div>
<div className="bg-slate-50/70 p-3 rounded-xl border border-slate-100">
<MapPin className="w-3.5 h-3.5 text-blue-600 mx-auto mb-1" />
<div className="text-[8px] font-black text-slate-400 uppercase tracking-widest">{t('region', 'Region')}</div>
<div className="text-slate-800 mt-0.5 truncate">{conversation.nationality}</div>
</div>
</div>
<div className="space-y-3">
<h5 className="text-[9px] font-black text-slate-400 uppercase tracking-widest pl-1">{t('compliance_profile', 'Compliance Profile')}</h5>
<div className="space-y-2">
{[
{ label: t('visa_transfer', 'Visa Transfer'), value: t('transferable', 'Transferable'), icon: Briefcase, color: 'text-blue-500' },
{ label: t('medical_status', 'Medical Status'), value: t('cleared', 'Cleared'), icon: ShieldCheck, color: 'text-emerald-500' },
{ label: t('passport_ocr', 'Passport OCR'), value: t('verified', 'Verified'), icon: FileText, color: 'text-slate-500' }
].map((item, i) => (
<div key={i} className="flex items-center justify-between p-3 bg-slate-50/40 border border-slate-100 rounded-xl text-[10px] font-bold text-slate-600">
<div className="flex items-center space-x-2">
<item.icon className={`w-3.5 h-3.5 ${item.color}`} />
<span className="text-slate-500">{item.label}</span>
</div>
<span className="text-slate-900 font-extrabold">{item.value}</span>
</div>
))}
</div>
</div>
<div className="pt-2 space-y-2">
<Link
href={`/employer/workers/${conversation.worker_id || conversation.id}`}
className="w-full bg-[#185FA5] hover:bg-[#144f8a] text-white py-3 rounded-xl text-xs font-black uppercase tracking-widest flex items-center justify-center space-x-1.5 shadow-xs"
>
<Eye className="w-4 h-4" />
<span>{t('open_worker_profile', 'Open Worker Profile')}</span>
</Link>
{conversation.worker_status !== 'hired' && conversation.worker_status !== 'hidden' && (
<button
type="button"
onClick={() => {
router.post(`/employer/workers/${conversation.worker_id}/mark-hired`, {}, {
preserveScroll: true,
onSuccess: () => {
toast.success(t('marked_hired', 'Worker successfully marked as Hired!'));
router.reload();
}
});
}}
className="w-full bg-emerald-650 bg-emerald-600 hover:bg-emerald-700 text-white py-3 rounded-xl text-xs font-black uppercase tracking-widest flex items-center justify-center space-x-1.5 shadow-md border border-emerald-700"
>
<CheckCircle2 className="w-4.5 h-4.5 text-white" />
<span>{t('mark_hired_action', 'Mark Hired')}</span>
</button>
)}
</div>
</div>
</aside>
)}
</div>
{/* Interactive File Attachment Modal Placeholder */}
@ -606,6 +529,44 @@ export default function Show({ conversation, initialMessages, conversations = []
</div>
)}
{/* Hired Confirmation Modal */}
{showHireConfirmModal && (
<div className="fixed inset-0 bg-slate-900/60 backdrop-blur-xs flex items-center justify-center p-4 z-50 animate-fade-in">
<div className="bg-white rounded-3xl w-full max-w-md p-6 relative animate-zoom-in text-center space-y-4 border border-slate-200 shadow-2xl">
<div className="mx-auto w-16 h-16 rounded-full bg-blue-50 border border-blue-100 flex items-center justify-center text-blue-600">
<CheckCircle2 className="w-8 h-8" />
</div>
<div className="space-y-2">
<h4 className="font-extrabold text-lg text-slate-900">{t('confirm_hire_title', 'Confirm Hiring')}</h4>
<p className="text-xs text-slate-600 leading-normal px-2">
{t('confirm_hire_desc', 'Verify this worker with your needs and make sure to hire this worker. If hired, you can view this worker in the Hired Workers page.')}
</p>
</div>
<div className="flex space-x-3 pt-2 text-xs font-bold">
<button
type="button"
onClick={() => setShowHireConfirmModal(false)}
className="flex-1 py-3 border border-slate-200 hover:bg-slate-50 rounded-xl transition-colors"
>
{t('cancel', 'Cancel')}
</button>
<button
type="button"
onClick={() => {
setShowHireConfirmModal(false);
handleMarkHired();
}}
className="flex-1 bg-emerald-600 hover:bg-emerald-700 text-white py-3 rounded-xl shadow-sm transition-colors"
>
{t('confirm_hire_action', 'Confirm Hire')}
</button>
</div>
</div>
</div>
)}
</EmployerLayout>
);
}

View File

@ -19,7 +19,8 @@ import {
X,
HeartHandshake,
CheckCircle,
MapPin
MapPin,
Calendar
} from 'lucide-react';
const getLanguageFlag = (lang) => {
@ -99,7 +100,7 @@ export default function Shortlist({ shortlistedWorkers }) {
{/* Card Header with Photo Option and Availability indicator */}
<div className="bg-slate-50/50 p-6 border-b border-slate-100 flex items-start justify-between relative">
<div className="flex items-center space-x-3.5">
<div className="flex items-center space-x-3.5 min-w-0 flex-1">
{/* Photo (optional) container */}
<div className="w-14 h-14 rounded-2xl bg-blue-100 text-[#185FA5] flex items-center justify-center font-bold text-xl border border-blue-200 shadow-inner flex-shrink-0 overflow-hidden relative">
{worker.photo ? (
@ -109,10 +110,10 @@ export default function Shortlist({ shortlistedWorkers }) {
)}
</div>
<div className="truncate">
<div className="min-w-0 flex-1">
<div className="font-extrabold text-base text-slate-900 group-hover:text-[#185FA5] transition-colors truncate flex items-center space-x-1.5">
<span>{worker.name}</span>
{worker.verified && (
{false && (
<span className="inline-flex items-center px-1.5 py-0.5 rounded-full text-[8px] font-black uppercase tracking-wider bg-emerald-500 text-white shadow-xs gap-0.5" title="OCR Verified">
<CheckCircle2 className="w-3 h-3" />
<span>{t('verified', 'Verified')}</span>
@ -122,18 +123,23 @@ export default function Shortlist({ shortlistedWorkers }) {
{/* Passport Status Verification Badge & Visa Status */}
<div className="flex flex-wrap gap-1 mt-0.5">
{worker.visa_expiry_date ? (
<div className={`flex items-center space-x-1 text-[9px] font-black uppercase px-1.5 py-0.5 rounded border ${
worker.passport_status.toLowerCase().includes('pending')
worker.document_expiry_days !== null && worker.document_expiry_days <= 30
? 'text-rose-700 bg-rose-50 border-rose-200 animate-pulse'
: worker.document_expiry_days !== null && worker.document_expiry_days <= 90
? 'text-amber-700 bg-amber-50 border-amber-200'
: 'text-emerald-700 bg-emerald-50 border-emerald-100'
: 'text-[#185FA5] bg-blue-50 border-blue-100'
}`}>
<ShieldCheck className={`w-3 h-3 flex-shrink-0 ${
worker.passport_status.toLowerCase().includes('pending')
? 'text-amber-600'
: 'text-emerald-600'
}`} />
<span>{worker.passport_status}</span>
<Calendar className="w-3 h-3 flex-shrink-0" />
<span>Visa Exp: {worker.visa_expiry_date}</span>
</div>
) : (
<div className="flex items-center space-x-1 text-[9px] font-black uppercase text-amber-700 bg-amber-50 px-1.5 py-0.5 rounded border border-amber-200">
<Calendar className="w-3 h-3 flex-shrink-0 text-amber-600" />
<span>Visa Expiry Pending</span>
</div>
)}
{worker.visa_status && (worker.visa_status.toLowerCase().includes('tourist') || worker.visa_status.toLowerCase().includes('visit')) ? (
<div className="flex items-center space-x-1 text-[9px] font-black uppercase text-amber-700 bg-amber-50 px-1.5 py-0.5 rounded border border-amber-200">
<AlertTriangle className="w-3 h-3 text-amber-600 flex-shrink-0 animate-bounce" />
@ -382,21 +388,26 @@ export default function Shortlist({ shortlistedWorkers }) {
<div>
<div className="flex items-center space-x-1">
<h4 className="font-extrabold text-lg text-slate-900">{previewWorker.name}</h4>
{previewWorker.verified && <CheckCircle2 className="w-4 h-4 text-emerald-600" />}
</div>
<div className={`flex items-center space-x-1 font-black text-[9px] uppercase tracking-wider ${
previewWorker.passport_status?.toLowerCase().includes('pending')
? 'text-amber-700 bg-amber-50 border border-amber-200 px-1.5 py-0.5 rounded'
: 'text-emerald-700 bg-emerald-50 border border-emerald-100 px-1.5 py-0.5 rounded'
{previewWorker.visa_expiry_date ? (
<div className={`flex items-center space-x-1 font-black text-[9px] uppercase tracking-wider border px-1.5 py-0.5 rounded w-fit ${
previewWorker.document_expiry_days !== null && previewWorker.document_expiry_days <= 30
? 'text-rose-700 bg-rose-50 border-rose-200 animate-pulse'
: previewWorker.document_expiry_days !== null && previewWorker.document_expiry_days <= 90
? 'text-amber-700 bg-amber-50 border border-amber-200'
: 'text-[#185FA5] bg-blue-50 border-blue-100'
}`}>
<ShieldCheck className={`w-3.5 h-3.5 flex-shrink-0 ${
previewWorker.passport_status?.toLowerCase().includes('pending')
? 'text-amber-600'
: 'text-emerald-600'
}`} />
<span>{previewWorker.passport_status}</span>
<Calendar className="w-3.5 h-3.5 flex-shrink-0" />
<span>Visa Exp: {previewWorker.visa_expiry_date}</span>
</div>
) : (
<div className="flex items-center space-x-1 font-black text-[9px] uppercase tracking-wider text-amber-700 bg-amber-50 border border-amber-200 px-1.5 py-0.5 rounded w-fit">
<Calendar className="w-3.5 h-3.5 flex-shrink-0 text-amber-600" />
<span>Visa Expiry Pending</span>
</div>
)}
<div className="text-xs text-slate-500 font-bold">{previewWorker.nationality}</div>
</div>
</div>

View File

@ -649,7 +649,7 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
{/* Card Header with Photo Option and Availability indicator */}
<div className="bg-slate-50/50 p-6 border-b border-slate-100 flex items-start justify-between relative">
<div className="flex items-center space-x-3.5">
<div className="flex items-center space-x-3.5 min-w-0 flex-1">
{/* Photo (optional) container */}
<div className="w-14 h-14 rounded-2xl bg-blue-100 text-[#185FA5] flex items-center justify-center font-bold text-xl border border-blue-200 shadow-inner flex-shrink-0 overflow-hidden relative">
{worker.photo ? (
@ -659,10 +659,10 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
)}
</div>
<div className="truncate">
<div className="min-w-0 flex-1">
<div className="font-extrabold text-base text-slate-900 group-hover:text-[#185FA5] transition-colors truncate flex items-center space-x-1.5">
<span>{worker.name}</span>
{worker.verified && (
{false && (
<span className="inline-flex items-center px-1.5 py-0.5 rounded-full text-[8px] font-black uppercase tracking-wider bg-emerald-500 text-white shadow-xs gap-0.5 animate-pulse" title="OCR Verified">
<CheckCircle2 className="w-3 h-3" />
<span>{t('verified')}</span>
@ -672,18 +672,23 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
{/* Passport Status Verification Badge & Visa Status */}
<div className="flex flex-wrap gap-1 mt-0.5">
{worker.visa_expiry_date ? (
<div className={`flex items-center space-x-1 text-[9px] font-black uppercase px-1.5 py-0.5 rounded border ${
worker.passport_status.toLowerCase().includes('pending')
worker.document_expiry_days !== null && worker.document_expiry_days <= 30
? 'text-rose-700 bg-rose-50 border-rose-200 animate-pulse'
: worker.document_expiry_days !== null && worker.document_expiry_days <= 90
? 'text-amber-700 bg-amber-50 border-amber-200'
: 'text-emerald-700 bg-emerald-50 border-emerald-100'
: 'text-[#185FA5] bg-blue-50 border-blue-100'
}`}>
<ShieldCheck className={`w-3 h-3 flex-shrink-0 ${
worker.passport_status.toLowerCase().includes('pending')
? 'text-amber-600'
: 'text-emerald-600'
}`} />
<span>{worker.passport_status}</span>
<Calendar className="w-3 h-3 flex-shrink-0" />
<span>Visa Exp: {worker.visa_expiry_date}</span>
</div>
) : (
<div className="flex items-center space-x-1 text-[9px] font-black uppercase text-amber-700 bg-amber-50 px-1.5 py-0.5 rounded border border-amber-200">
<Calendar className="w-3 h-3 flex-shrink-0 text-amber-600" />
<span>Visa Expiry Pending</span>
</div>
)}
{worker.visa_status && (worker.visa_status.toLowerCase().includes('tourist') || worker.visa_status.toLowerCase().includes('visit')) ? (
<div className="flex items-center space-x-1 text-[9px] font-black uppercase text-amber-700 bg-amber-50 px-1.5 py-0.5 rounded border border-amber-200">
<AlertTriangle className="w-3 h-3 text-amber-600 flex-shrink-0 animate-bounce" />
@ -722,7 +727,7 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
</div>
{/* Top Right Availability status and bookmark */}
<div className="flex flex-col items-end space-y-2">
<div className="flex flex-col items-end space-y-2 flex-shrink-0 ml-4">
<button
type="button"
onClick={() => toggleShortlist(worker.id)}
@ -952,21 +957,26 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
<div>
<div className="flex items-center space-x-1">
<h4 className="font-extrabold text-lg text-slate-900">{previewWorker.name}</h4>
{previewWorker.verified && <CheckCircle2 className="w-4 h-4 text-emerald-600" />}
</div>
<div className={`flex items-center space-x-1 font-black text-[9px] uppercase tracking-wider ${
previewWorker.passport_status?.toLowerCase().includes('pending')
? 'text-amber-700 bg-amber-50 border border-amber-200 px-1.5 py-0.5 rounded'
: 'text-emerald-700 bg-emerald-50 border border-emerald-100 px-1.5 py-0.5 rounded'
{previewWorker.visa_expiry_date ? (
<div className={`flex items-center space-x-1 font-black text-[9px] uppercase tracking-wider border px-1.5 py-0.5 rounded w-fit ${
previewWorker.document_expiry_days !== null && previewWorker.document_expiry_days <= 30
? 'text-rose-700 bg-rose-50 border-rose-200 animate-pulse'
: previewWorker.document_expiry_days !== null && previewWorker.document_expiry_days <= 90
? 'text-amber-700 bg-amber-50 border border-amber-200'
: 'text-[#185FA5] bg-blue-50 border-blue-100'
}`}>
<ShieldCheck className={`w-3.5 h-3.5 flex-shrink-0 ${
previewWorker.passport_status?.toLowerCase().includes('pending')
? 'text-amber-600'
: 'text-emerald-600'
}`} />
<span>{previewWorker.passport_status}</span>
<Calendar className="w-3.5 h-3.5 flex-shrink-0" />
<span>Visa Exp: {previewWorker.visa_expiry_date}</span>
</div>
) : (
<div className="flex items-center space-x-1 font-black text-[9px] uppercase tracking-wider text-amber-700 bg-amber-50 border border-amber-200 px-1.5 py-0.5 rounded w-fit">
<Calendar className="w-3.5 h-3.5 flex-shrink-0 text-amber-600" />
<span>Visa Expiry Pending</span>
</div>
)}
<div className="text-xs text-slate-500 font-bold">{previewWorker.nationality}</div>
</div>
</div>

View File

@ -24,7 +24,8 @@ import {
UserCheck,
AlertTriangle,
User,
Phone
Phone,
MapPin
} from 'lucide-react';
import { toast } from 'sonner';
@ -41,6 +42,34 @@ const getLanguageFlag = (lang) => {
return flags[lang] || '🌐';
};
const getExpiryStatus = (expiryDate) => {
if (!expiryDate) return { text: 'Expiry Pending', color: 'text-slate-500 bg-slate-50 border-slate-200' };
const expiry = new Date(expiryDate);
const today = new Date();
expiry.setHours(0,0,0,0);
today.setHours(0,0,0,0);
const diffTime = expiry.getTime() - today.getTime();
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
if (diffDays < 0) {
return {
text: `Expired \${Math.abs(diffDays)} days ago`,
color: 'text-rose-700 bg-rose-50 border-rose-200 font-black'
};
} else if (diffDays === 0) {
return {
text: 'Expires today',
color: 'text-amber-700 bg-amber-50 border-amber-200 font-black'
};
} else {
return {
text: `Valid until \${expiryDate} (Expires in \${diffDays} days)`,
color: 'text-emerald-700 bg-emerald-50 border-emerald-150 font-black'
};
}
};
export default function Show({ worker }) {
const { t } = useTranslation();
const passportDoc = worker.documents?.find(d => d.type === 'passport');
@ -54,6 +83,7 @@ export default function Show({ worker }) {
// Hiring flow states
const [availabilityStatus, setAvailabilityStatus] = useState(worker.availability_status);
const [showHiredModal, setShowHiredModal] = useState(false);
const [showHireConfirmModal, setShowHireConfirmModal] = useState(false);
// Reviews state
const [showReviewModal, setShowReviewModal] = useState(false);
@ -145,26 +175,7 @@ export default function Show({ worker }) {
<span>{t('back_to_find_workers', 'BACK TO FIND WORKERS')}</span>
</Link>
{/* Quick Profile Actions */}
<div className="flex items-center space-x-2">
<button
onClick={handleShare}
className="p-2.5 bg-white border border-slate-200 hover:bg-slate-50 rounded-xl text-slate-500 hover:text-[#185FA5] transition-colors flex items-center space-x-1 text-xs font-bold shadow-xs"
title="Share Profile link"
>
<Share2 className="w-4 h-4" />
<span className="hidden sm:inline">{t('share_profile', 'Share Profile')}</span>
</button>
<button
onClick={() => setShowReportModal(true)}
className="p-2.5 bg-white border border-slate-200 hover:bg-rose-50 rounded-xl text-slate-400 hover:text-rose-600 transition-colors flex items-center space-x-1 text-xs font-bold shadow-xs"
title="Report Abuse / Terms violations"
>
<Flag className="w-4 h-4" />
<span className="hidden sm:inline">{t('report_worker', 'Report Worker')}</span>
</button>
</div>
</div>
<div className="bg-white rounded-3xl border border-slate-200 shadow-sm overflow-hidden">
@ -183,28 +194,28 @@ export default function Show({ worker }) {
<div className="space-y-1.5">
<div className="flex items-center space-x-2">
<h1 className="text-2xl sm:text-3xl font-extrabold tracking-tight text-slate-900">{worker.name}</h1>
{worker.verified && (
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-[9px] font-black uppercase tracking-wider bg-emerald-500 text-white shadow-xs gap-1 animate-pulse" title="OCR Verified via UAE PDPL Compliant Pipeline">
<CheckCircle2 className="w-3.5 h-3.5" />
<span>{t('verified')}</span>
</span>
)}
</div>
{/* Passport verification status bar & Visa Status */}
<div className="flex flex-wrap gap-2">
{worker.visa_expiry_date ? (
<div className={`flex items-center space-x-1.5 px-2.5 py-1 rounded-lg border text-[9px] font-black uppercase tracking-wider w-fit ${
worker.passport_status.toLowerCase().includes('pending')
worker.document_expiry_days !== null && worker.document_expiry_days <= 30
? 'bg-rose-50 text-rose-700 border-rose-200 animate-pulse'
: worker.document_expiry_days !== null && worker.document_expiry_days <= 90
? 'bg-amber-50 text-amber-700 border-amber-200'
: 'bg-emerald-50 text-emerald-700 border-emerald-100'
: 'bg-blue-50 text-[#185FA5] border-blue-100'
}`}>
<ShieldCheck className={`w-3.5 h-3.5 ${
worker.passport_status.toLowerCase().includes('pending')
? 'text-amber-600'
: 'text-emerald-600'
}`} />
<span>{worker.passport_status}</span>
<Calendar className="w-3.5 h-3.5" />
<span>Visa Exp: {worker.visa_expiry_date}</span>
</div>
) : (
<div className="flex items-center space-x-1.5 px-2.5 py-1 rounded-lg border text-[9px] font-black uppercase tracking-wider w-fit bg-amber-50 text-amber-700 border-amber-200">
<Calendar className="w-3.5 h-3.5 text-amber-600" />
<span>Visa Expiry Pending</span>
</div>
)}
{worker.visa_status && (worker.visa_status.toLowerCase().includes('tourist') || worker.visa_status.toLowerCase().includes('visit')) ? (
<div className="flex items-center space-x-1.5 bg-amber-50 px-2.5 py-1 rounded-lg text-amber-800 border border-amber-200 text-[9px] font-black uppercase tracking-wider w-fit shadow-xs">
<AlertTriangle className="w-3.5 h-3.5 text-amber-600 animate-bounce" />
@ -267,7 +278,7 @@ export default function Show({ worker }) {
{availabilityStatus !== 'Hired' && (
<button
type="button"
onClick={handleMarkHired}
onClick={() => setShowHireConfirmModal(true)}
className="bg-emerald-600 hover:bg-emerald-700 text-white border border-emerald-700 px-8 py-3 rounded-xl font-black text-sm shadow-md transition-all flex items-center justify-center space-x-2 flex-1 sm:flex-initial scale-105 active:scale-100"
>
<CheckCircle2 className="w-4 h-4 text-white" />
@ -310,7 +321,7 @@ export default function Show({ worker }) {
<div className="flex items-center justify-between pb-3 border-b border-slate-200">
<div className="flex items-center space-x-2 text-xs text-slate-600 font-bold">
<DollarSign className="w-4 h-4 text-emerald-600" />
<DollarSign className="w-4 h-4 text-slate-500" />
<span>{t('salary_expectations', 'Salary Expectations')}</span>
</div>
<span className="font-extrabold text-slate-900 text-sm">{worker.salary} {t('aed', 'AED')} / {t('mo', 'mo')}</span>
@ -318,7 +329,7 @@ export default function Show({ worker }) {
<div className="flex items-center justify-between pb-3 border-b border-slate-200">
<div className="flex items-center space-x-2 text-xs text-slate-600 font-bold">
<Briefcase className="w-4 h-4 text-amber-600" />
<Briefcase className="w-4 h-4 text-slate-500" />
<span>{t('work_experience', 'Work Experience')}</span>
</div>
<span className="font-extrabold text-slate-900 text-xs">{worker.experience}</span>
@ -326,7 +337,7 @@ export default function Show({ worker }) {
<div className="flex items-center justify-between pb-3 border-b border-slate-200">
<div className="flex items-center space-x-2 text-xs text-slate-600 font-bold">
<Sparkles className="w-4 h-4 text-purple-600" />
<Sparkles className="w-4 h-4 text-slate-500" />
<span>{t('job_type', 'Job Type')}</span>
</div>
<span className="font-black text-slate-900 text-xs uppercase">{worker.preferred_job_type}</span>
@ -334,7 +345,7 @@ export default function Show({ worker }) {
<div className="flex items-center justify-between pb-3 border-b border-slate-200">
<div className="flex items-center space-x-2 text-xs text-slate-600 font-bold">
<HeartHandshake className="w-4 h-4 text-rose-500" />
<HeartHandshake className="w-4 h-4 text-slate-500" />
<span>{t('accommodation_preference', 'Accommodation Preference')}</span>
</div>
<span className="font-black text-slate-900 text-xs uppercase">{worker.live_in_out}</span>
@ -342,19 +353,31 @@ export default function Show({ worker }) {
<div className="flex items-center justify-between pb-3 border-b border-slate-200">
<div className="flex items-center space-x-2 text-xs text-slate-600 font-bold">
<ShieldCheck className={`w-4 h-4 ${worker.passport_status.toLowerCase().includes('pending') ? 'text-amber-600' : 'text-emerald-600'}`} />
<span>{t('passport_verification', 'Passport Verification')}</span>
<MapPin className="w-4 h-4 text-slate-500" />
<span>{t('preferred_location', 'Preferred Location')}</span>
</div>
<span className={`font-bold text-xs uppercase ${worker.passport_status.toLowerCase().includes('pending') ? 'text-amber-700' : 'text-emerald-700'}`}>{worker.passport_status}</span>
<span className="font-extrabold text-slate-900 text-xs capitalize">{worker.preferred_location || t('any', 'Any')}</span>
</div>
<div className="flex items-center justify-between pb-3 border-b border-slate-200">
<div className="flex items-center space-x-2 text-xs text-slate-600 font-bold">
<Globe2 className="w-4 h-4 text-slate-500" />
<span>{t('current_location_status', 'Current Location')}</span>
</div>
<span className="font-extrabold text-slate-900 text-xs capitalize">
{worker.in_country ? t('in_country', 'In-Country') : t('out_country', 'Out-of-Country')}
</span>
</div>
{worker.in_country && (
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2 text-xs text-slate-600 font-bold">
<FileText className="w-4 h-4 text-blue-600" />
<span>{t('visa_status', 'Visa Status')}</span>
<FileText className="w-4 h-4 text-slate-500" />
<span>{t('visa_type', 'Visa Type')}</span>
</div>
<span className="font-bold text-blue-700 text-xs uppercase">{worker.visa_status}</span>
<span className="font-extrabold text-slate-900 text-xs capitalize">{worker.visa_status || t('pending', 'Pending')}</span>
</div>
)}
</div>
{/* Languages Spoken with visual flags */}
@ -400,108 +423,232 @@ export default function Show({ worker }) {
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Document Card: Passport */}
<div className="bg-white border border-slate-200 rounded-2xl overflow-hidden shadow-xs hover:shadow-sm">
<div className="bg-slate-50 px-4 py-3 border-b border-slate-100 flex items-center justify-between">
<span className="text-[10px] font-black text-slate-500 uppercase tracking-widest">{t('passport_verified', 'Passport (PDPL Compliant)')}</span>
<ShieldCheck className="w-4 h-4 text-emerald-500" />
{passportDoc ? (
<div className="bg-white border border-slate-200 rounded-2xl overflow-hidden shadow-xs hover:shadow-sm lg:col-span-2">
<div className="bg-slate-50 px-5 py-3.5 border-b border-slate-100 flex items-center justify-between">
<div className="flex items-center space-x-2">
<span className="text-[11px] font-black text-slate-800 uppercase tracking-wider">{t('passport_details', 'Passport Details')}</span>
{(() => {
const expiryStatus = getExpiryStatus(passportDoc.expiry_date);
return (
<span className={`inline-flex items-center px-2 py-0.5 rounded text-[8px] font-black border uppercase tracking-wider \${expiryStatus.color}`}>
\${expiryStatus.text}
</span>
);
})()}
</div>
<div className="p-4 flex space-x-4">
<div className="w-20 h-28 bg-slate-950 rounded-lg flex-shrink-0 border border-slate-800 flex flex-col items-center justify-center p-1.5 text-center relative shadow-inner">
<ShieldCheck className="w-6 h-6 text-emerald-500 mb-1 animate-pulse" />
<span className="text-[6px] font-black text-emerald-500 uppercase tracking-tighter block">{t('scan_purged', 'Scan Purged')}</span>
<span className="text-[5px] font-medium text-slate-400 mt-1 leading-tight block">{t('uae_pdpl_compliant', 'UAE PDPL compliant')}</span>
{passportDoc.ocr_accuracy && (
<span className="text-[9px] font-bold text-emerald-600 bg-emerald-50/50 border border-emerald-100 px-2 py-0.5 rounded-full">
{passportDoc.ocr_accuracy}% Match
</span>
)}
</div>
<div className="flex-1 space-y-2">
<div className="p-6">
{passportDoc.ocr_data ? (
<div className="grid grid-cols-2 md:grid-cols-3 gap-y-4 gap-x-6">
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('given_names', 'Given Names')}</div>
<div className="text-xs font-bold text-slate-800">{passportDoc.ocr_data.given_names || 'N/A'}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('surname', 'Surname')}</div>
<div className="text-xs font-bold text-slate-800">{passportDoc.ocr_data.surname || 'N/A'}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('passport_number', 'Passport Number')}</div>
<div className="text-xs font-bold text-slate-800">{passportDoc.number}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('sex', 'Sex')}</div>
<div className="text-xs font-bold text-slate-800 uppercase">{passportDoc.ocr_data.sex || 'N/A'}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('date_of_birth', 'Date of Birth')}</div>
<div className="text-xs font-bold text-slate-800">{passportDoc.ocr_data.date_of_birth || 'N/A'}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('place_of_birth', 'Place of Birth')}</div>
<div className="text-xs font-bold text-slate-800">{passportDoc.ocr_data.place_of_birth || 'N/A'}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('issue_date', 'Date of Issue')}</div>
<div className="text-xs font-bold text-slate-800">{passportDoc.issue_date || 'N/A'}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('expiry_date', 'Date of Expiry')}</div>
<div className="text-xs font-bold text-emerald-600">{passportDoc.expiry_date || 'N/A'}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('issuing_country', 'Issuing Country')}</div>
<div className="text-xs font-bold text-slate-800">{passportDoc.ocr_data.issuing_country || 'N/A'}</div>
</div>
</div>
) : (
<div className="grid grid-cols-2 md:grid-cols-3 gap-y-4 gap-x-6">
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('document_id', 'Document ID')}</div>
<div className="text-xs font-bold text-slate-800">{passportDoc?.number || 'L82739102-UAE'}</div>
<div className="text-xs font-bold text-slate-800">{passportDoc.number}</div>
</div>
<div className="grid grid-cols-2 gap-1">
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('issue_date', 'Issue')}</div>
<div className="text-[10px] font-bold text-slate-800">{passportDoc?.issue_date || '12 Jan 2021'}</div>
<div className="text-xs font-bold text-slate-800">{passportDoc.issue_date || 'N/A'}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('expiry_date', 'Expiry')}</div>
<div className="text-[10px] font-bold text-emerald-600">{passportDoc?.expiry_date || '11 Jan 2031'}</div>
<div className="text-xs font-bold text-emerald-600">{passportDoc.expiry_date || 'N/A'}</div>
</div>
</div>
)}
<div className="mt-6 pt-4 border-t border-slate-100 flex items-center justify-between text-xs text-slate-500 font-medium">
<span className="flex items-center space-x-1.5">
<ShieldCheck className="w-4 h-4 text-emerald-600" />
<span>UAE PDPL Compliant Scan Storage (Permanently Purged)</span>
</span>
{passportDoc.file_path && (
<a
href={passportDoc.file_path}
target="_blank"
rel="noreferrer"
className="inline-flex items-center space-x-1 text-[9px] font-black text-blue-600 hover:text-blue-700 bg-blue-50 px-2.5 py-1.5 rounded-lg border border-blue-155 transition-colors uppercase tracking-wider"
>
<FileText className="w-3.5 h-3.5" />
<span>View Passport Scan</span>
</a>
)}
</div>
</div>
</div>
) : (
<div className="bg-white border border-slate-200 border-dashed rounded-2xl p-6 flex flex-col items-center justify-center text-center space-y-3 min-h-[170px] lg:col-span-2">
<div className="w-12 h-12 rounded-full bg-slate-50 border border-slate-100 flex items-center justify-center text-slate-400">
<FileText className="w-6 h-6" />
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('ocr_match', 'OCR Match')}</div>
<div className="flex items-center space-x-2 mt-0.5">
<div className="flex-1 h-1 bg-slate-100 rounded-full overflow-hidden">
<div className="h-full bg-emerald-500" style={{ width: `${passportDoc?.ocr_accuracy || 95}%` }} />
</div>
<span className="text-[9px] font-bold text-emerald-600">{passportDoc?.ocr_accuracy || 95}%</span>
</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('physical_scan_storage', 'Physical Scan Storage')}</div>
<span className="text-[7px] font-mono text-emerald-600 bg-emerald-50 px-1 py-0.5 rounded font-black uppercase tracking-wider block w-fit border border-emerald-100">{t('permanently_purged', 'Permanently Purged')}</span>
</div>
</div>
<div className="text-xs font-black text-slate-800 uppercase tracking-wider">{t('passport_pending', 'Passport Pending')}</div>
<p className="text-[10px] text-slate-500 font-medium mt-1 leading-normal max-w-[200px]">
{t('passport_pending_desc', 'Candidate has not uploaded a passport document yet.')}
</p>
</div>
</div>
)}
{/* Document Card: Visa */}
<div className="bg-white border border-slate-200 rounded-2xl overflow-hidden shadow-xs hover:shadow-sm">
<div className="bg-slate-50 px-4 py-3 border-b border-slate-100 flex items-center justify-between">
<span className="text-[10px] font-black text-slate-500 uppercase tracking-widest">{t('entry_visa_verified', 'Entry Visa (PDPL Compliant)')}</span>
{visaDoc ? (
<div className="bg-white border border-slate-200 rounded-2xl overflow-hidden shadow-xs hover:shadow-sm lg:col-span-2">
<div className="bg-slate-50 px-5 py-3.5 border-b border-slate-100 flex items-center justify-between">
<div className="flex items-center space-x-2">
<span className="text-[11px] font-black text-slate-800 uppercase tracking-wider">{t('visa_details', 'Entry Visa Details')}</span>
{(() => {
const expiryStatus = getExpiryStatus(passportDoc.expiry_date);
return (
<span className={`inline-flex items-center px-2 py-0.5 rounded text-[8px] font-black border uppercase tracking-wider \${expiryStatus.color}`}>
\${expiryStatus.text}
</span>
);
})()}
</div>
{worker.visa_status && (worker.visa_status.toLowerCase().includes('tourist') || worker.visa_status.toLowerCase().includes('visit') || worker.visa_status.toLowerCase().includes('cancelled')) ? (
<AlertTriangle className="w-4 h-4 text-amber-500 animate-pulse" />
<span className="inline-flex items-center bg-amber-50 border border-amber-200 text-[9px] font-black uppercase text-amber-700 px-2 py-0.5 rounded-full tracking-wide">
{t('transfer_required', 'Transfer Required')}
</span>
) : (
<ShieldCheck className="w-4 h-4 text-emerald-500" />
<span className="inline-flex items-center bg-emerald-50 border border-emerald-250 text-[9px] font-black uppercase text-emerald-700 px-2 py-0.5 rounded-full tracking-wide">
{t('clear_records', 'Clear Records')}
</span>
)}
</div>
<div className="p-4 flex space-x-4">
<div className="w-20 h-28 bg-slate-950 rounded-lg flex-shrink-0 border border-slate-800 flex flex-col items-center justify-center p-1.5 text-center relative shadow-inner">
<ShieldCheck className="w-6 h-6 text-emerald-500 mb-1 animate-pulse" />
<span className="text-[6px] font-black text-emerald-500 uppercase tracking-tighter block">{t('scan_purged', 'Scan Purged')}</span>
<span className="text-[5px] font-medium text-slate-400 mt-1 leading-tight block">{t('uae_pdpl_compliant', 'UAE PDPL compliant')}</span>
</div>
<div className="flex-1 space-y-2">
<div className="p-6">
{visaDoc.ocr_data ? (
<div className="grid grid-cols-2 md:grid-cols-3 gap-y-4 gap-x-6">
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('full_name', 'Full Name')}</div>
<div className="text-xs font-bold text-slate-800">{visaDoc.ocr_data.name || 'N/A'}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('file_number', 'File Number')}</div>
<div className="text-xs font-bold text-slate-800">{visaDoc.number}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('profession', 'Profession')}</div>
<div className="text-xs font-bold text-slate-800">{visaDoc.ocr_data.profession || worker.visa_status || 'N/A'}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('sponsor', 'Sponsor')}</div>
<div className="text-xs font-bold text-slate-800">{visaDoc.ocr_data.sponsor || 'N/A'}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('passport_number', 'Passport Number')}</div>
<div className="text-xs font-bold text-slate-800">{visaDoc.ocr_data.passport_number || 'N/A'}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('place_of_issue', 'Place of Issue')}</div>
<div className="text-xs font-bold text-slate-800">{visaDoc.ocr_data.place_of_issue || 'N/A'}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('issue_date', 'Date of Issue')}</div>
<div className="text-xs font-bold text-slate-800">{visaDoc.issue_date || 'N/A'}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('expiry_date', 'Date of Expiry')}</div>
<div className="text-xs font-bold text-emerald-600">{visaDoc.expiry_date || 'N/A'}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('accompanied_by', 'Accompanied By')}</div>
<div className="text-xs font-bold text-slate-800">{visaDoc.ocr_data.accompanied_by || 'N/A'}</div>
</div>
</div>
) : (
<div className="grid grid-cols-2 md:grid-cols-3 gap-y-4 gap-x-6">
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('document_id', 'Document ID')}</div>
<div className="text-xs font-bold text-slate-800">{visaDoc?.number || 'V2839102-DXB'}</div>
<div className="text-xs font-bold text-slate-800">{visaDoc.number}</div>
</div>
<div className="grid grid-cols-2 gap-1">
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('issue_date', 'Issue')}</div>
<div className="text-[10px] font-bold text-slate-800">{visaDoc?.issue_date || '12 Jan 2023'}</div>
<div className="text-xs font-bold text-slate-800">{visaDoc.issue_date || 'N/A'}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('expiry_date', 'Expiry')}</div>
<div className="text-[10px] font-bold text-emerald-600">{visaDoc?.expiry_date || '11 Jan 2026'}</div>
<div className="text-xs font-bold text-emerald-600">{visaDoc.expiry_date || 'N/A'}</div>
</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('visa_category', 'Visa Category')}</div>
<div className="text-xs font-bold text-slate-800">{worker.visa_status}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('verification_status', 'Verification status')}</div>
<div className="flex items-center space-x-1.5 mt-0.5">
{worker.visa_status && (worker.visa_status.toLowerCase().includes('tourist') || worker.visa_status.toLowerCase().includes('visit')) ? (
<span className="inline-flex items-center bg-amber-50 border border-amber-200 text-[8px] font-black uppercase text-amber-700 px-1.5 py-0.5 rounded tracking-wide">
{t('transfer_required', 'Transfer Required')}
)}
<div className="mt-6 pt-4 border-t border-slate-100 flex items-center justify-between text-xs text-slate-500 font-medium">
<span className="flex items-center space-x-1.5">
<ShieldCheck className="w-4 h-4 text-emerald-600" />
<span>UAE PDPL Compliant Scan Storage (Permanently Purged)</span>
</span>
) : worker.visa_status && worker.visa_status.toLowerCase().includes('cancelled') ? (
<span className="inline-flex items-center bg-rose-50 border border-rose-200 text-[8px] font-black uppercase text-rose-700 px-1.5 py-0.5 rounded tracking-wide animate-pulse">
{t('needs_regularization', 'Needs Regularization')}
</span>
) : (
<>
<CheckCircle2 className="w-3.5 h-3.5 text-emerald-600" />
<span className="text-[9px] font-bold text-slate-700">{t('clear_records', 'Clear Records')}</span>
</>
{visaDoc.file_path && (
<a
href={visaDoc.file_path}
target="_blank"
rel="noreferrer"
className="inline-flex items-center space-x-1 text-[9px] font-black text-blue-600 hover:text-blue-700 bg-blue-50 px-2.5 py-1.5 rounded-lg border border-blue-155 transition-colors uppercase tracking-wider"
>
<FileText className="w-3.5 h-3.5" />
<span>View Visa Scan</span>
</a>
)}
</div>
</div>
</div>
) : (
<div className="bg-white border border-slate-200 border-dashed rounded-2xl p-6 flex flex-col items-center justify-center text-center space-y-3 min-h-[170px] lg:col-span-2">
<div className="w-12 h-12 rounded-full bg-slate-50 border border-slate-100 flex items-center justify-center text-slate-400">
<FileText className="w-6 h-6" />
</div>
<div>
<div className="text-xs font-black text-slate-800 uppercase tracking-wider">{t('visa_pending', 'Visa Pending')}</div>
<p className="text-[10px] text-slate-500 font-medium mt-1 leading-normal max-w-[200px]">
{t('visa_pending_desc', 'Candidate has not uploaded a visa document yet.')}
</p>
</div>
</div>
)}
</div>
</div>
</div>
@ -740,6 +887,44 @@ export default function Show({ worker }) {
</div>
)}
{/* Hired Confirmation Modal */}
{showHireConfirmModal && (
<div className="fixed inset-0 bg-slate-900/60 backdrop-blur-xs flex items-center justify-center p-4 z-50 animate-fade-in">
<div className="bg-white rounded-3xl w-full max-w-md p-6 relative animate-zoom-in text-center space-y-4 border border-slate-200 shadow-2xl">
<div className="mx-auto w-16 h-16 rounded-full bg-blue-50 border border-blue-100 flex items-center justify-center text-blue-600">
<CheckCircle2 className="w-8 h-8" />
</div>
<div className="space-y-2">
<h4 className="font-extrabold text-lg text-slate-900">{t('confirm_hire_title', 'Confirm Hiring')}</h4>
<p className="text-xs text-slate-600 leading-normal px-2">
{t('confirm_hire_desc', 'Verify this worker with your needs and make sure to hire this worker. If hired, you can view this worker in the Hired Workers page.')}
</p>
</div>
<div className="flex space-x-3 pt-2 text-xs font-bold">
<button
type="button"
onClick={() => setShowHireConfirmModal(false)}
className="flex-1 py-3 border border-slate-200 hover:bg-slate-50 rounded-xl transition-colors"
>
{t('cancel', 'Cancel')}
</button>
<button
type="button"
onClick={() => {
setShowHireConfirmModal(false);
handleMarkHired();
}}
className="flex-1 bg-emerald-600 hover:bg-emerald-700 text-white py-3 rounded-xl shadow-sm transition-colors"
>
{t('confirm_hire_action', 'Confirm Hire')}
</button>
</div>
</div>
</div>
)}
</EmployerLayout>
);
}

View File

@ -47,8 +47,8 @@
"bookmark_list": "Bookmark list for interviews",
"open_shortlist": "Open shortlist book",
"discover_insights": "Discover Insights & Market Stats",
"dubai_insights": "Dubai General Market Insights",
"hired_dubai": "Hired using app in Dubai",
"dubai_insights": "General Market Insights",
"hired_dubai": "Hired using app in the region",
"cooking_care": "Cooking, Care, Driving",
"top_skills": "Top Skills Hired",
"turnover_rate": "Turnover rate",
@ -242,19 +242,19 @@
"charity_events_support_drives": "Charity Events & Support Drives",
"community_charity_events_sponsor_hub": "Community Charity Events - Sponsor Hub",
"community_charity_event_posted": "COMMUNITY CHARITY EVENT POSTED!",
"community_charity_event_posted_desc": "Instant Push Notification sent to all workers in Dubai! Morning-of reminder notification scheduled successfully.",
"dubai_community_support_drives": "Dubai Community Support Drives",
"community_charity_event_posted_desc": "Instant Push Notification sent to all workers! Morning-of reminder notification scheduled successfully.",
"dubai_community_support_drives": "Community Support Drives",
"free_medical_checks_title": "Free Medical Checks, Food Drives & Support Services",
"free_medical_checks_desc": "Organizing a support program? Share community campaigns directly. The platform will automatically push an instant pop-up notification to workers, followed by a morning-of event reminder to keep attendance strong.",
"search_charity_events_placeholder": "Search charity events & drives...",
"post_charity_event": "Post Charity Event",
"broadcast_support_program_desc": "Broadcast a support program, medical camp, or distribution drive to the worker community.",
"charity_drive_metadata": "Charity Drive Metadata (Dubai Support)",
"charity_drive_metadata": "Charity Drive Metadata",
"event_time_placeholder": "e.g. 9:00 AM - 4:00 PM",
"what_is_provided": "What is Being Provided",
"provided_items_placeholder": "e.g. Free Medical Check, Food Boxes, Supplies",
"location_details_pin": "Location Details & Pin URL",
"location_details_placeholder": "e.g. Al Quoz Community Center, Dubai",
"location_details_placeholder": "e.g. Al Quoz Community Center",
"maps_pin_link_placeholder": "Google Maps Pin Link (e.g. https://maps.app.goo.gl/xyz)",
"title_label": "Title",
"title_placeholder": "e.g. Free Dental checkup by Emirates Charity",
@ -422,5 +422,7 @@
"feedback_helps_sponsors_desc": "Your feedback helps other sponsors make secure and direct hiring choices.",
"share_your_experience": "Share Your Experience",
"share_experience_placeholder": "Tell other sponsors about childcare skills, cleaning, cooking style, driving safety, etc...",
"post_trust_review_action": "Post Trust Review"
"post_trust_review_action": "Post Trust Review",
"no_charity_events_title": "No Charity Events",
"no_charity_events_desc": "You haven't posted any charity events yet"
}

View File

@ -47,8 +47,8 @@
"bookmark_list": "साक्षात्कार के लिए बुकमार्क सूची",
"open_shortlist": "शॉर्टलिस्ट बुक खोलें",
"discover_insights": "इनसाइट्स और मार्केट आँकड़े खोजें",
"dubai_insights": "दुबई सामान्य बाज़ार इनसाइट्स",
"hired_dubai": "दुबई में ऐप का उपयोग करके काम पर रखा गया",
"dubai_insights": "सामान्य बाज़ार इनसाइट्स",
"hired_dubai": "क्षेत्र में ऐप का उपयोग करके काम पर रखा गया",
"cooking_care": "कुकिंग, केयर, ड्राइविंग",
"top_skills": "शीर्ष कौशल काम पर रखे गए",
"turnover_rate": "टर्नओवर दर",
@ -242,19 +242,19 @@
"charity_events_support_drives": "चैरिटी कार्यक्रम और सहायता अभियान",
"community_charity_events_sponsor_hub": "सामुदायिक चैरिटी कार्यक्रम - प्रायोजक केंद्र",
"community_charity_event_posted": "सामुदायिक चैरिटी कार्यक्रम पोस्ट किया गया!",
"community_charity_event_posted_desc": "दुबई में सभी कामगारों को त्वरित पुश अधिसूचना भेजी गई! कार्यक्रम की सुबह का अनुस्मारक सफलतापूर्वक निर्धारित किया गया।",
"dubai_community_support_drives": "दुबई सामुदायिक सहायता अभियान",
"community_charity_event_posted_desc": "सभी श्रमिकों को तत्काल पुश सूचना भेजी गई! सुबह का अनुस्मारक सफलतापूर्वक शेड्यूल किया गया।",
"dubai_community_support_drives": "सामुदायिक सहायता अभियान",
"free_medical_checks_title": "मुफ़्त चिकित्सा जांच, भोजन अभियान और सहायता सेवाएँ",
"free_medical_checks_desc": "एक सहायता कार्यक्रम का आयोजन कर रहे हैं? सीधे सामुदायिक अभियानों को साझा करें। प्लेटफ़ॉर्म स्वचालित रूप से कामगारों को एक त्वरित पॉप-अप अधिसूचना भेजेगा, जिसके बाद उपस्थिति को मजबूत रखने के लिए कार्यक्रम की सुबह अनुस्मारक भेजा जाएगा।",
"search_charity_events_placeholder": "चैरिटी कार्यक्रमों और अभियानों की खोज करें...",
"post_charity_event": "चैरिटी कार्यक्रम पोस्ट करें",
"broadcast_support_program_desc": "कामगार समुदाय के लिए एक सहायता कार्यक्रम, चिकित्सा शिविर या वितरण अभियान का प्रसारण करें।",
"charity_drive_metadata": "चैरिटी अभियान मेटाडेटा (दुबई सहायता)",
"charity_drive_metadata": "चैरिटी ड्राइव मेटाडेटा",
"event_time_placeholder": "जैसे 9:00 पूर्वाह्न - 4:00 अपराह्न",
"what_is_provided": "क्या प्रदान किया जा रहा है",
"provided_items_placeholder": "जैसे मुफ़्त चिकित्सा जांच, भोजन के डिब्बे, आपूर्ति",
"location_details_pin": "स्थान विवरण और पिन यूआरएल",
"location_details_placeholder": "जैसे अल कुओज़ कम्युनिटी सेंटर, दुबई",
"location_details_placeholder": "जैसे- अल क्वोज़ कम्युनिटी सेंटर",
"maps_pin_link_placeholder": "गूगल मैप्स पिन लिंक (जैसे https://maps.app.goo.gl/xyz)",
"title_label": "शीर्षक",
"title_placeholder": "जैसे अमीरात चैरिटी द्वारा मुफ्त दंत चिकित्सा जांच",
@ -422,5 +422,7 @@
"feedback_helps_sponsors_desc": "आपकी प्रतिक्रिया अन्य प्रायोजकों को सुरक्षित और प्रत्यक्ष भर्ती विकल्प चुनने में मदद करती है।",
"share_your_experience": "अपना अनुभव साझा करें",
"share_experience_placeholder": "अन्य प्रायोजकों को बाल देखभाल कौशल, सफाई, खाना पकाने की शैली, ड्राइविंग सुरक्षा आदि के बारे में बताएं...",
"post_trust_review_action": "समीक्षा पोस्ट करें"
"post_trust_review_action": "समीक्षा पोस्ट करें",
"no_charity_events_title": "कोई चैरिटी कार्यक्रम नहीं",
"no_charity_events_desc": "आपने अभी तक कोई चैरिटी कार्यक्रम पोस्ट नहीं किया है"
}

View File

@ -47,8 +47,8 @@
"bookmark_list": "Hifadhi orodha kwa ajili ya mahojiano",
"open_shortlist": "Fungua kitabu cha orodha fupi",
"discover_insights": "Gundua Maarifa na Takwimu za Soko",
"dubai_insights": "Maarifa ya Jumla ya Soko la Dubai",
"hired_dubai": "Walioajiriwa kwa kutumia programu huko Dubai",
"dubai_insights": "Maarifa ya Jumla ya Soko",
"hired_dubai": "Walioajiriwa kwa kutumia programu katika mkoa",
"cooking_care": "Kupika, Matunzo, Kuendesha Gari",
"top_skills": "Ujuzi Unaoongoza Kuajiriwa",
"turnover_rate": "Kiwango cha mabadiliko ya wafanyakazi",
@ -242,19 +242,19 @@
"charity_events_support_drives": "Matukio ya Misaada & Kampeni za Usaidizi",
"community_charity_events_sponsor_hub": "Matukio ya Misaada ya Jamii - Kituo cha Mdhamini",
"community_charity_event_posted": "TUKIO LA MISAADA LA JAMII LIMEPOSITIWA!",
"community_charity_event_posted_desc": "Arifa ya papo hapo imetumwa kwa wafanyakazi wote huko Dubai! Kikumbusho cha asubuhi ya tukio kimeratibiwa kwa mafanikio.",
"dubai_community_support_drives": "Kampeni za Usaidizi za Jamii ya Dubai",
"community_charity_event_posted_desc": "Arifa ya papo hapo imetumwa kwa wafanyakazi wote! Kikumbusho cha asubuhi ya tukio kimeratibiwa kwa mafanikio.",
"dubai_community_support_drives": "Kampeni za Usaidizi za Jamii",
"free_medical_checks_title": "Uchunguzi wa Matibabu Bila Malipo, Kampeni za Chakula na Huduma za Usaidizi",
"free_medical_checks_desc": "Unaratibu mpango wa usaidizi? Shiriki kampeni za jamii moja kwa moja. Jukwaa litatuma arifa ya papo hapo kwa wafanyakazi, ikifuatiwa na kikumbusho cha asubuhi ya tukio ili kudumisha mahudhurio mazuri.",
"search_charity_events_placeholder": "Tafuta matukio ya misaada na kampeni...",
"post_charity_event": "Positi Tukio la Misaada",
"broadcast_support_program_desc": "Tangaza mpango wa usaidizi, kambi ya matibabu, au kampeni ya ugawaji kwa jamii ya wafanyakazi.",
"charity_drive_metadata": "Metadata ya Kampeni ya Misaada (Usaidizi wa Dubai)",
"charity_drive_metadata": "Metadata ya Kampeni ya Misaada",
"event_time_placeholder": "m.f. 9:00 Asubuhi - 4:00 Jioni",
"what_is_provided": "Kile Kinachotolewa",
"provided_items_placeholder": "m.f. Uchunguzi wa Matibabu wa Bure, Masanduku ya Chakula, Vifaa",
"location_details_pin": "Maelezo ya Eneo & URL ya Ramani",
"location_details_placeholder": "m.f. Kituo cha Jamii cha Al Quoz, Dubai",
"location_details_placeholder": "m.f. Kituo cha Jamii cha Al Quoz",
"maps_pin_link_placeholder": "Kiungo cha Ramani ya Google Maps (m.f. https://maps.app.goo.gl/xyz)",
"title_label": "Kichwa",
"title_placeholder": "m.f. Uchunguzi wa meno wa bure na Shirika la Misaada la Emirates",
@ -422,5 +422,7 @@
"feedback_helps_sponsors_desc": "Maoni yako yanasaidia wafadhili wengine kufanya maamuzi salama na ya moja kwa moja ya kuajiri.",
"share_your_experience": "Shiriki Uzoefu Wako",
"share_experience_placeholder": "Waambie wafadhili wengine kuhusu ujuzi wa kulelea watoto, kusafisha, mtindo wa kupika, usalama wa kuendesha gari, n.k...",
"post_trust_review_action": "Chapisha Mapitio ya Uaminifu"
"post_trust_review_action": "Chapisha Mapitio ya Uaminifu",
"no_charity_events_title": "Hakuna Matukio ya Misaada",
"no_charity_events_desc": "Bado haujaposti matukio yoyote ya misaada"
}

View File

@ -47,8 +47,8 @@
"bookmark_list": "நேர்காணல்களுக்கான புக்மார்க் பட்டியல்",
"open_shortlist": "சுருக்கப்பட்டியல் புத்தகத்தைத் திற",
"discover_insights": "சந்தை புள்ளிவிவரங்கள் & நுண்ணறிவுகளைக் கண்டறியவும்",
"dubai_insights": "துபாய் பொதுச் சந்தை நுண்ணறிவு",
"hired_dubai": "துபாயில் செயலியைப் பயன்படுத்தி பணியமர்த்தப்பட்டவர்கள்",
"dubai_insights": "பொதுச் சந்தை நுண்ணறிவு",
"hired_dubai": "இப்பகுதியில் செயலியைப் பயன்படுத்தி பணியமர்த்தப்பட்டவர்கள்",
"cooking_care": "சமையல், பராமரிப்பு, வாகனம் ஓட்டுதல்",
"top_skills": "அதிகம் பணியமர்த்தப்பட்ட திறன்கள்",
"turnover_rate": "பணியாளர் வருவாய் விகிதம்",
@ -242,19 +242,19 @@
"charity_events_support_drives": "அறக்கட்டளை நிகழ்வுகள் & ஆதரவு இயக்கங்கள்",
"community_charity_events_sponsor_hub": "சமூக அறக்கட்டளை நிகழ்வுகள் - முதலாளி மையம்",
"community_charity_event_posted": "சமூக அறக்கட்டளை நிகழ்வு வெளியிடப்பட்டது!",
"community_charity_event_posted_desc": "துபாயில் உள்ள அனைத்து பணியாளர்களுக்கும் உடனடி புஷ் அறிவிப்பு அனுப்பப்பட்டது! நிகழ்வு காலையிலேயே நினைவூட்டல் வெற்றிகரமாக திட்டமிடப்பட்டது.",
"dubai_community_support_drives": "துபாய் சமூக ஆதரவு இயக்கங்கள்",
"community_charity_event_posted_desc": "அனைத்து ஊழியர்களுக்கும் உடனடி புஷ் அறிவிப்பு அனுப்பப்பட்டது! காலையில் நினைவூட்டல் வெற்றிகரமாக திட்டமிடப்பட்டது.",
"dubai_community_support_drives": "சமூக ஆதரவு இயக்கங்கள்",
"free_medical_checks_title": "இலவச மருத்துவ பரிசோதனைகள், உணவு இயக்கங்கள் & ஆதரவு சேவைகள்",
"free_medical_checks_desc": "ஆதரவு திட்டத்தை ஏற்பாடு செய்கிறீர்களா? சமூக பிரச்சாரங்களை நேரடியாக பகிரவும். தளம் தானாகவே பணியாளர்களுக்கு உடனடி புஷ் அறிவிப்பை அனுப்பும், அதைத் தொடர்ந்து வருகையை வலுவாக வைத்திருக்க நிகழ்வு காலையில் நினைவூட்டல் அனுப்பப்படும்.",
"search_charity_events_placeholder": "அறக்கட்டளை நிகழ்வுகள் & இயக்கங்களை தேடுக...",
"post_charity_event": "அறக்கட்டளை நிகழ்வை வெளியிடு",
"broadcast_support_program_desc": "பணியாளர் சமூகத்திற்கு ஒரு ஆதரவு திட்டம், மருத்துவ முகாம் அல்லது விநியோக இயக்கத்தை ஒளிபரப்பவும்.",
"charity_drive_metadata": "அறக்கட்டளை இயக்க மெட்டாடேட்டா (துபாய் ஆதரவு)",
"charity_drive_metadata": "அறக்கட்டளை இயக்க மெட்டாடேட்டா",
"event_time_placeholder": "உதாரணமாக 9:00 முற்பகல் - 4:00 பிற்பகல்",
"what_is_provided": "வழங்கப்படுவது என்ன",
"provided_items_placeholder": "உதாரணமாக இலவச மருத்துவ பரிசோதனை, உணவுப் பெட்டிகள், பொருட்கள்",
"location_details_pin": "இட விவரங்கள் & கூகுள் மேப் இணைப்பு",
"location_details_placeholder": "உதாரணமாக அல் கூஸ் சமூக மையம், துபாய்",
"location_details_placeholder": "எ.கா. அல் கோஸ் சமூக மையம்",
"maps_pin_link_placeholder": "கூகுள் மேப்ஸ் இருப்பிட இணைப்பு (உதாரணமாக https://maps.app.goo.gl/xyz)",
"title_label": "தலைப்பு",
"title_placeholder": "உதாரணமாக எமிரேட்ஸ் அறக்கட்டளையின் இலவச பல் பரிசோதனை",
@ -422,5 +422,7 @@
"feedback_helps_sponsors_desc": "உங்கள் கருத்து மற்ற முதலாளிகளுக்கு பாதுகாப்பான மற்றும் நேரடி பணியமர்த்தல் முடிவுகளை எடுக்க உதவுகிறது.",
"share_your_experience": "உங்கள் அனுபவத்தைப் பகிர்ந்து கொள்ளுங்கள்",
"share_experience_placeholder": "குழந்தை பராமரிப்பு திறன்கள், சுத்தம் செய்தல், சமையல் நடைமுறை, ஓட்டுநர் பாதுகாப்பு போன்றவற்றைப் பற்றி மற்ற முதலாளிகளுக்குத் தெரியப்படுத்துங்கள்...",
"post_trust_review_action": "மதிப்புரையை வெளியிடு"
"post_trust_review_action": "மதிப்புரையை வெளியிடு",
"no_charity_events_title": "அறக்கட்டளை நிகழ்வுகள் எதுவுமில்லை",
"no_charity_events_desc": "நீங்கள் இன்னும் எந்த அறக்கட்டளை நிகழ்வையும் வெளியிடவில்லை"
}

View File

@ -47,8 +47,8 @@
"bookmark_list": "I-save ang listahan para sa mga interview",
"open_shortlist": "Buksan ang shortlist book",
"discover_insights": "Tuklasin ang mga Insight at Stats sa Merkado",
"dubai_insights": "Mga Pangkalahatang Insight sa Merkado sa Dubai",
"hired_dubai": "Na-hire gamit ang app sa Dubai",
"dubai_insights": "Mga Pangkalahatang Insight sa Merkado",
"hired_dubai": "Na-hire gamit ang app sa rehiyon",
"cooking_care": "Pagluluto, Pag-aalaga, Pagmamaneho",
"top_skills": "Mga Pangunahing Kasanayang Na-hire",
"turnover_rate": "Turnover rate",
@ -242,19 +242,19 @@
"charity_events_support_drives": "Mga Kaganapang Kawanggawa & Kampanya sa Usaid",
"community_charity_events_sponsor_hub": "Mga Kaganapang Kawanggawa sa Komunidad - Kituo ng Sponsor",
"community_charity_event_posted": "NAI-POST NA ANG CHARITY EVENT NG KOMUNIDAD!",
"community_charity_event_posted_desc": "Agad na nagpadala ng push notification sa lahat ng manggagawa sa Dubai! Matagumpay na nai-schedule ang umagang paalala.",
"dubai_community_support_drives": "Mga Kampanya sa Suporta ng Komunidad sa Dubai",
"community_charity_event_posted_desc": "Agad na nagpadala ng push notification sa lahat ng manggagawa! Matagumpay na nai-schedule ang umagang paalala.",
"dubai_community_support_drives": "Mga Kampanya sa Suporta ng Komunidad",
"free_medical_checks_title": "Libreng Ugnayan sa Kalusugan, Pamamahagi ng Pagkain & mga Serbisyo sa Suporta",
"free_medical_checks_desc": "Nag-oorganisa ng programang suporta? Ibahagi ang mga kampanya ng komunidad nang direkta. Awtomatikong magpapadala ng push notification ang platform sa mga manggagawa, na susundan ng paalala sa umaga ng kaganapan.",
"search_charity_events_placeholder": "Maghanap ng mga kawanggawa & kampanya...",
"post_charity_event": "I-post ang Charity Event",
"broadcast_support_program_desc": "Ipalaganap ang programang suporta, medical camp, o pamamahagi sa komunidad ng mga manggagawa.",
"charity_drive_metadata": "Metadata ng Charity Drive (Suporta sa Dubai)",
"charity_drive_metadata": "Metadata ng Charity Drive",
"event_time_placeholder": "hal. 9:00 AM - 4:00 PM",
"what_is_provided": "Ano ang Ipinapamahagi",
"provided_items_placeholder": "hal. Libreng Dental Check, Kahon ng Pagkain, mga Kagamitan",
"location_details_pin": "Mga Detalye ng Lokasyon & Pin Link",
"location_details_placeholder": "hal. Al Quoz Community Center, Dubai",
"location_details_placeholder": "hal. Al Quoz Community Center",
"maps_pin_link_placeholder": "Link ng Google Maps (hal. https://maps.app.goo.gl/xyz)",
"title_label": "Pamagat",
"title_placeholder": "hal. Libreng Dental Checkup ng Emirates Charity",
@ -422,5 +422,7 @@
"feedback_helps_sponsors_desc": "Ang iyong puna ay tumutulong sa iba pang mga sponsor na gumawa ng ligtas at direktang mga desisyon sa pag-hire.",
"share_your_experience": "Ibahagi ang Iyong Karanasan",
"share_experience_placeholder": "Sabihin sa iba pang mga sponsor ang tungkol sa mga kasanayan sa childcare, paglilinis, estilo ng pagluluto, kaligtasan sa pagmamaneho, atbp...",
"post_trust_review_action": "Mag-post ng Review ng Tiwala"
"post_trust_review_action": "Mag-post ng Review ng Tiwala",
"no_charity_events_title": "Walang mga Charity Event",
"no_charity_events_desc": "Hindi ka pa nagpo-post ng anumang mga charity event"
}

View File

@ -537,6 +537,70 @@ public function test_register_passport_and_visa_with_direct_json_payload()
$this->assertEquals('Sponsor Name', $visaDoc->ocr_data['sponsor']);
}
/**
* Test worker passport and visa registration when they are passed as JSON strings.
*/
public function test_register_passport_and_visa_with_json_encoded_strings()
{
$response = $this->postJson('/api/workers/register', [
'name' => 'JSON String Worker',
'phone' => '+971509999999',
'salary' => 3000,
'password' => 'secret123',
'language' => 'EN',
'passport' => json_encode([
'authority' => 'Authority',
'date_of_birth' => '14/05/1990',
'date_of_expiry' => '05/07/2028',
'date_of_issue' => '05/07/2018',
'document_type' => 'P',
'given_names' => 'Johnny',
'issuing_country' => 'PHL',
'nationality' => 'PHL',
'passport_number' => 'Z99B1234',
'personal_number' => '28',
'place_of_birth' => 'Manila',
'sex' => 'M',
'surname' => 'Test',
]),
'visa' => json_encode([
'accompanied_by' => '0',
'expiry_date' => '2027-02-15',
'file_number' => '202/2022/2840234',
'id_number' => '784198839607840',
'issue_date' => '2025-02-16',
'name' => 'Johnny Test',
'passport_number' => 'Z99B1234',
'place_of_issue' => 'Abu Dhabi',
'profession' => 'HELPER',
'sponsor' => 'Sponsor Name',
])
]);
$response->assertStatus(201);
$workerId = $response->json('data.worker.id');
$this->assertDatabaseHas('workers', [
'id' => $workerId,
'name' => 'Johnny Test',
'phone' => '+971509999999',
'verified' => true,
'visa_status' => 'HELPER',
]);
$this->assertDatabaseHas('worker_documents', [
'worker_id' => $workerId,
'type' => 'passport',
'number' => 'Z99B1234',
]);
$this->assertDatabaseHas('worker_documents', [
'worker_id' => $workerId,
'type' => 'visa',
'number' => '202/2022/2840234',
]);
}
/**
* Test updating profile with new API fields.
*/