573 lines
21 KiB
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);
|
|
}
|
|
}
|
|
} |