faq added, issue fixed
This commit is contained in:
parent
ed7f2f8f4e
commit
56d8fc49aa
@ -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)),
|
||||
];
|
||||
};
|
||||
|
||||
@ -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(),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
87
app/Http/Controllers/Admin/FaqController.php
Normal file
87
app/Http/Controllers/Admin/FaqController.php
Normal file
@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Faq;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class FaqController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$faqs = Faq::orderBy('id', 'desc')->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.');
|
||||
}
|
||||
}
|
||||
@ -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')) {
|
||||
|
||||
@ -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)),
|
||||
];
|
||||
});
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
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,17 +253,14 @@ public function update(Request $request)
|
||||
$profile->verification_status = 'approved';
|
||||
}
|
||||
|
||||
$profile->save();
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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),
|
||||
]);
|
||||
}
|
||||
$profile->save();
|
||||
|
||||
// Update session user name/email
|
||||
session(['user' => (object)[
|
||||
@ -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.');
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -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'
|
||||
];
|
||||
}
|
||||
|
||||
15
app/Models/Faq.php
Normal file
15
app/Models/Faq.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Faq extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'question',
|
||||
'answer',
|
||||
'category',
|
||||
'is_published'
|
||||
];
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('employer_profiles', function (Blueprint $table) {
|
||||
$table->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']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('employer_profiles', function (Blueprint $table) {
|
||||
$table->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']);
|
||||
});
|
||||
}
|
||||
};
|
||||
31
database/migrations/2026_06_19_141645_create_faqs_table.php
Normal file
31
database/migrations/2026_06_19_141645_create_faqs_table.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('faqs', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@ -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 },
|
||||
|
||||
@ -252,11 +252,7 @@ export default function EmployerLayout({ children, title, fullPage = false }) {
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{/* Chat/Notifications */}
|
||||
<Link href="/employer/messages" className="relative p-2.5 text-slate-500 hover:text-[#185FA5] hover:bg-blue-50 rounded-xl transition-all group">
|
||||
<MessageSquare className="w-5 h-5" />
|
||||
<span className="absolute top-2 right-2 w-2 h-2 bg-red-500 rounded-full border-2 border-white group-hover:scale-110 transition-transform" />
|
||||
</Link>
|
||||
|
||||
|
||||
{/* Profile Section with Dropdown */}
|
||||
<DropdownMenu>
|
||||
|
||||
285
resources/js/Pages/Admin/Faqs/Index.jsx
Normal file
285
resources/js/Pages/Admin/Faqs/Index.jsx
Normal file
@ -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 (
|
||||
<AdminLayout title="FAQ Management">
|
||||
<Head title="Master Data - FAQ Management" />
|
||||
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
{/* Header Actions */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div className="relative flex-1 max-w-sm">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Find FAQ..."
|
||||
value={searchTerm}
|
||||
onChange={e => 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"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleOpenAdd}
|
||||
className="bg-[#0F6E56] text-white px-6 py-2.5 rounded-xl text-sm font-bold flex items-center justify-center space-x-2 hover:bg-[#085041] transition-all shadow-lg shadow-teal-500/10 cursor-pointer"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
<span>Add FAQ</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* FAQs Card */}
|
||||
<div className="bg-white rounded-[24px] border border-slate-200 shadow-sm overflow-hidden">
|
||||
<div className="p-6 border-b border-slate-50 bg-slate-50/30">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="p-2 bg-teal-50 rounded-lg">
|
||||
<HelpCircle className="w-5 h-5 text-[#0F6E56]" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-sm font-black text-slate-900 uppercase tracking-tight font-sans">Frequently Asked Questions</h2>
|
||||
<p className="text-[10px] text-slate-400 font-bold uppercase tracking-widest mt-0.5 font-sans">Manage support & compliance FAQs displayed to users</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-white hover:bg-white">
|
||||
<TableHead className="font-bold text-slate-400 text-[10px] uppercase tracking-widest h-12 pl-6">Question & Answer</TableHead>
|
||||
<TableHead className="font-bold text-slate-400 text-[10px] uppercase tracking-widest h-12">Status</TableHead>
|
||||
<TableHead className="font-bold text-slate-400 text-[10px] uppercase tracking-widest h-12 text-right pr-6">Action</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredFaqs.length > 0 ? (
|
||||
filteredFaqs.map((faq) => (
|
||||
<TableRow key={faq.id} className="hover:bg-slate-50/50 transition-colors group">
|
||||
<TableCell className="py-4 pl-6 max-w-[500px]">
|
||||
<div className="space-y-1">
|
||||
<div className="font-bold text-slate-900 text-sm leading-snug">
|
||||
{faq.question.trim().endsWith('?') ? faq.question.trim() : faq.question.trim() + '?'}
|
||||
</div>
|
||||
<div className="text-slate-500 text-xs font-medium leading-relaxed line-clamp-2">{faq.answer}</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="align-middle">
|
||||
<span className={`inline-flex items-center px-2 py-0.5 rounded text-[10px] font-black uppercase tracking-tight ${
|
||||
faq.is_published
|
||||
? 'bg-emerald-50 text-emerald-600'
|
||||
: 'bg-slate-100 text-slate-500'
|
||||
}`}>
|
||||
{faq.is_published ? 'Published' : 'Draft'}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-right pr-6 align-middle">
|
||||
<div className="flex items-center justify-end space-x-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
onClick={() => handleOpenEdit(faq)}
|
||||
className="p-2 text-slate-400 hover:text-[#0F6E56] hover:bg-teal-50 rounded-lg transition-all cursor-pointer"
|
||||
>
|
||||
<Edit2 className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(faq.id)}
|
||||
className="p-2 text-slate-400 hover:text-rose-500 hover:bg-rose-50 rounded-lg transition-all cursor-pointer"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={3} className="py-12 text-center text-slate-400 font-bold text-xs">
|
||||
No FAQs found. Add one to get started!
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<div className="p-6 bg-slate-50/50 border-t border-slate-100">
|
||||
<div className="flex items-start space-x-3 text-slate-400">
|
||||
<Info className="w-4 h-4 shrink-0 mt-0.5" />
|
||||
<p className="text-[10px] font-bold uppercase tracking-wide leading-relaxed">
|
||||
Tip: Draft FAQs will not be visible on user-facing support panels. Publish them to make them active.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* FAQ Add/Edit Modal */}
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogContent className="sm:max-w-[500px] rounded-[24px] bg-white">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl font-black text-slate-900 tracking-tight">
|
||||
{editingFaq ? 'Edit FAQ' : 'New FAQ'}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="py-4 space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Question</label>
|
||||
<input
|
||||
className="w-full px-4 py-3 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. How do I request a refund?"
|
||||
value={questionText}
|
||||
onChange={e => setQuestionText(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Answer</label>
|
||||
<textarea
|
||||
className="w-full px-4 py-3 bg-slate-50 border border-slate-100 rounded-xl text-sm font-bold focus:ring-4 focus:ring-teal-500/10 outline-none min-h-[120px] resize-none"
|
||||
placeholder="Describe the answer in detail..."
|
||||
value={answerText}
|
||||
onChange={e => setAnswerText(e.target.value)}
|
||||
/>
|
||||
{error && (
|
||||
<p className="text-xs text-rose-600 font-bold ml-1">{error}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsPublished(!isPublished)}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none ${
|
||||
isPublished ? 'bg-[#0F6E56]' : 'bg-slate-200'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
isPublished ? 'translate-x-6' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
<span className="text-xs font-bold text-slate-600">Publish Immediately</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="sm:justify-end gap-3 pt-4 border-t border-slate-100">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsDialogOpen(false)}
|
||||
className="px-6 py-2.5 rounded-xl text-sm font-bold text-slate-400 uppercase tracking-widest hover:bg-slate-50 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-[#0F6E56] text-white px-8 py-2.5 rounded-xl text-sm font-bold uppercase tracking-widest shadow-lg shadow-teal-500/20 active:scale-95 transition-all cursor-pointer"
|
||||
>
|
||||
{editingFaq ? 'Update' : 'Add FAQ'}
|
||||
</button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</AdminLayout>
|
||||
);
|
||||
}
|
||||
@ -174,6 +174,32 @@ export default function Index({ tickets, filters }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Frequently Asked Questions */}
|
||||
<div className="bg-white rounded-3xl border border-slate-200 p-8 shadow-sm space-y-6">
|
||||
<div className="flex items-center gap-2 pb-2">
|
||||
<MessageSquare className="w-5 h-5 text-[#185FA5]" />
|
||||
<h3 className="text-base font-bold text-slate-900">Frequently Asked Questions</h3>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* FAQ 1 */}
|
||||
<div className="p-6 bg-slate-50/50 hover:bg-slate-100/50 rounded-2xl border border-slate-150 transition-colors">
|
||||
<h4 className="text-sm font-bold text-slate-800">How long does a ticket resolution take?</h4>
|
||||
<p className="text-xs text-slate-500 font-medium leading-relaxed mt-2">
|
||||
For priority high tickets, our engineering support response is typically under 2 hours. General queries resolve within 24 hours.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* FAQ 2 */}
|
||||
<div className="p-6 bg-slate-50/50 hover:bg-slate-100/50 rounded-2xl border border-slate-150 transition-colors">
|
||||
<h4 className="text-sm font-bold text-slate-800">What is Tadbeer contract compliance?</h4>
|
||||
<p className="text-xs text-slate-500 font-medium leading-relaxed mt-2">
|
||||
All worker hirings conform to MOHRE Tadbeer rules. Contracts are electronically generated and uploaded for review.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filters Row */}
|
||||
<div className="bg-white border border-slate-200 rounded-2xl shadow-sm overflow-hidden">
|
||||
<div className="p-4 border-b border-slate-100 flex flex-col lg:flex-row lg:items-center justify-between gap-4 bg-slate-50/20">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
241
resources/js/Pages/Employer/ProfileEdit.jsx
Normal file
241
resources/js/Pages/Employer/ProfileEdit.jsx
Normal file
@ -0,0 +1,241 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Head, useForm, Link } from '@inertiajs/react';
|
||||
import EmployerLayout from '../../Layouts/EmployerLayout';
|
||||
import { useTranslation } from '../../lib/LanguageContext';
|
||||
import {
|
||||
User,
|
||||
Mail,
|
||||
Phone,
|
||||
MapPin,
|
||||
Home,
|
||||
Camera,
|
||||
Save,
|
||||
XCircle,
|
||||
ArrowLeft,
|
||||
Compass
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export default function ProfileEdit({ employerProfile }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data, setData, post, processing, errors } = useForm({
|
||||
name: employerProfile?.name || '',
|
||||
email: employerProfile?.email || '',
|
||||
phone: employerProfile?.phone || '',
|
||||
address: employerProfile?.address || '',
|
||||
property_type: employerProfile?.property_type || '',
|
||||
profile_photo: null,
|
||||
});
|
||||
|
||||
const [photoPreview, setPhotoPreview] = useState(employerProfile?.profile_photo_url || null);
|
||||
|
||||
const handlePhotoChange = (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
setData('profile_photo', file);
|
||||
setPhotoPreview(URL.createObjectURL(file));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
post('/employer/profile/update', {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
toast.success(t('profile_updated_successfully', 'Profile updated successfully!'));
|
||||
},
|
||||
onError: (errs) => {
|
||||
toast.error(t('profile_update_failed', 'Failed to update profile'), {
|
||||
description: Object.values(errs)[0] || t('check_form_fields', 'Please check the form fields.')
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<EmployerLayout title={t('edit_profile', 'Edit Profile')}>
|
||||
<Head title={`${t('edit_profile', 'Edit Profile')} - ${t('employer_portal', 'Employer Portal')}`} />
|
||||
|
||||
<div className="max-w-3xl mx-auto pb-16 space-y-8 select-none animate-in fade-in duration-500">
|
||||
{/* Back Link */}
|
||||
<div className="flex items-center">
|
||||
<Link
|
||||
href="/employer/profile"
|
||||
className="text-slate-450 hover:text-[#185FA5] flex items-center gap-2 text-xs font-black uppercase tracking-wider transition-colors group"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 transition-transform group-hover:-translate-x-1" />
|
||||
<span>{t('back_to_profile', 'Back to Profile')}</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Edit Form Card */}
|
||||
<div className="bg-white rounded-3xl border border-slate-200 shadow-sm overflow-hidden">
|
||||
{/* Header Banner Accent */}
|
||||
<div className="h-28 bg-gradient-to-r from-[#185FA5] to-[#2d7ad6] relative">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_right,rgba(255,255,255,0.1),transparent)] pointer-events-none" />
|
||||
<div className="absolute -bottom-6 left-8 flex items-center gap-2 text-white/5 select-none">
|
||||
<Compass className="w-20 h-20 stroke-[0.5]" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="px-8 pb-8 pt-4 space-y-8 relative">
|
||||
|
||||
{/* Profile Photo Floating Container */}
|
||||
<div className="flex flex-col sm:flex-row items-center gap-6 p-6 bg-slate-50 rounded-2xl border border-slate-150 -mt-14 relative z-10">
|
||||
<div className="relative group">
|
||||
<div className="w-20 h-20 rounded-2xl bg-white p-1 border border-slate-200 shadow-md overflow-hidden">
|
||||
{photoPreview ? (
|
||||
<img
|
||||
src={photoPreview}
|
||||
alt="Preview"
|
||||
className="w-full h-full rounded-xl object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full rounded-xl bg-[#185FA5]/10 text-[#185FA5] flex items-center justify-center font-black text-3xl">
|
||||
{data.name?.charAt(0) || 'U'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<label className="absolute -bottom-2 -right-2 p-2 bg-[#185FA5] text-white rounded-xl shadow-lg hover:bg-[#144f8a] cursor-pointer transition-colors border border-white">
|
||||
<Camera className="w-4 h-4" />
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handlePhotoChange}
|
||||
className="hidden"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="space-y-1 text-center sm:text-left flex-1">
|
||||
<h4 className="text-xs font-black text-slate-800 uppercase tracking-wide">{t('profile_avatar', 'Profile Photo')}</h4>
|
||||
<p className="text-[10px] text-slate-400 font-bold uppercase tracking-wider">
|
||||
{t('avatar_rules', 'Upload high-resolution avatar. Max 10MB.')}
|
||||
</p>
|
||||
</div>
|
||||
{errors.profile_photo && (
|
||||
<p className="text-xs text-red-500 mt-1">{errors.profile_photo}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Fields Grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6 pt-4">
|
||||
{/* Full Name */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-black text-slate-500 uppercase tracking-tighter ml-1">
|
||||
{t('full_name', 'Full Name')}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<User className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
value={data.name}
|
||||
onChange={(e) => setData('name', e.target.value)}
|
||||
className="w-full pl-12 pr-4 py-3 bg-slate-50 border border-slate-150 rounded-2xl text-xs font-bold focus:ring-2 focus:ring-blue-100 focus:bg-white transition-all outline-none"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{errors.name && <p className="text-xs text-red-500 mt-1">{errors.name}</p>}
|
||||
</div>
|
||||
|
||||
{/* Email */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-black text-slate-500 uppercase tracking-tighter ml-1">
|
||||
{t('email_address', 'Email Address')}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Mail className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||
<input
|
||||
type="email"
|
||||
value={data.email}
|
||||
onChange={(e) => setData('email', e.target.value)}
|
||||
className="w-full pl-12 pr-4 py-3 bg-slate-50 border border-slate-150 rounded-2xl text-xs font-bold focus:ring-2 focus:ring-blue-100 focus:bg-white transition-all outline-none"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{errors.email && <p className="text-xs text-red-500 mt-1">{errors.email}</p>}
|
||||
</div>
|
||||
|
||||
{/* Phone */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-black text-slate-500 uppercase tracking-tighter ml-1">
|
||||
{t('phone_number', 'Phone Number')}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Phone className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
value={data.phone}
|
||||
onChange={(e) => setData('phone', e.target.value)}
|
||||
className="w-full pl-12 pr-4 py-3 bg-slate-50 border border-slate-150 rounded-2xl text-xs font-bold focus:ring-2 focus:ring-blue-100 focus:bg-white transition-all outline-none"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{errors.phone && <p className="text-xs text-red-500 mt-1">{errors.phone}</p>}
|
||||
</div>
|
||||
|
||||
{/* Property Type */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-black text-slate-500 uppercase tracking-tighter ml-1">
|
||||
{t('property_type', 'Property Type')}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Home className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||
<select
|
||||
value={data.property_type}
|
||||
onChange={(e) => setData('property_type', e.target.value)}
|
||||
className="w-full pl-12 pr-4 py-3 bg-slate-50 border border-slate-150 rounded-2xl text-xs font-bold focus:ring-2 focus:ring-blue-100 focus:bg-white transition-all outline-none cursor-pointer"
|
||||
>
|
||||
<option value="">{t('select_property_type', 'Select Property Type')}</option>
|
||||
<option value="Villa">{t('villa', 'Villa')}</option>
|
||||
<option value="Apartment">{t('apartment', 'Apartment')}</option>
|
||||
<option value="Penthouse">{t('penthouse', 'Penthouse')}</option>
|
||||
<option value="Townhouse">{t('townhouse', 'Townhouse')}</option>
|
||||
</select>
|
||||
</div>
|
||||
{errors.property_type && <p className="text-xs text-red-500 mt-1">{errors.property_type}</p>}
|
||||
</div>
|
||||
|
||||
{/* Address */}
|
||||
<div className="space-y-2 sm:col-span-2">
|
||||
<label className="text-xs font-black text-slate-500 uppercase tracking-tighter ml-1">
|
||||
{t('address', 'Address')}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<MapPin className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
value={data.address}
|
||||
onChange={(e) => setData('address', e.target.value)}
|
||||
className="w-full pl-12 pr-4 py-3 bg-slate-50 border border-slate-150 rounded-2xl text-xs font-bold focus:ring-2 focus:ring-blue-100 focus:bg-white transition-all outline-none"
|
||||
/>
|
||||
</div>
|
||||
{errors.address && <p className="text-xs text-red-500 mt-1">{errors.address}</p>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Form Action Controls */}
|
||||
<div className="pt-6 border-t border-slate-100 flex flex-col sm:flex-row items-center justify-end gap-4">
|
||||
<Link
|
||||
href="/employer/profile"
|
||||
className="w-full sm:w-auto px-6 py-3.5 border border-slate-200 hover:bg-slate-50 text-slate-650 rounded-2xl text-xs font-black uppercase tracking-wider text-center transition-colors outline-none"
|
||||
>
|
||||
{t('cancel', 'Cancel')}
|
||||
</Link>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={processing}
|
||||
className="w-full sm:w-auto bg-[#185FA5] hover:bg-[#144f8a] text-white px-8 py-3.5 rounded-2xl text-xs font-black uppercase tracking-wider flex items-center justify-center space-x-2 transition-all shadow-md shadow-blue-200/50"
|
||||
>
|
||||
<Save className="w-4 h-4" />
|
||||
<span>{t('save_changes', 'Save Changes')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</EmployerLayout>
|
||||
);
|
||||
}
|
||||
@ -14,7 +14,7 @@ import {
|
||||
MessageSquare
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function Index({ tickets }) {
|
||||
export default function Index({ tickets, faqs = [] }) {
|
||||
const { t } = useTranslation();
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState('all');
|
||||
@ -204,6 +204,7 @@ export default function Index({ tickets }) {
|
||||
</div>
|
||||
|
||||
{/* FAQ section */}
|
||||
{faqs && faqs.length > 0 && (
|
||||
<div className="bg-slate-50 p-6 rounded-3xl border border-slate-200/80 space-y-4">
|
||||
<h3 className="font-black text-slate-800 text-sm flex items-center space-x-2">
|
||||
<MessageSquare className="w-4 h-4 text-[#185FA5]" />
|
||||
@ -211,16 +212,17 @@ export default function Index({ tickets }) {
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-xs">
|
||||
<div className="bg-white p-4 rounded-2xl border border-slate-200/50 space-y-1">
|
||||
<h4 className="font-black text-slate-800">{t('faq_q1', 'How long does a ticket resolution take?')}</h4>
|
||||
<p className="text-slate-500 font-semibold">{t('faq_a1', 'For priority high tickets, our engineering support response is typically under 2 hours. General queries resolve within 24 hours.')}</p>
|
||||
</div>
|
||||
<div className="bg-white p-4 rounded-2xl border border-slate-200/50 space-y-1">
|
||||
<h4 className="font-black text-slate-800">{t('faq_q2', 'What is Tadbeer contract compliance?')}</h4>
|
||||
<p className="text-slate-500 font-semibold">{t('faq_a2', 'All worker hirings conform to MOHRE Tadbeer rules. Contracts are electronically generated and uploaded for review.')}</p>
|
||||
{faqs.map((faq) => (
|
||||
<div key={faq.id} className="bg-white p-4 rounded-2xl border border-slate-200/50 space-y-1">
|
||||
<h4 className="font-black text-slate-800">
|
||||
{faq.question.trim().endsWith('?') ? faq.question.trim() : faq.question.trim() + '?'}
|
||||
</h4>
|
||||
<p className="text-slate-500 font-semibold">{faq.answer}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</EmployerLayout>
|
||||
|
||||
@ -80,28 +80,32 @@
|
||||
})->name('admin.subscriptions');
|
||||
|
||||
Route::get('/payments', function () {
|
||||
$payments = \Illuminate\Support\Facades\DB::table('payments')
|
||||
->leftJoin('users', 'payments.user_id', '=', 'users.id')
|
||||
->select('payments.*', 'users.name as user_name', 'users.email as user_email')
|
||||
->orderBy('payments.created_at', 'desc')
|
||||
$payments = \Illuminate\Support\Facades\DB::table('subscriptions')
|
||||
->leftJoin('users', 'subscriptions.user_id', '=', 'users.id')
|
||||
->select('subscriptions.*', 'users.name as user_name', 'users.email as user_email')
|
||||
->orderBy('subscriptions.created_at', 'desc')
|
||||
->get()
|
||||
->map(function($payment) {
|
||||
->map(function($sub) {
|
||||
$planLabel = ucfirst($sub->plan_id) . ' Sponsor Pass';
|
||||
$status = $sub->status === 'active' ? 'Completed' : (ucfirst($sub->status) ?: 'Completed');
|
||||
$startsAt = $sub->starts_at ?? $sub->created_at;
|
||||
$expiresAt = $sub->expires_at ?? date('Y-m-d H:i:s', strtotime($startsAt . ' +1 month'));
|
||||
return [
|
||||
'id' => 'PAY-' . str_pad($payment->id, 3, '0', STR_PAD_LEFT),
|
||||
'employer' => $payment->user_name ?: 'System Guest / Sponsor',
|
||||
'plan' => $payment->description ?: 'Subscription Plan',
|
||||
'amount' => (float)$payment->amount,
|
||||
'method' => 'Stripe Gateway',
|
||||
'date' => date('Y-m-d', strtotime($payment->created_at)),
|
||||
'status' => $payment->status === 'success' ? 'Completed' : (ucfirst($payment->status) ?: 'Completed'),
|
||||
'period' => date('M Y', strtotime($payment->created_at)) . ' - ' . date('M Y', strtotime($payment->created_at . ' +1 month')),
|
||||
'id' => 'PAY-' . str_pad($sub->id, 3, '0', STR_PAD_LEFT),
|
||||
'employer' => $sub->user_name ?: 'System Guest / Sponsor',
|
||||
'plan' => $planLabel,
|
||||
'amount' => (float)$sub->amount_aed,
|
||||
'method' => 'PayTabs Gateway',
|
||||
'date' => date('Y-m-d', strtotime($startsAt)),
|
||||
'status' => $status,
|
||||
'period' => date('M Y', strtotime($startsAt)) . ' - ' . date('M Y', strtotime($expiresAt)),
|
||||
];
|
||||
});
|
||||
|
||||
$totalRevenue = \Illuminate\Support\Facades\DB::table('payments')->where('status', 'success')->sum('amount');
|
||||
$activeSubs = \Illuminate\Support\Facades\DB::table('sponsors')->where('subscription_status', 'active')->count();
|
||||
$failedPayments = \Illuminate\Support\Facades\DB::table('payments')->where('status', 'failed')->count();
|
||||
$avgTicket = \Illuminate\Support\Facades\DB::table('payments')->where('status', 'success')->avg('amount') ?: 0;
|
||||
$totalRevenue = \Illuminate\Support\Facades\DB::table('subscriptions')->where('status', 'active')->sum('amount_aed');
|
||||
$activeSubs = \Illuminate\Support\Facades\DB::table('subscriptions')->where('status', 'active')->count();
|
||||
$failedPayments = \Illuminate\Support\Facades\DB::table('subscriptions')->where('status', 'failed')->count();
|
||||
$avgTicket = \Illuminate\Support\Facades\DB::table('subscriptions')->where('status', 'active')->avg('amount_aed') ?: 0;
|
||||
|
||||
$stats = [
|
||||
'total_revenue' => number_format($totalRevenue, 2),
|
||||
@ -243,12 +247,16 @@
|
||||
// Route::post('/disputes/{id}/resolve', [\App\Http\Controllers\Admin\AdminExtraController::class, 'resolveDispute'])->name('admin.disputes.resolve');
|
||||
// Route::post('/disputes/{id}/add-note', [\App\Http\Controllers\Admin\AdminExtraController::class, 'addDisputeNote'])->name('admin.disputes.add-note');
|
||||
|
||||
Route::get('/notifications', [\App\Http\Controllers\Admin\AdminExtraController::class, 'notifications'])->name('admin.notifications');
|
||||
Route::post('/notifications/broadcast', [\App\Http\Controllers\Admin\AdminExtraController::class, 'broadcastNotification'])->name('admin.notifications.broadcast');
|
||||
|
||||
Route::get('/analytics', [\App\Http\Controllers\Admin\AdminExtraController::class, 'analytics'])->name('admin.analytics');
|
||||
Route::get('/audit-logs', [\App\Http\Controllers\Admin\AdminExtraController::class, 'auditLogs'])->name('admin.audit-logs');
|
||||
|
||||
// Admin FAQs Management
|
||||
Route::get('/faqs', [\App\Http\Controllers\Admin\FaqController::class, 'index'])->name('admin.faqs');
|
||||
Route::post('/faqs', [\App\Http\Controllers\Admin\FaqController::class, 'store'])->name('admin.faqs.store');
|
||||
Route::post('/faqs/{id}', [\App\Http\Controllers\Admin\FaqController::class, 'update'])->name('admin.faqs.update');
|
||||
Route::delete('/faqs/{id}', [\App\Http\Controllers\Admin\FaqController::class, 'destroy'])->name('admin.faqs.destroy');
|
||||
|
||||
// Admin Support Tickets
|
||||
Route::get('/tickets', [\App\Http\Controllers\Admin\SupportTicketController::class, 'index'])->name('admin.tickets.index');
|
||||
Route::get('/tickets/{id}', [\App\Http\Controllers\Admin\SupportTicketController::class, 'show'])->name('admin.tickets.show');
|
||||
@ -374,6 +382,7 @@
|
||||
Route::post('/events/create', [\App\Http\Controllers\Employer\AnnouncementController::class, 'store'])->name('employer.events.store');
|
||||
|
||||
Route::get('/profile', [\App\Http\Controllers\Employer\ProfileController::class, 'index'])->name('employer.profile');
|
||||
Route::get('/profile/edit', [\App\Http\Controllers\Employer\ProfileController::class, 'edit'])->name('employer.profile.edit');
|
||||
Route::post('/profile/update', [\App\Http\Controllers\Employer\ProfileController::class, 'update'])->name('employer.profile.update');
|
||||
|
||||
Route::get('/payments', [\App\Http\Controllers\Employer\PaymentController::class, 'index'])->name('employer.payments');
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user