all()); $status = $request->input('status', 'hadir'); $rules = [ 'id_teknisi' => 'required|exists:teknisis,id_teknisi', 'status' => 'nullable|in:hadir,izin,sakit', 'keterangan' => 'nullable|string|max:255', 'latitude' => 'nullable|string', 'longitude' => 'nullable|string', ]; if ($status === 'hadir') { $rules['foto_absen_masuk'] = 'required|image|mimes:jpeg,png,jpg,gif|max:2048'; } else { $rules['foto_absen_masuk'] = 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048'; } if ($status === 'izin') { $rules['keterangan'] = 'required|string|max:255'; } $validator = Validator::make($request->all(), $rules, [ 'foto_absen_masuk.required' => 'Foto wajib untuk status Hadir', 'keterangan.required' => 'Keterangan wajib untuk status Izin', ]); if ($validator->fails()) { Log::error('Validation failed:', $validator->errors()->toArray()); return response()->json([ 'success' => false, 'message' => 'Validasi gagal', 'errors' => $validator->errors() ], 422); } try { // Cek apakah ada sesi yang masih aktif (belum absen keluar) $activeAbsen = Absensi::where('id_teknisi', $request->id_teknisi) ->whereNull('jam_keluar') ->whereDate('tanggal', Carbon::today()) ->first(); if ($activeAbsen) { return response()->json([ 'success' => false, 'message' => 'Anda masih memiliki sesi absen yang aktif' ], 400); } $data = [ 'id_teknisi' => $request->id_teknisi, 'tanggal' => Carbon::now('Asia/Jakarta')->toDateString(), 'jam_masuk' => $status === 'hadir' ? Carbon::now('Asia/Jakarta') : null, 'status' => $status, 'keterangan' => $request->keterangan, 'latitude' => $request->latitude, 'longitude' => $request->longitude, ]; if ($request->hasFile('foto_absen_masuk')) { $file = $request->file('foto_absen_masuk'); $filename = uniqid() . '.' . $file->extension(); $file->move(public_path('storage/absensi-masuk'), $filename); $data['foto_absen_masuk'] = 'absensi-masuk/' . $filename; } $absensi = Absensi::create($data); $absensi->load('teknisi'); return response()->json([ 'success' => true, 'message' => 'Absen masuk berhasil dicatat', 'data' => $absensi ], 201); } catch (\Exception $e) { Log::error('Error in absenMasuk: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'Gagal melakukan absen masuk', 'error' => $e->getMessage() ], 500); } } /** * Absen keluar untuk teknisi dengan support status. */ public function absenKeluar(Request $request) { Log::info('Absen Keluar Request:', $request->all()); $status = $request->input('status', 'hadir'); $rules = [ 'id_teknisi' => 'required|exists:teknisis,id_teknisi', 'status' => 'nullable|in:hadir,izin,sakit', 'keterangan' => 'nullable|string|max:255', 'latitude' => 'nullable|string', 'longitude' => 'nullable|string', ]; if ($status === 'hadir') { $rules['foto_absen_keluar'] = 'required|image|mimes:jpeg,png,jpg,gif|max:2048'; } else { $rules['foto_absen_keluar'] = 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048'; } if ($status === 'izin') { $rules['keterangan'] = 'required|string|max:255'; } $validator = Validator::make($request->all(), $rules, [ 'foto_absen_keluar.required' => 'Foto wajib untuk status Hadir', 'keterangan.required' => 'Keterangan wajib untuk status Izin', ]); if ($validator->fails()) { Log::error('Validation failed:', $validator->errors()->toArray()); return response()->json([ 'success' => false, 'message' => 'Validasi gagal', 'errors' => $validator->errors() ], 422); } try { // Cari sesi terbaru yang belum absen keluar $absensi = Absensi::where('id_teknisi', $request->id_teknisi) ->whereNull('jam_keluar') ->orderBy('id_absensi', 'desc') ->first(); if (!$absensi) { return response()->json([ 'success' => false, 'message' => 'Tidak ada sesi absen aktif yang ditemukan' ], 400); } $data = ['jam_keluar' => Carbon::now('Asia/Jakarta')]; if ($request->has('status')) { $data['status'] = $status; } if ($request->has('keterangan')) { $data['keterangan'] = $absensi->keterangan ? $absensi->keterangan . ' | ' . $request->keterangan : $request->keterangan; } if ($request->has('latitude')) { $data['latitude'] = $request->latitude; } if ($request->has('longitude')) { $data['longitude'] = $request->longitude; } if ($request->hasFile('foto_absen_keluar')) { $file = $request->file('foto_absen_keluar'); $filename = uniqid() . '.' . $file->extension(); $file->move(public_path('storage/absensi-keluar'), $filename); $data['foto_absen_keluar'] = 'absensi-keluar/' . $filename; } $absensi->update($data); $absensi->load('teknisi'); return response()->json([ 'success' => true, 'message' => 'Absen keluar berhasil dicatat', 'data' => $absensi ], 200); } catch (\Exception $e) { Log::error('Error in absenKeluar: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'Gagal melakukan absen keluar', 'error' => $e->getMessage() ], 500); } } /** * Mengecek status absensi teknisi hari ini. */ public function checkStatus($id_teknisi) { try { $teknisi = Teknisi::where('id_teknisi', $id_teknisi)->first(); if (!$teknisi) { return response()->json([ 'success' => false, 'message' => 'Teknisi tidak ditemukan' ], 404); } // Ambil sesi terbaru hari ini $absensi = Absensi::where('id_teknisi', $id_teknisi) ->whereDate('tanggal', Carbon::today()) ->orderBy('id_absensi', 'desc') ->first(); $status = [ 'sudah_absen_masuk' => false, 'sudah_absen_keluar' => false, 'data_absensi' => null, ]; if ($absensi) { $status['sudah_absen_masuk'] = !empty($absensi->jam_masuk) && empty($absensi->jam_keluar) && $absensi->status === 'hadir'; $status['sudah_absen_keluar'] = !empty($absensi->jam_keluar); $status['data_absensi'] = [ 'jam_masuk' => $absensi->jam_masuk, 'jam_keluar' => $absensi->jam_keluar, 'jam_masuk_formatted' => $absensi->jam_masuk_formatted, 'jam_keluar_formatted' => $absensi->jam_keluar_formatted, 'durasi_kerja_formatted' => $absensi->durasi_kerja_formatted, 'status' => $absensi->status, 'keterangan' => $absensi->keterangan, 'lokasi_masuk' => $absensi->lokasi_masuk ?? '-', 'lokasi_valid' => $absensi->lokasi_valid ?? false, 'latitude' => $absensi->latitude, 'longitude' => $absensi->longitude, 'foto_absen_masuk' => $absensi->foto_absen_masuk, 'foto_absen_keluar' => $absensi->foto_absen_keluar, ]; } return response()->json([ 'success' => true, 'message' => 'Status absensi berhasil diambil', 'data' => $status ], 200); } catch (\Exception $e) { Log::error('Error in checkStatus: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'Gagal mengecek status absensi', 'error' => $e->getMessage() ], 500); } } /** * Mendapatkan riwayat absensi teknisi per bulan * dengan field yang sudah diformat untuk blade & Flutter. */ public function riwayat(Request $request) { try { $id_teknisi = $request->query('id_teknisi'); if (!$id_teknisi) { return response()->json([ 'success' => false, 'message' => 'id_teknisi diperlukan' ], 400); } $query = Absensi::where('id_teknisi', $id_teknisi); if ($request->has('bulan') && $request->has('tahun')) { $query->filterByMonth($request->bulan, $request->tahun); } $absensis = $query->orderBy('tanggal', 'desc')->get(); // ── Transform: kirim field yang sudah diformat ────────────── $data = $absensis->map(function ($absensi) { // Hitung menit telat (jadwal masuk 08:00) $menitTelat = 0; $terlambat = false; if ($absensi->jam_masuk && $absensi->status === 'hadir') { $jamMasuk = Carbon::parse($absensi->jam_masuk)->setTimezone('Asia/Jakarta'); $jamJadwal = Carbon::parse( $absensi->tanggal->format('Y-m-d') . ' 08:00:00' )->setTimezone('Asia/Jakarta'); if ($jamMasuk->gt($jamJadwal)) { $terlambat = true; $menitTelat = $jamMasuk->diffInMinutes($jamJadwal); } } return [ 'tanggal' => $absensi->tanggal ? $absensi->tanggal->format('Y-m-d') : null, 'status' => $absensi->status, 'jam_masuk_formatted' => $absensi->jam_masuk_formatted, 'jam_keluar_formatted' => $absensi->jam_keluar_formatted, 'durasi_kerja_formatted' => $absensi->durasi_kerja_formatted, 'terlambat' => $terlambat, 'menit_telat' => $menitTelat, 'keterangan' => $absensi->keterangan, 'latitude' => $absensi->latitude, 'longitude' => $absensi->longitude, ]; }); return response()->json([ 'success' => true, 'message' => 'Riwayat absensi berhasil diambil', 'data' => $data ], 200); } catch (\Exception $e) { Log::error('Error in riwayat: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'Gagal mengambil riwayat absensi', 'error' => $e->getMessage() ], 500); } } /** * Mendapatkan statistik absensi. */ public function statistik(Request $request) { try { $startDate = $request->input('start_date'); $endDate = $request->input('end_date'); $idTeknisi = $request->input('id_teknisi'); $query = Absensi::query(); if ($startDate && $endDate) { $query->whereBetween('tanggal', [$startDate, $endDate]); } if ($idTeknisi) { $query->where('id_teknisi', $idTeknisi); } $statistik = [ 'total' => $query->count(), 'hadir' => (clone $query)->where('status', 'hadir')->count(), 'sakit' => (clone $query)->where('status', 'sakit')->count(), 'izin' => (clone $query)->where('status', 'izin')->count(), ]; return response()->json([ 'success' => true, 'message' => 'Statistik absensi berhasil diambil', 'data' => $statistik ], 200); } catch (\Exception $e) { Log::error('Error in statistik: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'Gagal mengambil statistik absensi', 'error' => $e->getMessage() ], 500); } } /** * Mendapatkan daftar status absensi yang tersedia. */ public function getStatusOptions() { return response()->json([ 'success' => true, 'message' => 'Daftar status absensi berhasil diambil', 'data' => [ 'hadir' => 'Hadir', 'izin' => 'Izin', 'sakit' => 'Sakit', ] ], 200); } /** * Mendapatkan rekap absensi bulanan teknisi. */ public function rekap(Request $request) { try { $id_teknisi = $request->query('id_teknisi'); $bulan = (int) $request->query('bulan', date('n')); $tahun = (int) $request->query('tahun', date('Y')); if (!$id_teknisi) { return response()->json([ 'success' => false, 'message' => 'id_teknisi diperlukan' ], 400); } $absensis = Absensi::where('id_teknisi', $id_teknisi) ->whereMonth('tanggal', $bulan) ->whereYear('tanggal', $tahun) ->get(); $hadir = $absensis->where('status', 'hadir')->count(); $izin = $absensis->where('status', 'izin')->count(); $sakit = $absensis->where('status', 'sakit')->count(); $total = $absensis->count(); // Hitung persentase kehadiran $persentase = $total > 0 ? round(($hadir / $total) * 100, 1) : 0; // Hitung rata-rata jam masuk $hadirItems = $absensis->where('status', 'hadir') ->filter(fn($a) => $a->jam_masuk !== null); $rataJamMasuk = '-'; $rataJamKeluar = '-'; $rataDurasi = '-'; $terlambat = 0; $streak = 0; if ($hadirItems->count() > 0) { // Rata-rata masuk $totalMasukMenit = $hadirItems->sum(function ($a) { return Carbon::parse($a->jam_masuk) ->setTimezone('Asia/Jakarta') ->hour * 60 + Carbon::parse($a->jam_masuk) ->setTimezone('Asia/Jakarta') ->minute; }); $avgMasuk = round($totalMasukMenit / $hadirItems->count()); $rataJamMasuk = sprintf('%02d:%02d', intdiv($avgMasuk, 60), $avgMasuk % 60); // Rata-rata keluar $keluarItems = $hadirItems->filter(fn($a) => $a->jam_keluar !== null); if ($keluarItems->count() > 0) { $totalKeluarMenit = $keluarItems->sum(function ($a) { return Carbon::parse($a->jam_keluar) ->setTimezone('Asia/Jakarta') ->hour * 60 + Carbon::parse($a->jam_keluar) ->setTimezone('Asia/Jakarta') ->minute; }); $avgKeluar = round($totalKeluarMenit / $keluarItems->count()); $rataJamKeluar = sprintf('%02d:%02d', intdiv($avgKeluar, 60), $avgKeluar % 60); // Rata-rata durasi $totalDurasiMenit = $keluarItems->sum(fn($a) => $a->durasi_kerja); $avgDurasi = round($totalDurasiMenit / $keluarItems->count()); $jam = intdiv($avgDurasi, 60); $menit = $avgDurasi % 60; $rataDurasi = "{$jam}j {$menit}m"; } // Hitung keterlambatan $jadwalMasuk = '08:00'; $terlambat = $hadirItems->filter(function ($a) use ($jadwalMasuk) { $jamMasuk = Carbon::parse($a->jam_masuk)->setTimezone('Asia/Jakarta'); $jamJadwal = Carbon::parse( $a->tanggal->format('Y-m-d') . ' ' . $jadwalMasuk )->setTimezone('Asia/Jakarta'); return $jamMasuk->gt($jamJadwal); })->count(); } // Hitung streak (berturut-turut hadir dari hari ini mundur) $sortedDesc = $absensis->sortByDesc('tanggal'); foreach ($sortedDesc as $a) { if ($a->status === 'hadir') $streak++; else break; } // Nama bulan Indonesia $namaBulan = [ 1=>'Januari',2=>'Februari',3=>'Maret',4=>'April', 5=>'Mei',6=>'Juni',7=>'Juli',8=>'Agustus', 9=>'September',10=>'Oktober',11=>'November',12=>'Desember' ]; return response()->json([ 'success' => true, 'message' => 'Rekap absensi berhasil diambil', 'data' => [ 'bulan' => ($namaBulan[$bulan] ?? $bulan) . ' ' . $tahun, 'total_hari_kerja'=> $total, 'hadir' => $hadir, 'izin' => $izin, 'sakit' => $sakit, 'persentase' => $persentase, 'rata_masuk' => $rataJamMasuk, 'rata_keluar' => $rataJamKeluar, 'rata_durasi' => $rataDurasi, 'keterlambatan' => $terlambat, 'streak' => $streak, ] ], 200); } catch (\Exception $e) { Log::error('Error in rekap: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'Gagal mengambil rekap absensi', 'error' => $e->getMessage() ], 500); } } /** * Mendapatkan status absensi per tanggal dalam 1 bulan (untuk kalender). * Response: { "1": "hadir", "5": "izin", "10": "alpha", ... } */ public function kalender(Request $request) { try { $id_teknisi = $request->query('id_teknisi'); $bulan = $request->query('bulan', date('n')); $tahun = $request->query('tahun', date('Y')); if (!$id_teknisi) { return response()->json([ 'success' => false, 'message' => 'id_teknisi diperlukan' ], 400); } $absensis = Absensi::where('id_teknisi', $id_teknisi) ->whereMonth('tanggal', $bulan) ->whereYear('tanggal', $tahun) ->get(['tanggal', 'status']); // Map tanggal (angka) => status $data = []; foreach ($absensis as $absensi) { $tgl = (int) Carbon::parse($absensi->tanggal)->format('j'); $data[$tgl] = $absensi->status; } return response()->json([ 'success' => true, 'message' => 'Data kalender berhasil diambil', 'data' => $data ], 200); } catch (\Exception $e) { Log::error('Error in kalender: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'Gagal mengambil data kalender', 'error' => $e->getMessage() ], 500); } } }