db = Database::getInstance(); $this->auth = new Auth(); $this->loadData(); } private function loadData() { // Get existing panel count $result = $this->db->query("SELECT COUNT(*) as count FROM panel_data"); $this->existingCount = $result->fetch_assoc()['count']; // Load statistics $this->statistics = []; $stats_query = $this->db->query(" SELECT s.*, GROUP_CONCAT(DISTINCT a.id) as attribute_ids, GROUP_CONCAT(DISTINCT a.name) as attribute_names FROM statistics s JOIN statistic_attributes sa ON s.id = sa.statistic_id JOIN attributes a ON a.id = sa.attribute_id GROUP BY s.id "); while ($stat = $stats_query->fetch_assoc()) { $this->statistics[] = $stat; } // Load attributes $this->attributes = []; $this->attributesById = []; $attr_query = $this->db->query("SELECT * FROM attributes ORDER BY created_at ASC"); while ($attr = $attr_query->fetch_assoc()) { $this->attributes[] = $attr; $this->attributesById[$attr['id']] = $attr; } } // Method to load approved directives private function loadApprovedDirectives() { $directives = []; $query = $this->db->query(" SELECT pd.id, pd.attribute1_id, pd.attribute2_id, pd.choice1, pd.choice2 FROM panel_directives pd WHERE pd.status = 'approved' "); if ($query && $query->num_rows > 0) { while ($directive = $query->fetch_assoc()) { $directives[] = $directive; } } return $directives; } public function deletePanelData() { try { // Check if user is admin if (!$this->auth->isAdmin()) { throw new Exception("Unauthorized: Only administrators can delete panel data"); } // Begin transaction $this->db->query("START TRANSACTION"); // Delete all panel data $sql = "TRUNCATE TABLE panel_data"; if (!$this->db->query($sql)) { throw new Exception("Failed to delete panel data"); } // Log the action (optional) $userId = $_SESSION['user_id']; $timestamp = date('Y-m-d H:i:s'); error_log("Panel data deleted by user ID: $userId at $timestamp"); $this->db->query("COMMIT"); return [ 'success' => true, 'message' => 'Panel data deleted successfully' ]; } catch (Exception $e) { $this->db->query("ROLLBACK"); error_log("Delete panel error: " . $e->getMessage()); return [ 'success' => false, 'message' => $e->getMessage() ]; } } public function calculateAlignmentScore() { try { // If panel is empty, return 100% deviation (0% alignment) $countCheck = $this->db->query("SELECT COUNT(*) as count FROM panel_data"); if ($countCheck->fetch_assoc()['count'] == 0) { return [ 'success' => true, 'score' => 100.00, // Maximum deviation 'message' => 'Panel is empty' ]; } // First, add actual_percentage column to statistic_combinations if it doesn't exist $this->db->query(" SHOW COLUMNS FROM statistic_combinations LIKE 'actual_percentage' "); if ($this->db->getConnection()->affected_rows == 0) { $this->db->query(" ALTER TABLE statistic_combinations ADD COLUMN actual_percentage DECIMAL(10,4) NULL DEFAULT NULL "); } // Make sure statistics are updated $this->updateStatisticChecks(); // Calculate average deviation from all combinations $query = $this->db->query(" SELECT sc.percentage as target_percentage, sc.actual_percentage FROM statistic_combinations sc WHERE sc.actual_percentage IS NOT NULL "); $totalDeviation = 0; $combinationCount = 0; while ($row = $query->fetch_assoc()) { $targetPct = floatval($row['target_percentage']); $actualPct = floatval($row['actual_percentage']); // Calculate deviation as percentage difference $deviation = abs($actualPct - $targetPct); $totalDeviation += $deviation; $combinationCount++; } // Calculate average deviation $averageDeviation = $combinationCount > 0 ? ($totalDeviation / $combinationCount) : 100; // Ensure deviation is capped at 100% $averageDeviation = min(100, $averageDeviation); return [ 'success' => true, 'score' => round($averageDeviation, 2), 'message' => 'Score calculated successfully' ]; } catch (Exception $e) { error_log("Alignment score calculation error: " . $e->getMessage()); return [ 'success' => false, 'message' => $e->getMessage() ]; } } public function updateStatisticChecks() { try { // First, check if actual_percentage column exists $columnCheck = $this->db->query("SHOW COLUMNS FROM statistic_combinations LIKE 'actual_percentage'"); if ($columnCheck->num_rows === 0) { // Add the column if it doesn't exist $this->db->query("ALTER TABLE statistic_combinations ADD COLUMN actual_percentage DECIMAL(10,4) NULL DEFAULT NULL"); } // Get total panel count $countResult = $this->db->query("SELECT COUNT(*) AS total FROM panel_data"); $totalPanelCount = $countResult->fetch_assoc()['total']; if ($totalPanelCount == 0) { return ['success' => false, 'message' => 'No panel data found']; } // Get all panel data first - this is more efficient than querying for each combination $panelData = []; $panelQuery = $this->db->query("SELECT panelist_id, attribute_values FROM panel_data"); while ($row = $panelQuery->fetch_assoc()) { $panelData[] = [ 'id' => $row['panelist_id'], 'values' => json_decode($row['attribute_values'], true) ]; } // Get all statistics with their attribute information $stats = []; $statsQuery = $this->db->query(" SELECT s.id, s.name, s.type FROM statistics s "); while ($stat = $statsQuery->fetch_assoc()) { $stats[$stat['id']] = [ 'id' => $stat['id'], 'name' => $stat['name'], 'type' => $stat['type'], 'attributes' => [] ]; // Get attributes for this statistic $attrQuery = $this->db->query(" SELECT sa.attribute_id, a.name AS attribute_name, a.choice_type FROM statistic_attributes sa JOIN attributes a ON sa.attribute_id = a.id WHERE sa.statistic_id = {$stat['id']} ORDER BY sa.id "); while ($attr = $attrQuery->fetch_assoc()) { $stats[$stat['id']]['attributes'][] = [ 'id' => $attr['attribute_id'], 'name' => $attr['attribute_name'], 'choice_type' => $attr['choice_type'] ]; } } // Get all statistic combinations $combosQuery = $this->db->query(" SELECT sc.id, sc.statistic_id, sc.combination_values, sc.percentage AS target_percentage FROM statistic_combinations sc ORDER BY sc.statistic_id, sc.id "); $updated = 0; $errors = 0; // Process each combination while ($combo = $combosQuery->fetch_assoc()) { $comboId = $combo['id']; $statId = $combo['statistic_id']; $comboValues = json_decode($combo['combination_values'], true); // Skip if no statistic info or attributes if (!isset($stats[$statId]) || empty($stats[$statId]['attributes'])) { continue; } $stat = $stats[$statId]; $attributes = $stat['attributes']; // Count matches for this combination $matchCount = 0; // Check each panel member foreach ($panelData as $panel) { $matches = true; // For combined statistics, check all attributes in the combination for ($i = 0; $i < count($attributes); $i++) { $attrId = $attributes[$i]['id']; $isMultiple = ($attributes[$i]['choice_type'] === 'multiple'); $targetValue = $comboValues[$i] ?? null; $panelValue = $panel['values'][$attrId] ?? null; if ($targetValue === null) { $matches = false; break; } if ($isMultiple) { // For multiple choice, check if value is in array if (!is_array($panelValue) || !in_array($targetValue, $panelValue)) { $matches = false; break; } } else { // For single choice, check exact match if ($panelValue !== $targetValue) { $matches = false; break; } } } if ($matches) { $matchCount++; } } // Calculate percentage $percentage = ($matchCount / $totalPanelCount) * 100; // Update the database $updateSql = "UPDATE statistic_combinations SET actual_percentage = {$percentage} WHERE id = {$comboId}"; if ($this->db->query($updateSql)) { $updated++; } else { error_log("Error updating combination #{$comboId}: " . $this->db->getLastError()); $errors++; } } return [ 'success' => true, 'message' => 'Statistics updated successfully', 'updated' => $updated, 'errors' => $errors ]; } catch (Exception $e) { error_log("Update statistic checks error: " . $e->getMessage()); return [ 'success' => false, 'message' => $e->getMessage() ]; } } public function calculateOptimalCount() { try { $denominators = []; foreach ($this->statistics as $stat) { $combos = $this->db->query(" SELECT percentage FROM statistic_combinations WHERE statistic_id = {$stat['id']} "); while ($combo = $combos->fetch_assoc()) { $percentage = floatval($combo['percentage']); if ($percentage > 0 && $percentage < 100) { $decimal = $percentage - floor($percentage); if ($decimal > 0) { $denominators[] = 1 / $decimal; } } } } if (empty($denominators)) { return ['success' => false, 'message' => 'No valid statistics found']; } $optimal = $this->calculateLCM($denominators); if ($this->existingCount > 0) { $nextMultiple = ceil($this->existingCount / $optimal) * $optimal; $optimal = $nextMultiple - $this->existingCount; } $additional_attributes = []; if ($this->existingCount > 0) { $attr_query = $this->db->query(" SELECT * FROM attributes WHERE created_at > ( SELECT MAX(created_at) FROM panel_data ) "); while ($attr = $attr_query->fetch_assoc()) { $additional_attributes[] = $attr; } } return [ 'success' => true, 'optimal_count' => $optimal, 'additional_attributes' => $additional_attributes ]; } catch (Exception $e) { error_log("Optimal count calculation error: " . $e->getMessage()); return ['success' => false, 'message' => $e->getMessage()]; } } private function calculateLCM($numbers) { $lcm = 1; foreach ($numbers as $number) { $lcm = $this->lcm($lcm, ceil($number)); } return $lcm; } private function lcm($a, $b) { return ($a * $b) / $this->gcd($a, $b); } private function gcd($a, $b) { while ($b != 0) { $t = $b; $b = $a % $b; $a = $t; } return $a; } // Modified generatePanelData to include directive checks and existing panel statistics public function generatePanelData($count) { try { $_SESSION['panel_generation_progress'] = 0; $_SESSION['panel_generation_status'] = 'Initializing...'; $result = $this->db->query("SELECT MAX(CAST(panelist_id AS UNSIGNED)) as max_id FROM panel_data"); $maxId = $result->fetch_assoc()['max_id'] ?? 0; $nextId = $maxId + 1; // Get statistics information for generation $statisticData = $this->getStatisticData(); // Load approved directives $approvedDirectives = $this->loadApprovedDirectives(); // Get existing panel statistics $existingPanelStats = $this->getExistingPanelStats($statisticData); // Analyze current panel's deviations from targets $requiredAdjustments = $this->calculateRequiredAdjustments($statisticData, $existingPanelStats, $count); $batchSize = min(100, $count); $totalBatches = ceil($count / $batchSize); $processedCount = 0; for ($batch = 0; $batch < $totalBatches; $batch++) { $currentBatchSize = min($batchSize, $count - $processedCount); $this->generateBatch( $currentBatchSize, $nextId + $processedCount, $statisticData, $approvedDirectives, $requiredAdjustments ); $processedCount += $currentBatchSize; $progress = ($processedCount / $count) * 100; $_SESSION['panel_generation_progress'] = $progress; $_SESSION['panel_generation_status'] = "Generating panel data... " . round($progress) . "%"; } $_SESSION['panel_generation_progress'] = 100; $_SESSION['panel_generation_status'] = 'Complete!'; // Update statistics checks to compute Panel % $this->updateStatisticChecks(); $alignmentScore = $this->calculateAlignmentScore(); return [ 'success' => true, 'alignment_score' => $alignmentScore['score'] ?? null ]; } catch (Exception $e) { error_log("Panel generation error: " . $e->getMessage()); return ['success' => false, 'message' => $e->getMessage()]; } } // Add methods to analyze existing panel statistics private function getExistingPanelStats($statisticData) { $existingStats = []; $totalPanelists = 0; // Count total existing panelists $countResult = $this->db->query("SELECT COUNT(*) as total FROM panel_data"); if ($countResult && $countResult->num_rows > 0) { $totalPanelists = $countResult->fetch_assoc()['total']; } if ($totalPanelists == 0) { return []; // No existing panel data } // For each statistic and combination, calculate current percentages foreach ($statisticData as $statId => $stat) { $existingStats[$statId] = [ 'combinations' => [] ]; // Get attribute IDs for this statistic $attributeIds = array_keys($stat['attributes']); // For each combination, calculate actual count and percentage foreach ($stat['combinations'] as $comboIndex => $combo) { $values = $combo['values']; $count = 0; // Build a query to count matching panel members $whereConditions = []; for ($i = 0; $i < count($attributeIds); $i++) { $attrId = $attributeIds[$i]; $value = $values[$i]; $attribute = $this->attributesById[$attrId]; if ($attribute['choice_type'] === 'multiple') { // For multiple choice, use JSON_CONTAINS to check if the array contains the value $whereConditions[] = "JSON_CONTAINS(attribute_values, '\"$value\"', '$.$attrId')"; } else { // For single choice, directly compare the value $whereConditions[] = "JSON_UNQUOTE(JSON_EXTRACT(attribute_values, '$.$attrId')) = '$value'"; } } if (!empty($whereConditions)) { $whereClause = implode(' AND ', $whereConditions); $query = "SELECT COUNT(*) as count FROM panel_data WHERE $whereClause"; $countResult = $this->db->query($query); if ($countResult && $countResult->num_rows > 0) { $count = $countResult->fetch_assoc()['count']; } } // Calculate actual percentage $actualPercentage = $totalPanelists > 0 ? ($count / $totalPanelists) * 100 : 0; $existingStats[$statId]['combinations'][$comboIndex] = [ 'values' => $values, 'count' => $count, 'percentage' => $actualPercentage, 'target_percentage' => $combo['percentage'] ]; } } return [ 'total_panelists' => $totalPanelists, 'statistics' => $existingStats ]; } private function calculateRequiredAdjustments($statisticData, $existingPanelStats, $newCount) { $adjustments = []; // If no existing panel, no adjustments needed if (empty($existingPanelStats)) { return $adjustments; } $existingCount = $existingPanelStats['total_panelists']; $totalCount = $existingCount + $newCount; // Calculate required counts for each combination in the final panel foreach ($statisticData as $statId => $stat) { $adjustments[$statId] = [ 'combinations' => [] ]; foreach ($stat['combinations'] as $comboIndex => $combo) { // Target percentage from statistics $targetPercentage = $combo['percentage']; // Target count in the final panel $targetCount = round(($targetPercentage / 100) * $totalCount); // Current count in the existing panel $currentCount = 0; if (isset($existingPanelStats['statistics'][$statId]['combinations'][$comboIndex])) { $currentCount = $existingPanelStats['statistics'][$statId]['combinations'][$comboIndex]['count']; } // Calculate how many more we need in the new batch $neededCount = max(0, $targetCount - $currentCount); // Calculate what percentage of the new batch should have this combination $newBatchPercentage = ($newCount > 0) ? ($neededCount / $newCount) * 100 : 0; $adjustments[$statId]['combinations'][$comboIndex] = [ 'values' => $combo['values'], 'target_percentage' => $targetPercentage, 'current_count' => $currentCount, 'target_count' => $targetCount, 'needed_count' => $neededCount, 'new_batch_percentage' => $newBatchPercentage ]; } } return $adjustments; } private function getStatisticData() { $statisticData = []; // Get all statistics with attribute info $query = $this->db->query(" SELECT s.id, s.name, s.type, s.sum_type, sa.attribute_id, a.choice_type, a.choices FROM statistics s JOIN statistic_attributes sa ON s.id = sa.statistic_id JOIN attributes a ON a.id = sa.attribute_id ORDER BY s.id, sa.id "); while ($row = $query->fetch_assoc()) { $statId = $row['id']; if (!isset($statisticData[$statId])) { $statisticData[$statId] = [ 'name' => $row['name'], 'type' => $row['type'], 'sum_type' => $row['sum_type'], 'attributes' => [], 'combinations' => [] ]; } $statisticData[$statId]['attributes'][$row['attribute_id']] = [ 'choice_type' => $row['choice_type'], 'choices' => json_decode($row['choices'], true) ]; } // Get combinations for each statistic foreach (array_keys($statisticData) as $statId) { $query = $this->db->query(" SELECT combination_values, percentage FROM statistic_combinations WHERE statistic_id = $statId "); while ($row = $query->fetch_assoc()) { $statisticData[$statId]['combinations'][] = [ 'values' => json_decode($row['combination_values'], true), 'percentage' => floatval($row['percentage']) ]; } } return $statisticData; } // Modified the generateBatch method to incorporate directive checks and adjustments private function generateBatch($size, $startId, $statisticData, $approvedDirectives, $requiredAdjustments = []) { $this->db->query("START TRANSACTION"); try { // First, generate the basic attribute values for each panelist $panelistData = []; for ($i = 0; $i < $size; $i++) { $panelistId = str_pad($startId + $i, 6, '0', STR_PAD_LEFT); $attributeValues = $this->generateInitialAttributeValues(); $panelistData[] = [ 'id' => $panelistId, 'values' => $attributeValues ]; } // Process statistics and adjust attribute values using the required adjustments $this->applyStatisticsToPanel($panelistData, $statisticData, $size, $approvedDirectives, $requiredAdjustments); // Final directive check before inserting $validPanelists = $this->validateAgainstDirectives($panelistData, $approvedDirectives); // Insert the panelist data into the database foreach ($validPanelists as $panelist) { $sql = "INSERT INTO panel_data (panelist_id, attribute_values, created_by) VALUES ( '" . $this->db->escape($panelist['id']) . "', '" . $this->db->escape(json_encode($panelist['values'])) . "', " . $_SESSION['user_id'] . " )"; if (!$this->db->query($sql)) { throw new Exception("Failed to insert panelist data: " . $this->db->getLastError()); } } $this->db->query("COMMIT"); return true; } catch (Exception $e) { $this->db->query("ROLLBACK"); error_log("Generate batch error: " . $e->getMessage()); throw $e; } } private function generateInitialAttributeValues() { $values = []; foreach ($this->attributes as $attr) { $choices = json_decode($attr['choices'], true); // For multiple choice attributes, initialize with an empty array if ($attr['choice_type'] === 'multiple') { $values[$attr['id']] = []; } else { // For single choice, select a random choice $values[$attr['id']] = $choices[array_rand($choices)]; } } return $values; } // Add a new method to validate panelists against directives private function validateAgainstDirectives($panelistData, $approvedDirectives) { if (empty($approvedDirectives)) { return $panelistData; // No directives to check against } $validPanelists = []; foreach ($panelistData as $panelist) { $isValid = true; // Check against each directive foreach ($approvedDirectives as $directive) { $attr1Id = $directive['attribute1_id']; $attr2Id = $directive['attribute2_id']; $choice1 = $directive['choice1']; $choice2 = $directive['choice2']; // Get the panelist's values for these attributes $panelAttr1Value = $panelist['values'][$attr1Id] ?? null; $panelAttr2Value = $panelist['values'][$attr2Id] ?? null; // Check if this combination is forbidden $matchesAttr1 = false; $matchesAttr2 = false; // Check attribute 1 if (is_array($panelAttr1Value)) { // For multiple choice attributes $matchesAttr1 = in_array($choice1, $panelAttr1Value); } else { // For single choice attributes $matchesAttr1 = ($panelAttr1Value === $choice1); } // Check attribute 2 if (is_array($panelAttr2Value)) { // For multiple choice attributes $matchesAttr2 = in_array($choice2, $panelAttr2Value); } else { // For single choice attributes $matchesAttr2 = ($panelAttr2Value === $choice2); } // If both attributes match, this panelist violates the directive if ($matchesAttr1 && $matchesAttr2) { $isValid = false; break; } } // If the panelist is valid (doesn't violate any directives), add to valid list if ($isValid) { $validPanelists[] = $panelist; } } return $validPanelists; } // Modify the applyStatisticsToPanel method to incorporate directive checks and adjustments private function applyStatisticsToPanel(&$panelistData, $statisticData, $size, $approvedDirectives, $requiredAdjustments = []) { // Process each statistic foreach ($statisticData as $statId => $stat) { $hasMultipleChoice = false; // Check if any attribute in this statistic is multiple choice foreach ($stat['attributes'] as $attrData) { if ($attrData['choice_type'] === 'multiple') { $hasMultipleChoice = true; break; } } // Process each combination for this statistic foreach ($stat['combinations'] as $comboIndex => $combination) { // Use adjusted target if available, otherwise use the original $targetPercentage = $combination['percentage']; if (!empty($requiredAdjustments) && isset($requiredAdjustments[$statId]['combinations'][$comboIndex])) { $targetPercentage = $requiredAdjustments[$statId]['combinations'][$comboIndex]['new_batch_percentage']; } $targetCount = round(($targetPercentage / 100) * $size); if ($targetCount <= 0) { continue; } // Get array of attribute IDs for this statistic $attributeIds = array_keys($stat['attributes']); // If dealing with multiple choice attributes if ($hasMultipleChoice) { $this->applyMultipleChoiceCombination( $panelistData, $combination, $attributeIds, $targetCount, $approvedDirectives ); } else { $this->applySingleChoiceCombination( $panelistData, $combination, $attributeIds, $targetCount, $approvedDirectives ); } } // For attributes that are multiple choice, check if any panelist has no choices // and assign a random choice if needed foreach ($stat['attributes'] as $attrId => $attrData) { if ($attrData['choice_type'] === 'multiple') { foreach ($panelistData as &$panelist) { if (empty($panelist['values'][$attrId])) { // Check if there's a NOTA option in the choices $notaIndex = array_search('NOTA', $attrData['choices']); if ($notaIndex !== false) { // Assign NOTA $panelist['values'][$attrId] = ['NOTA']; } else { // Randomly select one or more choices $numChoices = mt_rand(1, min(3, count($attrData['choices']))); $selectedIndices = array_rand($attrData['choices'], $numChoices); if (!is_array($selectedIndices)) { $selectedIndices = [$selectedIndices]; } $panelist['values'][$attrId] = array_map(function($idx) use ($attrData) { return $attrData['choices'][$idx]; }, $selectedIndices); } // Make sure this doesn't violate any directives if (!empty($approvedDirectives) && $this->violatesDirectives($panelist['values'], $approvedDirectives)) { // If it violates, try to assign NOTA instead if ($notaIndex !== false) { $panelist['values'][$attrId] = ['NOTA']; } else { // Or clear the attribute value $panelist['values'][$attrId] = []; } } } } } } } } // Modified applyMultipleChoiceCombination method to respect directives private function applyMultipleChoiceCombination(&$panelistData, $combination, $attributeIds, $targetCount, $approvedDirectives) { // Shuffle panelists to randomly assign combinations $indices = range(0, count($panelistData) - 1); shuffle($indices); $assignedCount = 0; $values = $combination['values']; // Loop through panelists until we've assigned enough foreach ($indices as $index) { if ($assignedCount >= $targetCount) { break; } $panelist = &$panelistData[$index]; $canAssign = true; // Check if this combination will conflict with existing values for ($i = 0; $i < count($attributeIds); $i++) { $attrId = $attributeIds[$i]; $value = $values[$i]; $attribute = $this->attributesById[$attrId]; // For multiple choice attributes if ($attribute['choice_type'] === 'multiple') { // Check for NOTA conflict if ($value === 'NOTA') { // If panelist already has values for this attribute, can't assign NOTA if (!empty($panelist['values'][$attrId])) { $canAssign = false; break; } } else { // Check if panelist already has NOTA for this attribute if (is_array($panelist['values'][$attrId]) && in_array('NOTA', $panelist['values'][$attrId])) { $canAssign = false; break; } } } // For single choice attributes, all values must match else if ($panelist['values'][$attrId] != $value) { $canAssign = false; break; } } // If we can assign this combination to the panelist, check directives first if ($canAssign) { // Create a temporary copy with the new values to check against directives $tempValues = $panelist['values']; // Apply the values to the temporary copy for ($i = 0; $i < count($attributeIds); $i++) { $attrId = $attributeIds[$i]; $value = $values[$i]; $attribute = $this->attributesById[$attrId]; if ($attribute['choice_type'] === 'multiple') { // For NOTA, clear all existing values and set only NOTA if ($value === 'NOTA') { $tempValues[$attrId] = ['NOTA']; } else { // For multiple choice, add to the array if not already present if (!is_array($tempValues[$attrId])) { $tempValues[$attrId] = []; } if (!in_array($value, $tempValues[$attrId])) { $tempValues[$attrId][] = $value; } } } else { // For single choice, directly set the value $tempValues[$attrId] = $value; } } // Check if this would violate any directive if (!empty($approvedDirectives) && $this->violatesDirectives($tempValues, $approvedDirectives)) { $canAssign = false; } else { // If no violation, apply the changes for real $panelist['values'] = $tempValues; $assignedCount++; } } } // If we couldn't assign enough, try creating more assignments (that respect directives) if ($assignedCount < $targetCount) { // This is an adaptation of the original code, but with directive checking // It might not be able to satisfy all requirements if directives are restrictive $remainingCount = $targetCount - $assignedCount; $attemptsLeft = count($panelistData) * 2; // Limit attempts to avoid infinite loops while ($remainingCount > 0 && $attemptsLeft > 0) { $randomIndex = array_rand($panelistData); $panelist = &$panelistData[$randomIndex]; // Create a temporary copy with the new values $tempValues = $panelist['values']; // Try to apply the combination for ($i = 0; $i < count($attributeIds); $i++) { $attrId = $attributeIds[$i]; $value = $values[$i]; $attribute = $this->attributesById[$attrId]; if ($attribute['choice_type'] === 'multiple') { if ($value === 'NOTA') { $tempValues[$attrId] = ['NOTA']; } else { if (!is_array($tempValues[$attrId])) { $tempValues[$attrId] = []; } // Remove NOTA if present if (is_array($tempValues[$attrId])) { $notaIndex = array_search('NOTA', $tempValues[$attrId]); if ($notaIndex !== false) { array_splice($tempValues[$attrId], $notaIndex, 1); } } if (!in_array($value, $tempValues[$attrId])) { $tempValues[$attrId][] = $value; } } } else { $tempValues[$attrId] = $value; } } // Only apply if it doesn't violate directives if (empty($approvedDirectives) || !$this->violatesDirectives($tempValues, $approvedDirectives)) { $panelist['values'] = $tempValues; $remainingCount--; } $attemptsLeft--; } } } // Modified applySingleChoiceCombination method to respect directives private function applySingleChoiceCombination(&$panelistData, $combination, $attributeIds, $targetCount, $approvedDirectives) { // Shuffle panelists to randomly assign combinations $indices = range(0, count($panelistData) - 1); shuffle($indices); $assignedCount = 0; $values = $combination['values']; // Loop through panelists until we've assigned enough foreach ($indices as $index) { if ($assignedCount >= $targetCount) { break; } // Create a temporary copy of the values $tempValues = $panelistData[$index]['values']; // Apply the values to this panelist for ($i = 0; $i < count($attributeIds); $i++) { $tempValues[$attributeIds[$i]] = $values[$i]; } // Check if this would violate any directive if (empty($approvedDirectives) || !$this->violatesDirectives($tempValues, $approvedDirectives)) { // If it doesn't violate, apply the changes $panelistData[$index]['values'] = $tempValues; $assignedCount++; } } // If we couldn't assign enough, try with more attempts but still respecting directives if ($assignedCount < $targetCount) { // This is similar to the original code but with directive checking $remainingCount = $targetCount - $assignedCount; $attemptsLeft = count($panelistData) * 2; // Limit attempts to avoid infinite loops while ($remainingCount > 0 && $attemptsLeft > 0) { $randomIndex = array_rand($panelistData); // Create a temporary copy $tempValues = $panelistData[$randomIndex]['values']; // Apply the values for ($i = 0; $i < count($attributeIds); $i++) { $tempValues[$attributeIds[$i]] = $values[$i]; } // Check directives if (empty($approvedDirectives) || !$this->violatesDirectives($tempValues, $approvedDirectives)) { $panelistData[$randomIndex]['values'] = $tempValues; $remainingCount--; } $attemptsLeft--; } } } // New method to check if attribute values violate any directive private function violatesDirectives($attributeValues, $approvedDirectives) { foreach ($approvedDirectives as $directive) { $attr1Id = $directive['attribute1_id']; $attr2Id = $directive['attribute2_id']; $choice1 = $directive['choice1']; $choice2 = $directive['choice2']; // Get the values for these attributes $attr1Value = $attributeValues[$attr1Id] ?? null; $attr2Value = $attributeValues[$attr2Id] ?? null; // Check if this combination is forbidden $matchesAttr1 = false; $matchesAttr2 = false; // Check attribute 1 if (is_array($attr1Value)) { // For multiple choice attributes $matchesAttr1 = in_array($choice1, $attr1Value); } else { // For single choice attributes $matchesAttr1 = ($attr1Value === $choice1); } // Check attribute 2 if (is_array($attr2Value)) { // For multiple choice attributes $matchesAttr2 = in_array($choice2, $attr2Value); } else { // For single choice attributes $matchesAttr2 = ($attr2Value === $choice2); } // If both attributes match, this violates the directive if ($matchesAttr1 && $matchesAttr2) { return true; } } return false; // No directive violations found } public function calculateRmseScore() { try { // If panel is empty, return 100% deviation (0% alignment) $countCheck = $this->db->query("SELECT COUNT(*) as count FROM panel_data"); if ($countCheck->fetch_assoc()['count'] == 0) { return [ 'success' => true, 'rmse' => 100.00, // Maximum deviation 'message' => 'Panel is empty' ]; } // Make sure statistics are updated $this->updateStatisticChecks(); // Calculate RMSE $query = $this->db->query(" SELECT SQRT(AVG(POW(percentage - actual_percentage, 2))) as rmse FROM statistic_combinations WHERE actual_percentage IS NOT NULL "); if (!$query) { error_log("RMSE error: Failed to execute RMSE query: " . $this->db->getLastError()); return [ 'success' => false, 'message' => 'Database error: ' . $this->db->getLastError() ]; } $row = $query->fetch_assoc(); if ($row && isset($row['rmse'])) { $rmse = is_null($row['rmse']) ? 100 : floatval($row['rmse']); return [ 'success' => true, 'rmse' => $rmse, 'message' => 'RMSE calculated successfully' ]; } else { return [ 'success' => false, 'message' => 'No valid combinations found for RMSE calculation' ]; } } catch (Exception $e) { error_log("RMSE calculation error: " . $e->getMessage()); return [ 'success' => false, 'message' => $e->getMessage() ]; } } public function getProgress() { $alignmentScore = null; $progress = $_SESSION['panel_generation_progress'] ?? 0; if ($progress >= 100) { $alignmentResult = $this->calculateAlignmentScore(); if ($alignmentResult['success']) { $alignmentScore = $alignmentResult['score']; } } return [ 'success' => true, 'progress' => $progress, 'status' => $_SESSION['panel_generation_status'] ?? 'Initializing...', 'alignment_score' => $alignmentScore ]; } public function deletePanelist($panelistId) { try { $sql = "DELETE FROM panel_data WHERE panelist_id = '" . $this->db->escape($panelistId) . "'"; if ($this->db->query($sql)) { // Update statistics after deleting a panelist $this->updateStatisticChecks(); $alignmentScore = $this->calculateAlignmentScore(); return [ 'success' => true, 'alignment_score' => $alignmentScore['score'] ?? null ]; } throw new Exception("Failed to delete panelist"); } catch (Exception $e) { error_log("Delete panelist error: " . $e->getMessage()); return ['success' => false, 'message' => $e->getMessage()]; } } } // Handle requests header('Content-Type: application/json'); $auth = new Auth(); if (!$auth->isLoggedIn()) { echo json_encode(['success' => false, 'message' => 'Unauthorized']); exit; } $handler = new PanelAlignmentHandler(); $action = $_POST['action'] ?? ''; $response = ['success' => false, 'message' => 'Invalid action']; switch ($action) { case 'calculate_optimal': $response = $handler->calculateOptimalCount(); break; case 'generate_panel': $count = intval($_POST['count'] ?? 0); if ($count > 0) { $response = $handler->generatePanelData($count); } else { $response = ['success' => false, 'message' => 'Invalid count']; } break; case 'get_progress': $response = $handler->getProgress(); break; case 'delete_panelist': $panelistId = $_POST['panelist_id'] ?? ''; if ($panelistId) { $response = $handler->deletePanelist($panelistId); } else { $response = ['success' => false, 'message' => 'Invalid panelist ID']; } break; case 'get_alignment_score': $response = $handler->calculateAlignmentScore(); break; case 'get_rmse_score': $response = $handler->calculateRmseScore(); break; case 'delete_panel': $response = $handler->deletePanelData(); break; case 'update_statistic_checks': $response = $handler->updateStatisticChecks(); break; } echo json_encode($response);