diff --git a/app/Http/Controllers/Admin/AdminExtraController.php b/app/Http/Controllers/Admin/AdminExtraController.php index e4eda98..68b3ef9 100644 --- a/app/Http/Controllers/Admin/AdminExtraController.php +++ b/app/Http/Controllers/Admin/AdminExtraController.php @@ -734,6 +734,9 @@ public function storeAnnouncement(Request $request) 'event_time' => 'required|string', 'location_details' => 'required|string', 'location_pin' => 'required|string|url', + 'contact_person_name' => 'required|string|max:255', + 'contact_number' => 'required|string|max:50', + 'country_code' => 'nullable|string|max:10', ]); $body = json_encode([ @@ -744,6 +747,9 @@ public function storeAnnouncement(Request $request) 'location_details' => $request->location_details, 'location_pin' => $request->location_pin, 'content' => $request->content, + 'contact_person_name' => $request->contact_person_name, + 'contact_number' => $request->contact_number, + 'country_code' => $request->country_code ?? '+971', ]); $ann = \App\Models\Announcement::create([ @@ -801,7 +807,7 @@ public function deleteAnnouncement(Request $request, $id) } /** - * Broadcast FCM Push Notification for new Event/Announcement. + * Broadcast FCM Push Notification and save to Database for new Event/Announcement. */ protected function notifyUsersOfEvent($ann) { @@ -816,31 +822,29 @@ protected function notifyUsersOfEvent($ann) } // Notify all workers - $workers = \App\Models\Worker::whereNotNull('fcm_token')->get(); + $workers = \App\Models\Worker::all(); foreach ($workers as $worker) { - \App\Services\FCMService::sendPushNotification( - $worker->fcm_token, + $worker->notify(new \App\Notifications\GenericNotification( $title, $body, [ 'type' => 'announcement', 'announcement_id' => $ann->id, ] - ); + )); } // Notify all employers - $employers = \App\Models\User::where('role', 'employer')->whereNotNull('fcm_token')->get(); + $employers = \App\Models\User::where('role', 'employer')->get(); foreach ($employers as $employer) { - \App\Services\FCMService::sendPushNotification( - $employer->fcm_token, + $employer->notify(new \App\Notifications\GenericNotification( $title, $body, [ 'type' => 'announcement', 'announcement_id' => $ann->id, ] - ); + )); } } } diff --git a/app/Http/Controllers/Api/EmployerAuthController.php b/app/Http/Controllers/Api/EmployerAuthController.php index 017be9e..d371462 100644 --- a/app/Http/Controllers/Api/EmployerAuthController.php +++ b/app/Http/Controllers/Api/EmployerAuthController.php @@ -324,14 +324,6 @@ public function register(Request $request) 'address' => $request->address, ]); - if ($request->filled('fcm_token')) { - \App\Services\FCMService::sendPushNotification( - $request->fcm_token, - 'Registration Initiated', - 'Your verification code has been sent to your email.' - ); - } - // Try sending email try { \Illuminate\Support\Facades\Mail::to($request->email)->send(new \App\Mail\EmployerOtpMail( diff --git a/app/Http/Controllers/Api/EmployerWorkerController.php b/app/Http/Controllers/Api/EmployerWorkerController.php index 56faa72..031608e 100644 --- a/app/Http/Controllers/Api/EmployerWorkerController.php +++ b/app/Http/Controllers/Api/EmployerWorkerController.php @@ -852,10 +852,10 @@ public function hireCandidate(Request $request, $id = null) )); } else if ($updatedOffer) { $worker->notify(new \App\Notifications\GenericNotification( - "New Job Offer", - "Employer " . ($employer->name ?? "Employer") . " has sent you a direct hire offer.", + "Direct Hiring Offer", + ($employer->name ?? "Employer") . " wants to hire you. Do you want to accept this job offer?", [ - 'type' => 'hire_request', + 'type' => 'direct_hire_request', 'offer_id' => $updatedOffer->id, 'status' => 'pending', ] diff --git a/app/Http/Controllers/Api/WorkerJobController.php b/app/Http/Controllers/Api/WorkerJobController.php index 85ef88f..f883ad9 100644 --- a/app/Http/Controllers/Api/WorkerJobController.php +++ b/app/Http/Controllers/Api/WorkerJobController.php @@ -1108,22 +1108,6 @@ private function triggerStatusNotification($application, $status) $job = $application->jobPost; $employer = $job ? $job->employer : null; - // When a new application is submitted: notify employer - if ($status === 'applied') { - if ($employer) { - $employer->notify(new \App\Notifications\GenericNotification( - "New Job Application", - "A candidate has applied for your job: " . ($job->title ?? 'Job Post'), - [ - 'type' => 'job_application_submitted', - 'job_id' => $job->id, - 'application_id' => $application->id, - ] - )); - } - return; - } - // When status is updated: notify worker if ($worker) { $title = "Job Application Update"; @@ -1134,6 +1118,7 @@ private function triggerStatusNotification($application, $status) $body = "You have been shortlisted for the job: " . ($job->title ?? 'Job Post'); $type = 'shortlisted'; break; + case 'applied': case 'review': case 'reviewing': $body = "Your application is under review for the job: " . ($job->title ?? 'Job Post'); diff --git a/app/Http/Controllers/Employer/AnnouncementController.php b/app/Http/Controllers/Employer/AnnouncementController.php index 34f0ae4..578e770 100644 --- a/app/Http/Controllers/Employer/AnnouncementController.php +++ b/app/Http/Controllers/Employer/AnnouncementController.php @@ -72,6 +72,9 @@ public function store(Request $request) 'event_time' => 'required|string', 'location_details' => 'required|string', 'location_pin' => 'required|string|url', + 'contact_person_name' => 'required|string|max:255', + 'contact_number' => 'required|string|max:50', + 'country_code' => 'nullable|string|max:10', ]); $body = json_encode([ @@ -82,6 +85,9 @@ public function store(Request $request) 'location_details' => $request->location_details, 'location_pin' => $request->location_pin, 'content' => $request->content, + 'contact_person_name' => $request->contact_person_name, + 'contact_number' => $request->contact_number, + 'country_code' => $request->country_code ?? '+971', ]); $sess = session('user'); diff --git a/app/Http/Controllers/Employer/CandidateController.php b/app/Http/Controllers/Employer/CandidateController.php index f53d200..a71efef 100644 --- a/app/Http/Controllers/Employer/CandidateController.php +++ b/app/Http/Controllers/Employer/CandidateController.php @@ -341,10 +341,10 @@ public function updateStatus(Request $request, $id) // Notify worker about the hire offer if it is set to pending if ($dbStatus === 'pending') { $offer->worker->notify(new \App\Notifications\GenericNotification( - "Action Required: Confirm Hiring", - "Employer " . ($offer->employer->name ?? "Employer") . " has sent you a direct hire offer. Please confirm.", + "Direct Hiring Offer", + ($offer->employer->name ?? "Employer") . " wants to hire you. Do you want to accept this job offer?", [ - 'type' => 'hire_request', + 'type' => 'direct_hire_request', 'offer_id' => $offer->id, 'status' => 'pending', ] @@ -458,22 +458,6 @@ private function triggerStatusNotification($application, $status) $job = $application->jobPost; $employer = $job ? $job->employer : null; - // When a new application is submitted: notify employer - if ($status === 'applied') { - if ($employer) { - $employer->notify(new \App\Notifications\GenericNotification( - "New Job Application", - "A candidate has applied for your job: " . ($job->title ?? 'Job Post'), - [ - 'type' => 'job_application_submitted', - 'job_id' => $job->id, - 'application_id' => $application->id, - ] - )); - } - return; - } - // When status is updated: notify worker if ($worker) { $title = "Job Application Update"; @@ -484,6 +468,7 @@ private function triggerStatusNotification($application, $status) $body = "You have been shortlisted for the job: " . ($job->title ?? 'Job Post'); $type = 'shortlisted'; break; + case 'applied': case 'review': case 'reviewing': $body = "Your application is under review for the job: " . ($job->title ?? 'Job Post'); diff --git a/app/Http/Controllers/Employer/WorkerController.php b/app/Http/Controllers/Employer/WorkerController.php index e2c08a0..41abe6e 100644 --- a/app/Http/Controllers/Employer/WorkerController.php +++ b/app/Http/Controllers/Employer/WorkerController.php @@ -497,6 +497,8 @@ public function markHired(Request $request, $id) ->where('worker_id', $worker->id) ->first(); + $application = null; + if ($offer) { $offer->update(['status' => 'pending']); } else { @@ -531,16 +533,28 @@ public function markHired(Request $request, $id) } // Notify the worker - $worker->notify(new \App\Notifications\GenericNotification( - "Action Required: Confirm Hiring", - "Employer " . ($user->name ?? "Employer") . " has sent you a hire request. Please confirm.", - [ - 'type' => 'hire_request', - 'offer_id' => $offer->id ?? '', - 'application_id' => $application->id ?? '', - 'status' => $offer ? 'pending' : 'hire_requested', - ] - )); + if ($offer) { + $worker->notify(new \App\Notifications\GenericNotification( + "Direct Hiring Offer", + ($user->name ?? "Employer") . " wants to hire you. Do you want to accept this job offer?", + [ + 'type' => 'direct_hire_request', + 'offer_id' => $offer->id, + 'status' => 'pending', + ] + )); + } else { + $worker->notify(new \App\Notifications\GenericNotification( + "Action Required: Confirm Hiring", + "Employer " . ($user->name ?? "Employer") . " wants to hire you for '" . ($application->jobPost->title ?? 'Job Post') . "'. Please confirm.", + [ + 'type' => 'hire_request', + 'job_id' => $application->jobPost->id ?? '', + 'application_id' => $application->id, + 'status' => 'hire_requested', + ] + )); + } return back()->with('success', 'Hire request initiated successfully! Awaiting candidate confirmation.'); } diff --git a/app/Models/JobPost.php b/app/Models/JobPost.php index c4efafe..f014fce 100644 --- a/app/Models/JobPost.php +++ b/app/Models/JobPost.php @@ -10,6 +10,34 @@ class JobPost extends Model { use HasFactory, SoftDeletes; + protected static function booted() + { + static::updated(function ($jobPost) { + if ($jobPost->wasChanged('status')) { + $oldStatus = $jobPost->getOriginal('status'); + $newStatus = $jobPost->status; + + // Send push notification to all workers who applied to this job + $applications = $jobPost->applications()->with('worker')->get(); + foreach ($applications as $app) { + $worker = $app->worker; + if ($worker) { + $worker->notify(new \App\Notifications\GenericNotification( + "Job Status Changed", + "The status of the job '" . $jobPost->title . "' has been changed from '" . ucfirst($oldStatus) . "' to '" . ucfirst($newStatus) . "'.", + [ + 'type' => 'job_status_change', + 'job_id' => $jobPost->id, + 'old_status' => $oldStatus, + 'new_status' => $newStatus, + ] + )); + } + } + } + }); + } + protected $fillable = [ 'employer_id', 'title', diff --git a/app/Models/Worker.php b/app/Models/Worker.php index bbf1926..1e07b2a 100644 --- a/app/Models/Worker.php +++ b/app/Models/Worker.php @@ -11,6 +11,27 @@ class Worker extends Model { use HasFactory, SoftDeletes, Notifiable; + protected static function booted() + { + static::updated(function ($worker) { + if ($worker->wasChanged('status')) { + $oldStatus = $worker->getOriginal('status'); + $newStatus = $worker->status; + + $worker->notify(new \App\Notifications\GenericNotification( + "Profile Status Update", + "Your profile status has changed from '" . ucfirst($oldStatus) . "' to '" . ucfirst($newStatus) . "'.", + [ + 'type' => 'worker_status_change', + 'worker_id' => $worker->id, + 'old_status' => $oldStatus, + 'new_status' => $newStatus, + ] + )); + } + }); + } + protected $fillable = [ 'name', 'email', diff --git a/app/Services/FCMService.php b/app/Services/FCMService.php index f6d3c00..e09052c 100644 --- a/app/Services/FCMService.php +++ b/app/Services/FCMService.php @@ -59,7 +59,7 @@ public static function getAccessToken() $base64UrlSignature = self::base64UrlEncode($signature); $jwtToken = $signatureInput . '.' . $base64UrlSignature; - $response = Http::asForm()->post('https://oauth2.googleapis.com/token', [ + $response = Http::withoutVerifying()->asForm()->post('https://oauth2.googleapis.com/token', [ 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', 'assertion' => $jwtToken, ]); @@ -140,7 +140,7 @@ public static function sendPushNotification($token, $title, $body, array $data = $payload['message']['data'] = $formattedData; } - $response = Http::withToken($accessToken) + $response = Http::withoutVerifying()->withToken($accessToken) ->post("https://fcm.googleapis.com/v1/projects/{$projectId}/messages:send", $payload); if ($response->failed()) { diff --git a/resources/js/Pages/Admin/Events/Index.jsx b/resources/js/Pages/Admin/Events/Index.jsx index 875b5ca..a9b9b1d 100644 --- a/resources/js/Pages/Admin/Events/Index.jsx +++ b/resources/js/Pages/Admin/Events/Index.jsx @@ -6,6 +6,15 @@ import { Calendar, MapPin, Clock, Gift, Sparkles, Search, ChevronDown, ChevronUp, Building, User, XCircle, Eye, BellRing } from 'lucide-react'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, + DialogFooter +} from '@/components/ui/dialog'; const hoursList = [ '12:00', '12:30', '01:00', '01:30', '02:00', '02:30', @@ -17,6 +26,7 @@ const hoursList = [ export default function Announcements({ initialAnnouncements }) { const [announcements, setAnnouncements] = useState(initialAnnouncements || []); const [isFormOpen, setIsFormOpen] = useState(false); + const [selectedAnnouncement, setSelectedAnnouncement] = useState(null); const [searchQuery, setSearchQuery] = useState(''); const [statusFilter, setStatusFilter] = useState('All'); const [expandedCards, setExpandedCards] = useState({}); @@ -36,7 +46,10 @@ export default function Announcements({ initialAnnouncements }) { event_date: '', event_time: '09:00 AM - 04:00 PM', location_details: '', - location_pin: '' + location_pin: '', + contact_person_name: '', + contact_number: '', + country_code: '+971' }); const [errors, setErrors] = useState({}); const [toastMessage, setToastMessage] = useState(null); @@ -62,6 +75,8 @@ export default function Announcements({ initialAnnouncements }) { } else if (!newAnnouncement.location_pin.startsWith('http://') && !newAnnouncement.location_pin.startsWith('https://')) { newErrors.location_pin = 'Must be a valid URL'; } + if (!newAnnouncement.contact_person_name.trim()) newErrors.contact_person_name = 'Contact person name is required'; + if (!newAnnouncement.contact_number.trim()) newErrors.contact_number = 'Contact number is required'; if (Object.keys(newErrors).length > 0) { setErrors(newErrors); @@ -75,7 +90,10 @@ export default function Announcements({ initialAnnouncements }) { event_date: newAnnouncement.event_date, event_time: newAnnouncement.event_time, location_details: newAnnouncement.location_details, - location_pin: newAnnouncement.location_pin + location_pin: newAnnouncement.location_pin, + contact_person_name: newAnnouncement.contact_person_name, + contact_number: newAnnouncement.contact_number, + country_code: newAnnouncement.country_code }, { onSuccess: () => { setNewAnnouncement({ @@ -85,7 +103,10 @@ export default function Announcements({ initialAnnouncements }) { event_date: '', event_time: '09:00 AM - 04:00 PM', location_details: '', - location_pin: '' + location_pin: '', + contact_person_name: '', + contact_number: '', + country_code: '+971' }); setStartTimeHour('09:00'); setStartTimeAmpm('AM'); @@ -509,6 +530,39 @@ export default function Announcements({ initialAnnouncements }) { +
+
+ + setNewAnnouncement({ ...newAnnouncement, contact_person_name: e.target.value })} + className={`w-full px-4 py-2.5 rounded-xl border ${errors.contact_person_name ? 'border-red-500' : 'border-slate-300'} text-sm focus:ring-2 focus:ring-[#0F6E56]/20 focus:border-[#0F6E56] outline-none transition-all`} + placeholder="e.g. Jane Doe" + /> + {errors.contact_person_name &&

{errors.contact_person_name}

} +
+
+ +
+ setNewAnnouncement({ ...newAnnouncement, country_code: e.target.value })} + className="w-16 px-2 py-2.5 bg-white border border-slate-300 rounded-xl text-xs font-bold focus:ring-2 focus:ring-[#0F6E56]/20 focus:border-[#0F6E56] outline-none text-center" + /> + setNewAnnouncement({ ...newAnnouncement, contact_number: e.target.value })} + className={`flex-1 px-4 py-2.5 rounded-xl border ${errors.contact_number ? 'border-red-500' : 'border-slate-300'} text-sm focus:ring-2 focus:ring-[#0F6E56]/20 focus:border-[#0F6E56] outline-none transition-all`} + placeholder="551234567" + /> +
+ {errors.contact_number &&

{errors.contact_number}

} +
+
+