employer registeration document upload

This commit is contained in:
mohanmd 2026-06-15 14:40:44 +05:30
parent 47d0d64550
commit 5c9458f784
12 changed files with 409 additions and 105 deletions

View File

@ -422,6 +422,11 @@ public function uploadEmiratesId(Request $request)
$extractedIdNumber = '784-' . rand(1970, 2005) . '-' . rand(1000000, 9999999) . '-' . rand(1, 9);
$extractedExpiry = now()->addYears(rand(2, 4))->toDateString();
// We are not storing this document, just extract data and delete this file
if (file_exists(public_path($filePath))) {
unlink(public_path($filePath));
}
// Update or create EmployerProfile
$profile = EmployerProfile::where('user_id', $user->id)->first();
if (!$profile) {
@ -435,7 +440,7 @@ public function uploadEmiratesId(Request $request)
}
$profile->update([
'emirates_id_front' => $filePath,
'emirates_id_front' => null,
'emirates_id_number' => $extractedIdNumber,
'emirates_id_expiry' => $extractedExpiry,
]);
@ -443,7 +448,7 @@ public function uploadEmiratesId(Request $request)
// Sync with corresponding sponsor record if found
if ($sponsor) {
$sponsor->update([
'emirates_id_file' => $filePath,
'emirates_id_file' => null,
]);
}

View File

