all(), [ 'device_id' => 'required|exists:devices,id', 'door_open' => 'required|boolean', 'door_location' => 'nullable|string|in:front_panel,back_panel,side_door,main_door', 'open_duration_seconds' => 'nullable|integer|min:0', 'door_opened_at' => 'nullable|date', 'door_closed_at' => 'nullable|date|after:door_opened_at', 'access_card_data' => 'nullable|array', 'proper_closure' => 'nullable|boolean', 'metadata' => 'nullable|array' ]); if ($validator->fails()) { return response()->json([ 'success' => false, 'message' => 'Validation failed', 'errors' => $validator->errors() ], 422); } $data = $validator->validated(); $recordedAt = now(); // Cek apakah dalam jam kerja yang sah $isAuthorizedAccess = $this->checkAuthorizedAccess($recordedAt, $data); // Hitung durasi terbuka $openDuration = $this->calculateOpenDuration($data); // Deteksi pembukaan paksa $isForcedEntry = $this->detectForcedEntry($data, $isAuthorizedAccess, $openDuration); // Tentukan tipe akses $accessType = $this->determineAccessType($data['door_open'], $isAuthorizedAccess, $isForcedEntry, $openDuration); // Tentukan proper closure $properClosure = $data['proper_closure'] ?? true; if (!$data['door_open'] && !isset($data['proper_closure'])) { // Jika pintu ditutup tapi tidak ada info proper_closure, anggap normal $properClosure = true; } // Simpan data door reading $doorReading = DoorReading::create([ 'device_id' => $data['device_id'], 'door_open' => $data['door_open'], 'is_authorized_access' => $isAuthorizedAccess, 'is_forced_entry' => $isForcedEntry, 'access_type' => $accessType, 'door_location' => $data['door_location'] ?? 'main_door', 'open_duration_seconds' => $openDuration, 'proper_closure' => $properClosure, 'access_card_data' => $data['access_card_data'] ?? null, 'metadata' => $data['metadata'] ?? null, 'door_opened_at' => $data['door_opened_at'] ?? null, 'door_closed_at' => $data['door_closed_at'] ?? null, 'recorded_at' => $recordedAt ]); // Jika akses tidak sah atau pembukaan paksa, buat alert $alertSent = false; if (!$isAuthorizedAccess || $isForcedEntry || $accessType === 'unauthorized') { $this->handleUnauthorizedAccess($doorReading); $alertSent = true; } return response()->json([ 'success' => true, 'message' => 'Door sensor data received successfully', 'data' => [ 'id' => $doorReading->id, 'door_open' => $doorReading->door_open, 'access_type' => $accessType, 'is_authorized_access' => $isAuthorizedAccess, 'is_forced_entry' => $isForcedEntry, 'security_risk_level' => $doorReading->getSecurityRiskLevel(), 'alert_sent' => $alertSent, 'recorded_at' => $recordedAt->toISOString() ] ], 201); } catch (\Exception $e) { Log::error('Door sensor data receive error: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'Failed to process door sensor data', 'error' => $e->getMessage() ], 500); } } /** * Ambil data door sensor 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'); $doorOpen = $request->get('door_open'); $query = DoorReading::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 ($doorOpen !== null) { $query->where('door_open', $doorOpen === '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 sensor data', 'error' => $e->getMessage() ], 500); } } /** * Ambil statistik akses pintu */ public function getStatistics(Request $request): JsonResponse { try { $deviceId = $request->get('device_id'); $hours = $request->get('hours', 24); $query = DoorReading::where('recorded_at', '>=', now()->subHours($hours)); if ($deviceId) { $query->where('device_id', $deviceId); } $stats = [ 'total_readings' => $query->count(), 'door_opened_count' => $query->where('door_open', true)->count(), 'authorized_access_count' => $query->where('is_authorized_access', true)->count(), 'unauthorized_access_count' => $query->where('is_authorized_access', false)->count(), 'forced_entry_count' => $query->where('is_forced_entry', true)->count(), 'normal_access_count' => $query->where('access_type', 'normal')->count(), 'maintenance_access_count' => $query->where('access_type', 'maintenance')->count(), 'avg_open_duration' => $query->where('door_open', true)->avg('open_duration_seconds'), 'max_open_duration' => $query->max('open_duration_seconds'), 'currently_open_doors' => $query->currentlyOpen()->count(), 'latest_access' => $query->where('door_open', true)->orderBy('recorded_at', 'desc')->first(), 'door_locations' => $this->getDoorLocationStatistics($query), 'access_types' => $this->getAccessTypeStatistics($query), 'security_incidents' => $query->where(function($q) { $q->where('is_forced_entry', true) ->orWhere('is_authorized_access', false); })->count() ]; // Hitung persentase if ($stats['total_readings'] > 0) { $stats['unauthorized_percentage'] = round(($stats['unauthorized_access_count'] / $stats['total_readings']) * 100, 2); $stats['security_incident_percentage'] = round(($stats['security_incidents'] / $stats['total_readings']) * 100, 2); } else { $stats['unauthorized_percentage'] = 0; $stats['security_incident_percentage'] = 0; } return response()->json([ 'success' => true, 'data' => $stats, 'period_hours' => $hours ]); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => 'Failed to fetch door statistics', 'error' => $e->getMessage() ], 500); } } /** * Get doors that are currently open */ public function getCurrentlyOpenDoors(Request $request): JsonResponse { try { $deviceId = $request->get('device_id'); $query = DoorReading::currentlyOpen()->with('device'); if ($deviceId) { $query->where('device_id', $deviceId); } $openDoors = $query->get(); return response()->json([ 'success' => true, 'data' => $openDoors, 'count' => $openDoors->count(), 'message' => $openDoors->count() > 0 ? 'Found open doors' : 'All doors are closed' ]); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => 'Failed to fetch open doors', 'error' => $e->getMessage() ], 500); } } /** * Cek apakah akses dalam jam kerja yang sah */ private function checkAuthorizedAccess(Carbon $timestamp, array $data): bool { // Jam kerja: Senin-Jumat 07:00-18:00 $workStart = 7; // 07:00 $workEnd = 18; // 18:00 $hour = $timestamp->hour; $isWeekday = $timestamp->isWeekday(); $isWorkingHours = $isWeekday && $hour >= $workStart && $hour < $workEnd; // Jika ada data kartu akses, dianggap sah if (isset($data['access_card_data']) && !empty($data['access_card_data'])) { return true; } // Jika dalam jam kerja, dianggap sah return $isWorkingHours; } /** * Hitung durasi terbuka */ private function calculateOpenDuration(array $data): int { if (isset($data['door_opened_at']) && isset($data['door_closed_at'])) { $start = Carbon::parse($data['door_opened_at']); $end = Carbon::parse($data['door_closed_at']); return $start->diffInSeconds($end); } return $data['open_duration_seconds'] ?? 0; } /** * Deteksi pembukaan paksa */ private function detectForcedEntry(array $data, bool $isAuthorizedAccess, int $openDuration): bool { // Indikator pembukaan paksa: // 1. Tidak ada otorisasi dan tidak ada kartu akses // 2. Durasi sangat singkat (<3 detik) - kemungkinan dipaksa lalu ditutup cepat // 3. Tidak ditutup dengan benar di luar jam kerja if (!$isAuthorizedAccess && empty($data['access_card_data'])) { return true; } if ($openDuration > 0 && $openDuration < 3) { return true; } if (isset($data['proper_closure']) && !$data['proper_closure'] && !$isAuthorizedAccess) { return true; } return false; } /** * Tentukan tipe akses */ private function determineAccessType(bool $doorOpen, bool $isAuthorizedAccess, bool $isForcedEntry, int $openDuration): string { if ($isForcedEntry) { return 'forced'; } if (!$isAuthorizedAccess) { return 'unauthorized'; } // Maintenance jika terbuka lama (>5 menit) dengan otorisasi if ($doorOpen && $openDuration > 300 && $isAuthorizedAccess) { return 'maintenance'; } return 'normal'; } /** * Handle akses tidak sah - buat alert dan kirim notifikasi */ private function handleUnauthorizedAccess(DoorReading $reading): void { try { $device = $reading->device; // Tentukan priority berdasarkan tipe akses $priority = match($reading->access_type) { 'forced' => 'critical', 'unauthorized' => 'high', default => 'medium' }; // Buat alert $alert = Alert::create([ 'device_id' => $reading->device_id, 'type' => 'door_access', 'priority' => $priority, 'title' => 'Akses Pintu Tidak Sah Terdeteksi', 'message' => $this->generateAlertMessage($reading, $device), 'data' => [ 'door_reading_id' => $reading->id, 'access_type' => $reading->access_type, 'door_location' => $reading->door_location, 'is_forced_entry' => $reading->is_forced_entry, 'open_duration' => $reading->open_duration_seconds, 'is_authorized_access' => $reading->is_authorized_access, 'security_risk_level' => $reading->getSecurityRiskLevel() ], 'is_read' => false ]); // Kirim notifikasi $this->sendNotification($alert, $reading); Log::info("Unauthorized door access alert created", [ 'alert_id' => $alert->id, 'device_id' => $reading->device_id, 'access_type' => $reading->access_type, 'door_location' => $reading->door_location ]); } catch (\Exception $e) { Log::error('Failed to handle unauthorized door access: ' . $e->getMessage()); } } /** * Generate pesan alert berdasarkan data door */ private function generateAlertMessage(DoorReading $reading, Device $device): string { $accessInfo = $reading->is_authorized_access ? 'akses sah' : 'akses tidak sah'; $locationInfo = $reading->door_location ? " pada {$reading->door_location}" : ''; $durationInfo = $reading->open_duration_seconds > 0 ? " selama {$reading->open_duration_seconds} detik" : ''; $message = "Akses {$reading->access_type} terdeteksi pada {$device->name}{$locationInfo}. "; $message .= "Status: {$accessInfo}{$durationInfo}."; if ($reading->is_forced_entry) { $message .= " PERINGATAN: Indikasi pembukaan paksa!"; } return $message; } /** * Kirim notifikasi menggunakan NotificationService */ private function sendNotification(Alert $alert, DoorReading $reading): void { try { $notificationService = new NotificationService(); $success = $notificationService->sendDoorAlert($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 notification service error: ' . $e->getMessage()); } } /** * Statistik per lokasi pintu */ private function getDoorLocationStatistics($query): array { $locations = ['front_panel', 'back_panel', 'side_door', 'main_door']; $locationStats = []; foreach ($locations as $location) { $locationStats[$location] = $query->where('door_location', $location)->count(); } return $locationStats; } /** * Statistik per tipe akses */ private function getAccessTypeStatistics($query): array { $types = ['normal', 'unauthorized', 'forced', 'maintenance']; $typeStats = []; foreach ($types as $type) { $typeStats[$type] = $query->where('access_type', $type)->count(); } return $typeStats; } }