where('id_user', $userId) ->where('tanggal', $date) ->first(); if (!$jadwal) { return null; } $jamMasuk = $jadwal->shift->jam_mulai; $jamPulang = $jadwal->shift->jam_selesai; $status = 'Normal'; $lokasi = $jadwal->kantor; $penggunaanPoin = PenggunaanPoin::where('id_user', $userId) ->where('tanggal_penggunaan', $date) ->where('id_status', StatusPengajuan::DISETUJUI) ->first(); if ($penggunaanPoin) { if ($penggunaanPoin->id_pengurangan == JenisPengurangan::MASUK_SIANG_POIN && $penggunaanPoin->jam_masuk_custom) { $jamMasuk = $penggunaanPoin->jam_masuk_custom; if (strlen($jamMasuk) == 5) $jamMasuk .= ':00'; $status = 'Masuk Siang (Poin)'; } if ($penggunaanPoin->id_pengurangan == JenisPengurangan::PULANG_CEPAT_POIN && $penggunaanPoin->jam_pulang_custom) { $jamPulang = $penggunaanPoin->jam_pulang_custom; if (strlen($jamPulang) == 5) $jamPulang .= ':00'; $status = 'Pulang Cepat (Poin)'; } } return [ 'jam_masuk' => $jamMasuk, 'jam_pulang' => $jamPulang, 'shift_nama' => $jadwal->shift->nama_shift, 'status_jadwal' => $status, 'lokasi' => $lokasi ]; } public function absenMasuk($user, $request) { if (!$user->is_face_registered || !$user->dataWajah || $user->dataWajah->is_verified != StatusVerifikasiWajah::APPROVED) { throw new \Exception('Presensi ditolak. Wajah Anda belum terdaftar atau belum disetujui/diverifikasi oleh HRD.', 403); } $hariIni = Carbon::today('Asia/Jakarta')->toDateString(); $jamSekarang = Carbon::now('Asia/Jakarta'); $jadwal = JadwalKerja::with(['shift']) ->where('id_user', $user->id) ->where('tanggal', $hariIni) ->first(); if (!$jadwal) { throw new \Exception('Anda tidak memiliki jadwal kerja hari ini.', 403); } $cekPresensi = Presensi::where('id_user', $user->id) ->where('tanggal', $hariIni) ->first(); if ($cekPresensi) { throw new \Exception('Anda sudah melakukan absen masuk hari ini.', 400); } $kantor = $user->kantor; if (!$kantor) { throw new \Exception('Kantor belum disetting.', 400); } $jarak = $this->calculateDistance($request->latitude, $request->longitude, $kantor->latitude, $kantor->longitude); $radiusKantor = $kantor->radius; $isDalamRadius = ($jarak <= $radiusKantor); $dynamicSchedule = $this->getDynamicSchedule($user->id, $hariIni); if (!$dynamicSchedule) { $jamMasukEfektif = $jadwal->shift->jam_mulai; $statusJadwal = 'Normal'; } else { $jamMasukEfektif = $dynamicSchedule['jam_masuk']; $statusJadwal = $dynamicSchedule['status_jadwal']; } $toleransi = 15; $batasTerlambat = Carbon::parse($jamMasukEfektif, 'Asia/Jakarta')->addMinutes($toleransi); $isTerlambat = $jamSekarang->greaterThan($batasTerlambat); $jamJadwalMasuk = Carbon::parse($jamMasukEfektif, 'Asia/Jakarta'); $waktuTerlambat = null; $waktuMasukAwal = null; if ($jamSekarang->greaterThan($jamJadwalMasuk)) { $waktuTerlambat = $jamSekarang->diff($jamJadwalMasuk)->format('%H:%I:%S'); } else if ($jamSekarang->lessThan($jamJadwalMasuk)) { $waktuMasukAwal = $jamSekarang->diff($jamJadwalMasuk)->format('%H:%I:%S'); } $idValidasi = StatusValidasi::VALID; $statusKehadiran = 'Tepat Waktu'; $alasanTelat = $request->alasan_telat ?? null; if ($isTerlambat) { $idValidasi = StatusValidasi::PENDING; $statusKehadiran = 'Terlambat'; } else if ($waktuMasukAwal != null) { $idValidasi = StatusValidasi::VALID; $statusKehadiran = 'Datang Awal'; if (str_contains($statusJadwal, 'Poin')) { $statusKehadiran = 'Masuk Siang (Poin) - Datang Awal'; } } else { $idValidasi = StatusValidasi::VALID; if (str_contains($statusJadwal, 'Poin')) { $statusKehadiran = 'Masuk Siang (Poin)'; } } if (!$isDalamRadius) { $idValidasi = StatusValidasi::PENDING; } $idStatus = ($statusKehadiran === 'Terlambat') ? StatusPresensi::TERLAMBAT : StatusPresensi::TEPAT_WAKTU; $faceVerified = 0; if ($user->dataWajah && $user->dataWajah->is_verified == StatusVerifikasiWajah::APPROVED) { $faceVerified = 1; } $fotoPath = $this->uploadFile($request->file('foto'), 'masuk', $user->id); $presensi = Presensi::create([ 'id_user' => $user->id, 'tanggal' => $hariIni, 'jam_masuk' => $jamSekarang->format('H:i:s'), 'lat_masuk' => $request->latitude, 'lon_masuk' => $request->longitude, 'foto_wajah_masuk' => $fotoPath, 'id_status' => $idStatus, 'alasan_telat' => $alasanTelat, 'id_validasi' => $idValidasi, 'verifikasi_wajah' => $faceVerified, 'keterangan_luar_radius' => $request->keterangan_luar_radius ?? null, 'waktu_terlambat' => $waktuTerlambat, 'waktu_masuk_awal' => $waktuMasukAwal ]); return [ 'jam_masuk' => $jamSekarang->format('H:i'), 'status_kehadiran' => $statusKehadiran, 'dalam_radius' => $isDalamRadius, 'jarak' => round($jarak) . ' meter', 'face_verified' => (bool) $faceVerified, 'jam_efektif' => $jamMasukEfektif ]; } public function absenPulang($user, $request) { if (!$user->is_face_registered || !$user->dataWajah || $user->dataWajah->is_verified != StatusVerifikasiWajah::APPROVED) { throw new \Exception('Presensi ditolak. Wajah Anda belum terdaftar atau belum disetujui/diverifikasi oleh HRD.', 403); } $jamSekarang = Carbon::now('Asia/Jakarta'); $presensi = Presensi::where('id_user', $user->id) ->whereNull('jam_pulang') ->latest() ->first(); if (!$presensi) { throw new \Exception('Belum ada data absen masuk hari ini atau sudah pulang.', 404); } if ($presensi->jam_pulang) { throw new \Exception('Anda sudah absen pulang hari ini.', 400); } $tanggalPresensi = $presensi->tanggal; $jadwalCek = JadwalKerja::with('shift') ->where('id_user', $user->id) ->where('tanggal', $tanggalPresensi) ->first(); $isShiftMalam = false; if ($jadwalCek && $jadwalCek->shift) { $isShiftMalam = $jadwalCek->shift->jam_selesai < $jadwalCek->shift->jam_mulai; } $isHariIni = Carbon::parse($tanggalPresensi)->isToday(); if (!$isHariIni && !$isShiftMalam) { throw new \Exception('Tidak bisa absen pulang untuk shift normal hari sebelumnya. Hubungi HRD untuk koreksi.', 400); } $jamMasukPresensi = Carbon::parse($presensi->jam_masuk, 'Asia/Jakarta'); if (!$isShiftMalam && $jamSekarang->lessThan($jamMasukPresensi)) { throw new \Exception('Jam pulang tidak boleh lebih awal dari jam masuk (' . $presensi->jam_masuk . ').', 400); } $dynamicSchedule = $this->getDynamicSchedule($user->id, $tanggalPresensi); if (!$dynamicSchedule) { $jadwalFallback = $jadwalCek; if (!$jadwalFallback) { throw new \Exception('Jadwal kerja tidak ditemukan.', 404); } $jamPulangEfektif = $jadwalFallback->shift->jam_selesai; $statusJadwal = 'Normal'; } else { $jamPulangEfektif = $dynamicSchedule['jam_pulang']; $statusJadwal = $dynamicSchedule['status_jadwal']; } $jamPulangTarget = Carbon::parse($jamPulangEfektif, 'Asia/Jakarta'); $isPulangAwal = $jamSekarang->lessThan($jamPulangTarget); $waktuPulangAwal = null; $waktuPulangAkhir = null; if ($jamSekarang->lessThan($jamPulangTarget)) { $waktuPulangAwal = $jamSekarang->diff($jamPulangTarget)->format('%H:%I:%S'); } else if ($jamSekarang->greaterThan($jamPulangTarget)) { $waktuPulangAkhir = $jamSekarang->diff($jamPulangTarget)->format('%H:%I:%S'); } $statusPulang = 'Tepat Waktu'; $keterangan = $request->keterangan_pulang ?? null; if ($isPulangAwal) { $statusPulang = 'Pulang Awal'; } else if ($waktuPulangAkhir != null) { $statusPulang = 'Lembur / Pulang Akhir'; } else { if (str_contains($statusJadwal, 'Poin')) { $statusPulang = 'Pulang Cepat (Poin)'; } } $fotoPath = $this->uploadFile($request->file('foto'), 'pulang', $user->id); $presensi->update([ 'jam_pulang' => $jamSekarang->format('H:i:s'), 'lat_pulang' => $request->latitude, 'lon_pulang' => $request->longitude, 'foto_wajah_pulang' => $fotoPath, 'keterangan_pulang' => $keterangan, 'waktu_pulang_awal' => $waktuPulangAwal, 'waktu_pulang_akhir' => $waktuPulangAkhir ]); return [ 'jam_pulang' => $jamSekarang->format('H:i'), 'status_pulang' => $statusPulang, 'keterangan' => $keterangan ]; } private function uploadFile(UploadedFile $file, $type, $userId) { $fileName = "{$type}_{$userId}_" . time() . '.' . $file->getClientOriginalExtension(); return $file->storeAs('uploads/absensi', $fileName, 'public'); } private function calculateDistance($lat1, $lon1, $lat2, $lon2) { $earthRadius = 6371000; $dLat = deg2rad($lat2 - $lat1); $dLon = deg2rad($lon2 - $lon1); $a = sin($dLat / 2) * sin($dLat / 2) + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLon / 2) * sin($dLon / 2); $c = 2 * atan2(sqrt($a), sqrt(1 - $a)); return $earthRadius * $c; } }