Compare commits

...

15 Commits

Author SHA1 Message Date
4a77c7689c Merge branch 'master' of https://git.iproat.com/mohanmd/migrant-web into mohan 2026-06-30 11:57:40 +05:30
f8dcadf911 admin portal - changes payment plan configuration 2026-06-30 11:57:04 +05:30
bb22b8d227 Merge pull request 'api fixed' (#24) from mohan into master
Reviewed-on: #24
2026-06-24 18:11:53 +00:00
4601446553 Merge pull request 'fixed api issue' (#23) from mohan into master
Reviewed-on: #23
2026-06-24 16:48:30 +00:00
ffb371438a Merge pull request 'fixed api issue' (#22) from mohan into master
Reviewed-on: #22
2026-06-24 16:39:39 +00:00
e6b6cd74d3 Merge pull request 'fixed api issue' (#21) from mohan into master
Reviewed-on: #21
2026-06-24 16:27:35 +00:00
476573d22e Merge pull request 'swagger fix' (#20) from mohan into master
Reviewed-on: #20
2026-06-24 16:19:07 +00:00
735159906d Merge pull request 'mohan' (#19) from mohan into master
Reviewed-on: #19
2026-06-24 16:09:17 +00:00
f4f6928432 Merge pull request 'api issue fixed' (#18) from mohan into master
Reviewed-on: #18
2026-06-24 12:03:45 +00:00
93b3018a0a Merge pull request 'mohan' (#17) from mohan into master
Reviewed-on: #17
2026-06-24 11:05:30 +00:00
6071c61440 Merge pull request 'api changes' (#16) from mohan into master
Reviewed-on: #16
2026-06-24 10:08:26 +00:00
2733d281aa Merge pull request 'mohan' (#15) from mohan into master
Reviewed-on: #15
2026-06-24 07:35:11 +00:00
1bf07aa0fd Merge pull request 'mohan' (#14) from mohan into master
Reviewed-on: #14
2026-06-23 19:12:28 +00:00
765292e625 Merge pull request 'worker view profile issue' (#13) from mohan into master
Reviewed-on: #13
2026-06-23 13:55:29 +00:00
52957149ff Merge pull request 'mohan' (#12) from mohan into master
Reviewed-on: #12
2026-06-23 13:43:57 +00:00
5 changed files with 225 additions and 17 deletions

View File

@ -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 },

View File

@ -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,6 +277,7 @@ export default function SubscriptionsIndex({ plans: initialPlans }) {
/> />
</div> </div>
</div> </div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Duration</label> <label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Duration</label>
<select <select
@ -269,6 +290,32 @@ export default function SubscriptionsIndex({ plans: initialPlans }) {
<option value="Yearly">Yearly</option> <option value="Yearly">Yearly</option>
</select> </select>
</div> </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 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

View File

@ -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",

View File

@ -3,6 +3,7 @@
"find_workers": "कामगार खोजें", "find_workers": "कामगार खोजें",
"shortlist": "शॉर्टलिस्ट", "shortlist": "शॉर्टलिस्ट",
"candidates": "नियुक्त कर्मचारी", "candidates": "नियुक्त कर्मचारी",
"my_jobs": "मेरी नौकरियाँ",
"messages": "संदेश", "messages": "संदेश",
"charity_events": "दान कार्यक्रम", "charity_events": "दान कार्यक्रम",
"subscription": "सदस्यता", "subscription": "सदस्यता",

View 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');
}
}