dga_feedback-2.0.0/src/Service/DgaFeedbackService.php

src/Service/DgaFeedbackService.php
<?php

namespace Drupal\dga_feedback\Service;

use Drupal\Core\Database\Connection;

/**
 * Service for managing feedback data and calculations.
 */
class DgaFeedbackService {

  /**
   * The database connection.
   *
   * @var \Drupal\Core\Database\Connection
   */
  protected $database;

  /**
   * Constructs a DgaFeedbackService object.
   *
   * @param \Drupal\Core\Database\Connection $database
   *   The database connection.
   */
  public function __construct(Connection $database) {
    $this->database = $database;
  }

  /**
   * Saves a feedback submission.
   *
   * @param array $data
   *   Array containing:
   *   - entity_type: The entity type (default: 'node')
   *   - entity_id: The entity ID
   *   - is_useful: "yes" or "no"
   *   - reasons: Array of selected reasons
   *   - feedback: Optional feedback text
   *   - gender: Optional gender selection
   *   - url: The URL where feedback was submitted
   *   - user_id: Optional user ID
   *   - ip_address: Optional IP address
   *
   * @return int|false
   *   The ID of the inserted record, or FALSE on failure.
   */
  public function saveFeedback(array $data) {
    try {
      // Normalize URL
      $url = $data['url'] ?? '/';
      $url = trim($url);
      if (empty($url) || $url === '/') {
        $url = '/';
      } else {
        // Remove language prefix if still present.
        if (preg_match('#^/[a-z]{2}(/.*)?$#', $url, $matches)) {
          $url = isset($matches[1]) ? $matches[1] : '/';
          if (empty($url)) {
            $url = '/';
          }
        }
        // Remove trailing slash.
        $url = rtrim($url, '/');
        if (empty($url)) {
          $url = '/';
        }
      }
      
      // SECURITY: Sanitize reasons array (already sanitized in controller, but double-check here)
      // Get configurable limits from settings
      $config = \Drupal::config('dga_feedback.settings');
      $reason_max_length = (int) ($config->get('reason_max_length') ?? 200);
      $reason_max_count = (int) ($config->get('reason_max_count') ?? 10);
      $feedback_max_length = (int) ($config->get('feedback_max_length') ?? 5000);
      
      $reasons_data = isset($data['reasons']) && is_array($data['reasons']) ? $data['reasons'] : [];
      $sanitized_reasons = [];
      foreach ($reasons_data as $reason) {
        if (is_string($reason)) {
          $reason = trim(strip_tags($reason));
          if (strlen($reason) > 0 && strlen($reason) <= $reason_max_length) {
            $sanitized_reasons[] = $reason;
          }
        }
        if (count($sanitized_reasons) >= $reason_max_count) {
          break; // Limit to configured max reasons
        }
      }
      
      // SECURITY: Sanitize feedback text (already sanitized in controller, but double-check here)
      $feedback_text = isset($data['feedback']) ? trim($data['feedback']) : '';
      $feedback_text = strip_tags($feedback_text);
      if (strlen($feedback_text) > $feedback_max_length) {
        $feedback_text = substr($feedback_text, 0, $feedback_max_length);
      }
      
      $fields = [
        'entity_type' => $data['entity_type'] ?? 'node',
        'entity_id' => (int) ($data['entity_id'] ?? 0),
        'is_useful' => $data['is_useful'] === 'yes' ? 'yes' : 'no',
        'reasons' => !empty($sanitized_reasons) ? json_encode($sanitized_reasons) : '',
        'feedback' => $feedback_text,
        'gender' => isset($data['gender']) && in_array($data['gender'], ['male', 'female']) ? $data['gender'] : NULL,
        'url' => $url,
        'user_id' => $data['user_id'] ?? NULL, // NULL is valid for anonymous
        'ip_address' => $data['ip_address'] ?? NULL,
        'created' => time(),
      ];
      
      // Validate is_useful value.
      if (!in_array($fields['is_useful'], ['yes', 'no'])) {
        \Drupal::logger('dga_feedback')->error('Invalid is_useful value: @value', ['@value' => $data['is_useful'] ?? 'missing']);
        return FALSE;
      }
      
      // Validate required fields
      if (empty($fields['url'])) {
        \Drupal::logger('dga_feedback')->error('Empty URL field');
        return FALSE;
      }

      try {
        // Direct insert - Drupal's database layer auto-commits
        // Transaction wrapper can cause issues with anonymous users
        // Do NOT wrap in transaction - direct insert works better for anonymous
        $feedback_id = $this->database->insert('dga_feedback')
          ->fields($fields)
          ->execute();

        if ($feedback_id) {
          // Invalidate cache tags after saving feedback.
          \Drupal::service('cache_tags.invalidator')->invalidateTags(['dga_feedback:submissions']);
        }
      } catch (\Exception $e) {
        \Drupal::logger('dga_feedback')->error('Exception saving feedback: @message, Trace: @trace', [
          '@message' => $e->getMessage(),
          '@trace' => $e->getTraceAsString(),
        ]);
        throw $e;
      }

      return $feedback_id;
    }
    catch (\Exception $e) {
      \Drupal::logger('dga_feedback')->error('Error saving feedback: @message, Trace: @trace', [
        '@message' => $e->getMessage(),
        '@trace' => $e->getTraceAsString(),
      ]);
      return FALSE;
    }
    catch (\Throwable $e) {
      \Drupal::logger('dga_feedback')->error('Fatal error saving feedback: @message, Trace: @trace', [
        '@message' => $e->getMessage(),
        '@trace' => $e->getTraceAsString(),
      ]);
      return FALSE;
    }
  }

