faq added, issue fixed

This commit is contained in:
mohanmd 2026-06-19 16:05:22 +05:30
parent ed7f2f8f4e
commit 56d8fc49aa
20 changed files with 1524 additions and 661 deletions

View File

@ -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)),
];
};

View File

@ -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(),
];
}

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

View File

@ -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')) {

View File

@ -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)),
];
});

View File

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

View File

@ -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,
]);
}

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

View File

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

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

View File

@ -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,23 +204,25 @@ export default function Index({ tickets }) {
</div>
{/* FAQ section */}
<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]" />
<span>{t('frequently_asked_questions', 'Frequently Asked Questions')}</span>
</h3>
{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]" />
<span>{t('frequently_asked_questions', 'Frequently Asked Questions')}</span>
</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>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-xs">
{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>
)}
</div>
</EmployerLayout>

View File

@ -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');