@ -85,7 +85,6 @@ public function register(Request $request)
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255',
'phone' => 'required|string|regex:/^[0-9]{7,15}$/',
'emirates_id_file' => 'nullable|file|mimes:jpeg,png,jpg,pdf|max:10240',
], [
'phone.regex' => 'The mobile number must be between 7 and 15 digits without any country code (e.g. 501234567).',
]);
@ -99,19 +98,6 @@ public function register(Request $request)
], 409);
}
// Store file if provided
$emiratesIdPath = null;
if ($request->hasFile('emirates_id_file')) {
$destinationPath = public_path('uploads/licenses');
if (!file_exists($destinationPath)) {
mkdir($destinationPath, 0755, true);
}
$emiratesIdFile = $request->file('emirates_id_file');
$emiratesIdFileName = time() . '_emirates_id_' . preg_replace('/[^a-zA-Z0-9_.-]/', '', $emiratesIdFile->getClientOriginalName());
$emiratesIdFile->move($destinationPath, $emiratesIdFileName);
$emiratesIdPath = 'uploads/licenses/' . $emiratesIdFileName;
}
// 3. Generate 6-digit OTP, hash & store with 10-min expiry
$otp = (string) mt_rand(100000, 999999);
$hashedOtp = Hash::make($otp);
@ -122,7 +108,6 @@ public function register(Request $request)
'name' => $request->name,
'email' => $request->email,
'phone' => $request->phone,
'emirates_id_file' => $emiratesIdPath,
],
'employer_otp' => [
'hash' => $hashedOtp,
@ -208,8 +193,8 @@ public function verifyEmail(Request $request)
// Success - mark verified
session(['employer_email_verified' => true]);
return redirect()->route('employer.register-payment')
->with('success', 'Email verified successfully! Please choose a subscription plan to continue.');
return redirect()->route('employer.upload-emirates-id')
->with('success', 'Email verified successfully! Please upload your Emirates ID.');
}
public function resendOtp(Request $request)
@ -258,13 +243,73 @@ public function resendOtp(Request $request)
]);
}
public function showRegisterPayment()
public function showUploadEmiratesId()
{
if (!session()->has('pending_employer_registration') || !session('employer_email_verified')) {
return redirect()->route('employer.register')
->with('error', 'Please complete email verification first.');
}
return Inertia::render('Employer/Auth/UploadEmiratesId', [
'email' => session('pending_employer_registration.email'),
]);
}
public function uploadEmiratesId(Request $request)
{
if (!session()->has('pending_employer_registration') || !session('employer_email_verified')) {
return redirect()->route('employer.register')
->with('error', 'Registration session expired. Please start over.');
}
$request->validate([
'emirates_id_file' => 'required|file|mimes:jpeg,png,jpg,pdf|max:10240',
], [
'emirates_id_file.required' => 'Please upload a clear scan or image of your Emirates ID.',
'emirates_id_file.mimes' => 'The Emirates ID document must be an image (jpg, jpeg, png) or a PDF file.',
'emirates_id_file.max' => 'The document must not exceed 10MB.',
]);
$filePath = null;
if ($request->hasFile('emirates_id_file')) {
$destinationPath = public_path('uploads/documents');
if (!file_exists($destinationPath)) {
mkdir($destinationPath, 0755, true);
}
$file = $request->file('emirates_id_file');
$fileName = time() . '_employer_eid_' . preg_replace('/[^a-zA-Z0-9_.-]/', '', $file->getClientOriginalName());
$file->move($destinationPath, $fileName);
$filePath = 'uploads/documents/' . $fileName;
// OCR Simulated extraction
$extractedIdNumber = '784-' . rand(1970, 2005) . '-' . rand(1000000, 9999999) . '-' . rand(1, 9);
$extractedExpiry = now()->addYears(rand(2, 4))->toDateString();
// We are not storing this document, just extract data and delete this file
if (file_exists(public_path($filePath))) {
unlink(public_path($filePath));
}
$filePath = null; // Ensure we don't save the path
}
$pending = session('pending_employer_registration');
$pending['emirates_id_file'] = null;
$pending['emirates_id_number'] = $extractedIdNumber;
$pending['emirates_id_expiry'] = $extractedExpiry;
session(['pending_employer_registration' => $pending]);
session(['employer_emirates_id_uploaded' => true]);
return redirect()->route('employer.register-payment')
->with('success', 'Emirates ID uploaded and verified successfully.');
}
public function showRegisterPayment()
{
if (!session()->has('pending_employer_registration') || !session('employer_email_verified') || !session('employer_emirates_id_uploaded')) {
return redirect()->route('employer.register')
->with('error', 'Please complete Emirates ID upload first.');
}
return Inertia::render('Employer/Auth/RegisterPayment', [
'email' => session('pending_employer_registration.email'),
'plans' => [
@ -319,7 +364,7 @@ public function storeRegisterPayment(Request $request)
public function showCreatePassword()
{
if (!session()->has('pending_employer_registration') || !session('employer_email_verified') || !session()->has('pending_employer_payment')) {
if (!session()->has('pending_employer_registration') || !session('employer_email_verified') || !session('employer_emirates_id_uploaded') || !session()->has('pending_employer_payment')) {
return redirect()->route('employer.register')
->with('error', 'Please complete payment step first.');
}
@ -344,7 +389,7 @@ public function createPassword(Request $request)
'password.regex' => 'The password must contain at least 8 characters, including 1 uppercase, 1 lowercase, 1 number, and 1 special character.',
]);
if (!session()->has('pending_employer_registration') || !session('employer_email_verified') || !session()->has('pending_employer_payment')) {
if (!session()->has('pending_employer_registration') || !session('employer_email_verified') || !session('employer_emirates_id_uploaded') || !session()->has('pending_employer_payment')) {
return redirect()->route('employer.register')
->with('error', 'Registration session expired. Please start over.');
}
@ -371,6 +416,9 @@ public function createPassword(Request $request)
'verification_status' => 'approved',
'language' => 'English',
'notifications' => true,
'emirates_id_front' => null, // We did not store the file
'emirates_id_number' => $pending['emirates_id_number'] ?? null,
'emirates_id_expiry' => $pending['emirates_id_expiry'] ?? null,
]);
// Create Sponsor
@ -389,7 +437,7 @@ public function createPassword(Request $request)
'otp_verified_at' => now(),
'status' => 'active',
'last_login_at' => now(),
'emirates_id_file' => $pending['emirates_id_file'] ?? null,
'emirates_id_file' => null, // We did not store the file
]);
// Create active subscription in database
@ -425,6 +473,7 @@ public function createPassword(Request $request)
'pending_employer_registration',
'employer_otp',
'employer_email_verified',
'employer_emirates_id_uploaded',
'pending_employer_payment'
]);

