mohan #25
@ -67,6 +67,7 @@ export default function EmployerLayout({ children, title, fullPage = false }) {
|
|||||||
{ name: 'Find Workers', translationKey: 'find_workers', href: '/employer/workers', icon: Search },
|
{ name: 'Find Workers', translationKey: 'find_workers', href: '/employer/workers', icon: Search },
|
||||||
{ name: 'Shortlist', translationKey: 'shortlist', href: '/employer/shortlist', icon: Bookmark },
|
{ name: 'Shortlist', translationKey: 'shortlist', href: '/employer/shortlist', icon: Bookmark },
|
||||||
{ name: 'Hired Workers', translationKey: 'candidates', href: '/employer/candidates', icon: UserCheck },
|
{ name: 'Hired Workers', translationKey: 'candidates', href: '/employer/candidates', icon: UserCheck },
|
||||||
|
{ name: 'My Jobs', translationKey: 'my_jobs', href: '/employer/jobs', icon: Briefcase },
|
||||||
{ 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 },
|
||||||
|
|||||||
@ -36,9 +36,9 @@ import {
|
|||||||
|
|
||||||
export default function SubscriptionsIndex({ plans: initialPlans }) {
|
export default function SubscriptionsIndex({ plans: initialPlans }) {
|
||||||
const [plans, setPlans] = useState(initialPlans || [
|
const [plans, setPlans] = useState(initialPlans || [
|
||||||
{ id: 'basic', name: 'Basic Search', price: 99, duration: 'Monthly', features: ['Browse 500+ workers', '10 Shortlists'], status: 'Active' },
|
{ id: 'basic', name: 'Basic Search', price: 99, duration: 'Monthly', features: ['Browse 500+ workers', '10 Shortlists'], status: 'Active', maxContactPeople: 10, enableJobApply: false },
|
||||||
{ id: 'premium', name: 'Premium Pass', price: 199, duration: 'Monthly', features: ['Unlimited shortlisting', 'Direct messaging'], status: 'Active' },
|
{ id: 'premium', name: 'Premium Pass', price: 199, duration: 'Monthly', features: ['Unlimited shortlisting', 'Direct messaging'], status: 'Active', maxContactPeople: 50, enableJobApply: true },
|
||||||
{ id: 'vip', name: 'VIP Concierge', price: 499, duration: 'Monthly', features: ['Dedicated Manager', '30-day replacement'], status: 'Active' },
|
{ id: 'vip', name: 'VIP Concierge', price: 499, duration: 'Monthly', features: ['Dedicated Manager', '30-day replacement'], status: 'Active', maxContactPeople: 100, enableJobApply: true },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
@ -50,6 +50,8 @@ export default function SubscriptionsIndex({ plans: initialPlans }) {
|
|||||||
const [price, setPrice] = useState('');
|
const [price, setPrice] = useState('');
|
||||||
const [duration, setDuration] = useState('Monthly');
|
const [duration, setDuration] = useState('Monthly');
|
||||||
const [features, setFeatures] = useState('');
|
const [features, setFeatures] = useState('');
|
||||||
|
const [maxContactPeople, setMaxContactPeople] = useState('');
|
||||||
|
const [enableJobApply, setEnableJobApply] = useState(false);
|
||||||
|
|
||||||
const handleCreateClick = () => {
|
const handleCreateClick = () => {
|
||||||
setEditingPlan(null);
|
setEditingPlan(null);
|
||||||
@ -57,6 +59,8 @@ export default function SubscriptionsIndex({ plans: initialPlans }) {
|
|||||||
setPrice('');
|
setPrice('');
|
||||||
setDuration('Monthly');
|
setDuration('Monthly');
|
||||||
setFeatures('');
|
setFeatures('');
|
||||||
|
setMaxContactPeople('');
|
||||||
|
setEnableJobApply(false);
|
||||||
setIsDialogOpen(true);
|
setIsDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -66,6 +70,8 @@ export default function SubscriptionsIndex({ plans: initialPlans }) {
|
|||||||
setPrice(plan.price);
|
setPrice(plan.price);
|
||||||
setDuration(plan.duration);
|
setDuration(plan.duration);
|
||||||
setFeatures(plan.features.join('\n'));
|
setFeatures(plan.features.join('\n'));
|
||||||
|
setMaxContactPeople(plan.maxContactPeople !== undefined ? plan.maxContactPeople : '');
|
||||||
|
setEnableJobApply(!!plan.enableJobApply);
|
||||||
setIsDialogOpen(true);
|
setIsDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -93,7 +99,9 @@ export default function SubscriptionsIndex({ plans: initialPlans }) {
|
|||||||
name,
|
name,
|
||||||
price: Number(price),
|
price: Number(price),
|
||||||
duration,
|
duration,
|
||||||
features: featuresArray
|
features: featuresArray,
|
||||||
|
maxContactPeople: maxContactPeople !== '' ? Number(maxContactPeople) : '',
|
||||||
|
enableJobApply
|
||||||
} : p));
|
} : p));
|
||||||
} else {
|
} else {
|
||||||
const newId = name.toLowerCase().replace(/\s+/g, '-');
|
const newId = name.toLowerCase().replace(/\s+/g, '-');
|
||||||
@ -103,7 +111,9 @@ export default function SubscriptionsIndex({ plans: initialPlans }) {
|
|||||||
price: Number(price),
|
price: Number(price),
|
||||||
duration,
|
duration,
|
||||||
features: featuresArray,
|
features: featuresArray,
|
||||||
status: 'Active'
|
status: 'Active',
|
||||||
|
maxContactPeople: maxContactPeople !== '' ? Number(maxContactPeople) : '',
|
||||||
|
enableJobApply
|
||||||
}]);
|
}]);
|
||||||
}
|
}
|
||||||
setIsDialogOpen(false);
|
setIsDialogOpen(false);
|
||||||
@ -149,6 +159,8 @@ export default function SubscriptionsIndex({ plans: initialPlans }) {
|
|||||||
<TableHead className="font-semibold text-slate-600 text-xs h-12">Plan Name</TableHead>
|
<TableHead className="font-semibold text-slate-600 text-xs h-12">Plan Name</TableHead>
|
||||||
<TableHead className="font-semibold text-slate-600 text-xs h-12">Pricing (AED)</TableHead>
|
<TableHead className="font-semibold text-slate-600 text-xs h-12">Pricing (AED)</TableHead>
|
||||||
<TableHead className="font-semibold text-slate-600 text-xs h-12">Duration</TableHead>
|
<TableHead className="font-semibold text-slate-600 text-xs h-12">Duration</TableHead>
|
||||||
|
<TableHead className="font-semibold text-slate-600 text-xs h-12">Max Contacts</TableHead>
|
||||||
|
<TableHead className="font-semibold text-slate-600 text-xs h-12">Job Apply</TableHead>
|
||||||
<TableHead className="font-semibold text-slate-600 text-xs h-12">Key Features</TableHead>
|
<TableHead className="font-semibold text-slate-600 text-xs h-12">Key Features</TableHead>
|
||||||
<TableHead className="font-semibold text-slate-600 text-xs h-12">Status</TableHead>
|
<TableHead className="font-semibold text-slate-600 text-xs h-12">Status</TableHead>
|
||||||
<TableHead className="font-semibold text-slate-600 text-xs h-12 text-right pr-6">Actions</TableHead>
|
<TableHead className="font-semibold text-slate-600 text-xs h-12 text-right pr-6">Actions</TableHead>
|
||||||
@ -157,7 +169,7 @@ export default function SubscriptionsIndex({ plans: initialPlans }) {
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{filteredPlans.length === 0 ? (
|
{filteredPlans.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={6} className="text-center py-8 text-slate-400 text-sm">
|
<TableCell colSpan={8} className="text-center py-8 text-slate-400 text-sm">
|
||||||
No plans found
|
No plans found
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@ -180,6 +192,14 @@ export default function SubscriptionsIndex({ plans: initialPlans }) {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="font-bold text-slate-900">{plan.price}</TableCell>
|
<TableCell className="font-bold text-slate-900">{plan.price}</TableCell>
|
||||||
<TableCell className="text-sm text-slate-500 font-medium">{plan.duration}</TableCell>
|
<TableCell className="text-sm text-slate-500 font-medium">{plan.duration}</TableCell>
|
||||||
|
<TableCell className="text-sm text-slate-700 font-bold">{plan.maxContactPeople || 'Unlimited'}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span className={`inline-flex items-center px-2 py-0.5 rounded text-[10px] font-black uppercase tracking-wider ${
|
||||||
|
plan.enableJobApply ? 'bg-emerald-50 text-emerald-700' : 'bg-rose-50 text-rose-600'
|
||||||
|
}`}>
|
||||||
|
{plan.enableJobApply ? 'Enabled' : 'Disabled'}
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{plan.features.map((f, i) => (
|
{plan.features.map((f, i) => (
|
||||||
@ -257,18 +277,45 @@ export default function SubscriptionsIndex({ plans: initialPlans }) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Duration</label>
|
<div className="space-y-2">
|
||||||
<select
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Duration</label>
|
||||||
value={duration}
|
<select
|
||||||
onChange={(e) => setDuration(e.target.value)}
|
value={duration}
|
||||||
className="w-full px-4 py-2.5 bg-slate-50 border border-slate-100 rounded-xl text-sm font-bold focus:ring-4 focus:ring-teal-500/10 outline-none"
|
onChange={(e) => setDuration(e.target.value)}
|
||||||
>
|
className="w-full px-4 py-2.5 bg-slate-50 border border-slate-100 rounded-xl text-sm font-bold focus:ring-4 focus:ring-teal-500/10 outline-none"
|
||||||
<option value="Monthly">Monthly</option>
|
>
|
||||||
<option value="Quarterly">Quarterly</option>
|
<option value="Monthly">Monthly</option>
|
||||||
<option value="Yearly">Yearly</option>
|
<option value="Quarterly">Quarterly</option>
|
||||||
</select>
|
<option value="Yearly">Yearly</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Max Contact People</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={maxContactPeople}
|
||||||
|
onChange={(e) => setMaxContactPeople(e.target.value)}
|
||||||
|
className="w-full px-4 py-2.5 bg-slate-50 border border-slate-100 rounded-xl text-sm font-bold focus:ring-4 focus:ring-teal-500/10 outline-none"
|
||||||
|
placeholder="e.g. 10 (blank for unlimited)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-3 bg-slate-50 p-3.5 rounded-xl border border-slate-100/50">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="enableJobApply"
|
||||||
|
checked={enableJobApply}
|
||||||
|
onChange={(e) => setEnableJobApply(e.target.checked)}
|
||||||
|
className="w-4.5 h-4.5 text-teal-600 border-slate-300 rounded focus:ring-teal-500"
|
||||||
|
/>
|
||||||
|
<label htmlFor="enableJobApply" className="text-xs font-bold text-slate-700 cursor-pointer select-none">
|
||||||
|
Enable Job Apply Module
|
||||||
|
<span className="block text-[10px] font-medium text-slate-400 mt-0.5">Allows employers subscribed to this plan to receive job applications.</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Features (one per line)</label>
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Features (one per line)</label>
|
||||||
<textarea
|
<textarea
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
"find_workers": "Find Workers",
|
"find_workers": "Find Workers",
|
||||||
"shortlist": "Shortlist",
|
"shortlist": "Shortlist",
|
||||||
"candidates": "Hired Workers",
|
"candidates": "Hired Workers",
|
||||||
|
"my_jobs": "My Jobs",
|
||||||
"messages": "Messages",
|
"messages": "Messages",
|
||||||
"charity_events": "Charity Events",
|
"charity_events": "Charity Events",
|
||||||
"subscription": "Subscription",
|
"subscription": "Subscription",
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
"find_workers": "कामगार खोजें",
|
"find_workers": "कामगार खोजें",
|
||||||
"shortlist": "शॉर्टलिस्ट",
|
"shortlist": "शॉर्टलिस्ट",
|
||||||
"candidates": "नियुक्त कर्मचारी",
|
"candidates": "नियुक्त कर्मचारी",
|
||||||
|
"my_jobs": "मेरी नौकरियाँ",
|
||||||
"messages": "संदेश",
|
"messages": "संदेश",
|
||||||
"charity_events": "दान कार्यक्रम",
|
"charity_events": "दान कार्यक्रम",
|
||||||
"subscription": "सदस्यता",
|
"subscription": "सदस्यता",
|
||||||
|
|||||||
158
tests/Feature/EmployerJobTest.php
Normal file
158
tests/Feature/EmployerJobTest.php
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\JobPost;
|
||||||
|
use App\Models\JobApplication;
|
||||||
|
use App\Models\Worker;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class EmployerJobTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
protected $employer;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->employer = User::create([
|
||||||
|
'name' => 'Jane Sponsor',
|
||||||
|
'email' => 'sponsor@example.com',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
'role' => 'employer',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test displaying the employer jobs list.
|
||||||
|
*/
|
||||||
|
public function test_employer_can_view_jobs_list()
|
||||||
|
{
|
||||||
|
$job = JobPost::create([
|
||||||
|
'employer_id' => $this->employer->id,
|
||||||
|
'title' => 'Experienced Mason',
|
||||||
|
'workers_needed' => 5,
|
||||||
|
'job_type' => 'Full Time',
|
||||||
|
'location' => 'Dubai Marina',
|
||||||
|
'salary' => 2800,
|
||||||
|
'start_date' => '2026-07-01',
|
||||||
|
'description' => 'Looking for experienced masons for a residential project.',
|
||||||
|
'requirements' => '5+ years experience, basic English.',
|
||||||
|
'status' => 'active',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->actingAs($this->employer)
|
||||||
|
->withSession(['user' => $this->employer])
|
||||||
|
->get('/employer/jobs');
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertSee('Experienced Mason');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test displaying the create job form.
|
||||||
|
*/
|
||||||
|
public function test_employer_can_view_create_job_form()
|
||||||
|
{
|
||||||
|
$response = $this->actingAs($this->employer)
|
||||||
|
->withSession(['user' => $this->employer])
|
||||||
|
->get('/employer/jobs/create');
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test storing a new job posting.
|
||||||
|
*/
|
||||||
|
public function test_employer_can_post_job()
|
||||||
|
{
|
||||||
|
$response = $this->actingAs($this->employer)
|
||||||
|
->withSession(['user' => $this->employer])
|
||||||
|
->post('/employer/jobs/create', [
|
||||||
|
'title' => 'Villa Cleaner',
|
||||||
|
'workers_needed' => 2,
|
||||||
|
'job_type' => 'Part Time',
|
||||||
|
'location' => 'Al Barsha',
|
||||||
|
'salary' => 1800,
|
||||||
|
'start_date' => '2026-07-10',
|
||||||
|
'description' => 'Need part-time villa cleaners for daily housekeeping tasks.',
|
||||||
|
'requirements' => 'Housekeeping experience is a plus.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertRedirect('/employer/jobs');
|
||||||
|
$response->assertSessionHas('success');
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('job_posts', [
|
||||||
|
'employer_id' => $this->employer->id,
|
||||||
|
'title' => 'Villa Cleaner',
|
||||||
|
'workers_needed' => 2,
|
||||||
|
'job_type' => 'Part Time',
|
||||||
|
'location' => 'Al Barsha',
|
||||||
|
'salary' => 1800,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test validation during job posting.
|
||||||
|
*/
|
||||||
|
public function test_job_posting_requires_fields()
|
||||||
|
{
|
||||||
|
$response = $this->actingAs($this->employer)
|
||||||
|
->withSession(['user' => $this->employer])
|
||||||
|
->post('/employer/jobs/create', [
|
||||||
|
'title' => '',
|
||||||
|
'workers_needed' => 0,
|
||||||
|
'salary' => -100,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertSessionHasErrors(['title', 'workers_needed', 'location', 'salary', 'job_type', 'start_date', 'description']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test viewing job applicants.
|
||||||
|
*/
|
||||||
|
public function test_employer_can_view_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/{$job->id}/applicants");
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertSee('John Worker');
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user