db = Database::getInstance(); $this->ensureDirectivesTable(); } catch (Exception $e) { error_log("Database connection error: " . $e->getMessage()); throw $e; } } public static function getInstance() { if (self::$instance === null) { self::$instance = new self(); } return self::$instance; } /** * Ensure the directives table exists */ private function ensureDirectivesTable() { // Check if the panel_directives table exists $result = $this->db->query("SHOW TABLES LIKE 'panel_directives'"); if ($result->num_rows === 0) { // Create the table if it doesn't exist $sql = "CREATE TABLE IF NOT EXISTS panel_directives ( id INT AUTO_INCREMENT PRIMARY KEY, attribute1_id INT NOT NULL, attribute2_id INT NOT NULL, choice1 VARCHAR(255) NOT NULL, choice2 VARCHAR(255) NOT NULL, attribute1_name VARCHAR(255) NOT NULL, attribute2_name VARCHAR(255) NOT NULL, status ENUM('pending', 'approved') NOT NULL DEFAULT 'pending', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP )"; $this->db->query($sql); // Add indexes $this->db->query("ALTER TABLE panel_directives ADD INDEX (attribute1_id)"); $this->db->query("ALTER TABLE panel_directives ADD INDEX (attribute2_id)"); $this->db->query("ALTER TABLE panel_directives ADD INDEX (status)"); // Add foreign key constraints $this->db->query("ALTER TABLE panel_directives ADD CONSTRAINT panel_directives_attribute1_fk FOREIGN KEY (attribute1_id) REFERENCES attributes(id) ON DELETE CASCADE"); $this->db->query("ALTER TABLE panel_directives ADD CONSTRAINT panel_directives_attribute2_fk FOREIGN KEY (attribute2_id) REFERENCES attributes(id) ON DELETE CASCADE"); // Add unique index $this->db->query("ALTER TABLE panel_directives ADD UNIQUE INDEX unique_directive (attribute1_id, attribute2_id, choice1(50), choice2(50))"); } } /** * Add a directive to exclude specific attribute combinations * * @param string $data JSON data with attribute1_id, attribute2_id, choice1, choice2 * @return array Response with success status and message */ public function addDirective($data) { try { // Parse the data $directiveData = json_decode($data, true); if (!$directiveData) { throw new Exception("Invalid directive data format"); } // Validate required fields if (!isset($directiveData['attribute1_id']) || !isset($directiveData['attribute2_id']) || !isset($directiveData['choice1']) || !isset($directiveData['choice2'])) { throw new Exception("Missing required directive fields"); } // Get attribute names for better readability $attribute1Id = (int)$directiveData['attribute1_id']; $attribute2Id = (int)$directiveData['attribute2_id']; $choice1 = $this->db->escape($directiveData['choice1']); $choice2 = $this->db->escape($directiveData['choice2']); // Get attribute names $stmt = $this->db->prepare("SELECT id, name FROM attributes WHERE id IN (?, ?)"); $stmt->bind_param('ii', $attribute1Id, $attribute2Id); $stmt->execute(); $result = $stmt->get_result(); $attributeNames = []; while ($row = $result->fetch_assoc()) { $attributeNames[$row['id']] = $row['name']; } if (count($attributeNames) !== 2) { throw new Exception("One or both attributes not found"); } // Start transaction $this->db->query("START TRANSACTION"); // Check if this directive already exists $stmt = $this->db->prepare(" SELECT id FROM panel_directives WHERE attribute1_id = ? AND attribute2_id = ? AND choice1 = ? AND choice2 = ? "); $stmt->bind_param('iiss', $attribute1Id, $attribute2Id, $choice1, $choice2); $stmt->execute(); $existingResult = $stmt->get_result(); if ($existingResult->num_rows > 0) { throw new Exception("This directive already exists"); } // Insert the new directive $stmt = $this->db->prepare(" INSERT INTO panel_directives ( attribute1_id, attribute2_id, choice1, choice2, attribute1_name, attribute2_name, status, created_at ) VALUES (?, ?, ?, ?, ?, ?, 'pending', NOW()) "); $attr1Name = $attributeNames[$attribute1Id]; $attr2Name = $attributeNames[$attribute2Id]; $stmt->bind_param('iissss', $attribute1Id, $attribute2Id, $choice1, $choice2, $attr1Name, $attr2Name ); if (!$stmt->execute()) { throw new Exception("Failed to insert directive: " . $stmt->error); } $directiveId = $this->db->getLastInsertId(); $this->db->query("COMMIT"); return [ 'success' => true, 'directive_id' => $directiveId, 'message' => 'Directive added successfully' ]; } catch (Exception $e) { if ($this->db->inTransaction()) { $this->db->query("ROLLBACK"); } error_log("Error adding directive: " . $e->getMessage()); return [ 'success' => false, 'message' => $e->getMessage() ]; } } /** * Get all directives * * @return array Response with success status and directives data */ public function getDirectives() { try { $sql = "SELECT id, attribute1_id, attribute2_id, choice1, choice2, attribute1_name, attribute2_name, status FROM panel_directives ORDER BY created_at DESC"; $result = $this->db->query($sql); if ($result === false) { throw new Exception("Failed to fetch directives: " . $this->db->getLastError()); } $directives = []; while ($row = $result->fetch_assoc()) { $directives[] = $row; } return [ 'success' => true, 'directives' => $directives ]; } catch (Exception $e) { error_log("Error in getDirectives: " . $e->getMessage()); return [ 'success' => false, 'message' => $e->getMessage() ]; } } /** * Approve a directive * * @param int $directiveId The directive ID to approve * @return array Response with success status and message */ public function approveDirective($directiveId) { try { $stmt = $this->db->prepare(" UPDATE panel_directives SET status = 'approved' WHERE id = ? "); $stmt->bind_param('i', $directiveId); if (!$stmt->execute()) { throw new Exception("Failed to approve directive: " . $stmt->error); } if ($stmt->affected_rows === 0) { throw new Exception("Directive not found"); } return [ 'success' => true, 'message' => 'Directive approved successfully' ]; } catch (Exception $e) { error_log("Error approving directive: " . $e->getMessage()); return [ 'success' => false, 'message' => $e->getMessage() ]; } } /** * Revoke an approved directive (set back to pending) * * @param int $directiveId The directive ID to revoke * @return array Response with success status and message */ public function revokeDirective($directiveId) { try { $stmt = $this->db->prepare(" UPDATE panel_directives SET status = 'pending' WHERE id = ? "); $stmt->bind_param('i', $directiveId); if (!$stmt->execute()) { throw new Exception("Failed to revoke directive: " . $stmt->error); } if ($stmt->affected_rows === 0) { throw new Exception("Directive not found"); } return [ 'success' => true, 'message' => 'Directive revoked successfully' ]; } catch (Exception $e) { error_log("Error revoking directive: " . $e->getMessage()); return [ 'success' => false, 'message' => $e->getMessage() ]; } } /** * Delete a directive * * @param int $directiveId The directive ID to delete * @return array Response with success status and message */ public function deleteDirective($directiveId) { try { $stmt = $this->db->prepare("DELETE FROM panel_directives WHERE id = ?"); $stmt->bind_param('i', $directiveId); if (!$stmt->execute()) { throw new Exception("Failed to delete directive: " . $stmt->error); } if ($stmt->affected_rows === 0) { throw new Exception("Directive not found"); } return [ 'success' => true, 'message' => 'Directive deleted successfully' ]; } catch (Exception $e) { error_log("Error deleting directive: " . $e->getMessage()); return [ 'success' => false, 'message' => $e->getMessage() ]; } } /** * Find panel members that match a specific directive * * @param int $directiveId The directive ID to check * @return array Response with success status and matching panel members */ public function findMatchingPanelMembers($directiveId) { try { // Get directive details $stmt = $this->db->prepare(" SELECT attribute1_id, attribute2_id, choice1, choice2 FROM panel_directives WHERE id = ? "); $stmt->bind_param('i', $directiveId); $stmt->execute(); $directive = $stmt->get_result()->fetch_assoc(); if (!$directive) { throw new Exception("Directive not found"); } // Find panel members that match this directive $matchingMembers = []; // For now, we'll just return success // In future implementation, we'll add logic to find matching panel members return [ 'success' => true, 'directive_id' => $directiveId, 'matching_count' => count($matchingMembers), 'matching_members' => $matchingMembers ]; } catch (Exception $e) { error_log("Error finding matching panel members: " . $e->getMessage()); return [ 'success' => false, 'message' => $e->getMessage() ]; } } /** * Get the current state of the integrity check * * @return array|null The current state or null if no state found */ public function getCurrentState() { if ($this->currentState === null) { $result = $this->db->query(" SELECT * FROM panel_analysis_state WHERE id = (SELECT MAX(id) FROM panel_analysis_state) "); if (!$result) { error_log("Failed to fetch current state: " . $this->db->getLastError()); return null; } $this->currentState = $result->fetch_assoc(); } return $this->currentState; } /** * Update the state of the integrity check * * @param array $updates Key-value pairs to update * @return array Response with success status and message */ private function updateState($updates) { $setClauses = []; foreach ($updates as $field => $value) { $setClauses[] = "$field = " . (is_numeric($value) ? $value : "'" . $this->db->escape($value) . "'"); } $sql = "UPDATE panel_analysis_state SET " . implode(', ', $setClauses) . " WHERE id = (SELECT MAX(id) FROM panel_analysis_state)"; $success = $this->db->query($sql); if ($success) { $this->currentState = null; } return [ 'success' => $success, 'message' => $success ? 'State updated' : 'Failed to update state' ]; } /** * Clean up any resources or processes * * @return bool Success status */ public function cleanup() { if (!$this->forceCleanup) { error_log("Skipping automatic cleanup"); return true; } $this->db->query("START TRANSACTION"); try { error_log("Starting forced cleanup process"); $result = $this->db->query("SELECT * FROM panel_analysis_state WHERE is_running = 1"); if ($result && $result->num_rows > 0) { error_log("Found running processes: " . $result->num_rows); } $this->db->query("UPDATE panel_analysis_state SET is_running = 0, status = 'Check terminated' WHERE is_running = 1"); $this->db->query("DELETE FROM panel_processing_status WHERE status IN ('pending', 'processing')"); $lockFile = sys_get_temp_dir() . '/integrity_check.lock'; if (file_exists($lockFile)) { error_log("Removing lock file"); unlink($lockFile); } $this->db->query("COMMIT"); error_log("Cleanup completed successfully"); return true; } catch (Exception $e) { $this->db->query("ROLLBACK"); error_log("Cleanup error: " . $e->getMessage()); throw $e; } } /** * Force cleanup of resources and processes * * @return bool Success status */ public function forceCleanup() { $this->forceCleanup = true; $result = $this->cleanup(); $this->forceCleanup = false; return $result; } /** * Start the integrity check process */ public function startIntegrityCheck() { try { // Create a database table for tracking progress if it doesn't exist $this->createIntegrityCheckTable(); // Get approved directives $directives = $this->getApprovedDirectives(); if (empty($directives)) { throw new Exception("No approved directives found. Please approve directives first."); } // Count total panel members $countResult = $this->db->query("SELECT COUNT(*) as total FROM panel_data"); $totalCount = $countResult->fetch_assoc()['total']; if ($totalCount == 0) { throw new Exception("No panel members found to check."); } // Reset progress tracking $this->db->query("TRUNCATE TABLE panel_integrity_check_state"); // Initialize progress state $sql = "INSERT INTO panel_integrity_check_state (is_running, is_paused, processed_count, total_count, status, start_time) VALUES (1, 0, 0, ?, 'running', NOW())"; $stmt = $this->db->prepare($sql); $stmt->bind_param('i', $totalCount); if (!$stmt->execute()) { throw new Exception("Failed to initialize check state: " . $stmt->error); } // Create a table to store results if it doesn't exist $this->createResultsTable(); // Clear previous results $this->db->query("TRUNCATE TABLE panel_integrity_results"); return [ 'success' => true, 'message' => 'Integrity check started successfully' ]; } catch (Exception $e) { error_log("Start integrity check error: " . $e->getMessage()); return [ 'success' => false, 'message' => $e->getMessage() ]; } } /** * Create the integrity check state table if it doesn't exist */ private function createIntegrityCheckTable() { $this->db->query(" CREATE TABLE IF NOT EXISTS panel_integrity_check_state ( id INT AUTO_INCREMENT PRIMARY KEY, is_running TINYINT(1) NOT NULL DEFAULT 0, is_paused TINYINT(1) NOT NULL DEFAULT 0, processed_count INT NOT NULL DEFAULT 0, total_count INT NOT NULL DEFAULT 0, status VARCHAR(50) NOT NULL DEFAULT '', start_time TIMESTAMP NULL, last_processed_id INT DEFAULT NULL ) "); } /** * Create the results table if it doesn't exist */ private function createResultsTable() { $this->db->query(" CREATE TABLE IF NOT EXISTS panel_integrity_results ( id INT AUTO_INCREMENT PRIMARY KEY, panelist_id VARCHAR(10) NOT NULL, directive_id INT NOT NULL, attribute1_id INT NOT NULL, attribute2_id INT NOT NULL, choice1 VARCHAR(255) NOT NULL, choice2 VARCHAR(255) NOT NULL, actual_value1 VARCHAR(255) NOT NULL, actual_value2 VARCHAR(255) NOT NULL, attribute1_name VARCHAR(255) NOT NULL, attribute2_name VARCHAR(255) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX (panelist_id), INDEX (directive_id) ) "); } /** * Get approved directives */ private function getApprovedDirectives() { $result = $this->db->query(" SELECT * FROM panel_directives WHERE status = 'approved' "); $directives = []; while ($row = $result->fetch_assoc()) { $directives[] = $row; } return $directives; } /** * Pause the integrity check */ public function pauseCheck() { try { $this->db->query(" UPDATE panel_integrity_check_state SET is_paused = 1, status = 'paused' WHERE is_running = 1 "); return [ 'success' => true, 'message' => 'Check paused successfully' ]; } catch (Exception $e) { error_log("Pause check error: " . $e->getMessage()); return [ 'success' => false, 'message' => $e->getMessage() ]; } } /** * Resume the integrity check */ public function resumeCheck() { try { $this->db->query(" UPDATE panel_integrity_check_state SET is_paused = 0, status = 'running' WHERE is_running = 1 AND is_paused = 1 "); return [ 'success' => true, 'message' => 'Check resumed successfully' ]; } catch (Exception $e) { error_log("Resume check error: " . $e->getMessage()); return [ 'success' => false, 'message' => $e->getMessage() ]; } } /** * Stop the integrity check */ public function stopCheck() { try { $this->db->query(" UPDATE panel_integrity_check_state SET is_running = 0, is_paused = 0, status = 'stopped' WHERE is_running = 1 "); return [ 'success' => true, 'message' => 'Check stopped successfully' ]; } catch (Exception $e) { error_log("Stop check error: " . $e->getMessage()); return [ 'success' => false, 'message' => $e->getMessage() ]; } } /** * Get the current check status */ public function getCheckStatus() { try { $result = $this->db->query(" SELECT * FROM panel_integrity_check_state ORDER BY id DESC LIMIT 1 "); if ($result->num_rows == 0) { return [ 'success' => true, 'is_running' => false, 'is_paused' => false, 'progress' => 0 ]; } $state = $result->fetch_assoc(); $progress = ($state['total_count'] > 0) ? ($state['processed_count'] / $state['total_count'] * 100) : 0; return [ 'success' => true, 'is_running' => (bool)$state['is_running'], 'is_paused' => (bool)$state['is_paused'], 'progress' => $progress, 'processed' => $state['processed_count'], 'total' => $state['total_count'], 'status' => $state['status'] ]; } catch (Exception $e) { error_log("Get check status error: " . $e->getMessage()); return [ 'success' => false, 'message' => $e->getMessage() ]; } } /** * Get the check progress and process more panel members */ public function getCheckProgress() { try { // First, get current state $state = $this->getCheckStatus(); // If check is not running or is paused, just return the state if (!$state['success'] || !$state['is_running'] || $state['is_paused']) { return $state; } // Process a batch of panel members $this->processNextBatch(); // Get updated state $updatedState = $this->getCheckStatus(); // If check is complete, include results if ($updatedState['success'] && (!$updatedState['is_running'] || $updatedState['processed'] >= $updatedState['total'])) { $updatedState['status'] = 'completed'; $updatedState['results'] = $this->getCheckResults(); // Mark as completed $this->db->query(" UPDATE panel_integrity_check_state SET is_running = 0, status = 'completed' WHERE is_running = 1 "); } return $updatedState; } catch (Exception $e) { error_log("Get check progress error: " . $e->getMessage()); return [ 'success' => false, 'message' => $e->getMessage() ]; } } /** * Process the next batch of panel members */ private function processNextBatch($batchSize = 50) { // Get current state $result = $this->db->query(" SELECT * FROM panel_integrity_check_state WHERE is_running = 1 AND is_paused = 0 ORDER BY id DESC LIMIT 1 "); if ($result->num_rows == 0) { return; // No running check } $state = $result->fetch_assoc(); $lastProcessedId = $state['last_processed_id'] ?? 0; // Get approved directives $directives = $this->getApprovedDirectives(); if (empty($directives)) { return; // No directives to check } // Get a batch of unprocessed panel members $query = " SELECT * FROM panel_data WHERE id > ? ORDER BY id ASC LIMIT ? "; $stmt = $this->db->prepare($query); $stmt->bind_param('ii', $lastProcessedId, $batchSize); $stmt->execute(); $result = $stmt->get_result(); if ($result->num_rows == 0) { // No more panel members to process $this->db->query(" UPDATE panel_integrity_check_state SET is_running = 0, status = 'completed', processed_count = total_count WHERE id = {$state['id']} "); return; } $processedCount = 0; $newLastId = $lastProcessedId; // Check each panel member against directives while ($panelist = $result->fetch_assoc()) { $newLastId = $panelist['id']; $processedCount++; // Parse attribute values $attributeValues = json_decode($panelist['attribute_values'], true); if (!$attributeValues) continue; // Check against each directive foreach ($directives as $directive) { $attr1Id = $directive['attribute1_id']; $attr2Id = $directive['attribute2_id']; $choice1 = $directive['choice1']; $choice2 = $directive['choice2']; // Get actual values $actualValue1 = isset($attributeValues[$attr1Id]) ? $attributeValues[$attr1Id] : null; $actualValue2 = isset($attributeValues[$attr2Id]) ? $attributeValues[$attr2Id] : null; // Convert arrays to strings for comparison if (is_array($actualValue1)) $actualValue1 = implode(', ', $actualValue1); if (is_array($actualValue2)) $actualValue2 = implode(', ', $actualValue2); // Check if value matches exactly or contains the choice (for array values) $value1Matches = false; $value2Matches = false; // For the first attribute value if ($actualValue1 === $choice1) { $value1Matches = true; } else if (is_string($actualValue1) && strpos($actualValue1, $choice1) !== false) { // Check if it's part of a comma-separated list (from a multi-select) $parts = array_map('trim', explode(',', $actualValue1)); if (in_array($choice1, $parts)) { $value1Matches = true; } } // For the second attribute value if ($actualValue2 === $choice2) { $value2Matches = true; } else if (is_string($actualValue2) && strpos($actualValue2, $choice2) !== false) { // Check if it's part of a comma-separated list (from a multi-select) $parts = array_map('trim', explode(',', $actualValue2)); if (in_array($choice2, $parts)) { $value2Matches = true; } } // Check if panelist violates directive if ($value1Matches && $value2Matches) { // This panelist violates the directive - add to results $stmt = $this->db->prepare(" INSERT INTO panel_integrity_results (panelist_id, directive_id, attribute1_id, attribute2_id, choice1, choice2, actual_value1, actual_value2, attribute1_name, attribute2_name) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) "); $stmt->bind_param( 'siisssssss', $panelist['panelist_id'], $directive['id'], $attr1Id, $attr2Id, $choice1, $choice2, $actualValue1, $actualValue2, $directive['attribute1_name'], $directive['attribute2_name'] ); $stmt->execute(); // Continue checking other directives - a panelist might violate multiple directives // (removed the break statement here) } } } // Update progress $this->db->query(" UPDATE panel_integrity_check_state SET processed_count = processed_count + {$processedCount}, last_processed_id = {$newLastId} WHERE id = {$state['id']} "); } /** * Get the results of the integrity check */ private function getCheckResults() { $result = $this->db->query(" SELECT * FROM panel_integrity_results ORDER BY id ASC "); $results = []; while ($row = $result->fetch_assoc()) { $results[] = $row; } return $results; } /** * Delete violating panel members */ public function deleteViolatingMembers($panelists) { try { if (empty($panelists)) { throw new Exception("No panelists specified for deletion"); } $placeholders = implode(',', array_fill(0, count($panelists), '?')); $types = str_repeat('s', count($panelists)); $stmt = $this->db->prepare(" DELETE FROM panel_data WHERE panelist_id IN ({$placeholders}) "); $stmt->bind_param($types, ...$panelists); $stmt->execute(); $deletedCount = $stmt->affected_rows; return [ 'success' => true, 'deleted_count' => $deletedCount, 'message' => "{$deletedCount} panel members deleted successfully" ]; } catch (Exception $e) { error_log("Delete violating members error: " . $e->getMessage()); return [ 'success' => false, 'message' => $e->getMessage() ]; } } } ?>