safety report module api

This commit is contained in:
mohanmd 2026-06-05 15:02:41 +05:30
parent abf9cdebde
commit 66eb284f3b
36 changed files with 2338 additions and 412 deletions

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,6 +20,8 @@ class EmployerProfile extends Model
'nationality', 'nationality',
'family_size', 'family_size',
'accommodation', 'accommodation',
'district' 'district',
'emirates_id_number',
'emirates_id_expiry'
]; ];
} }

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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": {

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@ -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 */}

View File

@ -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 */}

View File

@ -49,6 +49,18 @@ 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');
@ -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('');
@ -106,9 +125,18 @@ export default function WorkerManagement({ workers }) {
setSelectedWorker({ setSelectedWorker({
...selectedWorker, ...selectedWorker,
name: editForm.name, name: editForm.name,
phone: editForm.phone,
gender: editForm.gender,
language: editForm.language,
category: editForm.category, category: editForm.category,
experience: editForm.experience, experience: editForm.experience,
bio: editForm.bio 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);
@ -154,7 +191,7 @@ export default function WorkerManagement({ workers }) {
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;
}); });
@ -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
onClick={() => openWorkerDetails(worker)}
className="p-2 hover:bg-slate-100 rounded-lg transition-colors text-[#0F6E56] font-bold"
title="Manage Profile"
>
<Eye className="w-4 h-4" />
</button>
<DropdownMenu>
<DropdownMenuTrigger className="p-2 hover:bg-slate-100 rounded-lg transition-colors focus:outline-none">
<MoreHorizontal className="w-4 h-4 text-slate-400" />
</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>
<DropdownMenuItem <DropdownMenuItem
onClick={() => handleToggleStatus(worker.id, worker.status === 'active' ? 'suspended' : 'active')} onClick={() => handleToggleStatus(worker.id, worker.status === 'active' ? 'suspended' : 'active')}
className="flex items-center gap-2 p-2 rounded-lg cursor-pointer font-bold text-xs text-red-600 hover:bg-red-50" className="flex items-center gap-2 p-2 rounded-lg cursor-pointer font-bold text-xs text-red-600 hover:bg-red-50"
> >
{worker.status === 'active' ? ( {worker.status === 'active' ? (
<><Ban className="w-3.5 h-3.5" /> Suspend Account</> <><Ban className="w-3.5 h-3.5" /> Suspend Account</>
) : ( ) : (
<><UserCheck className="w-3.5 h-3.5" /> Activate Account</> <><UserCheck className="w-3.5 h-3.5" /> Activate Account</>
)} )}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator className="my-1" /> <DropdownMenuSeparator className="my-1" />
<DropdownMenuItem <DropdownMenuItem
onClick={() => handleAvailabilityOverride(worker.id, 'Available Now')} 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" 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-emerald-500" /> Mark Available <CheckSquare className="w-3.5 h-3.5 text-emerald-500" /> Mark Available
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => handleAvailabilityOverride(worker.id, 'Engaged')} 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" 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 <CheckSquare className="w-3.5 h-3.5 text-slate-400" /> Mark Engaged
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>
</TableCell> </TableCell>
</TableRow> </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>
</div> <label className="text-[10px] font-bold text-slate-500">Full Name</label>
<div> <input
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Category</label> type="text"
<div className="text-xs font-black text-slate-800">{selectedWorker?.category}</div> value={editForm.name}
</div> onChange={e => setEditForm({ ...editForm, name: e.target.value })}
</div> 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="space-y-4"> </div>
<h3 className="text-xs font-black text-slate-400 uppercase tracking-wider">Contact & Parameters</h3> <div>
<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"> <label className="text-[10px] font-bold text-slate-500">Mobile Number</label>
<div className="flex items-center space-x-2"> <input
<Mail className="w-4 h-4 text-teal-600" /> type="text"
<span>{selectedWorker?.email}</span> 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 className="flex items-center space-x-2 pt-1"> </div>
<Phone className="w-4 h-4 text-teal-600" />
<span>{selectedWorker?.phone}</span> <div className="space-y-4">
</div> <h3 className="text-xs font-black text-slate-400 uppercase tracking-wider">Classification & Experience</h3>
<div className="flex items-center space-x-2 pt-1"> <div className="space-y-3">
<Clock className="w-4 h-4 text-teal-600" /> <div>
<span>Age: {selectedWorker?.age} Years Old</span> <label className="text-[10px] font-bold text-slate-500">Category</label>
<input
type="text"
value={editForm.category}
onChange={e => setEditForm({ ...editForm, category: 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">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">
@ -426,21 +713,19 @@ export default function WorkerManagement({ workers }) {
<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" />
@ -484,11 +767,10 @@ export default function WorkerManagement({ workers }) {
<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>
@ -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>

View File

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

View File

@ -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'}`} />

View File

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

View File

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

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

View File

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

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

View File

@ -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/**'],