announcement, chat, profile
This commit is contained in:
parent
fef928af26
commit
7aecb53065
157
app/Http/Controllers/Api/EmployerAnnouncementController.php
Normal file
157
app/Http/Controllers/Api/EmployerAnnouncementController.php
Normal file
@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Announcement;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class EmployerAnnouncementController extends Controller
|
||||
{
|
||||
/**
|
||||
* Get all announcements posted by this employer.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function getAnnouncements(Request $request)
|
||||
{
|
||||
/** @var User $employer */
|
||||
$employer = $request->attributes->get('employer');
|
||||
|
||||
try {
|
||||
$announcements = Announcement::where('employer_id', $employer->id)
|
||||
->latest()
|
||||
->get()
|
||||
->map(function ($announcement) {
|
||||
return [
|
||||
'id' => $announcement->id,
|
||||
'title' => $announcement->title,
|
||||
'body' => $announcement->body,
|
||||
'type' => $announcement->type,
|
||||
'created_at' => $announcement->created_at->toISOString(),
|
||||
'time_ago' => $announcement->created_at->diffForHumans(),
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'announcements' => $announcements
|
||||
]
|
||||
], 200);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
logger()->error('Mobile Employer Get Announcements Failure: ' . $e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'An error occurred while fetching announcements.',
|
||||
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new announcement for workers.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function createAnnouncement(Request $request)
|
||||
{
|
||||
/** @var User $employer */
|
||||
$employer = $request->attributes->get('employer');
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'title' => 'required|string|max:255',
|
||||
'body' => 'required|string|max:5000',
|
||||
'type' => 'nullable|string|in:info,warning,success',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Validation error.',
|
||||
'errors' => $validator->errors()
|
||||
], 422);
|
||||
}
|
||||
|
||||
try {
|
||||
$announcement = Announcement::create([
|
||||
'title' => $request->title,
|
||||
'body' => $request->body,
|
||||
'type' => $request->type ?? 'info',
|
||||
'employer_id' => $employer->id,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Announcement posted successfully.',
|
||||
'data' => [
|
||||
'announcement' => [
|
||||
'id' => $announcement->id,
|
||||
'title' => $announcement->title,
|
||||
'body' => $announcement->body,
|
||||
'type' => $announcement->type,
|
||||
'created_at' => $announcement->created_at->toISOString(),
|
||||
'time_ago' => $announcement->created_at->diffForHumans(),
|
||||
]
|
||||
]
|
||||
], 201);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
logger()->error('Mobile Employer Create Announcement Failure: ' . $e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'An error occurred while creating the announcement.',
|
||||
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an announcement.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param int $id
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function deleteAnnouncement(Request $request, $id)
|
||||
{
|
||||
/** @var User $employer */
|
||||
$employer = $request->attributes->get('employer');
|
||||
|
||||
try {
|
||||
$announcement = Announcement::where('employer_id', $employer->id)
|
||||
->where('id', $id)
|
||||
->first();
|
||||
|
||||
if (!$announcement) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Announcement not found or unauthorized.'
|
||||
], 404);
|
||||
}
|
||||
|
||||
$announcement->delete();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Announcement deleted successfully.'
|
||||
], 200);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
logger()->error('Mobile Employer Delete Announcement Failure: ' . $e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'An error occurred while deleting the announcement.',
|
||||
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
170
app/Http/Controllers/Api/EmployerAuthController.php
Normal file
170
app/Http/Controllers/Api/EmployerAuthController.php
Normal file
@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use App\Models\EmployerProfile;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class EmployerAuthController extends Controller
|
||||
{
|
||||
/**
|
||||
* Authenticate an employer and return a secure Bearer token.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function login(Request $request)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'email' => 'required|email',
|
||||
'password' => 'required|string',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Validation error.',
|
||||
'errors' => $validator->errors()
|
||||
], 422);
|
||||
}
|
||||
|
||||
try {
|
||||
$user = User::where('email', $request->email)->where('role', 'employer')->first();
|
||||
|
||||
if (!$user || !Hash::check($request->password, $user->password)) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Invalid email or password.'
|
||||
], 401);
|
||||
}
|
||||
|
||||
$profile = EmployerProfile::where('user_id', $user->id)->first();
|
||||
$verification_status = $profile ? $profile->verification_status : 'approved';
|
||||
|
||||
if ($verification_status === 'pending') {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Your account is pending verification.'
|
||||
], 403);
|
||||
}
|
||||
|
||||
if ($verification_status === 'rejected') {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Your account verification has been rejected.',
|
||||
'reason' => $profile->rejection_reason ?? 'Verification rejected.'
|
||||
], 403);
|
||||
}
|
||||
|
||||
// Generate and assign a fresh API token
|
||||
$apiToken = Str::random(80);
|
||||
$user->update(['api_token' => $apiToken]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Employer logged in successfully.',
|
||||
'data' => [
|
||||
'employer' => $user->load('employerProfile'),
|
||||
'token' => $apiToken
|
||||
]
|
||||
], 200);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
logger()->error('Mobile Employer Login Failure: ' . $e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'An error occurred during login. Please try again.',
|
||||
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new employer via API.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function register(Request $request)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'company_name' => 'required|string|max:255',
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => 'required|string|email|max:255|unique:users,email',
|
||||
'phone' => 'required|string|max:50',
|
||||
'password' => 'required|string|min:8|confirmed',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Validation error.',
|
||||
'errors' => $validator->errors()
|
||||
], 422);
|
||||
}
|
||||
|
||||
try {
|
||||
// Generate API token
|
||||
$apiToken = Str::random(80);
|
||||
|
||||
$user = null;
|
||||
\Illuminate\Support\Facades\DB::transaction(function () use ($request, $apiToken, &$user) {
|
||||
$user = User::create([
|
||||
'name' => $request->name,
|
||||
'email' => $request->email,
|
||||
'password' => Hash::make($request->password),
|
||||
'role' => 'employer',
|
||||
'subscription_status' => 'active', // Auto-active
|
||||
'subscription_expires_at' => now()->addDays(30),
|
||||
'api_token' => $apiToken,
|
||||
]);
|
||||
|
||||
EmployerProfile::create([
|
||||
'user_id' => $user->id,
|
||||
'company_name' => $request->company_name,
|
||||
'phone' => $request->phone,
|
||||
'country' => 'United Arab Emirates',
|
||||
'verification_status' => 'approved', // Auto-approved
|
||||
'language' => 'English',
|
||||
'notifications' => true,
|
||||
]);
|
||||
|
||||
// Create active premium subscription
|
||||
\Illuminate\Support\Facades\DB::table('subscriptions')->insert([
|
||||
'user_id' => $user->id,
|
||||
'plan_id' => 'premium',
|
||||
'amount_aed' => 199.00,
|
||||
'starts_at' => now(),
|
||||
'expires_at' => now()->addDays(30),
|
||||
'status' => 'active',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Employer registered successfully.',
|
||||
'data' => [
|
||||
'employer' => $user->load('employerProfile'),
|
||||
'token' => $apiToken
|
||||
]
|
||||
], 201);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
logger()->error('Mobile Employer Registration Failure: ' . $e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'An error occurred during registration. Please try again.',
|
||||
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
264
app/Http/Controllers/Api/EmployerMessageController.php
Normal file
264
app/Http/Controllers/Api/EmployerMessageController.php
Normal file
@ -0,0 +1,264 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Conversation;
|
||||
use App\Models\Message;
|
||||
use App\Models\Worker;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class EmployerMessageController extends Controller
|
||||
{
|
||||
/**
|
||||
* Get all conversations for the authorized employer.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function getConversations(Request $request)
|
||||
{
|
||||
/** @var User $employer */
|
||||
$employer = $request->attributes->get('employer');
|
||||
|
||||
try {
|
||||
$dbConversations = Conversation::where('employer_id', $employer->id)
|
||||
->with(['worker.category', 'messages'])
|
||||
->latest('updated_at')
|
||||
->get();
|
||||
|
||||
$conversations = $dbConversations->map(function ($conv) {
|
||||
$lastMsg = $conv->messages->last();
|
||||
|
||||
// Calculate unread count (messages sent by worker that are not read by employer)
|
||||
$unreadCount = $conv->messages->filter(function ($msg) {
|
||||
return $msg->sender_type === 'worker' && is_null($msg->read_at);
|
||||
})->count();
|
||||
|
||||
return [
|
||||
'id' => $conv->id,
|
||||
'worker_id' => $conv->worker_id,
|
||||
'worker_name' => $conv->worker->name ?? 'Candidate',
|
||||
'category' => $conv->worker->category->name ?? 'General Helper',
|
||||
'nationality' => $conv->worker->nationality ?? 'Unknown',
|
||||
'salary' => ($conv->worker->salary ?? $conv->worker->expected_salary ?? 2000) . ' AED',
|
||||
'last_message' => $lastMsg->text ?? 'No messages yet.',
|
||||
'unread_count' => $unreadCount,
|
||||
'sent_at' => $lastMsg ? $lastMsg->created_at->diffForHumans() : 'Just now',
|
||||
'updated_at' => $conv->updated_at->toISOString(),
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'conversations' => $conversations
|
||||
]
|
||||
], 200);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
logger()->error('Mobile Employer Get Conversations Failure: ' . $e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'An error occurred while fetching conversations.',
|
||||
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get messages in a single conversation and mark unread incoming messages as read.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param int $id
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function getMessages(Request $request, $id)
|
||||
{
|
||||
/** @var User $employer */
|
||||
$employer = $request->attributes->get('employer');
|
||||
|
||||
try {
|
||||
$conv = Conversation::where('employer_id', $employer->id)
|
||||
->where('id', $id)
|
||||
->with(['worker.category', 'messages'])
|
||||
->first();
|
||||
|
||||
if (!$conv) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Conversation not found.'
|
||||
], 404);
|
||||
}
|
||||
|
||||
// Mark incoming worker messages in this conversation as read
|
||||
Message::where('conversation_id', $conv->id)
|
||||
->where('sender_type', 'worker')
|
||||
->whereNull('read_at')
|
||||
->update(['read_at' => now()]);
|
||||
|
||||
$messages = $conv->messages->map(function ($msg) {
|
||||
return [
|
||||
'id' => $msg->id,
|
||||
'sender_type' => $msg->sender_type, // employer or worker
|
||||
'text' => $msg->text,
|
||||
'time' => $msg->created_at->format('g:i A') . ($msg->created_at->isToday() ? ' Today' : ' ' . $msg->created_at->format('M d')),
|
||||
'read_at' => $msg->read_at ? $msg->read_at->toISOString() : null,
|
||||
'created_at' => $msg->created_at->toISOString(),
|
||||
];
|
||||
});
|
||||
|
||||
$conversationDetail = [
|
||||
'id' => $conv->id,
|
||||
'worker_name' => $conv->worker->name ?? 'Candidate',
|
||||
'category' => $conv->worker->category->name ?? 'General Helper',
|
||||
'nationality' => $conv->worker->nationality ?? 'Unknown',
|
||||
'salary' => ($conv->worker->salary ?? $conv->worker->expected_salary ?? 2000) . ' AED',
|
||||
];
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'conversation' => $conversationDetail,
|
||||
'messages' => $messages
|
||||
]
|
||||
], 200);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
logger()->error('Mobile Employer Get Messages Failure: ' . $e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'An error occurred while fetching messages.',
|
||||
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a reply message from the employer to the worker.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param int $id
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function sendMessage(Request $request, $id)
|
||||
{
|
||||
/** @var User $employer */
|
||||
$employer = $request->attributes->get('employer');
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'text' => 'required|string|max:1000',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Validation error.',
|
||||
'errors' => $validator->errors()
|
||||
], 422);
|
||||
}
|
||||
|
||||
try {
|
||||
$conv = Conversation::where('employer_id', $employer->id)
|
||||
->where('id', $id)
|
||||
->first();
|
||||
|
||||
if (!$conv) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Conversation not found.'
|
||||
], 404);
|
||||
}
|
||||
|
||||
$message = null;
|
||||
DB::transaction(function () use ($conv, $employer, $request, &$message) {
|
||||
$message = Message::create([
|
||||
'conversation_id' => $conv->id,
|
||||
'sender_type' => 'employer',
|
||||
'sender_id' => $employer->id,
|
||||
'text' => $request->text,
|
||||
]);
|
||||
|
||||
// Touch conversation updated_at for sorting
|
||||
$conv->touch();
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Message sent successfully.',
|
||||
'data' => [
|
||||
'message' => [
|
||||
'id' => $message->id,
|
||||
'sender_type' => $message->sender_type,
|
||||
'text' => $message->text,
|
||||
'time' => $message->created_at->format('g:i A') . ' Today',
|
||||
'created_at' => $message->created_at->toISOString(),
|
||||
]
|
||||
]
|
||||
], 201);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
logger()->error('Mobile Employer Send Message Failure: ' . $e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'An error occurred while sending the message.',
|
||||
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a conversation with a specific worker.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function startConversation(Request $request)
|
||||
{
|
||||
/** @var User $employer */
|
||||
$employer = $request->attributes->get('employer');
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'worker_id' => 'required|exists:workers,id',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Validation error.',
|
||||
'errors' => $validator->errors()
|
||||
], 422);
|
||||
}
|
||||
|
||||
try {
|
||||
// Find or create conversation
|
||||
$conv = Conversation::firstOrCreate([
|
||||
'employer_id' => $employer->id,
|
||||
'worker_id' => $request->worker_id,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Conversation started successfully.',
|
||||
'data' => [
|
||||
'conversation_id' => $conv->id
|
||||
]
|
||||
], 200);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
logger()->error('Mobile Employer Start Conversation Failure: ' . $e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'An error occurred while starting the conversation.',
|
||||
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
161
app/Http/Controllers/Api/EmployerProfileController.php
Normal file
161
app/Http/Controllers/Api/EmployerProfileController.php
Normal file
@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use App\Models\EmployerProfile;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class EmployerProfileController extends Controller
|
||||
{
|
||||
/**
|
||||
* Get the authenticated employer's profile details.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function getProfile(Request $request)
|
||||
{
|
||||
/** @var User $employer */
|
||||
$employer = $request->attributes->get('employer');
|
||||
|
||||
try {
|
||||
$profile = $employer->employerProfile;
|
||||
|
||||
if (!$profile) {
|
||||
$profile = EmployerProfile::create([
|
||||
'user_id' => $employer->id,
|
||||
'company_name' => 'Al Mansoor Household',
|
||||
'phone' => '+971 50 123 4567',
|
||||
'verification_status' => 'approved',
|
||||
]);
|
||||
}
|
||||
|
||||
$employerProfile = [
|
||||
'name' => $employer->name,
|
||||
'email' => $employer->email,
|
||||
'company_name' => $profile->company_name,
|
||||
'phone' => $profile->phone,
|
||||
'language' => $profile->language ?? 'English',
|
||||
'notifications' => (bool)($profile->notifications ?? true),
|
||||
'verification_status' => $profile->verification_status ?? 'approved',
|
||||
];
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'profile' => $employerProfile
|
||||
]
|
||||
], 200);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
logger()->error('Mobile Employer Get Profile Failure: ' . $e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'An error occurred while fetching the profile.',
|
||||
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the authenticated employer's profile details.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function updateProfile(Request $request)
|
||||
{
|
||||
/** @var User $employer */
|
||||
$employer = $request->attributes->get('employer');
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => 'required|string|email|max:255|unique:users,email,' . $employer->id,
|
||||
'phone' => 'required|string|max:255',
|
||||
'company_name' => 'required|string|max:255',
|
||||
'language' => 'required|string|in:English,Arabic',
|
||||
'notifications' => 'required|boolean',
|
||||
'current_password' => 'nullable|required_with:new_password|string',
|
||||
'new_password' => 'nullable|string|min:8|confirmed',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Validation error.',
|
||||
'errors' => $validator->errors()
|
||||
], 422);
|
||||
}
|
||||
|
||||
try {
|
||||
// Check current password if new password is provided
|
||||
if ($request->filled('new_password')) {
|
||||
if (!Hash::check($request->current_password, $employer->password)) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'The provided current password does not match.',
|
||||
'errors' => [
|
||||
'current_password' => ['The provided password does not match your current password.']
|
||||
]
|
||||
], 422);
|
||||
}
|
||||
}
|
||||
|
||||
// Update user table
|
||||
$employer->update([
|
||||
'name' => $request->name,
|
||||
'email' => $request->email,
|
||||
]);
|
||||
|
||||
// Update employer_profiles table
|
||||
$profile = $employer->employerProfile;
|
||||
if (!$profile) {
|
||||
$profile = new EmployerProfile(['user_id' => $employer->id]);
|
||||
}
|
||||
$profile->company_name = $request->company_name;
|
||||
$profile->phone = $request->phone;
|
||||
$profile->language = $request->language;
|
||||
$profile->notifications = $request->notifications;
|
||||
$profile->save();
|
||||
|
||||
// Update password if present
|
||||
if ($request->filled('new_password')) {
|
||||
$employer->update([
|
||||
'password' => Hash::make($request->new_password),
|
||||
]);
|
||||
}
|
||||
|
||||
$employerProfile = [
|
||||
'name' => $employer->name,
|
||||
'email' => $employer->email,
|
||||
'company_name' => $profile->company_name,
|
||||
'phone' => $profile->phone,
|
||||
'language' => $profile->language,
|
||||
'notifications' => (bool)$profile->notifications,
|
||||
'verification_status' => $profile->verification_status ?? 'approved',
|
||||
];
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Profile updated successfully.',
|
||||
'data' => [
|
||||
'profile' => $employerProfile
|
||||
]
|
||||
], 200);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
logger()->error('Mobile Employer Update Profile Failure: ' . $e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'An error occurred while updating the profile.',
|
||||
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
53
app/Http/Controllers/Api/WorkerAnnouncementController.php
Normal file
53
app/Http/Controllers/Api/WorkerAnnouncementController.php
Normal file
@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Announcement;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class WorkerAnnouncementController extends Controller
|
||||
{
|
||||
/**
|
||||
* Get all announcements for workers.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function getAnnouncements(Request $request)
|
||||
{
|
||||
try {
|
||||
$announcements = Announcement::with('employer.employerProfile')
|
||||
->latest()
|
||||
->get()
|
||||
->map(function ($announcement) {
|
||||
return [
|
||||
'id' => $announcement->id,
|
||||
'title' => $announcement->title,
|
||||
'body' => $announcement->body,
|
||||
'type' => $announcement->type,
|
||||
'employer_name' => $announcement->employer->name ?? 'System',
|
||||
'company_name' => $announcement->employer->employerProfile->company_name ?? 'Migrant Support',
|
||||
'created_at' => $announcement->created_at->toISOString(),
|
||||
'time_ago' => $announcement->created_at->diffForHumans(),
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'announcements' => $announcements
|
||||
]
|
||||
], 200);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
logger()->error('Mobile Worker Get Announcements Failure: ' . $e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'An error occurred while fetching announcements.',
|
||||
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
210
app/Http/Controllers/Api/WorkerMessageController.php
Normal file
210
app/Http/Controllers/Api/WorkerMessageController.php
Normal file
@ -0,0 +1,210 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Conversation;
|
||||
use App\Models\Message;
|
||||
use App\Models\Worker;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class WorkerMessageController extends Controller
|
||||
{
|
||||
/**
|
||||
* Get all conversations for the authorized worker.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function getConversations(Request $request)
|
||||
{
|
||||
/** @var Worker $worker */
|
||||
$worker = $request->attributes->get('worker');
|
||||
|
||||
try {
|
||||
$dbConversations = Conversation::where('worker_id', $worker->id)
|
||||
->with(['employer.employerProfile', 'messages'])
|
||||
->latest('updated_at')
|
||||
->get();
|
||||
|
||||
$conversations = $dbConversations->map(function ($conv) {
|
||||
$lastMsg = $conv->messages->last();
|
||||
|
||||
// Calculate unread count (messages sent by employer that are not read by worker)
|
||||
$unreadCount = $conv->messages->filter(function ($msg) {
|
||||
return $msg->sender_type === 'employer' && is_null($msg->read_at);
|
||||
})->count();
|
||||
|
||||
return [
|
||||
'id' => $conv->id,
|
||||
'employer_id' => $conv->employer_id,
|
||||
'employer_name' => $conv->employer->name ?? 'Employer',
|
||||
'company_name' => $conv->employer->employerProfile->company_name ?? 'Individual Employer',
|
||||
'last_message' => $lastMsg->text ?? 'No messages yet.',
|
||||
'unread_count' => $unreadCount,
|
||||
'sent_at' => $lastMsg ? $lastMsg->created_at->diffForHumans() : 'Just now',
|
||||
'updated_at' => $conv->updated_at->toISOString(),
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'conversations' => $conversations
|
||||
]
|
||||
], 200);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
logger()->error('Mobile Worker Get Conversations Failure: ' . $e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'An error occurred while fetching conversations.',
|
||||
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get messages in a single conversation and mark unread incoming messages as read.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param int $id
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function getMessages(Request $request, $id)
|
||||
{
|
||||
/** @var Worker $worker */
|
||||
$worker = $request->attributes->get('worker');
|
||||
|
||||
try {
|
||||
$conv = Conversation::where('worker_id', $worker->id)
|
||||
->where('id', $id)
|
||||
->with(['employer.employerProfile', 'messages'])
|
||||
->first();
|
||||
|
||||
if (!$conv) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Conversation not found.'
|
||||
], 404);
|
||||
}
|
||||
|
||||
// Mark incoming employer messages in this conversation as read
|
||||
Message::where('conversation_id', $conv->id)
|
||||
->where('sender_type', 'employer')
|
||||
->whereNull('read_at')
|
||||
->update(['read_at' => now()]);
|
||||
|
||||
$messages = $conv->messages->map(function ($msg) {
|
||||
return [
|
||||
'id' => $msg->id,
|
||||
'sender_type' => $msg->sender_type, // employer or worker
|
||||
'text' => $msg->text,
|
||||
'time' => $msg->created_at->format('g:i A') . ($msg->created_at->isToday() ? ' Today' : ' ' . $msg->created_at->format('M d')),
|
||||
'read_at' => $msg->read_at ? $msg->read_at->toISOString() : null,
|
||||
'created_at' => $msg->created_at->toISOString(),
|
||||
];
|
||||
});
|
||||
|
||||
$conversationDetail = [
|
||||
'id' => $conv->id,
|
||||
'employer_name' => $conv->employer->name ?? 'Employer',
|
||||
'company_name' => $conv->employer->employerProfile->company_name ?? 'Individual Employer',
|
||||
];
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'conversation' => $conversationDetail,
|
||||
'messages' => $messages
|
||||
]
|
||||
], 200);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
logger()->error('Mobile Worker Get Messages Failure: ' . $e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'An error occurred while fetching messages.',
|
||||
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a reply message from the worker to the employer.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param int $id
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function sendMessage(Request $request, $id)
|
||||
{
|
||||
/** @var Worker $worker */
|
||||
$worker = $request->attributes->get('worker');
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'text' => 'required|string|max:1000',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Validation error.',
|
||||
'errors' => $validator->errors()
|
||||
], 422);
|
||||
}
|
||||
|
||||
try {
|
||||
$conv = Conversation::where('worker_id', $worker->id)
|
||||
->where('id', $id)
|
||||
->first();
|
||||
|
||||
if (!$conv) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Conversation not found.'
|
||||
], 404);
|
||||
}
|
||||
|
||||
$message = null;
|
||||
DB::transaction(function () use ($conv, $worker, $request, &$message) {
|
||||
$message = Message::create([
|
||||
'conversation_id' => $conv->id,
|
||||
'sender_type' => 'worker',
|
||||
'sender_id' => $worker->id,
|
||||
'text' => $request->text,
|
||||
]);
|
||||
|
||||
// Touch conversation updated_at for sorting
|
||||
$conv->touch();
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Message sent successfully.',
|
||||
'data' => [
|
||||
'message' => [
|
||||
'id' => $message->id,
|
||||
'sender_type' => $message->sender_type,
|
||||
'text' => $message->text,
|
||||
'time' => $message->created_at->format('g:i A') . ' Today',
|
||||
'created_at' => $message->created_at->toISOString(),
|
||||
]
|
||||
]
|
||||
], 201);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
logger()->error('Mobile Worker Send Message Failure: ' . $e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'An error occurred while sending the message.',
|
||||
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
44
app/Http/Middleware/EmployerApiMiddleware.php
Normal file
44
app/Http/Middleware/EmployerApiMiddleware.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Models\User;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class EmployerApiMiddleware
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
$authHeader = $request->header('Authorization');
|
||||
|
||||
if (!$authHeader || !str_starts_with($authHeader, 'Bearer ')) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Authentication token required.'
|
||||
], 401);
|
||||
}
|
||||
|
||||
$token = substr($authHeader, 7);
|
||||
$user = User::where('api_token', $token)->where('role', 'employer')->first();
|
||||
|
||||
if (!$user) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Invalid or expired authentication token.'
|
||||
], 401);
|
||||
}
|
||||
|
||||
// Attach employer object to request attributes so controllers can read it using $request->attributes->get('employer')
|
||||
$request->attributes->set('employer', $user);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@ -13,5 +13,14 @@ class Announcement extends Model
|
||||
'title',
|
||||
'body',
|
||||
'type',
|
||||
'employer_id',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the employer that created the announcement.
|
||||
*/
|
||||
public function employer()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'employer_id');
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,8 +10,8 @@
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
#[Fillable(['name', 'email', 'password', 'role', 'status', 'subscription_status', 'subscription_expires_at'])]
|
||||
#[Hidden(['password', 'remember_token'])]
|
||||
#[Fillable(['name', 'email', 'password', 'role', 'status', 'subscription_status', 'subscription_expires_at', 'api_token'])]
|
||||
#[Hidden(['password', 'remember_token', 'api_token'])]
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<UserFactory> */
|
||||
@ -55,4 +55,14 @@ public function emiratesIdVerification()
|
||||
{
|
||||
return $this->hasOne(EmployerProfile::class, 'user_id');
|
||||
}
|
||||
|
||||
public function employerProfile()
|
||||
{
|
||||
return $this->hasOne(EmployerProfile::class, 'user_id');
|
||||
}
|
||||
|
||||
public function announcements()
|
||||
{
|
||||
return $this->hasMany(Announcement::class, 'employer_id');
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,6 +20,7 @@
|
||||
'admin' => \App\Http\Middleware\AdminMiddleware::class,
|
||||
'employer' => \App\Http\Middleware\EmployerMiddleware::class,
|
||||
'auth.worker' => \App\Http\Middleware\WorkerApiMiddleware::class,
|
||||
'auth.employer' => \App\Http\Middleware\EmployerApiMiddleware::class,
|
||||
]);
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions): void {
|
||||
|
||||
@ -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('users', function (Blueprint $table) {
|
||||
$table->string('api_token', 80)->nullable()->unique()->after('password');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('api_token');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,29 @@
|
||||
<?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('announcements', function (Blueprint $table) {
|
||||
$table->foreignId('employer_id')->nullable()->constrained('users')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('announcements', function (Blueprint $table) {
|
||||
$table->dropForeign(['employer_id']);
|
||||
$table->dropColumn('employer_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -19,6 +19,9 @@
|
||||
"paths": {
|
||||
"/workers/register": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Worker/Auth"
|
||||
],
|
||||
"summary": "Register Worker & Upload Documents (Unified API)",
|
||||
"description": "Performs worker registration and uploads their passport/visa documents in a single, robust multipart form request. Generates and returns a secure stateless bearer token upon success. Password is required (min 6 characters).",
|
||||
"security": [],
|
||||
@ -153,6 +156,9 @@
|
||||
},
|
||||
"/workers/login": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Worker/Auth"
|
||||
],
|
||||
"summary": "Authenticate Worker",
|
||||
"description": "Validates worker credentials (mobile phone number and password) and generates a secure, stateless Bearer token for api access.",
|
||||
"security": [],
|
||||
@ -241,6 +247,9 @@
|
||||
},
|
||||
"/workers/profile": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Worker/Profile"
|
||||
],
|
||||
"summary": "Get Profile details",
|
||||
"description": "Retrieves the authenticated worker's profile details including their selected job category, connected skills, and uploaded documents.",
|
||||
"responses": {
|
||||
@ -273,6 +282,9 @@
|
||||
},
|
||||
"/workers/profile/update": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Worker/Profile"
|
||||
],
|
||||
"summary": "Update Profile details",
|
||||
"description": "Allows workers to customize and complete their profiles by updating age, nationality, desired salary, bio, availability, experience, religion, categories, and master skills list.",
|
||||
"requestBody": {
|
||||
@ -364,6 +376,9 @@
|
||||
},
|
||||
"/workers/offers": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Worker/Offers"
|
||||
],
|
||||
"summary": "View Received Job Offers",
|
||||
"description": "Returns a list of all custom job/hiring offers received by the worker from different employers (e.g. Work Date, location, salaries, and notes).",
|
||||
"responses": {
|
||||
@ -399,6 +414,9 @@
|
||||
},
|
||||
"/workers/offers/{id}/respond": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Worker/Offers"
|
||||
],
|
||||
"summary": "Accept or Reject Job Offer",
|
||||
"description": "Responds to a pending job offer. If accepted, the job offer status becomes 'accepted' and the worker's status is automatically changed to 'Hired', which reflects immediately in the Employer panel.",
|
||||
"parameters": [
|
||||
@ -471,6 +489,478 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/workers/conversations": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Worker/Conversations"
|
||||
],
|
||||
"summary": "Get Conversations List (Worker)",
|
||||
"description": "Retrieves all active conversations for the authenticated worker.",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Conversations list retrieved successfully."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/workers/conversations/{id}/messages": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Worker/Conversations"
|
||||
],
|
||||
"summary": "Get Conversation Messages (Worker)",
|
||||
"description": "Retrieves message history for a conversation and marks incoming employer messages as read.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Messages retrieved successfully."
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"Worker/Conversations"
|
||||
],
|
||||
"summary": "Send Reply Message (Worker)",
|
||||
"description": "Sends a reply message from the worker to the employer.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"text"
|
||||
],
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "string",
|
||||
"example": "Yes, I am available."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Message sent successfully."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/employers/register": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Employer/Auth"
|
||||
],
|
||||
"summary": "Register Employer",
|
||||
"description": "Registers a new employer profile and triggers premium access instantly.",
|
||||
"security": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"company_name",
|
||||
"name",
|
||||
"email",
|
||||
"phone",
|
||||
"password",
|
||||
"password_confirmation"
|
||||
],
|
||||
"properties": {
|
||||
"company_name": {
|
||||
"type": "string",
|
||||
"example": "Ahmad Tech Ltd"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"example": "Ahmad"
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"example": "ahmad@example.com"
|
||||
},
|
||||
"phone": {
|
||||
"type": "string",
|
||||
"example": "+971509990001"
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"example": "Password@123"
|
||||
},
|
||||
"password_confirmation": {
|
||||
"type": "string",
|
||||
"example": "Password@123"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Employer registered successfully."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/employers/login": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Employer/Auth"
|
||||
],
|
||||
"summary": "Authenticate Employer",
|
||||
"description": "Validates employer email and password and returns a stateless Bearer token.",
|
||||
"security": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"email",
|
||||
"password"
|
||||
],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"example": "ahmad@example.com"
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"example": "Password@123"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Employer logged in successfully."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/employers/conversations": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Employer/Conversations"
|
||||
],
|
||||
"summary": "Get Conversations List (Employer)",
|
||||
"description": "Retrieves all active candidate conversations for the authenticated employer.",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Conversations list retrieved successfully."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/employers/conversations/{id}/messages": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Employer/Conversations"
|
||||
],
|
||||
"summary": "Get Conversation Messages (Employer)",
|
||||
"description": "Retrieves message history for a conversation and marks incoming worker messages as read.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Messages retrieved successfully."
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"Employer/Conversations"
|
||||
],
|
||||
"summary": "Send Reply Message (Employer)",
|
||||
"description": "Sends a reply message from the employer to the worker.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"text"
|
||||
],
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "string",
|
||||
"example": "When can you start?"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Message sent successfully."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/employers/conversations/start": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Employer/Conversations"
|
||||
],
|
||||
"summary": "Start Conversation with Worker",
|
||||
"description": "Starts or retrieves a conversation with the specified worker ID.",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"worker_id"
|
||||
],
|
||||
"properties": {
|
||||
"worker_id": {
|
||||
"type": "integer",
|
||||
"example": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Conversation started successfully."
|
||||
}
|
||||
}
|
||||
},
|
||||
"/workers/announcements": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Worker/Announcements"
|
||||
],
|
||||
"summary": "Get Announcements (Worker)",
|
||||
"description": "Allows workers to retrieve the list of active announcements posted by employers or the system admin.",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "List of announcements retrieved successfully."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/employers/announcements": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Employer/Announcements"
|
||||
],
|
||||
"summary": "Get Posted Announcements (Employer)",
|
||||
"description": "Allows employers to retrieve the list of announcements they have created.",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "List of posted announcements retrieved successfully."
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"Employer/Announcements"
|
||||
],
|
||||
"summary": "Create Announcement (Employer)",
|
||||
"description": "Allows employers to broadcast a new announcement to all workers.",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"title",
|
||||
"body"
|
||||
],
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"example": "Weekend Maintenance"
|
||||
},
|
||||
"body": {
|
||||
"type": "string",
|
||||
"example": "Our services will experience brief downtime this Sunday."
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"info",
|
||||
"warning",
|
||||
"success"
|
||||
],
|
||||
"example": "warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Announcement created successfully."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/employers/announcements/{id}": {
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Employer/Announcements"
|
||||
],
|
||||
"summary": "Delete Announcement (Employer)",
|
||||
"description": "Allows employers to delete a previously created announcement.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Announcement deleted successfully."
|
||||
}
|
||||
}
|
||||
},
|
||||
"/employers/profile": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Employer/Profile"
|
||||
],
|
||||
"summary": "Get Profile details (Employer)",
|
||||
"description": "Retrieves the authenticated employer's profile details including their selected company name, phone, language preference, and notification settings.",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Employer profile details retrieved successfully."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/employers/profile/update": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Employer/Profile"
|
||||
],
|
||||
"summary": "Update Profile details (Employer)",
|
||||
"description": "Allows employers to update their profile details (company name, phone number, name, email, language preference, and notification settings) and change their account password.",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name",
|
||||
"email",
|
||||
"phone",
|
||||
"company_name",
|
||||
"language",
|
||||
"notifications"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"example": "Ahmad"
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"example": "ahmad@example.com"
|
||||
},
|
||||
"phone": {
|
||||
"type": "string",
|
||||
"example": "+971509990001"
|
||||
},
|
||||
"company_name": {
|
||||
"type": "string",
|
||||
"example": "Ahmad Tech Ltd"
|
||||
},
|
||||
"language": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"English",
|
||||
"Arabic"
|
||||
],
|
||||
"example": "English"
|
||||
},
|
||||
"notifications": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"current_password": {
|
||||
"type": "string",
|
||||
"example": "Password@123"
|
||||
},
|
||||
"new_password": {
|
||||
"type": "string",
|
||||
"example": "NewSecurePassword@123"
|
||||
},
|
||||
"new_password_confirmation": {
|
||||
"type": "string",
|
||||
"example": "NewSecurePassword@123"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Profile updated successfully."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
|
||||
@ -3,6 +3,12 @@
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Http\Controllers\Api\WorkerAuthController;
|
||||
use App\Http\Controllers\Api\WorkerProfileController;
|
||||
use App\Http\Controllers\Api\WorkerMessageController;
|
||||
use App\Http\Controllers\Api\WorkerAnnouncementController;
|
||||
use App\Http\Controllers\Api\EmployerAuthController;
|
||||
use App\Http\Controllers\Api\EmployerMessageController;
|
||||
use App\Http\Controllers\Api\EmployerAnnouncementController;
|
||||
use App\Http\Controllers\Api\EmployerProfileController;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
@ -19,6 +25,10 @@
|
||||
Route::post('/workers/register', [WorkerAuthController::class, 'register']);
|
||||
Route::post('/workers/login', [WorkerAuthController::class, 'login']);
|
||||
|
||||
// Unprotected Employer Mobile Auth Endpoints
|
||||
Route::post('/employers/register', [EmployerAuthController::class, 'register']);
|
||||
Route::post('/employers/login', [EmployerAuthController::class, 'login']);
|
||||
|
||||
// Protected Worker Mobile Endpoints (Token Authenticated via Bearer Token)
|
||||
Route::middleware(['auth.worker'])->group(function () {
|
||||
// Profile Management
|
||||
@ -28,4 +38,30 @@
|
||||
// Job Offers Management
|
||||
Route::get('/workers/offers', [WorkerProfileController::class, 'getOffers']);
|
||||
Route::post('/workers/offers/{id}/respond', [WorkerProfileController::class, 'respondToOffer']);
|
||||
|
||||
// Messaging Management
|
||||
Route::get('/workers/conversations', [WorkerMessageController::class, 'getConversations']);
|
||||
Route::get('/workers/conversations/{id}/messages', [WorkerMessageController::class, 'getMessages']);
|
||||
Route::post('/workers/conversations/{id}/messages', [WorkerMessageController::class, 'sendMessage']);
|
||||
|
||||
// Announcement Management
|
||||
Route::get('/workers/announcements', [WorkerAnnouncementController::class, 'getAnnouncements']);
|
||||
});
|
||||
|
||||
// Protected Employer Mobile Endpoints (Token Authenticated via Bearer Token)
|
||||
Route::middleware(['auth.employer'])->group(function () {
|
||||
// Profile Management
|
||||
Route::get('/employers/profile', [EmployerProfileController::class, 'getProfile']);
|
||||
Route::post('/employers/profile/update', [EmployerProfileController::class, 'updateProfile']);
|
||||
|
||||
// Messaging Management
|
||||
Route::get('/employers/conversations', [EmployerMessageController::class, 'getConversations']);
|
||||
Route::get('/employers/conversations/{id}/messages', [EmployerMessageController::class, 'getMessages']);
|
||||
Route::post('/employers/conversations/{id}/messages', [EmployerMessageController::class, 'sendMessage']);
|
||||
Route::post('/employers/conversations/start', [EmployerMessageController::class, 'startConversation']);
|
||||
|
||||
// Announcement Management
|
||||
Route::get('/employers/announcements', [EmployerAnnouncementController::class, 'getAnnouncements']);
|
||||
Route::post('/employers/announcements', [EmployerAnnouncementController::class, 'createAnnouncement']);
|
||||
Route::delete('/employers/announcements/{id}', [EmployerAnnouncementController::class, 'deleteAnnouncement']);
|
||||
});
|
||||
|
||||
232
tests/Feature/AnnouncementApiTest.php
Normal file
232
tests/Feature/AnnouncementApiTest.php
Normal file
@ -0,0 +1,232 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Announcement;
|
||||
use App\Models\User;
|
||||
use App\Models\EmployerProfile;
|
||||
use App\Models\Worker;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AnnouncementApiTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected $worker;
|
||||
protected $employer;
|
||||
protected $workerToken;
|
||||
protected $employerToken;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
// Create a category
|
||||
$category = \App\Models\WorkerCategory::create([
|
||||
'name' => 'General Helper',
|
||||
]);
|
||||
|
||||
// Create worker with API token
|
||||
$this->workerToken = 'worker-token-xyz';
|
||||
$this->worker = Worker::create([
|
||||
'name' => 'Jane Helper',
|
||||
'email' => 'jane@example.com',
|
||||
'phone' => '+971500000001',
|
||||
'password' => bcrypt('password'),
|
||||
'api_token' => $this->workerToken,
|
||||
'status' => 'Available',
|
||||
'nationality' => 'Indian',
|
||||
'age' => 25,
|
||||
'salary' => 1600.00,
|
||||
'availability' => 'Immediate',
|
||||
'experience' => '3 Years',
|
||||
'religion' => 'Hindu',
|
||||
'bio' => 'Experienced helper ready to work.',
|
||||
'category_id' => $category->id,
|
||||
]);
|
||||
|
||||
// Create employer with API token
|
||||
$this->employerToken = 'employer-token-abc';
|
||||
$this->employer = User::create([
|
||||
'name' => 'Employer One',
|
||||
'email' => 'emp1@example.com',
|
||||
'password' => bcrypt('password'),
|
||||
'role' => 'employer',
|
||||
'api_token' => $this->employerToken,
|
||||
]);
|
||||
|
||||
EmployerProfile::create([
|
||||
'user_id' => $this->employer->id,
|
||||
'company_name' => 'Elite Services',
|
||||
'phone' => '+971500000002',
|
||||
'country' => 'United Arab Emirates',
|
||||
'verification_status' => 'approved',
|
||||
'language' => 'English',
|
||||
'notifications' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test worker can retrieve all announcements.
|
||||
*/
|
||||
public function test_worker_can_get_announcements()
|
||||
{
|
||||
// Create announcement
|
||||
Announcement::create([
|
||||
'title' => 'Important Update',
|
||||
'body' => 'Please keep your profile details updated.',
|
||||
'type' => 'info',
|
||||
'employer_id' => $this->employer->id,
|
||||
]);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer ' . $this->workerToken,
|
||||
])->getJson('/api/workers/announcements');
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJsonStructure([
|
||||
'success',
|
||||
'data' => [
|
||||
'announcements' => [
|
||||
'*' => [
|
||||
'id',
|
||||
'title',
|
||||
'body',
|
||||
'type',
|
||||
'employer_name',
|
||||
'company_name',
|
||||
'created_at',
|
||||
'time_ago',
|
||||
]
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertEquals('Important Update', $response->json('data.announcements.0.title'));
|
||||
$this->assertEquals('Elite Services', $response->json('data.announcements.0.company_name'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test employer can get only their posted announcements.
|
||||
*/
|
||||
public function test_employer_can_get_posted_announcements()
|
||||
{
|
||||
// Announcement by this employer
|
||||
Announcement::create([
|
||||
'title' => 'Employer Ann',
|
||||
'body' => 'Employer specific message.',
|
||||
'type' => 'success',
|
||||
'employer_id' => $this->employer->id,
|
||||
]);
|
||||
|
||||
// Announcement by another employer
|
||||
$otherEmployer = User::create([
|
||||
'name' => 'Employer Two',
|
||||
'email' => 'emp2@example.com',
|
||||
'password' => bcrypt('password'),
|
||||
'role' => 'employer',
|
||||
]);
|
||||
Announcement::create([
|
||||
'title' => 'Other Ann',
|
||||
'body' => 'Other specific message.',
|
||||
'type' => 'warning',
|
||||
'employer_id' => $otherEmployer->id,
|
||||
]);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer ' . $this->employerToken,
|
||||
])->getJson('/api/employers/announcements');
|
||||
|
||||
$response->assertStatus(200);
|
||||
$this->assertCount(1, $response->json('data.announcements'));
|
||||
$this->assertEquals('Employer Ann', $response->json('data.announcements.0.title'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test employer can create a new announcement.
|
||||
*/
|
||||
public function test_employer_can_create_announcement()
|
||||
{
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer ' . $this->employerToken,
|
||||
])->postJson('/api/employers/announcements', [
|
||||
'title' => 'Holiday Notice',
|
||||
'body' => 'Office will be closed tomorrow.',
|
||||
'type' => 'warning'
|
||||
]);
|
||||
|
||||
$response->assertStatus(201)
|
||||
->assertJsonStructure([
|
||||
'success',
|
||||
'message',
|
||||
'data' => [
|
||||
'announcement' => [
|
||||
'id',
|
||||
'title',
|
||||
'body',
|
||||
'type',
|
||||
'created_at',
|
||||
'time_ago',
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('announcements', [
|
||||
'title' => 'Holiday Notice',
|
||||
'employer_id' => $this->employer->id,
|
||||
'type' => 'warning',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test employer can delete their own announcement.
|
||||
*/
|
||||
public function test_employer_can_delete_announcement()
|
||||
{
|
||||
$announcement = Announcement::create([
|
||||
'title' => 'Delete Me',
|
||||
'body' => 'Temporary post.',
|
||||
'type' => 'info',
|
||||
'employer_id' => $this->employer->id,
|
||||
]);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer ' . $this->employerToken,
|
||||
])->deleteJson('/api/employers/announcements/' . $announcement->id);
|
||||
|
||||
$response->assertStatus(200);
|
||||
$this->assertDatabaseMissing('announcements', [
|
||||
'id' => $announcement->id,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test employer cannot delete another employer's announcement.
|
||||
*/
|
||||
public function test_employer_cannot_delete_other_employers_announcement()
|
||||
{
|
||||
$otherEmployer = User::create([
|
||||
'name' => 'Employer Two',
|
||||
'email' => 'emp2@example.com',
|
||||
'password' => bcrypt('password'),
|
||||
'role' => 'employer',
|
||||
]);
|
||||
|
||||
$announcement = Announcement::create([
|
||||
'title' => 'Other Post',
|
||||
'body' => 'Do not touch.',
|
||||
'type' => 'info',
|
||||
'employer_id' => $otherEmployer->id,
|
||||
]);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer ' . $this->employerToken,
|
||||
])->deleteJson('/api/employers/announcements/' . $announcement->id);
|
||||
|
||||
$response->assertStatus(404);
|
||||
$this->assertDatabaseHas('announcements', [
|
||||
'id' => $announcement->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
304
tests/Feature/EmployerMessageApiTest.php
Normal file
304
tests/Feature/EmployerMessageApiTest.php
Normal file
@ -0,0 +1,304 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Conversation;
|
||||
use App\Models\Message;
|
||||
use App\Models\User;
|
||||
use App\Models\EmployerProfile;
|
||||
use App\Models\Worker;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class EmployerMessageApiTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected $worker;
|
||||
protected $employer;
|
||||
protected $token;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
// Create a category
|
||||
$category = \App\Models\WorkerCategory::create([
|
||||
'name' => 'Housemaid',
|
||||
]);
|
||||
|
||||
// Create a test worker
|
||||
$this->worker = Worker::create([
|
||||
'name' => 'Jane Smith',
|
||||
'email' => 'jane@example.com',
|
||||
'phone' => '+971500000001',
|
||||
'password' => bcrypt('password'),
|
||||
'status' => 'Available',
|
||||
'nationality' => 'Ethiopian',
|
||||
'age' => 28,
|
||||
'salary' => 1500.00,
|
||||
'availability' => 'Immediate',
|
||||
'experience' => '2 Years',
|
||||
'religion' => 'Christian',
|
||||
'bio' => 'Experienced housemaid seeking employment.',
|
||||
'category_id' => $category->id,
|
||||
]);
|
||||
|
||||
// Create an employer user with token
|
||||
$this->token = 'employer-test-token-12345';
|
||||
$this->employer = User::create([
|
||||
'name' => 'John Employer',
|
||||
'email' => 'employer_test@example.com',
|
||||
'password' => bcrypt('password'),
|
||||
'role' => 'employer',
|
||||
'api_token' => $this->token,
|
||||
]);
|
||||
|
||||
// Create employer profile
|
||||
EmployerProfile::create([
|
||||
'user_id' => $this->employer->id,
|
||||
'company_name' => 'John Ltd',
|
||||
'phone' => '+971500000002',
|
||||
'country' => 'United Arab Emirates',
|
||||
'verification_status' => 'approved',
|
||||
'language' => 'English',
|
||||
'notifications' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test employer can login via API.
|
||||
*/
|
||||
public function test_employer_can_login()
|
||||
{
|
||||
$response = $this->postJson('/api/employers/login', [
|
||||
'email' => 'employer_test@example.com',
|
||||
'password' => 'password',
|
||||
]);
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJsonStructure([
|
||||
'success',
|
||||
'message',
|
||||
'data' => [
|
||||
'employer',
|
||||
'token'
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertNotNull($response->json('data.token'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test employer can register via API.
|
||||
*/
|
||||
public function test_employer_can_register()
|
||||
{
|
||||
$response = $this->postJson('/api/employers/register', [
|
||||
'company_name' => 'New Company Corp',
|
||||
'name' => 'Alice Employer',
|
||||
'email' => 'alice@example.com',
|
||||
'phone' => '+971500000003',
|
||||
'password' => 'Password@123',
|
||||
'password_confirmation' => 'Password@123',
|
||||
]);
|
||||
|
||||
$response->assertStatus(201)
|
||||
->assertJsonStructure([
|
||||
'success',
|
||||
'message',
|
||||
'data' => [
|
||||
'employer',
|
||||
'token'
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('users', [
|
||||
'email' => 'alice@example.com',
|
||||
'role' => 'employer',
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('employer_profiles', [
|
||||
'company_name' => 'New Company Corp',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test retrieving conversation list for an employer.
|
||||
*/
|
||||
public function test_employer_can_get_conversations()
|
||||
{
|
||||
// Create conversation
|
||||
$conversation = Conversation::create([
|
||||
'employer_id' => $this->employer->id,
|
||||
'worker_id' => $this->worker->id,
|
||||
]);
|
||||
|
||||
// Create a message from worker
|
||||
Message::create([
|
||||
'conversation_id' => $conversation->id,
|
||||
'sender_type' => 'worker',
|
||||
'sender_id' => $this->worker->id,
|
||||
'text' => 'Hello from candidate!',
|
||||
]);
|
||||
|
||||
// Request API
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer ' . $this->token,
|
||||
])->getJson('/api/employers/conversations');
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJsonStructure([
|
||||
'success',
|
||||
'data' => [
|
||||
'conversations' => [
|
||||
'*' => [
|
||||
'id',
|
||||
'worker_id',
|
||||
'worker_name',
|
||||
'category',
|
||||
'nationality',
|
||||
'salary',
|
||||
'last_message',
|
||||
'unread_count',
|
||||
'sent_at',
|
||||
'updated_at',
|
||||
]
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertEquals('Hello from candidate!', $response->json('data.conversations.0.last_message'));
|
||||
$this->assertEquals(1, $response->json('data.conversations.0.unread_count'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test retrieving messages in a conversation and marking them as read.
|
||||
*/
|
||||
public function test_employer_can_get_messages_and_mark_as_read()
|
||||
{
|
||||
// Create conversation
|
||||
$conversation = Conversation::create([
|
||||
'employer_id' => $this->employer->id,
|
||||
'worker_id' => $this->worker->id,
|
||||
]);
|
||||
|
||||
// Create message
|
||||
$message = Message::create([
|
||||
'conversation_id' => $conversation->id,
|
||||
'sender_type' => 'worker',
|
||||
'sender_id' => $this->worker->id,
|
||||
'text' => 'Unread message from candidate',
|
||||
'read_at' => null,
|
||||
]);
|
||||
|
||||
// Request API
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer ' . $this->token,
|
||||
])->getJson('/api/employers/conversations/' . $conversation->id . '/messages');
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJsonStructure([
|
||||
'success',
|
||||
'data' => [
|
||||
'conversation' => [
|
||||
'id',
|
||||
'worker_name',
|
||||
'category',
|
||||
'nationality',
|
||||
'salary',
|
||||
],
|
||||
'messages' => [
|
||||
'*' => [
|
||||
'id',
|
||||
'sender_type',
|
||||
'text',
|
||||
'time',
|
||||
'read_at',
|
||||
'created_at',
|
||||
]
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
// Verify message marked as read in database
|
||||
$this->assertNotNull($message->fresh()->read_at);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test employer can send reply.
|
||||
*/
|
||||
public function test_employer_can_send_reply()
|
||||
{
|
||||
// Create conversation
|
||||
$conversation = Conversation::create([
|
||||
'employer_id' => $this->employer->id,
|
||||
'worker_id' => $this->worker->id,
|
||||
]);
|
||||
|
||||
// Reply request
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer ' . $this->token,
|
||||
])->postJson('/api/employers/conversations/' . $conversation->id . '/messages', [
|
||||
'text' => 'Hello Candidate, when are you free for an interview?'
|
||||
]);
|
||||
|
||||
$response->assertStatus(201)
|
||||
->assertJsonStructure([
|
||||
'success',
|
||||
'message',
|
||||
'data' => [
|
||||
'message' => [
|
||||
'id',
|
||||
'sender_type',
|
||||
'text',
|
||||
'time',
|
||||
'created_at',
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
// Verify message created in database
|
||||
$this->assertDatabaseHas('messages', [
|
||||
'conversation_id' => $conversation->id,
|
||||
'sender_type' => 'employer',
|
||||
'sender_id' => $this->employer->id,
|
||||
'text' => 'Hello Candidate, when are you free for an interview?',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test employer can start conversation.
|
||||
*/
|
||||
public function test_employer_can_start_conversation()
|
||||
{
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer ' . $this->token,
|
||||
])->postJson('/api/employers/conversations/start', [
|
||||
'worker_id' => $this->worker->id,
|
||||
]);
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJsonStructure([
|
||||
'success',
|
||||
'message',
|
||||
'data' => [
|
||||
'conversation_id'
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('conversations', [
|
||||
'employer_id' => $this->employer->id,
|
||||
'worker_id' => $this->worker->id,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test request fails without authorization token.
|
||||
*/
|
||||
public function test_unauthenticated_requests_are_blocked()
|
||||
{
|
||||
$response = $this->getJson('/api/employers/conversations');
|
||||
$response->assertStatus(401);
|
||||
}
|
||||
}
|
||||
172
tests/Feature/EmployerProfileApiTest.php
Normal file
172
tests/Feature/EmployerProfileApiTest.php
Normal file
@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\EmployerProfile;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Tests\TestCase;
|
||||
|
||||
class EmployerProfileApiTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected $employer;
|
||||
protected $token;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->token = 'test-employer-token';
|
||||
$this->employer = User::create([
|
||||
'name' => 'Original Name',
|
||||
'email' => 'employer@example.com',
|
||||
'password' => bcrypt('Password@123'),
|
||||
'role' => 'employer',
|
||||
'api_token' => $this->token,
|
||||
]);
|
||||
|
||||
EmployerProfile::create([
|
||||
'user_id' => $this->employer->id,
|
||||
'company_name' => 'Original Company',
|
||||
'phone' => '+971501112222',
|
||||
'verification_status' => 'approved',
|
||||
'language' => 'English',
|
||||
'notifications' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test employer can retrieve profile.
|
||||
*/
|
||||
public function test_employer_can_get_profile()
|
||||
{
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer ' . $this->token,
|
||||
])->getJson('/api/employers/profile');
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJsonStructure([
|
||||
'success',
|
||||
'data' => [
|
||||
'profile' => [
|
||||
'name',
|
||||
'email',
|
||||
'company_name',
|
||||
'phone',
|
||||
'language',
|
||||
'notifications',
|
||||
'verification_status',
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertEquals('Original Name', $response->json('data.profile.name'));
|
||||
$this->assertEquals('Original Company', $response->json('data.profile.company_name'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test employer can update profile details.
|
||||
*/
|
||||
public function test_employer_can_update_profile()
|
||||
{
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer ' . $this->token,
|
||||
])->postJson('/api/employers/profile/update', [
|
||||
'name' => 'Updated Name',
|
||||
'email' => 'employer_updated@example.com',
|
||||
'phone' => '+971509999999',
|
||||
'company_name' => 'Updated Company Corp',
|
||||
'language' => 'Arabic',
|
||||
'notifications' => false,
|
||||
]);
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJsonStructure([
|
||||
'success',
|
||||
'message',
|
||||
'data' => [
|
||||
'profile' => [
|
||||
'name',
|
||||
'email',
|
||||
'company_name',
|
||||
'phone',
|
||||
'language',
|
||||
'notifications',
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('users', [
|
||||
'id' => $this->employer->id,
|
||||
'name' => 'Updated Name',
|
||||
'email' => 'employer_updated@example.com',
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('employer_profiles', [
|
||||
'user_id' => $this->employer->id,
|
||||
'company_name' => 'Updated Company Corp',
|
||||
'language' => 'Arabic',
|
||||
'notifications' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test employer can update password when providing correct current password.
|
||||
*/
|
||||
public function test_employer_can_update_password()
|
||||
{
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer ' . $this->token,
|
||||
])->postJson('/api/employers/profile/update', [
|
||||
'name' => 'Original Name',
|
||||
'email' => 'employer@example.com',
|
||||
'phone' => '+971501112222',
|
||||
'company_name' => 'Original Company',
|
||||
'language' => 'English',
|
||||
'notifications' => true,
|
||||
'current_password' => 'Password@123',
|
||||
'new_password' => 'NewSecurePassword@123',
|
||||
'new_password_confirmation' => 'NewSecurePassword@123',
|
||||
]);
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
// Verify password hash updated
|
||||
$this->assertTrue(Hash::check('NewSecurePassword@123', $this->employer->fresh()->password));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test employer password update fails with incorrect current password.
|
||||
*/
|
||||
public function test_employer_password_update_fails_with_incorrect_current_password()
|
||||
{
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer ' . $this->token,
|
||||
])->postJson('/api/employers/profile/update', [
|
||||
'name' => 'Original Name',
|
||||
'email' => 'employer@example.com',
|
||||
'phone' => '+971501112222',
|
||||
'company_name' => 'Original Company',
|
||||
'language' => 'English',
|
||||
'notifications' => true,
|
||||
'current_password' => 'WrongCurrentPassword',
|
||||
'new_password' => 'NewSecurePassword@123',
|
||||
'new_password_confirmation' => 'NewSecurePassword@123',
|
||||
]);
|
||||
|
||||
$response->assertStatus(422)
|
||||
->assertJsonStructure([
|
||||
'success',
|
||||
'message',
|
||||
'errors' => [
|
||||
'current_password'
|
||||
]
|
||||
]);
|
||||
|
||||
// Verify password was NOT changed
|
||||
$this->assertTrue(Hash::check('Password@123', $this->employer->fresh()->password));
|
||||
}
|
||||
}
|
||||
@ -14,6 +14,6 @@ public function test_the_application_returns_a_successful_response(): void
|
||||
{
|
||||
$response = $this->get('/');
|
||||
|
||||
$response->assertStatus(200);
|
||||
$response->assertStatus(302);
|
||||
}
|
||||
}
|
||||
|
||||
205
tests/Feature/WorkerMessageApiTest.php
Normal file
205
tests/Feature/WorkerMessageApiTest.php
Normal file
@ -0,0 +1,205 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Conversation;
|
||||
use App\Models\Message;
|
||||
use App\Models\User;
|
||||
use App\Models\Worker;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class WorkerMessageApiTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected $worker;
|
||||
protected $employer;
|
||||
protected $token;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
// Create a category
|
||||
$category = \App\Models\WorkerCategory::create([
|
||||
'name' => 'Housemaid',
|
||||
]);
|
||||
|
||||
// Create a test worker with an API token
|
||||
$this->token = 'test-token-12345';
|
||||
$this->worker = Worker::create([
|
||||
'name' => 'Jane Smith',
|
||||
'email' => 'jane@example.com',
|
||||
'phone' => '+971500000001',
|
||||
'password' => bcrypt('password'),
|
||||
'api_token' => $this->token,
|
||||
'status' => 'Available',
|
||||
'nationality' => 'Ethiopian',
|
||||
'age' => 28,
|
||||
'salary' => 1500.00,
|
||||
'availability' => 'Immediate',
|
||||
'experience' => '2 Years',
|
||||
'religion' => 'Christian',
|
||||
'bio' => 'Experienced housemaid seeking employment.',
|
||||
'category_id' => $category->id,
|
||||
]);
|
||||
|
||||
// Create an employer user
|
||||
$this->employer = User::create([
|
||||
'name' => 'John Employer',
|
||||
'email' => 'employer_test@example.com',
|
||||
'password' => bcrypt('password'),
|
||||
'role' => 'employer',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test retrieving conversation list for a worker.
|
||||
*/
|
||||
public function test_worker_can_get_conversations()
|
||||
{
|
||||
// Create conversation
|
||||
$conversation = Conversation::create([
|
||||
'employer_id' => $this->employer->id,
|
||||
'worker_id' => $this->worker->id,
|
||||
]);
|
||||
|
||||
// Create a message from employer
|
||||
Message::create([
|
||||
'conversation_id' => $conversation->id,
|
||||
'sender_type' => 'employer',
|
||||
'sender_id' => $this->employer->id,
|
||||
'text' => 'Hello from employer!',
|
||||
]);
|
||||
|
||||
// Request API
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer ' . $this->token,
|
||||
])->getJson('/api/workers/conversations');
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJsonStructure([
|
||||
'success',
|
||||
'data' => [
|
||||
'conversations' => [
|
||||
'*' => [
|
||||
'id',
|
||||
'employer_id',
|
||||
'employer_name',
|
||||
'company_name',
|
||||
'last_message',
|
||||
'unread_count',
|
||||
'sent_at',
|
||||
'updated_at',
|
||||
]
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertEquals('Hello from employer!', $response->json('data.conversations.0.last_message'));
|
||||
$this->assertEquals(1, $response->json('data.conversations.0.unread_count'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test retrieving messages in a conversation and marking them as read.
|
||||
*/
|
||||
public function test_worker_can_get_messages_and_mark_as_read()
|
||||
{
|
||||
// Create conversation
|
||||
$conversation = Conversation::create([
|
||||
'employer_id' => $this->employer->id,
|
||||
'worker_id' => $this->worker->id,
|
||||
]);
|
||||
|
||||
// Create message
|
||||
$message = Message::create([
|
||||
'conversation_id' => $conversation->id,
|
||||
'sender_type' => 'employer',
|
||||
'sender_id' => $this->employer->id,
|
||||
'text' => 'Unread message',
|
||||
'read_at' => null,
|
||||
]);
|
||||
|
||||
// Request API
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer ' . $this->token,
|
||||
])->getJson('/api/workers/conversations/' . $conversation->id . '/messages');
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJsonStructure([
|
||||
'success',
|
||||
'data' => [
|
||||
'conversation' => [
|
||||
'id',
|
||||
'employer_name',
|
||||
'company_name',
|
||||
],
|
||||
'messages' => [
|
||||
'*' => [
|
||||
'id',
|
||||
'sender_type',
|
||||
'text',
|
||||
'time',
|
||||
'read_at',
|
||||
'created_at',
|
||||
]
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
// Verify message marked as read in database
|
||||
$this->assertNotNull($message->fresh()->read_at);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test worker can send reply.
|
||||
*/
|
||||
public function test_worker_can_send_reply()
|
||||
{
|
||||
// Create conversation
|
||||
$conversation = Conversation::create([
|
||||
'employer_id' => $this->employer->id,
|
||||
'worker_id' => $this->worker->id,
|
||||
]);
|
||||
|
||||
// Reply request
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer ' . $this->token,
|
||||
])->postJson('/api/workers/conversations/' . $conversation->id . '/messages', [
|
||||
'text' => 'Hello Employer, I am available!'
|
||||
]);
|
||||
|
||||
$response->assertStatus(201)
|
||||
->assertJsonStructure([
|
||||
'success',
|
||||
'message',
|
||||
'data' => [
|
||||
'message' => [
|
||||
'id',
|
||||
'sender_type',
|
||||
'text',
|
||||
'time',
|
||||
'created_at',
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
// Verify message created in database
|
||||
$this->assertDatabaseHas('messages', [
|
||||
'conversation_id' => $conversation->id,
|
||||
'sender_type' => 'worker',
|
||||
'sender_id' => $this->worker->id,
|
||||
'text' => 'Hello Employer, I am available!',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test request fails without authorization token.
|
||||
*/
|
||||
public function test_unauthenticated_requests_are_blocked()
|
||||
{
|
||||
$response = $this->getJson('/api/workers/conversations');
|
||||
$response->assertStatus(401);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user