  /**
   * Gets feedback statistics for a specific entity or URL.
   *
   * @param string|null $entity_type
   *   The entity type to filter by (optional).
   * @param int|null $entity_id
   *   The entity ID to filter by (optional).
   * @param string|null $url
   *   The URL to filter by (optional).
   *
   * @return array
   *   Array with:
   *   - yes_count: Number of "yes" responses (int)
   *   - no_count: Number of "no" responses (int)
   *   - total_count: Total number of feedbacks (int)
   *   - yes_percentage: Percentage of "yes" responses (float)
   */
  public function getStatistics($entity_type = NULL, $entity_id = NULL, $url = NULL) {
    $query = $this->database->select('dga_feedback', 'f');
    $query->addExpression('COUNT(f.id)', 'total_count');
    $query->addExpression('SUM(CASE WHEN f.is_useful = \'yes\' THEN 1 ELSE 0 END)', 'yes_count');
    $query->addExpression('SUM(CASE WHEN f.is_useful = \'no\' THEN 1 ELSE 0 END)', 'no_count');

    // Match by entity OR URL when both are provided
    if ($entity_type && $entity_id !== NULL && $entity_id > 0 && $url) {
      $or_group = $query->orConditionGroup();
      $or_group->condition(
        $query->andConditionGroup()
          ->condition('f.entity_type', $entity_type)
          ->condition('f.entity_id', $entity_id)
      );
      
      $normalized_url = rtrim($url, '/') ?: '/';
      if ($normalized_url === '/') {
        $or_group->condition('f.url', '/');
      } else {
        $or_group->condition(
          $query->orConditionGroup()
            ->condition('f.url', $normalized_url)
            ->condition('f.url', $normalized_url . '/')
        );
      }
      
      $query->condition($or_group);
    }
    elseif ($entity_type && $entity_id !== NULL && $entity_id > 0) {
      $query->condition('f.entity_type', $entity_type);
      $query->condition('f.entity_id', $entity_id);
    }
    elseif ($url) {
      $normalized_url = rtrim($url, '/') ?: '/';
      
      $conditions = $query->orConditionGroup();
      $conditions->condition('f.url', $normalized_url);
      
      if ($normalized_url !== '/') {
        $conditions->condition('f.url', $normalized_url . '/');
      }
      
      if (preg_match('#^/[a-z]{2}/(.+)$#', $normalized_url, $matches)) {
        $without_lang = '/' . $matches[1];
        $conditions->condition('f.url', $without_lang);
        if ($without_lang !== '/') {
          $conditions->condition('f.url', $without_lang . '/');
        }
      }
      else if ($normalized_url !== '/') {
        $lang_prefixes = ['/en', '/ar'];
        foreach ($lang_prefixes as $prefix) {
          $with_lang = $prefix . $normalized_url;
          $conditions->condition('f.url', $with_lang);
          $conditions->condition('f.url', $with_lang . '/');
        }
      }
      
      $query->condition($conditions);
    }

    $result = $query->execute()->fetchObject();

    // Handle empty result set.
    if (!$result || $result->total_count == 0) {
      return [
        'yes_count' => 0,
        'no_count' => 0,
        'total_count' => 0,
        'yes_percentage' => 0.0,
      ];
    }

    $total_count = (int) $result->total_count;
    $yes_count = (int) ($result->yes_count ?? 0);
    $no_count = (int) ($result->no_count ?? 0);
    $yes_percentage = $total_count > 0 ? (($yes_count / $total_count) * 100) : 0.0;

    return [
      'yes_count' => $yes_count,
      'no_count' => $no_count,
      'total_count' => $total_count,
      'yes_percentage' => round($yes_percentage, 1),
    ];
  }

