worker page profile update
This commit is contained in:
parent
0a497e56ed
commit
d2c3564c26
@ -30,6 +30,8 @@ public function getProfile(Request $request)
|
|||||||
|
|
||||||
$worker->load(['skills', 'documents']);
|
$worker->load(['skills', 'documents']);
|
||||||
|
|
||||||
|
$worker->makeHidden(['email', 'religion', 'availability', 'bio', 'country', 'city', 'area']);
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'data' => [
|
'data' => [
|
||||||
@ -49,16 +51,29 @@ public function updateProfile(Request $request)
|
|||||||
/** @var Worker $worker */
|
/** @var Worker $worker */
|
||||||
$worker = $request->attributes->get('worker');
|
$worker = $request->attributes->get('worker');
|
||||||
|
|
||||||
|
$data = $request->all();
|
||||||
|
if (isset($data['passport']) && is_string($data['passport'])) {
|
||||||
|
$decoded = json_decode($data['passport'], true);
|
||||||
|
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
|
||||||
|
$data['passport'] = $decoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isset($data['visa']) && is_string($data['visa'])) {
|
||||||
|
$decoded = json_decode($data['visa'], true);
|
||||||
|
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
|
||||||
|
$data['visa'] = $decoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$request->merge($data);
|
||||||
|
|
||||||
$validator = Validator::make($request->all(), [
|
$validator = Validator::make($request->all(), [
|
||||||
'name' => 'nullable|string|max:255',
|
'name' => 'nullable|string|max:255',
|
||||||
|
'phone' => 'nullable|string|max:50|unique:workers,phone,' . $worker->id,
|
||||||
'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',
|
'language' => 'nullable|string',
|
||||||
'salary' => 'nullable|numeric|min:0',
|
'salary' => 'nullable|numeric|min:0',
|
||||||
'availability' => 'nullable|string|max:100',
|
|
||||||
'experience' => 'nullable|string|max:100',
|
'experience' => 'nullable|string|max:100',
|
||||||
'religion' => 'nullable|string|max:100',
|
|
||||||
'bio' => 'nullable|string|max:1000',
|
|
||||||
'skills' => 'nullable|array',
|
'skills' => 'nullable|array',
|
||||||
'skills.*' => 'exists:skills,id',
|
'skills.*' => 'exists:skills,id',
|
||||||
'in_country' => 'nullable',
|
'in_country' => 'nullable',
|
||||||
@ -67,10 +82,19 @@ public function updateProfile(Request $request)
|
|||||||
'gender' => 'nullable|string|in:male,female,other',
|
'gender' => 'nullable|string|in:male,female,other',
|
||||||
'live_in_out' => 'nullable|string|in:live_in,live_out',
|
'live_in_out' => 'nullable|string|in:live_in,live_out',
|
||||||
'preferred_location' => 'nullable|string|max:255',
|
'preferred_location' => 'nullable|string|max:255',
|
||||||
'country' => 'nullable|string|max:100',
|
|
||||||
'city' => 'nullable|string|max:100',
|
|
||||||
'area' => 'nullable|string|max:100',
|
|
||||||
'fcm_token' => 'nullable|string|max:255',
|
'fcm_token' => 'nullable|string|max:255',
|
||||||
|
'passport' => 'nullable|array',
|
||||||
|
'passport.passport_number' => [
|
||||||
|
'nullable',
|
||||||
|
'string',
|
||||||
|
\Illuminate\Validation\Rule::unique('worker_documents', 'number')->where(function ($query) use ($worker) {
|
||||||
|
return $query->where('type', 'passport')->where('worker_id', '!=', $worker->id);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
'visa' => 'nullable|array',
|
||||||
|
], [
|
||||||
|
'phone.unique' => 'This mobile number is already registered.',
|
||||||
|
'passport.passport_number.unique' => 'This passport number is already registered.',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($validator->fails()) {
|
if ($validator->fails()) {
|
||||||
@ -81,27 +105,40 @@ public function updateProfile(Request $request)
|
|||||||
], 422);
|
], 422);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Document Update Check: Verify passport number matches existing passport number if already verified/saved
|
||||||
|
$existingPassport = $worker->documents()->where('type', 'passport')->first();
|
||||||
|
if ($existingPassport && $request->has('passport')) {
|
||||||
|
$passportData = $request->input('passport');
|
||||||
|
if ($passportData) {
|
||||||
|
$newPassportNumber = $passportData['passport_number'] ?? $passportData['number'] ?? null;
|
||||||
|
if ($newPassportNumber && strcasecmp(trim($existingPassport->number), trim($newPassportNumber)) !== 0) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Validation error.',
|
||||||
|
'errors' => [
|
||||||
|
'passport.passport_number' => ['The passport number cannot be changed. It must match the previously verified passport number.']
|
||||||
|
]
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
DB::transaction(function () use ($request, $worker) {
|
DB::transaction(function () use ($request, $worker, $existingPassport) {
|
||||||
// Update worker attributes
|
// Update worker attributes
|
||||||
$updateData = $request->only([
|
$updateData = $request->only([
|
||||||
'name',
|
'name',
|
||||||
|
'phone',
|
||||||
'age',
|
'age',
|
||||||
'nationality',
|
'nationality',
|
||||||
'language',
|
'language',
|
||||||
'salary',
|
'salary',
|
||||||
'availability',
|
|
||||||
'experience',
|
'experience',
|
||||||
'religion',
|
|
||||||
'bio',
|
|
||||||
'visa_status',
|
'visa_status',
|
||||||
'preferred_job_type',
|
'preferred_job_type',
|
||||||
'gender',
|
'gender',
|
||||||
'live_in_out',
|
'live_in_out',
|
||||||
'preferred_location',
|
'preferred_location',
|
||||||
'country',
|
|
||||||
'city',
|
|
||||||
'area',
|
|
||||||
'fcm_token'
|
'fcm_token'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -120,10 +157,89 @@ public function updateProfile(Request $request)
|
|||||||
if ($request->has('skills')) {
|
if ($request->has('skills')) {
|
||||||
$worker->skills()->sync($request->skills ?: []);
|
$worker->skills()->sync($request->skills ?: []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Process passport document creation/update
|
||||||
|
if ($request->has('passport')) {
|
||||||
|
$passportDataInput = $request->input('passport');
|
||||||
|
if ($passportDataInput) {
|
||||||
|
$expiryDate = $passportDataInput['date_of_expiry'] ?? null;
|
||||||
|
$issueDate = $passportDataInput['date_of_issue'] ?? null;
|
||||||
|
$expiryDateFormatted = $expiryDate ? $this->normaliseDateForController($expiryDate) : null;
|
||||||
|
$issueDateFormatted = $issueDate ? $this->normaliseDateForController($issueDate) : null;
|
||||||
|
|
||||||
|
if ($existingPassport) {
|
||||||
|
$updateFields = [
|
||||||
|
'ocr_data' => $passportDataInput,
|
||||||
|
];
|
||||||
|
if ($expiryDateFormatted) {
|
||||||
|
$updateFields['expiry_date'] = $expiryDateFormatted;
|
||||||
|
}
|
||||||
|
if ($issueDateFormatted) {
|
||||||
|
$updateFields['issue_date'] = $issueDateFormatted;
|
||||||
|
}
|
||||||
|
$existingPassport->update($updateFields);
|
||||||
|
} else {
|
||||||
|
$passportNum = $passportDataInput['passport_number'] ?? $passportDataInput['number'] ?? ('P' . rand(1000000, 9999999));
|
||||||
|
$expiryDateFormatted = $expiryDateFormatted ?: now()->addYears(8)->toDateString();
|
||||||
|
$issueDateFormatted = $issueDateFormatted ?: now()->subYears(3)->toDateString();
|
||||||
|
|
||||||
|
$worker->documents()->create([
|
||||||
|
'type' => 'passport',
|
||||||
|
'number' => $passportNum,
|
||||||
|
'issue_date' => $issueDateFormatted,
|
||||||
|
'expiry_date' => $expiryDateFormatted,
|
||||||
|
'ocr_accuracy' => 99.0,
|
||||||
|
'file_path' => null,
|
||||||
|
'ocr_data' => $passportDataInput,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process visa document creation/update
|
||||||
|
if ($request->has('visa')) {
|
||||||
|
$visaDataInput = $request->input('visa');
|
||||||
|
if ($visaDataInput) {
|
||||||
|
$existingVisa = $worker->documents()->where('type', 'visa')->first();
|
||||||
|
$expiryDate = $visaDataInput['expiry_date'] ?? null;
|
||||||
|
$issueDate = $visaDataInput['issue_date'] ?? null;
|
||||||
|
$expiryDateFormatted = $expiryDate ? $this->normaliseDateForController($expiryDate) : null;
|
||||||
|
$issueDateFormatted = $issueDate ? $this->normaliseDateForController($issueDate) : null;
|
||||||
|
|
||||||
|
if ($existingVisa) {
|
||||||
|
$updateFields = [
|
||||||
|
'ocr_data' => $visaDataInput,
|
||||||
|
];
|
||||||
|
if ($expiryDateFormatted) {
|
||||||
|
$updateFields['expiry_date'] = $expiryDateFormatted;
|
||||||
|
}
|
||||||
|
if ($issueDateFormatted) {
|
||||||
|
$updateFields['issue_date'] = $issueDateFormatted;
|
||||||
|
}
|
||||||
|
$existingVisa->update($updateFields);
|
||||||
|
} else {
|
||||||
|
$visaNum = $visaDataInput['file_number'] ?? $visaDataInput['id_number'] ?? ('V' . rand(1000000, 9999999));
|
||||||
|
$expiryDateFormatted = $expiryDateFormatted ?: now()->addYears(2)->toDateString();
|
||||||
|
$issueDateFormatted = $issueDateFormatted ?: now()->subYears(2)->toDateString();
|
||||||
|
|
||||||
|
$worker->documents()->create([
|
||||||
|
'type' => 'visa',
|
||||||
|
'number' => $visaNum,
|
||||||
|
'issue_date' => $issueDateFormatted,
|
||||||
|
'expiry_date' => $expiryDateFormatted,
|
||||||
|
'ocr_accuracy' => 99.0,
|
||||||
|
'file_path' => null,
|
||||||
|
'ocr_data' => $visaDataInput,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$worker->load(['skills', 'documents']);
|
$worker->load(['skills', 'documents']);
|
||||||
|
|
||||||
|
$worker->makeHidden(['email', 'religion', 'availability', 'bio', 'country', 'city', 'area']);
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => 'Profile updated successfully.',
|
'message' => 'Profile updated successfully.',
|
||||||
@ -552,4 +668,35 @@ public function getDashboard(Request $request)
|
|||||||
], 500);
|
], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* E.g. "14/05/1990" -> "1990-05-14"
|
||||||
|
*/
|
||||||
|
private function normaliseDateForController(?string $raw): ?string
|
||||||
|
{
|
||||||
|
if (empty($raw)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$raw = trim($raw);
|
||||||
|
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $raw)) {
|
||||||
|
return $raw;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (preg_match('/^(\d{1,2})[\/\- ]([A-Za-z0-9]+)[\/\- ](\d{4})$/', $raw, $m)) {
|
||||||
|
$day = str_pad($m[1], 2, '0', STR_PAD_LEFT);
|
||||||
|
$month = $m[2];
|
||||||
|
$year = $m[3];
|
||||||
|
if (is_numeric($month)) {
|
||||||
|
$month = str_pad($month, 2, '0', STR_PAD_LEFT);
|
||||||
|
return "{$year}-{$month}-{$day}";
|
||||||
|
}
|
||||||
|
$ts = strtotime("{$day} {$month} {$year}");
|
||||||
|
return $ts ? date('Y-m-d', $ts) : $raw;
|
||||||
|
}
|
||||||
|
$dt = new \DateTime($raw);
|
||||||
|
return $dt->format('Y-m-d');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $raw;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11035
public/swagger.json
11035
public/swagger.json
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 81 KiB |
@ -22,7 +22,8 @@ import {
|
|||||||
Clock,
|
Clock,
|
||||||
Ban,
|
Ban,
|
||||||
FileEdit,
|
FileEdit,
|
||||||
CheckSquare
|
CheckSquare,
|
||||||
|
MapPin
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
@ -278,123 +279,149 @@ export default function WorkerManagement({ workers }) {
|
|||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow className="bg-gray-50/50 hover:bg-gray-50/50">
|
<TableRow className="bg-gray-50/50 hover:bg-gray-50/50">
|
||||||
<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</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">Languages</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">Nationality</TableHead>
|
||||||
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest py-4">Exp & Languages</TableHead>
|
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest py-4">Preferred Location</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">Accommodation</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">Salary</TableHead>
|
||||||
|
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest py-4">Status</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>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{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">
|
const languages = worker.languages || (worker.language ? worker.language.split(',').map(l => l.trim()) : []);
|
||||||
<TableCell className="py-4 pl-8">
|
const isLiveIn = worker.live_in_out?.toLowerCase().includes('in') || worker.live_in_out?.toLowerCase().includes('stay');
|
||||||
<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">
|
return (
|
||||||
{worker.name.charAt(0)}
|
<TableRow key={worker.id} className="group hover:bg-slate-50/50 transition-colors">
|
||||||
</div>
|
<TableCell className="py-4 pl-8">
|
||||||
<div>
|
<div className="flex items-center gap-3">
|
||||||
<div className="font-bold text-gray-900 text-sm flex items-center gap-1.5">
|
<div className="w-10 h-10 rounded-full bg-[#0F6E56]/10 flex items-center justify-center text-[#0F6E56] font-bold text-sm">
|
||||||
<span>{worker.name}</span>
|
{worker.name.charAt(0)}
|
||||||
{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">
|
|
||||||
<AlertTriangle className="w-2.5 h-2.5 mr-0.5" /> Suspicious
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-400 font-medium mt-0.5">{worker.phone}</div>
|
<div>
|
||||||
</div>
|
<div className="font-bold text-gray-900 text-sm flex items-center gap-1.5">
|
||||||
</div>
|
<span>{worker.name}</span>
|
||||||
</TableCell>
|
{worker.id === 103 && (
|
||||||
<TableCell>
|
<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">
|
||||||
<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' :
|
<AlertTriangle className="w-2.5 h-2.5 mr-0.5" /> Suspicious
|
||||||
worker.availability === 'In Interview' ? 'bg-blue-50 text-blue-600' : 'bg-slate-100 text-slate-500'
|
</Badge>
|
||||||
}`}>
|
|
||||||
{worker.availability}
|
|
||||||
</span>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<div className="flex items-center space-x-1.5">
|
|
||||||
<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`} />
|
|
||||||
</div>
|
|
||||||
<span className={`text-[10px] font-black uppercase tracking-widest text-${worker.verified ? 'emerald' : 'amber'}-600`}>
|
|
||||||
{worker.verified ? 'OCR Verified' : 'Pending Verification'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<div className="text-xs font-bold text-slate-800 flex items-center gap-1.5">
|
|
||||||
<Globe className="w-3.5 h-3.5 text-slate-400" /> {worker.nationality} • {worker.experience}
|
|
||||||
</div>
|
|
||||||
<div className="text-[10px] text-slate-500 font-semibold flex items-center gap-1 mt-0.5">
|
|
||||||
<span className="text-slate-400 uppercase tracking-widest text-[8px] font-black">Lang:</span> {worker.language}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<div className="space-y-0.5 text-xs">
|
|
||||||
{worker.country ? (
|
|
||||||
<>
|
|
||||||
<div className="font-bold text-slate-800">{worker.city}, {worker.country}</div>
|
|
||||||
<div className="text-[10px] text-slate-400 font-bold">{worker.area}</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<span className="text-slate-400 italic">Not set</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
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'
|
|
||||||
: worker.status === 'suspended' ? 'bg-red-100 text-red-800 border-none' : 'bg-slate-50 text-slate-700 border-slate-200'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{worker.status}
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right pr-8">
|
|
||||||
<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
|
|
||||||
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"
|
|
||||||
>
|
|
||||||
{worker.status === 'active' ? (
|
|
||||||
<><Ban className="w-3.5 h-3.5" /> Suspend Account</>
|
|
||||||
) : (
|
|
||||||
<><UserCheck className="w-3.5 h-3.5" /> Activate Account</>
|
|
||||||
)}
|
)}
|
||||||
</DropdownMenuItem>
|
</div>
|
||||||
|
<div className="text-[11px] text-gray-500 font-semibold flex items-center gap-1.5 mt-0.5">
|
||||||
|
<span className={`inline-block w-1.5 h-1.5 rounded-full ${worker.gender === 'Female' ? 'bg-pink-400' : 'bg-sky-400'}`}></span>
|
||||||
|
<span>{worker.gender || 'Female'}</span>
|
||||||
|
{worker.age && <span>• {worker.age} Yrs</span>}
|
||||||
|
</div>
|
||||||
|
<div className="text-[11px] text-gray-400 font-medium flex items-center gap-1 mt-0.5">
|
||||||
|
<Phone className="w-3 h-3 text-slate-400" />
|
||||||
|
<span>{worker.phone}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="py-4">
|
||||||
|
<div className="flex flex-wrap gap-1 max-w-[150px]">
|
||||||
|
{languages.length > 0 ? languages.map((lang, idx) => (
|
||||||
|
<span key={idx} className="bg-slate-100 text-slate-700 px-1.5 py-0.5 rounded text-[10px] font-semibold border border-slate-200/60">
|
||||||
|
{lang}
|
||||||
|
</span>
|
||||||
|
)) : (
|
||||||
|
<span className="text-slate-400 italic text-[11px]">English</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="py-4">
|
||||||
|
<div className="flex items-center gap-1.5 text-xs text-slate-700 font-bold">
|
||||||
|
<Globe className="w-3.5 h-3.5 text-slate-400" />
|
||||||
|
<span>{worker.nationality}</span>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="py-4">
|
||||||
|
<div className="text-xs space-y-0.5">
|
||||||
|
{worker.preferred_location ? (
|
||||||
|
<div className="font-bold text-slate-800 flex items-center gap-1">
|
||||||
|
<MapPin className="w-3.5 h-3.5 text-slate-400 flex-shrink-0" />
|
||||||
|
<span>{worker.preferred_location}</span>
|
||||||
|
</div>
|
||||||
|
) : worker.city ? (
|
||||||
|
<div className="flex items-start gap-1">
|
||||||
|
<MapPin className="w-3.5 h-3.5 text-slate-400 flex-shrink-0 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<div className="font-bold text-slate-800">{worker.city}</div>
|
||||||
|
<div className="text-[10px] text-slate-400 font-bold">{worker.area}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="text-slate-400 italic">Not set</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="py-4">
|
||||||
|
<span className={`px-2 py-0.5 rounded-full text-[10px] font-bold ${
|
||||||
|
isLiveIn ? 'bg-[#0F6E56]/10 text-[#0F6E56]' : 'bg-amber-100 text-amber-800'
|
||||||
|
}`}>
|
||||||
|
{isLiveIn ? 'Live-in' : 'Live-out'}
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="py-4">
|
||||||
|
<div className="text-xs font-bold text-slate-900">
|
||||||
|
{worker.salary ? `${worker.salary.toLocaleString()} AED` : 'N/A'}
|
||||||
|
<span className="text-[10px] text-gray-400 font-medium block">/ month</span>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="py-4">
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={`px-2 py-0.5 rounded-full font-black text-[9px] uppercase tracking-wider block w-fit ${
|
||||||
|
worker.status === 'active'
|
||||||
|
? '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'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{worker.status}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right pr-8 py-4">
|
||||||
|
<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>
|
||||||
|
|
||||||
</DropdownMenuContent>
|
<DropdownMenuItem
|
||||||
</DropdownMenu>
|
onClick={() => handleToggleStatus(worker.id, worker.status === 'active' ? 'suspended' : 'active')}
|
||||||
</div>
|
className="flex items-center gap-2 p-2 rounded-lg cursor-pointer font-bold text-xs text-red-600 hover:bg-red-50"
|
||||||
</TableCell>
|
>
|
||||||
</TableRow>
|
{worker.status === 'active' ? (
|
||||||
))
|
<><Ban className="w-3.5 h-3.5" /> Suspend Account</>
|
||||||
|
) : (
|
||||||
|
<><UserCheck className="w-3.5 h-3.5" /> Activate Account</>
|
||||||
|
)}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})
|
||||||
) : (
|
) : (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={7} className="py-20 text-center">
|
<TableCell colSpan={8} 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>
|
||||||
|
|||||||
@ -216,7 +216,6 @@ public function test_s2_update_experience_and_preferences()
|
|||||||
])->postJson('/api/workers/profile/update', [
|
])->postJson('/api/workers/profile/update', [
|
||||||
'experience' => '3 Years Cooking & Childcare',
|
'experience' => '3 Years Cooking & Childcare',
|
||||||
'salary' => 1800,
|
'salary' => 1800,
|
||||||
'bio' => 'Extremely experienced in Arabic and Western cooking.',
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response->assertStatus(200);
|
$response->assertStatus(200);
|
||||||
@ -633,9 +632,6 @@ public function test_update_profile_with_new_api_fields()
|
|||||||
'gender' => 'female',
|
'gender' => 'female',
|
||||||
'live_in_out' => 'live_in',
|
'live_in_out' => 'live_in',
|
||||||
'preferred_location' => 'Abu Dhabi',
|
'preferred_location' => 'Abu Dhabi',
|
||||||
'country' => 'UAE',
|
|
||||||
'city' => 'Abu Dhabi',
|
|
||||||
'area' => 'Khalifa City',
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response->assertStatus(200);
|
$response->assertStatus(200);
|
||||||
@ -648,9 +644,6 @@ public function test_update_profile_with_new_api_fields()
|
|||||||
'gender' => 'female',
|
'gender' => 'female',
|
||||||
'live_in_out' => 'live_in',
|
'live_in_out' => 'live_in',
|
||||||
'preferred_location' => 'Abu Dhabi',
|
'preferred_location' => 'Abu Dhabi',
|
||||||
'country' => 'UAE',
|
|
||||||
'city' => 'Abu Dhabi',
|
|
||||||
'area' => 'Khalifa City',
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1234,4 +1227,208 @@ public function test_register_validation_fails_if_passport_number_already_exists
|
|||||||
$response->json('errors')['passport.passport_number'][0]
|
$response->json('errors')['passport.passport_number'][0]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test profile update fails if updating a passport document with a different passport number.
|
||||||
|
*/
|
||||||
|
public function test_update_profile_fails_if_passport_number_does_not_match_existing()
|
||||||
|
{
|
||||||
|
$worker = Worker::create([
|
||||||
|
'name' => 'Test Worker',
|
||||||
|
'email' => 'test@example.com',
|
||||||
|
'phone' => '+971503333333',
|
||||||
|
'language' => 'HI',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
'nationality' => 'Indian',
|
||||||
|
'age' => 25,
|
||||||
|
'salary' => 1500,
|
||||||
|
'availability' => 'Immediate',
|
||||||
|
'experience' => 'None',
|
||||||
|
'religion' => 'Not Specified',
|
||||||
|
'bio' => 'New helper',
|
||||||
|
'verified' => false,
|
||||||
|
'status' => 'active',
|
||||||
|
'api_token' => 'bearer-test-token-passport-mismatch',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$worker->documents()->create([
|
||||||
|
'type' => 'passport',
|
||||||
|
'number' => 'ABC123456',
|
||||||
|
'issue_date' => '2020-01-01',
|
||||||
|
'expiry_date' => '2030-01-01',
|
||||||
|
'ocr_accuracy' => 95.0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer bearer-test-token-passport-mismatch',
|
||||||
|
])->postJson('/api/workers/profile/update', [
|
||||||
|
'passport' => [
|
||||||
|
'passport_number' => 'XYZ987654', // Different passport number
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(422)
|
||||||
|
->assertJsonStructure([
|
||||||
|
'success',
|
||||||
|
'message',
|
||||||
|
'errors' => [
|
||||||
|
'passport.passport_number'
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
'The passport number cannot be changed. It must match the previously verified passport number.',
|
||||||
|
$response->json('errors')['passport.passport_number'][0]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test profile update succeeds if passport number matches existing.
|
||||||
|
*/
|
||||||
|
public function test_update_profile_succeeds_if_passport_number_matches_existing()
|
||||||
|
{
|
||||||
|
$worker = Worker::create([
|
||||||
|
'name' => 'Test Worker',
|
||||||
|
'email' => 'test2@example.com',
|
||||||
|
'phone' => '+971503333334',
|
||||||
|
'language' => 'HI',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
'nationality' => 'Indian',
|
||||||
|
'age' => 25,
|
||||||
|
'salary' => 1500,
|
||||||
|
'availability' => 'Immediate',
|
||||||
|
'experience' => 'None',
|
||||||
|
'religion' => 'Not Specified',
|
||||||
|
'bio' => 'New helper',
|
||||||
|
'verified' => false,
|
||||||
|
'status' => 'active',
|
||||||
|
'api_token' => 'bearer-test-token-passport-match',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$worker->documents()->create([
|
||||||
|
'type' => 'passport',
|
||||||
|
'number' => 'ABC123456',
|
||||||
|
'issue_date' => '2020-01-01',
|
||||||
|
'expiry_date' => '2030-01-01',
|
||||||
|
'ocr_accuracy' => 95.0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer bearer-test-token-passport-match',
|
||||||
|
])->postJson('/api/workers/profile/update', [
|
||||||
|
'passport' => [
|
||||||
|
'passport_number' => 'ABC123456', // Same passport number
|
||||||
|
'date_of_expiry' => '2032-01-01', // Updating expiry date
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('worker_documents', [
|
||||||
|
'worker_id' => $worker->id,
|
||||||
|
'type' => 'passport',
|
||||||
|
'number' => 'ABC123456',
|
||||||
|
'expiry_date' => '2032-01-01',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test unique mobile number validation on profile update.
|
||||||
|
*/
|
||||||
|
public function test_update_profile_fails_if_phone_already_exists()
|
||||||
|
{
|
||||||
|
// 1. Create original worker with phone +971504444444
|
||||||
|
Worker::create([
|
||||||
|
'name' => 'Other Worker',
|
||||||
|
'email' => 'other@example.com',
|
||||||
|
'phone' => '+971504444444',
|
||||||
|
'language' => 'HI',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
'nationality' => 'Indian',
|
||||||
|
'age' => 25,
|
||||||
|
'salary' => 1500,
|
||||||
|
'availability' => 'Immediate',
|
||||||
|
'experience' => 'None',
|
||||||
|
'religion' => 'Not Specified',
|
||||||
|
'bio' => 'New helper',
|
||||||
|
'verified' => false,
|
||||||
|
'status' => 'active',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 2. Create current worker with phone +971505555555
|
||||||
|
$worker = Worker::create([
|
||||||
|
'name' => 'Current Worker',
|
||||||
|
'email' => 'current@example.com',
|
||||||
|
'phone' => '+971505555555',
|
||||||
|
'language' => 'HI',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
'nationality' => 'Indian',
|
||||||
|
'age' => 25,
|
||||||
|
'salary' => 1500,
|
||||||
|
'availability' => 'Immediate',
|
||||||
|
'experience' => 'None',
|
||||||
|
'religion' => 'Not Specified',
|
||||||
|
'bio' => 'New helper',
|
||||||
|
'verified' => false,
|
||||||
|
'status' => 'active',
|
||||||
|
'api_token' => 'bearer-test-token-phone-unique',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 3. Attempt to update current worker's phone to the other worker's phone
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer bearer-test-token-phone-unique',
|
||||||
|
])->postJson('/api/workers/profile/update', [
|
||||||
|
'phone' => '+971504444444',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(422)
|
||||||
|
->assertJsonValidationErrors(['phone']);
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
'This mobile number is already registered.',
|
||||||
|
$response->json('errors')['phone'][0]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test deprecated fields are hidden from JSON response.
|
||||||
|
*/
|
||||||
|
public function test_profile_response_does_not_contain_deprecated_fields()
|
||||||
|
{
|
||||||
|
$worker = Worker::create([
|
||||||
|
'name' => 'Test Worker',
|
||||||
|
'email' => 'test_dep@example.com',
|
||||||
|
'phone' => '+971503333335',
|
||||||
|
'language' => 'HI',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
'nationality' => 'Indian',
|
||||||
|
'age' => 25,
|
||||||
|
'salary' => 1500,
|
||||||
|
'availability' => 'Immediate',
|
||||||
|
'experience' => 'None',
|
||||||
|
'religion' => 'Christian',
|
||||||
|
'bio' => 'Some Bio',
|
||||||
|
'country' => 'UAE',
|
||||||
|
'city' => 'Dubai',
|
||||||
|
'area' => 'JLT',
|
||||||
|
'verified' => false,
|
||||||
|
'status' => 'active',
|
||||||
|
'api_token' => 'bearer-test-token-deprecated-fields',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer bearer-test-token-deprecated-fields',
|
||||||
|
])->getJson('/api/workers/profile');
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$json = $response->json('data.worker');
|
||||||
|
|
||||||
|
$this->assertArrayNotHasKey('email', $json);
|
||||||
|
$this->assertArrayNotHasKey('religion', $json);
|
||||||
|
$this->assertArrayNotHasKey('availability', $json);
|
||||||
|
$this->assertArrayNotHasKey('bio', $json);
|
||||||
|
$this->assertArrayNotHasKey('country', $json);
|
||||||
|
$this->assertArrayNotHasKey('city', $json);
|
||||||
|
$this->assertArrayNotHasKey('area', $json);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,7 +23,7 @@ export default defineConfig({
|
|||||||
port: 5173,
|
port: 5173,
|
||||||
cors: true,
|
cors: true,
|
||||||
hmr: {
|
hmr: {
|
||||||
host: '192.168.29.192',
|
host: '192.168.29.131',
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
ignored: ['**/storage/framework/views/**'],
|
ignored: ['**/storage/framework/views/**'],
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user