job api, menu

This commit is contained in:
mohanmd 2026-07-01 14:24:17 +05:30
parent e191516573
commit 685ac1eabd
10 changed files with 405 additions and 99 deletions

View File

@ -34,6 +34,7 @@ public function listJobs(Request $request)
'description' => $job->description, 'description' => $job->description,
'requirements' => $job->requirements, 'requirements' => $job->requirements,
'company_name' => $job->employer->employerProfile->company_name ?? 'Private Sponsor', 'company_name' => $job->employer->employerProfile->company_name ?? 'Private Sponsor',
'posted_by' => $job->employer->name ?? 'Private Sponsor',
'posted_at' => $job->created_at->toIso8601String(), 'posted_at' => $job->created_at->toIso8601String(),
]; ];
}); });
@ -77,6 +78,7 @@ public function jobDetails($id)
'description' => $job->description, 'description' => $job->description,
'requirements' => $job->requirements, 'requirements' => $job->requirements,
'company_name' => $job->employer->employerProfile->company_name ?? 'Private Sponsor', 'company_name' => $job->employer->employerProfile->company_name ?? 'Private Sponsor',
'posted_by' => $job->employer->name ?? 'Private Sponsor',
'posted_at' => $job->created_at->toIso8601String(), 'posted_at' => $job->created_at->toIso8601String(),
] ]
] ]
@ -154,6 +156,7 @@ public function appliedJobs(Request $request)
'salary' => (int) $job->salary, 'salary' => (int) $job->salary,
'job_type' => $job->job_type, 'job_type' => $job->job_type,
'company_name' => $job->employer->employerProfile->company_name ?? 'Private Sponsor', 'company_name' => $job->employer->employerProfile->company_name ?? 'Private Sponsor',
'posted_by' => $job->employer->name ?? 'Private Sponsor',
] : null ] : null
]; ];
}); });
@ -697,6 +700,7 @@ public function appliedJobDetail(Request $request, $id)
'description' => $job->description, 'description' => $job->description,
'requirements' => $job->requirements, 'requirements' => $job->requirements,
'company_name' => $job->employer->employerProfile->company_name ?? 'Private Sponsor', 'company_name' => $job->employer->employerProfile->company_name ?? 'Private Sponsor',
'posted_by' => $job->employer->name ?? 'Private Sponsor',
'posted_at' => $job->created_at->toIso8601String(), 'posted_at' => $job->created_at->toIso8601String(),
] : null ] : null
] ]

View File