  /**
   * Gets feedback statistics for the current page URL.
   *
   * @param string $url
   *   The URL path.
   *
   * @return array
   *   Array with statistics.
   */
  public function getStatisticsByUrl($url) {
    return $this->getStatistics(NULL, NULL, $url);
  }

  /**
   * Gets feedback statistics for a specific entity.
   *
   * @param string $entity_type
   *   The entity type.
   * @param int $entity_id
   *   The entity ID.
   *
   * @return array
   *   Array with statistics.
   */
  public function getStatisticsByEntity($entity_type, $entity_id) {
    return $this->getStatistics($entity_type, $entity_id, NULL);
  }

  /**
   * Gets overall statistics across all feedbacks.
   *
   * @return array
   *   Array with statistics for all feedbacks.
   */
  public function getOverallStatistics() {
    return $this->getStatistics(NULL, NULL, NULL);
  }

  /**
   * Gets all feedback submissions with pagination.
   *
   * @param int $limit
   *   Number of items per page.
   * @param int $offset
   *   Offset for pagination.
   * @param array $filters
   *   Optional filters.
   * @param string $sort_by
   *   Field to sort by (default: 'created').
   * @param string $sort_direction
   *   Sort direction: 'ASC' or 'DESC' (default: 'DESC').
   *
   * @return array
   *   Array of feedback submissions.
   */
  public function getAllSubmissions($limit = 50, $offset = 0, array $filters = [], $sort_by = 'created', $sort_direction = 'DESC') {
    $query = $this->database->select('dga_feedback', 'f');
    $query->fields('f');
    
    // Validate sort parameters.
    $valid_sort_fields = ['id', 'is_useful', 'created', 'url', 'entity_type', 'entity_id', 'user_id'];
    if (!in_array($sort_by, $valid_sort_fields)) {
      $sort_by = 'created';
    }
    
    $sort_direction = strtoupper($sort_direction);
    if ($sort_direction !== 'ASC' && $sort_direction !== 'DESC') {
      $sort_direction = 'DESC';
    }
    
    $query->orderBy('f.' . $sort_by, $sort_direction);
    $query->range($offset, $limit);

    // Apply filters.
    if (isset($filters['id']) && $filters['id'] > 0) {
      $query->condition('f.id', $filters['id']);
    }
    if (!empty($filters['entity_type'])) {
      $query->condition('f.entity_type', $filters['entity_type']);
    }
    if (isset($filters['entity_id']) && $filters['entity_id'] > 0) {
      $query->condition('f.entity_id', (int) $filters['entity_id']);
    }
    if (!empty($filters['url'])) {
      $query->condition('f.url', '%' . $this->database->escapeLike($filters['url']) . '%', 'LIKE');
    }
    if (!empty($filters['is_useful'])) {
      $query->condition('f.is_useful', $filters['is_useful']);
    }
    if (!empty($filters['feedback'])) {
      $query->condition('f.feedback', '%' . $this->database->escapeLike($filters['feedback']) . '%', 'LIKE');
    }
    if (isset($filters['user_id']) && $filters['user_id'] !== NULL && $filters['user_id'] !== '') {
      $query->condition('f.user_id', (int) $filters['user_id']);
    }
    if (!empty($filters['ip_address'])) {
      $query->condition('f.ip_address', '%' . $this->database->escapeLike($filters['ip_address']) . '%', 'LIKE');
    }
    if (!empty($filters['date_from'])) {
      $query->condition('f.created', $filters['date_from'], '>=');
    }
    if (!empty($filters['date_to'])) {
      $query->condition('f.created', $filters['date_to'], '<=');
    }

    $results = $query->execute()->fetchAll(\PDO::FETCH_ASSOC);
    
    // Decode JSON reasons
    foreach ($results as &$result) {
      if (!empty($result['reasons'])) {
        $decoded = json_decode($result['reasons'], TRUE);
        $result['reasons'] = is_array($decoded) ? $decoded : [];
      } else {
        $result['reasons'] = [];
      }
    }
    
    return $results;
  }