1
dummy_eid.pdf Normal file
View File

@ -0,0 +1 @@
Dummy PDF Content

View File

@ -2377,7 +2377,7 @@
"Employer/Auth"
],
"summary": "Register Sponsor (Alias)",
"description": "Legacy route matching the simplified sponsor registration (Name, Email, Phone).",
"description": "Legacy route matching the simplified sponsor registration (Name, Email, Phone) (Step 1).",
"security": [],
"requestBody": {
"required": true,
@ -2474,7 +2474,7 @@
"Employer/Auth"
],
"summary": "Upload Emirates ID scan & Secure OCR Extract during Onboarding",
"description": "Accepts an email and an Emirates ID scan file, performs automatic OCR extraction of card fields (ID number and expiry), updates the employer's profile fields, and associates the file with their sponsor record.",
"description": "Accepts an email and an Emirates ID scan file, performs automatic OCR extraction of card fields (ID number and expiry), updates the employer's profile fields, and associates the file with their sponsor record (Step 3 - REQUIRED). We are not storing this document, just extracting the data and deleting the file.",
"security": [],
"requestBody": {
"required": true,
@ -2549,7 +2549,7 @@
"Employer/Auth"
],
"summary": "Sponsor Subscription Payment",
"description": "Submits a successful plan selection and PayTabs payment confirmation (Step 3).",
"description": "Submits a successful plan selection and PayTabs payment confirmation (Step 4).",
"security": [],
"requestBody": {
"required": true,
@ -2598,7 +2598,7 @@
"Employer/Auth"
],
"summary": "Create Sponsor Account Password",
"description": "Configures and finalizes the portal login password to complete registration (Step 4). Returns bearer token.",
"description": "Configures and finalizes the portal login password to complete registration (Step 5). Returns bearer token.",
"security": [],
"requestBody": {
"required": true,

View File

@ -108,25 +108,26 @@ export default function CreatePassword() {
{[
{ step: 1, label: 'Register' },
{ step: 2, label: 'Verify' },
{ step: 3, label: 'Payment' },
{ step: 4, label: 'Password' }
{ step: 3, label: 'Emirates ID' },
{ step: 4, label: 'Payment' },
{ step: 5, label: 'Password' }
].map((s) => (
<React.Fragment key={s.step}>
<div className="flex items-center space-x-1 sm:space-x-1.5">
<div className={`w-5 h-5 rounded-full flex items-center justify-center font-bold text-[10px] border transition-all duration-300 ${
s.step === 4
s.step === 5
? 'bg-[#185FA5] text-white border-[#185FA5] shadow-xs'
: 'bg-blue-50 text-[#185FA5] border-blue-200'
}`}>
{s.step}
</div>
<span className={`text-[10px] font-bold tracking-tight ${
s.step === 4 ? 'text-[#185FA5]' : 'text-slate-400'
}`}>
{s.label}
</span>
{s.step === 5 && (
<span className="text-[10px] font-bold tracking-tight text-[#185FA5]">
{s.label}
</span>
)}
</div>
{s.step < 4 && (
{s.step < 5 && (
<div className="w-4 sm:w-6 h-0.5 bg-slate-100" />
)}
</React.Fragment>

View File

@ -14,7 +14,6 @@ export default function Register() {
name: '',
email: '',
phone: '',
emirates_id_file: null,
});
const [loading, setLoading] = useState(false);
@ -65,9 +64,6 @@ export default function Register() {
formData.append('name', data.name);
formData.append('email', data.email);
formData.append('phone', data.phone);
if (data.emirates_id_file) {
formData.append('emirates_id_file', data.emirates_id_file);
}
try {
await axios.post('/employer/register', formData, {
@ -102,8 +98,9 @@ export default function Register() {
{[
{ step: 1, label: 'Register' },
{ step: 2, label: 'Verify' },
{ step: 3, label: 'Payment' },
{ step: 4, label: 'Password' }
{ step: 3, label: 'Emirates ID' },
{ step: 4, label: 'Payment' },
{ step: 5, label: 'Password' }
].map((s) => (
<React.Fragment key={s.step}>
<div className="flex items-center space-x-1 sm:space-x-1.5">
@ -116,13 +113,13 @@ export default function Register() {
}`}>
{s.step}
</div>
<span className={`text-[10px] font-bold tracking-tight ${
s.step === 1 ? 'text-[#185FA5]' : 'text-slate-400'
}`}>
{s.label}
</span>
{s.step === 1 && (
<span className="text-[10px] font-bold tracking-tight text-[#185FA5]">
{s.label}
</span>
)}
</div>
{s.step < 4 && (
{s.step < 5 && (
<div className="w-4 sm:w-6 h-0.5 bg-slate-100" />
)}
</React.Fragment>
@ -260,25 +257,6 @@ export default function Register() {
)}
</div>
{/* Emirates ID File */}
<div>
<label className="block text-xs font-semibold text-gray-700 mb-1.5 uppercase tracking-wider font-bold">Emirates ID Document (Optional)</label>
<input
type="file"
accept="image/jpeg,image/png,image/jpg,application/pdf"
onChange={(e) => handleInputChange('emirates_id_file', e.target.files[0])}
className={`w-full px-3.5 py-2.5 rounded-xl border text-sm focus:outline-none focus:ring-2 transition-all file:mr-4 file:py-1 file:px-3 file:rounded-lg file:border-0 file:text-xs file:font-semibold file:bg-[#185FA5]/10 file:text-[#185FA5] hover:file:bg-[#185FA5]/20 cursor-pointer ${
errors.emirates_id_file
? 'border-red-500 focus:ring-red-500/20 focus:border-red-500'
: 'border-slate-300 focus:ring-[#185FA5]/20 focus:border-[#185FA5]'
}`}
/>
{errors.emirates_id_file && (
<p className="mt-1.5 text-xs text-red-500 font-medium">
{Array.isArray(errors.emirates_id_file) ? errors.emirates_id_file[0] : errors.emirates_id_file}
</p>
)}
</div>
<button
type="submit"

View File

@ -64,33 +64,33 @@ export default function RegisterPayment({ email, plans }) {
setLoading(false);
}
};
const renderStepIndicator = () => (
<div className="w-full flex items-center justify-center space-x-1 sm:space-x-2 select-none pb-4 border-b border-slate-100">
{[
{ step: 1, label: 'Register' },
{ step: 2, label: 'Verify' },
{ step: 3, label: 'Payment' },
{ step: 4, label: 'Password' }
{ step: 3, label: 'Emirates ID' },
{ step: 4, label: 'Payment' },
{ step: 5, label: 'Password' }
].map((s) => (
<React.Fragment key={s.step}>
<div className="flex items-center space-x-1 sm:space-x-1.5">
<div className={`w-5 h-5 rounded-full flex items-center justify-center font-bold text-[10px] border transition-all duration-300 ${
s.step === 3
s.step === 4
? 'bg-[#185FA5] text-white border-[#185FA5] shadow-xs'
: s.step < 3
: s.step < 4
? 'bg-blue-50 text-[#185FA5] border-blue-200'
: 'bg-white text-slate-400 border-slate-200'
}`}>
{s.step}
</div>
<span className={`text-[10px] font-bold tracking-tight ${
s.step === 3 ? 'text-[#185FA5]' : 'text-slate-400'
}`}>
{s.label}
</span>
{s.step === 4 && (
<span className="text-[10px] font-bold tracking-tight text-[#185FA5]">
{s.label}
</span>
)}
</div>
{s.step < 4 && (
{s.step < 5 && (
<div className="w-4 sm:w-6 h-0.5 bg-slate-100" />
)}
</React.Fragment>
@ -116,8 +116,11 @@ export default function RegisterPayment({ email, plans }) {
<div className="space-y-3 pt-6">
<span className="inline-block px-3 py-1 bg-white/10 border border-white/20 rounded-full text-xs font-semibold uppercase tracking-wider">
Step 3: Annual Pass
Step 4: Subscription Plan
</span>
</div>
<div className="space-y-3">
<h1 className="text-4xl font-extrabold tracking-tight leading-tight">
Purchase annual subscription to access.
</h1>

View File

@ -0,0 +1,254 @@
import React, { useState } from 'react';
import { Head, Link, router } from '@inertiajs/react';
import axios from 'axios';
import { toast } from 'sonner';
import {
Loader2,
ShieldAlert,
ShieldCheck,
UploadCloud,
FileText,
Users,
DollarSign,
MessageSquare,
ArrowLeft
} from 'lucide-react';
export default function UploadEmiratesId({ email }) {
const [file, setFile] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const handleFileChange = (e) => {
const selectedFile = e.target.files[0];
if (selectedFile) {
if (selectedFile.size > 10240 * 1024) {
setError('The document must not exceed 10MB.');
toast.error('The document must not exceed 10MB.');
return;
}
setFile(selectedFile);
setError(null);
}
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!file) {
setError('Please upload a clear scan or image of your Emirates ID.');
toast.error('Please upload your Emirates ID document.');
return;
}
setLoading(true);
setError(null);
const formData = new FormData();
formData.append('emirates_id_file', file);
try {
await axios.post('/employer/upload-emirates-id', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
toast.success('Emirates ID scanned successfully. Data extracted and document deleted.');
router.visit('/employer/register-payment');
} catch (err) {
if (err.response && err.response.data && err.response.data.errors) {
setError(err.response.data.errors.emirates_id_file || 'Validation failed.');
toast.error('Validation failed. Please verify the document.');
} else {
toast.error('Scan extraction failed. Please try again.');
}
} finally {
setLoading(false);
}
};
const renderStepIndicator = () => (
<div className="w-full flex items-center justify-center space-x-1 sm:space-x-2 select-none pb-4 border-b border-slate-100">
{[
{ step: 1, label: 'Register' },
{ step: 2, label: 'Verify' },
{ step: 3, label: 'Emirates ID' },
{ step: 4, label: 'Payment' },
{ step: 5, label: 'Password' }
].map((s) => (
<React.Fragment key={s.step}>
<div className="flex items-center space-x-1 sm:space-x-1.5">
<div className={`w-5 h-5 rounded-full flex items-center justify-center font-bold text-[10px] border transition-all duration-300 ${
s.step === 3
? 'bg-[#185FA5] text-white border-[#185FA5] shadow-xs'
: s.step < 3
? 'bg-blue-50 text-[#185FA5] border-blue-200'
: 'bg-white text-slate-400 border-slate-200'
}`}>
{s.step}
</div>
{s.step === 3 && (
<span className="text-[10px] font-bold tracking-tight text-[#185FA5]">
{s.label}
</span>
)}
</div>
{s.step < 5 && (
<div className="w-4 sm:w-6 h-0.5 bg-slate-100" />
)}
</React.Fragment>
))}
</div>
);
return (
<div className="min-h-screen grid grid-cols-1 lg:grid-cols-12 bg-slate-50 font-sans">
<Head title="Upload Emirates ID - Migrant Portal" />
{/* Left Brand Panel (Desktop Only) */}
<div className="hidden lg:flex lg:col-span-5 bg-[#185FA5] p-12 flex-col justify-between text-white relative overflow-hidden select-none">
<div className="absolute inset-0 bg-gradient-to-br from-blue-600/20 to-black/30 pointer-events-none" />
<div className="relative z-10 space-y-6">
<div className="flex items-center space-x-3">
<div className="w-10 h-10 bg-white text-[#185FA5] rounded-xl flex items-center justify-center font-bold text-xl shadow-md">
M
</div>
<span className="font-bold text-2xl tracking-tight">Migrant Portal</span>
</div>
<div className="space-y-3 pt-6">
<span className="inline-block px-3 py-1 bg-white/10 border border-white/20 rounded-full text-xs font-semibold uppercase tracking-wider">
Employer Registration
</span>
<h1 className="text-4xl font-extrabold tracking-tight leading-tight">
Find verified domestic workers directly.
</h1>
<p className="text-blue-100 text-sm font-medium">
Unlock direct access to top domestic candidates with a Migrant premium subscription. Find, interview, and hire directly without middlemen.
</p>
</div>
{/* Stat Row */}
<div className="grid grid-cols-3 gap-4 pt-8 border-t border-white/10">
<div className="bg-white/10 backdrop-blur-sm p-4 rounded-xl border border-white/10 text-center space-y-1">
<Users className="w-6 h-6 text-emerald-300 mx-auto" />
<div className="font-bold text-lg">500+</div>
<div className="text-[10px] text-blue-100 uppercase tracking-wider font-semibold">Verified Workers</div>
</div>
<div className="bg-white/10 backdrop-blur-sm p-4 rounded-xl border border-white/10 text-center space-y-1">
<DollarSign className="w-6 h-6 text-emerald-300 mx-auto" />
<div className="font-bold text-lg">Premium</div>
<div className="text-[10px] text-blue-100 uppercase tracking-wider font-semibold">Employer Access</div>
</div>
<div className="bg-white/10 backdrop-blur-sm p-4 rounded-xl border border-white/10 text-center space-y-1">
<MessageSquare className="w-6 h-6 text-emerald-300 mx-auto" />
<div className="font-bold text-lg">Direct</div>
<div className="text-[10px] text-blue-100 uppercase tracking-wider font-semibold">Messaging</div>
</div>
</div>
</div>
<div className="relative z-10 text-xs text-blue-200 font-medium">
© 2026 Migrant. All rights reserved.
</div>
</div>
{/* Right Form Container */}
<div className="col-span-1 lg:col-span-7 flex flex-col justify-center items-center p-6 sm:p-12 overflow-y-auto">
<div className="w-full max-w-lg bg-white rounded-2xl shadow-sm border border-slate-200 p-8 space-y-6">
{renderStepIndicator()}
<div>
<h2 className="text-2xl font-bold text-gray-900 tracking-tight">Emirates ID Verification</h2>
<p className="text-xs text-gray-500 mt-1">Upload your identity document for quick OCR scanning</p>
</div>
{/* Security & Disclaimer Banner */}
<div className="bg-blue-50/70 border border-blue-100 rounded-xl p-4 flex items-start space-x-3">
<ShieldCheck className="w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5" />
<div className="space-y-1">
<h4 className="text-xs font-bold text-blue-900 uppercase tracking-wider">Data Privacy Disclaimer</h4>
<p className="text-[11px] text-blue-700 leading-relaxed font-medium">
We do not store your physical Emirates ID document. Your card is temporarily scanned only to securely extract the ID number and expiry date via automated OCR, and the file is permanently deleted immediately after extraction.
</p>
</div>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
{/* File Upload Area */}
<div>
<label className="block text-xs font-semibold text-gray-700 mb-2 uppercase tracking-wider">
Emirates ID Document
</label>
<div className={`border-2 border-dashed rounded-xl p-6 text-center transition-all relative ${
file
? 'border-emerald-500 bg-emerald-50/20'
: 'border-slate-300 hover:border-[#185FA5] bg-slate-50/50'
}`}>
<input
type="file"
accept="image/jpeg,image/png,image/jpg,application/pdf"
onChange={handleFileChange}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
/>
{!file ? (
<div className="space-y-2">
<UploadCloud className="w-8 h-8 text-slate-400 mx-auto" />
<p className="text-xs font-bold text-gray-700">
Click or Drag image/PDF file here
</p>
<p className="text-[10px] text-gray-400">
Supported: JPG, JPEG, PNG, PDF (Max 10MB)
</p>
</div>
) : (
<div className="flex items-center justify-center space-x-3">
<FileText className="w-8 h-8 text-emerald-600" />
<div className="text-left">
<p className="text-xs font-bold text-gray-800 truncate max-w-[200px]">
{file.name}
</p>
<p className="text-[10px] text-gray-400">
{(file.size / (1024 * 1024)).toFixed(2)} MB Ready for Scan
</p>
</div>
</div>
)}
</div>
{error && (
<p className="mt-2 text-xs text-red-500 font-semibold flex items-center">
<ShieldAlert className="w-3.5 h-3.5 mr-1" />
{error}
</p>
)}
</div>
<button
type="submit"
disabled={loading || !file}
className="w-full bg-[#185FA5] hover:bg-[#0C447C] active:bg-[#08305c] text-white rounded-xl h-11 font-semibold text-sm transition-colors shadow-sm flex items-center justify-center disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? (
<Loader2 className="w-5 h-5 animate-spin mr-2" />
) : (
'Scan & Extract Data'
)}
</button>
</form>
<div className="border-t border-slate-100 pt-6 text-center">
<Link href="/employer/register" className="inline-flex items-center space-x-1 text-xs text-gray-600 font-medium hover:text-[#185FA5] hover:underline">
<ArrowLeft className="w-3.5 h-3.5 mr-1" />
<span>Change Email / Back</span>
</Link>
</div>
</div>
</div>
</div>
);
}

View File

@ -93,14 +93,14 @@ export default function VerifyEmail({ email }) {
setResending(false);
}
};
const renderStepIndicator = () => (
<div className="w-full flex items-center justify-center space-x-1 sm:space-x-2 select-none pb-4 border-b border-slate-100">
{[
{ step: 1, label: 'Register' },
{ step: 2, label: 'Verify' },
{ step: 3, label: 'Payment' },
{ step: 4, label: 'Password' }
{ step: 3, label: 'Emirates ID' },
{ step: 4, label: 'Payment' },
{ step: 5, label: 'Password' }
].map((s) => (
<React.Fragment key={s.step}>
<div className="flex items-center space-x-1 sm:space-x-1.5">
@ -113,13 +113,13 @@ export default function VerifyEmail({ email }) {
}`}>
{s.step}
</div>
<span className={`text-[10px] font-bold tracking-tight ${
s.step === 2 ? 'text-[#185FA5]' : 'text-slate-400'
}`}>
{s.label}
</span>
{s.step === 2 && (
<span className="text-[10px] font-bold tracking-tight text-[#185FA5]">
{s.label}
</span>
)}
</div>
{s.step < 4 && (
{s.step < 5 && (
<div className="w-4 sm:w-6 h-0.5 bg-slate-100" />
)}
</React.Fragment>

View File

@ -230,6 +230,8 @@
Route::get('/employer/verify-email', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'showVerifyEmail'])->name('employer.verify-email');
Route::post('/employer/verify-email', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'verifyEmail'])->name('employer.verify-email.submit')->middleware('throttle:5,1');
Route::post('/employer/resend-otp', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'resendOtp'])->name('employer.resend-otp')->middleware('throttle:3,1');
Route::get('/employer/upload-emirates-id', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'showUploadEmiratesId'])->name('employer.upload-emirates-id');
Route::post('/employer/upload-emirates-id', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'uploadEmiratesId'])->name('employer.upload-emirates-id.submit');
Route::get('/employer/register-payment', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'showRegisterPayment'])->name('employer.register-payment');
Route::post('/employer/register-payment', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'storeRegisterPayment'])->name('employer.register-payment.submit');
Route::get('/employer/create-password', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'showCreatePassword'])->name('employer.create-password');

View File

@ -138,7 +138,7 @@ public function test_employer_can_register()
$user = User::where('email', 'alice@example.com')->first();
$profile = EmployerProfile::where('user_id', $user->id)->first();
$this->assertNotNull($profile->emirates_id_front);
$this->assertNull($profile->emirates_id_front);
$this->assertNotNull($profile->emirates_id_number);
$this->assertNotNull($profile->emirates_id_expiry);
}

View File

@ -28,42 +28,52 @@ public function test_employer_web_registration_with_emirates_id()
'name' => 'Abdullah Ahmed',
'email' => 'abdullah@example.com',
'phone' => '501234567',
'emirates_id_file' => $emiratesIdFile,
]);
$responseStep1->assertRedirect(route('employer.verify-email'));
// Verify session data and file presence
// Verify session data
$pendingReg = session('pending_employer_registration');
$this->assertNotNull($pendingReg);
$this->assertEquals('Abdullah Ahmed', $pendingReg['name']);
$this->assertNotNull($pendingReg['emirates_id_file']);
$this->assertStringContainsString('uploads/licenses/', $pendingReg['emirates_id_file']);
// Step 2: OTP Verification (using local environment dummy code)
$responseStep2 = $this->post('/employer/verify-email', [
'otp' => '000000',
]);
$responseStep2->assertRedirect(route('employer.register-payment'));
$responseStep2->assertRedirect(route('employer.upload-emirates-id'));
$this->assertTrue(session('employer_email_verified'));
// Step 3: Payment Choice
$responseStep3 = $this->post('/employer/register-payment', [
// Step 3: Emirates ID Upload
$responseStep3 = $this->post('/employer/upload-emirates-id', [
'emirates_id_file' => $emiratesIdFile,
]);
$responseStep3->assertRedirect(route('employer.register-payment'));
$this->assertTrue(session('employer_emirates_id_uploaded'));
$pendingRegAfterId = session('pending_employer_registration');
$this->assertNotNull($pendingRegAfterId['emirates_id_number']);
$this->assertNotNull($pendingRegAfterId['emirates_id_expiry']);
$this->assertNull($pendingRegAfterId['emirates_id_file']);
// Step 4: Payment Choice
$responseStep4 = $this->post('/employer/register-payment', [
'plan_id' => 'premium',
'amount_aed' => 199.00,
'paytabs_transaction_id' => 'TRAN_ABC_123',
]);
$responseStep3->assertJson(['message' => 'Payment processed successfully.']);
$responseStep4->assertJson(['message' => 'Payment processed successfully.']);
// Step 4: Create Password & Create Records
$responseStep4 = $this->post('/employer/create-password', [
// Step 5: Create Password & Create Records
$responseStep5 = $this->post('/employer/create-password', [
'password' => 'SecurePass123!',
'password_confirmation' => 'SecurePass123!',
]);
$responseStep4->assertRedirect(route('employer.dashboard'));
$responseStep5->assertRedirect(route('employer.dashboard'));
// Verify Database Records
$this->assertDatabaseHas('users', [
@ -74,16 +84,17 @@ public function test_employer_web_registration_with_emirates_id()
$this->assertDatabaseHas('employer_profiles', [
'phone' => '501234567',
'company_name' => 'Abdullah Ahmed Household',
'emirates_id_front' => null, // File is not stored
]);
$profile = EmployerProfile::where('phone', '501234567')->first();
$this->assertNotNull($profile->emirates_id_number);
$this->assertNotNull($profile->emirates_id_expiry);
$this->assertDatabaseHas('sponsors', [
'email' => 'abdullah@example.com',
'mobile' => '501234567',
'emirates_id_file' => null, // File is not stored
]);
// Check Sponsor has the Emirates ID path matching the session stored file
$sponsor = Sponsor::where('email', 'abdullah@example.com')->first();
$this->assertNotNull($sponsor->emirates_id_file);
$this->assertEquals($pendingReg['emirates_id_file'], $sponsor->emirates_id_file);
}
}