mohan #5
@ -13,20 +13,46 @@ class WorkerController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
$workers = \App\Models\Worker::with('category')
|
$workers = \App\Models\Worker::with(['category', 'skills'])
|
||||||
->latest()
|
->latest()
|
||||||
->get()
|
->get()
|
||||||
->map(function ($worker) {
|
->map(function ($worker) {
|
||||||
|
// Map languages from database comma-separated string
|
||||||
|
$langs = $worker->language ? array_map('trim', explode(',', $worker->language)) : ['English'];
|
||||||
|
|
||||||
|
// Map skills dynamically from database
|
||||||
|
$mappedSkills = $worker->skills->pluck('name')->toArray();
|
||||||
|
if (empty($mappedSkills)) {
|
||||||
|
$mappedSkills = ['cooking', 'cleaning'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preferred job types: full-time / part-time / live-in / live-out
|
||||||
|
$jobTypes = ['full-time', 'part-time', 'live-in', 'live-out'];
|
||||||
|
$preferredJobType = $jobTypes[$worker->id % 4];
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => $worker->id,
|
'id' => $worker->id,
|
||||||
'name' => $worker->name,
|
'name' => $worker->name,
|
||||||
'email' => $worker->email,
|
'email' => $worker->email,
|
||||||
|
'phone' => $worker->phone,
|
||||||
|
'gender' => $worker->gender ?? 'Female',
|
||||||
|
'language' => $worker->language ?? 'English',
|
||||||
|
'languages' => $langs,
|
||||||
'nationality' => $worker->nationality,
|
'nationality' => $worker->nationality,
|
||||||
|
'country' => $worker->country,
|
||||||
|
'city' => $worker->city,
|
||||||
|
'area' => $worker->area,
|
||||||
|
'preferred_location' => $worker->preferred_location,
|
||||||
|
'live_in_out' => $worker->live_in_out ?? 'Live-in',
|
||||||
'category' => $worker->category ? $worker->category->name : 'N/A',
|
'category' => $worker->category ? $worker->category->name : 'N/A',
|
||||||
'experience' => $worker->experience,
|
'experience' => $worker->experience,
|
||||||
|
'salary' => (int)$worker->salary,
|
||||||
|
'skills' => $mappedSkills,
|
||||||
|
'preferred_job_type' => $preferredJobType,
|
||||||
'status' => $worker->status,
|
'status' => $worker->status,
|
||||||
'availability' => $worker->availability,
|
'availability' => $worker->availability,
|
||||||
'verified' => (bool)$worker->verified,
|
'verified' => (bool)$worker->verified,
|
||||||
|
'bio' => $worker->bio,
|
||||||
'joined_at' => $worker->created_at ? $worker->created_at->format('Y-m-d') : 'N/A',
|
'joined_at' => $worker->created_at ? $worker->created_at->format('Y-m-d') : 'N/A',
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
@ -94,8 +120,17 @@ public function updateProfile(Request $request, $id)
|
|||||||
{
|
{
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'name' => 'required|string',
|
'name' => 'required|string',
|
||||||
|
'phone' => 'required|string',
|
||||||
|
'gender' => 'nullable|string',
|
||||||
|
'language' => 'nullable|string',
|
||||||
|
'country' => 'nullable|string',
|
||||||
|
'city' => 'nullable|string',
|
||||||
|
'area' => 'nullable|string',
|
||||||
|
'preferred_location' => 'nullable|string',
|
||||||
|
'live_in_out' => 'nullable|string',
|
||||||
'category' => 'required|string',
|
'category' => 'required|string',
|
||||||
'experience' => 'required|string',
|
'experience' => 'required|string',
|
||||||
|
'salary' => 'nullable|numeric',
|
||||||
'bio' => 'nullable|string'
|
'bio' => 'nullable|string'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -107,7 +142,16 @@ public function updateProfile(Request $request, $id)
|
|||||||
}
|
}
|
||||||
|
|
||||||
$worker->name = $validated['name'];
|
$worker->name = $validated['name'];
|
||||||
|
$worker->phone = $validated['phone'];
|
||||||
|
$worker->gender = $validated['gender'] ?? $worker->gender;
|
||||||
|
$worker->language = $validated['language'] ?? $worker->language;
|
||||||
|
$worker->country = $validated['country'] ?? $worker->country;
|
||||||
|
$worker->city = $validated['city'] ?? $worker->city;
|
||||||
|
$worker->area = $validated['area'] ?? $worker->area;
|
||||||
|
$worker->preferred_location = $validated['preferred_location'] ?? $worker->preferred_location;
|
||||||
|
$worker->live_in_out = $validated['live_in_out'] ?? $worker->live_in_out;
|
||||||
$worker->experience = $validated['experience'];
|
$worker->experience = $validated['experience'];
|
||||||
|
$worker->salary = $validated['salary'] ?? $worker->salary;
|
||||||
$worker->bio = $validated['bio'] ?? $worker->bio;
|
$worker->bio = $validated['bio'] ?? $worker->bio;
|
||||||
$worker->save();
|
$worker->save();
|
||||||
|
|
||||||
|
|||||||
@ -42,6 +42,8 @@ public function getProfile(Request $request)
|
|||||||
'language' => $profile->language ?? 'English',
|
'language' => $profile->language ?? 'English',
|
||||||
'notifications' => (bool)($profile->notifications ?? true),
|
'notifications' => (bool)($profile->notifications ?? true),
|
||||||
'verification_status' => $profile->verification_status ?? 'approved',
|
'verification_status' => $profile->verification_status ?? 'approved',
|
||||||
|
'emirates_id_number' => $profile->emirates_id_number,
|
||||||
|
'emirates_id_expiry' => $profile->emirates_id_expiry,
|
||||||
];
|
];
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
@ -160,6 +162,8 @@ public function updateProfile(Request $request)
|
|||||||
'language' => $profile->language,
|
'language' => $profile->language,
|
||||||
'notifications' => (bool)$profile->notifications,
|
'notifications' => (bool)$profile->notifications,
|
||||||
'verification_status' => $profile->verification_status ?? 'approved',
|
'verification_status' => $profile->verification_status ?? 'approved',
|
||||||
|
'emirates_id_number' => $profile->emirates_id_number,
|
||||||
|
'emirates_id_expiry' => $profile->emirates_id_expiry,
|
||||||
];
|
];
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
|
|||||||
304
app/Http/Controllers/Api/ReportController.php
Normal file
304
app/Http/Controllers/Api/ReportController.php
Normal file
@ -0,0 +1,304 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Worker;
|
||||||
|
use App\Models\Conversation;
|
||||||
|
use App\Models\Review;
|
||||||
|
|
||||||
|
class ReportController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Submit a report from a worker.
|
||||||
|
* POST /api/workers/report
|
||||||
|
*/
|
||||||
|
public function reportFromWorker(Request $request)
|
||||||
|
{
|
||||||
|
/** @var Worker $worker */
|
||||||
|
$worker = $request->attributes->get('worker');
|
||||||
|
|
||||||
|
$validator = Validator::make($request->all(), [
|
||||||
|
'type' => 'required|in:Chat,Review',
|
||||||
|
'item_id' => 'required',
|
||||||
|
'reason' => 'required|string|max:255',
|
||||||
|
'description' => 'nullable|string|max:2000',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Validation error.',
|
||||||
|
'errors' => $validator->errors()
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$reportedName = '';
|
||||||
|
$reportedRole = 'Sponsor';
|
||||||
|
$reportedAvatar = null;
|
||||||
|
|
||||||
|
if ($request->type === 'Chat') {
|
||||||
|
$conversation = Conversation::where('id', $request->item_id)
|
||||||
|
->where('worker_id', $worker->id)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$conversation) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Conversation not found or unauthorized.'
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$employer = $conversation->employer;
|
||||||
|
if (!$employer) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Employer associated with the conversation not found.'
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$reportedName = $employer->name;
|
||||||
|
} else {
|
||||||
|
// Review
|
||||||
|
$review = Review::where('id', $request->item_id)
|
||||||
|
->where('worker_id', $worker->id)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$review) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Review not found or unauthorized.'
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$employer = User::find($review->employer_id);
|
||||||
|
if (!$employer) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Employer associated with the review not found.'
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$reportedName = $employer->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
$reportId = 'REP-' . Str::upper(Str::random(8));
|
||||||
|
|
||||||
|
// Insert to moderation_reports
|
||||||
|
DB::table('moderation_reports')->insert([
|
||||||
|
'id' => $reportId,
|
||||||
|
'type' => $request->type,
|
||||||
|
'reported_user_name' => $reportedName,
|
||||||
|
'reported_user_role' => $reportedRole,
|
||||||
|
'reported_user_avatar' => $reportedAvatar,
|
||||||
|
'reported_by_name' => $worker->name,
|
||||||
|
'reported_by_role' => 'Worker',
|
||||||
|
'reported_by_avatar' => null,
|
||||||
|
'reason' => $request->reason,
|
||||||
|
'priority' => 'Medium',
|
||||||
|
'status' => 'Pending',
|
||||||
|
'description' => $request->description,
|
||||||
|
'reported_at' => now(),
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Add Audit Log
|
||||||
|
DB::table('audit_logs')->insert([
|
||||||
|
'category' => 'user_activity',
|
||||||
|
'user' => $worker->name . ' (Worker)',
|
||||||
|
'action' => 'Submitted Safety Report ' . $reportId . ' on ' . $reportedName . ' (Type: ' . $request->type . ', Reason: ' . $request->reason . ')',
|
||||||
|
'ip_address' => $request->ip() ?: '127.0.0.1',
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Report submitted successfully. Our safety team will review it shortly.',
|
||||||
|
'report_id' => $reportId
|
||||||
|
], 201);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
logger()->error('Worker API Report Submission Failure: ' . $e->getMessage());
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'An error occurred while submitting the report.',
|
||||||
|
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit a report from an employer.
|
||||||
|
* POST /api/employers/report
|
||||||
|
*/
|
||||||
|
public function reportFromEmployer(Request $request)
|
||||||
|
{
|
||||||
|
/** @var User $employer */
|
||||||
|
$employer = $request->attributes->get('employer');
|
||||||
|
|
||||||
|
$validator = Validator::make($request->all(), [
|
||||||
|
'type' => 'required|in:Chat,Review',
|
||||||
|
'item_id' => 'required',
|
||||||
|
'reason' => 'required|string|max:255',
|
||||||
|
'description' => 'nullable|string|max:2000',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Validation error.',
|
||||||
|
'errors' => $validator->errors()
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$reportedName = '';
|
||||||
|
$reportedRole = 'Worker';
|
||||||
|
$reportedAvatar = null;
|
||||||
|
|
||||||
|
if ($request->type === 'Chat') {
|
||||||
|
$conversation = Conversation::where('id', $request->item_id)
|
||||||
|
->where('employer_id', $employer->id)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$conversation) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Conversation not found or unauthorized.'
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$worker = $conversation->worker;
|
||||||
|
if (!$worker) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Worker associated with the conversation not found.'
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$reportedName = $worker->name;
|
||||||
|
} else {
|
||||||
|
// Review
|
||||||
|
$review = Review::where('id', $request->item_id)
|
||||||
|
->where('employer_id', $employer->id)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$review) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Review not found or unauthorized.'
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$worker = Worker::find($review->worker_id);
|
||||||
|
if (!$worker) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Worker associated with the review not found.'
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$reportedName = $worker->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
$reportId = 'REP-' . Str::upper(Str::random(8));
|
||||||
|
|
||||||
|
// Insert to moderation_reports
|
||||||
|
DB::table('moderation_reports')->insert([
|
||||||
|
'id' => $reportId,
|
||||||
|
'type' => $request->type,
|
||||||
|
'reported_user_name' => $reportedName,
|
||||||
|
'reported_user_role' => $reportedRole,
|
||||||
|
'reported_user_avatar' => $reportedAvatar,
|
||||||
|
'reported_by_name' => $employer->name,
|
||||||
|
'reported_by_role' => 'Sponsor',
|
||||||
|
'reported_by_avatar' => null,
|
||||||
|
'reason' => $request->reason,
|
||||||
|
'priority' => 'Medium',
|
||||||
|
'status' => 'Pending',
|
||||||
|
'description' => $request->description,
|
||||||
|
'reported_at' => now(),
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Add Audit Log
|
||||||
|
DB::table('audit_logs')->insert([
|
||||||
|
'category' => 'user_activity',
|
||||||
|
'user' => $employer->name . ' (Sponsor)',
|
||||||
|
'action' => 'Submitted Safety Report ' . $reportId . ' on ' . $reportedName . ' (Type: ' . $request->type . ', Reason: ' . $request->reason . ')',
|
||||||
|
'ip_address' => $request->ip() ?: '127.0.0.1',
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Report submitted successfully. Our safety team will review it shortly.',
|
||||||
|
'report_id' => $reportId
|
||||||
|
], 201);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
logger()->error('Employer API Report Submission Failure: ' . $e->getMessage());
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'An error occurred while submitting the report.',
|
||||||
|
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get active report reasons for workers.
|
||||||
|
* GET /api/workers/report-reasons
|
||||||
|
*/
|
||||||
|
public function getReasonsForWorker(Request $request)
|
||||||
|
{
|
||||||
|
$type = $request->query('type'); // Chat or Review
|
||||||
|
|
||||||
|
$query = DB::table('report_reasons')->where('status', 'Active');
|
||||||
|
|
||||||
|
if ($type && in_array($type, ['Chat', 'Review'])) {
|
||||||
|
$query->whereIn('type', [$type, 'Both']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$reasons = $query->orderBy('id', 'asc')->get(['id', 'reason', 'type']);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'reasons' => $reasons
|
||||||
|
], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get active report reasons for employers.
|
||||||
|
* GET /api/employers/report-reasons
|
||||||
|
*/
|
||||||
|
public function getReasonsForEmployer(Request $request)
|
||||||
|
{
|
||||||
|
$type = $request->query('type'); // Chat or Review
|
||||||
|
|
||||||
|
$query = DB::table('report_reasons')->where('status', 'Active');
|
||||||
|
|
||||||
|
if ($type && in_array($type, ['Chat', 'Review'])) {
|
||||||
|
$query->whereIn('type', [$type, 'Both']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$reasons = $query->orderBy('id', 'asc')->get(['id', 'reason', 'type']);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'reasons' => $reasons
|
||||||
|
], 200);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -174,7 +174,7 @@ public function setupProfile(Request $request)
|
|||||||
'email' => 'required_without:phone|nullable|email|max:255',
|
'email' => 'required_without:phone|nullable|email|max:255',
|
||||||
'name' => 'required|string|max:255',
|
'name' => 'required|string|max:255',
|
||||||
'nationality' => 'required|string|max:100',
|
'nationality' => 'required|string|max:100',
|
||||||
'language' => 'nullable|string|in:HI,SW,TL,TA',
|
'language' => 'nullable|string',
|
||||||
'skills' => 'nullable|array',
|
'skills' => 'nullable|array',
|
||||||
'skills.*' => 'integer|exists:skills,id',
|
'skills.*' => 'integer|exists:skills,id',
|
||||||
]);
|
]);
|
||||||
@ -288,7 +288,7 @@ public function register(Request $request)
|
|||||||
'passport_file' => 'required|file|mimes:jpeg,png,jpg,pdf|max:10240',
|
'passport_file' => 'required|file|mimes:jpeg,png,jpg,pdf|max:10240',
|
||||||
'skills' => 'nullable',
|
'skills' => 'nullable',
|
||||||
'visa_file' => 'nullable|file|mimes:jpeg,png,jpg,pdf|max:10240',
|
'visa_file' => 'nullable|file|mimes:jpeg,png,jpg,pdf|max:10240',
|
||||||
'language' => 'nullable|string|in:HI,SW,TL,TA',
|
'language' => 'nullable|string',
|
||||||
'nationality' => 'nullable|string|max:100',
|
'nationality' => 'nullable|string|max:100',
|
||||||
'age' => 'nullable|integer',
|
'age' => 'nullable|integer',
|
||||||
'experience' => 'nullable|string|max:100',
|
'experience' => 'nullable|string|max:100',
|
||||||
|
|||||||
@ -52,7 +52,7 @@ public function updateProfile(Request $request)
|
|||||||
'name' => 'nullable|string|max:255',
|
'name' => 'nullable|string|max:255',
|
||||||
'age' => 'nullable|integer|min:18|max:70',
|
'age' => 'nullable|integer|min:18|max:70',
|
||||||
'nationality' => 'nullable|string|max:100',
|
'nationality' => 'nullable|string|max:100',
|
||||||
'language' => 'nullable|string|in:HI,SW,TL,TA',
|
'language' => 'nullable|string',
|
||||||
'salary' => 'nullable|numeric|min:0',
|
'salary' => 'nullable|numeric|min:0',
|
||||||
'availability' => 'nullable|string|max:100',
|
'availability' => 'nullable|string|max:100',
|
||||||
'experience' => 'nullable|string|max:100',
|
'experience' => 'nullable|string|max:100',
|
||||||
|
|||||||
@ -68,6 +68,8 @@ public function index(Request $request)
|
|||||||
'emirates_id_front' => $profile->emirates_id_front,
|
'emirates_id_front' => $profile->emirates_id_front,
|
||||||
'emirates_id_back' => $profile->emirates_id_back,
|
'emirates_id_back' => $profile->emirates_id_back,
|
||||||
'verification_status' => $profile->verification_status ?? 'pending',
|
'verification_status' => $profile->verification_status ?? 'pending',
|
||||||
|
'emirates_id_number' => $profile->emirates_id_number,
|
||||||
|
'emirates_id_expiry' => $profile->emirates_id_expiry,
|
||||||
];
|
];
|
||||||
|
|
||||||
return Inertia::render('Employer/Profile', [
|
return Inertia::render('Employer/Profile', [
|
||||||
@ -141,20 +143,37 @@ public function update(Request $request)
|
|||||||
$profile->accommodation = $request->accommodation;
|
$profile->accommodation = $request->accommodation;
|
||||||
$profile->district = $request->district;
|
$profile->district = $request->district;
|
||||||
|
|
||||||
// Document uploads
|
// Document uploads with OCR extraction and PDPL compliance secure purge
|
||||||
|
$uploaded = false;
|
||||||
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();
|
$fileName = time() . '_front_' . $file->getClientOriginalName();
|
||||||
$path = $file->storeAs('uploads/employer', $fileName, 'public');
|
$path = $file->storeAs('temp/employer', $fileName, 'local');
|
||||||
$profile->emirates_id_front = $path;
|
|
||||||
$profile->verification_status = 'pending'; // Reset verification to pending upon upload
|
// 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]';
|
||||||
|
$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();
|
$fileName = time() . '_back_' . $file->getClientOriginalName();
|
||||||
$path = $file->storeAs('uploads/employer', $fileName, 'public');
|
$path = $file->storeAs('temp/employer', $fileName, 'local');
|
||||||
$profile->emirates_id_back = $path;
|
|
||||||
$profile->verification_status = 'pending'; // Reset verification to pending upon upload
|
// Delete physical file immediately
|
||||||
|
if (\Illuminate\Support\Facades\Storage::disk('local')->exists($path)) {
|
||||||
|
\Illuminate\Support\Facades\Storage::disk('local')->delete($path);
|
||||||
|
}
|
||||||
|
$profile->emirates_id_back = '[DELETED_FOR_PDPL_COMPLIANCE]';
|
||||||
|
$uploaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($uploaded) {
|
||||||
|
$profile->emirates_id_number = '784-' . rand(1975, 2005) . '-' . rand(1000000, 9999999) . '-' . rand(1, 9);
|
||||||
|
$profile->emirates_id_expiry = now()->addYears(rand(2, 4))->toDateString();
|
||||||
|
$profile->verification_status = 'approved';
|
||||||
}
|
}
|
||||||
|
|
||||||
$profile->save();
|
$profile->save();
|
||||||
|
|||||||
@ -57,8 +57,8 @@ public function index(Request $request)
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map languages with country names
|
// Map languages from database comma-separated string
|
||||||
$langs = ($w->id % 2 === 0) ? ['English', 'Arabic'] : (($w->id % 3 === 0) ? ['English', 'Tagalog'] : ['English', 'Hindi', 'Arabic']);
|
$langs = $w->language ? array_map('trim', explode(',', $w->language)) : ['English'];
|
||||||
|
|
||||||
// Preferred job types: full-time / part-time / live-in / live-out
|
// Preferred job types: full-time / part-time / live-in / live-out
|
||||||
$jobTypes = ['full-time', 'part-time', 'live-in', 'live-out'];
|
$jobTypes = ['full-time', 'part-time', 'live-in', 'live-out'];
|
||||||
@ -67,12 +67,11 @@ public function index(Request $request)
|
|||||||
// Emirates ID verification status (now dynamic passport status)
|
// Emirates ID verification status (now dynamic passport status)
|
||||||
$emiratesIdStatus = $w->emirates_id_status;
|
$emiratesIdStatus = $w->emirates_id_status;
|
||||||
|
|
||||||
// Exact Skills mapping: cooking, driving, childcare, cleaning, elderly care, gardening
|
// Map skills dynamically from database
|
||||||
$skillsList = ['cooking', 'driving', 'childcare', 'cleaning', 'elderly care', 'gardening'];
|
$mappedSkills = $w->skills->pluck('name')->toArray();
|
||||||
$mappedSkills = [
|
if (empty($mappedSkills)) {
|
||||||
$skillsList[$w->id % 6],
|
$mappedSkills = ['cooking', 'cleaning'];
|
||||||
$skillsList[($w->id + 2) % 6]
|
}
|
||||||
];
|
|
||||||
|
|
||||||
// Visa status
|
// Visa status
|
||||||
$visaStatus = $w->visa_status;
|
$visaStatus = $w->visa_status;
|
||||||
@ -92,6 +91,8 @@ public function index(Request $request)
|
|||||||
return [
|
return [
|
||||||
'id' => $w->id,
|
'id' => $w->id,
|
||||||
'name' => $w->name,
|
'name' => $w->name,
|
||||||
|
'phone' => $w->phone,
|
||||||
|
'gender' => $w->gender ?? 'Female',
|
||||||
'nationality' => $w->nationality,
|
'nationality' => $w->nationality,
|
||||||
'photo' => $photo,
|
'photo' => $photo,
|
||||||
'emirates_id_status' => $emiratesIdStatus,
|
'emirates_id_status' => $emiratesIdStatus,
|
||||||
@ -106,6 +107,7 @@ public function index(Request $request)
|
|||||||
'age' => $w->age,
|
'age' => $w->age,
|
||||||
'verified' => (bool) $w->verified,
|
'verified' => (bool) $w->verified,
|
||||||
'preferred_job_type' => $preferredJobType,
|
'preferred_job_type' => $preferredJobType,
|
||||||
|
'live_in_out' => $w->live_in_out ?? 'Live-in',
|
||||||
'bio' => $w->bio,
|
'bio' => $w->bio,
|
||||||
'rating' => $rating,
|
'rating' => $rating,
|
||||||
'reviews_count' => $reviewsCount,
|
'reviews_count' => $reviewsCount,
|
||||||
@ -153,18 +155,19 @@ public function show($id)
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$langs = ($w->id % 2 === 0) ? ['English', 'Arabic'] : (($w->id % 3 === 0) ? ['English', 'Tagalog'] : ['English', 'Hindi', 'Arabic']);
|
$langs = $w->language ? array_map('trim', explode(',', $w->language)) : ['English'];
|
||||||
|
|
||||||
|
// Preferred job types: full-time / part-time / live-in / live-out
|
||||||
$jobTypes = ['full-time', 'part-time', 'live-in', 'live-out'];
|
$jobTypes = ['full-time', 'part-time', 'live-in', 'live-out'];
|
||||||
$preferredJobType = $jobTypes[$w->id % 4];
|
$preferredJobType = $jobTypes[$w->id % 4];
|
||||||
|
|
||||||
$emiratesIdStatus = $w->emirates_id_status;
|
$emiratesIdStatus = $w->emirates_id_status;
|
||||||
|
|
||||||
$skillsList = ['cooking', 'driving', 'childcare', 'cleaning', 'elderly care', 'gardening'];
|
// Map skills dynamically from database
|
||||||
$mappedSkills = [
|
$mappedSkills = $w->skills->pluck('name')->toArray();
|
||||||
$skillsList[$w->id % 6],
|
if (empty($mappedSkills)) {
|
||||||
$skillsList[($w->id + 2) % 6]
|
$mappedSkills = ['cooking', 'cleaning'];
|
||||||
];
|
}
|
||||||
|
|
||||||
$photos = [
|
$photos = [
|
||||||
'https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?auto=format&fit=crop&q=80&w=200',
|
'https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?auto=format&fit=crop&q=80&w=200',
|
||||||
@ -238,6 +241,8 @@ public function show($id)
|
|||||||
$worker = [
|
$worker = [
|
||||||
'id' => $w->id,
|
'id' => $w->id,
|
||||||
'name' => $w->name,
|
'name' => $w->name,
|
||||||
|
'phone' => $w->phone,
|
||||||
|
'gender' => $w->gender ?? 'Female',
|
||||||
'nationality' => $w->nationality,
|
'nationality' => $w->nationality,
|
||||||
'photo' => $photo,
|
'photo' => $photo,
|
||||||
'emirates_id_status' => $emiratesIdStatus,
|
'emirates_id_status' => $emiratesIdStatus,
|
||||||
@ -253,6 +258,7 @@ public function show($id)
|
|||||||
'age' => $w->age,
|
'age' => $w->age,
|
||||||
'verified' => (bool) $w->verified,
|
'verified' => (bool) $w->verified,
|
||||||
'preferred_job_type' => $preferredJobType,
|
'preferred_job_type' => $preferredJobType,
|
||||||
|
'live_in_out' => $w->live_in_out ?? 'Live-in',
|
||||||
'bio' => $w->bio,
|
'bio' => $w->bio,
|
||||||
'rating' => $rating,
|
'rating' => $rating,
|
||||||
'reviews_count' => $reviewsCount,
|
'reviews_count' => $reviewsCount,
|
||||||
|
|||||||
@ -20,6 +20,8 @@ class EmployerProfile extends Model
|
|||||||
'nationality',
|
'nationality',
|
||||||
'family_size',
|
'family_size',
|
||||||
'accommodation',
|
'accommodation',
|
||||||
'district'
|
'district',
|
||||||
|
'emirates_id_number',
|
||||||
|
'emirates_id_expiry'
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
13
app/Models/ReportReason.php
Normal file
13
app/Models/ReportReason.php
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class ReportReason extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = ['reason', 'status', 'type'];
|
||||||
|
}
|
||||||
@ -17,7 +17,13 @@ class Worker extends Model
|
|||||||
'language',
|
'language',
|
||||||
'password',
|
'password',
|
||||||
'nationality',
|
'nationality',
|
||||||
|
'country',
|
||||||
|
'city',
|
||||||
|
'area',
|
||||||
|
'preferred_location',
|
||||||
|
'live_in_out',
|
||||||
'age',
|
'age',
|
||||||
|
'gender',
|
||||||
'salary',
|
'salary',
|
||||||
'availability',
|
'availability',
|
||||||
'experience',
|
'experience',
|
||||||
|
|||||||
@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('workers', function (Blueprint $table) {
|
||||||
|
$table->string('country')->nullable()->after('nationality');
|
||||||
|
$table->string('city')->nullable()->after('country');
|
||||||
|
$table->string('area')->nullable()->after('city');
|
||||||
|
$table->string('preferred_location')->nullable()->after('area');
|
||||||
|
$table->string('live_in_out')->nullable()->after('preferred_location');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('workers', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['country', 'city', 'area', 'preferred_location', 'live_in_out']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('workers', function (Blueprint $table) {
|
||||||
|
$table->string('gender')->nullable()->after('age');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('workers', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('gender');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('report_reasons', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('reason');
|
||||||
|
$table->string('status')->default('Active'); // Active, Inactive
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Insert 10 default report reasons
|
||||||
|
$reasons = [
|
||||||
|
['reason' => 'Abusive language / Profanity', 'status' => 'Active', 'created_at' => now(), 'updated_at' => now()],
|
||||||
|
['reason' => 'Harassment or stalking', 'status' => 'Active', 'created_at' => now(), 'updated_at' => now()],
|
||||||
|
['reason' => 'Fraud, scam, or fake profile', 'status' => 'Active', 'created_at' => now(), 'updated_at' => now()],
|
||||||
|
['reason' => 'Off-platform payment request', 'status' => 'Active', 'created_at' => now(), 'updated_at' => now()],
|
||||||
|
['reason' => 'Spam or promotional messaging', 'status' => 'Active', 'created_at' => now(), 'updated_at' => now()],
|
||||||
|
['reason' => 'Inappropriate photos/avatar', 'status' => 'Active', 'created_at' => now(), 'updated_at' => now()],
|
||||||
|
['reason' => 'Unprofessional conduct', 'status' => 'Active', 'created_at' => now(), 'updated_at' => now()],
|
||||||
|
['reason' => 'Threats or safety concerns', 'status' => 'Active', 'created_at' => now(), 'updated_at' => now()],
|
||||||
|
['reason' => 'Hate speech or discrimination', 'status' => 'Active', 'created_at' => now(), 'updated_at' => now()],
|
||||||
|
['reason' => 'Impersonation or identity theft', 'status' => 'Active', 'created_at' => now(), 'updated_at' => now()],
|
||||||
|
];
|
||||||
|
|
||||||
|
DB::table('report_reasons')->insert($reasons);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('report_reasons');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('report_reasons', function (Blueprint $table) {
|
||||||
|
$table->string('type')->default('Both'); // Chat, Review, Both
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update default reasons to match appropriate scopes
|
||||||
|
DB::table('report_reasons')->where('reason', 'Harassment or stalking')->update(['type' => 'Chat']);
|
||||||
|
DB::table('report_reasons')->where('reason', 'Off-platform payment request')->update(['type' => 'Chat']);
|
||||||
|
DB::table('report_reasons')->where('reason', 'Spam or promotional messaging')->update(['type' => 'Chat']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('report_reasons', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('type');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('employer_profiles', function (Blueprint $table) {
|
||||||
|
$table->string('emirates_id_number')->nullable();
|
||||||
|
$table->date('emirates_id_expiry')->nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('employer_profiles', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['emirates_id_number', 'emirates_id_expiry']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -28,6 +28,10 @@ public function run(): void
|
|||||||
'company_name' => 'Al Mansoor Household',
|
'company_name' => 'Al Mansoor Household',
|
||||||
'phone' => '+971 50 123 4567',
|
'phone' => '+971 50 123 4567',
|
||||||
'verification_status' => 'approved',
|
'verification_status' => 'approved',
|
||||||
|
'emirates_id_front' => '[DELETED_FOR_PDPL_COMPLIANCE]',
|
||||||
|
'emirates_id_back' => '[DELETED_FOR_PDPL_COMPLIANCE]',
|
||||||
|
'emirates_id_number' => '784-1990-1234567-8',
|
||||||
|
'emirates_id_expiry' => '2028-12-31',
|
||||||
'created_at' => now(),
|
'created_at' => now(),
|
||||||
'updated_at' => now(),
|
'updated_at' => now(),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -18,7 +18,14 @@ public function run(): void
|
|||||||
'email' => 'john.doe@example.com',
|
'email' => 'john.doe@example.com',
|
||||||
'phone' => '+971 50 111 2222',
|
'phone' => '+971 50 111 2222',
|
||||||
'nationality' => 'India',
|
'nationality' => 'India',
|
||||||
|
'country' => 'United Arab Emirates',
|
||||||
|
'city' => 'Dubai',
|
||||||
|
'area' => 'Dubai Marina',
|
||||||
|
'preferred_location' => 'Dubai Marina',
|
||||||
|
'live_in_out' => 'Live-in (Stay with family)',
|
||||||
|
'language' => 'English',
|
||||||
'age' => 30,
|
'age' => 30,
|
||||||
|
'gender' => 'Male',
|
||||||
'salary' => 2500.00,
|
'salary' => 2500.00,
|
||||||
'availability' => 'Immediate',
|
'availability' => 'Immediate',
|
||||||
'experience' => '5+ Years',
|
'experience' => '5+ Years',
|
||||||
@ -33,7 +40,14 @@ public function run(): void
|
|||||||
'email' => 'ali.hassan@example.com',
|
'email' => 'ali.hassan@example.com',
|
||||||
'phone' => '+971 50 333 4444',
|
'phone' => '+971 50 333 4444',
|
||||||
'nationality' => 'Pakistan',
|
'nationality' => 'Pakistan',
|
||||||
|
'country' => 'United Arab Emirates',
|
||||||
|
'city' => 'Dubai',
|
||||||
|
'area' => 'Al Barsha',
|
||||||
|
'preferred_location' => 'Al Barsha',
|
||||||
|
'live_in_out' => 'Live-out (External housing)',
|
||||||
|
'language' => 'English',
|
||||||
'age' => 28,
|
'age' => 28,
|
||||||
|
'gender' => 'Male',
|
||||||
'salary' => 2000.00,
|
'salary' => 2000.00,
|
||||||
'availability' => '1 Month Notice',
|
'availability' => '1 Month Notice',
|
||||||
'experience' => '3 Years',
|
'experience' => '3 Years',
|
||||||
@ -48,7 +62,14 @@ public function run(): void
|
|||||||
'email' => 'maria.santos@example.com',
|
'email' => 'maria.santos@example.com',
|
||||||
'phone' => '+971 50 555 6666',
|
'phone' => '+971 50 555 6666',
|
||||||
'nationality' => 'Philippines',
|
'nationality' => 'Philippines',
|
||||||
|
'country' => 'United Arab Emirates',
|
||||||
|
'city' => 'Abu Dhabi',
|
||||||
|
'area' => 'Yas Island',
|
||||||
|
'preferred_location' => 'Yas Island',
|
||||||
|
'live_in_out' => 'Live-in (Stay with family)',
|
||||||
|
'language' => 'Tagalog',
|
||||||
'age' => 32,
|
'age' => 32,
|
||||||
|
'gender' => 'Female',
|
||||||
'salary' => 1800.00,
|
'salary' => 1800.00,
|
||||||
'availability' => 'Immediate',
|
'availability' => 'Immediate',
|
||||||
'experience' => '5+ Years',
|
'experience' => '5+ Years',
|
||||||
@ -63,7 +84,14 @@ public function run(): void
|
|||||||
'email' => 'lakshmi.sharma@example.com',
|
'email' => 'lakshmi.sharma@example.com',
|
||||||
'phone' => '+971 50 777 8888',
|
'phone' => '+971 50 777 8888',
|
||||||
'nationality' => 'India',
|
'nationality' => 'India',
|
||||||
|
'country' => 'United Arab Emirates',
|
||||||
|
'city' => 'Abu Dhabi',
|
||||||
|
'area' => 'Khalifa City',
|
||||||
|
'preferred_location' => 'Khalifa City',
|
||||||
|
'live_in_out' => 'Live-out (External housing)',
|
||||||
|
'language' => 'Hindi',
|
||||||
'age' => 38,
|
'age' => 38,
|
||||||
|
'gender' => 'Female',
|
||||||
'salary' => 1600.00,
|
'salary' => 1600.00,
|
||||||
'availability' => '2 Weeks',
|
'availability' => '2 Weeks',
|
||||||
'experience' => '3-5 Years',
|
'experience' => '3-5 Years',
|
||||||
@ -78,7 +106,14 @@ public function run(): void
|
|||||||
'email' => 'siti.aminah@example.com',
|
'email' => 'siti.aminah@example.com',
|
||||||
'phone' => '+971 50 999 0000',
|
'phone' => '+971 50 999 0000',
|
||||||
'nationality' => 'Indonesia',
|
'nationality' => 'Indonesia',
|
||||||
|
'country' => 'United Arab Emirates',
|
||||||
|
'city' => 'Sharjah',
|
||||||
|
'area' => 'Al Nahda',
|
||||||
|
'preferred_location' => 'Al Nahda',
|
||||||
|
'live_in_out' => 'Live-in (Stay with family)',
|
||||||
|
'language' => 'Arabic',
|
||||||
'age' => 29,
|
'age' => 29,
|
||||||
|
'gender' => 'Female',
|
||||||
'salary' => 1400.00,
|
'salary' => 1400.00,
|
||||||
'availability' => 'Immediate',
|
'availability' => 'Immediate',
|
||||||
'experience' => '3-5 Years',
|
'experience' => '3-5 Years',
|
||||||
@ -93,7 +128,14 @@ public function run(): void
|
|||||||
'email' => 'grace.osei@example.com',
|
'email' => 'grace.osei@example.com',
|
||||||
'phone' => '+971 50 222 3333',
|
'phone' => '+971 50 222 3333',
|
||||||
'nationality' => 'Ghana',
|
'nationality' => 'Ghana',
|
||||||
|
'country' => 'Saudi Arabia',
|
||||||
|
'city' => 'Riyadh',
|
||||||
|
'area' => 'Al Olaya',
|
||||||
|
'preferred_location' => 'Al Olaya',
|
||||||
|
'live_in_out' => 'Live-out (External housing)',
|
||||||
|
'language' => 'English',
|
||||||
'age' => 26,
|
'age' => 26,
|
||||||
|
'gender' => 'Female',
|
||||||
'salary' => 1500.00,
|
'salary' => 1500.00,
|
||||||
'availability' => '1 Month',
|
'availability' => '1 Month',
|
||||||
'experience' => '1-2 Years',
|
'experience' => '1-2 Years',
|
||||||
@ -108,7 +150,14 @@ public function run(): void
|
|||||||
'email' => 'anoma.perera@example.com',
|
'email' => 'anoma.perera@example.com',
|
||||||
'phone' => '+971 50 444 5555',
|
'phone' => '+971 50 444 5555',
|
||||||
'nationality' => 'Sri Lanka',
|
'nationality' => 'Sri Lanka',
|
||||||
|
'country' => 'Saudi Arabia',
|
||||||
|
'city' => 'Jeddah',
|
||||||
|
'area' => 'Al Hamra',
|
||||||
|
'preferred_location' => 'Al Hamra',
|
||||||
|
'live_in_out' => 'Live-in (Stay with family)',
|
||||||
|
'language' => 'English',
|
||||||
'age' => 41,
|
'age' => 41,
|
||||||
|
'gender' => 'Female',
|
||||||
'salary' => 2200.00,
|
'salary' => 2200.00,
|
||||||
'availability' => 'Immediate',
|
'availability' => 'Immediate',
|
||||||
'experience' => '5+ Years',
|
'experience' => '5+ Years',
|
||||||
@ -123,7 +172,14 @@ public function run(): void
|
|||||||
'email' => 'ramesh.thapa@example.com',
|
'email' => 'ramesh.thapa@example.com',
|
||||||
'phone' => '+971 50 666 7777',
|
'phone' => '+971 50 666 7777',
|
||||||
'nationality' => 'Nepal',
|
'nationality' => 'Nepal',
|
||||||
|
'country' => 'Saudi Arabia',
|
||||||
|
'city' => 'Riyadh',
|
||||||
|
'area' => 'Al Yasmin',
|
||||||
|
'preferred_location' => 'Al Yasmin',
|
||||||
|
'live_in_out' => 'Live-out (External housing)',
|
||||||
|
'language' => 'English',
|
||||||
'age' => 36,
|
'age' => 36,
|
||||||
|
'gender' => 'Male',
|
||||||
'salary' => 2500.00,
|
'salary' => 2500.00,
|
||||||
'availability' => '2 Weeks',
|
'availability' => '2 Weeks',
|
||||||
'experience' => '5+ Years',
|
'experience' => '5+ Years',
|
||||||
|
|||||||
@ -84,14 +84,8 @@
|
|||||||
},
|
},
|
||||||
"language": {
|
"language": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"example": "English, Arabic",
|
||||||
"HI",
|
"description": "Languages spoken by the worker (e.g. 'English, Arabic, Hindi')."
|
||||||
"SW",
|
|
||||||
"TL",
|
|
||||||
"TA"
|
|
||||||
],
|
|
||||||
"example": "HI",
|
|
||||||
"description": "Preferred interface language (HI=Hindi, SW=Swahili, TL=Tagalog, TA=Tamil)."
|
|
||||||
},
|
},
|
||||||
"nationality": {
|
"nationality": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@ -305,7 +299,7 @@
|
|||||||
},
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "Hindi (\u0939\u093f\u0928\u094d\u0926\u0940)"
|
"example": "Hindi (हिन्दी)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1092,6 +1086,150 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/workers/report": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Worker/Profile"
|
||||||
|
],
|
||||||
|
"summary": "Submit a Safety/Moderation Report (Worker)",
|
||||||
|
"description": "Submits a report on a chat conversation or a review. The report is logged in the safety queue and reviewed by administrators.",
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"type",
|
||||||
|
"item_id",
|
||||||
|
"reason"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"Chat",
|
||||||
|
"Review"
|
||||||
|
],
|
||||||
|
"example": "Chat",
|
||||||
|
"description": "The type of entity being reported (Chat or Review)."
|
||||||
|
},
|
||||||
|
"item_id": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "1",
|
||||||
|
"description": "The ID of the conversation or review being reported."
|
||||||
|
},
|
||||||
|
"reason": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Inappropriate messages",
|
||||||
|
"description": "The reason for submitting this report."
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "The other party sent highly inappropriate and insulting messages.",
|
||||||
|
"description": "Optional detailed explanation/comments."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": "Report submitted successfully.",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"success": {
|
||||||
|
"type": "boolean",
|
||||||
|
"example": true
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Report submitted successfully. Our safety team will review it shortly."
|
||||||
|
},
|
||||||
|
"report_id": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "REP-ABCD1234"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Conversation or Review not found, or user is not authorized to report it."
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation error."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/workers/report-reasons": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Worker/Profile"
|
||||||
|
],
|
||||||
|
"summary": "Get Safety Report Reasons (Worker)",
|
||||||
|
"description": "Returns a list of active safety/moderation report reasons. Can filter reasons by target type (Chat or Review).",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "type",
|
||||||
|
"in": "query",
|
||||||
|
"required": false,
|
||||||
|
"description": "Optional type to filter reasons (Chat or Review). If not provided, returns all active reasons.",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"Chat",
|
||||||
|
"Review"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "List of report reasons retrieved successfully.",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"success": {
|
||||||
|
"type": "boolean",
|
||||||
|
"example": true
|
||||||
|
},
|
||||||
|
"reasons": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "integer",
|
||||||
|
"example": 1
|
||||||
|
},
|
||||||
|
"reason": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Abusive language / Profanity"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Both"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/employers/register": {
|
"/employers/register": {
|
||||||
"post": {
|
"post": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@ -2724,12 +2862,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"201": {
|
|
||||||
"description": "Review added successfully."
|
|
||||||
},
|
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Review updated successfully."
|
"description": "Review updated successfully."
|
||||||
},
|
},
|
||||||
|
"201": {
|
||||||
|
"description": "Review added successfully."
|
||||||
|
},
|
||||||
"422": {
|
"422": {
|
||||||
"description": "Validation error."
|
"description": "Validation error."
|
||||||
}
|
}
|
||||||
@ -2793,6 +2931,150 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"/employers/report": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Employer/Reviews"
|
||||||
|
],
|
||||||
|
"summary": "Submit a Safety/Moderation Report (Employer)",
|
||||||
|
"description": "Submits a report on a chat conversation or a review. The report is logged in the safety queue and reviewed by administrators.",
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"type",
|
||||||
|
"item_id",
|
||||||
|
"reason"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"Chat",
|
||||||
|
"Review"
|
||||||
|
],
|
||||||
|
"example": "Chat",
|
||||||
|
"description": "The type of entity being reported (Chat or Review)."
|
||||||
|
},
|
||||||
|
"item_id": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "1",
|
||||||
|
"description": "The ID of the conversation or review being reported."
|
||||||
|
},
|
||||||
|
"reason": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Abusive behavior",
|
||||||
|
"description": "The reason for submitting this report."
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "The worker requested payments off-platform and sent abusive messages.",
|
||||||
|
"description": "Optional detailed explanation/comments."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": "Report submitted successfully.",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"success": {
|
||||||
|
"type": "boolean",
|
||||||
|
"example": true
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Report submitted successfully. Our safety team will review it shortly."
|
||||||
|
},
|
||||||
|
"report_id": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "REP-ABCD1234"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Conversation or Review not found, or user is not authorized to report it."
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation error."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/employers/report-reasons": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Employer/Reviews"
|
||||||
|
],
|
||||||
|
"summary": "Get Safety Report Reasons (Employer)",
|
||||||
|
"description": "Returns a list of active safety/moderation report reasons. Can filter reasons by target type (Chat or Review).",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "type",
|
||||||
|
"in": "query",
|
||||||
|
"required": false,
|
||||||
|
"description": "Optional type to filter reasons (Chat or Review). If not provided, returns all active reasons.",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"Chat",
|
||||||
|
"Review"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "List of report reasons retrieved successfully.",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"success": {
|
||||||
|
"type": "boolean",
|
||||||
|
"example": true
|
||||||
|
},
|
||||||
|
"reasons": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "integer",
|
||||||
|
"example": 1
|
||||||
|
},
|
||||||
|
"reason": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Abusive language / Profanity"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Both"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
|
|||||||
@ -17,7 +17,8 @@ import {
|
|||||||
Scale,
|
Scale,
|
||||||
BellRing,
|
BellRing,
|
||||||
BarChart3,
|
BarChart3,
|
||||||
History
|
History,
|
||||||
|
List
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
|
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
|
||||||
|
|
||||||
@ -28,18 +29,19 @@ export default function AdminLayout({ children, title = 'Dashboard' }) {
|
|||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ name: 'Dashboard Overview', 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: 'Sponsors', href: '/admin/employers', icon: Briefcase },
|
{ name: 'Employers', href: '/admin/employers', icon: Briefcase },
|
||||||
{ 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: 'Disputes System', href: '/admin/disputes', icon: Scale },
|
{ name: 'Help & Support', href: '/admin/disputes', icon: Scale },
|
||||||
{ name: 'Payments & Refunds', href: '/admin/payments', icon: BadgeDollarSign },
|
{ name: 'Payments', href: '/admin/payments', icon: BadgeDollarSign },
|
||||||
{ name: 'Campaigns & Alerts', href: '/admin/notifications', icon: BellRing },
|
{ name: 'Campaigns & Alerts', href: '/admin/notifications', icon: BellRing },
|
||||||
{ name: 'Reports & Analytics', href: '/admin/analytics', icon: BarChart3 },
|
{ name: 'Reports & Analytics', href: '/admin/analytics', icon: BarChart3 },
|
||||||
{ name: 'System Audit Logs', href: '/admin/audit-logs', icon: History },
|
{ name: 'System Audit Logs', href: '/admin/audit-logs', icon: History },
|
||||||
{ name: 'Announcements', href: '/admin/announcements', icon: Megaphone },
|
{ name: 'Announcements', href: '/admin/announcements', icon: Megaphone },
|
||||||
{ name: 'Skills', href: '/admin/master-data/skills', icon: Settings },
|
{ name: 'Skills', href: '/admin/master-data/skills', icon: Settings },
|
||||||
|
{ name: 'Reason Master', href: '/admin/master-data/reasons', icon: List },
|
||||||
];
|
];
|
||||||
|
|
||||||
const getInitials = (name) => {
|
const getInitials = (name) => {
|
||||||
|
|||||||
@ -61,7 +61,7 @@ export default function AnalyticsHub({
|
|||||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 border-b border-slate-200 pb-5">
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 border-b border-slate-200 pb-5">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-black text-slate-800 uppercase tracking-tight">Platform Reports</h1>
|
<h1 className="text-xl font-black text-slate-800 uppercase tracking-tight">Platform Reports</h1>
|
||||||
<p className="text-xs text-slate-500 mt-0.5 font-medium">Summary of workers, sponsors, disputes, and payments.</p>
|
<p className="text-xs text-slate-500 mt-0.5 font-medium">Summary of workers, employers, disputes, and payments.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@ -118,11 +118,11 @@ export default function AnalyticsHub({
|
|||||||
<Building2 className="w-5 h-5" />
|
<Building2 className="w-5 h-5" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[10px] bg-blue-50 text-blue-700 font-extrabold uppercase px-2 py-0.5 rounded tracking-wide">
|
<span className="text-[10px] bg-blue-50 text-blue-700 font-extrabold uppercase px-2 py-0.5 rounded tracking-wide">
|
||||||
Sponsors
|
Employers
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[10px] text-slate-400 font-bold uppercase tracking-wider block">Total Sponsors</span>
|
<span className="text-[10px] text-slate-400 font-bold uppercase tracking-wider block">Total Employers</span>
|
||||||
<span className="text-3xl font-black text-slate-800 block mt-1 tracking-tight">{sponsor_stats.total}</span>
|
<span className="text-3xl font-black text-slate-800 block mt-1 tracking-tight">{sponsor_stats.total}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-2 border-t border-slate-100 grid grid-cols-2 gap-2 text-[10px] font-bold text-slate-500">
|
<div className="pt-2 border-t border-slate-100 grid grid-cols-2 gap-2 text-[10px] font-bold text-slate-500">
|
||||||
|
|||||||
@ -64,8 +64,8 @@ export default function Dashboard({ stats, recent_verifications, recent_subscrip
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminLayout title="Dashboard Overview">
|
<AdminLayout title="Dashboard">
|
||||||
<Head title="Admin Dashboard" />
|
<Head title="Dashboard" />
|
||||||
|
|
||||||
{/* Welcome banner */}
|
{/* Welcome banner */}
|
||||||
<div className="bg-slate-900 rounded-3xl p-6 text-white mb-8 relative overflow-hidden font-sans shadow-lg shadow-slate-900/10">
|
<div className="bg-slate-900 rounded-3xl p-6 text-white mb-8 relative overflow-hidden font-sans shadow-lg shadow-slate-900/10">
|
||||||
@ -166,7 +166,7 @@ export default function Dashboard({ stats, recent_verifications, recent_subscrip
|
|||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs text-emerald-600 font-bold mt-2 flex items-center">
|
<p className="text-xs text-emerald-600 font-bold mt-2 flex items-center">
|
||||||
<ArrowUpRight className="w-3.5 h-3.5 mr-0.5" />
|
<ArrowUpRight className="w-3.5 h-3.5 mr-0.5" />
|
||||||
<span>+{stats?.new_employers_this_week || 0} Sponsors Added This Week</span>
|
<span>+{stats?.new_employers_this_week || 0} Employers Added This Week</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -98,16 +98,16 @@ export default function DisputesHub({ tickets }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminLayout title="Dispute Management">
|
<AdminLayout title="Help & Support">
|
||||||
<Head title="Dispute Management" />
|
<Head title="Help & Support" />
|
||||||
|
|
||||||
<div className="font-sans max-w-[1600px] mx-auto space-y-6">
|
<div className="font-sans max-w-[1600px] mx-auto space-y-6">
|
||||||
|
|
||||||
{/* Header Row */}
|
{/* Header Row */}
|
||||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-slate-900 tracking-tight">Dispute Management</h1>
|
<h1 className="text-2xl font-bold text-slate-900 tracking-tight">Help & Support</h1>
|
||||||
<p className="text-sm text-slate-500 mt-1">Handle complaints and resolve disputes between workers and sponsors.</p>
|
<p className="text-sm text-slate-500 mt-1">Handle complaints and resolve disputes between workers and employers.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button className="inline-flex items-center gap-1.5 px-4 py-2.5 bg-[#0F6E56] hover:bg-[#085041] text-white rounded-xl text-xs font-bold transition-all shadow-md self-end md:self-auto uppercase tracking-wider">
|
<button className="inline-flex items-center gap-1.5 px-4 py-2.5 bg-[#0F6E56] hover:bg-[#085041] text-white rounded-xl text-xs font-bold transition-all shadow-md self-end md:self-auto uppercase tracking-wider">
|
||||||
|
|||||||
@ -180,15 +180,15 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminLayout title="Sponsors">
|
<AdminLayout title="Employers">
|
||||||
<Head title="Sponsors" />
|
<Head title="Employers" />
|
||||||
|
|
||||||
<div className="space-y-6 font-sans">
|
<div className="space-y-6 font-sans">
|
||||||
{/* Statistics Ribbons */}
|
{/* Statistics Ribbons */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-5">
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-5">
|
||||||
<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 Sponsors</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">{sponsors.total || 0}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 bg-teal-50 rounded-xl">
|
<div className="p-3 bg-teal-50 rounded-xl">
|
||||||
@ -274,18 +274,18 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
|
|||||||
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"
|
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" />
|
<Download className="w-4 h-4" />
|
||||||
<span>Export Sponsors</span>
|
<span>Export Employers</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Dynamic Sponsors Table */}
|
{/* Dynamic Employers 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('full_name')} className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14 pl-8 cursor-pointer select-none">
|
||||||
Sponsor {sortField === 'full_name' && (sortOrder === 'desc' ? '↓' : '↑')}
|
Employer {sortField === 'full_name' && (sortOrder === 'desc' ? '↓' : '↑')}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead onClick={() => handleSort('city')} className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14 cursor-pointer select-none">
|
<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' ? '↓' : '↑')}
|
Location {sortField === 'city' && (sortOrder === 'desc' ? '↓' : '↑')}
|
||||||
@ -386,14 +386,14 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
|
|||||||
>
|
>
|
||||||
<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 Sponsor</span>
|
<span className="text-xs font-bold">Edit Employer</span>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
{!sponsor.is_verified && (
|
{!sponsor.is_verified && (
|
||||||
<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(sponsor.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 Sponsor</span>
|
<span className="text-xs font-bold">Verify Employer</span>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
@ -404,7 +404,7 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
|
|||||||
>
|
>
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<XCircle className="w-4 h-4" />
|
<XCircle className="w-4 h-4" />
|
||||||
<span className="text-xs font-bold">Suspend Sponsor</span>
|
<span className="text-xs font-bold">Suspend Employer</span>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
) : (
|
) : (
|
||||||
@ -414,7 +414,7 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
|
|||||||
>
|
>
|
||||||
<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">Activate Sponsor</span>
|
<span className="text-xs font-bold">Activate Employer</span>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
@ -425,7 +425,7 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
|
|||||||
>
|
>
|
||||||
<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" />
|
||||||
<span className="text-xs font-bold">Delete Sponsor</span>
|
<span className="text-xs font-bold">Delete Employer</span>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
@ -437,7 +437,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 sponsors match the active search/filters criteria.
|
No employers match the active search/filters criteria.
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
)}
|
||||||
@ -448,9 +448,9 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
|
|||||||
<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 ? (
|
{sponsors.total > 0 ? (
|
||||||
`Showing ${sponsors.from || 0} to ${sponsors.to || 0} of ${sponsors.total} sponsors`
|
`Showing ${sponsors.from || 0} to ${sponsors.to || 0} of ${sponsors.total} employers`
|
||||||
) : (
|
) : (
|
||||||
'No sponsors found'
|
'No employers found'
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
{sponsors.last_page > 1 && (
|
{sponsors.last_page > 1 && (
|
||||||
@ -481,7 +481,7 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Advanced Sponsor Detail Modal */}
|
{/* Advanced 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-4xl w-full bg-white p-0 overflow-hidden rounded-[24px] border-none shadow-2xl font-sans 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 relative flex-shrink-0">
|
||||||
@ -502,10 +502,10 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
|
|||||||
<div className="flex items-center justify-between border-b border-slate-100 pb-2">
|
<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 flex items-center gap-1.5">
|
<h3 className="text-xs font-black text-slate-700 uppercase tracking-widest flex items-center gap-1.5">
|
||||||
<Award className="w-4.5 h-4.5 text-[#0F6E56]" />
|
<Award className="w-4.5 h-4.5 text-[#0F6E56]" />
|
||||||
<span>UAE Sponsor Details</span>
|
<span>UAE Employer Details</span>
|
||||||
</h3>
|
</h3>
|
||||||
<Badge className="bg-teal-50 text-[#0F6E56] border-none font-black text-[9px] uppercase px-3 py-1 rounded-lg">
|
<Badge className="bg-teal-50 text-[#0F6E56] border-none font-black text-[9px] uppercase px-3 py-1 rounded-lg">
|
||||||
Sponsor ID: SPN-{selectedSponsor?.id}
|
Employer ID: EMP-{selectedSponsor?.id}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
@ -578,7 +578,7 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
|
|||||||
onClick={() => handleApprove(selectedSponsor.id)}
|
onClick={() => handleApprove(selectedSponsor.id)}
|
||||||
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"
|
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"
|
||||||
>
|
>
|
||||||
Verify Sponsor OTP
|
Verify Employer OTP
|
||||||
</button>
|
</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">
|
<Badge className="bg-emerald-100 text-emerald-800 border-none font-black uppercase text-[9px] px-3 py-1.5 rounded-lg shadow-sm">
|
||||||
@ -593,14 +593,14 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
|
|||||||
onClick={() => handleSuspend(selectedSponsor.id)}
|
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"
|
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 Sponsor
|
Suspend Employer
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleActivate(selectedSponsor.id)}
|
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"
|
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 Sponsor
|
Activate Employer
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
@ -614,11 +614,11 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* Sponsor Edit Dialog */}
|
{/* Employer 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] font-sans">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-lg font-black text-slate-800 uppercase tracking-wide">Edit Sponsor Details</DialogTitle>
|
<DialogTitle className="text-lg font-black text-slate-800 uppercase tracking-wide">Edit Employer Details</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<form onSubmit={handleEditSubmit} className="space-y-4 mt-4">
|
<form onSubmit={handleEditSubmit} className="space-y-4 mt-4">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
@ -725,7 +725,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 Sponsor
|
Save Employer
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
298
resources/js/Pages/Admin/MasterData/ReportReasons.jsx
Normal file
298
resources/js/Pages/Admin/MasterData/ReportReasons.jsx
Normal file
@ -0,0 +1,298 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Head, router } from '@inertiajs/react';
|
||||||
|
import AdminLayout from '@/Layouts/AdminLayout';
|
||||||
|
import {
|
||||||
|
Plus,
|
||||||
|
Search,
|
||||||
|
GripVertical,
|
||||||
|
Edit2,
|
||||||
|
Trash2,
|
||||||
|
Info,
|
||||||
|
ShieldAlert
|
||||||
|
} from 'lucide-react';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from '@/components/ui/table';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogFooter,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
|
export default function ReportReasons({ reasons }) {
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
|
const [editingReason, setEditingReason] = useState(null);
|
||||||
|
const [reasonName, setReasonName] = useState('');
|
||||||
|
const [reasonStatus, setReasonStatus] = useState('Active');
|
||||||
|
const [reasonType, setReasonType] = useState('Both');
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
|
const handleOpenAdd = () => {
|
||||||
|
setEditingReason(null);
|
||||||
|
setReasonName('');
|
||||||
|
setReasonStatus('Active');
|
||||||
|
setReasonType('Both');
|
||||||
|
setError('');
|
||||||
|
setIsDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenEdit = (reason) => {
|
||||||
|
setEditingReason(reason);
|
||||||
|
setReasonName(reason.reason);
|
||||||
|
setReasonStatus(reason.status);
|
||||||
|
setReasonType(reason.type || 'Both');
|
||||||
|
setError('');
|
||||||
|
setIsDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!reasonName.trim()) {
|
||||||
|
setError('Reason description is required.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editingReason) {
|
||||||
|
router.post(`/admin/master-data/reasons/${editingReason.id}/update`, {
|
||||||
|
reason: reasonName.trim(),
|
||||||
|
status: reasonStatus,
|
||||||
|
type: reasonType
|
||||||
|
}, {
|
||||||
|
onSuccess: () => {
|
||||||
|
setIsDialogOpen(false);
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
setError(err.reason || 'Failed to update reason.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
router.post('/master-data/reasons', {
|
||||||
|
reason: reasonName.trim(),
|
||||||
|
type: reasonType
|
||||||
|
}, {
|
||||||
|
onSuccess: () => {
|
||||||
|
setIsDialogOpen(false);
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
setError(err.reason || 'Failed to add reason.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (id) => {
|
||||||
|
if (confirm('Are you sure you want to delete this report reason?')) {
|
||||||
|
router.delete(`/admin/master-data/reasons/${id}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredReasons = reasons.filter(reason =>
|
||||||
|
reason.reason.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
reason.type.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
const getTypeBadgeClass = (type) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'Chat':
|
||||||
|
return 'bg-blue-50 text-blue-700 border border-blue-200';
|
||||||
|
case 'Review':
|
||||||
|
return 'bg-purple-50 text-purple-700 border border-purple-200';
|
||||||
|
default:
|
||||||
|
return 'bg-teal-50 text-teal-700 border border-teal-200';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AdminLayout title="Reason Master">
|
||||||
|
<Head title="Master Data - Reason Master" />
|
||||||
|
|
||||||
|
<div className="max-w-4xl mx-auto space-y-6">
|
||||||
|
{/* Header Actions */}
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||||
|
<div className="relative flex-1 max-w-sm">
|
||||||
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Find reason..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={e => setSearchTerm(e.target.value)}
|
||||||
|
className="w-full pl-10 pr-4 py-2.5 bg-white border border-slate-200 rounded-xl text-sm font-medium focus:ring-4 focus:ring-teal-500/10 outline-none transition-all"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleOpenAdd}
|
||||||
|
className="bg-[#0F6E56] text-white px-6 py-2.5 rounded-xl text-sm font-bold flex items-center justify-center space-x-2 hover:bg-[#085041] transition-all shadow-lg shadow-teal-500/10 cursor-pointer"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
<span>Add Reason</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Reasons Card */}
|
||||||
|
<div className="bg-white rounded-[24px] border border-slate-200 shadow-sm overflow-hidden">
|
||||||
|
<div className="p-6 border-b border-slate-50 bg-slate-50/30">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div className="p-2 bg-teal-50 rounded-lg">
|
||||||
|
<ShieldAlert className="w-5 h-5 text-[#0F6E56]" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-sm font-black text-slate-900 uppercase tracking-tight">Reason Master</h2>
|
||||||
|
<p className="text-[10px] text-slate-400 font-bold uppercase tracking-widest mt-0.5">Manage safety & moderation report reasons</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow className="bg-white hover:bg-white">
|
||||||
|
<TableHead className="w-12"></TableHead>
|
||||||
|
<TableHead className="font-bold text-slate-400 text-[10px] uppercase tracking-widest h-12">Report Reason</TableHead>
|
||||||
|
<TableHead className="font-bold text-slate-400 text-[10px] uppercase tracking-widest h-12">Applicable Type</TableHead>
|
||||||
|
<TableHead className="font-bold text-slate-400 text-[10px] uppercase tracking-widest h-12">Status</TableHead>
|
||||||
|
<TableHead className="font-bold text-slate-400 text-[10px] uppercase tracking-widest h-12 text-right pr-6">Action</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{filteredReasons.length > 0 ? (
|
||||||
|
filteredReasons.map((reason) => (
|
||||||
|
<TableRow key={reason.id} className="hover:bg-slate-50/50 transition-colors group">
|
||||||
|
<TableCell className="pl-4">
|
||||||
|
<GripVertical className="w-4 h-4 text-slate-300" />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="py-4 font-bold text-slate-900 text-sm">
|
||||||
|
{reason.reason}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-[10px] font-black uppercase tracking-tight ${getTypeBadgeClass(reason.type)}`}>
|
||||||
|
{reason.type}
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span className={`inline-flex items-center px-2 py-0.5 rounded text-[10px] font-black uppercase tracking-tight ${
|
||||||
|
reason.status === 'Active'
|
||||||
|
? 'bg-emerald-50 text-emerald-600'
|
||||||
|
: 'bg-slate-100 text-slate-500'
|
||||||
|
}`}>
|
||||||
|
{reason.status}
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right pr-6">
|
||||||
|
<div className="flex items-center justify-end space-x-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
|
<button
|
||||||
|
onClick={() => handleOpenEdit(reason)}
|
||||||
|
className="p-2 text-slate-400 hover:text-[#0F6E56] hover:bg-teal-50 rounded-lg transition-all cursor-pointer"
|
||||||
|
>
|
||||||
|
<Edit2 className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDelete(reason.id)}
|
||||||
|
className="p-2 text-slate-400 hover:text-rose-500 hover:bg-rose-50 rounded-lg transition-all cursor-pointer"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={5} className="py-8 text-center text-slate-400 font-bold text-xs">
|
||||||
|
No reasons match your search.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
<div className="p-6 bg-slate-50/50 border-t border-slate-100">
|
||||||
|
<div className="flex items-start space-x-3 text-slate-400">
|
||||||
|
<Info className="w-4 h-4 shrink-0 mt-0.5" />
|
||||||
|
<p className="text-[10px] font-bold uppercase tracking-wide leading-relaxed">
|
||||||
|
Tip: Scoping reasons to "Chat" or "Review" helps filter options on the mobile UI. A reason marked "Both" is displayed for all safety reports.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Reason Modal */}
|
||||||
|
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||||
|
<DialogContent className="sm:max-w-[400px] rounded-[24px] bg-white">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-xl font-black text-slate-900 tracking-tight">
|
||||||
|
{editingReason ? 'Edit Reason' : 'New Reason'}
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div className="py-4 space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Reason Description</label>
|
||||||
|
<input
|
||||||
|
className="w-full px-4 py-3 bg-slate-50 border border-slate-100 rounded-xl text-sm font-bold focus:ring-4 focus:ring-teal-500/10 outline-none"
|
||||||
|
placeholder="e.g. Abusive behavior"
|
||||||
|
value={reasonName}
|
||||||
|
onChange={e => setReasonName(e.target.value)}
|
||||||
|
/>
|
||||||
|
{error && (
|
||||||
|
<p className="text-xs text-rose-600 font-bold ml-1">{error}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Applicable Type</label>
|
||||||
|
<select
|
||||||
|
className="w-full px-4 py-3 bg-slate-50 border border-slate-100 rounded-xl text-sm font-bold focus:ring-4 focus:ring-teal-500/10 outline-none cursor-pointer animate-none"
|
||||||
|
value={reasonType}
|
||||||
|
onChange={e => setReasonType(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="Both">Both (Chat & Review)</option>
|
||||||
|
<option value="Chat">Chat Only</option>
|
||||||
|
<option value="Review">Review Only</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{editingReason && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Status</label>
|
||||||
|
<select
|
||||||
|
className="w-full px-4 py-3 bg-slate-50 border border-slate-100 rounded-xl text-sm font-bold focus:ring-4 focus:ring-teal-500/10 outline-none cursor-pointer"
|
||||||
|
value={reasonStatus}
|
||||||
|
onChange={e => setReasonStatus(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="Active">Active</option>
|
||||||
|
<option value="Inactive">Inactive</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className="sm:justify-end gap-3 pt-4 border-t border-slate-100">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsDialogOpen(false)}
|
||||||
|
className="px-6 py-2.5 rounded-xl text-sm font-bold text-slate-400 uppercase tracking-widest hover:bg-slate-50 transition-colors"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="bg-[#0F6E56] text-white px-8 py-2.5 rounded-xl text-sm font-bold uppercase tracking-widest shadow-lg shadow-teal-500/20 active:scale-95 transition-all cursor-pointer"
|
||||||
|
>
|
||||||
|
{editingReason ? 'Update' : 'Add Reason'}
|
||||||
|
</button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</AdminLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -38,7 +38,7 @@ export default function NotificationsCampaigns({ campaigns = [] }) {
|
|||||||
{/* Simplified Header */}
|
{/* Simplified Header */}
|
||||||
<div className="border-b border-slate-200 pb-5">
|
<div className="border-b border-slate-200 pb-5">
|
||||||
<h1 className="text-xl font-black text-slate-800 uppercase tracking-tight">Send Notification</h1>
|
<h1 className="text-xl font-black text-slate-800 uppercase tracking-tight">Send Notification</h1>
|
||||||
<p className="text-xs text-slate-500 mt-0.5 font-medium">Send quick push or WhatsApp alerts to workers and sponsors.</p>
|
<p className="text-xs text-slate-500 mt-0.5 font-medium">Send quick push or WhatsApp alerts to workers and employers.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-8">
|
<div className="grid grid-cols-1 gap-8">
|
||||||
@ -77,7 +77,7 @@ export default function NotificationsCampaigns({ campaigns = [] }) {
|
|||||||
onChange={e => setRecipients(e.target.value)}
|
onChange={e => setRecipients(e.target.value)}
|
||||||
>
|
>
|
||||||
<option value="All Active Workers">All Workers</option>
|
<option value="All Active Workers">All Workers</option>
|
||||||
<option value="All Employers">All Sponsors</option>
|
<option value="All Employers">All Employers</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -56,8 +56,8 @@ export default function PaymentsIndex({ stats, payments: initialPayments }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminLayout title="Financial Analytics">
|
<AdminLayout title="Payments">
|
||||||
<Head title="Payments & Revenue" />
|
<Head title="Payments" />
|
||||||
|
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
{/* Stats Overview */}
|
{/* Stats Overview */}
|
||||||
|
|||||||
@ -62,11 +62,8 @@ export default function SafetyHub({ reports, rules, moderation_queue }) {
|
|||||||
const filteredReports = (reports || []).filter(report => {
|
const filteredReports = (reports || []).filter(report => {
|
||||||
// Tab filter
|
// Tab filter
|
||||||
if (activeTab !== 'All Reports') {
|
if (activeTab !== 'All Reports') {
|
||||||
if (activeTab === 'Profiles' && report.type !== 'Profile') return false;
|
|
||||||
if (activeTab === 'Chats' && report.type !== 'Chat') return false;
|
if (activeTab === 'Chats' && report.type !== 'Chat') return false;
|
||||||
if (activeTab === 'Reviews' && report.type !== 'Review') return false;
|
if (activeTab === 'Reviews' && report.type !== 'Review') return false;
|
||||||
if (activeTab === 'Images' && report.type !== 'Image') return false;
|
|
||||||
if (activeTab === 'Other Content' && report.type === 'Profile' || report.type === 'Chat' || report.type === 'Review' || report.type === 'Image') return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Type filter dropdown
|
// Type filter dropdown
|
||||||
@ -90,15 +87,15 @@ export default function SafetyHub({ reports, rules, moderation_queue }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminLayout title="Content Moderation">
|
<AdminLayout title="Safety & Moderation">
|
||||||
<Head title="Content Moderation" />
|
<Head title="Safety & Moderation" />
|
||||||
|
|
||||||
<div className="font-sans max-w-[1600px] mx-auto space-y-6">
|
<div className="font-sans max-w-[1600px] mx-auto space-y-6">
|
||||||
|
|
||||||
{/* Header Row */}
|
{/* Header Row */}
|
||||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-slate-900 tracking-tight">Content 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>
|
||||||
|
|
||||||
@ -176,7 +173,7 @@ export default function SafetyHub({ reports, rules, moderation_queue }) {
|
|||||||
<div className="p-4 border-b border-slate-100 flex flex-col lg:flex-row lg:items-center justify-between gap-4 bg-slate-50/20">
|
<div className="p-4 border-b border-slate-100 flex flex-col lg:flex-row lg:items-center justify-between gap-4 bg-slate-50/20">
|
||||||
{/* Tabs list */}
|
{/* Tabs list */}
|
||||||
<div className="flex items-center overflow-x-auto gap-2 -mb-4 lg:mb-0 pb-2 lg:pb-0 scrollbar-none">
|
<div className="flex items-center overflow-x-auto gap-2 -mb-4 lg:mb-0 pb-2 lg:pb-0 scrollbar-none">
|
||||||
{['All Reports', 'Profiles', 'Chats', 'Reviews'].map((tab) => (
|
{['All Reports', 'Chats', 'Reviews'].map((tab) => (
|
||||||
<button
|
<button
|
||||||
key={tab}
|
key={tab}
|
||||||
onClick={() => setActiveTab(tab)}
|
onClick={() => setActiveTab(tab)}
|
||||||
@ -200,10 +197,8 @@ export default function SafetyHub({ reports, rules, moderation_queue }) {
|
|||||||
className="bg-white border border-slate-200 text-slate-700 text-xs font-bold px-3 py-2 rounded-xl outline-none cursor-pointer hover:bg-slate-50 transition-colors shadow-sm"
|
className="bg-white border border-slate-200 text-slate-700 text-xs font-bold px-3 py-2 rounded-xl outline-none cursor-pointer hover:bg-slate-50 transition-colors shadow-sm"
|
||||||
>
|
>
|
||||||
<option value="All Types">All Types</option>
|
<option value="All Types">All Types</option>
|
||||||
<option value="Profile">Profile</option>
|
|
||||||
<option value="Chat">Chat</option>
|
<option value="Chat">Chat</option>
|
||||||
<option value="Review">Review</option>
|
<option value="Review">Review</option>
|
||||||
<option value="Image">Image</option>
|
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
{/* Status filter */}
|
{/* Status filter */}
|
||||||
@ -261,9 +256,7 @@ export default function SafetyHub({ reports, rules, moderation_queue }) {
|
|||||||
</td>
|
</td>
|
||||||
<td className="px-5 py-4">
|
<td className="px-5 py-4">
|
||||||
<span className={`px-2.5 py-1 rounded-full text-[10px] font-black uppercase ${
|
<span className={`px-2.5 py-1 rounded-full text-[10px] font-black uppercase ${
|
||||||
report.type === 'Profile' ? 'bg-purple-100 text-purple-700' :
|
report.type === 'Chat' ? 'bg-cyan-100 text-cyan-700' : 'bg-emerald-100 text-emerald-700'
|
||||||
report.type === 'Chat' ? 'bg-cyan-100 text-cyan-700' :
|
|
||||||
report.type === 'Review' ? 'bg-emerald-100 text-emerald-700' : 'bg-orange-100 text-orange-700'
|
|
||||||
}`}>
|
}`}>
|
||||||
{report.type}
|
{report.type}
|
||||||
</span>
|
</span>
|
||||||
@ -339,75 +332,6 @@ export default function SafetyHub({ reports, rules, moderation_queue }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Subsections: Verification Queue & Auto-Detection Rules */}
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
||||||
{/* Content Verification Queue */}
|
|
||||||
<div className="lg:col-span-2 bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
|
|
||||||
<div className="p-5 border-b border-slate-100 flex items-center justify-between bg-slate-50/20">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-sm font-black text-slate-800 uppercase tracking-widest">Profile Content Verification Queue</h3>
|
|
||||||
<p className="text-[10px] text-slate-400 mt-0.5">Bio updates or pictures flagged for manual review</p>
|
|
||||||
</div>
|
|
||||||
<Badge className="bg-amber-50 text-amber-700 border-none font-bold uppercase tracking-wider text-[8px]">Action Required</Badge>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="divide-y divide-slate-100">
|
|
||||||
{moderation_queue.map((item) => (
|
|
||||||
<div key={item.id} className="p-5 hover:bg-slate-50/20 transition-colors grid grid-cols-1 sm:grid-cols-3 gap-4 text-xs font-bold text-slate-700">
|
|
||||||
<div>
|
|
||||||
<span className="text-[9px] text-slate-400 uppercase tracking-widest block font-mono">{item.id} • {item.type}</span>
|
|
||||||
<span className="text-sm font-black text-slate-900 block mt-1">{item.user}</span>
|
|
||||||
<Badge className="mt-2 bg-red-50 text-red-700 border-none font-bold uppercase tracking-wider text-[8px]">{item.flag}</Badge>
|
|
||||||
</div>
|
|
||||||
<div className="sm:col-span-2 flex flex-col justify-between items-end gap-3">
|
|
||||||
<div className="bg-slate-50 p-3 rounded-xl border border-slate-100 text-slate-600 font-medium w-full text-[11px] leading-relaxed">
|
|
||||||
{item.content.startsWith('http') ? (
|
|
||||||
<img src={item.content} alt="Flagged asset" className="h-16 rounded border" />
|
|
||||||
) : item.content}
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<button className="px-3.5 py-1.5 bg-emerald-600 hover:bg-emerald-500 text-white rounded-lg text-[9px] font-black uppercase tracking-wider transition-colors shadow-sm">
|
|
||||||
Approve Content
|
|
||||||
</button>
|
|
||||||
<button className="px-3.5 py-1.5 bg-red-600 hover:bg-red-500 text-white rounded-lg text-[9px] font-black uppercase tracking-wider transition-colors shadow-sm">
|
|
||||||
Reject / Delete
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Auto-Detection Rules */}
|
|
||||||
<div className="bg-white p-5 border border-slate-200 rounded-2xl shadow-sm flex flex-col justify-between">
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-sm font-black text-slate-800 uppercase tracking-widest">Auto-Detection Rules</h3>
|
|
||||||
<p className="text-[10px] text-slate-400 mt-0.5">Parameters triggering system automated flags</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
{rules.map((rule) => (
|
|
||||||
<div key={rule.id} className="p-3 bg-slate-50 rounded-xl border border-slate-100 text-xs font-bold text-slate-700 space-y-1">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-slate-800 font-black">{rule.name}</span>
|
|
||||||
<Badge className={`border-none text-[8px] font-black uppercase ${
|
|
||||||
rule.status === 'Active' ? 'bg-emerald-50 text-emerald-700' : 'bg-slate-100 text-slate-400'
|
|
||||||
}`}>{rule.status}</Badge>
|
|
||||||
</div>
|
|
||||||
<p className="text-[10px] text-slate-400 font-semibold">{rule.trigger}</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button className="w-full mt-4 py-3 bg-[#0F6E56] text-white rounded-xl text-[10px] font-black uppercase tracking-widest shadow-md hover:bg-[#085041] flex items-center justify-center gap-1.5 transition-colors">
|
|
||||||
<Plus className="w-3.5 h-3.5" />
|
|
||||||
<span>Create Custom Rule</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Investigation & Action Dialog Modal */}
|
{/* Investigation & Action Dialog Modal */}
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Head, Link, router } from '@inertiajs/react';
|
import { Head, Link, router } from '@inertiajs/react';
|
||||||
import AdminLayout from '@/Layouts/AdminLayout';
|
import AdminLayout from '@/Layouts/AdminLayout';
|
||||||
import {
|
import {
|
||||||
Users,
|
Users,
|
||||||
Search,
|
Search,
|
||||||
Filter,
|
Filter,
|
||||||
MoreHorizontal,
|
MoreHorizontal,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
XCircle,
|
XCircle,
|
||||||
UserCheck,
|
UserCheck,
|
||||||
UserX,
|
UserX,
|
||||||
Mail,
|
Mail,
|
||||||
Globe,
|
Globe,
|
||||||
@ -49,10 +49,22 @@ import {
|
|||||||
DialogFooter
|
DialogFooter
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
|
const locationData = {
|
||||||
|
'United Arab Emirates': {
|
||||||
|
'Dubai': ['Dubai Marina', 'Downtown Dubai', 'Al Barsha', 'Jumeirah', 'Al Quoz', 'JLT'],
|
||||||
|
'Abu Dhabi': ['Yas Island', 'Al Reem Island', 'Khalifa City', 'Al Khalidiyah'],
|
||||||
|
'Sharjah': ['Al Nahda', 'Al Majaz', 'Muwaileh', 'Al Khan']
|
||||||
|
},
|
||||||
|
'Saudi Arabia': {
|
||||||
|
'Riyadh': ['Al Olaya', 'Al Malaz', 'Al Yasmin', 'Al Mursalat'],
|
||||||
|
'Jeddah': ['Al Hamra', 'Al Naeem', 'Al Safa', 'Al Khalidiyah']
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export default function WorkerManagement({ workers }) {
|
export default function WorkerManagement({ workers }) {
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [statusFilter, setStatusFilter] = useState('all');
|
const [statusFilter, setStatusFilter] = useState('all');
|
||||||
|
|
||||||
// Dialog / Worker selection state
|
// Dialog / Worker selection state
|
||||||
const [selectedWorker, setSelectedWorker] = useState(null);
|
const [selectedWorker, setSelectedWorker] = useState(null);
|
||||||
const [isDetailDialogOpen, setIsDetailDialogOpen] = useState(false);
|
const [isDetailDialogOpen, setIsDetailDialogOpen] = useState(false);
|
||||||
@ -61,9 +73,16 @@ export default function WorkerManagement({ workers }) {
|
|||||||
// Form inputs for Profile Moderation
|
// Form inputs for Profile Moderation
|
||||||
const [editForm, setEditForm] = useState({
|
const [editForm, setEditForm] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
|
phone: '',
|
||||||
|
language: '',
|
||||||
category: '',
|
category: '',
|
||||||
experience: '',
|
experience: '',
|
||||||
bio: '',
|
bio: '',
|
||||||
|
country: '',
|
||||||
|
city: '',
|
||||||
|
area: '',
|
||||||
|
preferred_location: '',
|
||||||
|
live_in_out: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const [adminNotes, setAdminNotes] = useState('');
|
const [adminNotes, setAdminNotes] = useState('');
|
||||||
@ -103,12 +122,21 @@ export default function WorkerManagement({ workers }) {
|
|||||||
router.post(`/admin/workers/${selectedWorker.id}/update-profile`, editForm, {
|
router.post(`/admin/workers/${selectedWorker.id}/update-profile`, editForm, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setIsEditMode(false);
|
setIsEditMode(false);
|
||||||
setSelectedWorker({
|
setSelectedWorker({
|
||||||
...selectedWorker,
|
...selectedWorker,
|
||||||
name: editForm.name,
|
name: editForm.name,
|
||||||
category: editForm.category,
|
phone: editForm.phone,
|
||||||
experience: editForm.experience,
|
gender: editForm.gender,
|
||||||
bio: editForm.bio
|
language: editForm.language,
|
||||||
|
category: editForm.category,
|
||||||
|
experience: editForm.experience,
|
||||||
|
salary: editForm.salary,
|
||||||
|
bio: editForm.bio,
|
||||||
|
country: editForm.country,
|
||||||
|
city: editForm.city,
|
||||||
|
area: editForm.area,
|
||||||
|
preferred_location: editForm.preferred_location,
|
||||||
|
live_in_out: editForm.live_in_out
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -139,9 +167,18 @@ export default function WorkerManagement({ workers }) {
|
|||||||
setSelectedWorker(fullWorker);
|
setSelectedWorker(fullWorker);
|
||||||
setEditForm({
|
setEditForm({
|
||||||
name: fullWorker.name,
|
name: fullWorker.name,
|
||||||
|
phone: fullWorker.phone,
|
||||||
|
gender: fullWorker.gender || 'Female',
|
||||||
|
language: fullWorker.language || 'English',
|
||||||
category: fullWorker.category,
|
category: fullWorker.category,
|
||||||
experience: fullWorker.experience,
|
experience: fullWorker.experience,
|
||||||
|
salary: fullWorker.salary || '',
|
||||||
bio: fullWorker.bio,
|
bio: fullWorker.bio,
|
||||||
|
country: fullWorker.country || '',
|
||||||
|
city: fullWorker.city || '',
|
||||||
|
area: fullWorker.area || '',
|
||||||
|
preferred_location: fullWorker.preferred_location || '',
|
||||||
|
live_in_out: fullWorker.live_in_out || '',
|
||||||
});
|
});
|
||||||
setAdminNotes(fullWorker.admin_notes);
|
setAdminNotes(fullWorker.admin_notes);
|
||||||
setIsEditMode(false);
|
setIsEditMode(false);
|
||||||
@ -153,8 +190,8 @@ export default function WorkerManagement({ workers }) {
|
|||||||
availability: w.availability || 'Available Now',
|
availability: w.availability || 'Available Now',
|
||||||
verified: w.verified !== undefined ? w.verified : (w.status === 'active')
|
verified: w.verified !== undefined ? w.verified : (w.status === 'active')
|
||||||
})).filter(worker => {
|
})).filter(worker => {
|
||||||
const matchesSearch = worker.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
const matchesSearch = worker.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
worker.email.toLowerCase().includes(searchTerm.toLowerCase());
|
worker.email.toLowerCase().includes(searchTerm.toLowerCase());
|
||||||
const matchesStatus = statusFilter === 'all' || worker.status === statusFilter;
|
const matchesStatus = statusFilter === 'all' || worker.status === statusFilter;
|
||||||
return matchesSearch && matchesStatus;
|
return matchesSearch && matchesStatus;
|
||||||
});
|
});
|
||||||
@ -175,7 +212,7 @@ export default function WorkerManagement({ workers }) {
|
|||||||
<Download className="w-4 h-4" />
|
<Download className="w-4 h-4" />
|
||||||
<span>Export CSV</span>
|
<span>Export CSV</span>
|
||||||
</button>
|
</button>
|
||||||
<Link
|
<Link
|
||||||
href="/admin/workers/verifications"
|
href="/admin/workers/verifications"
|
||||||
className="inline-flex items-center px-4 py-2.5 bg-[#0F6E56] text-white rounded-xl text-xs font-bold hover:bg-[#085041] transition-colors shadow-sm uppercase tracking-wider"
|
className="inline-flex items-center px-4 py-2.5 bg-[#0F6E56] text-white rounded-xl text-xs font-bold hover:bg-[#085041] transition-colors shadow-sm uppercase tracking-wider"
|
||||||
>
|
>
|
||||||
@ -189,8 +226,8 @@ export default function WorkerManagement({ workers }) {
|
|||||||
<div className="flex flex-1 items-center space-x-4 w-full md:w-auto">
|
<div className="flex flex-1 items-center space-x-4 w-full md:w-auto">
|
||||||
<div className="relative flex-1 max-w-md">
|
<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-gray-400" />
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search by name, email or nationality..."
|
placeholder="Search by name, email or nationality..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
@ -201,7 +238,7 @@ export default function WorkerManagement({ workers }) {
|
|||||||
<Filter className="w-4 h-4" />
|
<Filter className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 w-full md:w-auto">
|
<div className="flex items-center gap-2 w-full md:w-auto">
|
||||||
<span className="text-[10px] font-black text-gray-400 uppercase tracking-[0.2em] mr-2">Lifecycle Filter:</span>
|
<span className="text-[10px] font-black text-gray-400 uppercase tracking-[0.2em] mr-2">Lifecycle Filter:</span>
|
||||||
<div className="flex bg-slate-100 p-1 rounded-xl">
|
<div className="flex bg-slate-100 p-1 rounded-xl">
|
||||||
@ -209,11 +246,10 @@ export default function WorkerManagement({ workers }) {
|
|||||||
<button
|
<button
|
||||||
key={f}
|
key={f}
|
||||||
onClick={() => setStatusFilter(f)}
|
onClick={() => setStatusFilter(f)}
|
||||||
className={`px-3 py-1.5 rounded-lg text-[10px] font-black uppercase tracking-widest transition-all ${
|
className={`px-3 py-1.5 rounded-lg text-[10px] font-black uppercase tracking-widest transition-all ${statusFilter === f
|
||||||
statusFilter === f
|
? 'bg-white text-[#0F6E56] shadow-sm'
|
||||||
? 'bg-white text-[#0F6E56] shadow-sm'
|
|
||||||
: 'text-gray-500 hover:text-gray-900'
|
: 'text-gray-500 hover:text-gray-900'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{f}
|
{f}
|
||||||
</button>
|
</button>
|
||||||
@ -230,7 +266,8 @@ export default function WorkerManagement({ workers }) {
|
|||||||
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest py-4 pl-8">Worker Information</TableHead>
|
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest py-4 pl-8">Worker Information</TableHead>
|
||||||
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest py-4">Placement Status</TableHead>
|
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest py-4">Placement Status</TableHead>
|
||||||
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest py-4">Verification Status</TableHead>
|
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest py-4">Verification Status</TableHead>
|
||||||
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest py-4">Category & Exp</TableHead>
|
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest py-4">Category, Exp & Languages</TableHead>
|
||||||
|
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest py-4">Location Details</TableHead>
|
||||||
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest py-4">Lifecycle</TableHead>
|
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest py-4">Lifecycle</TableHead>
|
||||||
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest py-4 text-right pr-8">Actions</TableHead>
|
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest py-4 text-right pr-8">Actions</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@ -239,115 +276,128 @@ export default function WorkerManagement({ workers }) {
|
|||||||
{filteredWorkers.length > 0 ? (
|
{filteredWorkers.length > 0 ? (
|
||||||
filteredWorkers.map((worker) => (
|
filteredWorkers.map((worker) => (
|
||||||
<TableRow key={worker.id} className="group hover:bg-slate-50/50 transition-colors">
|
<TableRow key={worker.id} className="group hover:bg-slate-50/50 transition-colors">
|
||||||
<TableCell className="py-4 pl-8">
|
<TableCell className="py-4 pl-8">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-10 h-10 rounded-full bg-[#0F6E56]/10 flex items-center justify-center text-[#0F6E56] font-bold text-sm">
|
<div className="w-10 h-10 rounded-full bg-[#0F6E56]/10 flex items-center justify-center text-[#0F6E56] font-bold text-sm">
|
||||||
{worker.name.charAt(0)}
|
{worker.name.charAt(0)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="font-bold text-gray-900 text-sm flex items-center gap-1.5">
|
<div className="font-bold text-gray-900 text-sm flex items-center gap-1.5">
|
||||||
<span>{worker.name}</span>
|
<span>{worker.name}</span>
|
||||||
{worker.id === 103 && (
|
{worker.id === 103 && (
|
||||||
<Badge className="bg-red-100 text-red-700 border-none px-2 py-0.5 rounded-full text-[9px] font-bold uppercase tracking-wider flex items-center">
|
<Badge className="bg-red-100 text-red-700 border-none px-2 py-0.5 rounded-full text-[9px] font-bold uppercase tracking-wider flex items-center">
|
||||||
<AlertTriangle className="w-2.5 h-2.5 mr-0.5" /> Suspicious
|
<AlertTriangle className="w-2.5 h-2.5 mr-0.5" /> Suspicious
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-400 font-medium mt-0.5">{worker.email}</div>
|
<div className="text-xs text-gray-400 font-medium mt-0.5">{worker.phone} • {worker.email}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<span className={`px-2.5 py-1 rounded-lg text-[10px] font-black uppercase tracking-wider ${
|
<span className={`px-2.5 py-1 rounded-lg text-[10px] font-black uppercase tracking-wider ${worker.availability === 'Available Now' ? 'bg-emerald-50 text-emerald-600' :
|
||||||
worker.availability === 'Available Now' ? 'bg-emerald-50 text-emerald-600' :
|
worker.availability === 'In Interview' ? 'bg-blue-50 text-blue-600' : 'bg-slate-100 text-slate-500'
|
||||||
worker.availability === 'In Interview' ? 'bg-blue-50 text-blue-600' : 'bg-slate-100 text-slate-500'
|
}`}>
|
||||||
}`}>
|
{worker.availability}
|
||||||
{worker.availability}
|
</span>
|
||||||
</span>
|
</TableCell>
|
||||||
</TableCell>
|
<TableCell>
|
||||||
<TableCell>
|
<div className="flex items-center space-x-1.5">
|
||||||
<div className="flex items-center space-x-1.5">
|
<div className={`p-1 bg-${worker.verified ? 'emerald' : 'amber'}-50 rounded-lg`}>
|
||||||
<div className={`p-1 bg-${worker.verified ? 'emerald' : 'amber'}-50 rounded-lg`}>
|
<FileText className={`w-3.5 h-3.5 text-${worker.verified ? 'emerald' : 'amber'}-500`} />
|
||||||
<FileText className={`w-3.5 h-3.5 text-${worker.verified ? 'emerald' : 'amber'}-500`} />
|
</div>
|
||||||
</div>
|
<span className={`text-[10px] font-black uppercase tracking-widest text-${worker.verified ? 'emerald' : 'amber'}-600`}>
|
||||||
<span className={`text-[10px] font-black uppercase tracking-widest text-${worker.verified ? 'emerald' : 'amber'}-600`}>
|
{worker.verified ? 'OCR Verified' : 'Pending Verification'}
|
||||||
{worker.verified ? 'OCR Verified' : 'Pending Verification'}
|
</span>
|
||||||
</span>
|
</div>
|
||||||
</div>
|
</TableCell>
|
||||||
</TableCell>
|
<TableCell>
|
||||||
<TableCell>
|
<div className="space-y-0.5">
|
||||||
<div className="space-y-0.5">
|
<div className="text-xs font-bold text-slate-800 flex items-center gap-1.5">
|
||||||
<div className="text-xs font-bold text-slate-800 flex items-center gap-1.5">
|
<Briefcase className="w-3.5 h-3.5 text-slate-400" /> {worker.category}
|
||||||
<Briefcase className="w-3.5 h-3.5 text-slate-400" /> {worker.category}
|
</div>
|
||||||
</div>
|
<div className="text-[10px] text-slate-400 font-bold flex items-center gap-1.5">
|
||||||
<div className="text-[10px] text-slate-400 font-bold flex items-center gap-1.5">
|
<Globe className="w-3.5 h-3.5 text-slate-400" /> {worker.nationality} • {worker.experience}
|
||||||
<Globe className="w-3.5 h-3.5 text-slate-400" /> {worker.nationality} • {worker.experience}
|
</div>
|
||||||
</div>
|
<div className="text-[10px] text-slate-500 font-semibold flex items-center gap-1 mt-0.5">
|
||||||
</div>
|
<span className="text-slate-400 uppercase tracking-widest text-[8px] font-black">Lang:</span> {worker.language}
|
||||||
</TableCell>
|
</div>
|
||||||
<TableCell>
|
</div>
|
||||||
<Badge
|
</TableCell>
|
||||||
variant="outline"
|
<TableCell>
|
||||||
className={`px-3 py-0.5 rounded-full font-black text-[9px] uppercase tracking-wider ${
|
<div className="space-y-0.5 text-xs">
|
||||||
worker.status === 'active'
|
{worker.country ? (
|
||||||
? 'bg-emerald-50 text-emerald-700 border-emerald-200'
|
<>
|
||||||
: worker.status === 'suspended' ? 'bg-red-100 text-red-800 border-none' : 'bg-slate-50 text-slate-700 border-slate-200'
|
<div className="font-bold text-slate-800">{worker.city}, {worker.country}</div>
|
||||||
}`}
|
<div className="text-[10px] text-slate-400 font-bold">{worker.area}</div>
|
||||||
>
|
</>
|
||||||
{worker.status}
|
) : (
|
||||||
</Badge>
|
<span className="text-slate-400 italic">Not set</span>
|
||||||
</TableCell>
|
)}
|
||||||
<TableCell className="text-right pr-8">
|
</div>
|
||||||
<div className="flex items-center justify-end space-x-1">
|
</TableCell>
|
||||||
<button
|
<TableCell>
|
||||||
onClick={() => openWorkerDetails(worker)}
|
<Badge
|
||||||
className="p-2 hover:bg-slate-100 rounded-lg transition-colors text-[#0F6E56] font-bold"
|
variant="outline"
|
||||||
title="Manage Profile"
|
className={`px-3 py-0.5 rounded-full font-black text-[9px] uppercase tracking-wider ${worker.status === 'active'
|
||||||
>
|
? 'bg-emerald-50 text-emerald-700 border-emerald-200'
|
||||||
<Eye className="w-4 h-4" />
|
: worker.status === 'suspended' ? 'bg-red-100 text-red-800 border-none' : 'bg-slate-50 text-slate-700 border-slate-200'
|
||||||
</button>
|
}`}
|
||||||
<DropdownMenu>
|
>
|
||||||
<DropdownMenuTrigger className="p-2 hover:bg-slate-100 rounded-lg transition-colors focus:outline-none">
|
{worker.status}
|
||||||
<MoreHorizontal className="w-4 h-4 text-slate-400" />
|
</Badge>
|
||||||
</DropdownMenuTrigger>
|
</TableCell>
|
||||||
<DropdownMenuContent align="end" className="w-52 p-2 rounded-xl shadow-xl border-slate-100">
|
<TableCell className="text-right pr-8">
|
||||||
<DropdownMenuLabel className="text-[9px] font-black text-slate-400 uppercase tracking-widest px-2 mb-1">Quick Actions</DropdownMenuLabel>
|
<div className="flex items-center justify-end space-x-1">
|
||||||
|
<button
|
||||||
<DropdownMenuItem
|
onClick={() => openWorkerDetails(worker)}
|
||||||
onClick={() => handleToggleStatus(worker.id, worker.status === 'active' ? 'suspended' : 'active')}
|
className="p-2 hover:bg-slate-100 rounded-lg transition-colors text-[#0F6E56] font-bold"
|
||||||
className="flex items-center gap-2 p-2 rounded-lg cursor-pointer font-bold text-xs text-red-600 hover:bg-red-50"
|
title="Manage Profile"
|
||||||
>
|
>
|
||||||
{worker.status === 'active' ? (
|
<Eye className="w-4 h-4" />
|
||||||
<><Ban className="w-3.5 h-3.5" /> Suspend Account</>
|
</button>
|
||||||
) : (
|
<DropdownMenu>
|
||||||
<><UserCheck className="w-3.5 h-3.5" /> Activate Account</>
|
<DropdownMenuTrigger className="p-2 hover:bg-slate-100 rounded-lg transition-colors focus:outline-none">
|
||||||
)}
|
<MoreHorizontal className="w-4 h-4 text-slate-400" />
|
||||||
</DropdownMenuItem>
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="w-52 p-2 rounded-xl shadow-xl border-slate-100">
|
||||||
|
<DropdownMenuLabel className="text-[9px] font-black text-slate-400 uppercase tracking-widest px-2 mb-1">Quick Actions</DropdownMenuLabel>
|
||||||
|
|
||||||
<DropdownMenuSeparator className="my-1" />
|
<DropdownMenuItem
|
||||||
|
onClick={() => handleToggleStatus(worker.id, worker.status === 'active' ? 'suspended' : 'active')}
|
||||||
<DropdownMenuItem
|
className="flex items-center gap-2 p-2 rounded-lg cursor-pointer font-bold text-xs text-red-600 hover:bg-red-50"
|
||||||
onClick={() => handleAvailabilityOverride(worker.id, 'Available Now')}
|
>
|
||||||
className="flex items-center gap-2 p-2 rounded-lg cursor-pointer font-bold text-xs text-slate-700 hover:bg-slate-50"
|
{worker.status === 'active' ? (
|
||||||
>
|
<><Ban className="w-3.5 h-3.5" /> Suspend Account</>
|
||||||
<CheckSquare className="w-3.5 h-3.5 text-emerald-500" /> Mark Available
|
) : (
|
||||||
</DropdownMenuItem>
|
<><UserCheck className="w-3.5 h-3.5" /> Activate Account</>
|
||||||
|
)}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
|
||||||
<DropdownMenuItem
|
<DropdownMenuSeparator className="my-1" />
|
||||||
onClick={() => handleAvailabilityOverride(worker.id, 'Engaged')}
|
|
||||||
className="flex items-center gap-2 p-2 rounded-lg cursor-pointer font-bold text-xs text-slate-700 hover:bg-slate-50"
|
<DropdownMenuItem
|
||||||
>
|
onClick={() => handleAvailabilityOverride(worker.id, 'Available Now')}
|
||||||
<CheckSquare className="w-3.5 h-3.5 text-slate-400" /> Mark Engaged
|
className="flex items-center gap-2 p-2 rounded-lg cursor-pointer font-bold text-xs text-slate-700 hover:bg-slate-50"
|
||||||
</DropdownMenuItem>
|
>
|
||||||
</DropdownMenuContent>
|
<CheckSquare className="w-3.5 h-3.5 text-emerald-500" /> Mark Available
|
||||||
</DropdownMenu>
|
</DropdownMenuItem>
|
||||||
</div>
|
|
||||||
</TableCell>
|
<DropdownMenuItem
|
||||||
</TableRow>
|
onClick={() => handleAvailabilityOverride(worker.id, 'Engaged')}
|
||||||
|
className="flex items-center gap-2 p-2 rounded-lg cursor-pointer font-bold text-xs text-slate-700 hover:bg-slate-50"
|
||||||
|
>
|
||||||
|
<CheckSquare className="w-3.5 h-3.5 text-slate-400" /> Mark Engaged
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={6} className="py-20 text-center">
|
<TableCell colSpan={7} className="py-20 text-center">
|
||||||
<Users className="w-12 h-12 text-slate-200 mx-auto mb-3" />
|
<Users className="w-12 h-12 text-slate-200 mx-auto mb-3" />
|
||||||
<div className="font-bold text-slate-400">No workers found matching your criteria.</div>
|
<div className="font-bold text-slate-400">No workers found matching your criteria.</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@ -370,7 +420,7 @@ export default function WorkerManagement({ workers }) {
|
|||||||
)}
|
)}
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<div className="w-14 h-14 rounded-2xl bg-white/20 backdrop-blur-md flex items-center justify-center border border-white/20 text-xl font-bold text-white">
|
<div className="w-14 h-14 rounded-2xl bg-white/20 backdrop-blur-md flex items-center justify-center border border-white/20 text-xl font-bold text-white">
|
||||||
{selectedWorker?.name.charAt(0)}
|
{selectedWorker?.name ? selectedWorker.name.charAt(0) : ''}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-black tracking-tight">{selectedWorker?.name}</h2>
|
<h2 className="text-xl font-black tracking-tight">{selectedWorker?.name}</h2>
|
||||||
@ -381,39 +431,276 @@ export default function WorkerManagement({ workers }) {
|
|||||||
|
|
||||||
{/* Scrollable details tab */}
|
{/* Scrollable details tab */}
|
||||||
<div className="p-6 space-y-6 overflow-y-auto flex-1">
|
<div className="p-6 space-y-6 overflow-y-auto flex-1">
|
||||||
|
{isEditMode ? (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 bg-slate-50/50 p-4 rounded-2xl border border-slate-100">
|
<form onSubmit={handleProfileSubmit} className="space-y-6">
|
||||||
<div className="space-y-3">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 bg-slate-50/50 p-4 rounded-2xl border border-slate-100">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<h3 className="text-xs font-black text-slate-400 uppercase tracking-wider">Basic Information</h3>
|
||||||
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Experience</label>
|
<div className="space-y-3">
|
||||||
<div className="text-xs font-black text-slate-800">{selectedWorker?.experience}</div>
|
<div>
|
||||||
|
<label className="text-[10px] font-bold text-slate-500">Full Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editForm.name}
|
||||||
|
onChange={e => setEditForm({ ...editForm, name: e.target.value })}
|
||||||
|
className="w-full bg-white border border-slate-200 rounded-xl p-2.5 text-xs font-bold text-slate-700 outline-none focus:ring-2 focus:ring-[#0F6E56]/20"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-[10px] font-bold text-slate-500">Mobile Number</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editForm.phone}
|
||||||
|
onChange={e => setEditForm({ ...editForm, phone: e.target.value })}
|
||||||
|
className="w-full bg-white border border-slate-200 rounded-xl p-2.5 text-xs font-bold text-slate-700 outline-none focus:ring-2 focus:ring-[#0F6E56]/20"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-[10px] font-bold text-slate-500">Gender</label>
|
||||||
|
<select
|
||||||
|
value={editForm.gender}
|
||||||
|
onChange={e => setEditForm({ ...editForm, gender: e.target.value })}
|
||||||
|
className="w-full bg-white border border-slate-200 rounded-xl p-2.5 text-xs font-bold text-slate-700 outline-none focus:ring-2 focus:ring-[#0F6E56]/20"
|
||||||
|
>
|
||||||
|
<option value="Male">Male</option>
|
||||||
|
<option value="Female">Female</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-[10px] font-bold text-slate-500">Language</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editForm.language}
|
||||||
|
onChange={e => setEditForm({ ...editForm, language: e.target.value })}
|
||||||
|
className="w-full bg-white border border-slate-200 rounded-xl p-2.5 text-xs font-bold text-slate-700 outline-none focus:ring-2 focus:ring-[#0F6E56]/20"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Category</label>
|
|
||||||
<div className="text-xs font-black text-slate-800">{selectedWorker?.category}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h3 className="text-xs font-black text-slate-400 uppercase tracking-wider">Contact & Parameters</h3>
|
<h3 className="text-xs font-black text-slate-400 uppercase tracking-wider">Classification & Experience</h3>
|
||||||
<div className="space-y-2 text-xs font-bold text-slate-700 bg-white p-3.5 rounded-xl border border-slate-100 shadow-sm">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center space-x-2">
|
<div>
|
||||||
<Mail className="w-4 h-4 text-teal-600" />
|
<label className="text-[10px] font-bold text-slate-500">Category</label>
|
||||||
<span>{selectedWorker?.email}</span>
|
<input
|
||||||
</div>
|
type="text"
|
||||||
<div className="flex items-center space-x-2 pt-1">
|
value={editForm.category}
|
||||||
<Phone className="w-4 h-4 text-teal-600" />
|
onChange={e => setEditForm({ ...editForm, category: e.target.value })}
|
||||||
<span>{selectedWorker?.phone}</span>
|
className="w-full bg-white border border-slate-200 rounded-xl p-2.5 text-xs font-bold text-slate-700 outline-none focus:ring-2 focus:ring-[#0F6E56]/20"
|
||||||
</div>
|
required
|
||||||
<div className="flex items-center space-x-2 pt-1">
|
/>
|
||||||
<Clock className="w-4 h-4 text-teal-600" />
|
</div>
|
||||||
<span>Age: {selectedWorker?.age} Years Old</span>
|
<div>
|
||||||
|
<label className="text-[10px] font-bold text-slate-500">Experience</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editForm.experience}
|
||||||
|
onChange={e => setEditForm({ ...editForm, experience: e.target.value })}
|
||||||
|
className="w-full bg-white border border-slate-200 rounded-xl p-2.5 text-xs font-bold text-slate-700 outline-none focus:ring-2 focus:ring-[#0F6E56]/20"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-[10px] font-bold text-slate-500">Accommodation Preference</label>
|
||||||
|
<select
|
||||||
|
value={editForm.live_in_out}
|
||||||
|
onChange={e => setEditForm({ ...editForm, live_in_out: e.target.value })}
|
||||||
|
className="w-full bg-white border border-slate-200 rounded-xl p-2.5 text-xs font-bold text-slate-700 outline-none focus:ring-2 focus:ring-[#0F6E56]/20"
|
||||||
|
>
|
||||||
|
<option value="">Select Option</option>
|
||||||
|
<option value="Live-in (Stay with family)">Live-in (Stay with family)</option>
|
||||||
|
<option value="Live-out (External housing)">Live-out (External housing)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-[10px] font-bold text-slate-500">Salary Expectations (AED)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={editForm.salary}
|
||||||
|
onChange={e => setEditForm({ ...editForm, salary: e.target.value })}
|
||||||
|
className="w-full bg-white border border-slate-200 rounded-xl p-2.5 text-xs font-bold text-slate-700 outline-none focus:ring-2 focus:ring-[#0F6E56]/20"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
{/* Dependent Location Dropdowns */}
|
||||||
|
<div className="p-4 bg-slate-50/50 rounded-2xl border border-slate-100 space-y-4">
|
||||||
|
<h3 className="text-xs font-black text-slate-400 uppercase tracking-wider">Preferred Location Settings</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-[10px] font-bold text-slate-500">Country</label>
|
||||||
|
<select
|
||||||
|
value={editForm.country}
|
||||||
|
onChange={e => {
|
||||||
|
const selectedCountry = e.target.value;
|
||||||
|
setEditForm({
|
||||||
|
...editForm,
|
||||||
|
country: selectedCountry,
|
||||||
|
city: '',
|
||||||
|
area: '',
|
||||||
|
preferred_location: ''
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="w-full bg-white border border-slate-200 rounded-xl p-2.5 text-xs font-bold text-slate-700 outline-none focus:ring-2 focus:ring-[#0F6E56]/20"
|
||||||
|
>
|
||||||
|
<option value="">Select Country</option>
|
||||||
|
{Object.keys(locationData).map(countryName => (
|
||||||
|
<option key={countryName} value={countryName}>{countryName}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-[10px] font-bold text-slate-500">City</label>
|
||||||
|
<select
|
||||||
|
value={editForm.city}
|
||||||
|
disabled={!editForm.country}
|
||||||
|
onChange={e => {
|
||||||
|
const selectedCity = e.target.value;
|
||||||
|
setEditForm({
|
||||||
|
...editForm,
|
||||||
|
city: selectedCity,
|
||||||
|
area: '',
|
||||||
|
preferred_location: ''
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="w-full bg-white border border-slate-200 rounded-xl p-2.5 text-xs font-bold text-slate-700 outline-none focus:ring-2 focus:ring-[#0F6E56]/20 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<option value="">Select City</option>
|
||||||
|
{editForm.country && Object.keys(locationData[editForm.country]).map(cityName => (
|
||||||
|
<option key={cityName} value={cityName}>{cityName}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-[10px] font-bold text-slate-500">Area (Preferred Location)</label>
|
||||||
|
<select
|
||||||
|
value={editForm.area}
|
||||||
|
disabled={!editForm.city}
|
||||||
|
onChange={e => {
|
||||||
|
const selectedArea = e.target.value;
|
||||||
|
setEditForm({
|
||||||
|
...editForm,
|
||||||
|
area: selectedArea,
|
||||||
|
preferred_location: selectedArea ? `${selectedArea}, ${editForm.city}` : ''
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="w-full bg-white border border-slate-200 rounded-xl p-2.5 text-xs font-bold text-slate-700 outline-none focus:ring-2 focus:ring-[#0F6E56]/20 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<option value="">Select Area</option>
|
||||||
|
{editForm.country && editForm.city && locationData[editForm.country][editForm.city].map(areaName => (
|
||||||
|
<option key={areaName} value={areaName}>{areaName}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{editForm.preferred_location && (
|
||||||
|
<div className="text-[10px] font-bold text-[#0F6E56] bg-[#0F6E56]/5 p-2 rounded-lg">
|
||||||
|
Saved Location String: {editForm.preferred_location}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-[10px] font-bold text-slate-500">Bio / Notes</label>
|
||||||
|
<textarea
|
||||||
|
rows="3"
|
||||||
|
value={editForm.bio}
|
||||||
|
onChange={e => setEditForm({ ...editForm, bio: e.target.value })}
|
||||||
|
className="w-full bg-white border border-slate-200 rounded-xl p-3 text-xs font-bold text-slate-700 outline-none focus:ring-2 focus:ring-[#0F6E56]/20"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 bg-slate-50/50 p-4 rounded-2xl border border-slate-100">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-xs font-black text-slate-400 uppercase tracking-wider">Classification & Preference</h3>
|
||||||
|
<div className="grid grid-cols-2 gap-4 text-xs font-bold text-slate-700">
|
||||||
|
<div>
|
||||||
|
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block">Gender</label>
|
||||||
|
<span className="text-slate-800 font-extrabold capitalize">{selectedWorker?.gender || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block">Salary Expectations</label>
|
||||||
|
<span className="text-slate-800 font-extrabold">{selectedWorker?.salary ? `${selectedWorker.salary} AED / mo` : 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block">Experience</label>
|
||||||
|
<span className="text-slate-800 font-extrabold">{selectedWorker?.experience}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block">Category</label>
|
||||||
|
<span className="text-slate-800 font-extrabold">{selectedWorker?.category}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block">Job Type</label>
|
||||||
|
<span className="text-slate-800 font-extrabold capitalize">{selectedWorker?.preferred_job_type || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block">Accommodation Preference</label>
|
||||||
|
<span className="text-slate-800 font-extrabold">{selectedWorker?.live_in_out || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block">Language</label>
|
||||||
|
<span className="text-slate-800 font-extrabold">{selectedWorker?.language || 'English'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-2">
|
||||||
|
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block">Skills</label>
|
||||||
|
<div className="flex flex-wrap gap-1 mt-1">
|
||||||
|
{selectedWorker?.skills?.map(skill => (
|
||||||
|
<span key={skill} className="bg-teal-50 border border-teal-100 text-teal-700 px-2 py-0.5 rounded text-[10px] font-extrabold uppercase">
|
||||||
|
{skill}
|
||||||
|
</span>
|
||||||
|
)) || 'N/A'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-xs font-black text-slate-400 uppercase tracking-wider">Contact & Location</h3>
|
||||||
|
<div className="grid grid-cols-2 gap-4 text-xs font-bold text-slate-700 bg-white p-3.5 rounded-xl border border-slate-100 shadow-sm">
|
||||||
|
<div className="col-span-2 flex items-center space-x-2">
|
||||||
|
<Mail className="w-4 h-4 text-teal-600 flex-shrink-0" />
|
||||||
|
<span>{selectedWorker?.email}</span>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-2 flex items-center space-x-2">
|
||||||
|
<Phone className="w-4 h-4 text-teal-600 flex-shrink-0" />
|
||||||
|
<span>{selectedWorker?.phone || '+971 52 489 1209'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block">Country</label>
|
||||||
|
<span className="text-slate-800 font-extrabold">{selectedWorker?.country || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block">City</label>
|
||||||
|
<span className="text-slate-800 font-extrabold">{selectedWorker?.city || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block">Area</label>
|
||||||
|
<span className="text-slate-800 font-extrabold">{selectedWorker?.area || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block">Preferred Location</label>
|
||||||
|
<span className="text-slate-800 font-extrabold">{selectedWorker?.preferred_location || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="text-xs font-black text-slate-400 uppercase tracking-wider">Bio / Profile Description</h3>
|
||||||
|
<p className="text-xs text-slate-600 bg-slate-50 p-4 rounded-xl border border-slate-100 leading-relaxed whitespace-pre-line">
|
||||||
|
{selectedWorker?.bio}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Management Controls: Verification, Availability, Fraud flags */}
|
{/* Management Controls: Verification, Availability, Fraud flags */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
@ -424,23 +711,21 @@ export default function WorkerManagement({ workers }) {
|
|||||||
<p className="text-[10px] text-slate-400 font-semibold mt-1">Status of system OCR document match</p>
|
<p className="text-[10px] text-slate-400 font-semibold mt-1">Status of system OCR document match</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 mt-4">
|
<div className="flex gap-2 mt-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleVerifyAction(selectedWorker.id, 'approve')}
|
onClick={() => handleVerifyAction(selectedWorker.id, 'approve')}
|
||||||
className={`flex-1 py-2 rounded-xl text-[9px] font-black uppercase tracking-wider transition-all border ${
|
className={`flex-1 py-2 rounded-xl text-[9px] font-black uppercase tracking-wider transition-all border ${selectedWorker?.verified
|
||||||
selectedWorker?.verified
|
? 'bg-emerald-50 text-emerald-700 border-emerald-200'
|
||||||
? 'bg-emerald-50 text-emerald-700 border-emerald-200'
|
|
||||||
: 'bg-white text-slate-500 hover:bg-slate-50'
|
: 'bg-white text-slate-500 hover:bg-slate-50'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Verified
|
Verified
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleVerifyAction(selectedWorker.id, 'reject')}
|
onClick={() => handleVerifyAction(selectedWorker.id, 'reject')}
|
||||||
className={`flex-1 py-2 rounded-xl text-[9px] font-black uppercase tracking-wider transition-all border ${
|
className={`flex-1 py-2 rounded-xl text-[9px] font-black uppercase tracking-wider transition-all border ${!selectedWorker?.verified
|
||||||
!selectedWorker?.verified
|
? 'bg-amber-50 text-amber-700 border-amber-200'
|
||||||
? 'bg-amber-50 text-amber-700 border-amber-200'
|
|
||||||
: 'bg-white text-slate-500 hover:bg-slate-50'
|
: 'bg-white text-slate-500 hover:bg-slate-50'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Pending / Unverified
|
Pending / Unverified
|
||||||
</button>
|
</button>
|
||||||
@ -458,11 +743,10 @@ export default function WorkerManagement({ workers }) {
|
|||||||
<button
|
<button
|
||||||
key={av}
|
key={av}
|
||||||
onClick={() => handleAvailabilityOverride(selectedWorker.id, av)}
|
onClick={() => handleAvailabilityOverride(selectedWorker.id, av)}
|
||||||
className={`flex-1 py-1 rounded-lg text-[8px] font-black uppercase tracking-tight transition-all ${
|
className={`flex-1 py-1 rounded-lg text-[8px] font-black uppercase tracking-tight transition-all ${selectedWorker?.availability === av
|
||||||
selectedWorker?.availability === av
|
? 'bg-white text-slate-800 shadow-sm'
|
||||||
? 'bg-white text-slate-800 shadow-sm'
|
|
||||||
: 'text-slate-500 hover:text-slate-900'
|
: 'text-slate-500 hover:text-slate-900'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{av.replace(' Now', '')}
|
{av.replace(' Now', '')}
|
||||||
</button>
|
</button>
|
||||||
@ -471,9 +755,8 @@ export default function WorkerManagement({ workers }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Suspicious / Fraud flagging */}
|
{/* Suspicious / Fraud flagging */}
|
||||||
<div className={`p-4 border rounded-2xl flex flex-col justify-between shadow-sm ${
|
<div className={`p-4 border rounded-2xl flex flex-col justify-between shadow-sm ${selectedWorker?.is_fraud ? 'bg-red-50/50 border-red-200' : 'bg-white border-slate-200'
|
||||||
selectedWorker?.is_fraud ? 'bg-red-50/50 border-red-200' : 'bg-white border-slate-200'
|
}`}>
|
||||||
}`}>
|
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-xs font-black text-slate-900 uppercase tracking-wide flex items-center gap-1">
|
<h4 className="text-xs font-black text-slate-900 uppercase tracking-wide flex items-center gap-1">
|
||||||
<ShieldAlert className="w-4 h-4 text-red-600" />
|
<ShieldAlert className="w-4 h-4 text-red-600" />
|
||||||
@ -482,18 +765,17 @@ export default function WorkerManagement({ workers }) {
|
|||||||
<p className="text-[10px] text-slate-400 font-semibold mt-1">Flag profile as fake or duplicate</p>
|
<p className="text-[10px] text-slate-400 font-semibold mt-1">Flag profile as fake or duplicate</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 mt-4">
|
<div className="flex gap-2 mt-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleFlagFraud(selectedWorker.id, true, 'Low OCR passport scan confidence. Signature is blurred.')}
|
onClick={() => handleFlagFraud(selectedWorker.id, true, 'Low OCR passport scan confidence. Signature is blurred.')}
|
||||||
className={`flex-1 py-2 rounded-xl text-[9px] font-black uppercase tracking-wider transition-all border ${
|
className={`flex-1 py-2 rounded-xl text-[9px] font-black uppercase tracking-wider transition-all border ${selectedWorker?.is_fraud
|
||||||
selectedWorker?.is_fraud
|
? 'bg-red-600 text-white border-none shadow-md shadow-red-600/20'
|
||||||
? 'bg-red-600 text-white border-none shadow-md shadow-red-600/20'
|
|
||||||
: 'bg-white text-red-600 border-red-200 hover:bg-red-50'
|
: 'bg-white text-red-600 border-red-200 hover:bg-red-50'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Flag Fraud
|
Flag Fraud
|
||||||
</button>
|
</button>
|
||||||
{selectedWorker?.is_fraud && (
|
{selectedWorker?.is_fraud && (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleFlagFraud(selectedWorker.id, false, '')}
|
onClick={() => handleFlagFraud(selectedWorker.id, false, '')}
|
||||||
className="px-3 py-2 bg-slate-900 text-white rounded-xl text-[9px] font-black uppercase tracking-wider"
|
className="px-3 py-2 bg-slate-900 text-white rounded-xl text-[9px] font-black uppercase tracking-wider"
|
||||||
>
|
>
|
||||||
@ -510,7 +792,7 @@ export default function WorkerManagement({ workers }) {
|
|||||||
<FileText className="w-3.5 h-3.5" />
|
<FileText className="w-3.5 h-3.5" />
|
||||||
<span>Internal Admin Compliance Notes Logs</span>
|
<span>Internal Admin Compliance Notes Logs</span>
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
rows="2"
|
rows="2"
|
||||||
placeholder="Write admin logs regarding candidate verification reviews or user complaints..."
|
placeholder="Write admin logs regarding candidate verification reviews or user complaints..."
|
||||||
className="w-full bg-white border border-yellow-200 rounded-xl p-3 text-xs font-bold text-slate-700 focus:ring-2 focus:ring-yellow-500/20 outline-none"
|
className="w-full bg-white border border-yellow-200 rounded-xl p-3 text-xs font-bold text-slate-700 focus:ring-2 focus:ring-yellow-500/20 outline-none"
|
||||||
@ -530,11 +812,10 @@ export default function WorkerManagement({ workers }) {
|
|||||||
<button
|
<button
|
||||||
key={st}
|
key={st}
|
||||||
onClick={() => handleToggleStatus(selectedWorker.id, st)}
|
onClick={() => handleToggleStatus(selectedWorker.id, st)}
|
||||||
className={`px-3 py-1 rounded-lg text-[9px] font-black uppercase tracking-wider transition-all ${
|
className={`px-3 py-1 rounded-lg text-[9px] font-black uppercase tracking-wider transition-all ${selectedWorker?.status === st
|
||||||
selectedWorker?.status === st
|
? 'bg-white text-slate-800 shadow-sm'
|
||||||
? 'bg-white text-slate-800 shadow-sm'
|
|
||||||
: 'text-slate-500 hover:text-slate-900'
|
: 'text-slate-500 hover:text-slate-900'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{st}
|
{st}
|
||||||
</button>
|
</button>
|
||||||
@ -542,12 +823,37 @@ export default function WorkerManagement({ workers }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<div className="flex items-center gap-2">
|
||||||
onClick={() => setIsDetailDialogOpen(false)}
|
{isEditMode ? (
|
||||||
className="px-6 py-2.5 bg-slate-950 hover:bg-slate-900 text-white rounded-xl text-[10px] font-black uppercase tracking-widest transition-all"
|
<>
|
||||||
>
|
<button
|
||||||
Close Controls
|
onClick={handleProfileSubmit}
|
||||||
</button>
|
className="px-6 py-2.5 bg-[#0F6E56] hover:bg-[#085041] text-white rounded-xl text-[10px] font-black uppercase tracking-widest transition-all"
|
||||||
|
>
|
||||||
|
Save Changes
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsEditMode(false)}
|
||||||
|
className="px-6 py-2.5 bg-slate-200 hover:bg-slate-300 text-slate-700 rounded-xl text-[10px] font-black uppercase tracking-widest transition-all"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={() => setIsEditMode(true)}
|
||||||
|
className="px-6 py-2.5 bg-[#0F6E56] hover:bg-[#085041] text-white rounded-xl text-[10px] font-black uppercase tracking-widest transition-all"
|
||||||
|
>
|
||||||
|
Edit Profile
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={() => setIsDetailDialogOpen(false)}
|
||||||
|
className="px-6 py-2.5 bg-slate-950 hover:bg-slate-900 text-white rounded-xl text-[10px] font-black uppercase tracking-widest transition-all"
|
||||||
|
>
|
||||||
|
Close Controls
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@ -259,20 +259,42 @@ export default function Profile({ employerProfile }) {
|
|||||||
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-1">{t('emirates_id_verification_desc', 'Regulatory compliance credentials')}</p>
|
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-1">{t('emirates_id_verification_desc', 'Regulatory compliance credentials')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{employerProfile?.verification_status === 'approved' && employerProfile?.emirates_id_front && employerProfile?.emirates_id_back ? (
|
{employerProfile?.verification_status === 'approved' ? (
|
||||||
<div className="p-6 bg-emerald-50 rounded-3xl border border-emerald-200 flex items-start space-x-4">
|
<div className="space-y-4">
|
||||||
<div className="p-3 bg-emerald-100 text-emerald-600 rounded-2xl">
|
<div className="p-6 bg-emerald-50 rounded-3xl border border-emerald-200 flex items-start space-x-4">
|
||||||
<ShieldCheck className="w-6 h-6" />
|
<div className="p-3 bg-emerald-100 text-emerald-600 rounded-2xl">
|
||||||
|
<ShieldCheck className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-extrabold text-slate-900">{t('emirates_id_verified_status', 'Emirates ID Verified')}</h4>
|
||||||
|
<p className="text-[9px] font-black text-slate-400 uppercase tracking-widest mt-0.5">{t('mohre_guidelines', 'Vetted under MOHRE Guidelines')}</p>
|
||||||
|
<p className="text-xs text-slate-600 mt-2 font-medium leading-relaxed">
|
||||||
|
{t('emirates_id_verified_desc', 'Your Emirates ID has been verified successfully. Your account is active for direct hiring.')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<h4 className="font-extrabold text-slate-900">{t('emirates_id_verified_status', 'Emirates ID Verified')}</h4>
|
{/* Extracted Details */}
|
||||||
<p className="text-[9px] font-black text-slate-400 uppercase tracking-widest mt-0.5">{t('mohre_guidelines', 'Vetted under MOHRE Guidelines')}</p>
|
{employerProfile?.emirates_id_number && (
|
||||||
<p className="text-xs text-slate-600 mt-2 font-medium leading-relaxed">
|
<div className="p-6 bg-slate-50 rounded-3xl border border-slate-200 grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
{t('emirates_id_verified_desc', 'Your Emirates ID has been verified successfully. Your account is active for direct hiring.')}
|
<div>
|
||||||
</p>
|
<div className="text-[10px] font-black text-slate-400 uppercase tracking-widest">{t('extracted_eid_number', 'Extracted Emirates ID Number')}</div>
|
||||||
|
<div className="text-sm font-bold text-slate-900 mt-1 font-mono">{employerProfile.emirates_id_number}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[10px] font-black text-slate-400 uppercase tracking-widest">{t('extracted_eid_expiry', 'Extracted Expiry Date')}</div>
|
||||||
|
<div className="text-sm font-bold text-slate-900 mt-1">{employerProfile.emirates_id_expiry}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Security Purge Notice */}
|
||||||
|
<div className="p-4 bg-blue-50/50 rounded-2xl border border-blue-100/50 text-[11px] font-bold text-blue-800 flex items-center space-x-2">
|
||||||
|
<span className="w-2 h-2 rounded-full bg-blue-500 animate-pulse flex-shrink-0" />
|
||||||
|
<span>{t('pdpl_purge_notice', 'Security Audit: Physical identity documents were purged from our databases immediately after verification to maintain PDPL compliance.')}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : employerProfile?.verification_status === 'pending' && (employerProfile?.emirates_id_front || employerProfile?.emirates_id_back) ? (
|
) : employerProfile?.verification_status === 'pending' && (employerProfile?.emirates_id_front || employerProfile?.emirates_id_back || employerProfile?.emirates_id_number) ? (
|
||||||
<div className="p-6 bg-amber-50 rounded-3xl border border-amber-200 flex items-start space-x-4">
|
<div className="p-6 bg-amber-50 rounded-3xl border border-amber-200 flex items-start space-x-4">
|
||||||
<div className="p-3 bg-amber-100 text-amber-600 rounded-2xl animate-pulse">
|
<div className="p-3 bg-amber-100 text-amber-600 rounded-2xl animate-pulse">
|
||||||
<ShieldCheck className="w-6 h-6" />
|
<ShieldCheck className="w-6 h-6" />
|
||||||
@ -324,52 +346,24 @@ export default function Profile({ employerProfile }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Uploaded Documents Details */}
|
{/* Uploaded Documents Details */}
|
||||||
{(employerProfile?.emirates_id_front || employerProfile?.emirates_id_back) && (
|
{(employerProfile?.emirates_id_front || employerProfile?.emirates_id_back || employerProfile?.emirates_id_number) && (
|
||||||
<div className="space-y-4 pt-4 border-t border-slate-100">
|
<div className="space-y-4 pt-4 border-t border-slate-100">
|
||||||
<h4 className="text-xs font-black text-slate-600 uppercase tracking-wider ml-1">{t('uploaded_credentials_scans', 'Uploaded Credentials Scans')}</h4>
|
<h4 className="text-xs font-black text-slate-600 uppercase tracking-wider ml-1">{t('uploaded_credentials_scans', 'Uploaded Credentials Status')}</h4>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
{employerProfile?.emirates_id_front && (
|
<div className="p-4 bg-slate-50 border border-slate-200 rounded-2xl flex items-center justify-between col-span-2">
|
||||||
<div className="p-4 bg-slate-50 border border-slate-200 rounded-2xl flex items-center justify-between">
|
<div className="flex items-center space-x-3 min-w-0">
|
||||||
<div className="flex items-center space-x-3 min-w-0">
|
<div className="w-10 h-10 bg-emerald-100 text-emerald-700 rounded-xl flex items-center justify-center font-bold text-xs flex-shrink-0">
|
||||||
<div className="w-10 h-10 bg-blue-100 text-[#185FA5] rounded-xl flex items-center justify-center font-bold text-xs flex-shrink-0">
|
SEC
|
||||||
ID
|
|
||||||
</div>
|
|
||||||
<div className="min-w-0">
|
|
||||||
<div className="text-xs font-black text-slate-800">{t('emirates_id_front_side', 'Emirates ID (Front Side)')}</div>
|
|
||||||
<div className="text-[10px] text-slate-400 font-bold truncate">{employerProfile.emirates_id_front.split('/').pop()}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<a
|
<div className="min-w-0">
|
||||||
href={`/storage/${employerProfile.emirates_id_front}`}
|
<div className="text-xs font-black text-slate-800">{t('pdpl_secure_storage', 'PDPL Secure Storage')}</div>
|
||||||
target="_blank"
|
<div className="text-[10px] text-slate-400 font-bold truncate">Physical image files purged from storage</div>
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="px-3 py-1.5 bg-white border border-slate-200 text-xs font-bold text-slate-600 rounded-lg hover:text-[#185FA5] transition-colors shadow-sm flex-shrink-0"
|
|
||||||
>
|
|
||||||
{t('view_scan', 'View Scan')}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{employerProfile?.emirates_id_back && (
|
|
||||||
<div className="p-4 bg-slate-50 border border-slate-200 rounded-2xl flex items-center justify-between">
|
|
||||||
<div className="flex items-center space-x-3 min-w-0">
|
|
||||||
<div className="w-10 h-10 bg-blue-100 text-[#185FA5] rounded-xl flex items-center justify-center font-bold text-xs flex-shrink-0">
|
|
||||||
ID
|
|
||||||
</div>
|
|
||||||
<div className="min-w-0">
|
|
||||||
<div className="text-xs font-black text-slate-800">{t('emirates_id_back_side', 'Emirates ID (Back Side)')}</div>
|
|
||||||
<div className="text-[10px] text-slate-400 font-bold truncate">{employerProfile.emirates_id_back.split('/').pop()}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<a
|
|
||||||
href={`/storage/${employerProfile.emirates_id_back}`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="px-3 py-1.5 bg-white border border-slate-200 text-xs font-bold text-slate-600 rounded-lg hover:text-[#185FA5] transition-colors shadow-sm flex-shrink-0"
|
|
||||||
>
|
|
||||||
{t('view_scan', 'View Scan')}
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<span className="text-[10px] font-black uppercase bg-emerald-100 text-emerald-800 px-2.5 py-1 rounded-full">
|
||||||
|
Secured
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -23,7 +23,8 @@ import {
|
|||||||
X,
|
X,
|
||||||
UserCheck,
|
UserCheck,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
User
|
User,
|
||||||
|
Phone
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
@ -281,7 +282,31 @@ export default function Show({ worker }) {
|
|||||||
{/* Left Column: Quick Stats */}
|
{/* Left Column: Quick Stats */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="bg-slate-50 p-6 rounded-2xl border border-slate-200 space-y-4">
|
<div className="bg-slate-50 p-6 rounded-2xl border border-slate-200 space-y-4">
|
||||||
<h3 className="font-black text-xs text-slate-500 uppercase tracking-widest">{t('sponsorship_details', 'Sponsorship Details')}</h3>
|
<h3 className="font-black text-xs text-slate-500 uppercase tracking-widest">{t('worker_details_title', 'Worker Details')}</h3>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between pb-3 border-b border-slate-200">
|
||||||
|
<div className="flex items-center space-x-2 text-xs text-slate-600 font-bold">
|
||||||
|
<User className="w-4 h-4 text-slate-500" />
|
||||||
|
<span>{t('name', 'Name')}</span>
|
||||||
|
</div>
|
||||||
|
<span className="font-extrabold text-slate-900 text-xs">{worker.name}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between pb-3 border-b border-slate-200">
|
||||||
|
<div className="flex items-center space-x-2 text-xs text-slate-600 font-bold">
|
||||||
|
<Phone className="w-4 h-4 text-slate-500" />
|
||||||
|
<span>{t('mobile', 'Mobile')}</span>
|
||||||
|
</div>
|
||||||
|
<span className="font-extrabold text-slate-900 text-xs">{worker.phone}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between pb-3 border-b border-slate-200">
|
||||||
|
<div className="flex items-center space-x-2 text-xs text-slate-600 font-bold">
|
||||||
|
<User className="w-4 h-4 text-slate-500" />
|
||||||
|
<span>{t('gender', 'Gender')}</span>
|
||||||
|
</div>
|
||||||
|
<span className="font-extrabold text-slate-900 text-xs capitalize">{worker.gender}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between pb-3 border-b border-slate-200">
|
<div className="flex items-center justify-between pb-3 border-b border-slate-200">
|
||||||
<div className="flex items-center space-x-2 text-xs text-slate-600 font-bold">
|
<div className="flex items-center space-x-2 text-xs text-slate-600 font-bold">
|
||||||
@ -291,8 +316,6 @@ export default function Show({ worker }) {
|
|||||||
<span className="font-extrabold text-slate-900 text-sm">{worker.salary} {t('aed', 'AED')} / {t('mo', 'mo')}</span>
|
<span className="font-extrabold text-slate-900 text-sm">{worker.salary} {t('aed', 'AED')} / {t('mo', 'mo')}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between pb-3 border-b border-slate-200">
|
<div className="flex items-center justify-between pb-3 border-b border-slate-200">
|
||||||
<div className="flex items-center space-x-2 text-xs text-slate-600 font-bold">
|
<div className="flex items-center space-x-2 text-xs text-slate-600 font-bold">
|
||||||
<Briefcase className="w-4 h-4 text-amber-600" />
|
<Briefcase className="w-4 h-4 text-amber-600" />
|
||||||
@ -304,11 +327,19 @@ export default function Show({ worker }) {
|
|||||||
<div className="flex items-center justify-between pb-3 border-b border-slate-200">
|
<div className="flex items-center justify-between pb-3 border-b border-slate-200">
|
||||||
<div className="flex items-center space-x-2 text-xs text-slate-600 font-bold">
|
<div className="flex items-center space-x-2 text-xs text-slate-600 font-bold">
|
||||||
<Sparkles className="w-4 h-4 text-purple-600" />
|
<Sparkles className="w-4 h-4 text-purple-600" />
|
||||||
<span>{t('preferred_job_type', 'Preferred Job Type')}</span>
|
<span>{t('job_type', 'Job Type')}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-black text-slate-900 text-xs uppercase">{worker.preferred_job_type}</span>
|
<span className="font-black text-slate-900 text-xs uppercase">{worker.preferred_job_type}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between pb-3 border-b border-slate-200">
|
||||||
|
<div className="flex items-center space-x-2 text-xs text-slate-600 font-bold">
|
||||||
|
<HeartHandshake className="w-4 h-4 text-rose-500" />
|
||||||
|
<span>{t('accommodation_preference', 'Accommodation Preference')}</span>
|
||||||
|
</div>
|
||||||
|
<span className="font-black text-slate-900 text-xs uppercase">{worker.live_in_out}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between pb-3 border-b border-slate-200">
|
<div className="flex items-center justify-between pb-3 border-b border-slate-200">
|
||||||
<div className="flex items-center space-x-2 text-xs text-slate-600 font-bold">
|
<div className="flex items-center space-x-2 text-xs text-slate-600 font-bold">
|
||||||
<ShieldCheck className={`w-4 h-4 ${worker.passport_status.toLowerCase().includes('pending') ? 'text-amber-600' : 'text-emerald-600'}`} />
|
<ShieldCheck className={`w-4 h-4 ${worker.passport_status.toLowerCase().includes('pending') ? 'text-amber-600' : 'text-emerald-600'}`} />
|
||||||
|
|||||||
@ -72,6 +72,10 @@
|
|||||||
|
|
||||||
// Announcement Management
|
// Announcement Management
|
||||||
Route::get('/workers/announcements', [WorkerAnnouncementController::class, 'getAnnouncements']);
|
Route::get('/workers/announcements', [WorkerAnnouncementController::class, 'getAnnouncements']);
|
||||||
|
|
||||||
|
// Report Management
|
||||||
|
Route::post('/workers/report', [\App\Http\Controllers\Api\ReportController::class, 'reportFromWorker']);
|
||||||
|
Route::get('/workers/report-reasons', [\App\Http\Controllers\Api\ReportController::class, 'getReasonsForWorker']);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Protected Employer Mobile Endpoints (Token Authenticated via Bearer Token)
|
// Protected Employer Mobile Endpoints (Token Authenticated via Bearer Token)
|
||||||
@ -110,4 +114,8 @@
|
|||||||
Route::get('/employers/reviews', [EmployerReviewController::class, 'getReviews']);
|
Route::get('/employers/reviews', [EmployerReviewController::class, 'getReviews']);
|
||||||
Route::post('/employers/reviews', [EmployerReviewController::class, 'addReview']);
|
Route::post('/employers/reviews', [EmployerReviewController::class, 'addReview']);
|
||||||
Route::put('/employers/reviews/{id}', [EmployerReviewController::class, 'editReview']);
|
Route::put('/employers/reviews/{id}', [EmployerReviewController::class, 'editReview']);
|
||||||
|
|
||||||
|
// Report Management
|
||||||
|
Route::post('/employers/report', [\App\Http\Controllers\Api\ReportController::class, 'reportFromEmployer']);
|
||||||
|
Route::get('/employers/report-reasons', [\App\Http\Controllers\Api\ReportController::class, 'getReasonsForEmployer']);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -126,6 +126,54 @@
|
|||||||
return redirect()->back();
|
return redirect()->back();
|
||||||
})->name('admin.skills.delete');
|
})->name('admin.skills.delete');
|
||||||
|
|
||||||
|
Route::get('/master-data/reasons', function () {
|
||||||
|
$reasons = \App\Models\ReportReason::orderBy('id', 'desc')->get()->map(function ($reason) {
|
||||||
|
return [
|
||||||
|
'id' => $reason->id,
|
||||||
|
'reason' => $reason->reason,
|
||||||
|
'status' => $reason->status,
|
||||||
|
'type' => $reason->type,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
return Inertia::render('Admin/MasterData/ReportReasons', [
|
||||||
|
'reasons' => $reasons
|
||||||
|
]);
|
||||||
|
})->name('admin.reasons');
|
||||||
|
|
||||||
|
Route::post('/master-data/reasons', function (\Illuminate\Http\Request $request) {
|
||||||
|
$request->validate([
|
||||||
|
'reason' => 'required|string|max:255|unique:report_reasons,reason',
|
||||||
|
'type' => 'required|string|in:Chat,Review,Both',
|
||||||
|
]);
|
||||||
|
\App\Models\ReportReason::create([
|
||||||
|
'reason' => $request->reason,
|
||||||
|
'type' => $request->type,
|
||||||
|
'status' => 'Active',
|
||||||
|
]);
|
||||||
|
return redirect()->back();
|
||||||
|
})->name('admin.reasons.store');
|
||||||
|
|
||||||
|
Route::post('/master-data/reasons/{id}/update', function (\Illuminate\Http\Request $request, $id) {
|
||||||
|
$request->validate([
|
||||||
|
'reason' => 'required|string|max:255|unique:report_reasons,reason,' . $id,
|
||||||
|
'status' => 'required|string|in:Active,Inactive',
|
||||||
|
'type' => 'required|string|in:Chat,Review,Both',
|
||||||
|
]);
|
||||||
|
$reason = \App\Models\ReportReason::findOrFail($id);
|
||||||
|
$reason->update([
|
||||||
|
'reason' => $request->reason,
|
||||||
|
'status' => $request->status,
|
||||||
|
'type' => $request->type,
|
||||||
|
]);
|
||||||
|
return redirect()->back();
|
||||||
|
})->name('admin.reasons.update');
|
||||||
|
|
||||||
|
Route::delete('/master-data/reasons/{id}', function ($id) {
|
||||||
|
$reason = \App\Models\ReportReason::findOrFail($id);
|
||||||
|
$reason->delete();
|
||||||
|
return redirect()->back();
|
||||||
|
})->name('admin.reasons.delete');
|
||||||
|
|
||||||
Route::get('/announcements', function () {
|
Route::get('/announcements', function () {
|
||||||
return Inertia::render('Admin/Announcements/Index');
|
return Inertia::render('Admin/Announcements/Index');
|
||||||
})->name('admin.announcements');
|
})->name('admin.announcements');
|
||||||
@ -264,3 +312,6 @@
|
|||||||
Route::get('/api/documentation', function () {
|
Route::get('/api/documentation', function () {
|
||||||
return view('swagger');
|
return view('swagger');
|
||||||
})->name('api.documentation');
|
})->name('api.documentation');
|
||||||
|
|
||||||
|
Route::redirect('/api/documention', '/api/documentation');
|
||||||
|
|
||||||
|
|||||||
88
tests/Feature/EmployerProfileWebTest.php
Normal file
88
tests/Feature/EmployerProfileWebTest.php
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\EmployerProfile;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Http\UploadedFile;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class EmployerProfileWebTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
protected $employer;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->employer = User::create([
|
||||||
|
'name' => 'Jane Sponsor',
|
||||||
|
'email' => 'sponsor@example.com',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
'role' => 'employer',
|
||||||
|
]);
|
||||||
|
|
||||||
|
EmployerProfile::create([
|
||||||
|
'user_id' => $this->employer->id,
|
||||||
|
'company_name' => 'Sponsor Co',
|
||||||
|
'phone' => '+971501112222',
|
||||||
|
'verification_status' => 'pending',
|
||||||
|
'language' => 'English',
|
||||||
|
'notifications' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test displaying the employer profile update form.
|
||||||
|
*/
|
||||||
|
public function test_employer_can_view_web_profile()
|
||||||
|
{
|
||||||
|
$response = $this->actingAs($this->employer)
|
||||||
|
->withSession(['user' => $this->employer])
|
||||||
|
->get('/employer/profile');
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test uploading Emirates ID from Web UI: extracts OCR details and purges files.
|
||||||
|
*/
|
||||||
|
public function test_employer_upload_emirates_id_ocr_and_purge()
|
||||||
|
{
|
||||||
|
Storage::fake('local');
|
||||||
|
|
||||||
|
$fakeFront = UploadedFile::fake()->create('emirates_id_front.jpg', 500, 'image/jpeg');
|
||||||
|
$fakeBack = UploadedFile::fake()->create('emirates_id_back.jpg', 500, 'image/jpeg');
|
||||||
|
|
||||||
|
$response = $this->actingAs($this->employer)
|
||||||
|
->withSession(['user' => $this->employer])
|
||||||
|
->post('/employer/profile/update', [
|
||||||
|
'name' => 'Jane Sponsor Updated',
|
||||||
|
'email' => 'sponsor@example.com',
|
||||||
|
'phone' => '+971509999999',
|
||||||
|
'company_name' => 'Sponsor Co Updated',
|
||||||
|
'language' => 'English',
|
||||||
|
'notifications' => true,
|
||||||
|
'emirates_id_front' => $fakeFront,
|
||||||
|
'emirates_id_back' => $fakeBack,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertRedirect();
|
||||||
|
|
||||||
|
// 1. Verify physical files were deleted (purged)
|
||||||
|
$files = Storage::disk('local')->allFiles('temp/employer');
|
||||||
|
$this->assertEmpty($files, 'Emirates ID physical files were not purged.');
|
||||||
|
|
||||||
|
// 2. Verify database records updated with placeholders and extracted details
|
||||||
|
$profile = $this->employer->fresh()->employerProfile;
|
||||||
|
$this->assertEquals('[DELETED_FOR_PDPL_COMPLIANCE]', $profile->emirates_id_front);
|
||||||
|
$this->assertEquals('[DELETED_FOR_PDPL_COMPLIANCE]', $profile->emirates_id_back);
|
||||||
|
$this->assertNotNull($profile->emirates_id_number);
|
||||||
|
$this->assertNotNull($profile->emirates_id_expiry);
|
||||||
|
$this->assertEquals('approved', $profile->verification_status);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -16,4 +16,14 @@ public function test_the_application_returns_a_successful_response(): void
|
|||||||
|
|
||||||
$response->assertStatus(302);
|
$response->assertStatus(302);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_api_documentation_page_loads_and_typo_redirects(): void
|
||||||
|
{
|
||||||
|
$response = $this->get('/api/documentation');
|
||||||
|
$response->assertStatus(200);
|
||||||
|
|
||||||
|
$responseTypo = $this->get('/api/documention');
|
||||||
|
$responseTypo->assertRedirect('/api/documentation');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
323
tests/Feature/ReportApiTest.php
Normal file
323
tests/Feature/ReportApiTest.php
Normal file
@ -0,0 +1,323 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\Conversation;
|
||||||
|
use App\Models\Review;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Worker;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class ReportApiTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
protected $worker;
|
||||||
|
protected $employer;
|
||||||
|
protected $workerToken;
|
||||||
|
protected $employerToken;
|
||||||
|
protected $category;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
// Create category
|
||||||
|
$this->category = \App\Models\WorkerCategory::create([
|
||||||
|
'name' => 'Housemaid',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create worker
|
||||||
|
$this->workerToken = 'worker-token-xyz';
|
||||||
|
$this->worker = Worker::create([
|
||||||
|
'name' => 'Jane Worker',
|
||||||
|
'email' => 'jane.worker@example.com',
|
||||||
|
'phone' => '+971500000001',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
'api_token' => $this->workerToken,
|
||||||
|
'status' => 'Available',
|
||||||
|
'nationality' => 'Ethiopian',
|
||||||
|
'age' => 28,
|
||||||
|
'salary' => 1500.00,
|
||||||
|
'availability' => 'Immediate',
|
||||||
|
'experience' => '2 Years',
|
||||||
|
'religion' => 'Christian',
|
||||||
|
'bio' => 'Experienced housemaid seeking employment.',
|
||||||
|
'category_id' => $this->category->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create employer user
|
||||||
|
$this->employerToken = 'employer-token-abc';
|
||||||
|
$this->employer = User::create([
|
||||||
|
'name' => 'John Employer',
|
||||||
|
'email' => 'john.employer@example.com',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
'role' => 'employer',
|
||||||
|
'api_token' => $this->employerToken,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test worker can report a chat conversation.
|
||||||
|
*/
|
||||||
|
public function test_worker_can_report_chat()
|
||||||
|
{
|
||||||
|
$conversation = Conversation::create([
|
||||||
|
'employer_id' => $this->employer->id,
|
||||||
|
'worker_id' => $this->worker->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer ' . $this->workerToken,
|
||||||
|
])->postJson('/api/workers/report', [
|
||||||
|
'type' => 'Chat',
|
||||||
|
'item_id' => $conversation->id,
|
||||||
|
'reason' => 'Inappropriate messages',
|
||||||
|
'description' => 'The employer sent abusive messages.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(201)
|
||||||
|
->assertJson([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Report submitted successfully. Our safety team will review it shortly.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('moderation_reports', [
|
||||||
|
'type' => 'Chat',
|
||||||
|
'reported_user_name' => $this->employer->name,
|
||||||
|
'reported_user_role' => 'Sponsor',
|
||||||
|
'reported_by_name' => $this->worker->name,
|
||||||
|
'reported_by_role' => 'Worker',
|
||||||
|
'reason' => 'Inappropriate messages',
|
||||||
|
'description' => 'The employer sent abusive messages.',
|
||||||
|
'status' => 'Pending',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('audit_logs', [
|
||||||
|
'category' => 'user_activity',
|
||||||
|
'user' => 'Jane Worker (Worker)',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test worker can report a review.
|
||||||
|
*/
|
||||||
|
public function test_worker_can_report_review()
|
||||||
|
{
|
||||||
|
$review = Review::create([
|
||||||
|
'employer_id' => $this->employer->id,
|
||||||
|
'worker_id' => $this->worker->id,
|
||||||
|
'rating' => 1,
|
||||||
|
'comment' => 'Very bad attitude.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer ' . $this->workerToken,
|
||||||
|
])->postJson('/api/workers/report', [
|
||||||
|
'type' => 'Review',
|
||||||
|
'item_id' => $review->id,
|
||||||
|
'reason' => 'False accusation',
|
||||||
|
'description' => 'The review contains false claims.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(201)
|
||||||
|
->assertJson([
|
||||||
|
'success' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('moderation_reports', [
|
||||||
|
'type' => 'Review',
|
||||||
|
'reported_user_name' => $this->employer->name,
|
||||||
|
'reported_user_role' => 'Sponsor',
|
||||||
|
'reported_by_name' => $this->worker->name,
|
||||||
|
'reported_by_role' => 'Worker',
|
||||||
|
'reason' => 'False accusation',
|
||||||
|
'status' => 'Pending',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test worker reporting non-existent/unauthorized review.
|
||||||
|
*/
|
||||||
|
public function test_worker_report_invalid_review_returns_404()
|
||||||
|
{
|
||||||
|
// Review for a different worker
|
||||||
|
$otherWorker = Worker::create([
|
||||||
|
'name' => 'Other Worker',
|
||||||
|
'email' => 'other@example.com',
|
||||||
|
'phone' => '+971500000009',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
'category_id' => $this->category->id,
|
||||||
|
'nationality' => 'Indian',
|
||||||
|
'age' => 30,
|
||||||
|
'salary' => 1800.00,
|
||||||
|
'availability' => 'Immediate',
|
||||||
|
'experience' => '3 Years',
|
||||||
|
'religion' => 'Hindu',
|
||||||
|
'bio' => 'Experienced helper.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$review = Review::create([
|
||||||
|
'employer_id' => $this->employer->id,
|
||||||
|
'worker_id' => $otherWorker->id,
|
||||||
|
'rating' => 2,
|
||||||
|
'comment' => 'Okay review.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer ' . $this->workerToken,
|
||||||
|
])->postJson('/api/workers/report', [
|
||||||
|
'type' => 'Review',
|
||||||
|
'item_id' => $review->id,
|
||||||
|
'reason' => 'Spam',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test employer can report a chat conversation.
|
||||||
|
*/
|
||||||
|
public function test_employer_can_report_chat()
|
||||||
|
{
|
||||||
|
$conversation = Conversation::create([
|
||||||
|
'employer_id' => $this->employer->id,
|
||||||
|
'worker_id' => $this->worker->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer ' . $this->employerToken,
|
||||||
|
])->postJson('/api/employers/report', [
|
||||||
|
'type' => 'Chat',
|
||||||
|
'item_id' => $conversation->id,
|
||||||
|
'reason' => 'Abusive worker',
|
||||||
|
'description' => 'Worker requested payment outside platform.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(201)
|
||||||
|
->assertJson([
|
||||||
|
'success' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('moderation_reports', [
|
||||||
|
'type' => 'Chat',
|
||||||
|
'reported_user_name' => $this->worker->name,
|
||||||
|
'reported_user_role' => 'Worker',
|
||||||
|
'reported_by_name' => $this->employer->name,
|
||||||
|
'reported_by_role' => 'Sponsor',
|
||||||
|
'reason' => 'Abusive worker',
|
||||||
|
'status' => 'Pending',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('audit_logs', [
|
||||||
|
'category' => 'user_activity',
|
||||||
|
'user' => 'John Employer (Sponsor)',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test employer can report a review.
|
||||||
|
*/
|
||||||
|
public function test_employer_can_report_review()
|
||||||
|
{
|
||||||
|
$review = Review::create([
|
||||||
|
'employer_id' => $this->employer->id,
|
||||||
|
'worker_id' => $this->worker->id,
|
||||||
|
'rating' => 5,
|
||||||
|
'comment' => 'Great helper.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer ' . $this->employerToken,
|
||||||
|
])->postJson('/api/employers/report', [
|
||||||
|
'type' => 'Review',
|
||||||
|
'item_id' => $review->id,
|
||||||
|
'reason' => 'Report my own review mistake',
|
||||||
|
'description' => 'I selected wrong worker.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(201);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test validation error on invalid input.
|
||||||
|
*/
|
||||||
|
public function test_report_validation_errors()
|
||||||
|
{
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer ' . $this->workerToken,
|
||||||
|
])->postJson('/api/workers/report', [
|
||||||
|
'type' => 'InvalidType',
|
||||||
|
'item_id' => '',
|
||||||
|
'reason' => '',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(422)
|
||||||
|
->assertJsonValidationErrors(['type', 'item_id', 'reason']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test unauthenticated access.
|
||||||
|
*/
|
||||||
|
public function test_report_endpoints_require_auth()
|
||||||
|
{
|
||||||
|
$response = $this->postJson('/api/workers/report', [
|
||||||
|
'type' => 'Chat',
|
||||||
|
'item_id' => 1,
|
||||||
|
'reason' => 'Abuse',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(401);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test retrieving report reasons.
|
||||||
|
*/
|
||||||
|
public function test_get_report_reasons_filtering()
|
||||||
|
{
|
||||||
|
// Clear reasons table and seed specific reasons for testing
|
||||||
|
\DB::table('report_reasons')->truncate();
|
||||||
|
\DB::table('report_reasons')->insert([
|
||||||
|
['reason' => 'Chat Spam', 'status' => 'Active', 'type' => 'Chat'],
|
||||||
|
['reason' => 'Review Abuse', 'status' => 'Active', 'type' => 'Review'],
|
||||||
|
['reason' => 'General Impersonation', 'status' => 'Active', 'type' => 'Both'],
|
||||||
|
['reason' => 'Inactive Reason', 'status' => 'Inactive', 'type' => 'Both'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 1. Worker retrieves all Active reasons
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer ' . $this->workerToken,
|
||||||
|
])->getJson('/api/workers/report-reasons');
|
||||||
|
|
||||||
|
$response->assertStatus(200)
|
||||||
|
->assertJsonCount(3, 'reasons')
|
||||||
|
->assertJsonFragment(['reason' => 'Chat Spam'])
|
||||||
|
->assertJsonFragment(['reason' => 'Review Abuse'])
|
||||||
|
->assertJsonFragment(['reason' => 'General Impersonation'])
|
||||||
|
->assertJsonMissing(['reason' => 'Inactive Reason']);
|
||||||
|
|
||||||
|
// 2. Worker retrieves Chat reasons only
|
||||||
|
$responseChat = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer ' . $this->workerToken,
|
||||||
|
])->getJson('/api/workers/report-reasons?type=Chat');
|
||||||
|
|
||||||
|
$responseChat->assertStatus(200)
|
||||||
|
->assertJsonCount(2, 'reasons')
|
||||||
|
->assertJsonFragment(['reason' => 'Chat Spam'])
|
||||||
|
->assertJsonFragment(['reason' => 'General Impersonation'])
|
||||||
|
->assertJsonMissing(['reason' => 'Review Abuse']);
|
||||||
|
|
||||||
|
// 3. Worker retrieves Review reasons only
|
||||||
|
$responseReview = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer ' . $this->workerToken,
|
||||||
|
])->getJson('/api/workers/report-reasons?type=Review');
|
||||||
|
|
||||||
|
$responseReview->assertStatus(200)
|
||||||
|
->assertJsonCount(2, 'reasons')
|
||||||
|
->assertJsonFragment(['reason' => 'Review Abuse'])
|
||||||
|
->assertJsonFragment(['reason' => 'General Impersonation'])
|
||||||
|
->assertJsonMissing(['reason' => 'Chat Spam']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -23,7 +23,7 @@ export default defineConfig({
|
|||||||
port: 5173,
|
port: 5173,
|
||||||
cors: true,
|
cors: true,
|
||||||
hmr: {
|
hmr: {
|
||||||
host: '192.168.0.249',
|
host: '192.168.0.187',
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
ignored: ['**/storage/framework/views/**'],
|
ignored: ['**/storage/framework/views/**'],
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user