  /**
   * Gets total count of submissions with optional filters.
   *
   * @param array $filters
   *   Optional filters (same as getAllSubmissions).
   *
   * @return int
   *   Total count of submissions.
   */
  public function getSubmissionsCount(array $filters = []) {
    $query = $this->database->select('dga_feedback', 'f');
    $query->addExpression('COUNT(f.id)', 'count');

    // Apply same filters as getAllSubmissions.
    if (isset($filters['id']) && $filters['id'] > 0) {
      $query->condition('f.id', $filters['id']);
    }
    if (!empty($filters['entity_type'])) {
      $query->condition('f.entity_type', $filters['entity_type']);
    }
    if (isset($filters['entity_id']) && $filters['entity_id'] > 0) {
      $query->condition('f.entity_id', (int) $filters['entity_id']);
    }
    if (!empty($filters['url'])) {
      $query->condition('f.url', '%' . $this->database->escapeLike($filters['url']) . '%', 'LIKE');
    }
    if (!empty($filters['is_useful'])) {
      $query->condition('f.is_useful', $filters['is_useful']);
    }
    if (!empty($filters['feedback'])) {
      $query->condition('f.feedback', '%' . $this->database->escapeLike($filters['feedback']) . '%', 'LIKE');
    }
    if (isset($filters['user_id']) && $filters['user_id'] !== NULL && $filters['user_id'] !== '') {
      $query->condition('f.user_id', (int) $filters['user_id']);
    }
    if (!empty($filters['ip_address'])) {
      $query->condition('f.ip_address', '%' . $this->database->escapeLike($filters['ip_address']) . '%', 'LIKE');
    }
    if (!empty($filters['date_from'])) {
      $query->condition('f.created', $filters['date_from'], '>=');
    }
    if (!empty($filters['date_to'])) {
      $query->condition('f.created', $filters['date_to'], '<=');
    }

    $result = $query->execute()->fetchField();
    return (int) $result;
  }

