MIF_E31221353/app/Http/Controllers/AbsensiController.php

431 lines
14 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Models\Attendance;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Response;
use Illuminate\Validation\Rule;
class AbsensiController extends Controller
{
/**
* Tampilkan riwayat absensi pengguna saat ini.
*/
public function indexUser(Request $request)
{
$this->finalizeExpiredAttendancesForUser(Auth::id());
$baseQuery = Attendance::where('user_id', Auth::id());
if ($request->filled('start_date')) {
$baseQuery->whereDate('clock_in', '>=', $request->start_date);
}
if ($request->filled('end_date')) {
$baseQuery->whereDate('clock_in', '<=', $request->end_date);
}
if ($request->filled('q')) {
$q = $request->q;
$baseQuery->where(function ($inner) use ($q) {
$inner->where('status', 'like', "%{$q}%")
->orWhere('note', 'like', "%{$q}%");
});
}
$items = (clone $baseQuery)
->orderByDesc('clock_in')
->paginate(10)
->withQueryString();
// Validasi diterima: clock_in & clock_out ada, durasi >= 12 jam
$verifiedCount = (clone $baseQuery)
->whereNotNull('clock_in')
->whereNotNull('clock_out')
->get()
->filter(function ($item) {
return $item->clock_in && $item->clock_out && $item->clock_in->diffInHours($item->clock_out) >= 12;
})
->count();
// Validasi ditolak: clock_in & clock_out ada, durasi < 12 jam
$rejectedCount = (clone $baseQuery)
->whereNotNull('clock_in')
->whereNotNull('clock_out')
->get()
->filter(function ($item) {
return $item->clock_in && $item->clock_out && $item->clock_in->diffInHours($item->clock_out) < 12;
})
->count();
$todayDate = now()->toDateString();
$nowTz = now(config('app.timezone'));
$attendanceEnabled = (bool) (Auth::user()->attendance_enabled ?? true);
$todayAttendance = (clone $baseQuery)
->whereDate('clock_in', $todayDate)
->orderByDesc('clock_in')
->first();
$openAttendance = (clone $baseQuery)
->whereNull('clock_out')
->where(function ($inner) {
$inner->whereNull('status')->orWhere('status', 'hadir');
})
->orderByDesc('clock_in')
->first();
$canClockIn = $todayAttendance === null;
$canMarkSpecial = $canClockIn;
$canClockOut = $openAttendance !== null;
$openClockInTime = $openAttendance?->clock_in?->timezone(config('app.timezone'));
if (! $attendanceEnabled) {
$canClockIn = false;
$canMarkSpecial = false;
$canClockOut = false;
}
return view('absensi.history', compact(
'items',
'verifiedCount',
'rejectedCount',
'todayAttendance',
'openAttendance',
'canClockIn',
'canClockOut',
'canMarkSpecial',
'openClockInTime',
'nowTz',
'attendanceEnabled'
));
}
public function edit(Attendance $attendance)
{
abort_unless($attendance->user_id === Auth::id(), 403);
$statuses = [
'hadir' => 'Hadir',
'izin' => 'Izin',
'sakit' => 'Sakit',
'alpha' => 'Alpha',
];
return view('absensi.edit', [
'attendance' => $attendance,
'statuses' => $statuses,
]);
}
public function update(Request $request, Attendance $attendance)
{
abort_unless($attendance->user_id === Auth::id(), 403);
$validated = $request->validate([
'note' => ['nullable', 'string', 'max:255'],
'action' => ['nullable', 'string', Rule::in(['update_note', 'mark_sick', 'mark_izin'])],
]);
$action = $validated['action'] ?? 'update_note';
if (in_array($action, ['mark_sick', 'mark_izin'], true)) {
$status = $action === 'mark_sick' ? 'sakit' : 'izin';
$attendance->update([
'status' => $status,
'note' => $validated['note'] ?? null,
'clock_out' => null,
]);
return redirect()
->route('user.absensi')
->with('status', 'Data absensi ditandai sebagai ' . strtoupper($status) . '.');
}
$attendance->update([
'note' => $validated['note'] ?? null,
]);
return redirect()
->route('user.absensi')
->with('status', 'Data absensi berhasil diperbarui.');
}
/**
* Export data absensi user ke CSV
*/
public function exportCsv(Request $request)
{
$query = Attendance::where('user_id', Auth::id());
// Filter tanggal mulai
if ($request->filled('start_date')) {
$query->whereDate('clock_in', '>=', $request->start_date);
}
// Filter tanggal akhir
if ($request->filled('end_date')) {
$query->whereDate('clock_in', '<=', $request->end_date);
}
// Pencarian keyword status/catatan
if ($request->filled('q')) {
$q = $request->q;
$query->where(function ($w) use ($q) {
$w->where('status', 'like', "%{$q}%")
->orWhere('note', 'like', "%{$q}%");
});
}
$rows = $query->orderBy('clock_in')->get();
$callback = function () use ($rows) {
$out = fopen('php://output', 'w');
// Header CSV
fputcsv($out, ['Tanggal', 'Masuk', 'Keluar', 'Durasi (menit)', 'Status', 'Catatan']);
foreach ($rows as $r) {
$in = $r->clock_in ? $r->clock_in->timezone(config('app.timezone')) : null;
$outAt = $r->clock_out ? $r->clock_out->timezone(config('app.timezone')) : null;
$minutes = ($in && $outAt) ? $outAt->diffInMinutes($in) : 0;
fputcsv($out, [
$in ? $in->format('Y-m-d') : '-',
$in ? $in->format('H:i') : '-',
$outAt ? $outAt->format('H:i') : '-',
$minutes,
$r->status ?? '-',
$r->note ?? '-',
]);
}
fclose($out);
};
$fname = 'absensi_' . now()->format('Ymd_His') . '.csv';
return Response::streamDownload($callback, $fname, [
'Content-Type' => 'text/csv',
]);
}
/**
* Absen masuk (clock in)
*/
public function clockIn(Request $request)
{
$userId = Auth::id();
if (! (Auth::user()->attendance_enabled ?? true)) {
return back()->withErrors([
'absensi' => 'Absensi untuk akun Anda sedang dinonaktifkan oleh admin.',
]);
}
$this->finalizeExpiredAttendancesForUser($userId);
$unfinishedAttendance = Attendance::where('user_id', $userId)
->whereNull('clock_out')
->where(function ($inner) {
$inner->whereNull('status')->orWhere('status', 'hadir');
})
->orderByDesc('clock_in')
->first();
if ($unfinishedAttendance) {
$openedAt = $unfinishedAttendance->clock_in?->timezone(config('app.timezone'));
$dateLabel = $openedAt ? $openedAt->format('d M Y H:i') : 'sebelumnya';
$nowTz = now(config('app.timezone'));
if ($openedAt && $openedAt->isSameDay($nowTz)) {
return back()->withErrors([
'absensi' => 'Anda masih memiliki absensi yang belum diselesaikan sejak ' . $dateLabel . '. Silakan selesaikan terlebih dahulu.',
]);
}
$autoRejectNote = 'Sistem: Absensi ditolak karena tidak melakukan absen keluar.';
$note = trim($autoRejectNote . ' ' . ($unfinishedAttendance->note ?? ''));
$unfinishedAttendance->update([
'status' => 'alpha',
'note' => $note,
]);
$request->merge([
'note' => trim($autoRejectNote . ' ' . ($request->note ?? '')),
]);
}
$todayDate = now()->toDateString();
$hasTodayAttendance = Attendance::where('user_id', $userId)
->whereDate('clock_in', $todayDate)
->exists();
if ($hasTodayAttendance) {
return back()->withErrors([
'absensi' => 'Anda sudah memiliki data absensi pada tanggal ini.',
]);
}
$validated = $request->validate([
'selfie_photo' => 'required|image|mimes:jpeg,png,jpg|max:2048',
'note' => 'nullable|string|max:255',
'latitude' => 'nullable|numeric|between:-90,90',
'longitude' => 'nullable|numeric|between:-180,180',
'accuracy' => 'nullable|numeric|min:0',
'location_name' => 'nullable|string|max:255',
]);
$path = $request->file('selfie_photo')->store('selfies', 'public');
Attendance::create([
'user_id' => Auth::id(),
'clock_in' => now(),
'status' => 'hadir',
'selfie_photo' => $path, // contoh: selfies/abc.jpg
'clock_in_latitude' => $validated['latitude'] ?? null,
'clock_in_longitude' => $validated['longitude'] ?? null,
'clock_in_accuracy' => $validated['accuracy'] ?? null,
'clock_in_location_name' => $validated['location_name'] ?? null,
'note' => $validated['note'] ?? null,
]);
return back()->with('status', 'Absen masuk berhasil.');
}
/**
* Absen keluar (clock out)
*/
public function clockOut(Request $request)
{
if (! (Auth::user()->attendance_enabled ?? true)) {
return back()->withErrors(['absensi' => 'Absensi untuk akun Anda sedang dinonaktifkan oleh admin.']);
}
$this->finalizeExpiredAttendancesForUser(Auth::id());
$attendance = Attendance::where('user_id', Auth::id())
->whereNull('clock_out')
->orderByDesc('clock_in')
->first();
if (! $attendance) {
return back()->withErrors(['absensi' => 'Belum ada absen masuk hari ini.']);
}
$clockIn = $attendance->clock_in?->timezone(config('app.timezone'));
$now = now(config('app.timezone'));
if ($clockIn && $now->greaterThan($clockIn->copy()->addHours(13))) {
$attendance->update([
'status' => 'alpha',
'note' => trim('Sistem: Absen keluar ditolak karena melebihi batas 13 jam. ' . ($attendance->note ?? '')),
]);
return back()->withErrors(['absensi' => 'Batas 13 jam telah terlampaui, absen keluar tidak dapat diproses.']);
}
// Jika kurang dari 12 jam, status jadi alpha (tidak diterima)
if ($clockIn && $now->diffInHours($clockIn) < 12) {
$attendance->update([
'clock_out' => $now,
'status' => 'alpha',
'note' => trim(($attendance->note ? $attendance->note . ' ' : '') . 'Sistem: Absen keluar sebelum 12 jam, validasi tidak diterima.')
]);
return back()->withErrors(['absensi' => 'Absen keluar tidak diterima karena belum 12 jam kerja. Status: Alpha.']);
}
$attendance->update([
'clock_out' => $now,
'status' => 'hadir',
]);
return back()->with('status', 'Absen keluar berhasil.');
}
public function markSick(Request $request)
{
return $this->createSpecialAttendance($request, 'sakit');
}
public function markIzin(Request $request)
{
return $this->createSpecialAttendance($request, 'izin');
}
protected function createSpecialAttendance(Request $request, string $status)
{
if (! (Auth::user()->attendance_enabled ?? true)) {
return back()->withErrors(['absensi' => 'Absensi untuk akun Anda sedang dinonaktifkan oleh admin.']);
}
$validated = $request->validate([
'note' => ['nullable', 'string', 'max:255'],
]);
$today = now()->toDateString();
$alreadyExists = Attendance::where('user_id', Auth::id())
->whereDate('clock_in', $today)
->exists();
if ($alreadyExists) {
return back()->withErrors(['absensi' => 'Anda sudah memiliki data absensi pada tanggal ini.']);
}
Attendance::create([
'user_id' => Auth::id(),
'clock_in' => now(),
'clock_out' => null,
'status' => $status,
'note' => $validated['note'] ?? null,
]);
return back()->with('status', 'Absensi ' . ucfirst($status) . ' berhasil dicatat.');
}
protected function finalizeExpiredAttendancesForUser(int $userId): void
{
$openAttendances = Attendance::where('user_id', $userId)
->whereNull('clock_out')
->where(function ($inner) {
$inner->whereNull('status')->orWhere('status', 'hadir');
})
->get();
if ($openAttendances->isEmpty()) {
return;
}
$tz = config('app.timezone');
$nowTz = now($tz);
foreach ($openAttendances as $attendance) {
$clockIn = $attendance->clock_in?->timezone($tz);
if (! $clockIn) {
continue;
}
$deadline = $clockIn->copy()->addHours(13);
if ($nowTz->lessThan($deadline)) {
continue;
}
$notePrefix = 'Sistem: Absensi otomatis ditolak karena tidak melakukan absen keluar dalam 13 jam (batas: ' . $deadline->format('d M Y H:i') . ').';
$attendance->update([
'clock_out' => $deadline->copy()->timezone($tz),
'status' => 'alpha',
'note' => trim($notePrefix . ' ' . ($attendance->note ?? '')),
]);
}
}
}