document upload issue api
This commit is contained in:
parent
69305c09e3
commit
3cc74f797e
@ -62,4 +62,6 @@ AWS_DEFAULT_REGION=us-east-1
|
|||||||
AWS_BUCKET=
|
AWS_BUCKET=
|
||||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||||
|
|
||||||
|
OCR_API_URL=http://192.168.0.220:5000/passport
|
||||||
|
|
||||||
VITE_APP_NAME="${APP_NAME}"
|
VITE_APP_NAME="${APP_NAME}"
|
||||||
|
|||||||
261
app/Http/Controllers/Admin/EmployerController.php
Normal file
261
app/Http/Controllers/Admin/EmployerController.php
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\EmployerProfile;
|
||||||
|
use App\Models\Sponsor;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class EmployerController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* List all employers (role = 'employer') with filters, search, and sorting.
|
||||||
|
*/
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$query = User::where('role', 'employer')->with(['employerProfile', 'sponsor']);
|
||||||
|
|
||||||
|
// Search
|
||||||
|
if ($request->filled('search')) {
|
||||||
|
$search = $request->input('search');
|
||||||
|
$query->where(function($q) use ($search) {
|
||||||
|
$q->where('name', 'like', "%{$search}%")
|
||||||
|
->orWhere('email', 'like', "%{$search}%")
|
||||||
|
->orWhereHas('employerProfile', function($qp) use ($search) {
|
||||||
|
$qp->where('company_name', 'like', "%{$search}%")
|
||||||
|
->orWhere('phone', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status filter (on sponsor status)
|
||||||
|
if ($request->filled('status') && $request->input('status') !== 'All') {
|
||||||
|
$status = strtolower($request->input('status'));
|
||||||
|
$query->whereHas('sponsor', function($qs) use ($status) {
|
||||||
|
$qs->where('status', $status);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Location filter
|
||||||
|
if ($request->filled('location') && $request->input('location') !== 'All') {
|
||||||
|
$location = $request->input('location');
|
||||||
|
$query->whereHas('employerProfile', function($qp) use ($location) {
|
||||||
|
$qp->where('country', $location);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sorting
|
||||||
|
$sortField = $request->input('sort_field', 'created_at');
|
||||||
|
$sortOrder = $request->input('sort_order', 'desc');
|
||||||
|
if (in_array($sortField, ['name', 'email', 'created_at'])) {
|
||||||
|
$query->orderBy($sortField, $sortOrder);
|
||||||
|
} else {
|
||||||
|
$query->orderBy('created_at', 'desc');
|
||||||
|
}
|
||||||
|
|
||||||
|
$employers = $query->paginate(10)->withQueryString();
|
||||||
|
|
||||||
|
// Transform results to format status correctly from sponsor
|
||||||
|
$employers->getCollection()->transform(function($emp) {
|
||||||
|
$emp->status = $emp->sponsor ? $emp->sponsor->status : 'active';
|
||||||
|
return $emp;
|
||||||
|
});
|
||||||
|
|
||||||
|
return Inertia::render('Admin/Employers/Index', [
|
||||||
|
'employers' => $employers,
|
||||||
|
'filters' => $request->only(['search', 'status', 'location', 'sort_field', 'sort_order'])
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export employers as CSV.
|
||||||
|
*/
|
||||||
|
public function export(Request $request)
|
||||||
|
{
|
||||||
|
$query = User::where('role', 'employer')->with(['employerProfile', 'sponsor']);
|
||||||
|
|
||||||
|
// Search
|
||||||
|
if ($request->filled('search')) {
|
||||||
|
$search = $request->input('search');
|
||||||
|
$query->where(function($q) use ($search) {
|
||||||
|
$q->where('name', 'like', "%{$search}%")
|
||||||
|
->orWhere('email', 'like', "%{$search}%")
|
||||||
|
->orWhereHas('employerProfile', function($qp) use ($search) {
|
||||||
|
$qp->where('company_name', 'like', "%{$search}%")
|
||||||
|
->orWhere('phone', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status filter
|
||||||
|
if ($request->filled('status') && $request->input('status') !== 'All') {
|
||||||
|
$status = strtolower($request->input('status'));
|
||||||
|
$query->whereHas('sponsor', function($qs) use ($status) {
|
||||||
|
$qs->where('status', $status);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$employers = $query->get();
|
||||||
|
|
||||||
|
$headers = [
|
||||||
|
"Content-type" => "text/csv",
|
||||||
|
"Content-Disposition" => "attachment; filename=employers_export_" . date('Ymd_His') . ".csv",
|
||||||
|
"Pragma" => "no-cache",
|
||||||
|
"Cache-Control" => "must-revalidate, post-check=0, pre-check=0",
|
||||||
|
"Expires" => "0"
|
||||||
|
];
|
||||||
|
|
||||||
|
$columns = ['ID', 'Name', 'Email', 'Company Name', 'Phone', 'Country', 'Status', 'Subscription Status', 'Created At'];
|
||||||
|
|
||||||
|
$callback = function() use ($employers, $columns) {
|
||||||
|
$file = fopen('php://output', 'w');
|
||||||
|
fputcsv($file, $columns);
|
||||||
|
|
||||||
|
foreach ($employers as $emp) {
|
||||||
|
fputcsv($file, [
|
||||||
|
$emp->id,
|
||||||
|
$emp->name,
|
||||||
|
$emp->email,
|
||||||
|
$emp->employerProfile->company_name ?? 'N/A',
|
||||||
|
$emp->employerProfile->phone ?? 'N/A',
|
||||||
|
$emp->employerProfile->country ?? 'N/A',
|
||||||
|
$emp->sponsor ? $emp->sponsor->status : 'active',
|
||||||
|
$emp->subscription_status ?? 'none',
|
||||||
|
$emp->created_at ? $emp->created_at->toDateTimeString() : 'N/A',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fclose($file);
|
||||||
|
};
|
||||||
|
|
||||||
|
return response()->stream($callback, 200, $headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify / approve employer profile.
|
||||||
|
*/
|
||||||
|
public function verify($id)
|
||||||
|
{
|
||||||
|
$user = User::where('role', 'employer')->findOrFail($id);
|
||||||
|
if ($user->employerProfile) {
|
||||||
|
$user->employerProfile->update([
|
||||||
|
'verification_status' => 'approved'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$sponsor = Sponsor::where('email', $user->email)->first();
|
||||||
|
if ($sponsor) {
|
||||||
|
$sponsor->update([
|
||||||
|
'is_verified' => true,
|
||||||
|
'status' => 'active'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return back()->with('success', "Employer '{$user->name}' profile verified and activated successfully.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Suspend an employer.
|
||||||
|
*/
|
||||||
|
public function suspend($id)
|
||||||
|
{
|
||||||
|
$user = User::where('role', 'employer')->findOrFail($id);
|
||||||
|
|
||||||
|
$sponsor = Sponsor::where('email', $user->email)->first();
|
||||||
|
if ($sponsor) {
|
||||||
|
$sponsor->update(['status' => 'suspended']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return back()->with('success', "Employer '{$user->name}' suspended successfully.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activate a suspended/inactive employer.
|
||||||
|
*/
|
||||||
|
public function activate($id)
|
||||||
|
{
|
||||||
|
$user = User::where('role', 'employer')->findOrFail($id);
|
||||||
|
|
||||||
|
$sponsor = Sponsor::where('email', $user->email)->first();
|
||||||
|
if ($sponsor) {
|
||||||
|
$sponsor->update(['status' => 'active']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return back()->with('success', "Employer '{$user->name}' activated successfully.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an employer.
|
||||||
|
*/
|
||||||
|
public function delete($id)
|
||||||
|
{
|
||||||
|
$user = User::where('role', 'employer')->findOrFail($id);
|
||||||
|
$name = $user->name;
|
||||||
|
|
||||||
|
$sponsor = Sponsor::where('email', $user->email)->first();
|
||||||
|
if ($sponsor) {
|
||||||
|
$sponsor->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user->employerProfile) {
|
||||||
|
$user->employerProfile->delete();
|
||||||
|
}
|
||||||
|
$user->delete();
|
||||||
|
|
||||||
|
return back()->with('success', "Employer '{$name}' deleted successfully.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an employer's fields.
|
||||||
|
*/
|
||||||
|
public function update(Request $request, $id)
|
||||||
|
{
|
||||||
|
$user = User::where('role', 'employer')->findOrFail($id);
|
||||||
|
$validated = $request->validate([
|
||||||
|
'name' => 'required|string|max:255',
|
||||||
|
'email' => 'required|email|unique:users,email,' . $user->id,
|
||||||
|
'company_name' => 'nullable|string|max:255',
|
||||||
|
'phone' => 'nullable|string',
|
||||||
|
'country' => 'nullable|string',
|
||||||
|
'subscription_status' => 'nullable|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user->update([
|
||||||
|
'name' => $validated['name'],
|
||||||
|
'email' => $validated['email'],
|
||||||
|
'subscription_status' => $validated['subscription_status'] ?? $user->subscription_status,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($user->employerProfile) {
|
||||||
|
$user->employerProfile->update([
|
||||||
|
'company_name' => $validated['company_name'] ?? $user->employerProfile->company_name,
|
||||||
|
'phone' => $validated['phone'] ?? $user->employerProfile->phone,
|
||||||
|
'country' => $validated['country'] ?? $user->employerProfile->country,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
EmployerProfile::create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'company_name' => $validated['company_name'],
|
||||||
|
'phone' => $validated['phone'],
|
||||||
|
'country' => $validated['country'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$sponsor = Sponsor::where('email', $user->email)->first();
|
||||||
|
if ($sponsor) {
|
||||||
|
$sponsor->update([
|
||||||
|
'full_name' => $validated['name'],
|
||||||
|
'email' => $validated['email'],
|
||||||
|
'mobile' => $validated['phone'] ?? $sponsor->mobile,
|
||||||
|
'organization_name' => $validated['company_name'] ?? $sponsor->organization_name,
|
||||||
|
'city' => $validated['country'] ?? $sponsor->city,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return back()->with('success', "Employer details saved successfully.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -22,6 +22,7 @@ public function index(Request $request)
|
|||||||
$search = $request->input('search');
|
$search = $request->input('search');
|
||||||
$query->where(function($q) use ($search) {
|
$query->where(function($q) use ($search) {
|
||||||
$q->where('full_name', 'like', "%{$search}%")
|
$q->where('full_name', 'like', "%{$search}%")
|
||||||
|
->orWhere('organization_name', 'like', "%{$search}%")
|
||||||
->orWhere('email', 'like', "%{$search}%")
|
->orWhere('email', 'like', "%{$search}%")
|
||||||
->orWhere('mobile', 'like', "%{$search}%");
|
->orWhere('mobile', 'like', "%{$search}%");
|
||||||
});
|
});
|
||||||
@ -32,15 +33,35 @@ public function index(Request $request)
|
|||||||
$query->where('status', strtolower($request->input('status')));
|
$query->where('status', strtolower($request->input('status')));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server-side Location/City filter
|
|
||||||
if ($request->filled('location') && $request->input('location') !== 'All') {
|
|
||||||
$query->where('city', $request->input('location'));
|
// Verification filter
|
||||||
|
if ($request->filled('verification') && $request->input('verification') !== 'All') {
|
||||||
|
$isVerified = $request->input('verification') === 'Verified';
|
||||||
|
$query->where('is_verified', $isVerified);
|
||||||
|
}
|
||||||
|
|
||||||
|
// License filter
|
||||||
|
if ($request->filled('license') && $request->input('license') !== 'All') {
|
||||||
|
$license = $request->input('license');
|
||||||
|
if ($license === 'Expired') {
|
||||||
|
$query->where('license_expiry', '<', now());
|
||||||
|
} elseif ($license === 'Valid') {
|
||||||
|
$query->where('license_expiry', '>=', now());
|
||||||
|
} elseif ($license === 'Expiring Soon') {
|
||||||
|
$query->whereBetween('license_expiry', [now(), now()->addDays(30)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nationality filter
|
||||||
|
if ($request->filled('nationality') && $request->input('nationality') !== 'All') {
|
||||||
|
$query->where('nationality', $request->input('nationality'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server-side Sorting
|
// Server-side Sorting
|
||||||
$sortField = $request->input('sort_field', 'created_at');
|
$sortField = $request->input('sort_field', 'created_at');
|
||||||
$sortOrder = $request->input('sort_order', 'desc');
|
$sortOrder = $request->input('sort_order', 'desc');
|
||||||
if (in_array($sortField, ['full_name', 'email', 'city', 'status', 'created_at'])) {
|
if (in_array($sortField, ['full_name', 'organization_name', 'email', 'city', 'status', 'created_at'])) {
|
||||||
$query->orderBy($sortField, $sortOrder);
|
$query->orderBy($sortField, $sortOrder);
|
||||||
} else {
|
} else {
|
||||||
$query->orderBy('created_at', 'desc');
|
$query->orderBy('created_at', 'desc');
|
||||||
@ -49,9 +70,41 @@ public function index(Request $request)
|
|||||||
// Paginate
|
// Paginate
|
||||||
$sponsors = $query->paginate(10)->withQueryString();
|
$sponsors = $query->paginate(10)->withQueryString();
|
||||||
|
|
||||||
return Inertia::render('Admin/Employers/Index', [
|
// Get distinct nationalities
|
||||||
|
$nationalities = [
|
||||||
|
'Afghan', 'Albanian', 'Algerian', 'American', 'Andorran', 'Angolan', 'Argentinian', 'Armenian', 'Australian', 'Austrian',
|
||||||
|
'Azerbaijani', 'Bahamian', 'Bahraini', 'Bangladeshi', 'Barbadian', 'Belarusian', 'Belgian', 'Belizean', 'Beninese', 'Bhutanese',
|
||||||
|
'Bolivian', 'Bosnian', 'Brazilian', 'British', 'Bruneian', 'Bulgarian', 'Burkinabe', 'Burmese', 'Burundian', 'Cambodian',
|
||||||
|
'Cameroonian', 'Canadian', 'Cape Verdean', 'Central African', 'Chadian', 'Chilean', 'Chinese', 'Colombian', 'Comoran', 'Congolese',
|
||||||
|
'Costa Rican', 'Croatian', 'Cuban', 'Cypriot', 'Czech', 'Danish', 'Djiboutian', 'Dominican', 'Dutch', 'East Timorese',
|
||||||
|
'Ecuadorian', 'Egyptian', 'Emirati', 'Equatorial Guinean', 'Eritrean', 'Estonian', 'Ethiopian', 'Fijian', 'Filipino', 'Finnish',
|
||||||
|
'French', 'Gabonese', 'Gambian', 'Georgian', 'German', 'Ghanaian', 'Greek', 'Grenadian', 'Guatemalan', 'Guinean',
|
||||||
|
'Guyanese', 'Haitian', 'Honduran', 'Hungarian', 'Icelandic', 'Indian', 'Indonesian', 'Iranian', 'Iraqi', 'Irish',
|
||||||
|
'Italian', 'Ivorian', 'Jamaican', 'Japanese', 'Jordanian', 'Kazakh', 'Kenyan', 'Kuwaiti', 'Kyrgyz', 'Laotian',
|
||||||
|
'Latvian', 'Lebanese', 'Liberian', 'Libyan', 'Liechtensteiner', 'Lithuanian', 'Luxembourger', 'Macedonian', 'Malagasy', 'Malawian',
|
||||||
|
'Malaysian', 'Maldivian', 'Malian', 'Maltese', 'Mauritanian', 'Mauritian', 'Mexican', 'Micronesian', 'Moldovan', 'Monacan',
|
||||||
|
'Mongolian', 'Montenegrin', 'Moroccan', 'Mozambican', 'Namibian', 'Nauruan', 'Nepalese', 'New Zealander', 'Nicaraguan', 'Nigerian',
|
||||||
|
'Nigerien', 'North Korean', 'Norwegian', 'Omani', 'Pakistani', 'Palauans', 'Palestinian', 'Panamanian', 'Papua New Guinean', 'Paraguayan',
|
||||||
|
'Peruvian', 'Polish', 'Portuguese', 'Qatari', 'Romanian', 'Russian', 'Rwandan', 'Saint Lucian', 'Salvadoran', 'Samoan',
|
||||||
|
'San Marinese', 'Sao Tomean', 'Saudi Arabian', 'Senegalese', 'Serbian', 'Seychellois', 'Sierra Leonean', 'Singaporean', 'Slovak', 'Slovenian',
|
||||||
|
'Solomon Islander', 'Somali', 'South African', 'South Korean', 'Spanish', 'Sri Lankan', 'Sudanese', 'Surinamese', 'Swazi', 'Swedish',
|
||||||
|
'Swiss', 'Syrian', 'Taiwanese', 'Tajik', 'Tanzanian', 'Thai', 'Togolese', 'Tongan', 'Trinidadian', 'Tunisian',
|
||||||
|
'Turkish', 'Turkmen', 'Tuvaluan', 'Ugandan', 'Ukrainian', 'Uruguayan', 'Uzbek', 'Vanuatuan', 'Venezuelan', 'Vietnamese',
|
||||||
|
'Western Samoan', 'Yemeni', 'Zambian', 'Zimbabwean'
|
||||||
|
];
|
||||||
|
|
||||||
|
// 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(),
|
||||||
|
];
|
||||||
|
|
||||||
|
return Inertia::render('Admin/CharityOrganizations/Index', [
|
||||||
'sponsors' => $sponsors,
|
'sponsors' => $sponsors,
|
||||||
'filters' => $request->only(['search', 'status', 'location', 'sort_field', 'sort_order'])
|
'filters' => $request->only(['search', 'status', 'verification', 'license', 'nationality', 'sort_field', 'sort_order']),
|
||||||
|
'nationalities' => $nationalities,
|
||||||
|
'stats' => $stats
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,6 +120,7 @@ public function export(Request $request)
|
|||||||
$search = $request->input('search');
|
$search = $request->input('search');
|
||||||
$query->where(function($q) use ($search) {
|
$query->where(function($q) use ($search) {
|
||||||
$q->where('full_name', 'like', "%{$search}%")
|
$q->where('full_name', 'like', "%{$search}%")
|
||||||
|
->orWhere('organization_name', 'like', "%{$search}%")
|
||||||
->orWhere('email', 'like', "%{$search}%")
|
->orWhere('email', 'like', "%{$search}%")
|
||||||
->orWhere('mobile', 'like', "%{$search}%");
|
->orWhere('mobile', 'like', "%{$search}%");
|
||||||
});
|
});
|
||||||
@ -77,15 +131,35 @@ public function export(Request $request)
|
|||||||
$query->where('status', strtolower($request->input('status')));
|
$query->where('status', strtolower($request->input('status')));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server-side Location/City filter
|
|
||||||
if ($request->filled('location') && $request->input('location') !== 'All') {
|
|
||||||
$query->where('city', $request->input('location'));
|
// Verification filter
|
||||||
|
if ($request->filled('verification') && $request->input('verification') !== 'All') {
|
||||||
|
$isVerified = $request->input('verification') === 'Verified';
|
||||||
|
$query->where('is_verified', $isVerified);
|
||||||
|
}
|
||||||
|
|
||||||
|
// License filter
|
||||||
|
if ($request->filled('license') && $request->input('license') !== 'All') {
|
||||||
|
$license = $request->input('license');
|
||||||
|
if ($license === 'Expired') {
|
||||||
|
$query->where('license_expiry', '<', now());
|
||||||
|
} elseif ($license === 'Valid') {
|
||||||
|
$query->where('license_expiry', '>=', now());
|
||||||
|
} elseif ($license === 'Expiring Soon') {
|
||||||
|
$query->whereBetween('license_expiry', [now(), now()->addDays(30)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nationality filter
|
||||||
|
if ($request->filled('nationality') && $request->input('nationality') !== 'All') {
|
||||||
|
$query->where('nationality', $request->input('nationality'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server-side Sorting
|
// Server-side Sorting
|
||||||
$sortField = $request->input('sort_field', 'created_at');
|
$sortField = $request->input('sort_field', 'created_at');
|
||||||
$sortOrder = $request->input('sort_order', 'desc');
|
$sortOrder = $request->input('sort_order', 'desc');
|
||||||
if (in_array($sortField, ['full_name', 'email', 'city', 'status', 'created_at'])) {
|
if (in_array($sortField, ['full_name', 'organization_name', 'email', 'city', 'status', 'created_at'])) {
|
||||||
$query->orderBy($sortField, $sortOrder);
|
$query->orderBy($sortField, $sortOrder);
|
||||||
} else {
|
} else {
|
||||||
$query->orderBy('created_at', 'desc');
|
$query->orderBy('created_at', 'desc');
|
||||||
@ -95,13 +169,13 @@ public function export(Request $request)
|
|||||||
|
|
||||||
$headers = [
|
$headers = [
|
||||||
"Content-type" => "text/csv",
|
"Content-type" => "text/csv",
|
||||||
"Content-Disposition" => "attachment; filename=sponsors_export_" . date('Ymd_His') . ".csv",
|
"Content-Disposition" => "attachment; filename=charity_organizations_export_" . date('Ymd_His') . ".csv",
|
||||||
"Pragma" => "no-cache",
|
"Pragma" => "no-cache",
|
||||||
"Cache-Control" => "must-revalidate, post-check=0, pre-check=0",
|
"Cache-Control" => "must-revalidate, post-check=0, pre-check=0",
|
||||||
"Expires" => "0"
|
"Expires" => "0"
|
||||||
];
|
];
|
||||||
|
|
||||||
$columns = ['ID', 'Full Name', 'Email', 'Mobile', 'City', 'Nationality', 'Address', 'Status', 'OTP Verified', 'Verified At', 'Subscription Plan', 'Subscription Status', 'Subscription Expiry', 'Created At'];
|
$columns = ['ID', 'Organization Name', 'Full Name', 'Email', 'Mobile', 'City', 'Nationality', 'Address', 'Status', 'Verified', 'License Expiry', 'Created At'];
|
||||||
|
|
||||||
$callback = function() use ($sponsors, $columns) {
|
$callback = function() use ($sponsors, $columns) {
|
||||||
$file = fopen('php://output', 'w');
|
$file = fopen('php://output', 'w');
|
||||||
@ -110,6 +184,7 @@ public function export(Request $request)
|
|||||||
foreach ($sponsors as $sponsor) {
|
foreach ($sponsors as $sponsor) {
|
||||||
fputcsv($file, [
|
fputcsv($file, [
|
||||||
$sponsor->id,
|
$sponsor->id,
|
||||||
|
$sponsor->organization_name ?? 'N/A',
|
||||||
$sponsor->full_name,
|
$sponsor->full_name,
|
||||||
$sponsor->email,
|
$sponsor->email,
|
||||||
$sponsor->mobile,
|
$sponsor->mobile,
|
||||||
@ -118,10 +193,7 @@ public function export(Request $request)
|
|||||||
$sponsor->address ?? 'N/A',
|
$sponsor->address ?? 'N/A',
|
||||||
$sponsor->status,
|
$sponsor->status,
|
||||||
$sponsor->is_verified ? 'Yes' : 'No',
|
$sponsor->is_verified ? 'Yes' : 'No',
|
||||||
$sponsor->otp_verified_at ? $sponsor->otp_verified_at->toDateTimeString() : 'N/A',
|
$sponsor->license_expiry ? $sponsor->license_expiry->toDateString() : 'N/A',
|
||||||
$sponsor->subscription_plan ?? 'None',
|
|
||||||
$sponsor->subscription_status ?? 'None',
|
|
||||||
$sponsor->subscription_end_date ? $sponsor->subscription_end_date->toDateString() : 'N/A',
|
|
||||||
$sponsor->created_at ? $sponsor->created_at->toDateTimeString() : 'N/A',
|
$sponsor->created_at ? $sponsor->created_at->toDateTimeString() : 'N/A',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@ -193,6 +265,7 @@ public function update(Request $request, $id)
|
|||||||
$sponsor = Sponsor::findOrFail($id);
|
$sponsor = Sponsor::findOrFail($id);
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'full_name' => 'required|string|max:255',
|
'full_name' => 'required|string|max:255',
|
||||||
|
'organization_name' => 'nullable|string|max:255',
|
||||||
'email' => 'required|email|unique:sponsors,email,' . $sponsor->id,
|
'email' => 'required|email|unique:sponsors,email,' . $sponsor->id,
|
||||||
'mobile' => 'required|string',
|
'mobile' => 'required|string',
|
||||||
'city' => 'required|string',
|
'city' => 'required|string',
|
||||||
|
|||||||
@ -188,9 +188,12 @@ public function register(Request $request)
|
|||||||
{
|
{
|
||||||
$validator = Validator::make($request->all(), [
|
$validator = Validator::make($request->all(), [
|
||||||
'name' => 'required|string|max:255',
|
'name' => 'required|string|max:255',
|
||||||
'email' => 'required|string|email|max:255|unique:users,email',
|
'email' => 'required|string|email|max:255|unique:users,email|unique:sponsors,email',
|
||||||
'phone' => 'required|string|max:50',
|
'phone' => 'required|string|max:50|unique:employer_profiles,phone|unique:sponsors,mobile',
|
||||||
'fcm_token' => 'nullable|string|max:255',
|
'fcm_token' => 'nullable|string|max:255',
|
||||||
|
], [
|
||||||
|
'email.unique' => 'This email address is already registered.',
|
||||||
|
'phone.unique' => 'This mobile number is already registered.',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($validator->fails()) {
|
if ($validator->fails()) {
|
||||||
@ -409,23 +412,11 @@ public function uploadEmiratesId(Request $request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
$file = $request->file('emirates_id_file');
|
$file = $request->file('emirates_id_file');
|
||||||
$destinationPath = public_path('uploads/documents');
|
|
||||||
if (!file_exists($destinationPath)) {
|
|
||||||
mkdir($destinationPath, 0755, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
$fileName = time() . '_employer_eid_' . preg_replace('/[^a-zA-Z0-9_.-]/', '', $file->getClientOriginalName());
|
// Extract data using OcrDocumentService without storing the file on disk
|
||||||
$file->move($destinationPath, $fileName);
|
$ocrResult = \App\Services\OcrDocumentService::extractData($file);
|
||||||
$filePath = 'uploads/documents/' . $fileName;
|
$extractedIdNumber = $ocrResult['document_number'];
|
||||||
|
$extractedExpiry = $ocrResult['expiry_date'];
|
||||||
// OCR Simulated extraction
|
|
||||||
$extractedIdNumber = '784-' . rand(1970, 2005) . '-' . rand(1000000, 9999999) . '-' . rand(1, 9);
|
|
||||||
$extractedExpiry = now()->addYears(rand(2, 4))->toDateString();
|
|
||||||
|
|
||||||
// We are not storing this document, just extract data and delete this file
|
|
||||||
if (file_exists(public_path($filePath))) {
|
|
||||||
unlink(public_path($filePath));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update or create EmployerProfile
|
// Update or create EmployerProfile
|
||||||
$profile = EmployerProfile::where('user_id', $user->id)->first();
|
$profile = EmployerProfile::where('user_id', $user->id)->first();
|
||||||
|
|||||||
@ -87,7 +87,13 @@ public function updateProfile(Request $request)
|
|||||||
'unique:users,email,' . $employer->id,
|
'unique:users,email,' . $employer->id,
|
||||||
$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',
|
||||||
|
'unique:employer_profiles,phone,' . ($employer->employerProfile ? $employer->employerProfile->id : 'NULL'),
|
||||||
|
$sponsor ? 'unique:sponsors,mobile,' . $sponsor->id : 'unique:sponsors,mobile',
|
||||||
|
],
|
||||||
'company_name' => 'required|string|max:255',
|
'company_name' => 'required|string|max:255',
|
||||||
'language' => 'required|string|in:English,Arabic,english,arabic',
|
'language' => 'required|string|in:English,Arabic,english,arabic',
|
||||||
'notifications' => 'required|boolean',
|
'notifications' => 'required|boolean',
|
||||||
|
|||||||
@ -24,12 +24,12 @@ public function register(Request $request)
|
|||||||
{
|
{
|
||||||
$validator = Validator::make($request->all(), [
|
$validator = Validator::make($request->all(), [
|
||||||
'full_name' => 'required|string|max:255',
|
'full_name' => 'required|string|max:255',
|
||||||
'mobile' => 'required|string|max:50|unique:sponsors,mobile',
|
'mobile' => 'required|string|max:50|unique:sponsors,mobile|unique:employer_profiles,phone',
|
||||||
'password' => 'required|string|min:6',
|
'password' => 'required|string|min:6',
|
||||||
'license_file' => 'required|file|mimes:jpeg,png,jpg,pdf|max:10240',
|
'license_file' => 'required|file|mimes:jpeg,png,jpg,pdf|max:10240',
|
||||||
'emirates_id_file' => 'required|file|mimes:jpeg,png,jpg,pdf|max:10240',
|
'emirates_id_file' => 'required|file|mimes:jpeg,png,jpg,pdf|max:10240',
|
||||||
'organization_name' => 'required|string|max:255',
|
'organization_name' => 'required|string|max:255',
|
||||||
'email' => 'required|email|max:255|unique:sponsors,email',
|
'email' => 'required|email|max:255|unique:sponsors,email|unique:users,email',
|
||||||
'nationality' => 'required|string|max:100',
|
'nationality' => 'required|string|max:100',
|
||||||
'city' => 'required|string|max:100',
|
'city' => 'required|string|max:100',
|
||||||
'address' => 'required|string|max:255',
|
'address' => 'required|string|max:255',
|
||||||
@ -58,29 +58,19 @@ public function register(Request $request)
|
|||||||
$email = "sponsor.{$mobileClean}." . rand(10, 99) . "@migrant.ae";
|
$email = "sponsor.{$mobileClean}." . rand(10, 99) . "@migrant.ae";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store files
|
|
||||||
$destinationPath = public_path('uploads/licenses');
|
|
||||||
if (!file_exists($destinationPath)) {
|
|
||||||
mkdir($destinationPath, 0755, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
$licenseFile = $request->file('license_file');
|
$licenseFile = $request->file('license_file');
|
||||||
$licenseFileName = time() . '_license_' . preg_replace('/[^a-zA-Z0-9_.-]/', '', $licenseFile->getClientOriginalName());
|
$ocrLicense = \App\Services\OcrDocumentService::extractData($licenseFile);
|
||||||
$licenseFile->move($destinationPath, $licenseFileName);
|
$licenseExpiry = $request->license_expiry ?? $ocrLicense['expiry_date'];
|
||||||
$licensePath = 'uploads/licenses/' . $licenseFileName;
|
|
||||||
|
|
||||||
$emiratesIdPath = null;
|
$emiratesIdPath = null;
|
||||||
if ($request->hasFile('emirates_id_file')) {
|
if ($request->hasFile('emirates_id_file')) {
|
||||||
$emiratesIdFile = $request->file('emirates_id_file');
|
$emiratesIdFile = $request->file('emirates_id_file');
|
||||||
$emiratesIdFileName = time() . '_emirates_id_' . preg_replace('/[^a-zA-Z0-9_.-]/', '', $emiratesIdFile->getClientOriginalName());
|
\App\Services\OcrDocumentService::extractData($emiratesIdFile);
|
||||||
$emiratesIdFile->move($destinationPath, $emiratesIdFileName);
|
|
||||||
$emiratesIdPath = 'uploads/licenses/' . $emiratesIdFileName;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$licensePath = null;
|
||||||
$apiToken = Str::random(80);
|
$apiToken = Str::random(80);
|
||||||
|
|
||||||
$licenseExpiry = $request->license_expiry ?? now()->addYears(3)->toDateString();
|
|
||||||
|
|
||||||
$sponsor = DB::transaction(function () use ($request, $email, $licensePath, $emiratesIdPath, $apiToken, $licenseExpiry) {
|
$sponsor = DB::transaction(function () use ($request, $email, $licensePath, $emiratesIdPath, $apiToken, $licenseExpiry) {
|
||||||
return Sponsor::create([
|
return Sponsor::create([
|
||||||
'full_name' => $request->full_name,
|
'full_name' => $request->full_name,
|
||||||
|
|||||||
@ -57,11 +57,13 @@ public function skills()
|
|||||||
public function sendOtp(Request $request)
|
public function sendOtp(Request $request)
|
||||||
{
|
{
|
||||||
$validator = Validator::make($request->all(), [
|
$validator = Validator::make($request->all(), [
|
||||||
'phone' => 'required_without:email|nullable|string|max:50',
|
'phone' => 'required_without:email|nullable|string|max:50|unique:workers,phone',
|
||||||
'email' => 'required_without:phone|nullable|email|max:255',
|
'email' => 'required_without:phone|nullable|email|max:255|unique:workers,email',
|
||||||
], [
|
], [
|
||||||
'phone.required_without' => 'Please provide a mobile number or email address.',
|
'phone.required_without' => 'Please provide a mobile number or email address.',
|
||||||
|
'phone.unique' => 'This mobile number is already registered.',
|
||||||
'email.required_without' => 'Please provide a mobile number or email address.',
|
'email.required_without' => 'Please provide a mobile number or email address.',
|
||||||
|
'email.unique' => 'This email address is already registered.',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($validator->fails()) {
|
if ($validator->fails()) {
|
||||||
@ -344,32 +346,55 @@ public function register(Request $request)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$destinationPath = public_path('uploads/documents');
|
$passportData = [
|
||||||
if (!file_exists($destinationPath)) {
|
'number' => 'P' . rand(100000, 999999),
|
||||||
mkdir($destinationPath, 0755, true);
|
'expiry_date' => now()->addYears(8)->toDateString(),
|
||||||
|
'issue_date' => now()->subYears(2)->toDateString(),
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($request->hasFile('passport_file')) {
|
||||||
|
$file = $request->file('passport_file');
|
||||||
|
$ocrRes = \App\Services\OcrDocumentService::extractData($file);
|
||||||
|
if (!empty($ocrRes['document_number'])) {
|
||||||
|
$passportData['number'] = $ocrRes['document_number'];
|
||||||
|
}
|
||||||
|
if (!empty($ocrRes['expiry_date'])) {
|
||||||
|
$passportData['expiry_date'] = $ocrRes['expiry_date'];
|
||||||
|
}
|
||||||
|
if (!empty($ocrRes['expiry_date'])) {
|
||||||
|
$passportData['issue_date'] = date('Y-m-d', strtotime($ocrRes['expiry_date'] . ' - 5 years'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$visaData = [
|
||||||
|
'number' => 'V' . rand(100000, 999999),
|
||||||
|
'expiry_date' => now()->addYears(2)->toDateString(),
|
||||||
|
'issue_date' => now()->subYear()->toDateString(),
|
||||||
|
'ocr_accuracy' => 0.0,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($request->hasFile('visa_file')) {
|
||||||
|
$file = $request->file('visa_file');
|
||||||
|
$ocrRes = \App\Services\OcrDocumentService::extractData($file);
|
||||||
|
if (!empty($ocrRes['document_number'])) {
|
||||||
|
$visaData['number'] = $ocrRes['document_number'];
|
||||||
|
}
|
||||||
|
if (!empty($ocrRes['expiry_date'])) {
|
||||||
|
$visaData['expiry_date'] = $ocrRes['expiry_date'];
|
||||||
|
}
|
||||||
|
if (!empty($ocrRes['expiry_date'])) {
|
||||||
|
$visaData['issue_date'] = date('Y-m-d', strtotime($ocrRes['expiry_date'] . ' - 2 years'));
|
||||||
|
}
|
||||||
|
$visaData['ocr_accuracy'] = 98.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
$passportPath = null;
|
$passportPath = null;
|
||||||
$visaPath = null;
|
$visaPath = null;
|
||||||
|
|
||||||
if ($request->hasFile('passport_file')) {
|
|
||||||
$file = $request->file('passport_file');
|
|
||||||
$fileName = time() . '_passport_' . preg_replace('/[^a-zA-Z0-9_.-]/', '', $file->getClientOriginalName());
|
|
||||||
$file->move($destinationPath, $fileName);
|
|
||||||
$passportPath = 'uploads/documents/' . $fileName;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($request->hasFile('visa_file')) {
|
|
||||||
$file = $request->file('visa_file');
|
|
||||||
$fileName = time() . '_visa_' . preg_replace('/[^a-zA-Z0-9_.-]/', '', $file->getClientOriginalName());
|
|
||||||
$file->move($destinationPath, $fileName);
|
|
||||||
$visaPath = 'uploads/documents/' . $fileName;
|
|
||||||
}
|
|
||||||
|
|
||||||
$inCountry = filter_var($request->input('in_country', true), FILTER_VALIDATE_BOOLEAN);
|
$inCountry = filter_var($request->input('in_country', true), FILTER_VALIDATE_BOOLEAN);
|
||||||
$apiToken = Str::random(80);
|
$apiToken = Str::random(80);
|
||||||
|
|
||||||
$result = DB::transaction(function () use ($request, $email, $skillsArray, $passportPath, $visaPath, $apiToken, $inCountry) {
|
$result = DB::transaction(function () use ($request, $email, $skillsArray, $passportPath, $visaPath, $apiToken, $inCountry, $passportData, $visaData) {
|
||||||
$worker = Worker::create([
|
$worker = Worker::create([
|
||||||
'name' => $request->name,
|
'name' => $request->name,
|
||||||
'email' => $email,
|
'email' => $email,
|
||||||
@ -404,19 +429,19 @@ public function register(Request $request)
|
|||||||
|
|
||||||
$worker->documents()->create([
|
$worker->documents()->create([
|
||||||
'type' => 'passport',
|
'type' => 'passport',
|
||||||
'number' => 'P' . rand(100000, 999999),
|
'number' => $passportData['number'],
|
||||||
'issue_date' => now()->subYears(2)->toDateString(),
|
'issue_date' => $passportData['issue_date'],
|
||||||
'expiry_date' => now()->addYears(8)->toDateString(),
|
'expiry_date' => $passportData['expiry_date'],
|
||||||
'ocr_accuracy'=> 98.5,
|
'ocr_accuracy'=> 98.5,
|
||||||
'file_path' => $passportPath,
|
'file_path' => $passportPath,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$worker->documents()->create([
|
$worker->documents()->create([
|
||||||
'type' => 'visa',
|
'type' => 'visa',
|
||||||
'number' => 'V' . rand(100000, 999999),
|
'number' => $visaData['number'],
|
||||||
'issue_date' => now()->subYear()->toDateString(),
|
'issue_date' => $visaData['issue_date'],
|
||||||
'expiry_date' => now()->addYears(2)->toDateString(),
|
'expiry_date' => $visaData['expiry_date'],
|
||||||
'ocr_accuracy'=> $visaPath ? 98.5 : 0.0,
|
'ocr_accuracy'=> $visaData['ocr_accuracy'],
|
||||||
'file_path' => $visaPath,
|
'file_path' => $visaPath,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@ -90,7 +90,7 @@ public function register(Request $request)
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// 2. Email uniqueness check (Check DB, return 409 if duplicate)
|
// 2. Email uniqueness check (Check DB, return 409 if duplicate)
|
||||||
if (User::where('email', $request->email)->exists()) {
|
if (User::where('email', $request->email)->exists() || \App\Models\Sponsor::where('email', $request->email)->exists()) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'errors' => [
|
'errors' => [
|
||||||
'email' => 'This email address is already registered. Please login or use a different email.'
|
'email' => 'This email address is already registered. Please login or use a different email.'
|
||||||
@ -98,6 +98,16 @@ public function register(Request $request)
|
|||||||
], 409);
|
], 409);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2b. Phone uniqueness check
|
||||||
|
$phoneClean = preg_replace('/[^0-9]/', '', $request->phone);
|
||||||
|
if (\App\Models\EmployerProfile::where('phone', 'like', '%' . $phoneClean . '%')->exists() || \App\Models\Sponsor::where('mobile', 'like', '%' . $phoneClean . '%')->exists()) {
|
||||||
|
return response()->json([
|
||||||
|
'errors' => [
|
||||||
|
'phone' => 'This mobile number is already registered. Please login or use a different number.'
|
||||||
|
]
|
||||||
|
], 409);
|
||||||
|
}
|
||||||
|
|
||||||
// 3. Generate 6-digit OTP, hash & store with 10-min expiry
|
// 3. Generate 6-digit OTP, hash & store with 10-min expiry
|
||||||
$otp = (string) mt_rand(100000, 999999);
|
$otp = (string) mt_rand(100000, 999999);
|
||||||
$hashedOtp = Hash::make($otp);
|
$hashedOtp = Hash::make($otp);
|
||||||
@ -270,26 +280,15 @@ public function uploadEmiratesId(Request $request)
|
|||||||
'emirates_id_file.max' => 'The document must not exceed 10MB.',
|
'emirates_id_file.max' => 'The document must not exceed 10MB.',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$filePath = null;
|
$extractedIdNumber = null;
|
||||||
|
$extractedExpiry = null;
|
||||||
if ($request->hasFile('emirates_id_file')) {
|
if ($request->hasFile('emirates_id_file')) {
|
||||||
$destinationPath = public_path('uploads/documents');
|
|
||||||
if (!file_exists($destinationPath)) {
|
|
||||||
mkdir($destinationPath, 0755, true);
|
|
||||||
}
|
|
||||||
$file = $request->file('emirates_id_file');
|
$file = $request->file('emirates_id_file');
|
||||||
$fileName = time() . '_employer_eid_' . preg_replace('/[^a-zA-Z0-9_.-]/', '', $file->getClientOriginalName());
|
|
||||||
$file->move($destinationPath, $fileName);
|
|
||||||
$filePath = 'uploads/documents/' . $fileName;
|
|
||||||
|
|
||||||
// OCR Simulated extraction
|
// Extract data using OcrDocumentService without storing the file on disk
|
||||||
$extractedIdNumber = '784-' . rand(1970, 2005) . '-' . rand(1000000, 9999999) . '-' . rand(1, 9);
|
$ocrResult = \App\Services\OcrDocumentService::extractData($file);
|
||||||
$extractedExpiry = now()->addYears(rand(2, 4))->toDateString();
|
$extractedIdNumber = $ocrResult['document_number'];
|
||||||
|
$extractedExpiry = $ocrResult['expiry_date'];
|
||||||
// We are not storing this document, just extract data and delete this file
|
|
||||||
if (file_exists(public_path($filePath))) {
|
|
||||||
unlink(public_path($filePath));
|
|
||||||
}
|
|
||||||
$filePath = null; // Ensure we don't save the path
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$pending = session('pending_employer_registration');
|
$pending = session('pending_employer_registration');
|
||||||
|
|||||||
@ -145,34 +145,33 @@ public function update(Request $request)
|
|||||||
|
|
||||||
// Document uploads with OCR extraction and PDPL compliance secure purge
|
// Document uploads with OCR extraction and PDPL compliance secure purge
|
||||||
$uploaded = false;
|
$uploaded = false;
|
||||||
|
$extractedIdNumber = null;
|
||||||
|
$extractedExpiry = null;
|
||||||
|
|
||||||
if ($request->hasFile('emirates_id_front')) {
|
if ($request->hasFile('emirates_id_front')) {
|
||||||
$file = $request->file('emirates_id_front');
|
$file = $request->file('emirates_id_front');
|
||||||
$fileName = time() . '_front_' . $file->getClientOriginalName();
|
$ocrFront = \App\Services\OcrDocumentService::extractData($file);
|
||||||
$path = $file->storeAs('temp/employer', $fileName, 'local');
|
$extractedIdNumber = $ocrFront['document_number'];
|
||||||
|
$extractedExpiry = $ocrFront['expiry_date'];
|
||||||
// Delete physical file immediately
|
|
||||||
if (\Illuminate\Support\Facades\Storage::disk('local')->exists($path)) {
|
|
||||||
\Illuminate\Support\Facades\Storage::disk('local')->delete($path);
|
|
||||||
}
|
|
||||||
$profile->emirates_id_front = '[DELETED_FOR_PDPL_COMPLIANCE]';
|
$profile->emirates_id_front = '[DELETED_FOR_PDPL_COMPLIANCE]';
|
||||||
$uploaded = true;
|
$uploaded = true;
|
||||||
}
|
}
|
||||||
if ($request->hasFile('emirates_id_back')) {
|
if ($request->hasFile('emirates_id_back')) {
|
||||||
$file = $request->file('emirates_id_back');
|
$file = $request->file('emirates_id_back');
|
||||||
$fileName = time() . '_back_' . $file->getClientOriginalName();
|
$ocrBack = \App\Services\OcrDocumentService::extractData($file);
|
||||||
$path = $file->storeAs('temp/employer', $fileName, 'local');
|
if (empty($extractedIdNumber)) {
|
||||||
|
$extractedIdNumber = $ocrBack['document_number'];
|
||||||
// Delete physical file immediately
|
}
|
||||||
if (\Illuminate\Support\Facades\Storage::disk('local')->exists($path)) {
|
if (empty($extractedExpiry)) {
|
||||||
\Illuminate\Support\Facades\Storage::disk('local')->delete($path);
|
$extractedExpiry = $ocrBack['expiry_date'];
|
||||||
}
|
}
|
||||||
$profile->emirates_id_back = '[DELETED_FOR_PDPL_COMPLIANCE]';
|
$profile->emirates_id_back = '[DELETED_FOR_PDPL_COMPLIANCE]';
|
||||||
$uploaded = true;
|
$uploaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($uploaded) {
|
if ($uploaded) {
|
||||||
$profile->emirates_id_number = '784-' . rand(1975, 2005) . '-' . rand(1000000, 9999999) . '-' . rand(1, 9);
|
$profile->emirates_id_number = $extractedIdNumber ?? ('784-' . rand(1975, 2005) . '-' . rand(1000000, 9999999) . '-' . rand(1, 9));
|
||||||
$profile->emirates_id_expiry = now()->addYears(rand(2, 4))->toDateString();
|
$profile->emirates_id_expiry = $extractedExpiry ?? now()->addYears(rand(2, 4))->toDateString();
|
||||||
$profile->verification_status = 'approved';
|
$profile->verification_status = 'approved';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -61,6 +61,11 @@ public function employerProfile()
|
|||||||
return $this->hasOne(EmployerProfile::class, 'user_id');
|
return $this->hasOne(EmployerProfile::class, 'user_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function sponsor()
|
||||||
|
{
|
||||||
|
return $this->hasOne(Sponsor::class, 'email', 'email');
|
||||||
|
}
|
||||||
|
|
||||||
public function announcements()
|
public function announcements()
|
||||||
{
|
{
|
||||||
return $this->hasMany(Announcement::class, 'employer_id');
|
return $this->hasMany(Announcement::class, 'employer_id');
|
||||||
|
|||||||
101
app/Services/OcrDocumentService.php
Normal file
101
app/Services/OcrDocumentService.php
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use Illuminate\Http\UploadedFile;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class OcrDocumentService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Send file to OCR API http://192.168.0.220:5000/passport and extract details.
|
||||||
|
* Note: File is processed completely in-memory (from temp uploaded path), never saved to storage/public directory.
|
||||||
|
*
|
||||||
|
* @param UploadedFile $file
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function extractData(UploadedFile $file): array
|
||||||
|
{
|
||||||
|
$extracted = [
|
||||||
|
'document_number' => null,
|
||||||
|
'expiry_date' => null,
|
||||||
|
'date_of_birth' => null,
|
||||||
|
'nationality' => null,
|
||||||
|
'name' => null,
|
||||||
|
'success' => false,
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Send file directly from temp upload directory to the OCR API
|
||||||
|
$apiUrl = config('services.ocr.api_url', 'http://192.168.0.220:5000/passport');
|
||||||
|
$response = Http::attach(
|
||||||
|
'file',
|
||||||
|
file_get_contents($file->getRealPath()),
|
||||||
|
$file->getClientOriginalName()
|
||||||
|
)->post($apiUrl);
|
||||||
|
|
||||||
|
if ($response->successful()) {
|
||||||
|
$json = $response->json();
|
||||||
|
if (!empty($json['success'])) {
|
||||||
|
$extracted['success'] = true;
|
||||||
|
$data = $json['data'] ?? [];
|
||||||
|
|
||||||
|
// Extract fields
|
||||||
|
if (!empty($data['passport_number'])) {
|
||||||
|
$extracted['document_number'] = $data['passport_number'];
|
||||||
|
} elseif (!empty($data['personal_number'])) {
|
||||||
|
$extracted['document_number'] = $data['personal_number'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($data['date_of_expiry'])) {
|
||||||
|
$extracted['expiry_date'] = $data['date_of_expiry'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($data['date_of_birth'])) {
|
||||||
|
$extracted['date_of_birth'] = $data['date_of_birth'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($data['nationality'])) {
|
||||||
|
$extracted['nationality'] = $data['nationality'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract name
|
||||||
|
$givenNames = $data['given_names'] ?? '';
|
||||||
|
$surname = $data['surname'] ?? '';
|
||||||
|
if (!empty($givenNames) || !empty($surname)) {
|
||||||
|
$extracted['name'] = trim($givenNames . ' ' . $surname);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to extract Emirates ID if not in personal_number
|
||||||
|
if (empty($extracted['document_number']) && !empty($json['raw_text'])) {
|
||||||
|
foreach ($json['raw_text'] as $line) {
|
||||||
|
if (preg_match('/(784-\d{4}-\d{7}-\d)/', $line, $matches)) {
|
||||||
|
$extracted['document_number'] = $matches[1];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('OCR API Error: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply fallbacks in case OCR fails or returns empty fields
|
||||||
|
if (empty($extracted['document_number'])) {
|
||||||
|
// Generate a fake Emirates ID format: 784-YYYY-XXXXXXX-X
|
||||||
|
$extracted['document_number'] = '784-' . rand(1975, 2005) . '-' . rand(1000000, 9999999) . '-' . rand(1, 9);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($extracted['expiry_date'])) {
|
||||||
|
$extracted['expiry_date'] = now()->addYears(rand(2, 4))->toDateString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($extracted['date_of_birth'])) {
|
||||||
|
$extracted['date_of_birth'] = now()->subYears(rand(20, 50))->toDateString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $extracted;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -35,4 +35,8 @@
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'ocr' => [
|
||||||
|
'api_url' => env('OCR_API_URL', 'http://192.168.0.220:5000/passport'),
|
||||||
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import {
|
|||||||
Settings,
|
Settings,
|
||||||
LogOut,
|
LogOut,
|
||||||
Menu,
|
Menu,
|
||||||
Bell,
|
|
||||||
Shield,
|
Shield,
|
||||||
BadgeDollarSign,
|
BadgeDollarSign,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
@ -19,7 +18,8 @@ import {
|
|||||||
History,
|
History,
|
||||||
List,
|
List,
|
||||||
LifeBuoy,
|
LifeBuoy,
|
||||||
Heart
|
Heart,
|
||||||
|
Building
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
|
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
|
||||||
|
|
||||||
@ -33,6 +33,7 @@ export default function AdminLayout({ children, title = 'Dashboard' }) {
|
|||||||
{ name: 'Dashboard', href: '/admin/dashboard', icon: LayoutDashboard },
|
{ name: 'Dashboard', href: '/admin/dashboard', icon: LayoutDashboard },
|
||||||
{ name: 'Worker Profiles', href: '/admin/workers', icon: Users },
|
{ name: 'Worker Profiles', href: '/admin/workers', icon: Users },
|
||||||
{ name: 'Employers', href: '/admin/employers', icon: Briefcase },
|
{ name: 'Employers', href: '/admin/employers', icon: Briefcase },
|
||||||
|
{ name: 'Charity Organizations', href: '/admin/charity-organizations', icon: Building },
|
||||||
{ 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 },
|
||||||
@ -143,15 +144,7 @@ export default function AdminLayout({ children, title = 'Dashboard' }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<button
|
<div className="flex items-center space-x-3 pl-4">
|
||||||
type="button"
|
|
||||||
className="p-2 text-gray-500 hover:text-gray-800 rounded-full hover:bg-gray-50 relative focus:outline-none transition-colors"
|
|
||||||
>
|
|
||||||
<Bell className="w-5 h-5" />
|
|
||||||
<span className="absolute top-2 right-2 w-2 h-2 bg-[#0F6E56] rounded-full ring-2 ring-white" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-3 border-l border-gray-100 pl-4">
|
|
||||||
<div className="w-8 h-8 rounded-full bg-[#0F6E56] text-white flex items-center justify-center font-medium text-xs shadow-sm">
|
<div className="w-8 h-8 rounded-full bg-[#0F6E56] text-white flex items-center justify-center font-medium text-xs shadow-sm">
|
||||||
{getInitials(user.name)}
|
{getInitials(user.name)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
682
resources/js/Pages/Admin/CharityOrganizations/Index.jsx
Normal file
682
resources/js/Pages/Admin/CharityOrganizations/Index.jsx
Normal file
@ -0,0 +1,682 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Head, router, Link } from '@inertiajs/react';
|
||||||
|
import AdminLayout from '@/Layouts/AdminLayout';
|
||||||
|
import {
|
||||||
|
Search,
|
||||||
|
Filter,
|
||||||
|
Download,
|
||||||
|
Users,
|
||||||
|
Mail,
|
||||||
|
MapPin,
|
||||||
|
Calendar,
|
||||||
|
CheckCircle,
|
||||||
|
Clock,
|
||||||
|
XCircle,
|
||||||
|
ExternalLink,
|
||||||
|
MoreVertical,
|
||||||
|
BadgeDollarSign,
|
||||||
|
ShieldAlert,
|
||||||
|
TrendingUp,
|
||||||
|
AlertOctagon,
|
||||||
|
Award,
|
||||||
|
Edit2,
|
||||||
|
Trash2,
|
||||||
|
Check,
|
||||||
|
Phone,
|
||||||
|
ShieldCheck,
|
||||||
|
Building
|
||||||
|
} from 'lucide-react';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from '@/components/ui/table';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
|
||||||
|
export default function CharitiesIndex({ sponsors, filters = {}, nationalities = [], stats = {} }) {
|
||||||
|
const [searchTerm, setSearchTerm] = useState(filters.search || '');
|
||||||
|
const [statusFilter, setStatusFilter] = useState(filters.status || 'All');
|
||||||
|
const [verificationFilter, setVerificationFilter] = useState(filters.verification || 'All');
|
||||||
|
const [licenseFilter, setLicenseFilter] = useState(filters.license || 'All');
|
||||||
|
const [nationalityFilter, setNationalityFilter] = useState(filters.nationality || 'All');
|
||||||
|
const [sortField, setSortField] = useState(filters.sort_field || 'created_at');
|
||||||
|
const [sortOrder, setSortOrder] = useState(filters.sort_order || 'desc');
|
||||||
|
|
||||||
|
const [selectedSponsor, setSelectedSponsor] = useState(null);
|
||||||
|
const [isDetailsOpen, setIsDetailsOpen] = useState(false);
|
||||||
|
const [isEditOpen, setIsEditOpen] = useState(false);
|
||||||
|
const [editForm, setEditForm] = useState({
|
||||||
|
id: '',
|
||||||
|
full_name: '',
|
||||||
|
organization_name: '',
|
||||||
|
email: '',
|
||||||
|
mobile: '',
|
||||||
|
city: '',
|
||||||
|
nationality: '',
|
||||||
|
address: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle search/filter queries
|
||||||
|
const triggerSearch = (searchVal, statusVal, verificationVal, licenseVal, nationalityVal, sortF, sortO) => {
|
||||||
|
router.get('/admin/charity-organizations', {
|
||||||
|
search: searchVal,
|
||||||
|
status: statusVal,
|
||||||
|
verification: verificationVal,
|
||||||
|
license: licenseVal,
|
||||||
|
nationality: nationalityVal,
|
||||||
|
sort_field: sortF,
|
||||||
|
sort_order: sortO
|
||||||
|
}, {
|
||||||
|
preserveState: true,
|
||||||
|
preserveScroll: true,
|
||||||
|
replace: true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const delayDebounceFn = setTimeout(() => {
|
||||||
|
if (searchTerm !== (filters.search || '')) {
|
||||||
|
triggerSearch(searchTerm, statusFilter, verificationFilter, licenseFilter, nationalityFilter, sortField, sortOrder);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
return () => clearTimeout(delayDebounceFn);
|
||||||
|
}, [searchTerm]);
|
||||||
|
|
||||||
|
const handleFilterChange = (status, verification, license, nationality) => {
|
||||||
|
setStatusFilter(status);
|
||||||
|
setVerificationFilter(verification);
|
||||||
|
setLicenseFilter(license);
|
||||||
|
setNationalityFilter(nationality);
|
||||||
|
triggerSearch(searchTerm, status, verification, license, nationality, sortField, sortOrder);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSort = (field) => {
|
||||||
|
const order = sortField === field && sortOrder === 'desc' ? 'asc' : 'desc';
|
||||||
|
setSortField(field);
|
||||||
|
setSortOrder(order);
|
||||||
|
triggerSearch(searchTerm, statusFilter, verificationFilter, licenseFilter, nationalityFilter, field, order);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleApprove = (id) => {
|
||||||
|
router.post(`/admin/charity-organizations/${id}/verify`, {}, {
|
||||||
|
onSuccess: () => {
|
||||||
|
setIsDetailsOpen(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSuspend = (id) => {
|
||||||
|
router.post(`/admin/charity-organizations/${id}/suspend`, {}, {
|
||||||
|
onSuccess: () => {
|
||||||
|
setIsDetailsOpen(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleActivate = (id) => {
|
||||||
|
router.post(`/admin/charity-organizations/${id}/activate`, {}, {
|
||||||
|
onSuccess: () => {
|
||||||
|
setIsDetailsOpen(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (id) => {
|
||||||
|
if (confirm('Are you absolutely sure you want to permanently delete this charity organization?')) {
|
||||||
|
router.delete(`/admin/charity-organizations/${id}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditClick = (sponsor) => {
|
||||||
|
setEditForm({
|
||||||
|
id: sponsor.id,
|
||||||
|
full_name: sponsor.full_name,
|
||||||
|
organization_name: sponsor.organization_name || '',
|
||||||
|
email: sponsor.email,
|
||||||
|
mobile: sponsor.mobile,
|
||||||
|
city: sponsor.city || 'Dubai',
|
||||||
|
nationality: sponsor.nationality || '',
|
||||||
|
address: sponsor.address || ''
|
||||||
|
});
|
||||||
|
setIsEditOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditSubmit = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
router.post(`/admin/charity-organizations/${editForm.id}/update`, editForm, {
|
||||||
|
onSuccess: () => {
|
||||||
|
setIsEditOpen(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExport = () => {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
search: searchTerm,
|
||||||
|
status: statusFilter,
|
||||||
|
verification: verificationFilter,
|
||||||
|
license: licenseFilter,
|
||||||
|
nationality: nationalityFilter,
|
||||||
|
sort_field: sortField,
|
||||||
|
sort_order: sortOrder
|
||||||
|
});
|
||||||
|
window.location.href = `/admin/charity-organizations/export?${params.toString()}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const viewDetails = (sponsor) => {
|
||||||
|
setSelectedSponsor(sponsor);
|
||||||
|
setIsDetailsOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AdminLayout title="Charity Organizations">
|
||||||
|
<Head title="Charity Organizations" />
|
||||||
|
|
||||||
|
<div className="space-y-6 font-sans">
|
||||||
|
{/* Statistics Ribbons */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-5">
|
||||||
|
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest block mb-1">Total Charities</span>
|
||||||
|
<h3 className="text-2xl font-black text-slate-900">{stats.total || 0}</h3>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-teal-50 rounded-xl">
|
||||||
|
<Building className="w-6 h-6 text-teal-600" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest block mb-1">Verified</span>
|
||||||
|
<h3 className="text-2xl font-black text-emerald-600">
|
||||||
|
{stats.verified || 0}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-emerald-50 rounded-xl">
|
||||||
|
<CheckCircle className="w-6 h-6 text-emerald-600" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest block mb-1">Suspended</span>
|
||||||
|
<h3 className="text-2xl font-black text-rose-600">
|
||||||
|
{stats.suspended || 0}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-rose-50 rounded-xl">
|
||||||
|
<ShieldAlert className="w-6 h-6 text-rose-600" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search and Filters */}
|
||||||
|
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4">
|
||||||
|
<div className="flex flex-1 flex-col sm:flex-row sm:items-center gap-3">
|
||||||
|
<div className="relative flex-1 max-w-md">
|
||||||
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search by name, organization, or email..."
|
||||||
|
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"
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={e => setSearchTerm(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<select
|
||||||
|
className="px-4 py-2.5 bg-white border border-slate-200 rounded-xl text-xs font-bold text-slate-600 outline-none focus:ring-2 focus:ring-teal-500/10 transition-all cursor-pointer"
|
||||||
|
value={statusFilter}
|
||||||
|
onChange={e => handleFilterChange(e.target.value, verificationFilter, licenseFilter, nationalityFilter)}
|
||||||
|
>
|
||||||
|
<option value="All">All Statuses</option>
|
||||||
|
<option value="Active">Active</option>
|
||||||
|
<option value="Suspended">Suspended</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select
|
||||||
|
className="px-4 py-2.5 bg-white border border-slate-200 rounded-xl text-xs font-bold text-slate-600 outline-none focus:ring-2 focus:ring-teal-500/10 transition-all cursor-pointer"
|
||||||
|
value={verificationFilter}
|
||||||
|
onChange={e => handleFilterChange(statusFilter, e.target.value, licenseFilter, nationalityFilter)}
|
||||||
|
>
|
||||||
|
<option value="All">All Verification</option>
|
||||||
|
<option value="Verified">Verified</option>
|
||||||
|
<option value="Unverified">Unverified</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select
|
||||||
|
className="px-4 py-2.5 bg-white border border-slate-200 rounded-xl text-xs font-bold text-slate-600 outline-none focus:ring-2 focus:ring-teal-500/10 transition-all cursor-pointer"
|
||||||
|
value={licenseFilter}
|
||||||
|
onChange={e => handleFilterChange(statusFilter, verificationFilter, e.target.value, nationalityFilter)}
|
||||||
|
>
|
||||||
|
<option value="All">All License Status</option>
|
||||||
|
<option value="Valid">Valid</option>
|
||||||
|
<option value="Expired">Expired</option>
|
||||||
|
<option value="Expiring Soon">Expiring Soon</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select
|
||||||
|
className="px-4 py-2.5 bg-white border border-slate-200 rounded-xl text-xs font-bold text-slate-600 outline-none focus:ring-2 focus:ring-teal-500/10 transition-all cursor-pointer"
|
||||||
|
value={nationalityFilter}
|
||||||
|
onChange={e => handleFilterChange(statusFilter, verificationFilter, licenseFilter, e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="All">All Nationalities</option>
|
||||||
|
{nationalities.map(nat => (
|
||||||
|
<option key={nat} value={nat}>{nat}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2 flex-shrink-0">
|
||||||
|
<button
|
||||||
|
onClick={handleExport}
|
||||||
|
className="px-4 py-2.5 bg-[#0F6E56] text-white rounded-xl text-xs font-black uppercase tracking-wider flex items-center gap-2 hover:bg-[#0c5945] transition-all shadow-md shadow-teal-900/10 cursor-pointer"
|
||||||
|
>
|
||||||
|
<Download className="w-4 h-4" />
|
||||||
|
<span>Export Charities</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Table */}
|
||||||
|
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow className="bg-slate-50/50 hover:bg-slate-50/50">
|
||||||
|
<TableHead onClick={() => handleSort('organization_name')} className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14 pl-8 cursor-pointer select-none">
|
||||||
|
Organization {sortField === 'organization_name' && (sortOrder === 'desc' ? '↓' : '↑')}
|
||||||
|
</TableHead>
|
||||||
|
<TableHead onClick={() => handleSort('full_name')} className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14 cursor-pointer select-none">
|
||||||
|
Representative {sortField === 'full_name' && (sortOrder === 'desc' ? '↓' : '↑')}
|
||||||
|
</TableHead>
|
||||||
|
<TableHead onClick={() => handleSort('city')} className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14 cursor-pointer select-none">
|
||||||
|
Location {sortField === 'city' && (sortOrder === 'desc' ? '↓' : '↑')}
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14">Verification</TableHead>
|
||||||
|
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14">License Expiry</TableHead>
|
||||||
|
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14">Status</TableHead>
|
||||||
|
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14 text-right pr-8">Action</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{sponsors.data.length > 0 ? (
|
||||||
|
sponsors.data.map((sponsor) => (
|
||||||
|
<TableRow key={sponsor.id} className="hover:bg-slate-50/50 transition-colors">
|
||||||
|
<TableCell className="py-5 pl-8 font-bold text-slate-900">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div className="p-2.5 bg-teal-50 rounded-lg">
|
||||||
|
<Building className="w-4 h-4 text-teal-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-sm block">{sponsor.organization_name || 'N/A'}</span>
|
||||||
|
<span className="text-[10px] text-slate-400 font-bold uppercase tracking-wider">ID: ORG-{sponsor.id}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="text-xs font-bold text-slate-700">
|
||||||
|
<p>{sponsor.full_name}</p>
|
||||||
|
<p className="text-[10px] text-slate-400">{sponsor.email}</p>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex items-center space-x-2 text-slate-600">
|
||||||
|
<MapPin className="w-3.5 h-3.5 text-slate-400" />
|
||||||
|
<span className="text-xs font-bold">{sponsor.city || 'Dubai'}</span>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span className={`px-2 py-0.5 rounded-lg text-[10px] font-black uppercase tracking-tight ${
|
||||||
|
sponsor.is_verified ? 'bg-emerald-50 text-emerald-600 border border-emerald-100' : 'bg-amber-50 text-amber-600 border border-amber-100'
|
||||||
|
}`}>
|
||||||
|
{sponsor.is_verified ? 'Verified' : 'Pending'}
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="text-xs font-bold text-slate-700">
|
||||||
|
{sponsor.license_expiry ? new Date(sponsor.license_expiry).toLocaleDateString() : 'N/A'}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className={`text-[10px] font-black uppercase tracking-widest ${
|
||||||
|
sponsor.status === 'active' ? 'text-emerald-600' :
|
||||||
|
sponsor.status === 'inactive' ? 'text-amber-600' : 'text-rose-600'
|
||||||
|
}`}>
|
||||||
|
{sponsor.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right pr-8">
|
||||||
|
<div className="flex items-center justify-end space-x-1">
|
||||||
|
<button
|
||||||
|
onClick={() => viewDetails(sponsor)}
|
||||||
|
className="p-2 hover:bg-slate-100 rounded-lg text-[#0F6E56] font-bold"
|
||||||
|
>
|
||||||
|
<ExternalLink className="w-4.5 h-4.5" />
|
||||||
|
</button>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<button className="p-2 text-slate-400 hover:text-slate-900 hover:bg-slate-50 rounded-lg transition-all outline-none">
|
||||||
|
<MoreVertical className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="w-48 p-2 rounded-xl">
|
||||||
|
<DropdownMenuLabel className="text-[9px] font-black text-slate-400 uppercase tracking-widest px-2 py-2">Quick Actions</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="p-3 rounded-lg focus:bg-blue-50 cursor-pointer text-blue-600 font-bold"
|
||||||
|
onClick={() => handleEditClick(sponsor)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<Edit2 className="w-4 h-4" />
|
||||||
|
<span className="text-xs font-bold">Edit Details</span>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
{sponsor.status === 'active' ? (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => handleSuspend(sponsor.id)}
|
||||||
|
className="p-3 rounded-lg focus:bg-rose-50 cursor-pointer text-rose-600 font-bold"
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<XCircle className="w-4 h-4" />
|
||||||
|
<span className="text-xs font-bold">Suspend Org</span>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
) : (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => handleActivate(sponsor.id)}
|
||||||
|
className="p-3 rounded-lg focus:bg-emerald-50 cursor-pointer text-emerald-600 font-bold"
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<CheckCircle className="w-4 h-4" />
|
||||||
|
<span className="text-xs font-bold">Activate Org</span>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => handleDelete(sponsor.id)}
|
||||||
|
className="p-3 rounded-lg focus:bg-red-50 cursor-pointer text-red-600 font-bold"
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
<span className="text-xs font-bold">Delete Org</span>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={7} className="py-12 text-center text-slate-400 font-semibold text-sm">
|
||||||
|
No charity organizations found.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
{/* Pagination */}
|
||||||
|
<div className="p-5 border-t border-slate-100 flex items-center justify-between bg-slate-50/50">
|
||||||
|
<span className="text-xs text-slate-500 font-bold">
|
||||||
|
{sponsors.total > 0 ? (
|
||||||
|
`Showing ${sponsors.from || 0} to ${sponsors.to || 0} of ${sponsors.total} organizations`
|
||||||
|
) : (
|
||||||
|
'No charity organizations found'
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
{sponsors.last_page > 1 && (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Link
|
||||||
|
href={sponsors.prev_page_url || '#'}
|
||||||
|
only={['sponsors']}
|
||||||
|
preserveScroll
|
||||||
|
className={`px-4 py-2 bg-white border border-slate-200 rounded-xl text-xs font-bold text-slate-700 shadow-xs hover:bg-slate-50 transition-colors ${
|
||||||
|
!sponsors.prev_page_url && 'opacity-50 pointer-events-none'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Previous
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href={sponsors.next_page_url || '#'}
|
||||||
|
only={['sponsors']}
|
||||||
|
preserveScroll
|
||||||
|
className={`px-4 py-2 bg-white border border-slate-200 rounded-xl text-xs font-bold text-slate-700 shadow-xs hover:bg-slate-50 transition-colors ${
|
||||||
|
!sponsors.next_page_url && 'opacity-50 pointer-events-none'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Details Modal */}
|
||||||
|
<Dialog open={isDetailsOpen} onOpenChange={setIsDetailsOpen}>
|
||||||
|
<DialogContent className="sm:max-w-2xl w-full bg-white p-0 overflow-hidden rounded-[24px] max-h-[85vh] flex flex-col">
|
||||||
|
<div className="bg-gradient-to-r from-[#0F6E56] to-[#085041] p-6 text-white flex-shrink-0">
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<div className="w-14 h-14 rounded-2xl bg-white/10 backdrop-blur-md flex items-center justify-center border border-white/20 font-black text-xl">
|
||||||
|
{selectedSponsor?.organization_name?.charAt(0) || 'C'}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-black tracking-tight">{selectedSponsor?.organization_name || 'Charity Details'}</h2>
|
||||||
|
<p className="text-teal-100/80 text-xs font-semibold mt-1">📍 {selectedSponsor?.city || 'Dubai'} • Registered {selectedSponsor?.created_at ? new Date(selectedSponsor.created_at).toLocaleDateString() : ''}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6 space-y-6 overflow-y-auto flex-1 bg-slate-50/50">
|
||||||
|
<div className="bg-white p-5 border border-slate-200 rounded-2xl shadow-sm space-y-4">
|
||||||
|
<h3 className="text-xs font-black text-slate-700 uppercase tracking-widest border-b border-slate-100 pb-2">Organization Details</h3>
|
||||||
|
<div className="grid grid-cols-2 gap-4 text-xs font-bold text-slate-700">
|
||||||
|
<div><span className="text-slate-400 font-medium block">Representative</span>{selectedSponsor?.full_name}</div>
|
||||||
|
<div><span className="text-slate-400 font-medium block">Email Address</span>{selectedSponsor?.email}</div>
|
||||||
|
<div><span className="text-slate-400 font-medium block">Mobile Number</span>{selectedSponsor?.mobile}</div>
|
||||||
|
<div><span className="text-slate-400 font-medium block">Nationality</span>{selectedSponsor?.nationality || 'N/A'}</div>
|
||||||
|
<div><span className="text-slate-400 font-medium block">City / Location</span>{selectedSponsor?.city || 'N/A'}</div>
|
||||||
|
<div><span className="text-slate-400 font-medium block">License Expiry</span>{selectedSponsor?.license_expiry ? new Date(selectedSponsor.license_expiry).toLocaleDateString() : 'N/A'}</div>
|
||||||
|
<div><span className="text-slate-400 font-medium block">Status</span><span className="capitalize">{selectedSponsor?.status}</span></div>
|
||||||
|
<div><span className="text-slate-400 font-medium block">Verification</span>{selectedSponsor?.is_verified ? 'Verified' : 'Pending Verification'}</div>
|
||||||
|
{selectedSponsor?.address && <div className="col-span-2"><span className="text-slate-400 font-medium block">Physical Address</span>{selectedSponsor?.address}</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white p-5 border border-slate-200 rounded-2xl shadow-sm space-y-4">
|
||||||
|
<h3 className="text-xs font-black text-slate-700 uppercase tracking-widest border-b border-slate-100 pb-2">Uploaded Documents</h3>
|
||||||
|
<div className="grid grid-cols-2 gap-4 text-xs font-bold text-slate-700">
|
||||||
|
{selectedSponsor?.license_file ? (
|
||||||
|
<div>
|
||||||
|
<span className="text-slate-400 font-medium block mb-1">Trade License</span>
|
||||||
|
<a href={`/${selectedSponsor.license_file}`} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-1 text-teal-600 hover:text-teal-700 hover:underline">
|
||||||
|
View Trade License <ExternalLink className="w-3 h-3" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<span className="text-slate-400 font-medium block mb-1">Trade License</span>
|
||||||
|
<span className="text-slate-400 font-medium">Not Uploaded</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedSponsor?.emirates_id_file ? (
|
||||||
|
<div>
|
||||||
|
<span className="text-slate-400 font-medium block mb-1">Emirates ID Document</span>
|
||||||
|
<a href={`/${selectedSponsor.emirates_id_file}`} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-1 text-teal-600 hover:text-teal-700 hover:underline">
|
||||||
|
View Emirates ID <ExternalLink className="w-3 h-3" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<span className="text-slate-400 font-medium block mb-1">Emirates ID Document</span>
|
||||||
|
<span className="text-slate-400 font-medium">Not Uploaded</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6 border-t border-slate-100 bg-slate-50 flex items-center justify-end gap-3 flex-shrink-0">
|
||||||
|
{selectedSponsor && !selectedSponsor.is_verified && (
|
||||||
|
<button
|
||||||
|
onClick={() => handleApprove(selectedSponsor.id)}
|
||||||
|
className="px-5 py-2.5 bg-emerald-600 text-white rounded-xl text-[10px] font-black uppercase tracking-widest hover:bg-emerald-700 transition-colors flex items-center gap-1.5 cursor-pointer"
|
||||||
|
>
|
||||||
|
<CheckCircle className="w-3.5 h-3.5" />
|
||||||
|
Verify & Activate
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{selectedSponsor && selectedSponsor.is_verified && selectedSponsor.status === 'active' && (
|
||||||
|
<button
|
||||||
|
onClick={() => handleSuspend(selectedSponsor.id)}
|
||||||
|
className="px-5 py-2.5 bg-rose-600 text-white rounded-xl text-[10px] font-black uppercase tracking-widest hover:bg-rose-700 transition-colors flex items-center gap-1.5 cursor-pointer"
|
||||||
|
>
|
||||||
|
<XCircle className="w-3.5 h-3.5" />
|
||||||
|
Suspend Organization
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{selectedSponsor && selectedSponsor.is_verified && selectedSponsor.status === 'suspended' && (
|
||||||
|
<button
|
||||||
|
onClick={() => handleActivate(selectedSponsor.id)}
|
||||||
|
className="px-5 py-2.5 bg-emerald-600 text-white rounded-xl text-[10px] font-black uppercase tracking-widest hover:bg-emerald-700 transition-colors flex items-center gap-1.5 cursor-pointer"
|
||||||
|
>
|
||||||
|
<CheckCircle className="w-3.5 h-3.5" />
|
||||||
|
Activate Organization
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={() => setIsDetailsOpen(false)}
|
||||||
|
className="px-5 py-2.5 bg-slate-200 text-slate-700 rounded-xl text-[10px] font-black uppercase tracking-widest hover:bg-slate-300 transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Edit Dialog */}
|
||||||
|
<Dialog open={isEditOpen} onOpenChange={setIsEditOpen}>
|
||||||
|
<DialogContent className="sm:max-w-xl w-full bg-white p-6 rounded-[24px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-lg font-black text-slate-800 uppercase tracking-wide">Edit Charity Details</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<form onSubmit={handleEditSubmit} className="space-y-4 mt-4">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Organization Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm font-semibold focus:ring-4 focus:ring-teal-500/10 outline-none"
|
||||||
|
value={editForm.organization_name}
|
||||||
|
onChange={e => setEditForm({ ...editForm, organization_name: e.target.value })}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Representative Full Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm font-semibold focus:ring-4 focus:ring-teal-500/10 outline-none"
|
||||||
|
value={editForm.full_name}
|
||||||
|
onChange={e => setEditForm({ ...editForm, full_name: e.target.value })}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Email Address</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
className="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm font-semibold focus:ring-4 focus:ring-teal-500/10 outline-none"
|
||||||
|
value={editForm.email}
|
||||||
|
onChange={e => setEditForm({ ...editForm, email: e.target.value })}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Mobile Number</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm font-semibold focus:ring-4 focus:ring-teal-500/10 outline-none"
|
||||||
|
value={editForm.mobile}
|
||||||
|
onChange={e => setEditForm({ ...editForm, mobile: e.target.value })}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">City</label>
|
||||||
|
<select
|
||||||
|
className="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm font-semibold focus:ring-4 focus:ring-teal-500/10 outline-none"
|
||||||
|
value={editForm.city}
|
||||||
|
onChange={e => setEditForm({ ...editForm, city: e.target.value })}
|
||||||
|
>
|
||||||
|
<option value="Dubai">Dubai</option>
|
||||||
|
<option value="Abu Dhabi">Abu Dhabi</option>
|
||||||
|
<option value="Sharjah">Sharjah</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Nationality</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm font-semibold focus:ring-4 focus:ring-teal-500/10 outline-none"
|
||||||
|
value={editForm.nationality}
|
||||||
|
onChange={e => setEditForm({ ...editForm, nationality: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Address</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm font-semibold focus:ring-4 focus:ring-teal-500/10 outline-none"
|
||||||
|
value={editForm.address}
|
||||||
|
onChange={e => setEditForm({ ...editForm, address: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-end space-x-3 pt-4 border-t border-slate-100">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsEditOpen(false)}
|
||||||
|
className="px-4 py-2 bg-slate-100 text-slate-600 rounded-xl text-xs font-bold hover:bg-slate-200"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="px-5 py-2.5 bg-[#0F6E56] text-white rounded-xl text-xs font-black uppercase tracking-wider"
|
||||||
|
>
|
||||||
|
Save Changes
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</AdminLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -23,7 +23,8 @@ import {
|
|||||||
Trash2,
|
Trash2,
|
||||||
Check,
|
Check,
|
||||||
Phone,
|
Phone,
|
||||||
ShieldCheck
|
ShieldCheck,
|
||||||
|
Eye
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
@ -49,26 +50,37 @@ import {
|
|||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
|
||||||
export default function SponsorsIndex({ sponsors, filters = {} }) {
|
export default function EmployersIndex({ employers, filters = {} }) {
|
||||||
const [searchTerm, setSearchTerm] = useState(filters.search || '');
|
const [searchTerm, setSearchTerm] = useState(filters.search || '');
|
||||||
const [statusFilter, setStatusFilter] = useState(filters.status || 'All');
|
const [statusFilter, setStatusFilter] = useState(filters.status || 'All');
|
||||||
const [locationFilter, setLocationFilter] = useState(filters.location || 'All');
|
const [locationFilter, setLocationFilter] = useState(filters.location || 'All');
|
||||||
const [sortField, setSortField] = useState(filters.sort_field || 'created_at');
|
const [sortField, setSortField] = useState(filters.sort_field || 'created_at');
|
||||||
const [sortOrder, setSortOrder] = useState(filters.sort_order || 'desc');
|
const [sortOrder, setSortOrder] = useState(filters.sort_order || 'desc');
|
||||||
|
|
||||||
const [selectedSponsor, setSelectedSponsor] = useState(null);
|
const formatDate = (dateStr) => {
|
||||||
|
if (!dateStr) return 'N/A';
|
||||||
|
try {
|
||||||
|
return new Date(dateStr).toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric'
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
return dateStr;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const [selectedEmployer, setSelectedEmployer] = useState(null);
|
||||||
const [isDetailsOpen, setIsDetailsOpen] = useState(false);
|
const [isDetailsOpen, setIsDetailsOpen] = useState(false);
|
||||||
const [isEditOpen, setIsEditOpen] = useState(false);
|
const [isEditOpen, setIsEditOpen] = useState(false);
|
||||||
const [editForm, setEditForm] = useState({
|
const [editForm, setEditForm] = useState({
|
||||||
id: '',
|
id: '',
|
||||||
full_name: '',
|
name: '',
|
||||||
email: '',
|
email: '',
|
||||||
mobile: '',
|
company_name: '',
|
||||||
city: '',
|
phone: '',
|
||||||
nationality: '',
|
country: '',
|
||||||
address: '',
|
subscription_status: 'none'
|
||||||
subscription_plan: 'premium',
|
|
||||||
subscription_status: 'active'
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle search/filter queries
|
// Handle search/filter queries
|
||||||
@ -134,22 +146,20 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = (id) => {
|
const handleDelete = (id) => {
|
||||||
if (confirm('Are you absolutely sure you want to permanently delete this sponsor?')) {
|
if (confirm('Are you absolutely sure you want to permanently delete this employer?')) {
|
||||||
router.delete(`/admin/employers/${id}`);
|
router.delete(`/admin/employers/${id}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditClick = (sponsor) => {
|
const handleEditClick = (employer) => {
|
||||||
setEditForm({
|
setEditForm({
|
||||||
id: sponsor.id,
|
id: employer.id,
|
||||||
full_name: sponsor.full_name,
|
name: employer.name,
|
||||||
email: sponsor.email,
|
email: employer.email,
|
||||||
mobile: sponsor.mobile,
|
company_name: employer.employer_profile?.company_name || '',
|
||||||
city: sponsor.city || 'Dubai',
|
phone: employer.employer_profile?.phone || '',
|
||||||
nationality: sponsor.nationality || '',
|
country: employer.employer_profile?.country || 'UAE',
|
||||||
address: sponsor.address || '',
|
subscription_status: employer.subscription_status || 'none'
|
||||||
subscription_plan: sponsor.subscription_plan || 'premium',
|
|
||||||
subscription_status: sponsor.subscription_status || 'active'
|
|
||||||
});
|
});
|
||||||
setIsEditOpen(true);
|
setIsEditOpen(true);
|
||||||
};
|
};
|
||||||
@ -174,8 +184,8 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
|
|||||||
window.location.href = `/admin/employers/export?${params.toString()}`;
|
window.location.href = `/admin/employers/export?${params.toString()}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const viewDetails = (sponsor) => {
|
const viewDetails = (employer) => {
|
||||||
setSelectedSponsor(sponsor);
|
setSelectedEmployer(employer);
|
||||||
setIsDetailsOpen(true);
|
setIsDetailsOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -189,7 +199,7 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
|
|||||||
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm flex items-center justify-between">
|
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest block mb-1">Total Employers</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest block mb-1">Total Employers</span>
|
||||||
<h3 className="text-2xl font-black text-slate-900">{sponsors.total || 0}</h3>
|
<h3 className="text-2xl font-black text-slate-900">{employers.total || 0}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 bg-teal-50 rounded-xl">
|
<div className="p-3 bg-teal-50 rounded-xl">
|
||||||
<Users className="w-6 h-6 text-teal-600" />
|
<Users className="w-6 h-6 text-teal-600" />
|
||||||
@ -199,7 +209,7 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
|
|||||||
<div>
|
<div>
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest block mb-1">Active Plans</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest block mb-1">Active Plans</span>
|
||||||
<h3 className="text-2xl font-black text-blue-600">
|
<h3 className="text-2xl font-black text-blue-600">
|
||||||
{sponsors.data.filter(s => s.subscription_status === 'active').length}
|
{employers.data.filter(e => e.subscription_status === 'active').length}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 bg-blue-50 rounded-xl">
|
<div className="p-3 bg-blue-50 rounded-xl">
|
||||||
@ -210,7 +220,7 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
|
|||||||
<div>
|
<div>
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest block mb-1">Pending Approval</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest block mb-1">Pending Approval</span>
|
||||||
<h3 className="text-2xl font-black text-amber-500">
|
<h3 className="text-2xl font-black text-amber-500">
|
||||||
{sponsors.data.filter(s => !s.is_verified).length}
|
{employers.data.filter(e => e.employer_profile?.verification_status !== 'approved').length}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 bg-amber-50 rounded-xl">
|
<div className="p-3 bg-amber-50 rounded-xl">
|
||||||
@ -221,7 +231,7 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
|
|||||||
<div>
|
<div>
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest block mb-1">Suspended Accounts</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest block mb-1">Suspended Accounts</span>
|
||||||
<h3 className="text-2xl font-black text-rose-600">
|
<h3 className="text-2xl font-black text-rose-600">
|
||||||
{sponsors.data.filter(s => s.status === 'suspended').length}
|
{employers.data.filter(e => e.status === 'suspended').length}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 bg-rose-50 rounded-xl">
|
<div className="p-3 bg-rose-50 rounded-xl">
|
||||||
@ -237,7 +247,7 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
|
|||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search by name, email, or mobile..."
|
placeholder="Search by name, company, email..."
|
||||||
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"
|
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"
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={e => setSearchTerm(e.target.value)}
|
onChange={e => setSearchTerm(e.target.value)}
|
||||||
@ -252,20 +262,8 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
|
|||||||
>
|
>
|
||||||
<option value="All">All Statuses</option>
|
<option value="All">All Statuses</option>
|
||||||
<option value="Active">Active</option>
|
<option value="Active">Active</option>
|
||||||
<option value="Inactive">Inactive</option>
|
|
||||||
<option value="Suspended">Suspended</option>
|
<option value="Suspended">Suspended</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select
|
|
||||||
className="px-4 py-2.5 bg-white border border-slate-200 rounded-xl text-xs font-bold text-slate-600 outline-none focus:ring-2 focus:ring-teal-500/10 transition-all cursor-pointer"
|
|
||||||
value={locationFilter}
|
|
||||||
onChange={e => handleFilterChange(statusFilter, e.target.value)}
|
|
||||||
>
|
|
||||||
<option value="All">All Locations</option>
|
|
||||||
<option value="Dubai">Dubai</option>
|
|
||||||
<option value="Abu Dhabi">Abu Dhabi</option>
|
|
||||||
<option value="Sharjah">Sharjah</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
@ -279,17 +277,15 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Dynamic Employers Table */}
|
{/* Table */}
|
||||||
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
|
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow className="bg-slate-50/50 hover:bg-slate-50/50">
|
<TableRow className="bg-slate-50/50 hover:bg-slate-50/50">
|
||||||
<TableHead onClick={() => handleSort('full_name')} className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14 pl-8 cursor-pointer select-none">
|
<TableHead onClick={() => handleSort('name')} className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14 pl-8 cursor-pointer select-none">
|
||||||
Employer {sortField === 'full_name' && (sortOrder === 'desc' ? '↓' : '↑')}
|
Employer {sortField === 'name' && (sortOrder === 'desc' ? '↓' : '↑')}
|
||||||
</TableHead>
|
|
||||||
<TableHead onClick={() => handleSort('city')} className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14 cursor-pointer select-none">
|
|
||||||
Location {sortField === 'city' && (sortOrder === 'desc' ? '↓' : '↑')}
|
|
||||||
</TableHead>
|
</TableHead>
|
||||||
|
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14">Company Name</TableHead>
|
||||||
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14">Subscription Plan</TableHead>
|
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14">Subscription Plan</TableHead>
|
||||||
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14">Verification Status</TableHead>
|
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14">Verification Status</TableHead>
|
||||||
<TableHead onClick={() => handleSort('status')} className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14 cursor-pointer select-none">
|
<TableHead onClick={() => handleSort('status')} className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14 cursor-pointer select-none">
|
||||||
@ -299,77 +295,54 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{sponsors.data.length > 0 ? (
|
{employers.data.length > 0 ? (
|
||||||
sponsors.data.map((sponsor) => (
|
employers.data.map((emp) => (
|
||||||
<TableRow key={sponsor.id} className="hover:bg-slate-50/50 transition-colors">
|
<TableRow key={emp.id} className="hover:bg-slate-50/50 transition-colors">
|
||||||
<TableCell className="py-5 pl-8">
|
<TableCell className="py-5 pl-8">
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<div className="w-10 h-10 rounded-xl bg-[#0F6E56]/10 text-[#0F6E56] flex items-center justify-center border border-[#0F6E56]/20 font-bold text-sm">
|
<div className="w-10 h-10 rounded-xl bg-[#0F6E56]/10 text-[#0F6E56] flex items-center justify-center border border-[#0F6E56]/20 font-bold text-sm">
|
||||||
{sponsor.full_name.charAt(0)}
|
{emp.name?.charAt(0)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-sm font-bold text-slate-900 leading-none mb-1">{sponsor.full_name}</h4>
|
<h4 className="text-sm font-bold text-slate-900 leading-none mb-1">{emp.name}</h4>
|
||||||
<p className="text-xs text-slate-500 font-medium">{sponsor.email}</p>
|
<p className="text-xs text-slate-500 font-medium">{emp.email}</p>
|
||||||
<p className="text-[10px] text-slate-400 font-medium">{sponsor.mobile}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex items-center space-x-2 text-slate-600">
|
<div className="text-xs font-bold text-slate-700">
|
||||||
<MapPin className="w-3.5 h-3.5 text-slate-400" />
|
{emp.employer_profile?.company_name || 'N/A'}
|
||||||
<span className="text-xs font-bold">{sponsor.city || 'Dubai'}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex flex-col space-y-1">
|
<span className={`px-2 py-0.5 rounded-lg text-[10px] font-black uppercase tracking-tight ${
|
||||||
<span className={`px-2 py-0.5 rounded-lg text-[10px] font-black uppercase tracking-tight w-max ${
|
emp.subscription_status === 'active' ? 'bg-blue-50 text-blue-600 border border-blue-100' : 'bg-slate-100 text-slate-500'
|
||||||
sponsor.subscription_plan ? 'bg-blue-50 text-blue-600 border border-blue-100' : 'bg-slate-100 text-slate-500'
|
}`}>
|
||||||
}`}>
|
{emp.subscription_status || 'none'}
|
||||||
{sponsor.subscription_plan ? `${sponsor.subscription_plan} Pass` : 'No Plan'}
|
</span>
|
||||||
</span>
|
|
||||||
<span className={`text-[9px] font-bold ${
|
|
||||||
sponsor.subscription_status === 'active' ? 'text-emerald-600' : 'text-slate-400'
|
|
||||||
}`}>
|
|
||||||
Status: {sponsor.subscription_status.toUpperCase()}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex items-center space-x-1.5">
|
<span className={`text-xs font-bold uppercase ${
|
||||||
{sponsor.is_verified ? (
|
emp.employer_profile?.verification_status === 'approved' ? 'text-emerald-600' : 'text-amber-500'
|
||||||
<>
|
}`}>
|
||||||
<ShieldCheck className="w-4 h-4 text-emerald-500" />
|
{emp.employer_profile?.verification_status || 'pending'}
|
||||||
<span className="text-xs font-bold text-emerald-600">OTP Verified</span>
|
</span>
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Clock className="w-4 h-4 text-amber-500" />
|
|
||||||
<span className="text-xs font-bold text-amber-600">Unverified</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex items-center space-x-2">
|
<span className={`text-[10px] font-black uppercase tracking-widest ${
|
||||||
{sponsor.status === 'active' && <CheckCircle className="w-4 h-4 text-emerald-500" />}
|
emp.status === 'active' ? 'text-emerald-600' : 'text-rose-600'
|
||||||
{sponsor.status === 'inactive' && <Clock className="w-4 h-4 text-amber-500" />}
|
}`}>
|
||||||
{sponsor.status === 'suspended' && <XCircle className="w-4 h-4 text-rose-500" />}
|
{emp.status}
|
||||||
<span className={`text-[10px] font-black uppercase tracking-widest ${
|
</span>
|
||||||
sponsor.status === 'active' ? 'text-emerald-600' :
|
|
||||||
sponsor.status === 'inactive' ? 'text-amber-600' : 'text-rose-600'
|
|
||||||
}`}>
|
|
||||||
{sponsor.status}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right pr-8">
|
<TableCell className="text-right pr-8">
|
||||||
<div className="flex items-center justify-end space-x-1">
|
<div className="flex items-center justify-end space-x-1">
|
||||||
<button
|
<button
|
||||||
onClick={() => viewDetails(sponsor)}
|
onClick={() => viewDetails(emp)}
|
||||||
className="p-2 hover:bg-slate-100 rounded-lg text-[#0F6E56] font-bold"
|
className="p-2 hover:bg-slate-100 rounded-lg text-[#0F6E56] font-bold"
|
||||||
title="View Details"
|
title="View Details"
|
||||||
>
|
>
|
||||||
<ExternalLink className="w-4.5 h-4.5" />
|
<Eye className="w-4.5 h-4.5" />
|
||||||
</button>
|
</button>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
@ -382,24 +355,24 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
|
|||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="p-3 rounded-lg focus:bg-blue-50 cursor-pointer text-blue-600 font-bold"
|
className="p-3 rounded-lg focus:bg-blue-50 cursor-pointer text-blue-600 font-bold"
|
||||||
onClick={() => handleEditClick(sponsor)}
|
onClick={() => handleEditClick(emp)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<Edit2 className="w-4 h-4" />
|
<Edit2 className="w-4 h-4" />
|
||||||
<span className="text-xs font-bold">Edit Employer</span>
|
<span className="text-xs font-bold">Edit Employer</span>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
{!sponsor.is_verified && (
|
{emp.employer_profile?.verification_status !== 'approved' && (
|
||||||
<DropdownMenuItem className="p-3 rounded-lg focus:bg-emerald-50 cursor-pointer text-emerald-600 font-bold" onClick={() => handleApprove(sponsor.id)}>
|
<DropdownMenuItem className="p-3 rounded-lg focus:bg-emerald-50 cursor-pointer text-emerald-600 font-bold" onClick={() => handleApprove(emp.id)}>
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<CheckCircle className="w-4 h-4" />
|
<CheckCircle className="w-4 h-4" />
|
||||||
<span className="text-xs font-bold">Verify Employer</span>
|
<span className="text-xs font-bold">Verify Employer</span>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
{sponsor.status === 'active' ? (
|
{emp.status === 'active' ? (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => handleSuspend(sponsor.id)}
|
onClick={() => handleSuspend(emp.id)}
|
||||||
className="p-3 rounded-lg focus:bg-rose-50 cursor-pointer text-rose-600 font-bold"
|
className="p-3 rounded-lg focus:bg-rose-50 cursor-pointer text-rose-600 font-bold"
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
@ -409,7 +382,7 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
) : (
|
) : (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => handleActivate(sponsor.id)}
|
onClick={() => handleActivate(emp.id)}
|
||||||
className="p-3 rounded-lg focus:bg-emerald-50 cursor-pointer text-emerald-600 font-bold"
|
className="p-3 rounded-lg focus:bg-emerald-50 cursor-pointer text-emerald-600 font-bold"
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
@ -420,8 +393,8 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
|
|||||||
)}
|
)}
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => handleDelete(sponsor.id)}
|
onClick={() => handleDelete(emp.id)}
|
||||||
className="p-3 rounded-lg focus:bg-red-50 cursor-pointer text-red-600 font-bold animate-pulse"
|
className="p-3 rounded-lg focus:bg-red-50 cursor-pointer text-red-600 font-bold"
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<Trash2 className="w-4 h-4" />
|
<Trash2 className="w-4 h-4" />
|
||||||
@ -437,7 +410,7 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
|
|||||||
) : (
|
) : (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={6} className="py-12 text-center text-slate-400 font-semibold text-sm">
|
<TableCell colSpan={6} className="py-12 text-center text-slate-400 font-semibold text-sm">
|
||||||
No employers match the active search/filters criteria.
|
No employers found.
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
)}
|
||||||
@ -447,30 +420,30 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
|
|||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
<div className="p-5 border-t border-slate-100 flex items-center justify-between bg-slate-50/50">
|
<div className="p-5 border-t border-slate-100 flex items-center justify-between bg-slate-50/50">
|
||||||
<span className="text-xs text-slate-500 font-bold">
|
<span className="text-xs text-slate-500 font-bold">
|
||||||
{sponsors.total > 0 ? (
|
{employers.total > 0 ? (
|
||||||
`Showing ${sponsors.from || 0} to ${sponsors.to || 0} of ${sponsors.total} employers`
|
`Showing ${employers.from || 0} to ${employers.to || 0} of ${employers.total} employers`
|
||||||
) : (
|
) : (
|
||||||
'No employers found'
|
'No employers found'
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
{sponsors.last_page > 1 && (
|
{employers.last_page > 1 && (
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Link
|
<Link
|
||||||
href={sponsors.prev_page_url || '#'}
|
href={employers.prev_page_url || '#'}
|
||||||
only={['sponsors']}
|
only={['employers']}
|
||||||
preserveScroll
|
preserveScroll
|
||||||
className={`px-4 py-2 bg-white border border-slate-200 rounded-xl text-xs font-bold text-slate-700 shadow-xs hover:bg-slate-50 transition-colors ${
|
className={`px-4 py-2 bg-white border border-slate-200 rounded-xl text-xs font-bold text-slate-700 shadow-xs hover:bg-slate-50 transition-colors ${
|
||||||
!sponsors.prev_page_url && 'opacity-50 pointer-events-none'
|
!employers.prev_page_url && 'opacity-50 pointer-events-none'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Previous
|
Previous
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href={sponsors.next_page_url || '#'}
|
href={employers.next_page_url || '#'}
|
||||||
only={['sponsors']}
|
only={['employers']}
|
||||||
preserveScroll
|
preserveScroll
|
||||||
className={`px-4 py-2 bg-white border border-slate-200 rounded-xl text-xs font-bold text-slate-700 shadow-xs hover:bg-slate-50 transition-colors ${
|
className={`px-4 py-2 bg-white border border-slate-200 rounded-xl text-xs font-bold text-slate-700 shadow-xs hover:bg-slate-50 transition-colors ${
|
||||||
!sponsors.next_page_url && 'opacity-50 pointer-events-none'
|
!employers.next_page_url && 'opacity-50 pointer-events-none'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Next
|
Next
|
||||||
@ -481,142 +454,127 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Advanced Employer Detail Modal */}
|
{/* Employer Detail Modal */}
|
||||||
<Dialog open={isDetailsOpen} onOpenChange={setIsDetailsOpen}>
|
<Dialog open={isDetailsOpen} onOpenChange={setIsDetailsOpen}>
|
||||||
<DialogContent className="sm:max-w-4xl w-full bg-white p-0 overflow-hidden rounded-[24px] border-none shadow-2xl font-sans max-h-[85vh] flex flex-col">
|
<DialogContent className="sm:max-w-xl w-full bg-white p-0 overflow-hidden rounded-[24px] max-h-[85vh] flex flex-col">
|
||||||
<div className="bg-gradient-to-r from-[#0F6E56] to-[#085041] p-6 text-white relative flex-shrink-0">
|
<div className="bg-gradient-to-r from-[#0F6E56] to-[#085041] p-6 text-white flex-shrink-0">
|
||||||
<div className="relative z-10 flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<div className="w-14 h-14 rounded-2xl bg-white/10 backdrop-blur-md flex items-center justify-center border border-white/20 font-black text-xl">
|
<div className="w-14 h-14 rounded-2xl bg-white/10 backdrop-blur-md flex items-center justify-center border border-white/20 font-black text-xl">
|
||||||
{selectedSponsor?.full_name.charAt(0)}
|
{selectedEmployer?.name?.charAt(0)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-black tracking-tight">{selectedSponsor?.full_name}</h2>
|
<h2 className="text-xl font-black tracking-tight">{selectedEmployer?.name}</h2>
|
||||||
<p className="text-teal-100/80 text-xs font-semibold mt-1">📍 {selectedSponsor?.city || 'Dubai'} • Registered {new Date(selectedSponsor?.created_at).toLocaleDateString()}</p>
|
<p className="text-teal-100/80 text-xs font-semibold mt-1">📍 Registered {new Date(selectedEmployer?.created_at).toLocaleDateString()}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-6 space-y-6 overflow-y-auto flex-1 bg-slate-50/50">
|
<div className="p-6 space-y-6 overflow-y-auto flex-1 bg-slate-50/50">
|
||||||
{/* Verification info */}
|
{/* Employer Info Card */}
|
||||||
<div className="bg-white p-5 border border-slate-200 rounded-2xl shadow-sm space-y-4">
|
<div className="bg-white p-5 border border-slate-200 rounded-2xl shadow-xs space-y-4">
|
||||||
<div className="flex items-center justify-between border-b border-slate-100 pb-2">
|
<h3 className="text-xs font-black text-slate-700 uppercase tracking-widest border-b border-slate-100 pb-2 flex items-center gap-2">
|
||||||
<h3 className="text-xs font-black text-slate-700 uppercase tracking-widest flex items-center gap-1.5">
|
<Users className="w-4 h-4 text-[#0F6E56]" />
|
||||||
<Award className="w-4.5 h-4.5 text-[#0F6E56]" />
|
Employer Profile
|
||||||
<span>UAE Employer Details</span>
|
</h3>
|
||||||
</h3>
|
<div className="grid grid-cols-2 gap-4 text-xs">
|
||||||
<Badge className="bg-teal-50 text-[#0F6E56] border-none font-black text-[9px] uppercase px-3 py-1 rounded-lg">
|
<div>
|
||||||
Employer ID: EMP-{selectedSponsor?.id}
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Company / Household</span>
|
||||||
</Badge>
|
<span className="font-bold text-slate-800">{selectedEmployer?.employer_profile?.company_name || 'N/A'}</span>
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
<div className="text-xs font-bold text-slate-600 space-y-3">
|
|
||||||
<div className="flex items-center gap-2 text-slate-800">
|
|
||||||
<Mail className="w-4 h-4 text-slate-400" />
|
|
||||||
<span>Email: {selectedSponsor?.email}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-slate-800">
|
|
||||||
<Phone className="w-4 h-4 text-slate-400" />
|
|
||||||
<span>Mobile: {selectedSponsor?.mobile}</span>
|
|
||||||
</div>
|
|
||||||
{selectedSponsor?.address && (
|
|
||||||
<div className="flex items-center gap-2 text-slate-800">
|
|
||||||
<MapPin className="w-4 h-4 text-slate-400" />
|
|
||||||
<span>Address: {selectedSponsor?.address}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs font-bold text-slate-600 space-y-3">
|
<div>
|
||||||
<div>OTP Verification: {selectedSponsor?.is_verified ? 'Passed' : 'Pending'}</div>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Email Address</span>
|
||||||
{selectedSponsor?.otp_verified_at && (
|
<span className="font-bold text-slate-800">{selectedEmployer?.email || 'N/A'}</span>
|
||||||
<div>Verified At: {new Date(selectedSponsor.otp_verified_at).toLocaleString()}</div>
|
</div>
|
||||||
)}
|
<div>
|
||||||
<div>Nationality: {selectedSponsor?.nationality || 'N/A'}</div>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Phone Number</span>
|
||||||
|
<span className="font-bold text-slate-800">{selectedEmployer?.employer_profile?.phone || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Country / Location</span>
|
||||||
|
<span className="font-bold text-slate-800">{selectedEmployer?.employer_profile?.country || 'United Arab Emirates'}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Subscription monitoring */}
|
{/* Subscription Plan Card */}
|
||||||
<div className="bg-white p-5 border border-slate-200 rounded-2xl shadow-sm space-y-3">
|
<div className="bg-white p-5 border border-slate-200 rounded-2xl shadow-xs space-y-4">
|
||||||
<h3 className="text-xs font-black text-slate-700 uppercase tracking-widest flex items-center gap-1.5 border-b border-slate-100 pb-2">
|
<h3 className="text-xs font-black text-slate-700 uppercase tracking-widest border-b border-slate-100 pb-2 flex items-center gap-2">
|
||||||
<BadgeDollarSign className="w-4.5 h-4.5 text-blue-600" />
|
<BadgeDollarSign className="w-4 h-4 text-[#0F6E56]" />
|
||||||
<span>Premium Subscription Status</span>
|
Chosen Subscription Plan
|
||||||
</h3>
|
</h3>
|
||||||
<div className="grid grid-cols-2 gap-4 text-xs font-bold text-slate-700">
|
<div className="grid grid-cols-2 gap-4 text-xs">
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[9px] text-slate-400 uppercase tracking-widest block font-black mb-1">Active Plan</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Current Plan</span>
|
||||||
<span className="text-sm font-black text-blue-700 bg-blue-50 px-2.5 py-1 rounded-lg border border-blue-100/50 w-fit block capitalize">
|
<span className={`inline-block px-2 py-0.5 rounded-md text-[10px] font-black uppercase tracking-tight ${
|
||||||
{selectedSponsor?.subscription_plan || 'No Active Plan'}
|
selectedEmployer?.subscription_status === 'active'
|
||||||
|
? 'bg-blue-50 text-blue-600 border border-blue-100'
|
||||||
|
: 'bg-slate-100 text-slate-500'
|
||||||
|
}`}>
|
||||||
|
{selectedEmployer?.subscription_status === 'active'
|
||||||
|
? (selectedEmployer?.sponsor?.subscription_plan || 'Active Premium Plan')
|
||||||
|
: 'No Active Plan'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[9px] text-slate-400 uppercase tracking-widest block font-black mb-1">Payment Status</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Payment Status</span>
|
||||||
<span className="inline-flex items-center text-slate-800 bg-slate-100 px-2.5 py-1 rounded-lg border border-slate-200/50 capitalize">
|
<span className="font-bold text-slate-800 uppercase">{selectedEmployer?.sponsor?.payment_status || 'N/A'}</span>
|
||||||
{selectedSponsor?.payment_status}
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Plan Starts</span>
|
||||||
|
<span className="font-bold text-slate-800">{formatDate(selectedEmployer?.sponsor?.subscription_start_date)}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Plan Expires</span>
|
||||||
|
<span className="font-bold text-slate-800">{formatDate(selectedEmployer?.sponsor?.subscription_end_date)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Document Verification & OCR Extracted Details */}
|
||||||
|
<div className="bg-white p-5 border border-slate-200 rounded-2xl shadow-xs space-y-4">
|
||||||
|
<h3 className="text-xs font-black text-slate-700 uppercase tracking-widest border-b border-slate-100 pb-2 flex items-center gap-2">
|
||||||
|
<ShieldCheck className="w-4 h-4 text-[#0F6E56]" />
|
||||||
|
Emirates ID & Document Extracted Details
|
||||||
|
</h3>
|
||||||
|
<div className="grid grid-cols-2 gap-4 text-xs">
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Emirates ID Number</span>
|
||||||
|
<span className="font-mono font-bold text-slate-800">{selectedEmployer?.employer_profile?.emirates_id_number || 'Not Extracted / Pending'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Emirates ID Expiry</span>
|
||||||
|
<span className="font-bold text-slate-800">{formatDate(selectedEmployer?.employer_profile?.emirates_id_expiry)}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Verification Status</span>
|
||||||
|
<span className={`font-bold uppercase ${
|
||||||
|
selectedEmployer?.employer_profile?.verification_status === 'approved' ? 'text-emerald-600' : 'text-amber-500'
|
||||||
|
}`}>
|
||||||
|
{selectedEmployer?.employer_profile?.verification_status || 'Pending'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{selectedSponsor?.subscription_start_date && (
|
<div>
|
||||||
<div>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Trade License Expiry</span>
|
||||||
<span className="text-[9px] text-slate-400 uppercase tracking-widest block font-black mb-1">Start Date</span>
|
<span className="font-bold text-slate-800">{formatDate(selectedEmployer?.sponsor?.license_expiry)}</span>
|
||||||
<span className="font-mono text-slate-600">{new Date(selectedSponsor.subscription_start_date).toLocaleDateString()}</span>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{selectedSponsor?.subscription_end_date && (
|
|
||||||
<div>
|
|
||||||
<span className="text-[9px] text-slate-400 uppercase tracking-widest block font-black mb-1">Expiry Date</span>
|
|
||||||
<span className="font-mono text-slate-600">{new Date(selectedSponsor.subscription_end_date).toLocaleDateString()}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-6 border-t border-slate-100 bg-slate-50 flex items-center justify-between flex-shrink-0">
|
<div className="p-6 border-t border-slate-100 bg-slate-50 flex items-center justify-end gap-3 flex-shrink-0">
|
||||||
<div className="flex items-center space-x-2">
|
<button
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Verification Action:</span>
|
onClick={() => setIsDetailsOpen(false)}
|
||||||
{!selectedSponsor?.is_verified ? (
|
className="px-5 py-2.5 bg-slate-200 text-slate-700 rounded-xl text-[10px] font-black uppercase tracking-widest hover:bg-slate-300 transition-colors"
|
||||||
<button
|
>
|
||||||
onClick={() => handleApprove(selectedSponsor.id)}
|
Close
|
||||||
className="px-4 py-2.5 bg-emerald-600 text-white rounded-xl text-[10px] font-black uppercase tracking-widest shadow-md shadow-emerald-600/10 hover:bg-emerald-500 transition-colors"
|
</button>
|
||||||
>
|
|
||||||
Verify Employer OTP
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<Badge className="bg-emerald-100 text-emerald-800 border-none font-black uppercase text-[9px] px-3 py-1.5 rounded-lg shadow-sm">
|
|
||||||
OTP Verified
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-3">
|
|
||||||
{selectedSponsor?.status !== 'suspended' ? (
|
|
||||||
<button
|
|
||||||
onClick={() => handleSuspend(selectedSponsor.id)}
|
|
||||||
className="px-4 py-2.5 bg-red-600 text-white rounded-xl text-[10px] font-black uppercase tracking-widest shadow-md shadow-red-600/10 hover:bg-red-700 transition-all"
|
|
||||||
>
|
|
||||||
Suspend Employer
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
onClick={() => handleActivate(selectedSponsor.id)}
|
|
||||||
className="px-4 py-2.5 bg-slate-900 text-white rounded-xl text-[10px] font-black uppercase tracking-widest hover:bg-slate-800 transition-colors"
|
|
||||||
>
|
|
||||||
Activate Employer
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
onClick={() => setIsDetailsOpen(false)}
|
|
||||||
className="px-5 py-2.5 bg-slate-200 text-slate-700 rounded-xl text-[10px] font-black uppercase tracking-widest hover:bg-slate-300 transition-colors"
|
|
||||||
>
|
|
||||||
Close Details
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* Employer Edit Dialog */}
|
{/* Edit Dialog */}
|
||||||
<Dialog open={isEditOpen} onOpenChange={setIsEditOpen}>
|
<Dialog open={isEditOpen} onOpenChange={setIsEditOpen}>
|
||||||
<DialogContent className="sm:max-w-xl w-full bg-white p-6 rounded-[24px] font-sans">
|
<DialogContent className="sm:max-w-xl w-full bg-white p-6 rounded-[24px]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-lg font-black text-slate-800 uppercase tracking-wide">Edit Employer Details</DialogTitle>
|
<DialogTitle className="text-lg font-black text-slate-800 uppercase tracking-wide">Edit Employer Details</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
@ -626,11 +584,20 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm font-semibold focus:ring-4 focus:ring-teal-500/10 outline-none"
|
className="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm font-semibold focus:ring-4 focus:ring-teal-500/10 outline-none"
|
||||||
value={editForm.full_name}
|
value={editForm.name}
|
||||||
onChange={e => setEditForm({ ...editForm, full_name: e.target.value })}
|
onChange={e => setEditForm({ ...editForm, name: e.target.value })}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Company Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm font-semibold focus:ring-4 focus:ring-teal-500/10 outline-none"
|
||||||
|
value={editForm.company_name}
|
||||||
|
onChange={e => setEditForm({ ...editForm, company_name: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Email Address</label>
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Email Address</label>
|
||||||
@ -643,75 +610,15 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Mobile Number</label>
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Phone Number</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm font-semibold focus:ring-4 focus:ring-teal-500/10 outline-none"
|
className="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm font-semibold focus:ring-4 focus:ring-teal-500/10 outline-none"
|
||||||
value={editForm.mobile}
|
value={editForm.phone}
|
||||||
onChange={e => setEditForm({ ...editForm, mobile: e.target.value })}
|
onChange={e => setEditForm({ ...editForm, phone: e.target.value })}
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">City</label>
|
|
||||||
<select
|
|
||||||
className="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm font-semibold focus:ring-4 focus:ring-teal-500/10 outline-none"
|
|
||||||
value={editForm.city}
|
|
||||||
onChange={e => setEditForm({ ...editForm, city: e.target.value })}
|
|
||||||
>
|
|
||||||
<option value="Dubai">Dubai</option>
|
|
||||||
<option value="Abu Dhabi">Abu Dhabi</option>
|
|
||||||
<option value="Sharjah">Sharjah</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Nationality</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm font-semibold focus:ring-4 focus:ring-teal-500/10 outline-none"
|
|
||||||
value={editForm.nationality}
|
|
||||||
onChange={e => setEditForm({ ...editForm, nationality: e.target.value })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Address</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm font-semibold focus:ring-4 focus:ring-teal-500/10 outline-none"
|
|
||||||
value={editForm.address}
|
|
||||||
onChange={e => setEditForm({ ...editForm, address: e.target.value })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-4 border-t border-slate-100 pt-3">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Subscription Plan</label>
|
|
||||||
<select
|
|
||||||
className="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm font-semibold focus:ring-4 focus:ring-teal-500/10 outline-none"
|
|
||||||
value={editForm.subscription_plan}
|
|
||||||
onChange={e => setEditForm({ ...editForm, subscription_plan: e.target.value })}
|
|
||||||
>
|
|
||||||
<option value="basic">Basic Search</option>
|
|
||||||
<option value="premium">Premium Pass</option>
|
|
||||||
<option value="vip">VIP Concierge</option>
|
|
||||||
<option value="">No Active Plan</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Subscription Status</label>
|
|
||||||
<select
|
|
||||||
className="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm font-semibold focus:ring-4 focus:ring-teal-500/10 outline-none"
|
|
||||||
value={editForm.subscription_status}
|
|
||||||
onChange={e => setEditForm({ ...editForm, subscription_status: e.target.value })}
|
|
||||||
>
|
|
||||||
<option value="active">Active</option>
|
|
||||||
<option value="expired">Expired</option>
|
|
||||||
<option value="none">None</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-end space-x-3 pt-4 border-t border-slate-100">
|
<div className="flex items-center justify-end space-x-3 pt-4 border-t border-slate-100">
|
||||||
<button
|
<button
|
||||||
@ -725,7 +632,7 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
|
|||||||
type="submit"
|
type="submit"
|
||||||
className="px-5 py-2.5 bg-[#0F6E56] text-white rounded-xl text-xs font-black uppercase tracking-wider"
|
className="px-5 py-2.5 bg-[#0F6E56] text-white rounded-xl text-xs font-black uppercase tracking-wider"
|
||||||
>
|
>
|
||||||
Save Employer
|
Save Changes
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -14,8 +14,6 @@ import {
|
|||||||
Settings,
|
Settings,
|
||||||
FileText,
|
FileText,
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
Bell,
|
|
||||||
Calendar,
|
|
||||||
Search,
|
Search,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
MoreVertical,
|
MoreVertical,
|
||||||
@ -98,20 +96,6 @@ export default function SafetyHub({ reports, rules, moderation_queue }) {
|
|||||||
<h1 className="text-2xl font-bold text-slate-900 tracking-tight">Safety & Moderation</h1>
|
<h1 className="text-2xl font-bold text-slate-900 tracking-tight">Safety & Moderation</h1>
|
||||||
<p className="text-sm text-slate-500 mt-1">Review and take action on reported content, users and reviews.</p>
|
<p className="text-sm text-slate-500 mt-1">Review and take action on reported content, users and reviews.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Header Controls */}
|
|
||||||
<div className="flex items-center gap-3 self-end md:self-auto">
|
|
||||||
<button className="relative p-2.5 bg-white border border-slate-200 rounded-xl hover:bg-slate-50 transition-colors shadow-sm text-slate-600">
|
|
||||||
<Bell className="w-5 h-5" />
|
|
||||||
<span className="absolute top-1.5 right-1.5 w-4 h-4 bg-red-500 text-white rounded-full text-[9px] font-bold flex items-center justify-center">12</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2 px-4 py-2.5 bg-white border border-slate-200 rounded-xl hover:bg-slate-50 transition-colors shadow-sm text-xs font-semibold text-slate-700 cursor-pointer">
|
|
||||||
<Calendar className="w-4 h-4 text-slate-400" />
|
|
||||||
<span>May 13, 2025 - Jun 11, 2025</span>
|
|
||||||
<ChevronDown className="w-4 h-4 text-slate-400" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Upper Row Metric Cards */}
|
{/* Upper Row Metric Cards */}
|
||||||
|
|||||||
@ -33,13 +33,23 @@
|
|||||||
Route::get('/workers/verifications', [\App\Http\Controllers\Admin\WorkerVerificationController::class, 'index'])->name('admin.workers.verifications');
|
Route::get('/workers/verifications', [\App\Http\Controllers\Admin\WorkerVerificationController::class, 'index'])->name('admin.workers.verifications');
|
||||||
Route::post('/workers/{worker}/verify', [\App\Http\Controllers\Admin\WorkerVerificationController::class, 'verify'])->name('admin.workers.verify');
|
Route::post('/workers/{worker}/verify', [\App\Http\Controllers\Admin\WorkerVerificationController::class, 'verify'])->name('admin.workers.verify');
|
||||||
|
|
||||||
Route::get('/employers', [SponsorController::class, 'index'])->name('admin.employers');
|
// Employer Management Routes
|
||||||
Route::get('/employers/export', [SponsorController::class, 'export'])->name('admin.employers.export');
|
Route::get('/employers', [\App\Http\Controllers\Admin\EmployerController::class, 'index'])->name('admin.employers');
|
||||||
Route::post('/employers/{id}/verify', [SponsorController::class, 'verify'])->name('admin.employers.verify');
|
Route::get('/employers/export', [\App\Http\Controllers\Admin\EmployerController::class, 'export'])->name('admin.employers.export');
|
||||||
Route::post('/employers/{id}/suspend', [SponsorController::class, 'suspend'])->name('admin.employers.suspend');
|
Route::post('/employers/{id}/verify', [\App\Http\Controllers\Admin\EmployerController::class, 'verify'])->name('admin.employers.verify');
|
||||||
Route::post('/employers/{id}/activate', [SponsorController::class, 'activate'])->name('admin.employers.activate');
|
Route::post('/employers/{id}/suspend', [\App\Http\Controllers\Admin\EmployerController::class, 'suspend'])->name('admin.employers.suspend');
|
||||||
Route::post('/employers/{id}/update', [SponsorController::class, 'update'])->name('admin.employers.update');
|
Route::post('/employers/{id}/activate', [\App\Http\Controllers\Admin\EmployerController::class, 'activate'])->name('admin.employers.activate');
|
||||||
Route::delete('/employers/{id}', [SponsorController::class, 'delete'])->name('admin.employers.delete');
|
Route::post('/employers/{id}/update', [\App\Http\Controllers\Admin\EmployerController::class, 'update'])->name('admin.employers.update');
|
||||||
|
Route::delete('/employers/{id}', [\App\Http\Controllers\Admin\EmployerController::class, 'delete'])->name('admin.employers.delete');
|
||||||
|
|
||||||
|
// Charity Organization Management Routes
|
||||||
|
Route::get('/charity-organizations', [\App\Http\Controllers\Admin\SponsorController::class, 'index'])->name('admin.charity-organizations');
|
||||||
|
Route::get('/charity-organizations/export', [\App\Http\Controllers\Admin\SponsorController::class, 'export'])->name('admin.charity-organizations.export');
|
||||||
|
Route::post('/charity-organizations/{id}/verify', [\App\Http\Controllers\Admin\SponsorController::class, 'verify'])->name('admin.charity-organizations.verify');
|
||||||
|
Route::post('/charity-organizations/{id}/suspend', [\App\Http\Controllers\Admin\SponsorController::class, 'suspend'])->name('admin.charity-organizations.suspend');
|
||||||
|
Route::post('/charity-organizations/{id}/activate', [\App\Http\Controllers\Admin\SponsorController::class, 'activate'])->name('admin.charity-organizations.activate');
|
||||||
|
Route::post('/charity-organizations/{id}/update', [\App\Http\Controllers\Admin\SponsorController::class, 'update'])->name('admin.charity-organizations.update');
|
||||||
|
Route::delete('/charity-organizations/{id}', [\App\Http\Controllers\Admin\SponsorController::class, 'delete'])->name('admin.charity-organizations.delete');
|
||||||
|
|
||||||
Route::get('/subscriptions', function () {
|
Route::get('/subscriptions', function () {
|
||||||
return Inertia::render('Admin/Subscriptions/Index');
|
return Inertia::render('Admin/Subscriptions/Index');
|
||||||
|
|||||||
238
tests/Feature/AdminEmployerCharityTest.php
Normal file
238
tests/Feature/AdminEmployerCharityTest.php
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Sponsor;
|
||||||
|
use App\Models\EmployerProfile;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class AdminEmployerCharityTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
protected $admin;
|
||||||
|
protected $employer;
|
||||||
|
protected $charity;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
// 1. Create an Admin user
|
||||||
|
$this->admin = User::create([
|
||||||
|
'name' => 'System Admin',
|
||||||
|
'email' => 'admin@marketplace.com',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
'role' => 'admin',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 2. Create an Employer User
|
||||||
|
$this->employer = User::create([
|
||||||
|
'name' => 'John Employer',
|
||||||
|
'email' => 'john@employer.com',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
'role' => 'employer',
|
||||||
|
]);
|
||||||
|
|
||||||
|
EmployerProfile::create([
|
||||||
|
'user_id' => $this->employer->id,
|
||||||
|
'company_name' => 'John Enterprises',
|
||||||
|
'phone' => '+971501112222',
|
||||||
|
'country' => 'Dubai',
|
||||||
|
'verification_status' => 'pending',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Employers have a matching Sponsor record in our schema
|
||||||
|
Sponsor::create([
|
||||||
|
'full_name' => 'John Employer',
|
||||||
|
'email' => 'john@employer.com',
|
||||||
|
'mobile' => '+971501112222',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
'organization_name' => 'John Enterprises',
|
||||||
|
'city' => 'Dubai',
|
||||||
|
'status' => 'active',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 3. Create a Charity Organization (Sponsor)
|
||||||
|
$this->charity = Sponsor::create([
|
||||||
|
'full_name' => 'Charity Agent',
|
||||||
|
'organization_name' => 'Red Crescent',
|
||||||
|
'email' => 'contact@redcrescent.org',
|
||||||
|
'mobile' => '+971503334444',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
'city' => 'Abu Dhabi',
|
||||||
|
'status' => 'active',
|
||||||
|
'subscription_status' => 'active',
|
||||||
|
'subscription_plan' => 'premium',
|
||||||
|
'is_verified' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test admin can access employers listing page.
|
||||||
|
*/
|
||||||
|
public function test_admin_can_list_employers()
|
||||||
|
{
|
||||||
|
$response = $this->actingAs($this->admin)
|
||||||
|
->withSession(['user' => $this->admin])
|
||||||
|
->get('/admin/employers');
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertSee('John Employer');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test admin can update an employer profile.
|
||||||
|
*/
|
||||||
|
public function test_admin_can_update_employer()
|
||||||
|
{
|
||||||
|
$response = $this->actingAs($this->admin)
|
||||||
|
->withSession(['user' => $this->admin])
|
||||||
|
->post("/admin/employers/{$this->employer->id}/update", [
|
||||||
|
'name' => 'John Employer Updated',
|
||||||
|
'email' => 'john.updated@employer.com',
|
||||||
|
'company_name' => 'John MegaCorp',
|
||||||
|
'phone' => '+971509999999',
|
||||||
|
'country' => 'Abu Dhabi',
|
||||||
|
'subscription_status' => 'active',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(302);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('users', [
|
||||||
|
'id' => $this->employer->id,
|
||||||
|
'name' => 'John Employer Updated',
|
||||||
|
'email' => 'john.updated@employer.com',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('employer_profiles', [
|
||||||
|
'user_id' => $this->employer->id,
|
||||||
|
'company_name' => 'John MegaCorp',
|
||||||
|
'phone' => '+971509999999',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test admin can verify an employer.
|
||||||
|
*/
|
||||||
|
public function test_admin_can_verify_employer()
|
||||||
|
{
|
||||||
|
$response = $this->actingAs($this->admin)
|
||||||
|
->withSession(['user' => $this->admin])
|
||||||
|
->post("/admin/employers/{$this->employer->id}/verify");
|
||||||
|
|
||||||
|
$response->assertStatus(302);
|
||||||
|
$this->assertEquals('approved', $this->employer->fresh()->employerProfile->verification_status);
|
||||||
|
|
||||||
|
$sponsor = Sponsor::where('email', $this->employer->email)->first();
|
||||||
|
$this->assertEquals('active', $sponsor->status);
|
||||||
|
$this->assertTrue((bool)$sponsor->is_verified);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test admin can suspend and activate an employer.
|
||||||
|
*/
|
||||||
|
public function test_admin_can_suspend_and_activate_employer()
|
||||||
|
{
|
||||||
|
// Suspend
|
||||||
|
$response = $this->actingAs($this->admin)
|
||||||
|
->withSession(['user' => $this->admin])
|
||||||
|
->post("/admin/employers/{$this->employer->id}/suspend");
|
||||||
|
|
||||||
|
$response->assertStatus(302);
|
||||||
|
|
||||||
|
$sponsor = Sponsor::where('email', $this->employer->email)->first();
|
||||||
|
$this->assertEquals('suspended', $sponsor->status);
|
||||||
|
|
||||||
|
// Activate
|
||||||
|
$response = $this->actingAs($this->admin)
|
||||||
|
->withSession(['user' => $this->admin])
|
||||||
|
->post("/admin/employers/{$this->employer->id}/activate");
|
||||||
|
|
||||||
|
$response->assertStatus(302);
|
||||||
|
|
||||||
|
$sponsor = Sponsor::where('email', $this->employer->email)->first();
|
||||||
|
$this->assertEquals('active', $sponsor->status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test admin can delete an employer.
|
||||||
|
*/
|
||||||
|
public function test_admin_can_delete_employer()
|
||||||
|
{
|
||||||
|
$response = $this->actingAs($this->admin)
|
||||||
|
->withSession(['user' => $this->admin])
|
||||||
|
->delete("/admin/employers/{$this->employer->id}");
|
||||||
|
|
||||||
|
$response->assertStatus(302);
|
||||||
|
$this->assertDatabaseMissing('users', ['id' => $this->employer->id]);
|
||||||
|
$this->assertDatabaseMissing('employer_profiles', ['user_id' => $this->employer->id]);
|
||||||
|
$this->assertDatabaseMissing('sponsors', ['email' => $this->employer->email]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test admin can access charity organizations listing page.
|
||||||
|
*/
|
||||||
|
public function test_admin_can_list_charities()
|
||||||
|
{
|
||||||
|
$response = $this->actingAs($this->admin)
|
||||||
|
->withSession(['user' => $this->admin])
|
||||||
|
->get('/admin/charity-organizations');
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertSee('Red Crescent');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test admin can update a charity organization.
|
||||||
|
*/
|
||||||
|
public function test_admin_can_update_charity()
|
||||||
|
{
|
||||||
|
$response = $this->actingAs($this->admin)
|
||||||
|
->withSession(['user' => $this->admin])
|
||||||
|
->post("/admin/charity-organizations/{$this->charity->id}/update", [
|
||||||
|
'full_name' => 'Charity Agent Updated',
|
||||||
|
'organization_name' => 'Red Crescent UAE',
|
||||||
|
'email' => 'contact.ae@redcrescent.org',
|
||||||
|
'mobile' => '+971505555555',
|
||||||
|
'city' => 'Sharjah',
|
||||||
|
'nationality' => 'Emirati',
|
||||||
|
'address' => 'Corniche Road',
|
||||||
|
'subscription_plan' => 'vip',
|
||||||
|
'subscription_status' => 'active',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(302);
|
||||||
|
$this->assertDatabaseHas('sponsors', [
|
||||||
|
'id' => $this->charity->id,
|
||||||
|
'full_name' => 'Charity Agent Updated',
|
||||||
|
'organization_name' => 'Red Crescent UAE',
|
||||||
|
'email' => 'contact.ae@redcrescent.org',
|
||||||
|
'mobile' => '+971505555555',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test admin can suspend and activate a charity organization.
|
||||||
|
*/
|
||||||
|
public function test_admin_can_suspend_and_activate_charity()
|
||||||
|
{
|
||||||
|
// Suspend
|
||||||
|
$response = $this->actingAs($this->admin)
|
||||||
|
->withSession(['user' => $this->admin])
|
||||||
|
->post("/admin/charity-organizations/{$this->charity->id}/suspend");
|
||||||
|
|
||||||
|
$response->assertStatus(302);
|
||||||
|
$this->assertEquals('suspended', $this->charity->fresh()->status);
|
||||||
|
|
||||||
|
// Activate
|
||||||
|
$response = $this->actingAs($this->admin)
|
||||||
|
->withSession(['user' => $this->admin])
|
||||||
|
->post("/admin/charity-organizations/{$this->charity->id}/activate");
|
||||||
|
|
||||||
|
$response->assertStatus(302);
|
||||||
|
$this->assertEquals('active', $this->charity->fresh()->status);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -64,7 +64,8 @@ public function test_sponsor_can_register_with_license()
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$sponsor = Sponsor::where('mobile', '+971509990001')->first();
|
$sponsor = Sponsor::where('mobile', '+971509990001')->first();
|
||||||
$this->assertNotNull($sponsor->emirates_id_file);
|
$this->assertNull($sponsor->emirates_id_file);
|
||||||
|
$this->assertNull($sponsor->license_file);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -105,6 +105,39 @@ public function test_s1_otp_flow()
|
|||||||
$this->assertTrue(Cache::get('worker_otp_verified_' . $phone));
|
$this->assertTrue(Cache::get('worker_otp_verified_' . $phone));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test S1 Onboard: Send OTP fails if mobile number already exists.
|
||||||
|
*/
|
||||||
|
public function test_s1_otp_flow_fails_if_phone_already_exists()
|
||||||
|
{
|
||||||
|
Worker::create([
|
||||||
|
'name' => 'Existing Worker',
|
||||||
|
'email' => 'existing@example.com',
|
||||||
|
'phone' => '+971501234567',
|
||||||
|
'language' => 'HI',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
'nationality' => 'Indian',
|
||||||
|
'status' => 'active',
|
||||||
|
'age' => 25,
|
||||||
|
'salary' => 1500,
|
||||||
|
'availability' => 'Immediate',
|
||||||
|
'experience' => 'None',
|
||||||
|
'religion' => 'Not Specified',
|
||||||
|
'bio' => 'Test worker',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->postJson('/api/workers/send-otp', [
|
||||||
|
'phone' => '+971501234567',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(422)
|
||||||
|
->assertJson([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Validation error.',
|
||||||
|
])
|
||||||
|
->assertJsonValidationErrors(['phone']);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test S1 Onboard: Complete Basic Profile Setup.
|
* Test S1 Onboard: Complete Basic Profile Setup.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user