  /**
   * Gets statistics grouped by usefulness (yes/no).
   *
   * @return array
   *   Array with 'yes' and 'no' counts.
   */
  public function getUsefulnessDistribution() {
    $query = $this->database->select('dga_feedback', 'f');
    $query->addField('f', 'is_useful');
    $query->addExpression('COUNT(f.id)', 'count');
    $query->groupBy('f.is_useful');
    $query->orderBy('f.is_useful', 'ASC');

    $results = $query->execute()->fetchAll();
    $distribution = ['yes' => 0, 'no' => 0];

    foreach ($results as $result) {
      $useful = $result->is_useful ?? '';
      if (in_array($useful, ['yes', 'no'])) {
        $distribution[$useful] = (int) $result->count;
      }
    }

    return $distribution;
  }

  /**
   * Gets a single submission by ID.
   *
   * @param int $id
   *   The submission ID.
   *
   * @return array|false
   *   Array of submission data or FALSE if not found.
   */
  public function getSubmissionById($id) {
    $query = $this->database->select('dga_feedback', 'f');
    $query->fields('f');
    $query->condition('f.id', (int) $id);
    $result = $query->execute()->fetchAssoc();
    
    if ($result) {
      // Decode JSON reasons
      if (!empty($result['reasons'])) {
        $decoded = json_decode($result['reasons'], TRUE);
        $result['reasons'] = is_array($decoded) ? $decoded : [];
      } else {
        $result['reasons'] = [];
      }
    }
    
    return $result ? $result : FALSE;
  }

  /**
   * Deletes a submission by ID.
   *
   * @param int $id
   *   The submission ID.
   *
   * @return bool
   *   TRUE if deleted, FALSE otherwise.
   */
  public function deleteSubmission($id) {
    try {
      $deleted = $this->database->delete('dga_feedback')
        ->condition('id', (int) $id)
        ->execute();
      
      if ($deleted) {
        // Invalidate cache tags after deleting.
        \Drupal::service('cache_tags.invalidator')->invalidateTags(['dga_feedback:submissions']);
      }
      
      return $deleted > 0;
    } catch (\Exception $e) {
      \Drupal::logger('dga_feedback')->error('Error deleting feedback: @message', [
        '@message' => $e->getMessage(),
      ]);
      return FALSE;
    }
  }

  /**
   * Deletes multiple submissions by IDs.
   *
   * @param array $ids
   *   Array of submission IDs.
   *
   * @return int
   *   Number of submissions deleted.
   */
  public function bulkDeleteSubmissions(array $ids) {
    if (empty($ids)) {
      return 0;
    }

    try {
      $ids = array_filter(array_map('intval', $ids), function($id) {
        return $id > 0;
      });

      if (empty($ids)) {
        return 0;
      }

      $deleted = $this->database->delete('dga_feedback')
        ->condition('id', $ids, 'IN')
        ->execute();
      
      if ($deleted > 0) {
        // Invalidate cache tags after deleting.
        \Drupal::service('cache_tags.invalidator')->invalidateTags(['dga_feedback:submissions']);
      }
      
      return $deleted;
    } catch (\Exception $e) {
      \Drupal::logger('dga_feedback')->error('Error bulk deleting feedback: @message', [
        '@message' => $e->getMessage(),
      ]);
      return 0;
    }
  }

  /**
   * Updates a submission.
   *
   * @param int $id
   *   The submission ID.
   * @param array $data
   *   Array of fields to update.
   *
   * @return bool
   *   TRUE if updated, FALSE otherwise.
   */
  public function updateSubmission($id, array $data) {
    try {
      $fields = [];
      if (isset($data['is_useful']) && in_array($data['is_useful'], ['yes', 'no'])) {
        $fields['is_useful'] = $data['is_useful'];
      }
      if (isset($data['reasons']) && is_array($data['reasons'])) {
        $fields['reasons'] = json_encode($data['reasons']);
      }
      if (isset($data['feedback'])) {
        $fields['feedback'] = trim($data['feedback']);
      }
      if (isset($data['gender']) && in_array($data['gender'], ['male', 'female', NULL])) {
        $fields['gender'] = $data['gender'];
      }
      
      if (empty($fields)) {
        return FALSE;
      }
      
      $updated = $this->database->update('dga_feedback')
        ->fields($fields)
        ->condition('id', (int) $id)
        ->execute();
      
      if ($updated !== FALSE) {
        // Invalidate cache tags after updating.
        \Drupal::service('cache_tags.invalidator')->invalidateTags(['dga_feedback:submissions']);
      }
      
      return $updated !== FALSE;
    } catch (\Exception $e) {
      \Drupal::logger('dga_feedback')->error('Error updating feedback: @message', [
        '@message' => $e->getMessage(),
      ]);
      return FALSE;
    }
  }

