diff --git a/app/Http/Controllers/Api/EmployerWorkerController.php b/app/Http/Controllers/Api/EmployerWorkerController.php index 1f65a44..7c90f90 100644 --- a/app/Http/Controllers/Api/EmployerWorkerController.php +++ b/app/Http/Controllers/Api/EmployerWorkerController.php @@ -79,6 +79,8 @@ private function formatWorker(Worker $w) 'area' => $w->area, 'live_in_out' => $w->live_in_out, 'in_country' => (bool)$w->in_country, + 'document_expiry_days' => $w->document_expiry_days, + 'document_expiry_status' => $w->document_expiry_status, ]; } diff --git a/app/Http/Controllers/Api/SupportTicketController.php b/app/Http/Controllers/Api/SupportTicketController.php index 8a11e7b..153d476 100644 --- a/app/Http/Controllers/Api/SupportTicketController.php +++ b/app/Http/Controllers/Api/SupportTicketController.php @@ -122,7 +122,8 @@ public function replyToTicketFromWorker(Request $request, $id) } $validator = Validator::make($request->all(), [ - 'message' => 'required|string', + 'message' => 'required_without:voice_note|nullable|string', + 'voice_note' => 'nullable|file|mimes:mp3,wav,m4a,ogg,webm,aac,3gp,amr|max:10240', // 10MB limit ]); if ($validator->fails()) { @@ -133,10 +134,16 @@ public function replyToTicketFromWorker(Request $request, $id) ], 422); } + $voiceNotePath = null; + if ($request->hasFile('voice_note')) { + $voiceNotePath = $request->file('voice_note')->store('support_voice_notes', 'public'); + } + $reply = SupportTicketReply::create([ 'support_ticket_id' => $ticket->id, 'worker_id' => $worker->id, 'message' => $request->message, + 'voice_note_path' => $voiceNotePath, ]); if ($ticket->status === 'resolved') { @@ -150,6 +157,7 @@ public function replyToTicketFromWorker(Request $request, $id) 'id' => $reply->id, 'message' => $reply->message, 'sender_name' => $reply->sender_name, + 'voice_note_url' => $reply->voice_note_path ? asset('storage/' . $reply->voice_note_path) : null, 'created_at' => $reply->created_at->toIso8601String(), ] ], 201); @@ -250,7 +258,8 @@ public function replyToTicketFromEmployer(Request $request, $id) } $validator = Validator::make($request->all(), [ - 'message' => 'required|string', + 'message' => 'required_without:voice_note|nullable|string', + 'voice_note' => 'nullable|file|mimes:mp3,wav,m4a,ogg,webm,aac,3gp,amr|max:10240', // 10MB limit ]); if ($validator->fails()) { @@ -261,10 +270,16 @@ public function replyToTicketFromEmployer(Request $request, $id) ], 422); } + $voiceNotePath = null; + if ($request->hasFile('voice_note')) { + $voiceNotePath = $request->file('voice_note')->store('support_voice_notes', 'public'); + } + $reply = SupportTicketReply::create([ 'support_ticket_id' => $ticket->id, 'user_id' => $employer->id, 'message' => $request->message, + 'voice_note_path' => $voiceNotePath, ]); if ($ticket->status === 'resolved') { @@ -278,6 +293,7 @@ public function replyToTicketFromEmployer(Request $request, $id) 'id' => $reply->id, 'message' => $reply->message, 'sender_name' => $reply->sender_name, + 'voice_note_url' => $reply->voice_note_path ? asset('storage/' . $reply->voice_note_path) : null, 'created_at' => $reply->created_at->toIso8601String(), ] ], 201); @@ -320,6 +336,7 @@ public function getTicketDetail(Request $request, $id) 'sender_name' => $reply->sender_name, 'is_admin' => $reply->user && $reply->user->role === 'admin', 'is_developer_response' => (bool)$reply->is_developer_response, + 'voice_note_url' => $reply->voice_note_path ? asset('storage/' . $reply->voice_note_path) : null, 'created_at' => $reply->created_at->toIso8601String(), ]; }); diff --git a/app/Models/SupportTicketReply.php b/app/Models/SupportTicketReply.php index 35b546c..e9a73bb 100644 --- a/app/Models/SupportTicketReply.php +++ b/app/Models/SupportTicketReply.php @@ -17,6 +17,7 @@ class SupportTicketReply extends Model 'worker_id', 'message', 'is_developer_response', + 'voice_note_path', ]; public function ticket() diff --git a/app/Models/Worker.php b/app/Models/Worker.php index 28cbb7d..a6b8fec 100644 --- a/app/Models/Worker.php +++ b/app/Models/Worker.php @@ -55,6 +55,8 @@ class Worker extends Model 'passport_status', 'visa_status', 'emirates_id_status', + 'document_expiry_days', + 'document_expiry_status', ]; public function getPassportStatusAttribute() @@ -86,6 +88,46 @@ public function getEmiratesIdStatusAttribute() return $this->passport_status; } + public function getDocumentExpiryDaysAttribute() + { + $visa = $this->documents->where('type', 'visa')->first(); + $doc = $visa; + if (!$doc || !$doc->expiry_date) { + $passport = $this->documents->where('type', 'passport')->first(); + $doc = $passport; + } + + if ($doc && $doc->expiry_date) { + $expiry = \Carbon\Carbon::parse($doc->expiry_date); + return (int)now()->startOfDay()->diffInDays(\Carbon\Carbon::parse($expiry)->startOfDay(), false); + } + + return null; + } + + public function getDocumentExpiryStatusAttribute() + { + $visa = $this->documents->where('type', 'visa')->first(); + $doc = $visa; + $typeLabel = 'Visa'; + if (!$doc || !$doc->expiry_date) { + $passport = $this->documents->where('type', 'passport')->first(); + $doc = $passport; + $typeLabel = 'Passport'; + } + + if ($doc && $doc->expiry_date) { + $expiry = \Carbon\Carbon::parse($doc->expiry_date); + $days = (int)now()->startOfDay()->diffInDays(\Carbon\Carbon::parse($expiry)->startOfDay(), false); + if ($days < 0) { + return "$typeLabel Expired"; + } + return "$typeLabel expires in $days days"; + } + + return 'No Documents'; + } + public function category() { return $this->belongsTo(WorkerCategory::class, 'category_id'); diff --git a/database/migrations/2026_06_14_013119_add_voice_note_path_to_support_ticket_replies_table.php b/database/migrations/2026_06_14_013119_add_voice_note_path_to_support_ticket_replies_table.php new file mode 100644 index 0000000..475bb3d --- /dev/null +++ b/database/migrations/2026_06_14_013119_add_voice_note_path_to_support_ticket_replies_table.php @@ -0,0 +1,30 @@ +text('message')->nullable()->change(); + $table->string('voice_note_path')->nullable()->after('message'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('support_ticket_replies', function (Blueprint $table) { + $table->text('message')->nullable(false)->change(); + $table->dropColumn('voice_note_path'); + }); + } +}; diff --git a/public/swagger.json b/public/swagger.json index 5ec367b..104b2b0 100644 --- a/public/swagger.json +++ b/public/swagger.json @@ -2324,16 +2324,18 @@ "requestBody": { "required": true, "content": { - "application/json": { + "multipart/form-data": { "schema": { "type": "object", - "required": [ - "message" - ], "properties": { "message": { "type": "string", "example": "I tried clearing cache and it still crashes." + }, + "voice_note": { + "type": "string", + "format": "binary", + "description": "Optional audio/voice note file attachment." } } } @@ -4484,16 +4486,18 @@ "requestBody": { "required": true, "content": { - "application/json": { + "multipart/form-data": { "schema": { "type": "object", - "required": [ - "message" - ], "properties": { "message": { "type": "string", "example": "I tried a different card and it still fails." + }, + "voice_note": { + "type": "string", + "format": "binary", + "description": "Optional audio/voice note file attachment." } } } diff --git a/resources/js/Pages/Employer/Workers/Index.jsx b/resources/js/Pages/Employer/Workers/Index.jsx index 513fd5c..6c4ea61 100644 --- a/resources/js/Pages/Employer/Workers/Index.jsx +++ b/resources/js/Pages/Employer/Workers/Index.jsx @@ -705,6 +705,16 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],