admin portal - changes payment plan configuration

This commit is contained in:
mohanmd 2026-06-30 11:57:04 +05:30
parent 0319b470e6
commit f8dcadf911
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: 'Shortlist', translationKey: 'shortlist', href: '/employer/shortlist', icon: Bookmark },
{ 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: 'Charity Events', translationKey: 'charity_events', href: '/employer/events', icon: Heart },
{ name: 'Subscription', translationKey: 'subscription', href: '/employer/subscription', icon: CreditCard },

View File

@ -36,9 +36,9 @@ import {
export default function SubscriptionsIndex({ plans: initialPlans }) {
const [plans, setPlans] = useState(initialPlans || [
{ id: 'basic', name: 'Basic Search', price: 99, duration: 'Monthly', features: ['Browse 500+ workers', '10 Shortlists'], status: 'Active' },
{ id: 'premium', name: 'Premium Pass', price: 199, duration: 'Monthly', features: ['Unlimited shortlisting', 'Direct messaging'], status: 'Active' },
{ id: 'vip', name: 'VIP Concierge', price: 499, duration: 'Monthly', features: ['Dedicated Manager', '30-day replacement'], 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', maxContactPeople: 50, enableJobApply: true },
{ 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);
@ -50,6 +50,8 @@ export default function SubscriptionsIndex({ plans: initialPlans }) {
const [price, setPrice] = useState('');
const [duration, setDuration] = useState('Monthly');
const [features, setFeatures] = useState('');
const [maxContactPeople, setMaxContactPeople] = useState('');
const [enableJobApply, setEnableJobApply] = useState(false);
const handleCreateClick = () => {
setEditingPlan(null);
@ -57,6 +59,8 @@ export default function SubscriptionsIndex({ plans: initialPlans }) {
setPrice('');
setDuration('Monthly');
setFeatures('');
setMaxContactPeople('');
setEnableJobApply(false);
setIsDialogOpen(true);
};
@ -66,6 +70,8 @@ export default function SubscriptionsIndex({ plans: initialPlans }) {
setPrice(plan.price);
setDuration(plan.duration);
setFeatures(plan.features.join('\n'));
setMaxContactPeople(plan.maxContactPeople !== undefined ? plan.maxContactPeople : '');
setEnableJobApply(!!plan.enableJobApply);
setIsDialogOpen(true);
};
@ -93,7 +99,9 @@ export default function SubscriptionsIndex({ plans: initialPlans }) {
name,
price: Number(price),
duration,
features: featuresArray
features: featuresArray,
maxContactPeople: maxContactPeople !== '' ? Number(maxContactPeople) : '',
enableJobApply
} : p));
} else {
const newId = name.toLowerCase().replace(/\s+/g, '-');
@ -103,7 +111,9 @@ export default function SubscriptionsIndex({ plans: initialPlans }) {
price: Number(price),
duration,
features: featuresArray,
status: 'Active'
status: 'Active',
maxContactPeople: maxContactPeople !== '' ? Number(maxContactPeople) : '',
enableJobApply
}]);
}
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">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">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">Status</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>
{filteredPlans.length === 0 ? (
<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
</TableCell>
</TableRow>
@ -180,6 +192,14 @@ export default function SubscriptionsIndex({ plans: initialPlans }) {
</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-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>
<div className="flex flex-wrap gap-1">
{plan.features.map((f, i) => (
@ -257,18 +277,45 @@ export default function SubscriptionsIndex({ plans: initialPlans }) {
/>
</div>
</div>
<div className="space-y-2">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Duration</label>
<select
value={duration}
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="Yearly">Yearly</option>
</select>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Duration</label>
<select
value={duration}
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="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 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">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Features (one per line)</label>
<textarea

View File

@ -3,6 +3,7 @@
"find_workers": "Find Workers",
"shortlist": "Shortlist",
"candidates": "Hired Workers",
"my_jobs": "My Jobs",
"messages": "Messages",
"charity_events": "Charity Events",
"subscription": "Subscription",

View File

@ -3,6 +3,7 @@
"find_workers": "कामगार खोजें",
"shortlist": "शॉर्टलिस्ट",
"candidates": "नियुक्त कर्मचारी",
"my_jobs": "मेरी नौकरियाँ",
"messages": "संदेश",
"charity_events": "दान कार्यक्रम",
"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');
}
}