  /**
   * Gets the total count of unique URLs.
   *
   * @return int
   *   Total number of unique URLs.
   */
  public function getUniqueUrlCount() {
    $query = $this->database->select('dga_feedback', 'f');
    $query->addExpression('COUNT(DISTINCT f.url)', 'count');
    $result = $query->execute()->fetchField();
    return (int) $result;
  }

  /**
   * Gets statistics grouped by URL.
   *
   * @param int $limit
   *   Maximum number of URLs to return (default: 50).
   * @param string $order_by
   *   Field to order by: 'count', 'yes_percentage', or 'url' (default: 'count').
   * @param string $order_direction
   *   Order direction: 'ASC' or 'DESC' (default: 'DESC').
   *
   * @return array
   *   Array of URL statistics.
   */
  public function getStatisticsGroupedByUrl($limit = 50, $order_by = 'count', $order_direction = 'DESC') {
    $query = $this->database->select('dga_feedback', 'f');
    $query->addField('f', 'url');
    $query->addExpression('COUNT(f.id)', 'count');
    $query->addExpression('SUM(CASE WHEN f.is_useful = \'yes\' THEN 1 ELSE 0 END)', 'yes_count');
    $query->addExpression('SUM(CASE WHEN f.is_useful = \'no\' THEN 1 ELSE 0 END)', 'no_count');
    $query->groupBy('f.url');

    $valid_order_fields = ['count', 'yes_percentage', 'url'];
    if (!in_array($order_by, $valid_order_fields)) {
      $order_by = 'count';
    }

    $order_direction = strtoupper($order_direction);
    if ($order_direction !== 'ASC' && $order_direction !== 'DESC') {
      $order_direction = 'DESC';
    }

    // yes_percentage is calculated, not a database column, so we can't order by it in SQL
    // We'll fetch more results and sort in PHP if needed
    $fetch_limit = ($order_by === 'yes_percentage') ? $limit * 10 : $limit;

    if ($order_by === 'url') {
      $query->orderBy('f.url', $order_direction);
    } elseif ($order_by === 'count') {
      $query->orderBy('count', $order_direction);
    }
    // For yes_percentage, we'll order by count as fallback and sort in PHP

    $query->range(0, $fetch_limit);

    $results = $query->execute()->fetchAll();

    $url_stats = [];
    foreach ($results as $result) {
      $url = $result->url ?? '/';
      $count = (int) $result->count;
      $yes_count = (int) ($result->yes_count ?? 0);
      $no_count = (int) ($result->no_count ?? 0);
      $yes_percentage = $count > 0 ? (($yes_count / $count) * 100) : 0.0;

      $url_stats[] = [
        'url' => $url,
        'count' => $count,
        'yes_count' => $yes_count,
        'no_count' => $no_count,
        'yes_percentage' => round($yes_percentage, 1),
      ];
    }

    // Sort by yes_percentage if needed (since it's calculated)
    if ($order_by === 'yes_percentage') {
      usort($url_stats, function($a, $b) use ($order_direction) {
        if ($order_direction === 'DESC') {
          return $b['yes_percentage'] <=> $a['yes_percentage'];
        } else {
          return $a['yes_percentage'] <=> $b['yes_percentage'];
        }
      });
      // Limit to requested limit after sorting
      $url_stats = array_slice($url_stats, 0, $limit);
    }

    return $url_stats;
  }

