431 lines
14 KiB
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 ?? '')),
|
|
]);
|
|
}
|
|
}
|
|
}
|