SIPDAM/samooapk/laravel/app/Http/Controllers/Api/AbsensiApiController.php

573 lines
21 KiB
PHP

<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Absensi;
use App\Models\Teknisi;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
class AbsensiApiController extends Controller
{
/**
* Absen masuk untuk teknisi dengan support status.
*/
public function absenMasuk(Request $request)
{
Log::info('Absen Masuk 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_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')) {
$data['foto_absen_masuk'] = $request->file('foto_absen_masuk')
->store('absensi-masuk', 'public');
}
$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')) {
$data['foto_absen_keluar'] = $request->file('foto_absen_keluar')
->store('absensi-keluar', 'public');
}
$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);
}
}
}