311 lines
11 KiB
PHP
311 lines
11 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Enums\JenisPengurangan;
|
|
use App\Enums\StatusPengajuan;
|
|
use App\Enums\StatusPresensi;
|
|
use App\Enums\StatusValidasi;
|
|
use App\Enums\StatusVerifikasiWajah;
|
|
use App\Models\Presensi;
|
|
use App\Models\JadwalKerja;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Http\UploadedFile;
|
|
use App\Models\PenggunaanPoin;
|
|
|
|
class PresensiService
|
|
{
|
|
public function getDynamicSchedule($userId, $date)
|
|
{
|
|
$jadwal = JadwalKerja::with('shift')
|
|
->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;
|
|
}
|
|
}
|