query('q', '')); if (mb_strlen($term) < 2) { return response()->json(['data' => [], 'message' => 'Taip sekurang-kurangnya 2 aksara.']); } $members = Member::query() ->search($term) ->with('attendance') ->orderBy('nama') ->limit(25) ->get() ->map(fn (Member $m) => $this->present($m)); return response()->json([ 'data' => $members, 'message' => $members->isEmpty() ? 'Rekod tidak dijumpai. Individu ini bukan anggota koperasi yang berdaftar.' : null, ]); } /** Rekod kehadiran — elak double attendance */ public function store(Request $request): JsonResponse { $data = $request->validate([ 'member_id' => ['required', 'integer', 'exists:members,id'], ]); $member = Member::with('attendance')->findOrFail($data['member_id']); if ($member->attendance) { return response()->json([ 'status' => 'already', 'message' => 'Anggota ini sudah direkodkan hadir.', 'member' => $this->present($member), ], 200); } try { $attendance = AttendanceRecord::create([ 'member_id' => $member->id, 'attended_at' => now(), 'recorded_by' => $request->user()->id, 'ip_address' => $request->ip(), 'user_agent' => substr((string) $request->userAgent(), 0, 255), ]); } catch (UniqueConstraintViolationException $e) { // Race: dua kaunter rekod serentak $member->load('attendance'); return response()->json([ 'status' => 'already', 'message' => 'Anggota ini sudah direkodkan hadir.', 'member' => $this->present($member), ], 200); } AuditLog::record('attendance.record', "Rekod hadir: {$member->nama}", $attendance, [ 'member_id' => $member->id, ]); $member->load('attendance'); return response()->json([ 'status' => 'ok', 'message' => "Kehadiran {$member->nama} berjaya direkodkan.", 'member' => $this->present($member), ]); } private function present(Member $m): array { return [ 'id' => $m->id, 'nama' => $m->nama, 'no_pekerja' => $m->no_pekerja, 'no_anggota' => $m->no_anggota, 'masked_kp' => $m->maskedKp(), 'jabatan' => $m->jabatan, 'bahagian' => $m->bahagian, 'status_aktif' => (bool) $m->status_aktif, 'hadir' => (bool) $m->attendance, 'attended_at' => $m->attendance?->attended_at?->format('d/m/Y H:i:s'), ]; } }