all(), [ 'type' => 'required|in:in,out', 'latitude' => 'required|numeric|between:-90,90', 'longitude' => 'required|numeric|between:-180,180', 'photo' => 'required|image|mimes:jpeg,png,jpg|max:5120', // 5MB ]); if ($validator->fails()) { return response()->json([ 'message' => Str::ucfirst($validator->errors()->first()), 'data' => null ], 422); } $user = auth()->user(); $today = Carbon::today()->toDateString(); $existingAttendance = Attendance::where('user_id', $user->id) ->where('date', $today) ->where('type', $request->type) ->whereNotIn('status', ['rejected']) ->first(); if ($existingAttendance) { $typeText = $request->type == 'in' ? 'masuk' : 'keluar'; return response()->json([ 'message' => "Anda sudah melakukan absen {$typeText} hari ini.", 'data' => null ], 422); } if ($request->type == 'out') { $clockInRecord = Attendance::where('user_id', $user->id) ->where('date', $today) ->where('type', 'in') ->first(); if (!$clockInRecord) { return response()->json([ 'message' => 'Anda belum melakukan absen masuk hari ini.', 'data' => null ], 422); } } $photoPath = null; if ($request->hasFile('photo')) { $photo = $request->file('photo'); $filename = time() . '_' . $user->id . '_' . $request->type . '.' . $photo->getClientOriginalExtension(); $photoPath = $photo->storeAs('attendances', $filename, 'public'); } // Tentukan status late atau accepted $now = Carbon::now(); $status = 'accepted'; if ($request->type == 'in' && $now->format('H:i:s') > '08:00:00') { $status = 'late'; } if ($request->type == 'out' && $now->format('H:i:s') > '17:00:00') { $status = 'late'; } $nearestLocation = $this->findNearestLocation($request->latitude, $request->longitude); if (!$nearestLocation) { return response()->json([ 'message' => 'Lokasi Anda di luar area yang diizinkan untuk absen.', 'data' => null ], 422); } $attendance = Attendance::create([ 'user_id' => $user->id, 'date' => $today, 'type' => $request->type, 'time' => Carbon::now()->format('H:i:s'), 'photo' => $photoPath, 'latitude' => $request->latitude, 'longitude' => $request->longitude, 'location_id' => $nearestLocation->id, 'status' => $status ]); return response()->json([ 'message' => 'Absensi berhasil dicatat.', 'data' => $attendance->load(['location']) ], 200); } public function history(Request $request) { $user = auth()->user(); $data = Attendance::where('user_id', $user->id)->with(['location']); if ($request->has('date') && !empty($request->start_date)) { $data->where('date', '>=', $request->start_date); } if ($request->has('end_date') && !empty($request->end_date)) { $data->where('date', '<=', $request->end_date); } if ($request->has('status') && !empty($request->status)) { $data->where('status', $request->status); } if ($request->has('type') && !empty($request->type)) { $data->where('type', $request->type); } $total_data = $data->get()->count(); $length = intval($request->input('length', 10)); $start = intval($request->input('start', 0)); $data = $data->orderBy("date", "desc")->orderBy("time", "desc"); if (!$length && !$start) { $data = $data->get(); } else { $data = $data->skip($start)->take($length)->get(); } return response()->json([ 'message' => 'Data berhasil diambil.', 'data' => $data, 'recordsTotal' => $total_data, 'recordsFiltered' => $total_data, ], 200); } public function todayStatus() { $user = auth()->user(); $today = Carbon::today()->toDateString(); $clockIn = Attendance::where('user_id', $user->id) ->where('date', $today) ->where('type', 'in') ->first(); $clockOut = Attendance::where('user_id', $user->id) ->where('date', $today) ->where('type', 'out') ->first(); return response()->json([ 'message' => 'Status absensi hari ini berhasil diambil.', 'data' => [ 'clock_in' => $clockIn, 'clock_out' => $clockOut, 'can_clock_in' => !$clockIn, 'can_clock_out' => $clockIn && !$clockOut ] ], 200); } public function checkLocation(Request $request) { $validator = Validator::make($request->all(), [ 'latitude' => 'required|numeric|between:-90,90', 'longitude' => 'required|numeric|between:-180,180', ]); if ($validator->fails()) { return response()->json([ 'valid' => false, 'message' => $validator->errors()->first(), 'data' => null ], 422); } $location = $this->findNearestLocation($request->latitude, $request->longitude); if ($location) { return response()->json([ 'valid' => true, 'message' => 'Lokasi valid untuk check-in.', 'data' => $location ], 200); } else { return response()->json([ 'valid' => false, 'message' => 'Lokasi Anda di luar area yang diizinkan untuk absen.', 'data' => null ], 200); } } private function findNearestLocation($latitude, $longitude) { $locations = Location::all(); foreach ($locations as $location) { $distance = $this->calculateDistance( $latitude, $longitude, $location->center_lat, $location->center_lng ); // $distance dalam kilometer, radius dalam meter if ($distance * 1000 <= $location->radius) { return $location; } } return null; } private function calculateDistance($lat1, $lon1, $lat2, $lon2) { $earthRadius = 6371; // km $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; } public function areas() { // Ambil semua Location. Jika perlu filter aktif, tambahkan where. $locations = Location::all()->map(function($loc) { return [ 'id' => $loc->id, 'center_lat' => (float) $loc->center_lat, 'center_lng' => (float) $loc->center_lng, 'radius' => (float) $loc->radius, 'name' => $loc->name ?? null, ]; }); return response()->json($locations, 200); } }