migrant-web/app/Http/Controllers/Api/SponsorController.php

715 lines
32 KiB
PHP

<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Announcement;
use App\Models\Sponsor;
use Illuminate\Http\Request;
class SponsorController extends Controller
{
/**
* GET /api/sponsors/dashboard
*
* Returns the sponsor's profile summary and unread charity event count.
*/
public function getDashboard(Request $request)
{
/** @var Sponsor $sponsor */
$sponsor = $request->attributes->get('sponsor');
$sponsorId = $sponsor ? $sponsor->id : null;
try {
// Recent charity/update announcements for the dashboard preview
$recentEvents = Announcement::where(function ($q) use ($sponsorId) {
$q->where('status', 'approved');
if ($sponsorId) {
$q->orWhere('sponsor_id', $sponsorId);
}
})
->latest()
->limit(5)
->get()
->map(function ($event) {
$content = $event->body;
if (strpos($event->body, '{"type":"Charity"') === 0) {
$decoded = json_decode($event->body, true);
if ($decoded) {
$content = $decoded['content'] ?? $event->body;
}
}
return [
'id' => $event->id,
'title' => $event->title,
'body' => $content,
'type' => strtolower($event->type),
'status' => $event->status ?? 'pending',
'created_at' => $event->created_at->toIso8601String(),
'time_ago' => $event->created_at->diffForHumans(),
];
});
return response()->json([
'success' => true,
'data' => [
'sponsor' => [
'id' => $sponsor->id,
'full_name' => $sponsor->full_name,
'organization_name' => $sponsor->organization_name,
'mobile' => $sponsor->mobile,
'email' => $sponsor->email,
'city' => $sponsor->city,
'nationality' => $sponsor->nationality,
'is_verified' => $sponsor->is_verified,
'status' => $sponsor->status,
'license_file' => $sponsor->license_file ? asset($sponsor->license_file) : null,
'emirates_id_file' => $sponsor->emirates_id_file ? asset($sponsor->emirates_id_file) : null,
'license_expiry' => $sponsor->license_expiry ? $sponsor->license_expiry->toDateString() : null,
'validity' => $sponsor->license_expiry ? ($sponsor->license_expiry->isFuture() ? 'Valid' : 'Expired') : 'Pending Review',
'joined_at' => $sponsor->created_at->toIso8601String(),
],
'recent_charity_events' => $recentEvents,
'total_events' => Announcement::where(function ($q) use ($sponsorId) {
$q->where('status', 'approved');
if ($sponsorId) {
$q->orWhere('sponsor_id', $sponsorId);
}
})->count(),
'employer_stats' => [
'total' => \App\Models\User::where('role', 'employer')->count(),
'active' => \App\Models\User::where('role', 'employer')->where('subscription_status', 'active')->count(),
],
'worker_stats' => [
'total' => \App\Models\Worker::count(),
'active' => \App\Models\Worker::where('status', 'active')->count(),
],
]
], 200);
} catch (\Exception $e) {
logger()->error('Sponsor Dashboard API Failure: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'An error occurred while loading your dashboard.',
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
], 500);
}
}
/**
* GET /api/sponsors/charity-events
*
* Returns a paginated list of all charity/announcement events.
*/
public function getCharityEvents(Request $request)
{
try {
$page = (int) $request->input('page', 1);
$perPage = (int) $request->input('per_page', 15);
$type = $request->input('type'); // optional filter: 'charity', 'update', etc.
/** @var Sponsor $sponsor */
$sponsor = $request->attributes->get('sponsor');
$sponsorId = $sponsor ? $sponsor->id : null;
$query = Announcement::with(['employer.employerProfile', 'sponsor'])
->where(function ($q) use ($sponsorId) {
$q->where('status', 'approved');
if ($sponsorId) {
$q->orWhere('sponsor_id', $sponsorId);
}
})
->latest();
if ($type) {
if (strtolower($type) === 'charity') {
$query->whereIn('type', ['charity', 'Charity']);
} else {
$query->where('type', $type);
}
}
$total = $query->count();
$offset = ($page - 1) * $perPage;
$events = $query->skip($offset)->take($perPage)->get()
->map(function ($event) {
$postedBy = 'System';
$organization = 'Migrant Support';
if ($event->sponsor_id) {
$postedBy = $event->sponsor->full_name;
$organization = $event->sponsor->organization_name;
} elseif ($event->employer_id) {
$postedBy = $event->employer->name;
$organization = optional($event->employer)->employerProfile->company_name ?? 'Migrant Support';
}
// Decode charity details if they exist in json
$charityDetails = null;
$content = $event->body;
if (strpos($event->body, '{"type":"Charity"') === 0) {
$decoded = json_decode($event->body, true);
if ($decoded) {
$charityDetails = $decoded;
$content = $decoded['content'] ?? $event->body;
}
}
return [
'id' => $event->id,
'title' => $event->title,
'body' => $content,
'type' => strtolower($event->type),
'status' => $event->status ?? 'pending',
'remarks' => $event->remarks,
'posted_by' => $postedBy,
'organization' => $organization,
'created_at' => $event->created_at->toIso8601String(),
'time_ago' => $event->created_at->diffForHumans(),
'charity_details' => $charityDetails,
];
});
return response()->json([
'success' => true,
'data' => [
'events' => $events,
'pagination' => [
'total' => $total,
'per_page' => $perPage,
'current_page' => $page,
'last_page' => max(1, (int) ceil($total / $perPage)),
],
]
], 200);
} catch (\Exception $e) {
logger()->error('Sponsor Charity Events API Failure: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'An error occurred while fetching charity events.',
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
], 500);
}
}
/**
* POST /api/sponsors/charity-events
*
* Creates/Posts a new charity event for the authenticated sponsor.
*/
public function postCharityEvent(Request $request)
{
/** @var Sponsor $sponsor */
$sponsor = $request->attributes->get('sponsor');
if (!$sponsor) {
return response()->json([
'success' => false,
'message' => 'Unauthorized.'
], 401);
}
$validator = \Illuminate\Support\Facades\Validator::make($request->all(), [
'title' => 'required|string|max:255',
'body' => 'required|string',
'type' => 'nullable|string|in:charity,info,warning,success',
'event_date' => 'required|string',
'start_time' => 'required|string',
'end_time' => 'required|string',
'provided_items' => 'required|string',
'location_details' => 'required|string',
'location_pin' => 'required|string|url',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'message' => 'Validation error.',
'errors' => $validator->errors()
], 422);
}
try {
$eventTime = $request->start_time . ' - ' . $request->end_time;
$bodyJson = json_encode([
'type' => 'Charity',
'provided_items' => $request->provided_items,
'event_date' => $request->event_date,
'event_time' => $eventTime,
'location_details' => $request->location_details,
'location_pin' => $request->location_pin,
'content' => $request->body,
]);
$event = Announcement::create([
'title' => $request->title,
'body' => $bodyJson,
'type' => $request->type ?? 'charity',
'sponsor_id' => $sponsor->id,
'status' => 'pending',
]);
// Decode charity details for the response representation
$charityDetails = null;
$content = $event->body;
if (strpos($event->body, '{"type":"Charity"') === 0) {
$decoded = json_decode($event->body, true);
if ($decoded) {
$charityDetails = $decoded;
$content = $decoded['content'] ?? $event->body;
}
}
return response()->json([
'success' => true,
'message' => 'Charity event posted successfully.',
'data' => [
'id' => $event->id,
'title' => $event->title,
'body' => $content,
'type' => strtolower($event->type),
'posted_by' => $sponsor->full_name,
'organization' => $sponsor->organization_name,
'created_at' => $event->created_at->toIso8601String(),
'charity_details' => $charityDetails,
]
], 201);
} catch (\Exception $e) {
logger()->error('Sponsor Post Charity Event API Failure: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'An error occurred while posting the charity event.',
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
], 500);
}
}
/**
* GET /api/sponsors/profile
*
* Returns the authenticated sponsor's full profile.
*/
public function getProfile(Request $request)
{
/** @var Sponsor $sponsor */
$sponsor = $request->attributes->get('sponsor');
return response()->json([
'success' => true,
'data' => [
'sponsor' => $this->buildSponsorResponse($sponsor)
]
], 200);
}
/**
* Build the standardized sponsor response array with nested license and emirates_id objects.
*/
private function buildSponsorResponse(Sponsor $sponsor): array
{
$licenseData = [
'license_number' => $sponsor->license_data['license_no'] ?? $sponsor->license_data['license_number'] ?? null,
'organization_name' => $sponsor->organization_name,
'expiry_date' => $sponsor->license_expiry ? $sponsor->license_expiry->toDateString() : ($sponsor->license_data['expiry_date'] ?? null),
'document_type' => $sponsor->license_data['document_type'] ?? null,
'country' => $sponsor->license_data['country'] ?? null,
'authority' => $sponsor->license_data['authority'] ?? null,
'license_no' => $sponsor->license_data['license_no'] ?? null,
'company_name' => $sponsor->license_data['company_name'] ?? null,
'business_name' => $sponsor->license_data['business_name'] ?? null,
'license_category' => $sponsor->license_data['license_category'] ?? null,
'legal_type' => $sponsor->license_data['legal_type'] ?? null,
'issue_date' => $sponsor->license_data['issue_date'] ?? null,
'main_license_no' => $sponsor->license_data['main_license_no'] ?? null,
'register_no' => $sponsor->license_data['register_no'] ?? null,
'dcci_no' => $sponsor->license_data['dcci_no'] ?? null,
'members' => $sponsor->license_data['members'] ?? [],
];
$emiratesIdData = null;
if ($sponsor->emirates_id) {
$emiratesIdData = [
'emirates_id_number' => $sponsor->emirates_id,
'name' => $sponsor->emirates_id_name,
'nationality' => $sponsor->nationality,
'date_of_birth' => $sponsor->emirates_id_dob,
'expiry_date' => $sponsor->emirates_id_expiry,
'issue_date' => $sponsor->emirates_id_issue_date,
'employer' => $sponsor->emirates_id_employer,
'issue_place' => $sponsor->emirates_id_issue_place,
'occupation' => $sponsor->emirates_id_occupation,
];
}
return [
'id' => $sponsor->id,
'full_name' => $sponsor->full_name,
'organization_name' => $sponsor->organization_name,
'mobile' => $sponsor->mobile,
'email' => $sponsor->email,
'nationality' => $sponsor->nationality,
'city' => $sponsor->city,
'address' => $sponsor->address,
'country_code' => $sponsor->country_code,
'is_verified' => $sponsor->is_verified,
'status' => $sponsor->status,
'license_file' => $sponsor->license_file ? asset($sponsor->license_file) : null,
'emirates_id_file' => $sponsor->emirates_id_file ? asset($sponsor->emirates_id_file) : null,
'license_expiry' => $sponsor->license_expiry ? $sponsor->license_expiry->toDateString() : null,
'validity' => $sponsor->license_expiry ? ($sponsor->license_expiry->isFuture() ? 'Valid' : 'Expired') : 'Pending Review',
'joined_at' => $sponsor->created_at->toIso8601String(),
'fcm_token' => $sponsor->fcm_token,
'license' => $licenseData,
'emirates_id' => $emiratesIdData,
];
}
/**
* Change sponsor's password.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function changePassword(Request $request)
{
/** @var Sponsor $sponsor */
$sponsor = $request->attributes->get('sponsor');
$validator = \Illuminate\Support\Facades\Validator::make($request->all(), [
'current_password' => 'required|string',
'new_password' => 'required|string|min:8|confirmed',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'message' => 'Validation error.',
'errors' => $validator->errors()
], 422);
}
if (!\Illuminate\Support\Facades\Hash::check($request->current_password, $sponsor->password)) {
return response()->json([
'success' => false,
'message' => 'Validation error.',
'errors' => [
'current_password' => ['The current password is incorrect.']
]
], 422);
}
try {
$hashedPassword = \Illuminate\Support\Facades\Hash::make($request->new_password);
// Update sponsor table
$sponsor->update([
'password' => $hashedPassword
]);
// Sync with corresponding employer user record if found
$matchingEmployer = \App\Models\User::where('email', $sponsor->email)
->where('role', 'employer')
->first();
if ($matchingEmployer) {
$matchingEmployer->update([
'password' => $hashedPassword
]);
}
return response()->json([
'success' => true,
'message' => 'Password changed successfully.'
], 200);
} catch (\Exception $e) {
logger()->error('Mobile Sponsor Change Password Failure: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'An error occurred while changing password.',
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
], 500);
}
}
/**
* POST /api/sponsors/profile/update
*
* Updates the authenticated sponsor's profile.
*/
public function updateProfile(Request $request)
{
/** @var Sponsor $sponsor */
$sponsor = $request->attributes->get('sponsor');
if (!$sponsor) {
return response()->json([
'success' => false,
'message' => 'Unauthorized.'
], 401);
}
// Find matching employer user using sponsor's original email
$oldEmail = $sponsor->getOriginal('email') ?? $sponsor->email;
$matchingUser = \App\Models\User::where('email', $oldEmail)
->where('role', 'employer')
->first();
$matchingUserId = $matchingUser ? $matchingUser->id : 'NULL';
$validator = \Illuminate\Support\Facades\Validator::make($request->all(), [
'name' => 'required_without:full_name|nullable|string|max:255',
'full_name' => 'required_without:name|nullable|string|max:255',
'email' => 'required|email|max:255|unique:sponsors,email,' . $sponsor->id . '|unique:users,email,' . $matchingUserId,
'mobile' => 'required|string|max:255|unique:sponsors,mobile,' . $sponsor->id,
'organization_name' => 'required|string|max:255',
'city' => 'required|string|max:100',
'address' => 'required|string|max:255',
'nationality' => 'required|string|max:100',
'country_code' => 'required|string|max:10',
'fcm_token' => 'nullable|string|max:255',
// Emirates ID — accepts nested object OR flat fields
'emirates_id' => 'nullable|array',
'emirates_id.emirates_id_number' => 'nullable|string|max:255',
'emirates_id.name' => 'nullable|string|max:255',
'emirates_id.nationality' => 'nullable|string|max:255',
'emirates_id.date_of_birth' => 'nullable|string|max:255',
'emirates_id.expiry_date' => 'nullable|string|max:255',
'emirates_id.issue_date' => 'nullable|string|max:255',
'emirates_id.employer' => 'nullable|string|max:255',
'emirates_id.issue_place' => 'nullable|string|max:255',
'emirates_id.occupation' => 'nullable|string|max:255',
// Flat emirates ID fields (backward compatibility)
'id_number' => 'nullable|string|max:255',
'card_number' => 'nullable|string|max:255',
'date_of_birth' => 'nullable|string|max:255',
'gender' => 'nullable|string|max:255',
'issue_date' => 'nullable|string|max:255',
'expiry_date' => 'nullable|string|max:255',
'occupation' => 'nullable|string|max:255',
'employer' => 'nullable|string|max:255',
'issuing_place' => 'nullable|string|max:255',
// License — accepts nested object OR flat fields
'license' => 'nullable|array',
'license.document_type' => 'nullable|string|max:255',
'license.country' => 'nullable|string|max:255',
'license.authority' => 'nullable|string|max:255',
'license.license_no' => 'nullable|string|max:255',
'license.company_name' => 'nullable|string|max:255',
'license.business_name' => 'nullable|string|max:255',
'license.license_category' => 'nullable|string|max:255',
'license.legal_type' => 'nullable|string|max:255',
'license.main_license_no' => 'nullable|string|max:255',
'license.register_no' => 'nullable|string|max:255',
'license.dcci_no' => 'nullable|string|max:255',
'license.members' => 'nullable|array',
// Flat license fields (backward compatibility)
'document_type' => 'nullable|string|max:255',
'country' => 'nullable|string|max:255',
'authority' => 'nullable|string|max:255',
'license_no' => 'nullable|string|max:255',
'company_name' => 'nullable|string|max:255',
'business_name' => 'nullable|string|max:255',
'license_category' => 'nullable|string|max:255',
'legal_type' => 'nullable|string|max:255',
'main_license_no' => 'nullable|string|max:255',
'register_no' => 'nullable|string|max:255',
'dcci_no' => 'nullable|string|max:255',
'members' => 'nullable|array',
]);
$validator->after(function ($validator) use ($request) {
$nameFromEid = $request->input('emirates_id.name') ?? $request->input('full_name');
if ($request->has('full_name') && !$request->filled('full_name') && !$nameFromEid) {
$validator->errors()->add('full_name', 'Failed to extract name from the Emirates ID. Please upload a clearer document.');
}
});
if ($validator->fails()) {
return response()->json([
'success' => false,
'message' => 'Validation error.',
'errors' => $validator->errors()
], 422);
}
try {
// Resolve nested emirates_id input into flat variables
$eidInput = $request->input('emirates_id');
$eidNumber = null;
$eidName = null;
$eidDob = null;
$eidExpiry = null;
$eidIssueDate = null;
$eidEmployer = null;
$eidIssuePlace = null;
$eidOccupation = null;
$eidCardNumber = null;
$eidGender = null;
if (is_array($eidInput)) {
$eidNumber = $eidInput['emirates_id_number'] ?? $eidInput['id_number'] ?? null;
$eidName = $eidInput['name'] ?? null;
$eidDob = $eidInput['date_of_birth'] ?? $eidInput['dob'] ?? null;
$eidExpiry = $eidInput['expiry_date'] ?? null;
$eidIssueDate = $eidInput['issue_date'] ?? null;
$eidEmployer = $eidInput['employer'] ?? null;
$eidIssuePlace = $eidInput['issue_place'] ?? $eidInput['issuing_place'] ?? null;
$eidOccupation = $eidInput['occupation'] ?? null;
$eidCardNumber = $eidInput['card_number'] ?? null;
$eidGender = $eidInput['gender'] ?? null;
}
// Flat field fallbacks (backward compatibility)
$eidNumber = $eidNumber ?? $request->input('id_number');
$eidName = $eidName ?? $request->input('full_name');
$eidDob = $eidDob ?? $request->input('date_of_birth');
$eidExpiry = $eidExpiry ?? $request->input('expiry_date');
$eidIssueDate = $eidIssueDate ?? $request->input('issue_date');
$eidEmployer = $eidEmployer ?? $request->input('employer');
$eidIssuePlace = $eidIssuePlace ?? $request->input('issuing_place');
$eidOccupation = $eidOccupation ?? $request->input('occupation');
$eidCardNumber = $eidCardNumber ?? $request->input('card_number');
$eidGender = $eidGender ?? $request->input('gender');
$nameToUse = $eidName ?? $request->name ?? $request->full_name;
// Validate Emirates ID immutability — once set, it cannot be changed
if ($eidNumber) {
if ($sponsor->emirates_id && $sponsor->emirates_id !== $eidNumber) {
return response()->json([
'success' => false,
'message' => 'Emirates ID mismatch. You cannot change your registered Emirates ID number.',
'errors' => [
'id_number' => ['Emirates ID mismatch.']
]
], 422);
}
}
// Sync with corresponding employer user and profile records if found
if ($matchingUser) {
$userData = [
'name' => $nameToUse,
'email' => $request->email,
];
if ($request->has('fcm_token')) {
$userData['fcm_token'] = $request->fcm_token;
}
$matchingUser->update($userData);
$profile = $matchingUser->employerProfile;
if (!$profile) {
$profile = new \App\Models\EmployerProfile(['user_id' => $matchingUser->id]);
}
$profile->address = $request->address;
$profile->phone = $request->mobile;
if ($eidNumber) { $profile->emirates_id_number = $eidNumber; }
if ($eidCardNumber) { $profile->emirates_id_card_number = $eidCardNumber; }
if ($eidName) { $profile->emirates_id_name = $eidName; }
if ($eidDob) { $profile->emirates_id_dob = $eidDob; }
if ($request->has('nationality')) { $profile->nationality = $request->nationality; }
if ($eidGender) { $profile->emirates_id_gender = $eidGender; }
if ($eidIssueDate) { $profile->emirates_id_issue_date = $eidIssueDate; }
if ($eidExpiry) { $profile->emirates_id_expiry = $eidExpiry; }
if ($eidOccupation) { $profile->emirates_id_occupation = $eidOccupation; }
if ($eidEmployer) { $profile->emirates_id_employer = $eidEmployer; }
if ($eidIssuePlace) { $profile->emirates_id_issue_place = $eidIssuePlace; }
$profile->save();
}
// Update Sponsor profile
$sponsorData = [
'full_name' => $nameToUse,
'email' => $request->email,
'mobile' => $request->mobile,
'organization_name' => $request->organization_name,
'city' => $request->city,
'address' => $request->address,
'nationality' => $request->nationality,
'country_code' => $request->country_code,
];
if ($request->has('fcm_token')) {
$sponsorData['fcm_token'] = $request->fcm_token;
}
if ($eidNumber) { $sponsorData['emirates_id'] = $eidNumber; }
if ($eidCardNumber) { $sponsorData['emirates_id_card_number'] = $eidCardNumber; }
if ($eidName) { $sponsorData['emirates_id_name'] = $eidName; }
if ($eidDob) { $sponsorData['emirates_id_dob'] = $eidDob; }
if ($eidGender) { $sponsorData['emirates_id_gender'] = $eidGender; }
if ($eidIssueDate) { $sponsorData['emirates_id_issue_date'] = $eidIssueDate; }
if ($eidExpiry) { $sponsorData['emirates_id_expiry'] = $eidExpiry; }
if ($eidOccupation) { $sponsorData['emirates_id_occupation'] = $eidOccupation; }
if ($eidEmployer) { $sponsorData['emirates_id_employer'] = $eidEmployer; }
if ($eidIssuePlace) { $sponsorData['emirates_id_issue_place'] = $eidIssuePlace; }
// Parse and merge license data — from nested license object or flat fields
$currentLicenseData = is_array($sponsor->license_data) ? $sponsor->license_data : [];
$licenseInput = $request->input('license');
$newLicenseData = [];
if (is_array($licenseInput)) {
foreach ([
'document_type', 'country', 'authority', 'license_no', 'company_name',
'business_name', 'license_category', 'legal_type', 'issue_date', 'expiry_date',
'main_license_no', 'register_no', 'dcci_no', 'members'
] as $field) {
if (isset($licenseInput[$field])) {
$newLicenseData[$field] = $licenseInput[$field];
}
}
}
// Flat field fallbacks (backward compatibility)
foreach ([
'document_type', 'country', 'authority', 'license_no', 'company_name',
'business_name', 'license_category', 'legal_type', 'issue_date', 'expiry_date',
'main_license_no', 'register_no', 'dcci_no', 'members'
] as $field) {
if (!isset($newLicenseData[$field]) && $request->has($field)) {
$newLicenseData[$field] = $request->input($field);
}
}
if (!empty($newLicenseData)) {
$sponsorData['license_data'] = array_merge($currentLicenseData, $newLicenseData);
}
$sponsor->update($sponsorData);
return response()->json([
'success' => true,
'message' => 'Profile updated successfully.',
'data' => [
'sponsor' => $this->buildSponsorResponse($sponsor)
]
], 200);
} catch (\Exception $e) {
logger()->error('Mobile Sponsor Update Profile Failure: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'An error occurred while updating profile.',
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
], 500);
}
}
}