presensiService = $presensiService; } public function index() { $user = Auth::user(); $today = Carbon::now('Asia/Jakarta')->toDateString(); $jadwal = \App\Models\JadwalKerja::with(['shift', 'user']) ->where('id_user', $user->id) ->where('tanggal', $today) ->first(); if (!$jadwal) { return response()->json([ 'success' => false, 'message' => 'Tidak ada jadwal hari ini' ], 404); } $jamMasuk = $jadwal->shift->jam_mulai; $jamPulang = $jadwal->shift->jam_selesai; $infoJadwal = $jadwal->shift->nama_shift; $poinOverrides = PenggunaanPoin::where('id_user', $user->id) ->whereDate('tanggal_penggunaan', $today) ->where('id_status', StatusPengajuan::DISETUJUI) ->get(); foreach ($poinOverrides as $poin) { if ($poin->id_pengurangan == JenisPengurangan::MASUK_SIANG_POIN && $poin->jam_masuk_custom) { $jamMasuk = substr($poin->jam_masuk_custom, 0, 5) . ':00'; $infoJadwal .= ' (Masuk Siang)'; } if ($poin->id_pengurangan == JenisPengurangan::PULANG_CEPAT_POIN && $poin->jam_pulang_custom) { $jamPulang = substr($poin->jam_pulang_custom, 0, 5) . ':00'; $infoJadwal .= ' (Pulang Cepat)'; } } $presensi = Presensi::where('id_user', $user->id) ->where('tanggal', $today) ->first(); $ketMasuk = 'Belum Absen'; $ketPulang = null; if ($presensi) { if ($presensi->waktu_terlambat) { $ketMasuk = 'Terlambat'; } else if ($presensi->waktu_masuk_awal) { $ketMasuk = (str_contains($infoJadwal, 'Poin')) ? 'Masuk Siang (Poin) - Datang Awal' : 'Datang Awal'; } else { $ketMasuk = (str_contains($infoJadwal, 'Poin')) ? 'Masuk Siang (Poin)' : 'Tepat Waktu'; } if ($presensi->jam_pulang) { if ($presensi->waktu_pulang_awal) { $ketPulang = 'Pulang Awal'; } else if ($presensi->waktu_pulang_akhir) { $ketPulang = (str_contains($infoJadwal, 'Poin')) ? 'Pulang Cepat (Poin) - Lembur' : 'Lembur / Pulang Akhir'; } else { $ketPulang = (str_contains($infoJadwal, 'Poin')) ? 'Pulang Cepat (Poin)' : 'Tepat Waktu'; } } } return response()->json([ 'success' => true, 'data' => [ 'jadwal' => [ 'nama_shift' => $infoJadwal, 'jam_masuk' => $jamMasuk, 'jam_pulang' => $jamPulang, 'tanggal' => Carbon::parse($today)->translatedFormat('l, d F Y'), ], 'presensi' => [ 'jam_masuk' => $presensi ? $presensi->jam_masuk : null, 'jam_pulang' => $presensi ? $presensi->jam_pulang : null, 'foto_masuk' => $presensi ? $presensi->foto_masuk : null, 'status_validasi' => $presensi ? $presensi->id_validasi : null, 'keterangan' => $presensi ? $ketMasuk : null, 'keterangan_pulang' => $presensi ? $ketPulang : null, ] ] ]); } public function checkRadius(Request $request) { try { $request->validate([ 'latitude' => 'required|numeric', 'longitude' => 'required|numeric', ]); $user = Auth::user(); $kantor = $user->kantor; if (!$kantor) { return response()->json([ 'status' => 'error', 'message' => 'Data kantor belum disetting.', ], 400); } $distance = $this->calculateDistance( $request->latitude, $request->longitude, $kantor->latitude, $kantor->longitude, ); $isWithinRadius = $distance <= $kantor->radius; return response()->json([ 'status' => 'success', 'data' => [ 'is_within_radius' => $isWithinRadius, 'distance' => round($distance, 2), 'radius' => $kantor->radius, ] ]); } catch (\Exception $e) { return response()->json([ 'status' => 'error', 'message' => $e->getMessage(), ], 500); } } 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; } public function store(Request $request) { try { $request->validate([ 'latitude' => 'required|numeric|between:-90,90', 'longitude' => 'required|numeric|between:-180,180', 'foto' => 'required|image|max:2048', 'status' => 'required|in:masuk,pulang', 'keterangan_luar_radius' => 'nullable|string|max:500', 'alasan_telat' => 'nullable|string|max:500', ]); $user = Auth::user(); if ($request->status == 'masuk') { $data = $this->presensiService->absenMasuk($user, $request); return ApiResponse::success($data, 'Absen masuk berhasil'); } else { $data = $this->presensiService->absenPulang($user, $request); return ApiResponse::success($data, 'Absen pulang berhasil'); } } catch (\Exception $e) { $code = (int) $e->getCode(); if ($code < 100 || $code > 599) $code = 500; return ApiResponse::error($e->getMessage(), $code); } } public function history(Request $request) { try { $userId = Auth::id(); $month = $request->get('month', Carbon::now('Asia/Jakarta')->month); $year = $request->get('year', Carbon::now('Asia/Jakarta')->year); $history = Presensi::where('id_user', $userId) ->whereMonth('tanggal', $month) ->whereYear('tanggal', $year) ->latest('tanggal') ->get(); $enriched = $history->map(function ($item) use ($userId) { $jadwal = \App\Models\JadwalKerja::with('shift') ->where('id_user', $userId) ->where('tanggal', $item->tanggal) ->first(); $namaShift = $jadwal?->shift?->nama_shift ?? '-'; $statusMasuk = '-'; if ($item->jam_masuk) { if ($item->id_status == StatusPresensi::ALPHA) { $statusMasuk = 'Alpha (Batal)'; } elseif ($item->waktu_terlambat) { $statusMasuk = 'Terlambat'; } elseif ($item->waktu_masuk_awal) { $statusMasuk = 'Datang Awal'; } else { $statusMasuk = 'Tepat Waktu'; } } else { if ($item->alasan_telat && str_contains($item->alasan_telat, 'Cuti')) { $statusMasuk = 'Cuti'; } elseif ($item->id_status == StatusPresensi::IZIN) { $statusMasuk = 'Izin'; } elseif ($item->id_status == StatusPresensi::SAKIT) { $statusMasuk = 'Sakit'; } elseif ($item->id_status != null && $item->id_status != StatusPresensi::TEPAT_WAKTU && $item->id_status != StatusPresensi::TERLAMBAT) { $statusStatus = \App\Enums\StatusPresensi::label($item->id_status); if ($statusStatus !== 'Unknown') { $statusMasuk = $statusStatus; } } } $statusPulang = null; if ($item->jam_pulang) { if ($item->waktu_pulang_awal) { $statusPulang = 'Pulang Awal'; } elseif ($item->waktu_pulang_akhir) { $statusPulang = 'Lembur'; } else { $statusPulang = 'Tepat Waktu'; } } $totalJam = '-'; if ($item->jam_masuk && $item->jam_pulang) { try { $masuk = Carbon::parse($item->tanggal . ' ' . $item->jam_masuk); $pulang = Carbon::parse($item->tanggal . ' ' . $item->jam_pulang); if ($pulang->lessThan($masuk)) { $pulang->addDay(); } $diffMinutes = abs($pulang->diffInMinutes($masuk)); $hours = floor($diffMinutes / 60); $minutes = $diffMinutes % 60; $totalJam = $hours . 'j ' . $minutes . 'm'; } catch (\Exception $e) { $totalJam = '-'; } } elseif ($item->jam_masuk && !$item->jam_pulang && $item->id_status == StatusPresensi::ALPHA) { $totalJam = '0j 0m (Batal)'; } return [ 'id_presensi' => $item->id_presensi, 'tanggal' => Carbon::parse($item->tanggal)->translatedFormat('l, d F Y'), 'tanggal_raw' => $item->tanggal, 'jam_masuk' => $item->jam_masuk, 'jam_pulang' => $item->jam_pulang, 'shift' => $namaShift, 'status_masuk' => $statusMasuk, 'status_pulang' => $statusPulang, 'total_jam' => $totalJam, 'waktu_terlambat' => $item->waktu_terlambat, 'keterangan_luar_radius' => $item->keterangan_luar_radius, 'verifikasi_wajah' => (bool) $item->verifikasi_wajah, 'status_validasi' => $item->id_validasi, 'alasan_penolakan' => $item->alasan_penolakan, 'foto_masuk_url' => $item->foto_wajah_masuk ? asset('storage/' . $item->foto_wajah_masuk) : null, 'foto_pulang_url' => $item->foto_wajah_pulang ? asset('storage/' . $item->foto_wajah_pulang) : null, ]; }); return ApiResponse::success([ 'data' => $enriched->values(), 'month' => (int) $month, 'year' => (int) $year, ]); } catch (\Exception $e) { return ApiResponse::error($e->getMessage(), 500); } } public function resubmit(Request $request, $id) { try { $request->validate([ 'keterangan' => 'required|string|max:500', ]); $presensi = Presensi::where('id_presensi', $id) ->where('id_user', Auth::id()) ->firstOrFail(); if ($presensi->id_validasi != StatusValidasi::DITOLAK) { return ApiResponse::error('Hanya presensi yang ditolak yang bisa diajukan ulang.', 422); } $presensi->update([ 'id_validasi' => StatusValidasi::PENDING, 'alasan_penolakan' => null, 'keterangan_luar_radius' => $request->keterangan, ]); return ApiResponse::success(null, 'Presensi berhasil diajukan ulang. Menunggu validasi admin.'); } catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) { return ApiResponse::error('Data presensi tidak ditemukan.', 404); } catch (\Exception $e) { return ApiResponse::error($e->getMessage(), 500); } } }