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'
|
'amount' => 'required|numeric|min:1'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$payment = DB::table('payments')->where('id', $id)->first();
|
$payment = DB::table('subscriptions')->where('id', $id)->first();
|
||||||
if ($payment) {
|
if ($payment) {
|
||||||
DB::table('payments')->where('id', $id)->update([
|
DB::table('subscriptions')->where('id', $id)->update([
|
||||||
'status' => 'refunded',
|
'status' => 'refunded',
|
||||||
'description' => $payment->description . " (Refunded: " . $request->refund_reason . ")",
|
|
||||||
'updated_at' => now()
|
'updated_at' => now()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -448,7 +447,7 @@ public function analytics(Request $request)
|
|||||||
$orderColumn = 'users.created_at';
|
$orderColumn = 'users.created_at';
|
||||||
$headers = ['Employer ID', 'Name', 'Email', 'Status', 'Subscription Status', 'Total Spent', 'Joined Date'];
|
$headers = ['Employer ID', 'Name', 'Email', 'Status', 'Subscription Status', 'Total Spent', 'Joined Date'];
|
||||||
$mapFn = function($emp) {
|
$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 [
|
return [
|
||||||
'id' => 'EMP-' . str_pad($emp->id, 4, '0', STR_PAD_LEFT),
|
'id' => 'EMP-' . str_pad($emp->id, 4, '0', STR_PAD_LEFT),
|
||||||
'name' => $emp->name,
|
'name' => $emp->name,
|
||||||
@ -461,36 +460,37 @@ public function analytics(Request $request)
|
|||||||
};
|
};
|
||||||
|
|
||||||
} elseif ($reportType === 'payments') {
|
} elseif ($reportType === 'payments') {
|
||||||
$query = DB::table('payments')
|
$query = DB::table('subscriptions')
|
||||||
->leftJoin('users', 'payments.user_id', '=', 'users.id')
|
->leftJoin('users', 'subscriptions.user_id', '=', 'users.id')
|
||||||
->select('payments.*', 'users.name as user_name', 'users.email as user_email');
|
->select('subscriptions.*', 'users.name as user_name', 'users.email as user_email');
|
||||||
|
|
||||||
if ($startDate) {
|
if ($startDate) {
|
||||||
$query->whereDate('payments.created_at', '>=', $startDate);
|
$query->whereDate('subscriptions.created_at', '>=', $startDate);
|
||||||
}
|
}
|
||||||
if ($endDate) {
|
if ($endDate) {
|
||||||
$query->whereDate('payments.created_at', '<=', $endDate);
|
$query->whereDate('subscriptions.created_at', '<=', $endDate);
|
||||||
}
|
}
|
||||||
if ($status && $status !== 'all') {
|
if ($status && $status !== 'all') {
|
||||||
$query->where('payments.status', $status);
|
$query->where('subscriptions.status', $status === 'success' || $status === 'Completed' ? 'active' : $status);
|
||||||
}
|
}
|
||||||
if ($search) {
|
if ($search) {
|
||||||
$query->where(function($q) use ($search) {
|
$query->where(function($q) use ($search) {
|
||||||
$q->where('users.name', 'like', "%{$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'];
|
$headers = ['Payment ID', 'Employer Name', 'Employer Email', 'Amount', 'Description', 'Status', 'Date'];
|
||||||
$mapFn = function($pay) {
|
$mapFn = function($pay) {
|
||||||
|
$planLabel = ucfirst($pay->plan_id) . ' Sponsor Pass';
|
||||||
return [
|
return [
|
||||||
'id' => 'PAY-' . str_pad($pay->id, 4, '0', STR_PAD_LEFT),
|
'id' => 'PAY-' . str_pad($pay->id, 4, '0', STR_PAD_LEFT),
|
||||||
'employer_name' => $pay->user_name ?: 'System Guest / Sponsor',
|
'employer_name' => $pay->user_name ?: 'System Guest / Sponsor',
|
||||||
'employer_email' => $pay->user_email ?: 'no-email@marketplace.com',
|
'employer_email' => $pay->user_email ?: 'no-email@marketplace.com',
|
||||||
'amount' => number_format((float)$pay->amount, 2) . ' AED',
|
'amount' => number_format((float)$pay->amount_aed, 2) . ' AED',
|
||||||
'description' => $pay->description ?: 'Subscription Plan',
|
'description' => $planLabel,
|
||||||
'status' => ucfirst($pay->status),
|
'status' => $pay->status === 'active' ? 'Success' : ucfirst($pay->status),
|
||||||
'date' => date('Y-m-d H:i', strtotime($pay->created_at)),
|
'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();
|
$activeWorkers = \App\Models\Worker::where('status', 'active')->count();
|
||||||
$inactiveWorkers = $totalWorkers - $activeWorkers;
|
$inactiveWorkers = $totalWorkers - $activeWorkers;
|
||||||
|
|
||||||
$totalSponsors = \App\Models\Sponsor::count();
|
$totalEmployers = \App\Models\User::where('role', 'employer')->count();
|
||||||
$activeSponsors = \App\Models\Sponsor::where('status', 'active')->count();
|
$activeEmployers = \App\Models\User::where('role', 'employer')
|
||||||
$inactiveSponsors = $totalSponsors - $activeSponsors;
|
->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();
|
$activeSubs = \Illuminate\Support\Facades\DB::table('subscriptions')->where('status', 'active')->count();
|
||||||
$expiredSubs = \App\Models\Sponsor::where('subscription_status', 'expired')->count();
|
$expiredSubs = \Illuminate\Support\Facades\DB::table('subscriptions')->where('status', 'expired')->count();
|
||||||
$newSponsors = \App\Models\Sponsor::where('created_at', '>=', now()->subDays(7))->count();
|
$newEmployers = \App\Models\User::where('role', 'employer')->where('created_at', '>=', now()->subDays(7))->count();
|
||||||
$revenueSum = \Illuminate\Support\Facades\DB::table('payments')->where('status', 'success')->sum('amount');
|
$revenueSum = \Illuminate\Support\Facades\DB::table('subscriptions')->where('status', 'active')->sum('amount_aed');
|
||||||
|
|
||||||
// Dynamic conversion rates
|
// Dynamic conversion rates
|
||||||
$verifiedCount = \App\Models\Worker::where('verified', true)->count();
|
$verifiedCount = \App\Models\Worker::where('verified', true)->count();
|
||||||
@ -51,15 +56,18 @@ public function index()
|
|||||||
$monthStart = $date->copy()->startOfMonth();
|
$monthStart = $date->copy()->startOfMonth();
|
||||||
$monthEnd = $date->copy()->endOfMonth();
|
$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])
|
->whereBetween('created_at', [$monthStart, $monthEnd])
|
||||||
->count();
|
->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])
|
->whereBetween('created_at', [$monthStart, $monthEnd])
|
||||||
->count();
|
->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])
|
->whereBetween('created_at', [$monthStart, $monthEnd])
|
||||||
->count();
|
->count();
|
||||||
|
|
||||||
@ -85,12 +93,12 @@ public function index()
|
|||||||
'total_workers' => $totalWorkers,
|
'total_workers' => $totalWorkers,
|
||||||
'active_workers' => $activeWorkers,
|
'active_workers' => $activeWorkers,
|
||||||
'inactive_workers' => $inactiveWorkers,
|
'inactive_workers' => $inactiveWorkers,
|
||||||
'total_employers' => $totalSponsors,
|
'total_employers' => $totalEmployers,
|
||||||
'active_employers' => $activeSponsors,
|
'active_employers' => $activeEmployers,
|
||||||
'inactive_employers' => $inactiveSponsors,
|
'inactive_employers' => $inactiveEmployers,
|
||||||
'active_subscriptions' => $activeSubs,
|
'active_subscriptions' => $activeSubs,
|
||||||
'revenue_this_month_aed' => $revenueSum,
|
'revenue_this_month_aed' => $revenueSum,
|
||||||
'new_employers_this_week' => $newSponsors,
|
'new_employers_this_week' => $newEmployers,
|
||||||
'expired_subscriptions' => $expiredSubs,
|
'expired_subscriptions' => $expiredSubs,
|
||||||
'hiring_conversion_rate' => $hiringConversionRate,
|
'hiring_conversion_rate' => $hiringConversionRate,
|
||||||
'chat_to_hire_rate' => $chatToHireRate,
|
'chat_to_hire_rate' => $chatToHireRate,
|
||||||
@ -132,20 +140,23 @@ public function index()
|
|||||||
];
|
];
|
||||||
})->toArray();
|
})->toArray();
|
||||||
|
|
||||||
// Fetch recent subscriptions from Sponsors table
|
// Fetch recent subscriptions from subscriptions table
|
||||||
$recentSponsorsWithPlans = \App\Models\Sponsor::whereNotNull('subscription_plan')
|
$recentSubs = \Illuminate\Support\Facades\DB::table('subscriptions')
|
||||||
->latest('created_at')
|
->leftJoin('users', 'subscriptions.user_id', '=', 'users.id')
|
||||||
|
->select('subscriptions.*', 'users.name as employer_name')
|
||||||
|
->latest('subscriptions.created_at')
|
||||||
->take(5)
|
->take(5)
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
$recentSubscriptions = [];
|
$recentSubscriptions = [];
|
||||||
foreach ($recentSponsorsWithPlans as $sp) {
|
foreach ($recentSubs as $sub) {
|
||||||
|
$planName = ucfirst($sub->plan_id) . ' Pass';
|
||||||
$recentSubscriptions[] = [
|
$recentSubscriptions[] = [
|
||||||
'id' => $sp->id,
|
'id' => $sub->id,
|
||||||
'employer_name' => $sp->full_name,
|
'employer_name' => $sub->employer_name ?: 'System Guest / Sponsor',
|
||||||
'plan_name' => ucfirst($sp->subscription_plan) . ' Pass',
|
'plan_name' => $planName,
|
||||||
'amount_aed' => $sp->subscription_plan === 'vip' ? 499 : ($sp->subscription_plan === 'premium' ? 199 : 99),
|
'amount_aed' => (float)$sub->amount_aed,
|
||||||
'subscribed_at' => $sp->created_at->diffForHumans(),
|
'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)
|
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
|
// Server-side Search
|
||||||
if ($request->filled('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)
|
// Global stats counts (removing Active plans / Pending approval, replacing with Total, Verified, Suspended)
|
||||||
$stats = [
|
$stats = [
|
||||||
'total' => Sponsor::count(),
|
'total' => Sponsor::whereNotIn('email', function($q) {
|
||||||
'verified' => Sponsor::where('is_verified', true)->count(),
|
$q->select('email')->from('users')->where('role', 'employer');
|
||||||
'suspended' => Sponsor::where('status', 'suspended')->count(),
|
})->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', [
|
return Inertia::render('Admin/CharityOrganizations/Index', [
|
||||||
@ -113,7 +121,9 @@ public function index(Request $request)
|
|||||||
*/
|
*/
|
||||||
public function export(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
|
// Server-side Search
|
||||||
if ($request->filled('search')) {
|
if ($request->filled('search')) {
|
||||||
|
|||||||
@ -22,49 +22,28 @@ public function getPayments(Request $request)
|
|||||||
try {
|
try {
|
||||||
$employerId = $employer->id;
|
$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);
|
$page = (int)$request->input('page', 1);
|
||||||
$perPage = (int)$request->input('per_page', 15);
|
$perPage = (int)$request->input('per_page', 15);
|
||||||
|
|
||||||
$query = Payment::where('user_id', $employerId)
|
$query = DB::table('subscriptions')
|
||||||
->where(function ($q) {
|
->where('user_id', $employerId)
|
||||||
$q->where('description', 'like', '%Subscription%')
|
->orderBy('created_at', 'desc');
|
||||||
->orWhere('description', 'like', '%Pass%');
|
|
||||||
})
|
|
||||||
->latest();
|
|
||||||
$total = $query->count();
|
$total = $query->count();
|
||||||
$offset = ($page - 1) * $perPage;
|
$offset = ($page - 1) * $perPage;
|
||||||
|
|
||||||
$payments = $query->skip($offset)->take($perPage)->get()
|
$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 [
|
return [
|
||||||
'id' => $payment->id,
|
'id' => $sub->id,
|
||||||
'amount' => (float)$payment->amount,
|
'amount' => (float)$sub->amount_aed,
|
||||||
'currency' => $payment->currency,
|
'currency' => 'AED',
|
||||||
'description' => $payment->description,
|
'description' => $planLabel,
|
||||||
'status' => $payment->status,
|
'status' => $sub->status === 'active' ? 'success' : $sub->status,
|
||||||
'date' => $payment->created_at->format('Y-m-d H:i:s'),
|
'date' => date('Y-m-d H:i:s', strtotime($startsAt)),
|
||||||
'formatted_date' => $payment->created_at->format('M d, Y'),
|
'formatted_date' => date('M d, Y', strtotime($startsAt)),
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,6 @@
|
|||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Models\EmployerProfile;
|
use App\Models\EmployerProfile;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
use Illuminate\Validation\Rules\Password;
|
|
||||||
|
|
||||||
class ProfileController extends Controller
|
class ProfileController extends Controller
|
||||||
{
|
{
|
||||||
@ -36,6 +35,32 @@ private function resolveCurrentUser()
|
|||||||
return null;
|
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)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
$user = $this->resolveCurrentUser();
|
$user = $this->resolveCurrentUser();
|
||||||
@ -48,35 +73,59 @@ public function index(Request $request)
|
|||||||
if (!$profile) {
|
if (!$profile) {
|
||||||
$profile = EmployerProfile::create([
|
$profile = EmployerProfile::create([
|
||||||
'user_id' => $user->id,
|
'user_id' => $user->id,
|
||||||
'company_name' => 'Al Mansoor Household',
|
'phone' => '+971509990001',
|
||||||
'phone' => '+971 50 123 4567',
|
'emirates_id_number' => '784-1988-5310327-2',
|
||||||
'emirates_id_status' => 'approved',
|
'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 = [
|
$employerProfile = $this->buildProfileData($user, $profile);
|
||||||
'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,
|
|
||||||
];
|
|
||||||
|
|
||||||
return Inertia::render('Employer/Profile', [
|
return Inertia::render('Employer/Profile', [
|
||||||
'employerProfile' => $employerProfile,
|
'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)
|
public function update(Request $request)
|
||||||
{
|
{
|
||||||
$user = $this->resolveCurrentUser();
|
$user = $this->resolveCurrentUser();
|
||||||
@ -97,17 +146,20 @@ public function update(Request $request)
|
|||||||
$sponsor ? 'unique:sponsors,email,' . $sponsor->id : 'unique:sponsors,email',
|
$sponsor ? 'unique:sponsors,email,' . $sponsor->id : 'unique:sponsors,email',
|
||||||
],
|
],
|
||||||
'phone' => 'required|string|max:255',
|
'phone' => 'required|string|max:255',
|
||||||
'company_name' => 'required|string|max:255',
|
'company_name' => 'nullable|string|max:255',
|
||||||
'language' => 'required|string|in:English,Arabic,english,arabic',
|
'language' => 'nullable|string|in:English,Arabic,english,arabic',
|
||||||
'notifications' => 'required|boolean',
|
'notifications' => 'nullable|boolean',
|
||||||
'nationality' => 'nullable|string|max:255',
|
'nationality' => 'nullable|string|max:255',
|
||||||
'family_size' => 'nullable|string|max:255',
|
'family_size' => 'nullable|string|max:255',
|
||||||
'accommodation' => 'nullable|string|max:255',
|
'accommodation' => 'nullable|string|max:255',
|
||||||
'district' => 'nullable|string|max:255',
|
'district' => 'nullable|string|max:255',
|
||||||
'emirates_id_front' => 'nullable|file|image|max:10240',
|
'emirates_id_front' => 'nullable|file|image|max:10240',
|
||||||
'emirates_id_back' => 'nullable|file|image|max:10240',
|
'emirates_id_back' => 'nullable|file|image|max:10240',
|
||||||
'current_password' => 'nullable|required_with:new_password|string',
|
'address' => 'nullable|string|max:255',
|
||||||
'new_password' => 'nullable|string|min:8|confirmed',
|
'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;
|
$oldEmail = $user->getOriginal('email') ?? $user->email;
|
||||||
@ -133,17 +185,43 @@ public function update(Request $request)
|
|||||||
if (!$profile) {
|
if (!$profile) {
|
||||||
$profile = new EmployerProfile(['user_id' => $user->id]);
|
$profile = new EmployerProfile(['user_id' => $user->id]);
|
||||||
}
|
}
|
||||||
$profile->company_name = $request->company_name;
|
|
||||||
$profile->phone = $request->phone;
|
$profile->phone = $request->phone;
|
||||||
$profile->language = ucfirst(strtolower($request->language));
|
|
||||||
$profile->notifications = $request->notifications;
|
|
||||||
|
|
||||||
$profile->nationality = $request->nationality;
|
if ($request->has('company_name')) {
|
||||||
$profile->family_size = $request->family_size;
|
$profile->company_name = $request->company_name;
|
||||||
$profile->accommodation = $request->accommodation;
|
}
|
||||||
$profile->district = $request->district;
|
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;
|
$uploaded = false;
|
||||||
$extractedIdNumber = null;
|
$extractedIdNumber = null;
|
||||||
$extractedExpiry = null;
|
$extractedExpiry = null;
|
||||||
@ -175,18 +253,15 @@ public function update(Request $request)
|
|||||||
$profile->verification_status = 'approved';
|
$profile->verification_status = 'approved';
|
||||||
}
|
}
|
||||||
|
|
||||||
$profile->save();
|
// Handle profile photo upload
|
||||||
|
if ($request->hasFile('profile_photo')) {
|
||||||
// Update Password if provided
|
$file = $request->file('profile_photo');
|
||||||
if ($request->filled('new_password')) {
|
$path = $file->store('profile_photos', 'public');
|
||||||
if (!Hash::check($request->current_password, $user->password)) {
|
$profile->profile_photo_url = asset('storage/' . $path);
|
||||||
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
|
// Update session user name/email
|
||||||
session(['user' => (object)[
|
session(['user' => (object)[
|
||||||
'id' => $user->id,
|
'id' => $user->id,
|
||||||
@ -194,9 +269,9 @@ public function update(Request $request)
|
|||||||
'email' => $user->email,
|
'email' => $user->email,
|
||||||
'role' => 'employer',
|
'role' => 'employer',
|
||||||
'subscription_status' => $user->subscription_status ?? 'active',
|
'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', [
|
return Inertia::render('Employer/Support/Index', [
|
||||||
'tickets' => $tickets,
|
'tickets' => $tickets,
|
||||||
|
'faqs' => $faqs,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -29,6 +29,10 @@ class EmployerProfile extends Model
|
|||||||
'emirates_id_employer',
|
'emirates_id_employer',
|
||||||
'emirates_id_issue_place',
|
'emirates_id_issue_place',
|
||||||
'emirates_id_occupation',
|
'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,
|
List,
|
||||||
LifeBuoy,
|
LifeBuoy,
|
||||||
Heart,
|
Heart,
|
||||||
Building
|
Building,
|
||||||
|
HelpCircle
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
|
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: 'OCR Verification', href: '/admin/workers/verifications', icon: ShieldCheck },
|
||||||
{ name: 'Safety & Moderation', href: '/admin/safety', icon: ShieldAlert },
|
{ name: 'Safety & Moderation', href: '/admin/safety', icon: ShieldAlert },
|
||||||
{ name: 'Support Tickets', href: '/admin/tickets', icon: LifeBuoy },
|
{ 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: 'Payments', href: '/admin/payments', icon: BadgeDollarSign },
|
||||||
{ name: 'Campaigns & Alerts', href: '/admin/notifications', icon: BellRing },
|
|
||||||
{ name: 'Reports & Analytics', href: '/admin/analytics', icon: BarChart3 },
|
{ name: 'Reports & Analytics', href: '/admin/analytics', icon: BarChart3 },
|
||||||
{ name: 'System Audit Logs', href: '/admin/audit-logs', icon: History },
|
{ name: 'System Audit Logs', href: '/admin/audit-logs', icon: History },
|
||||||
{ name: 'Charity Events', href: '/admin/events', icon: Heart },
|
{ name: 'Charity Events', href: '/admin/events', icon: Heart },
|
||||||
|
|||||||
@ -252,11 +252,7 @@ export default function EmployerLayout({ children, title, fullPage = false }) {
|
|||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</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 */}
|
{/* Profile Section with Dropdown */}
|
||||||
<DropdownMenu>
|
<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>
|
||||||
</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 */}
|
{/* Filters Row */}
|
||||||
<div className="bg-white border border-slate-200 rounded-2xl shadow-sm overflow-hidden">
|
<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">
|
<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
|
MessageSquare
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
export default function Index({ tickets }) {
|
export default function Index({ tickets, faqs = [] }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [statusFilter, setStatusFilter] = useState('all');
|
const [statusFilter, setStatusFilter] = useState('all');
|
||||||
@ -204,23 +204,25 @@ export default function Index({ tickets }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* FAQ section */}
|
{/* FAQ section */}
|
||||||
<div className="bg-slate-50 p-6 rounded-3xl border border-slate-200/80 space-y-4">
|
{faqs && faqs.length > 0 && (
|
||||||
<h3 className="font-black text-slate-800 text-sm flex items-center space-x-2">
|
<div className="bg-slate-50 p-6 rounded-3xl border border-slate-200/80 space-y-4">
|
||||||
<MessageSquare className="w-4 h-4 text-[#185FA5]" />
|
<h3 className="font-black text-slate-800 text-sm flex items-center space-x-2">
|
||||||
<span>{t('frequently_asked_questions', 'Frequently Asked Questions')}</span>
|
<MessageSquare className="w-4 h-4 text-[#185FA5]" />
|
||||||
</h3>
|
<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">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-xs">
|
||||||
<h4 className="font-black text-slate-800">{t('faq_q1', 'How long does a ticket resolution take?')}</h4>
|
{faqs.map((faq) => (
|
||||||
<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 key={faq.id} className="bg-white p-4 rounded-2xl border border-slate-200/50 space-y-1">
|
||||||
</div>
|
<h4 className="font-black text-slate-800">
|
||||||
<div className="bg-white p-4 rounded-2xl border border-slate-200/50 space-y-1">
|
{faq.question.trim().endsWith('?') ? faq.question.trim() : faq.question.trim() + '?'}
|
||||||
<h4 className="font-black text-slate-800">{t('faq_q2', 'What is Tadbeer contract compliance?')}</h4>
|
</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>
|
<p className="text-slate-500 font-semibold">{faq.answer}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</EmployerLayout>
|
</EmployerLayout>
|
||||||
|
|||||||
@ -80,28 +80,32 @@
|
|||||||
})->name('admin.subscriptions');
|
})->name('admin.subscriptions');
|
||||||
|
|
||||||
Route::get('/payments', function () {
|
Route::get('/payments', function () {
|
||||||
$payments = \Illuminate\Support\Facades\DB::table('payments')
|
$payments = \Illuminate\Support\Facades\DB::table('subscriptions')
|
||||||
->leftJoin('users', 'payments.user_id', '=', 'users.id')
|
->leftJoin('users', 'subscriptions.user_id', '=', 'users.id')
|
||||||
->select('payments.*', 'users.name as user_name', 'users.email as user_email')
|
->select('subscriptions.*', 'users.name as user_name', 'users.email as user_email')
|
||||||
->orderBy('payments.created_at', 'desc')
|
->orderBy('subscriptions.created_at', 'desc')
|
||||||
->get()
|
->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 [
|
return [
|
||||||
'id' => 'PAY-' . str_pad($payment->id, 3, '0', STR_PAD_LEFT),
|
'id' => 'PAY-' . str_pad($sub->id, 3, '0', STR_PAD_LEFT),
|
||||||
'employer' => $payment->user_name ?: 'System Guest / Sponsor',
|
'employer' => $sub->user_name ?: 'System Guest / Sponsor',
|
||||||
'plan' => $payment->description ?: 'Subscription Plan',
|
'plan' => $planLabel,
|
||||||
'amount' => (float)$payment->amount,
|
'amount' => (float)$sub->amount_aed,
|
||||||
'method' => 'Stripe Gateway',
|
'method' => 'PayTabs Gateway',
|
||||||
'date' => date('Y-m-d', strtotime($payment->created_at)),
|
'date' => date('Y-m-d', strtotime($startsAt)),
|
||||||
'status' => $payment->status === 'success' ? 'Completed' : (ucfirst($payment->status) ?: 'Completed'),
|
'status' => $status,
|
||||||
'period' => date('M Y', strtotime($payment->created_at)) . ' - ' . date('M Y', strtotime($payment->created_at . ' +1 month')),
|
'period' => date('M Y', strtotime($startsAt)) . ' - ' . date('M Y', strtotime($expiresAt)),
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
$totalRevenue = \Illuminate\Support\Facades\DB::table('payments')->where('status', 'success')->sum('amount');
|
$totalRevenue = \Illuminate\Support\Facades\DB::table('subscriptions')->where('status', 'active')->sum('amount_aed');
|
||||||
$activeSubs = \Illuminate\Support\Facades\DB::table('sponsors')->where('subscription_status', 'active')->count();
|
$activeSubs = \Illuminate\Support\Facades\DB::table('subscriptions')->where('status', 'active')->count();
|
||||||
$failedPayments = \Illuminate\Support\Facades\DB::table('payments')->where('status', 'failed')->count();
|
$failedPayments = \Illuminate\Support\Facades\DB::table('subscriptions')->where('status', 'failed')->count();
|
||||||
$avgTicket = \Illuminate\Support\Facades\DB::table('payments')->where('status', 'success')->avg('amount') ?: 0;
|
$avgTicket = \Illuminate\Support\Facades\DB::table('subscriptions')->where('status', 'active')->avg('amount_aed') ?: 0;
|
||||||
|
|
||||||
$stats = [
|
$stats = [
|
||||||
'total_revenue' => number_format($totalRevenue, 2),
|
'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}/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::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('/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');
|
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
|
// Admin Support Tickets
|
||||||
Route::get('/tickets', [\App\Http\Controllers\Admin\SupportTicketController::class, 'index'])->name('admin.tickets.index');
|
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');
|
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::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', [\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::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');
|
Route::get('/payments', [\App\Http\Controllers\Employer\PaymentController::class, 'index'])->name('employer.payments');
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user