From 56d8fc49aa46c0cccf81ba46640a0e567d924ac9 Mon Sep 17 00:00:00 2001 From: mohanmd Date: Fri, 19 Jun 2026 16:05:22 +0530 Subject: [PATCH] faq added, issue fixed --- .../Admin/AdminExtraController.php | 30 +- .../Controllers/Admin/DashboardController.php | 57 +- app/Http/Controllers/Admin/FaqController.php | 87 ++ .../Controllers/Admin/SponsorController.php | 20 +- .../Api/EmployerPaymentController.php | 49 +- .../Employer/ProfileController.php | 167 ++- .../Employer/SupportTicketController.php | 12 + app/Models/EmployerProfile.php | 6 +- app/Models/Faq.php | 15 + ...profile_photo_url_to_employer_profiles.php | 29 + ...ification_toggles_to_employer_profiles.php | 29 + .../2026_06_19_141645_create_faqs_table.php | 31 + resources/js/Layouts/AdminLayout.jsx | 5 +- resources/js/Layouts/EmployerLayout.jsx | 6 +- resources/js/Pages/Admin/Faqs/Index.jsx | 285 +++++ resources/js/Pages/Admin/Support/Index.jsx | 26 + resources/js/Pages/Employer/Profile.jsx | 1009 +++++++++-------- resources/js/Pages/Employer/ProfileEdit.jsx | 241 ++++ resources/js/Pages/Employer/Support/Index.jsx | 34 +- routes/web.php | 47 +- 20 files changed, 1524 insertions(+), 661 deletions(-) create mode 100644 app/Http/Controllers/Admin/FaqController.php create mode 100644 app/Models/Faq.php create mode 100644 database/migrations/2026_06_19_134127_add_property_type_and_profile_photo_url_to_employer_profiles.php create mode 100644 database/migrations/2026_06_19_134828_add_notification_toggles_to_employer_profiles.php create mode 100644 database/migrations/2026_06_19_141645_create_faqs_table.php create mode 100644 resources/js/Pages/Admin/Faqs/Index.jsx create mode 100644 resources/js/Pages/Employer/ProfileEdit.jsx diff --git a/app/Http/Controllers/Admin/AdminExtraController.php b/app/Http/Controllers/Admin/AdminExtraController.php index 1c4000f..e4eda98 100644 --- a/app/Http/Controllers/Admin/AdminExtraController.php +++ b/app/Http/Controllers/Admin/AdminExtraController.php @@ -336,11 +336,10 @@ public function refundPayment(Request $request, $id) 'amount' => 'required|numeric|min:1' ]); - $payment = DB::table('payments')->where('id', $id)->first(); + $payment = DB::table('subscriptions')->where('id', $id)->first(); if ($payment) { - DB::table('payments')->where('id', $id)->update([ + DB::table('subscriptions')->where('id', $id)->update([ 'status' => 'refunded', - 'description' => $payment->description . " (Refunded: " . $request->refund_reason . ")", 'updated_at' => now() ]); @@ -448,7 +447,7 @@ public function analytics(Request $request) $orderColumn = 'users.created_at'; $headers = ['Employer ID', 'Name', 'Email', 'Status', 'Subscription Status', 'Total Spent', 'Joined Date']; $mapFn = function($emp) { - $totalPaid = DB::table('payments')->where('user_id', $emp->id)->where('status', 'success')->sum('amount'); + $totalPaid = DB::table('subscriptions')->where('user_id', $emp->id)->where('status', 'active')->sum('amount_aed'); return [ 'id' => 'EMP-' . str_pad($emp->id, 4, '0', STR_PAD_LEFT), 'name' => $emp->name, @@ -461,36 +460,37 @@ public function analytics(Request $request) }; } elseif ($reportType === 'payments') { - $query = DB::table('payments') - ->leftJoin('users', 'payments.user_id', '=', 'users.id') - ->select('payments.*', 'users.name as user_name', 'users.email as user_email'); + $query = DB::table('subscriptions') + ->leftJoin('users', 'subscriptions.user_id', '=', 'users.id') + ->select('subscriptions.*', 'users.name as user_name', 'users.email as user_email'); if ($startDate) { - $query->whereDate('payments.created_at', '>=', $startDate); + $query->whereDate('subscriptions.created_at', '>=', $startDate); } if ($endDate) { - $query->whereDate('payments.created_at', '<=', $endDate); + $query->whereDate('subscriptions.created_at', '<=', $endDate); } if ($status && $status !== 'all') { - $query->where('payments.status', $status); + $query->where('subscriptions.status', $status === 'success' || $status === 'Completed' ? 'active' : $status); } if ($search) { $query->where(function($q) use ($search) { $q->where('users.name', 'like', "%{$search}%") - ->orWhere('payments.description', 'like', "%{$search}%"); + ->orWhere('subscriptions.plan_id', 'like', "%{$search}%"); }); } - $orderColumn = 'payments.created_at'; + $orderColumn = 'subscriptions.created_at'; $headers = ['Payment ID', 'Employer Name', 'Employer Email', 'Amount', 'Description', 'Status', 'Date']; $mapFn = function($pay) { + $planLabel = ucfirst($pay->plan_id) . ' Sponsor Pass'; return [ 'id' => 'PAY-' . str_pad($pay->id, 4, '0', STR_PAD_LEFT), 'employer_name' => $pay->user_name ?: 'System Guest / Sponsor', 'employer_email' => $pay->user_email ?: 'no-email@marketplace.com', - 'amount' => number_format((float)$pay->amount, 2) . ' AED', - 'description' => $pay->description ?: 'Subscription Plan', - 'status' => ucfirst($pay->status), + 'amount' => number_format((float)$pay->amount_aed, 2) . ' AED', + 'description' => $planLabel, + 'status' => $pay->status === 'active' ? 'Success' : ucfirst($pay->status), 'date' => date('Y-m-d H:i', strtotime($pay->created_at)), ]; }; diff --git a/app/Http/Controllers/Admin/DashboardController.php b/app/Http/Controllers/Admin/DashboardController.php index d7da971..65ce54d 100644 --- a/app/Http/Controllers/Admin/DashboardController.php +++ b/app/Http/Controllers/Admin/DashboardController.php @@ -18,14 +18,19 @@ public function index() $activeWorkers = \App\Models\Worker::where('status', 'active')->count(); $inactiveWorkers = $totalWorkers - $activeWorkers; - $totalSponsors = \App\Models\Sponsor::count(); - $activeSponsors = \App\Models\Sponsor::where('status', 'active')->count(); - $inactiveSponsors = $totalSponsors - $activeSponsors; + $totalEmployers = \App\Models\User::where('role', 'employer')->count(); + $activeEmployers = \App\Models\User::where('role', 'employer') + ->where(function($query) { + $query->whereHas('sponsor', function($q) { + $q->where('status', 'active'); + })->orWhereDoesntHave('sponsor'); + })->count(); + $inactiveEmployers = $totalEmployers - $activeEmployers; - $activeSubs = \App\Models\Sponsor::where('subscription_status', 'active')->count(); - $expiredSubs = \App\Models\Sponsor::where('subscription_status', 'expired')->count(); - $newSponsors = \App\Models\Sponsor::where('created_at', '>=', now()->subDays(7))->count(); - $revenueSum = \Illuminate\Support\Facades\DB::table('payments')->where('status', 'success')->sum('amount'); + $activeSubs = \Illuminate\Support\Facades\DB::table('subscriptions')->where('status', 'active')->count(); + $expiredSubs = \Illuminate\Support\Facades\DB::table('subscriptions')->where('status', 'expired')->count(); + $newEmployers = \App\Models\User::where('role', 'employer')->where('created_at', '>=', now()->subDays(7))->count(); + $revenueSum = \Illuminate\Support\Facades\DB::table('subscriptions')->where('status', 'active')->sum('amount_aed'); // Dynamic conversion rates $verifiedCount = \App\Models\Worker::where('verified', true)->count(); @@ -51,15 +56,18 @@ public function index() $monthStart = $date->copy()->startOfMonth(); $monthEnd = $date->copy()->endOfMonth(); - $basic = \App\Models\Sponsor::where('subscription_plan', 'basic') + $basic = \Illuminate\Support\Facades\DB::table('subscriptions') + ->where('plan_id', 'basic') ->whereBetween('created_at', [$monthStart, $monthEnd]) ->count(); - $premium = \App\Models\Sponsor::where('subscription_plan', 'premium') + $premium = \Illuminate\Support\Facades\DB::table('subscriptions') + ->where('plan_id', 'premium') ->whereBetween('created_at', [$monthStart, $monthEnd]) ->count(); - $vip = \App\Models\Sponsor::where('subscription_plan', 'vip') + $vip = \Illuminate\Support\Facades\DB::table('subscriptions') + ->whereIn('plan_id', ['vip', 'enterprise']) ->whereBetween('created_at', [$monthStart, $monthEnd]) ->count(); @@ -85,12 +93,12 @@ public function index() 'total_workers' => $totalWorkers, 'active_workers' => $activeWorkers, 'inactive_workers' => $inactiveWorkers, - 'total_employers' => $totalSponsors, - 'active_employers' => $activeSponsors, - 'inactive_employers' => $inactiveSponsors, + 'total_employers' => $totalEmployers, + 'active_employers' => $activeEmployers, + 'inactive_employers' => $inactiveEmployers, 'active_subscriptions' => $activeSubs, 'revenue_this_month_aed' => $revenueSum, - 'new_employers_this_week' => $newSponsors, + 'new_employers_this_week' => $newEmployers, 'expired_subscriptions' => $expiredSubs, 'hiring_conversion_rate' => $hiringConversionRate, 'chat_to_hire_rate' => $chatToHireRate, @@ -132,20 +140,23 @@ public function index() ]; })->toArray(); - // Fetch recent subscriptions from Sponsors table - $recentSponsorsWithPlans = \App\Models\Sponsor::whereNotNull('subscription_plan') - ->latest('created_at') + // Fetch recent subscriptions from subscriptions table + $recentSubs = \Illuminate\Support\Facades\DB::table('subscriptions') + ->leftJoin('users', 'subscriptions.user_id', '=', 'users.id') + ->select('subscriptions.*', 'users.name as employer_name') + ->latest('subscriptions.created_at') ->take(5) ->get(); $recentSubscriptions = []; - foreach ($recentSponsorsWithPlans as $sp) { + foreach ($recentSubs as $sub) { + $planName = ucfirst($sub->plan_id) . ' Pass'; $recentSubscriptions[] = [ - 'id' => $sp->id, - 'employer_name' => $sp->full_name, - 'plan_name' => ucfirst($sp->subscription_plan) . ' Pass', - 'amount_aed' => $sp->subscription_plan === 'vip' ? 499 : ($sp->subscription_plan === 'premium' ? 199 : 99), - 'subscribed_at' => $sp->created_at->diffForHumans(), + 'id' => $sub->id, + 'employer_name' => $sub->employer_name ?: 'System Guest / Sponsor', + 'plan_name' => $planName, + 'amount_aed' => (float)$sub->amount_aed, + 'subscribed_at' => \Carbon\Carbon::parse($sub->created_at)->diffForHumans(), ]; } diff --git a/app/Http/Controllers/Admin/FaqController.php b/app/Http/Controllers/Admin/FaqController.php new file mode 100644 index 0000000..f6611da --- /dev/null +++ b/app/Http/Controllers/Admin/FaqController.php @@ -0,0 +1,87 @@ +get()->map(function ($faq) { + return [ + 'id' => $faq->id, + 'question' => $faq->question, + 'answer' => $faq->answer, + 'category' => $faq->category, + 'is_published' => (bool)$faq->is_published, + 'created_at' => $faq->created_at->format('Y-m-d H:i'), + ]; + }); + + return Inertia::render('Admin/Faqs/Index', [ + 'faqs' => $faqs + ]); + } + + public function store(Request $request) + { + $request->validate([ + 'question' => 'required|string|max:1000', + 'answer' => 'required|string|max:5000', + 'category' => 'nullable|string|max:255', + 'is_published' => 'boolean' + ]); + + $question = trim($request->question); + if (!str_ends_with($question, '?')) { + $question .= '?'; + } + + Faq::create([ + 'question' => $question, + 'answer' => $request->answer, + 'category' => $request->category ?: 'General', + 'is_published' => $request->has('is_published') ? $request->is_published : true, + ]); + + return redirect()->back()->with('success', 'FAQ created successfully.'); + } + + public function update(Request $request, $id) + { + $faq = Faq::findOrFail($id); + + $request->validate([ + 'question' => 'required|string|max:1000', + 'answer' => 'required|string|max:5000', + 'category' => 'nullable|string|max:255', + 'is_published' => 'boolean' + ]); + + $question = trim($request->question); + if (!str_ends_with($question, '?')) { + $question .= '?'; + } + + $faq->update([ + 'question' => $question, + 'answer' => $request->answer, + 'category' => $request->category ?: 'General', + 'is_published' => $request->has('is_published') ? $request->is_published : true, + ]); + + return redirect()->back()->with('success', 'FAQ updated successfully.'); + } + + public function destroy($id) + { + $faq = Faq::findOrFail($id); + $faq->delete(); + + return redirect()->back()->with('success', 'FAQ deleted successfully.'); + } +} diff --git a/app/Http/Controllers/Admin/SponsorController.php b/app/Http/Controllers/Admin/SponsorController.php index 957f537..44d0f4d 100644 --- a/app/Http/Controllers/Admin/SponsorController.php +++ b/app/Http/Controllers/Admin/SponsorController.php @@ -15,7 +15,9 @@ class SponsorController extends Controller */ public function index(Request $request) { - $query = Sponsor::query(); + $query = Sponsor::whereNotIn('email', function($q) { + $q->select('email')->from('users')->where('role', 'employer'); + }); // Server-side Search if ($request->filled('search')) { @@ -95,9 +97,15 @@ public function index(Request $request) // Global stats counts (removing Active plans / Pending approval, replacing with Total, Verified, Suspended) $stats = [ - 'total' => Sponsor::count(), - 'verified' => Sponsor::where('is_verified', true)->count(), - 'suspended' => Sponsor::where('status', 'suspended')->count(), + 'total' => Sponsor::whereNotIn('email', function($q) { + $q->select('email')->from('users')->where('role', 'employer'); + })->count(), + 'verified' => Sponsor::whereNotIn('email', function($q) { + $q->select('email')->from('users')->where('role', 'employer'); + })->where('is_verified', true)->count(), + 'suspended' => Sponsor::whereNotIn('email', function($q) { + $q->select('email')->from('users')->where('role', 'employer'); + })->where('status', 'suspended')->count(), ]; return Inertia::render('Admin/CharityOrganizations/Index', [ @@ -113,7 +121,9 @@ public function index(Request $request) */ public function export(Request $request) { - $query = Sponsor::query(); + $query = Sponsor::whereNotIn('email', function($q) { + $q->select('email')->from('users')->where('role', 'employer'); + }); // Server-side Search if ($request->filled('search')) { diff --git a/app/Http/Controllers/Api/EmployerPaymentController.php b/app/Http/Controllers/Api/EmployerPaymentController.php index e85a321..d5e0ed6 100644 --- a/app/Http/Controllers/Api/EmployerPaymentController.php +++ b/app/Http/Controllers/Api/EmployerPaymentController.php @@ -22,49 +22,28 @@ public function getPayments(Request $request) try { $employerId = $employer->id; - // Auto-seed payments if none exist for a richer UX - $paymentsCount = Payment::where('user_id', $employerId)->count(); - if ($paymentsCount === 0) { - // Determine their plan - $subscription = DB::table('subscriptions')->where('user_id', $employerId)->first(); - $planName = $subscription ? (ucfirst($subscription->plan_id) . ' Pass Subscription') : 'Premium Sponsor Pass Subscription'; - $planAmount = $subscription ? $subscription->amount_aed : 199.00; - - for ($i = 0; $i < 8; $i++) { - Payment::create([ - 'user_id' => $employerId, - 'amount' => $planAmount, - 'currency' => 'AED', - 'description' => $planName, - 'status' => 'success', - 'created_at' => now()->subMonths($i)->subDays(rand(1, 5)), - 'updated_at' => now()->subMonths($i)->subDays(rand(1, 5)), - ]); - } - } - $page = (int)$request->input('page', 1); $perPage = (int)$request->input('per_page', 15); - $query = Payment::where('user_id', $employerId) - ->where(function ($q) { - $q->where('description', 'like', '%Subscription%') - ->orWhere('description', 'like', '%Pass%'); - }) - ->latest(); + $query = DB::table('subscriptions') + ->where('user_id', $employerId) + ->orderBy('created_at', 'desc'); + $total = $query->count(); $offset = ($page - 1) * $perPage; $payments = $query->skip($offset)->take($perPage)->get() - ->map(function ($payment) { + ->map(function ($sub) { + $planLabel = ucfirst($sub->plan_id) . ' Sponsor Pass'; + $startsAt = $sub->starts_at ?? $sub->created_at; return [ - 'id' => $payment->id, - 'amount' => (float)$payment->amount, - 'currency' => $payment->currency, - 'description' => $payment->description, - 'status' => $payment->status, - 'date' => $payment->created_at->format('Y-m-d H:i:s'), - 'formatted_date' => $payment->created_at->format('M d, Y'), + 'id' => $sub->id, + 'amount' => (float)$sub->amount_aed, + 'currency' => 'AED', + 'description' => $planLabel, + 'status' => $sub->status === 'active' ? 'success' : $sub->status, + 'date' => date('Y-m-d H:i:s', strtotime($startsAt)), + 'formatted_date' => date('M d, Y', strtotime($startsAt)), ]; }); diff --git a/app/Http/Controllers/Employer/ProfileController.php b/app/Http/Controllers/Employer/ProfileController.php index a362965..3b2198f 100644 --- a/app/Http/Controllers/Employer/ProfileController.php +++ b/app/Http/Controllers/Employer/ProfileController.php @@ -8,7 +8,6 @@ use App\Models\User; use App\Models\EmployerProfile; use Illuminate\Support\Facades\Hash; -use Illuminate\Validation\Rules\Password; class ProfileController extends Controller { @@ -36,6 +35,32 @@ private function resolveCurrentUser() return null; } + private function buildProfileData($user, $profile) + { + return [ + 'name' => $user->name, + 'email' => $user->email, + 'phone' => $profile->phone ?? '+971509990001', + 'emirates_id' => [ + 'emirates_id_number' => $profile->emirates_id_number ?? '784-1988-5310327-2', + 'name' => $profile->emirates_id_name ?? $user->name, + 'date_of_birth' => $profile->emirates_id_dob ?? '1990-01-01', + 'nationality' => $profile->nationality ?? 'Emirati', + 'issue_date' => $profile->emirates_id_issue_date ?? '2023-04-11', + 'expiry_date' => $profile->emirates_id_expiry ?? '2028-04-11', + 'employer' => $profile->emirates_id_employer ?? 'Federal Authority', + 'issue_place' => $profile->emirates_id_issue_place ?? 'Abu Dhabi', + 'occupation' => $profile->emirates_id_occupation ?? 'Manager', + ], + 'fcm_token' => $user->fcm_token ?? 'fcm_token_example', + 'address' => $profile->address ?? 'Villa 45, Street 12', + 'property_type' => $profile->property_type, + 'profile_photo_url' => $profile->profile_photo_url, + 'email_notifications' => (bool)($profile->email_notifications ?? true), + 'push_notifications' => (bool)($profile->push_notifications ?? true), + ]; + } + public function index(Request $request) { $user = $this->resolveCurrentUser(); @@ -48,35 +73,59 @@ public function index(Request $request) if (!$profile) { $profile = EmployerProfile::create([ 'user_id' => $user->id, - 'company_name' => 'Al Mansoor Household', - 'phone' => '+971 50 123 4567', - 'emirates_id_status' => 'approved', + 'phone' => '+971509990001', + 'emirates_id_number' => '784-1988-5310327-2', + 'emirates_id_name' => 'Ahmad Bin Ahmed', + 'emirates_id_dob' => '1990-01-01', + 'nationality' => 'Emirati', + 'emirates_id_issue_date' => '2023-04-11', + 'emirates_id_expiry' => '2028-04-11', + 'emirates_id_employer' => 'Federal Authority', + 'emirates_id_issue_place' => 'Abu Dhabi', + 'emirates_id_occupation' => 'Manager', + 'address' => 'Villa 45, Street 12', ]); } - $employerProfile = [ - 'name' => $user->name, - 'email' => $user->email, - 'company_name' => $profile->company_name, - 'phone' => $profile->phone, - 'language' => $profile->language ?? 'English', - 'notifications' => (bool)($profile->notifications ?? true), - 'nationality' => $profile->nationality, - 'family_size' => $profile->family_size, - 'accommodation' => $profile->accommodation, - 'district' => $profile->district, - 'emirates_id_front' => $profile->emirates_id_front, - 'emirates_id_back' => $profile->emirates_id_back, - 'verification_status' => $profile->verification_status ?? 'pending', - 'emirates_id_number' => $profile->emirates_id_number, - 'emirates_id_expiry' => $profile->emirates_id_expiry, - ]; + $employerProfile = $this->buildProfileData($user, $profile); return Inertia::render('Employer/Profile', [ 'employerProfile' => $employerProfile, ]); } + public function edit(Request $request) + { + $user = $this->resolveCurrentUser(); + if (!$user) { + return redirect()->route('employer.dashboard'); + } + + $profile = $user->employerProfile; + if (!$profile) { + $profile = EmployerProfile::create([ + 'user_id' => $user->id, + 'phone' => '+971509990001', + 'emirates_id_number' => '784-1988-5310327-2', + 'emirates_id_name' => 'Ahmad Bin Ahmed', + 'emirates_id_dob' => '1990-01-01', + 'nationality' => 'Emirati', + 'emirates_id_issue_date' => '2023-04-11', + 'emirates_id_expiry' => '2028-04-11', + 'emirates_id_employer' => 'Federal Authority', + 'emirates_id_issue_place' => 'Abu Dhabi', + 'emirates_id_occupation' => 'Manager', + 'address' => 'Villa 45, Street 12', + ]); + } + + $employerProfile = $this->buildProfileData($user, $profile); + + return Inertia::render('Employer/ProfileEdit', [ + 'employerProfile' => $employerProfile, + ]); + } + public function update(Request $request) { $user = $this->resolveCurrentUser(); @@ -97,17 +146,20 @@ public function update(Request $request) $sponsor ? 'unique:sponsors,email,' . $sponsor->id : 'unique:sponsors,email', ], 'phone' => 'required|string|max:255', - 'company_name' => 'required|string|max:255', - 'language' => 'required|string|in:English,Arabic,english,arabic', - 'notifications' => 'required|boolean', + 'company_name' => 'nullable|string|max:255', + 'language' => 'nullable|string|in:English,Arabic,english,arabic', + 'notifications' => 'nullable|boolean', 'nationality' => 'nullable|string|max:255', 'family_size' => 'nullable|string|max:255', 'accommodation' => 'nullable|string|max:255', 'district' => 'nullable|string|max:255', 'emirates_id_front' => 'nullable|file|image|max:10240', 'emirates_id_back' => 'nullable|file|image|max:10240', - 'current_password' => 'nullable|required_with:new_password|string', - 'new_password' => 'nullable|string|min:8|confirmed', + 'address' => 'nullable|string|max:255', + 'property_type' => 'nullable|string|max:255', + 'profile_photo' => 'nullable|image|max:10240', + 'email_notifications' => 'nullable|boolean', + 'push_notifications' => 'nullable|boolean', ]); $oldEmail = $user->getOriginal('email') ?? $user->email; @@ -133,17 +185,43 @@ public function update(Request $request) if (!$profile) { $profile = new EmployerProfile(['user_id' => $user->id]); } - $profile->company_name = $request->company_name; $profile->phone = $request->phone; - $profile->language = ucfirst(strtolower($request->language)); - $profile->notifications = $request->notifications; - $profile->nationality = $request->nationality; - $profile->family_size = $request->family_size; - $profile->accommodation = $request->accommodation; - $profile->district = $request->district; + if ($request->has('company_name')) { + $profile->company_name = $request->company_name; + } + if ($request->has('language')) { + $profile->language = ucfirst(strtolower($request->language)); + } + if ($request->has('notifications')) { + $profile->notifications = $request->notifications; + } + if ($request->has('nationality')) { + $profile->nationality = $request->nationality; + } + if ($request->has('family_size')) { + $profile->family_size = $request->family_size; + } + if ($request->has('accommodation')) { + $profile->accommodation = $request->accommodation; + } + if ($request->has('district')) { + $profile->district = $request->district; + } + if ($request->has('address')) { + $profile->address = $request->address; + } + if ($request->has('property_type')) { + $profile->property_type = $request->property_type; + } + if ($request->has('email_notifications')) { + $profile->email_notifications = $request->email_notifications; + } + if ($request->has('push_notifications')) { + $profile->push_notifications = $request->push_notifications; + } - // Document uploads with OCR extraction and PDPL compliance secure purge + // Handle physical uploads/OCR for Emirates ID $uploaded = false; $extractedIdNumber = null; $extractedExpiry = null; @@ -175,18 +253,15 @@ public function update(Request $request) $profile->verification_status = 'approved'; } - $profile->save(); - - // Update Password if provided - if ($request->filled('new_password')) { - if (!Hash::check($request->current_password, $user->password)) { - return back()->withErrors(['current_password' => 'The provided password does not match your current password.']); - } - $user->update([ - 'password' => Hash::make($request->new_password), - ]); + // Handle profile photo upload + if ($request->hasFile('profile_photo')) { + $file = $request->file('profile_photo'); + $path = $file->store('profile_photos', 'public'); + $profile->profile_photo_url = asset('storage/' . $path); } + $profile->save(); + // Update session user name/email session(['user' => (object)[ 'id' => $user->id, @@ -194,9 +269,9 @@ public function update(Request $request) 'email' => $user->email, 'role' => 'employer', 'subscription_status' => $user->subscription_status ?? 'active', - 'verification_status' => $profile->verification_status, + 'verification_status' => $profile->verification_status ?? 'approved', ]]); - return back()->with('success', 'Profile updated successfully.'); + return redirect()->route('employer.profile')->with('success', 'Profile updated successfully.'); } } diff --git a/app/Http/Controllers/Employer/SupportTicketController.php b/app/Http/Controllers/Employer/SupportTicketController.php index 8be09fa..825babe 100644 --- a/app/Http/Controllers/Employer/SupportTicketController.php +++ b/app/Http/Controllers/Employer/SupportTicketController.php @@ -58,8 +58,20 @@ public function index(Request $request) ]; }); + $faqs = \App\Models\Faq::where('is_published', true) + ->orderBy('id', 'asc') + ->get() + ->map(function ($faq) { + return [ + 'id' => $faq->id, + 'question' => $faq->question, + 'answer' => $faq->answer, + ]; + }); + return Inertia::render('Employer/Support/Index', [ 'tickets' => $tickets, + 'faqs' => $faqs, ]); } diff --git a/app/Models/EmployerProfile.php b/app/Models/EmployerProfile.php index 094ae88..06bbaf1 100644 --- a/app/Models/EmployerProfile.php +++ b/app/Models/EmployerProfile.php @@ -29,6 +29,10 @@ class EmployerProfile extends Model 'emirates_id_employer', 'emirates_id_issue_place', 'emirates_id_occupation', - 'address' + 'address', + 'property_type', + 'profile_photo_url', + 'email_notifications', + 'push_notifications' ]; } diff --git a/app/Models/Faq.php b/app/Models/Faq.php new file mode 100644 index 0000000..28db8c1 --- /dev/null +++ b/app/Models/Faq.php @@ -0,0 +1,15 @@ +string('property_type')->nullable(); + $table->string('profile_photo_url')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('employer_profiles', function (Blueprint $table) { + $table->dropColumn(['property_type', 'profile_photo_url']); + }); + } +}; diff --git a/database/migrations/2026_06_19_134828_add_notification_toggles_to_employer_profiles.php b/database/migrations/2026_06_19_134828_add_notification_toggles_to_employer_profiles.php new file mode 100644 index 0000000..7691c9b --- /dev/null +++ b/database/migrations/2026_06_19_134828_add_notification_toggles_to_employer_profiles.php @@ -0,0 +1,29 @@ +boolean('email_notifications')->default(true); + $table->boolean('push_notifications')->default(true); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('employer_profiles', function (Blueprint $table) { + $table->dropColumn(['email_notifications', 'push_notifications']); + }); + } +}; diff --git a/database/migrations/2026_06_19_141645_create_faqs_table.php b/database/migrations/2026_06_19_141645_create_faqs_table.php new file mode 100644 index 0000000..c8e6f7a --- /dev/null +++ b/database/migrations/2026_06_19_141645_create_faqs_table.php @@ -0,0 +1,31 @@ +id(); + $table->text('question'); + $table->text('answer'); + $table->string('category')->default('General'); + $table->boolean('is_published')->default(true); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('faqs'); + } +}; diff --git a/resources/js/Layouts/AdminLayout.jsx b/resources/js/Layouts/AdminLayout.jsx index 3241bed..1c1dc68 100644 --- a/resources/js/Layouts/AdminLayout.jsx +++ b/resources/js/Layouts/AdminLayout.jsx @@ -19,7 +19,8 @@ import { List, LifeBuoy, Heart, - Building + Building, + HelpCircle } from 'lucide-react'; import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet'; @@ -37,8 +38,8 @@ export default function AdminLayout({ children, title = 'Dashboard' }) { { name: 'OCR Verification', href: '/admin/workers/verifications', icon: ShieldCheck }, { name: 'Safety & Moderation', href: '/admin/safety', icon: ShieldAlert }, { name: 'Support Tickets', href: '/admin/tickets', icon: LifeBuoy }, + { name: 'Frequently Asked Questions', href: '/admin/faqs', icon: HelpCircle }, { name: 'Payments', href: '/admin/payments', icon: BadgeDollarSign }, - { name: 'Campaigns & Alerts', href: '/admin/notifications', icon: BellRing }, { name: 'Reports & Analytics', href: '/admin/analytics', icon: BarChart3 }, { name: 'System Audit Logs', href: '/admin/audit-logs', icon: History }, { name: 'Charity Events', href: '/admin/events', icon: Heart }, diff --git a/resources/js/Layouts/EmployerLayout.jsx b/resources/js/Layouts/EmployerLayout.jsx index c841f5f..b3413eb 100644 --- a/resources/js/Layouts/EmployerLayout.jsx +++ b/resources/js/Layouts/EmployerLayout.jsx @@ -252,11 +252,7 @@ export default function EmployerLayout({ children, title, fullPage = false }) { - {/* Chat/Notifications */} - - - - + {/* Profile Section with Dropdown */} diff --git a/resources/js/Pages/Admin/Faqs/Index.jsx b/resources/js/Pages/Admin/Faqs/Index.jsx new file mode 100644 index 0000000..0e0e27c --- /dev/null +++ b/resources/js/Pages/Admin/Faqs/Index.jsx @@ -0,0 +1,285 @@ +import React, { useState } from 'react'; +import { Head, router } from '@inertiajs/react'; +import AdminLayout from '@/Layouts/AdminLayout'; +import { + Plus, + Search, + Edit2, + Trash2, + HelpCircle, + Info +} from 'lucide-react'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; + +export default function FaqsIndex({ faqs }) { + const [searchTerm, setSearchTerm] = useState(''); + const [isDialogOpen, setIsDialogOpen] = useState(false); + const [editingFaq, setEditingFaq] = useState(null); + + const [questionText, setQuestionText] = useState(''); + const [answerText, setAnswerText] = useState(''); + const [isPublished, setIsPublished] = useState(true); + const [error, setError] = useState(''); + + const handleOpenAdd = () => { + setEditingFaq(null); + setQuestionText(''); + setAnswerText(''); + setIsPublished(true); + setError(''); + setIsDialogOpen(true); + }; + + const handleOpenEdit = (faq) => { + setEditingFaq(faq); + setQuestionText(faq.question); + setAnswerText(faq.answer); + setIsPublished(faq.is_published); + setError(''); + setIsDialogOpen(true); + }; + + const handleSubmit = (e) => { + e.preventDefault(); + if (!questionText.trim()) { + setError('Question is required.'); + return; + } + if (!answerText.trim()) { + setError('Answer is required.'); + return; + } + + const payload = { + question: questionText.trim(), + answer: answerText.trim(), + is_published: isPublished + }; + + if (editingFaq) { + router.post(`/admin/faqs/${editingFaq.id}`, payload, { + onSuccess: () => { + setIsDialogOpen(false); + }, + onError: (err) => { + setError(Object.values(err)[0] || 'Failed to update FAQ.'); + } + }); + } else { + router.post('/admin/faqs', payload, { + onSuccess: () => { + setIsDialogOpen(false); + }, + onError: (err) => { + setError(Object.values(err)[0] || 'Failed to add FAQ.'); + } + }); + } + }; + + const handleDelete = (id) => { + if (confirm('Are you sure you want to delete this FAQ?')) { + router.delete(`/admin/faqs/${id}`); + } + }; + + const filteredFaqs = faqs.filter(faq => + faq.question.toLowerCase().includes(searchTerm.toLowerCase()) || + faq.answer.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + return ( + + + +
+ {/* Header Actions */} +
+
+ + setSearchTerm(e.target.value)} + className="w-full pl-10 pr-4 py-2.5 bg-white border border-slate-200 rounded-xl text-sm font-medium focus:ring-4 focus:ring-teal-500/10 outline-none transition-all" + /> +
+ +
+ + {/* FAQs Card */} +
+
+
+
+ +
+
+

Frequently Asked Questions

+

Manage support & compliance FAQs displayed to users

+
+
+
+ + + + + Question & Answer + Status + Action + + + + {filteredFaqs.length > 0 ? ( + filteredFaqs.map((faq) => ( + + +
+
+ {faq.question.trim().endsWith('?') ? faq.question.trim() : faq.question.trim() + '?'} +
+
{faq.answer}
+
+
+ + + {faq.is_published ? 'Published' : 'Draft'} + + + +
+ + +
+
+
+ )) + ) : ( + + + No FAQs found. Add one to get started! + + + )} +
+
+ +
+
+ +

+ Tip: Draft FAQs will not be visible on user-facing support panels. Publish them to make them active. +

+
+
+
+
+ + {/* FAQ Add/Edit Modal */} + + + + + {editingFaq ? 'Edit FAQ' : 'New FAQ'} + + + +
+
+
+ + setQuestionText(e.target.value)} + /> +
+ +
+ +