  /**
   * Gets most useful page (highest yes percentage).
   *
   * @return array|null
   *   Array with url, yes_percentage, and count, or NULL if no feedback exists.
   */
  public function getMostUsefulPage() {
    $stats = $this->getStatisticsGroupedByUrl(1, 'yes_percentage', 'DESC');
    return !empty($stats) ? $stats[0] : NULL;
  }

  /**
   * Gets most feedback page (highest count).
   *
   * @return array|null
   *   Array with url, yes_percentage, and count, or NULL if no feedback exists.
   */
  public function getMostFeedbackPage() {
    $stats = $this->getStatisticsGroupedByUrl(1, 'count', 'DESC');
    return !empty($stats) ? $stats[0] : NULL;
  }

  /**
   * Gets recent activity statistics (last 7 days, last 30 days).
   *
   * @return array
   *   Array with last_7_days and last_30_days statistics.
   */
  public function getRecentActivity() {
    $now = time();
    $seven_days_ago = $now - (7 * 24 * 60 * 60);
    $thirty_days_ago = $now - (30 * 24 * 60 * 60);

    // Last 7 days
    $query_7 = $this->database->select('dga_feedback', 'f');
    $query_7->addExpression('COUNT(f.id)', 'count');
    $query_7->addExpression('SUM(CASE WHEN f.is_useful = \'yes\' THEN 1 ELSE 0 END)', 'yes_count');
    $query_7->condition('f.created', $seven_days_ago, '>=');
    $result_7 = $query_7->execute()->fetchObject();

    // Last 30 days
    $query_30 = $this->database->select('dga_feedback', 'f');
    $query_30->addExpression('COUNT(f.id)', 'count');
    $query_30->addExpression('SUM(CASE WHEN f.is_useful = \'yes\' THEN 1 ELSE 0 END)', 'yes_count');
    $query_30->condition('f.created', $thirty_days_ago, '>=');
    $result_30 = $query_30->execute()->fetchObject();

    $count_7 = (int) ($result_7->count ?? 0);
    $yes_count_7 = (int) ($result_7->yes_count ?? 0);
    $useful_percentage_7 = $count_7 > 0 ? (($yes_count_7 / $count_7) * 100) : 0.0;

    $count_30 = (int) ($result_30->count ?? 0);
    $yes_count_30 = (int) ($result_30->yes_count ?? 0);
    $useful_percentage_30 = $count_30 > 0 ? (($yes_count_30 / $count_30) * 100) : 0.0;

    return [
      'last_7_days' => [
        'count' => $count_7,
        'useful_percentage' => round($useful_percentage_7, 1),
      ],
      'last_30_days' => [
        'count' => $count_30,
        'useful_percentage' => round($useful_percentage_30, 1),
      ],
    ];
  }

  /**
   * Gets useful feedback percentage (yes responses).
   *
   * @return float
   *   Percentage of useful feedback (0-100).
   */
  public function getUsefulPercentage() {
    $stats = $this->getOverallStatistics();
    return $stats['yes_percentage'] ?? 0.0;
  }

  /**
   * Gets submissions count by user type (anonymous vs authenticated).
   *
   * @return array
   *   Array with anonymous and authenticated counts.
   */
  public function getSubmissionsByUserType() {
    $anonymous_query = $this->database->select('dga_feedback', 'f');
    $anonymous_query->addExpression('COUNT(f.id)', 'count');
    $anonymous_query->isNull('f.user_id');
    $anonymous_count = (int) $anonymous_query->execute()->fetchField();

    $auth_query = $this->database->select('dga_feedback', 'f');
    $auth_query->addExpression('COUNT(f.id)', 'count');
    $auth_query->isNotNull('f.user_id');
    $auth_count = (int) $auth_query->execute()->fetchField();

    return [
      'anonymous' => $anonymous_count,
      'authenticated' => $auth_count,
    ];
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc