normalizeEsp32Payload($request->all()); // Validasi input $validator = Validator::make($input, [ 'device_id' => 'nullable|integer', 'door_opened' => 'required|boolean', 'access_method' => 'nullable|string|in:keycard,manual,force,emergency,maintenance,unknown', 'user_id_card' => 'nullable|string|max:50', 'duration_seconds' => 'nullable|integer|min:0', 'door_location' => 'nullable|string|in:front_door,back_door,side_door,main_entrance,rack', 'is_forced_entry' => 'nullable|boolean', 'door_opened_at' => 'nullable|date', 'door_closed_at' => 'nullable|date', 'metadata' => 'nullable|array' ]); if ($validator->fails()) { return response()->json([ 'success' => false, 'message' => 'Validation failed', 'errors' => $validator->errors() ], 422); } $data = $validator->validated(); // Resolve device $device = $this->resolveDevice($data, $input); if (!$device) { return response()->json([ 'success' => false, 'message' => 'Device not found. Pastikan device sudah terdaftar di database.', ], 422); } $data['device_id'] = $device->id; $recordedAt = now(); // Hitung durasi jika ada door_opened_at dan door_closed_at $durationSeconds = $data['duration_seconds'] ?? 0; if (isset($data['door_opened_at']) && isset($data['door_closed_at'])) { $openedAt = Carbon::parse($data['door_opened_at']); $closedAt = Carbon::parse($data['door_closed_at']); $durationSeconds = $openedAt->diffInSeconds($closedAt); } // Cek apakah dalam jam kerja $isAuthorizedTime = $this->checkAuthorizedTime($recordedAt); // Tentukan apakah akses sah $isAuthorizedAccess = $this->determineAuthorizedAccess( $data['door_opened'], $data['access_method'] ?? 'unknown', $data['user_id_card'] ?? null, $isAuthorizedTime, $data['is_forced_entry'] ?? false ); // Tentukan apakah akses mencurigakan $isSuspicious = $this->determineSuspiciousAccess( $data['door_opened'], $isAuthorizedAccess, $durationSeconds, $data['access_method'] ?? 'unknown', $data['user_id_card'] ?? null, $data['is_forced_entry'] ?? false ); // Tentukan tipe akses $accessType = $this->determineAccessType( $isAuthorizedAccess, $isAuthorizedTime, $data['access_method'] ?? 'unknown', $data['is_forced_entry'] ?? false ); // Simpan data Door Access $doorReading = DoorAccessReading::create([ 'device_id' => $data['device_id'], 'door_opened' => $data['door_opened'], 'is_authorized_access' => $isAuthorizedAccess, 'access_type' => $accessType, 'access_method' => $data['access_method'] ?? 'unknown', 'user_id_card' => $data['user_id_card'] ?? null, 'duration_seconds' => $durationSeconds, 'is_suspicious' => $isSuspicious, 'door_location' => $data['door_location'] ?? 'main_entrance', 'is_forced_entry' => $data['is_forced_entry'] ?? false, 'metadata' => $data['metadata'] ?? null, 'door_opened_at' => $data['door_opened_at'] ?? null, 'door_closed_at' => $data['door_closed_at'] ?? null, 'recorded_at' => $recordedAt ]); // Tandai device sebagai online Device::markOnline($data['device_id']); // Tulis activity log $sensor = \App\Models\Sensor::where('device_id', $data['device_id'])->where('type', 'reed_switch')->first(); \App\Models\ActivityLog::create([ 'device_id' => $data['device_id'], 'sensor_id' => $sensor?->id, 'event_type' => $data['door_opened'] ? 'door_opened' : 'door_closed', 'severity' => $isSuspicious ? 'warning' : 'info', 'title' => $data['door_opened'] ? 'Rak Dibuka' : 'Rak Ditutup', 'description'=> "Reed switch mendeteksi rak " . ($data['door_opened'] ? 'dibuka' : 'ditutup') . " di lokasi " . ($data['door_location'] ?? 'rack') . ". Durasi: {$durationSeconds}s.", 'event_data' => ['door_reading_id' => $doorReading->id, 'access_type' => $accessType, 'duration' => $durationSeconds], 'location' => $sensor?->device?->location ?? 'Rak A - Lantai 1', 'event_time' => $recordedAt, ]); // Jika akses mencurigakan atau tidak sah, buat alert dan kirim notifikasi $alertSent = false; if ($isSuspicious || !$isAuthorizedAccess || $accessType === 'forced_entry') { $this->handleSuspiciousAccess($doorReading); $alertSent = true; } return response()->json([ 'success' => true, 'message' => 'Door access data received successfully', 'data' => [ 'id' => $doorReading->id, 'door_opened' => $doorReading->door_opened, 'access_type' => $accessType, 'is_authorized' => $isAuthorizedAccess, 'is_suspicious' => $isSuspicious, 'alert_sent' => $alertSent, 'priority' => $doorReading->getPriorityLevel(), 'recorded_at' => $recordedAt->toISOString() ] ], 201); } catch (\Exception $e) { Log::error('Door access data receive error: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'Failed to process door access data', 'error' => $e->getMessage() ], 500); } } /** * Ambil data door access terbaru */ public function getLatestReadings(Request $request): JsonResponse { try { $deviceId = $request->get('device_id'); $limit = $request->get('limit', 50); $accessType = $request->get('access_type'); $doorLocation = $request->get('door_location'); $suspicious = $request->get('suspicious'); $query = DoorAccessReading::with('device') ->orderBy('recorded_at', 'desc'); if ($deviceId) { $query->where('device_id', $deviceId); } if ($accessType) { $query->where('access_type', $accessType); } if ($doorLocation) { $query->where('door_location', $doorLocation); } if ($suspicious !== null) { $query->where('is_suspicious', $suspicious === 'true'); } $readings = $query->limit($limit)->get(); return response()->json([ 'success' => true, 'data' => $readings, 'count' => $readings->count() ]); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => 'Failed to fetch door access data', 'error' => $e->getMessage() ], 500); } } /** * Ambil statistik door access */ public function getStatistics(Request $request): JsonResponse { try { $deviceId = $request->get('device_id'); $hours = $request->get('hours', 24); $query = DoorAccessReading::where('recorded_at', '>=', now()->subHours($hours)); if ($deviceId) { $query->where('device_id', $deviceId); } $stats = [ 'total_access_attempts' => $query->count(), 'door_opened_count' => $query->where('door_opened', true)->count(), 'authorized_count' => $query->where('is_authorized_access', true)->count(), 'unauthorized_count' => $query->where('is_authorized_access', false)->count(), 'suspicious_count' => $query->where('is_suspicious', true)->count(), 'forced_entry_count' => $query->where('is_forced_entry', true)->count(), 'avg_duration' => $query->where('door_opened', true)->avg('duration_seconds'), 'max_duration' => $query->max('duration_seconds'), 'latest_access' => $query->orderBy('recorded_at', 'desc')->first(), 'access_types' => $this->getAccessTypeStatistics($query), 'door_locations' => $this->getDoorLocationStatistics($query), 'access_methods' => $this->getAccessMethodStatistics($query) ]; // Hitung persentase if ($stats['total_access_attempts'] > 0) { $stats['authorized_percentage'] = round(($stats['authorized_count'] / $stats['total_access_attempts']) * 100, 2); $stats['suspicious_percentage'] = round(($stats['suspicious_count'] / $stats['total_access_attempts']) * 100, 2); } else { $stats['authorized_percentage'] = 0; $stats['suspicious_percentage'] = 0; } return response()->json([ 'success' => true, 'data' => $stats, 'period_hours' => $hours ]); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => 'Failed to fetch statistics', 'error' => $e->getMessage() ], 500); } } /** * Normalisasi payload dari ESP32 ke format yang diharapkan controller. * * ESP32 LoRa raw: { node_id, gateway_id, type, door_opened, device_id } * ESP32 MQTT buildPayload: { device_id, node, type, door } */ private function normalizeEsp32Payload(array $input): array { // Normalisasi door_opened: bisa dari "door" (MQTT buildPayload) if (!isset($input['door_opened']) && isset($input['door'])) { $input['door_opened'] = $input['door']; } // Atau dari "door_open" (format lain) if (!isset($input['door_opened']) && isset($input['door_open'])) { $input['door_opened'] = $input['door_open']; } // Pastikan boolean if (isset($input['door_opened'])) { $input['door_opened'] = filter_var($input['door_opened'], FILTER_VALIDATE_BOOLEAN); } if (isset($input['is_forced_entry'])) { $input['is_forced_entry'] = filter_var($input['is_forced_entry'], FILTER_VALIDATE_BOOLEAN); } return $input; } /** * Resolve device dari payload — cari by id, fallback ke device pertama aktif */ private function resolveDevice(array $data, array $rawInput): ?\App\Models\Device { if (!empty($data['device_id'])) { $device = \App\Models\Device::find((int) $data['device_id']); if ($device) return $device; } $nodeId = $rawInput['node_id'] ?? $rawInput['node'] ?? null; if ($nodeId) { $device = \App\Models\Device::where('device_id', $nodeId)->first(); if ($device) return $device; } return \App\Models\Device::where('is_active', true)->first() ?? \App\Models\Device::first(); } /** * Cek apakah waktu dalam jam kerja */ private function checkAuthorizedTime(Carbon $timestamp): bool { // Jam kerja: Senin-Jumat 07:00-18:00 $workStart = 7; // 07:00 $workEnd = 18; // 18:00 $hour = $timestamp->hour; $isWeekday = $timestamp->isWeekday(); return $isWeekday && $hour >= $workStart && $hour < $workEnd; } /** * Tentukan apakah akses sah */ private function determineAuthorizedAccess(bool $doorOpened, string $accessMethod, ?string $userIdCard, bool $isAuthorizedTime, bool $isForcedEntry): bool { if ($isForcedEntry) { return false; } if ($accessMethod === 'emergency') { return true; // Emergency access selalu dianggap sah } if ($accessMethod === 'keycard' && $userIdCard && $this->isValidIdCard($userIdCard)) { return true; } if ($accessMethod === 'maintenance' && $isAuthorizedTime) { return true; } return false; } /** * Tentukan apakah akses mencurigakan */ private function determineSuspiciousAccess(bool $doorOpened, bool $isAuthorized, int $duration, string $accessMethod, ?string $userIdCard, bool $isForcedEntry): bool { if ($isForcedEntry) { return true; } if (!$isAuthorized && $accessMethod !== 'emergency') { return true; } if ($duration > 180) { // Pintu terbuka lebih dari 3 menit return true; } if ($doorOpened && !$userIdCard && $accessMethod === 'manual') { return true; } return false; } /** * Tentukan tipe akses */ private function determineAccessType(bool $isAuthorized, bool $isAuthorizedTime, string $accessMethod, bool $isForcedEntry): string { if ($isForcedEntry) { return 'forced_entry'; } if ($accessMethod === 'emergency') { return 'emergency'; } if ($accessMethod === 'maintenance') { return 'maintenance'; } if ($isAuthorized && $isAuthorizedTime) { return 'authorized'; } if (!$isAuthorizedTime) { return 'after_hours'; } return 'unauthorized'; } /** * Validasi ID card (simulasi) */ private function isValidIdCard(string $idCard): bool { // Format: EMP-XXXX (4 digit angka) return preg_match('/^EMP-\d{4}$/', $idCard); } /** * Handle akses mencurigakan - buat alert dan kirim notifikasi */ private function handleSuspiciousAccess(DoorAccessReading $reading): void { try { $device = $reading->device; // Tentukan priority berdasarkan tipe akses $priority = match($reading->access_type) { 'forced_entry' => 'critical', 'unauthorized' => 'high', 'after_hours' => 'high', 'emergency' => 'medium', 'maintenance' => 'low', default => 'medium' }; // Buat alert $alert = Alert::create([ 'device_id' => $reading->device_id, 'type' => 'door_access_alert', 'priority' => $priority, 'title' => 'Akses Pintu Mencurigakan Terdeteksi', 'message' => $this->generateAlertMessage($reading, $device), 'data' => [ 'door_reading_id' => $reading->id, 'access_type' => $reading->access_type, 'access_method' => $reading->access_method, 'user_id_card' => $reading->user_id_card, 'duration' => $reading->duration_seconds, 'door_location' => $reading->door_location, 'is_forced_entry' => $reading->is_forced_entry, 'is_authorized_access' => $reading->is_authorized_access ], 'is_read' => false ]); // Kirim notifikasi $this->sendNotification($alert, $reading); Log::info("Suspicious door access alert created", [ 'alert_id' => $alert->id, 'device_id' => $reading->device_id, 'access_type' => $reading->access_type, 'user_id_card' => $reading->user_id_card ]); } catch (\Exception $e) { Log::error('Failed to handle suspicious door access: ' . $e->getMessage()); } } /** * Generate pesan alert berdasarkan data door access */ private function generateAlertMessage(DoorAccessReading $reading, Device $device): string { $locationInfo = $reading->door_location ? " di {$reading->door_location}" : ''; $userInfo = $reading->user_id_card ? " oleh {$reading->user_id_card}" : ' tanpa ID card'; $methodInfo = $reading->access_method !== 'unknown' ? " menggunakan {$reading->access_method}" : ''; if ($reading->is_forced_entry) { return "PAKSA MASUK terdeteksi pada {$device->name}{$locationInfo}. Durasi: {$reading->duration_seconds} detik."; } return "Akses {$reading->access_type} terdeteksi pada {$device->name}{$locationInfo}{$userInfo}{$methodInfo}. " . "Durasi: {$reading->duration_seconds} detik."; } /** * Kirim notifikasi menggunakan NotificationService */ private function sendNotification(Alert $alert, DoorAccessReading $reading): void { try { $notificationService = new NotificationService(); $success = $notificationService->sendDoorAccessAlert($alert, $reading); if ($success) { Log::info("Door access notification sent successfully", [ 'alert_id' => $alert->id, 'device_name' => $reading->device->name, 'access_type' => $reading->access_type ]); } else { Log::warning("Door access notification failed", [ 'alert_id' => $alert->id, 'device_id' => $reading->device_id ]); } } catch (\Exception $e) { Log::error('Door access notification service error: ' . $e->getMessage()); } } /** * Statistik per tipe akses */ private function getAccessTypeStatistics($query): array { $types = ['authorized', 'unauthorized', 'after_hours', 'forced_entry', 'emergency', 'maintenance']; $typeStats = []; foreach ($types as $type) { $typeStats[$type] = $query->where('access_type', $type)->count(); } return $typeStats; } /** * Statistik per lokasi pintu */ private function getDoorLocationStatistics($query): array { $locations = ['front_door', 'back_door', 'side_door', 'main_entrance']; $locationStats = []; foreach ($locations as $location) { $locationStats[$location] = $query->where('door_location', $location)->count(); } return $locationStats; } /** * Statistik per metode akses */ private function getAccessMethodStatistics($query): array { $methods = ['keycard', 'manual', 'force', 'emergency', 'maintenance', 'unknown']; $methodStats = []; foreach ($methods as $method) { $methodStats[$method] = $query->where('access_method', $method)->count(); } return $methodStats; } }