@ -309,4 +309,102 @@ public function destroy($id)
return redirect()->route('employer.jobs')->with('success', 'Job deleted successfully.'); return redirect()->route('employer.jobs')->with('success', 'Job deleted successfully.');
} }
public function allApplicants(Request $request)
{
$user = $this->resolveCurrentUser();
if (!$user) {
return redirect()->route('employer.login');
} }
if (!$this->checkJobAccess($user)) {
return redirect()->route('employer.subscription')
->with('error', 'Your current plan does not support the Job Apply module. Please upgrade to a Premium plan to access this feature.');
}
$jobIds = JobPost::where('employer_id', $user->id)->pluck('id');
$dbApplications = JobApplication::whereIn('job_id', $jobIds)
->with(['worker.skills', 'jobPost'])
->get();
$applicants = $dbApplications->map(function ($app) use ($user) {
$worker = $app->worker;
if (!$worker) return null;
$isSaved = \App\Models\Shortlist::where('employer_id', $user->id)
->where('worker_id', $worker->id)
->exists();
return [
'id' => $worker->id,
'application_id' => $app->id,
'name' => $worker->name,
'nationality' => $worker->nationality,
'salary' => (int) $worker->salary,
'experience' => ($worker->experience_years ?? 3) . ' Years',
'status' => ucfirst($app->status),
'notes' => $app->notes,
'status_history' => $app->status_history ?: [],
'match_score' => $worker->match_score ?? 90,
'is_saved' => $isSaved,
'job_title' => $app->jobPost->title ?? 'N/A',
'job_id' => $app->job_id,
];
})->filter()->values()->toArray();
return Inertia::render('Employer/Jobs/Applicants', [
'job' => null,
'applicants' => $applicants,
]);
}
public function shortlistedApplicants(Request $request)
{
$user = $this->resolveCurrentUser();
if (!$user) {
return redirect()->route('employer.login');
}
if (!$this->checkJobAccess($user)) {
return redirect()->route('employer.subscription')
->with('error', 'Your current plan does not support the Job Apply module. Please upgrade to a Premium plan to access this feature.');
}
$jobIds = JobPost::where('employer_id', $user->id)->pluck('id');
$dbApplications = JobApplication::whereIn('job_id', $jobIds)
->where('status', 'shortlisted')
->with(['worker.skills', 'jobPost'])
->get();
$applicants = $dbApplications->map(function ($app) use ($user) {
$worker = $app->worker;
if (!$worker) return null;
$isSaved = \App\Models\Shortlist::where('employer_id', $user->id)
->where('worker_id', $worker->id)
->exists();
return [
'id' => $worker->id,
'application_id' => $app->id,
'name' => $worker->name,
'nationality' => $worker->nationality,
'salary' => (int) $worker->salary,
'experience' => ($worker->experience_years ?? 3) . ' Years',
'status' => ucfirst($app->status),
'notes' => $app->notes,
'status_history' => $app->status_history ?: [],
'match_score' => $worker->match_score ?? 90,
'is_saved' => $isSaved,
'job_title' => $app->jobPost->title ?? 'N/A',
'job_id' => $app->job_id,
];
})->filter()->values()->toArray();
return Inertia::render('Employer/Jobs/Applicants', [
'job' => null,
'applicants' => $applicants,
'isShortlistedOnly' => true,
]);
}
}

View File

