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 ?? '')), ]); } } }