$idUser, 'tipe' => $tipe, 'judul' => $judul, 'pesan' => $pesan, 'data' => $data, 'is_read' => false, ]); $unreadCount = Notifikasi::where('id_user', $idUser)->where('is_read', false)->count(); broadcast(new NotifikasiCreated($idUser, $judul, $pesan, $tipe, $unreadCount)); if ($sendFcm) { $data['tipe'] = $tipe; \App\Jobs\SendFcmPushNotification::dispatch($idUser, $judul, $pesan, $data); } } catch (\Exception $e) { Log::error('NotifikasiService::kirim gagal: ' . $e->getMessage()); } } /** * Kirim notifikasi ke semua staff aktif (broadcast pengumuman). */ public function kirimBroadcast(string $tipe, string $judul, string $pesan, array $data = []): void { \App\Jobs\BroadcastNotifikasiJob::dispatch($tipe, $judul, $pesan, $data); } /** * Kirim ke semua user dengan role tertentu (contoh: HRD, Manajer). */ public function kirimKeRole(string $namaRole, string $tipe, string $judul, string $pesan, array $data = [], bool $sendFcm = true): void { $users = User::where('status_aktif', 1) ->whereHas('roles', function ($q) use ($namaRole) { $q->where('nama_role', $namaRole); }) ->pluck('id'); foreach ($users as $idUser) { $this->kirim($idUser, $tipe, $judul, $pesan, $data, $sendFcm); } } /** * Kirim push notification ke device FCM user via FCM HTTP v1 API. * Token yang sudah tidak valid (UNREGISTERED) otomatis dihapus dari DB. */ public function sendPushNotification(int $idUser, string $judul, string $pesan, array $data = []): void { $tokens = DeviceToken::where('id_user', $idUser)->get(); if ($tokens->isEmpty()) { Log::debug("[FCM] User {$idUser}: tidak ada device token."); return; } $accessToken = $this->getFcmAccessToken(); if (!$accessToken) { Log::error("[FCM] User {$idUser}: gagal mendapat OAuth token."); return; } $projectId = env('FIREBASE_PROJECT_ID'); foreach ($tokens as $deviceToken) { $token = $deviceToken->fcm_token; try { $response = \Illuminate\Support\Facades\Http::withToken($accessToken) ->withoutVerifying() ->post("https://fcm.googleapis.com/v1/projects/{$projectId}/messages:send", [ 'message' => [ 'token' => $token, 'notification' => ['title' => $judul, 'body' => $pesan], 'data' => array_map('strval', $data), 'android' => [ 'priority' => 'HIGH', 'notification' => [ 'channel_id' => 'hris_channel', 'click_action' => 'FLUTTER_NOTIFICATION_CLICK', ], ], ], ]); if ($response->successful()) { Log::info("[FCM] Berhasil kirim ke user {$idUser}, token=" . substr($token, 0, 20) . "..."); } else { $errorCode = $response->json('error.details.0.errorCode') ?? $response->json('error.status'); Log::warning("[FCM] Gagal kirim ke user {$idUser}: HTTP {$response->status()}, errorCode={$errorCode}"); // Hapus token kadaluarsa otomatis if (in_array($errorCode, ['UNREGISTERED', 'INVALID_ARGUMENT'])) { $deviceToken->delete(); Log::warning("[FCM] Token kadaluarsa dihapus untuk user {$idUser}."); } } } catch (\Exception $e) { Log::error("[FCM] Exception untuk user {$idUser}: " . $e->getMessage()); } } } /** * Ambil access token Google OAuth2 menggunakan Service Account JWT. * Dicache selama 50 menit (token OAuth biasanya berlaku 1 jam). */ private function getFcmAccessToken(): ?string { return \Illuminate\Support\Facades\Cache::remember('fcm_access_token', 3000, function () { try { $credPath = base_path(env('FIREBASE_CREDENTIALS')); $credentials = json_decode(file_get_contents($credPath), true); $now = time(); $header = rtrim(strtr(base64_encode(json_encode(['alg' => 'RS256', 'typ' => 'JWT'])), '+/', '-_'), '='); $payload = rtrim(strtr(base64_encode(json_encode([ 'iss' => $credentials['client_email'], 'scope' => 'https://www.googleapis.com/auth/firebase.messaging', 'aud' => 'https://oauth2.googleapis.com/token', 'iat' => $now, 'exp' => $now + 3600, ])), '+/', '-_'), '='); openssl_sign("$header.$payload", $sig, $credentials['private_key'], OPENSSL_ALGO_SHA256); $jwt = "$header.$payload." . rtrim(strtr(base64_encode($sig), '+/', '-_'), '='); $resp = \Illuminate\Support\Facades\Http::asForm()->withoutVerifying()->post('https://oauth2.googleapis.com/token', [ 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', 'assertion' => $jwt, ]); return $resp->json('access_token'); } catch (\Exception $e) { Log::error('FCM Token error: ' . $e->getMessage()); return null; } }); } }