@ -3932,6 +3932,10 @@
"type": "string", "type": "string",
"example": "Ahmad Tech Ltd" "example": "Ahmad Tech Ltd"
}, },
"posted_by": {
"type": "string",
"example": "Jane Sponsor"
},
"posted_at": { "posted_at": {
"type": "string", "type": "string",
"format": "date-time", "format": "date-time",
@ -4036,6 +4040,10 @@
"type": "string", "type": "string",
"example": "Ahmad Tech Ltd" "example": "Ahmad Tech Ltd"
}, },
"posted_by": {
"type": "string",
"example": "Jane Sponsor"
},
"posted_at": { "posted_at": {
"type": "string", "type": "string",
"format": "date-time", "format": "date-time",
@ -4238,6 +4246,10 @@
"company_name": { "company_name": {
"type": "string", "type": "string",
"example": "Ahmad Tech Ltd" "example": "Ahmad Tech Ltd"
},
"posted_by": {
"type": "string",
"example": "Jane Sponsor"
} }
} }
} }
@ -6521,7 +6533,7 @@
"Employer/JobApplicants" "Employer/JobApplicants"
], ],
"summary": "Update Job Application Status", "summary": "Update Job Application Status",
"description": "Allows employers to update the status of a job application (shortlisted, hired, rejected, etc.). Subscription limits (contact limits) are enforced when moving a worker to shortlisted or hired status for the first time.", "description": "Allows employers to update the status of a job application (e.g. shortlist, contacted, interview scheduled, selected, hired, rejected) with optional internal notes. Subscription limits (contact limits) are enforced when moving a worker to a contact status (shortlisted, contacted, interview_scheduled, selected, hired) for the first time.",
"security": [ "security": [
{ {
"bearerAuth": [] "bearerAuth": []
@ -6553,11 +6565,19 @@
"enum": [ "enum": [
"applied", "applied",
"shortlisted", "shortlisted",
"hired", "contacted",
"rejected" "interview scheduled",
"interview_scheduled",
"selected",
"rejected",
"hired"
], ],
"example": "shortlisted", "example": "shortlisted",
"description": "The new status of the application." "description": "The new status of the application."
},
"notes": {
"type": "string",
"description": "Optional internal notes about the applicant/interview"
} }
} }
} }
@ -6602,6 +6622,17 @@
"type": "string", "type": "string",
"example": "shortlisted" "example": "shortlisted"
}, },
"notes": {
"type": "string",
"example": "Scheduled phone screen for tomorrow.",
"nullable": true
},
"status_history": {
"type": "array",
"items": {
"type": "object"
}
},
"created_at": { "created_at": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
@ -6623,7 +6654,7 @@
"description": "Unauthenticated or invalid Bearer token." "description": "Unauthenticated or invalid Bearer token."
}, },
"403": { "403": {
"description": "Contact limit reached under the current subscription plan.", "description": "Contact limit reached under the current subscription plan or unauthorized.",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
@ -7423,78 +7454,6 @@
} }
} }
}, },
"/employers/applications/{id}/status": {
"post": {
"tags": [
"Employer/Jobs"
],
"summary": "Update Job Application Status",
"description": "Allows employers to update the status of a job application (e.g. shortlist, interview, hire, reject) with optional internal notes. Enforces plan-based contact limits and automatically adds to shortlist/Saved Workers.",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"description": "The Job Application ID",
"schema": {
"type": "integer"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"status"
],
"properties": {
"status": {
"type": "string",
"enum": [
"applied",
"shortlisted",
"contacted",
"interview scheduled",
"interview_scheduled",
"selected",
"rejected",
"hired"
],
"description": "New hiring stage status"
},
"notes": {
"type": "string",
"description": "Optional internal notes about the applicant/interview"
}
}
}
}
}
},
"responses": {
"200": {
"description": "Status updated successfully.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": { "type": "boolean", "example": true },
"message": { "type": "string", "example": "Application status updated successfully." }
}
}
}
}
},
"403": {
"description": "Plan limits reached or unauthorized."
}
}
}
},
"/workers/applied-jobs/{id}": { "/workers/applied-jobs/{id}": {
"get": { "get": {
"tags": [ "tags": [

View File

@ -1,4 +1,4 @@
import React, { useEffect } from 'react'; import React, { useEffect, useState } from 'react';
import { Link, usePage } from '@inertiajs/react'; import { Link, usePage } from '@inertiajs/react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { import {
@ -19,7 +19,9 @@ import {
Heart, Heart,
FileText, FileText,
ChevronDown, ChevronDown,
LifeBuoy ChevronRight,
LifeBuoy,
Users
} from 'lucide-react'; } from 'lucide-react';
import { import {
DropdownMenu, DropdownMenu,
@ -63,12 +65,72 @@ export default function EmployerLayout({ children, title, fullPage = false }) {
const isSubActive = true; const isSubActive = true;
const [openGroups, setOpenGroups] = useState({
workers: url.startsWith('/employer/workers') || url.startsWith('/employer/shortlist') || url.startsWith('/employer/candidates'),
jobs: url.startsWith('/employer/jobs'),
});
useEffect(() => {
setOpenGroups({
workers: url.startsWith('/employer/workers') || url.startsWith('/employer/shortlist') || url.startsWith('/employer/candidates'),
jobs: url.startsWith('/employer/jobs'),
});
}, [url]);
const toggleGroup = (groupKey) => {
setOpenGroups(prev => ({
...prev,
[groupKey]: !prev[groupKey]
}));
};
const isChildActive = (childHref) => {
let isActive = url.startsWith(childHref);
if (url.startsWith('/employer/workers/')) {
const isHired = props.worker?.status === 'Hired' || props.worker?.availability_status === 'Hired';
if (isHired) {
if (childHref === '/employer/candidates') {
isActive = true;
} else if (childHref === '/employer/workers') {
isActive = false;
}
} else {
if (childHref === '/employer/workers') {
isActive = true;
}
}
}
return isActive;
};
const navItems = [ const navItems = [
{ name: 'Dashboard', translationKey: 'dashboard', href: '/employer/dashboard', icon: LayoutDashboard }, { name: 'Dashboard', translationKey: 'dashboard', href: '/employer/dashboard', icon: LayoutDashboard },
{ name: 'Find Workers', translationKey: 'find_workers', href: '/employer/workers', icon: Search }, {
{ name: 'Shortlist', translationKey: 'shortlist', href: '/employer/shortlist', icon: Bookmark }, name: 'Workers',
{ name: 'Hired Workers', translationKey: 'candidates', href: '/employer/candidates', icon: UserCheck }, translationKey: 'workers_group',
...(user.enable_job_apply ? [{ name: 'My Jobs', translationKey: 'my_jobs', href: '/employer/jobs', icon: Briefcase }] : []), icon: Users,
isGroup: true,
groupKey: 'workers',
children: [
{ name: 'Workers List', translationKey: 'workers_list', href: '/employer/workers' },
{ name: 'Saved Workers', translationKey: 'saved_workers', href: '/employer/shortlist' },
{ name: 'Hired Workers', translationKey: 'hired_workers', href: '/employer/candidates' },
]
},
...(user.enable_job_apply ? [{
name: 'Jobs',
translationKey: 'jobs_group',
icon: Briefcase,
isGroup: true,
groupKey: 'jobs',
children: [
{ name: 'My Jobs', translationKey: 'my_jobs', href: '/employer/jobs' },
{ name: 'Applicants', translationKey: 'applicants', href: '/employer/jobs/all-applicants' },
{ name: 'Shortlisted Workers', translationKey: 'shortlisted_workers', href: '/employer/jobs/shortlisted' },
]
}] : []),
{ name: 'Messages', translationKey: 'messages', href: '/employer/messages', icon: MessageSquare, badge: unread_messages_count }, { name: 'Messages', translationKey: 'messages', href: '/employer/messages', icon: MessageSquare, badge: unread_messages_count },
{ name: 'Charity Events', translationKey: 'charity_events', href: '/employer/events', icon: Heart }, { name: 'Charity Events', translationKey: 'charity_events', href: '/employer/events', icon: Heart },
{ name: 'Subscription', translationKey: 'subscription', href: '/employer/subscription', icon: CreditCard }, { name: 'Subscription', translationKey: 'subscription', href: '/employer/subscription', icon: CreditCard },
@ -79,8 +141,8 @@ export default function EmployerLayout({ children, title, fullPage = false }) {
const mobileTabs = [ const mobileTabs = [
{ name: 'Search', translationKey: 'search', href: '/employer/workers', icon: Search }, { name: 'Search', translationKey: 'search', href: '/employer/workers', icon: Search },
{ name: 'Shortlist', translationKey: 'shortlist', href: '/employer/shortlist', icon: Bookmark }, { name: 'Saved Workers', translationKey: 'saved_workers', href: '/employer/shortlist', icon: Bookmark },
{ name: 'Hired Workers', translationKey: 'candidates', href: '/employer/candidates', icon: UserCheck }, { name: 'Hired Workers', translationKey: 'hired_workers', href: '/employer/candidates', icon: UserCheck },
{ name: 'Messages', translationKey: 'messages', href: '/employer/messages', icon: MessageSquare, badge: unread_messages_count }, { name: 'Messages', translationKey: 'messages', href: '/employer/messages', icon: MessageSquare, badge: unread_messages_count },
{ name: 'Profile', translationKey: 'profile', href: '/employer/profile', icon: User }, { name: 'Profile', translationKey: 'profile', href: '/employer/profile', icon: User },
]; ];
@ -165,6 +227,57 @@ export default function EmployerLayout({ children, title, fullPage = false }) {
{/* Navigation Items */} {/* Navigation Items */}
<nav className="p-4 space-y-1"> <nav className="p-4 space-y-1">
{navItems.map((item) => { {navItems.map((item) => {
if (item.isGroup) {
const Icon = item.icon;
const isOpen = openGroups[item.groupKey];
const hasActiveChild = item.children.some(child => isChildActive(child.href));
return (
<div key={item.name} className="space-y-1">
<button
onClick={() => toggleGroup(item.groupKey)}
className={`w-full flex items-center justify-between px-4 py-3 rounded-xl text-sm transition-all duration-200 group cursor-pointer border-0 outline-none bg-transparent ${
hasActiveChild
? 'text-[#185FA5] font-bold bg-slate-50'
: 'text-slate-600 hover:bg-slate-50 hover:text-slate-900'
}`}
>
<div className="flex items-center space-x-3">
<Icon className={`w-5 h-5 transition-colors ${hasActiveChild ? 'text-[#185FA5]' : 'text-slate-400 group-hover:text-slate-900'}`} />
<span className="tracking-tight">{t(item.translationKey, item.name)}</span>
</div>
{isOpen ? (
<ChevronDown className={`w-4 h-4 transition-transform duration-200 ${hasActiveChild ? 'text-[#185FA5]' : 'text-slate-400 group-hover:text-slate-900'}`} />
) : (
<ChevronRight className={`w-4 h-4 transition-transform duration-200 ${hasActiveChild ? 'text-[#185FA5]' : 'text-slate-400 group-hover:text-slate-900'}`} />
)}
</button>
{isOpen && (
<div className="pl-6 border-l border-slate-200 ml-6 space-y-1 py-1">
{item.children.map((child) => {
const isCurrent = isChildActive(child.href);
return (
<Link
key={child.name}
href={child.href}
className={`flex items-center px-4 py-2 rounded-xl text-xs transition-all duration-200 tracking-tight ${
isCurrent
? 'bg-[#185FA5] text-white font-bold shadow-md shadow-blue-900/10 scale-[1.01]'
: 'text-slate-500 hover:bg-slate-50 hover:text-slate-800 font-medium'
}`}
>
<span className={`mr-2.5 text-[10px] leading-none ${isCurrent ? 'text-white' : 'text-slate-300'}`}></span>
<span>{t(child.translationKey, child.name)}</span>
</Link>
);
})}
</div>
)}
</div>
);
}
const Icon = item.icon; const Icon = item.icon;
let isActive = url.startsWith(item.href); let isActive = url.startsWith(item.href);

View File

@ -20,7 +20,7 @@ import {
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { toast } from 'sonner'; import { toast } from 'sonner';
export default function Applicants({ job, applicants: initialApplicants }) { export default function Applicants({ job, applicants: initialApplicants, isShortlistedOnly = false }) {
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [applicants, setApplicants] = useState(initialApplicants || []); const [applicants, setApplicants] = useState(initialApplicants || []);
@ -55,39 +55,56 @@ export default function Applicants({ job, applicants: initialApplicants }) {
setSelectedApp(null); setSelectedApp(null);
}, },
onError: (errors) => { onError: (errors) => {
toast.error('Failed to update status: ' . (errors.error || errors.status || 'Error occurred')); toast.error('Failed to update status: ' + (errors.error || errors.status || 'Error occurred'));
} }
}); });
}; };
const pageTitle = isShortlistedOnly ? 'Shortlisted Workers' : (job ? 'Job Applicants' : 'All Applicants');
const headerTitle = isShortlistedOnly ? 'Shortlisted Workers' : (job ? `Applicants for ${job.title}` : 'All Job Applicants');
return ( return (
<EmployerLayout title="Job Applicants"> <EmployerLayout title={pageTitle}>
<Head title={`Applicants for ${job?.title || 'Job'}`} /> <Head title={pageTitle} />
<div className="space-y-8 select-none pb-12"> <div className="space-y-8 select-none pb-12">
{/* Job Header Info */} {/* Job Header Info */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6"> <div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
<div className="space-y-2"> <div className="space-y-2">
<Link <Link
href="/employer/jobs" href={job ? "/employer/jobs" : "/employer/dashboard"}
className="inline-flex items-center space-x-2 text-[10px] font-black text-[#185FA5] uppercase tracking-widest hover:translate-x-[-4px] transition-transform" className="inline-flex items-center space-x-2 text-[10px] font-black text-[#185FA5] uppercase tracking-widest hover:translate-x-[-4px] transition-transform"
> >
<ArrowLeft className="w-3.5 h-3.5" /> <ArrowLeft className="w-3.5 h-3.5" />
<span>Back to My Jobs</span> <span>{job ? 'Back to My Jobs' : 'Back to Dashboard'}</span>
</Link> </Link>
<h1 className="text-2xl font-black text-slate-900 tracking-tight"> <h1 className="text-2xl font-black text-slate-900 tracking-tight">
Applicants for <span className="text-[#185FA5]">{job?.title || 'Senior Mason Project'}</span> {isShortlistedOnly ? (
<span>Shortlisted <span className="text-[#185FA5]">Workers</span></span>
) : (
<span>Applicants for <span className="text-[#185FA5]">{job?.title || 'All Jobs'}</span></span>
)}
</h1> </h1>
{job && (
<div className="flex items-center space-x-3 text-[10px] font-bold text-slate-400 uppercase tracking-widest"> <div className="flex items-center space-x-3 text-[10px] font-bold text-slate-400 uppercase tracking-widest">
<span className="flex items-center"><DollarSign className="w-3.5 h-3.5 mr-1" /> {job?.salary || '2800'} AED</span> <span className="flex items-center"><DollarSign className="w-3.5 h-3.5 mr-1" /> {job.salary} AED</span>
</div> </div>
)}
</div> </div>
<div className="bg-white px-6 py-4 rounded-2xl border border-slate-200 shadow-sm flex items-center space-x-4 flex-shrink-0"> <div className="bg-white px-6 py-4 rounded-2xl border border-slate-200 shadow-sm flex items-center space-x-4 flex-shrink-0">
<div className="text-right"> <div className="text-right">
<div className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Hiring Progress</div> <div className="text-[9px] font-black text-slate-400 uppercase tracking-widest">
{isShortlistedOnly ? 'Total Shortlisted' : (job ? 'Hiring Progress' : 'Total Applicants')}
</div>
<div className="text-lg font-black text-slate-900 tracking-tight"> <div className="text-lg font-black text-slate-900 tracking-tight">
{applicants.filter(a => a.status?.toLowerCase() === 'hired').length} / {job?.workers_needed || 5} <span className="text-slate-300 text-sm">Positions</span> {isShortlistedOnly ? (
<span>{applicants.length} <span className="text-slate-300 text-sm">Workers</span></span>
) : job ? (
<span>{applicants.filter(a => a.status?.toLowerCase() === 'hired').length} / {job.workers_needed} <span className="text-slate-300 text-sm">Positions</span></span>
) : (
<span>{applicants.length} <span className="text-slate-300 text-sm">Applicants</span></span>
)}
</div> </div>
</div> </div>
<div className="w-12 h-12 rounded-xl bg-blue-50 flex items-center justify-center"> <div className="w-12 h-12 rounded-xl bg-blue-50 flex items-center justify-center">
@ -134,9 +151,14 @@ export default function Applicants({ job, applicants: initialApplicants }) {
)} )}
</h3> </h3>
<div className="flex items-center space-x-2 text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-0.5"> <div className="flex items-center space-x-2 text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-0.5">
<Globe2 className="w-3 h-3" /> <Globe2 className="w-3.5 h-3.5" />
<span>{applicant.nationality}</span> <span>{applicant.nationality}</span>
</div> </div>
{applicant.job_title && (
<div className="text-[10px] text-slate-500 font-bold mt-1">
Applied: <span className="text-[#185FA5]">{applicant.job_title}</span>
</div>
)}
</div> </div>
</div> </div>
<div className="flex flex-col items-end space-y-1"> <div className="flex flex-col items-end space-y-1">

View File

@ -426,5 +426,13 @@
"share_experience_placeholder": "Tell other sponsors about childcare skills, cleaning, cooking style, driving safety, etc...", "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_title": "No Charity Events",
"no_charity_events_desc": "You haven't posted any charity events yet" "no_charity_events_desc": "You haven't posted any charity events yet",
"workers_group": "Workers",
"workers_list": "Workers List",
"hired_workers": "Hired Workers",
"jobs_group": "Jobs",
"applicants": "Applicants",
"shortlisted_workers": "Shortlisted Workers",
"help_support": "Help & Support",
"payment_history": "Payment History"
} }

View File

@ -425,5 +425,13 @@
"share_experience_placeholder": "अन्य प्रायोजकों को बाल देखभाल कौशल, सफाई, खाना पकाने की शैली, ड्राइविंग सुरक्षा आदि के बारे में बताएं...", "share_experience_placeholder": "अन्य प्रायोजकों को बाल देखभाल कौशल, सफाई, खाना पकाने की शैली, ड्राइविंग सुरक्षा आदि के बारे में बताएं...",
"post_trust_review_action": "समीक्षा पोस्ट करें", "post_trust_review_action": "समीक्षा पोस्ट करें",
"no_charity_events_title": "कोई चैरिटी कार्यक्रम नहीं", "no_charity_events_title": "कोई चैरिटी कार्यक्रम नहीं",
"no_charity_events_desc": "आपने अभी तक कोई चैरिटी कार्यक्रम पोस्ट नहीं किया है" "no_charity_events_desc": "आपने अभी तक कोई चैरिटी कार्यक्रम पोस्ट नहीं किया है",
"workers_group": "कामगार",
"workers_list": "कामगारों की सूची",
"hired_workers": "नियुक्त कर्मचारी",
"jobs_group": "नौकरियाँ",
"applicants": "आवेदक",
"shortlisted_workers": "शॉर्टलिस्ट किए गए कामगार",
"help_support": "सहायता और समर्थन",
"payment_history": "भुगतान इतिहास"
} }

View File

@ -413,6 +413,8 @@
Route::get('/jobs', [\App\Http\Controllers\Employer\JobController::class, 'index'])->name('employer.jobs'); Route::get('/jobs', [\App\Http\Controllers\Employer\JobController::class, 'index'])->name('employer.jobs');
Route::get('/jobs/create', [\App\Http\Controllers\Employer\JobController::class, 'create'])->name('employer.jobs.create'); Route::get('/jobs/create', [\App\Http\Controllers\Employer\JobController::class, 'create'])->name('employer.jobs.create');
Route::post('/jobs/create', [\App\Http\Controllers\Employer\JobController::class, 'store'])->name('employer.jobs.store'); Route::post('/jobs/create', [\App\Http\Controllers\Employer\JobController::class, 'store'])->name('employer.jobs.store');
Route::get('/jobs/all-applicants', [\App\Http\Controllers\Employer\JobController::class, 'allApplicants'])->name('employer.jobs.all-applicants');
Route::get('/jobs/shortlisted', [\App\Http\Controllers\Employer\JobController::class, 'shortlistedApplicants'])->name('employer.jobs.shortlisted');
Route::get('/jobs/{id}/edit', [\App\Http\Controllers\Employer\JobController::class, 'edit'])->name('employer.jobs.edit'); Route::get('/jobs/{id}/edit', [\App\Http\Controllers\Employer\JobController::class, 'edit'])->name('employer.jobs.edit');
Route::post('/jobs/{id}/edit', [\App\Http\Controllers\Employer\JobController::class, 'update'])->name('employer.jobs.update'); Route::post('/jobs/{id}/edit', [\App\Http\Controllers\Employer\JobController::class, 'update'])->name('employer.jobs.update');
Route::delete('/jobs/{id}', [\App\Http\Controllers\Employer\JobController::class, 'destroy'])->name('employer.jobs.destroy'); Route::delete('/jobs/{id}', [\App\Http\Controllers\Employer\JobController::class, 'destroy'])->name('employer.jobs.destroy');

View File

@ -186,4 +186,92 @@ public function test_employer_can_view_applicants()
$response->assertStatus(200); $response->assertStatus(200);
$response->assertSee('John Worker'); $response->assertSee('John Worker');
} }
/**
* Test viewing all job applicants.
*/
public function test_employer_can_view_all_applicants()
{
$job = JobPost::create([
'employer_id' => $this->employer->id,
'title' => 'Mason for Project',
'workers_needed' => 3,
'job_type' => 'Contract',
'location' => 'Sharjah',
'salary' => 3000,
'start_date' => '2026-07-15',
'description' => 'Contract mason project.',
'status' => 'active',
]);
$worker = Worker::create([
'name' => 'John Worker',
'email' => 'worker@example.com',
'phone' => '+971500000001',
'nationality' => 'Indian',
'age' => 30,
'salary' => 2500,
'availability' => 'Available',
'experience' => '4 Years',
'religion' => 'Christian',
'bio' => 'Vetted domestic worker with great experience.',
]);
$application = JobApplication::create([
'job_id' => $job->id,
'worker_id' => $worker->id,
'status' => 'applied',
]);
$response = $this->actingAs($this->employer)
->withSession(['user' => $this->employer])
->get("/employer/jobs/all-applicants");
$response->assertStatus(200);
$response->assertSee('John Worker');
}
/**
* Test viewing shortlisted applicants.
*/
public function test_employer_can_view_shortlisted_applicants()
{
$job = JobPost::create([
'employer_id' => $this->employer->id,
'title' => 'Mason for Project',
'workers_needed' => 3,
'job_type' => 'Contract',
'location' => 'Sharjah',
'salary' => 3000,
'start_date' => '2026-07-15',
'description' => 'Contract mason project.',
'status' => 'active',
]);
$worker = Worker::create([
'name' => 'Shortlisted John',
'email' => 'worker_shortlisted@example.com',
'phone' => '+971500000002',
'nationality' => 'Indian',
'age' => 30,
'salary' => 2500,
'availability' => 'Available',
'experience' => '4 Years',
'religion' => 'Christian',
'bio' => 'Vetted domestic worker with great experience.',
]);
$application = JobApplication::create([
'job_id' => $job->id,
'worker_id' => $worker->id,
'status' => 'shortlisted',
]);
$response = $this->actingAs($this->employer)
->withSession(['user' => $this->employer])
->get("/employer/jobs/shortlisted");
$response->assertStatus(200);
$response->assertSee('Shortlisted John');
}
} }

View File

@ -154,6 +154,7 @@ public function test_worker_job_apis()
$response->assertJsonPath('success', true); $response->assertJsonPath('success', true);
$response->assertJsonCount(1, 'data.jobs'); $response->assertJsonCount(1, 'data.jobs');
$response->assertJsonPath('data.jobs.0.title', 'Available Plumber Job'); $response->assertJsonPath('data.jobs.0.title', 'Available Plumber Job');
$response->assertJsonPath('data.jobs.0.posted_by', 'Employer User');
// 2. Retrieve job details // 2. Retrieve job details
$response = $this->getJson("/api/workers/jobs/{$job->id}", [ $response = $this->getJson("/api/workers/jobs/{$job->id}", [
@ -162,6 +163,7 @@ public function test_worker_job_apis()
$response->assertStatus(200); $response->assertStatus(200);
$response->assertJsonPath('success', true); $response->assertJsonPath('success', true);
$response->assertJsonPath('data.job.title', 'Available Plumber Job'); $response->assertJsonPath('data.job.title', 'Available Plumber Job');
$response->assertJsonPath('data.job.posted_by', 'Employer User');
// 3. Apply for job // 3. Apply for job
$response = $this->postJson("/api/workers/jobs/{$job->id}/apply", [], [ $response = $this->postJson("/api/workers/jobs/{$job->id}/apply", [], [
@ -191,6 +193,7 @@ public function test_worker_job_apis()
$response->assertJsonPath('success', true); $response->assertJsonPath('success', true);
$response->assertJsonCount(1, 'data.applications'); $response->assertJsonCount(1, 'data.applications');
$response->assertJsonPath('data.applications.0.job.title', 'Available Plumber Job'); $response->assertJsonPath('data.applications.0.job.title', 'Available Plumber Job');
$response->assertJsonPath('data.applications.0.job.posted_by', 'Employer User');
$application = JobApplication::first(); $application = JobApplication::first();
@ -463,6 +466,7 @@ public function test_job_hiring_workflow_enhancements()
$response->assertStatus(200); $response->assertStatus(200);
$response->assertJsonPath('success', true); $response->assertJsonPath('success', true);
$response->assertJsonPath('data.application.status', 'applied'); $response->assertJsonPath('data.application.status', 'applied');
$response->assertJsonPath('data.application.job.posted_by', 'Employer User');
$response->assertJsonCount(0, 'data.application.status_history'); $response->assertJsonCount(0, 'data.application.status_history');
// 4. Employer updates status to shortlisted with notes // 4. Employer updates status to shortlisted with notes