'Method not allowed', 'allowed_methods' => ['GET', 'POST', 'PUT', 'DELETE'] ]); break; } } catch (Exception $e) { // Log error dengan detail $errorTime = microtime(true); $errorMemory = memory_get_usage(true); error_log("Geofences CRUD API Error: " . $e->getMessage()); error_log("Error occurred at: " . round($errorTime - $startTime, 2) . " seconds"); error_log("Memory at error: " . number_format($errorMemory / 1024 / 1024, 2) . " MB"); error_log("Error trace: " . $e->getTraceAsString()); http_response_code(500); echo json_encode([ 'error' => 'Internal server error', 'message' => $e->getMessage(), 'processing_time' => round($errorTime - $startTime, 2) . " seconds", 'memory_usage' => number_format(($errorMemory - $startMemory) / 1024 / 1024, 2) . " MB" ]); } finally { // Cleanup memory gc_collect_cycles(); } /** * Handle READ operations (GET) * ============================ * Get geofence records dengan optional filtering */ function handleRead($pdo, $user) { global $startTime, $startMemory; // Get query parameters $id = isset($_GET['id']) ? (int)$_GET['id'] : null; $nama = isset($_GET['nama']) ? trim($_GET['nama']) : ''; $types = isset($_GET['types']) ? trim($_GET['types']) : ''; $active = isset($_GET['active']) ? (int)$_GET['active'] : null; $editBy = isset($_GET['edit_by']) ? trim($_GET['edit_by']) : ''; $limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 100; $offset = isset($_GET['offset']) ? (int)$_GET['offset'] : 0; // Validate limit if ($limit > 1000) { $limit = 1000; } // Build SQL query $sql = "SELECT id, Nama, Catatan, Types, Coordinates, Tgl_Tambah, Tgl_Edit, Edit_By, Edit_By_ID, Active, Geometrys_only FROM geofences WHERE 1=1"; $params = array(); // Add filters if ($id) { $sql .= " AND id = ?"; $params[] = $id; } if (!empty($nama)) { $sql .= " AND Nama LIKE ?"; $params[] = '%' . $nama . '%'; } if (!empty($types)) { $sql .= " AND Types = ?"; $params[] = $types; } if ($active !== null) { $sql .= " AND Active = ?"; $params[] = $active; } if (!empty($editBy)) { $sql .= " AND Edit_By LIKE ?"; $params[] = '%' . $editBy . '%'; } // Add ordering dan pagination $sql .= " ORDER BY Tgl_Tambah DESC, id DESC"; $sql .= " OFFSET $offset ROWS FETCH NEXT $limit ROWS ONLY"; // Execute query $stmt = $pdo->prepare($sql); $stmt->execute($params); $results = $stmt->fetchAll(PDO::FETCH_ASSOC); // Process results - parse coordinates JSON foreach ($results as &$result) { // Parse coordinates JSON if (!empty($result['Coordinates'])) { $coordinates = json_decode($result['Coordinates'], true); if (json_last_error() === JSON_ERROR_NONE) { $result['Coordinates_parsed'] = $coordinates; } else { $result['Coordinates_parsed'] = null; } } // Convert Active bit to boolean $result['Active'] = (bool)$result['Active']; } // Get total count $countSql = "SELECT COUNT(*) as total FROM geofences WHERE 1=1"; $countParams = array(); if ($id) { $countSql .= " AND id = ?"; $countParams[] = $id; } if (!empty($nama)) { $countSql .= " AND Nama LIKE ?"; $countParams[] = '%' . $nama . '%'; } if (!empty($types)) { $countSql .= " AND Types = ?"; $countParams[] = $types; } if ($active !== null) { $countSql .= " AND Active = ?"; $countParams[] = $active; } if (!empty($editBy)) { $countSql .= " AND Edit_By LIKE ?"; $countParams[] = '%' . $editBy . '%'; } $countStmt = $pdo->prepare($countSql); $countStmt->execute($countParams); $totalCount = $countStmt->fetch(PDO::FETCH_ASSOC)['total']; // Performance monitoring $endTime = microtime(true); $endMemory = memory_get_usage(true); $processingTime = round($endTime - $startTime, 2); $memoryUsed = round(($endMemory - $startMemory) / 1024 / 1024, 2); // Return results http_response_code(200); echo json_encode([ 'success' => true, 'data' => $results, 'pagination' => [ 'total_records' => (int)$totalCount, 'returned_records' => count($results), 'limit' => $limit, 'offset' => $offset, 'has_more' => ($offset + $limit) < $totalCount ], 'filters_applied' => [ 'id' => $id, 'nama' => $nama, 'types' => $types, 'active' => $active, 'edit_by' => $editBy ], 'processing_info' => [ 'processing_time' => $processingTime . " seconds", 'memory_usage' => $memoryUsed . " MB", 'user' => $user ] ]); } /** * Handle CREATE operations (POST) * =============================== * Create new geofence record */ function handleCreate($pdo, $user) { global $startTime, $startMemory; // Get request body $input = json_decode(file_get_contents('php://input'), true); if (!$input) { http_response_code(400); echo json_encode([ 'error' => 'Invalid JSON input', 'details' => 'Request body must be valid JSON' ]); return; } // Validasi required fields $requiredFields = ['Nama', 'Types', 'Coordinates']; $missingFields = array(); foreach ($requiredFields as $field) { if (!isset($input[$field]) || trim($input[$field]) === '') { $missingFields[] = $field; } } if (!empty($missingFields)) { http_response_code(400); echo json_encode([ 'error' => 'Missing required fields', 'missing_fields' => $missingFields, 'required_fields' => $requiredFields ]); return; } // Sanitize input $nama = trim($input['Nama']); $catatan = isset($input['Catatan']) ? trim($input['Catatan']) : null; $types = trim($input['Types']); $coordinates = trim($input['Coordinates']); $active = isset($input['Active']) ? (bool)$input['Active'] : true; $geometrysOnly = isset($input['Geometrys_only']) ? trim($input['Geometrys_only']) : null; // Validate nama length if (strlen($nama) > 100) { http_response_code(400); echo json_encode([ 'error' => 'Nama too long', 'details' => 'Nama must be maximum 100 characters', 'current_length' => strlen($nama) ]); return; } // Validate catatan length if ($catatan && strlen($catatan) > 500) { http_response_code(400); echo json_encode([ 'error' => 'Catatan too long', 'details' => 'Catatan must be maximum 500 characters', 'current_length' => strlen($catatan) ]); return; } // Validate types length if (strlen($types) > 20) { http_response_code(400); echo json_encode([ 'error' => 'Types too long', 'details' => 'Types must be maximum 20 characters', 'current_length' => strlen($types) ]); return; } // Validate coordinates JSON format $coordinatesArray = json_decode($coordinates, true); if (json_last_error() !== JSON_ERROR_NONE) { http_response_code(400); echo json_encode([ 'error' => 'Invalid coordinates format', 'details' => 'Coordinates must be valid JSON', 'json_error' => json_last_error_msg() ]); return; } // Validate types value (common geofence types) $validTypes = ['polygon', 'circle', 'rectangle', 'polyline', 'point']; if (!in_array(strtolower($types), $validTypes)) { // Allow custom types but log warning error_log("Geofences CREATE - Custom type used: $types by user: $user"); } // Check for duplicate nama $checkSql = "SELECT COUNT(*) as count FROM geofences WHERE Nama = ?"; $checkStmt = $pdo->prepare($checkSql); $checkStmt->execute([$nama]); $existingCount = $checkStmt->fetch(PDO::FETCH_ASSOC)['count']; if ($existingCount > 0) { http_response_code(409); echo json_encode([ 'error' => 'Geofence name already exists', 'details' => 'A geofence with this name already exists', 'nama' => $nama ]); return; } // Insert record $sql = "INSERT INTO geofences (Nama, Catatan, Types, Coordinates, Tgl_Tambah, Tgl_Edit, Edit_By, Edit_By_ID, Active, Geometrys_only) VALUES (?, ?, ?, ?, GETDATE(), GETDATE(), ?, ?, ?, ?)"; $params = [ $nama, $catatan, $types, $coordinates, $user, null, // Edit_By_ID - could be set if you have user ID mapping $active ? 1 : 0, $geometrysOnly ]; $stmt = $pdo->prepare($sql); $result = $stmt->execute($params); if (!$result) { http_response_code(500); echo json_encode([ 'error' => 'Failed to create geofence record', 'details' => 'Database insert operation failed' ]); return; } // Get inserted ID $insertedId = $pdo->lastInsertId(); // Log successful creation error_log("Geofences CREATE - Record successfully created: ID $insertedId by user: $user from IP: " . SessionHelper::get_ip()); // Performance monitoring $endTime = microtime(true); $endMemory = memory_get_usage(true); $processingTime = round($endTime - $startTime, 2); $memoryUsed = round(($endMemory - $startMemory) / 1024 / 1024, 2); // Return success response http_response_code(201); echo json_encode([ 'success' => true, 'message' => 'Geofence record created successfully', 'data' => [ 'id' => (int)$insertedId, 'Nama' => $nama, 'Catatan' => $catatan, 'Types' => $types, 'Coordinates' => $coordinates, 'Coordinates_parsed' => $coordinatesArray, 'Active' => $active, 'Geometrys_only' => $geometrysOnly ], 'processing_info' => [ 'processing_time' => $processingTime . " seconds", 'memory_usage' => $memoryUsed . " MB", 'user' => $user, 'ip_address' => SessionHelper::get_ip() ] ]); } /** * Handle UPDATE operations (PUT) * ============================== * Update existing geofence record */ function handleUpdate($pdo, $user) { global $startTime, $startMemory; // Get request body $input = json_decode(file_get_contents('php://input'), true); if (!$input) { http_response_code(400); echo json_encode([ 'error' => 'Invalid JSON input', 'details' => 'Request body must be valid JSON' ]); return; } // Validasi required field untuk identify record if (!isset($input['id']) || (int)$input['id'] <= 0) { http_response_code(400); echo json_encode([ 'error' => 'Missing required field', 'details' => 'id is required to identify the record to update' ]); return; } $id = (int)$input['id']; // Check if record exists $checkSql = "SELECT * FROM geofences WHERE id = ?"; $checkStmt = $pdo->prepare($checkSql); $checkStmt->execute([$id]); $existingRecord = $checkStmt->fetch(PDO::FETCH_ASSOC); if (!$existingRecord) { http_response_code(404); echo json_encode([ 'error' => 'Geofence record not found', 'details' => 'No geofence record found with the specified ID', 'id' => $id ]); return; } // Build update fields $updateFields = array(); $updateParams = array(); $allowedFields = ['Nama', 'Catatan', 'Types', 'Coordinates', 'Active', 'Geometrys_only']; foreach ($allowedFields as $field) { if (isset($input[$field])) { $value = $input[$field]; // Validate specific fields if ($field === 'Nama' && !empty($value)) { $value = trim($value); if (strlen($value) > 100) { http_response_code(400); echo json_encode([ 'error' => 'Nama too long', 'details' => 'Nama must be maximum 100 characters' ]); return; } // Check for duplicate nama (exclude current record) $checkDupSql = "SELECT COUNT(*) as count FROM geofences WHERE Nama = ? AND id != ?"; $checkDupStmt = $pdo->prepare($checkDupSql); $checkDupStmt->execute([$value, $id]); $dupCount = $checkDupStmt->fetch(PDO::FETCH_ASSOC)['count']; if ($dupCount > 0) { http_response_code(409); echo json_encode([ 'error' => 'Geofence name already exists', 'details' => 'Another geofence with this name already exists' ]); return; } } if ($field === 'Catatan' && !empty($value)) { $value = trim($value); if (strlen($value) > 500) { http_response_code(400); echo json_encode([ 'error' => 'Catatan too long', 'details' => 'Catatan must be maximum 500 characters' ]); return; } } if ($field === 'Types' && !empty($value)) { $value = trim($value); if (strlen($value) > 20) { http_response_code(400); echo json_encode([ 'error' => 'Types too long', 'details' => 'Types must be maximum 20 characters' ]); return; } } if ($field === 'Coordinates' && !empty($value)) { $value = trim($value); // Validate JSON format $coordinatesArray = json_decode($value, true); if (json_last_error() !== JSON_ERROR_NONE) { http_response_code(400); echo json_encode([ 'error' => 'Invalid coordinates format', 'details' => 'Coordinates must be valid JSON' ]); return; } } if ($field === 'Active') { $value = $value ? 1 : 0; } if ($field === 'Geometrys_only' && !empty($value)) { $value = trim($value); } $updateFields[] = "$field = ?"; $updateParams[] = $value; } } // Check if ada fields untuk update if (empty($updateFields)) { http_response_code(400); echo json_encode([ 'error' => 'No fields to update', 'details' => 'At least one field must be provided for update', 'available_fields' => $allowedFields ]); return; } // Add audit fields $updateFields[] = "Tgl_Edit = GETDATE()"; $updateFields[] = "Edit_By = ?"; $updateParams[] = $user; // Build dan execute update query $sql = "UPDATE geofences SET " . implode(', ', $updateFields) . " WHERE id = ?"; $updateParams[] = $id; $stmt = $pdo->prepare($sql); $result = $stmt->execute($updateParams); if (!$result) { http_response_code(500); echo json_encode([ 'error' => 'Failed to update geofence record', 'details' => 'Database update operation failed' ]); return; } // Get updated record $getUpdatedSql = "SELECT * FROM geofences WHERE id = ?"; $getUpdatedStmt = $pdo->prepare($getUpdatedSql); $getUpdatedStmt->execute([$id]); $updatedRecord = $getUpdatedStmt->fetch(PDO::FETCH_ASSOC); // Parse coordinates in updated record if (!empty($updatedRecord['Coordinates'])) { $coordinates = json_decode($updatedRecord['Coordinates'], true); if (json_last_error() === JSON_ERROR_NONE) { $updatedRecord['Coordinates_parsed'] = $coordinates; } } // Convert Active bit to boolean $updatedRecord['Active'] = (bool)$updatedRecord['Active']; // Log successful update error_log("Geofences UPDATE - Record successfully updated: ID $id by user: $user from IP: " . SessionHelper::get_ip()); // Performance monitoring $endTime = microtime(true); $endMemory = memory_get_usage(true); $processingTime = round($endTime - $startTime, 2); $memoryUsed = round(($endMemory - $startMemory) / 1024 / 1024, 2); // Return success response http_response_code(200); echo json_encode([ 'success' => true, 'message' => 'Geofence record updated successfully', 'data' => $updatedRecord, 'changes' => [ 'fields_updated' => count($updateFields) - 2, // Exclude audit fields 'rows_affected' => $stmt->rowCount() ], 'processing_info' => [ 'processing_time' => $processingTime . " seconds", 'memory_usage' => $memoryUsed . " MB", 'user' => $user, 'ip_address' => SessionHelper::get_ip() ] ]); } /** * Handle DELETE operations (DELETE) * ================================= * Delete geofence record by ID */ function handleDelete($pdo, $user) { global $startTime, $startMemory; // Get request body $input = json_decode(file_get_contents('php://input'), true); if (!$input) { http_response_code(400); echo json_encode([ 'error' => 'Invalid JSON input', 'details' => 'Request body must be valid JSON' ]); return; } // Validasi required field if (!isset($input['id']) || (int)$input['id'] <= 0) { http_response_code(400); echo json_encode([ 'error' => 'Missing required field', 'details' => 'id is required to identify the record to delete' ]); return; } $id = (int)$input['id']; // Check if record exists dan get data sebelum delete $checkSql = "SELECT * FROM geofences WHERE id = ?"; $checkStmt = $pdo->prepare($checkSql); $checkStmt->execute([$id]); $existingRecord = $checkStmt->fetch(PDO::FETCH_ASSOC); if (!$existingRecord) { http_response_code(404); echo json_encode([ 'error' => 'Geofence record not found', 'details' => 'No geofence record found with the specified ID', 'id' => $id ]); return; } // Parse coordinates in existing record for response if (!empty($existingRecord['Coordinates'])) { $coordinates = json_decode($existingRecord['Coordinates'], true); if (json_last_error() === JSON_ERROR_NONE) { $existingRecord['Coordinates_parsed'] = $coordinates; } } // Convert Active bit to boolean $existingRecord['Active'] = (bool)$existingRecord['Active']; // Execute delete $sql = "DELETE FROM geofences WHERE id = ?"; $stmt = $pdo->prepare($sql); $result = $stmt->execute([$id]); if (!$result) { http_response_code(500); echo json_encode([ 'error' => 'Failed to delete geofence record', 'details' => 'Database delete operation failed' ]); return; } // Verify delete berhasil $rowsAffected = $stmt->rowCount(); if ($rowsAffected === 0) { http_response_code(404); echo json_encode([ 'error' => 'Geofence record deletion failed', 'details' => 'No records were deleted' ]); return; } // Log successful deletion error_log("Geofences DELETE - Record successfully deleted: ID $id (Name: {$existingRecord['Nama']}) by user: $user from IP: " . SessionHelper::get_ip()); // Performance monitoring $endTime = microtime(true); $endMemory = memory_get_usage(true); $processingTime = round($endTime - $startTime, 2); $memoryUsed = round(($endMemory - $startMemory) / 1024 / 1024, 2); // Return success response http_response_code(200); echo json_encode([ 'success' => true, 'message' => 'Geofence record deleted successfully', 'deleted_data' => $existingRecord, 'changes' => [ 'rows_affected' => $rowsAffected ], 'processing_info' => [ 'processing_time' => $processingTime . " seconds", 'memory_usage' => $memoryUsed . " MB", 'user' => $user, 'ip_address' => SessionHelper::get_ip() ] ]); }