Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .ruby-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
ruby-3.1.3
3.1.3
2 changes: 0 additions & 2 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ gem "acts_as_list"
# Build JSON APIs with ease [https://github.com/rails/jbuilder]
gem "jbuilder"

gem 'concurrent-ruby', '1.3.4'

# Use Redis adapter to run Action Cable in production
# gem "redis", "~> 4.0"

Expand Down
1 change: 0 additions & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,6 @@ DEPENDENCIES
acts_as_list
bootsnap
capybara
concurrent-ruby (= 1.3.4)
cssbundling-rails
debug
foreman (~> 0.88.1)
Expand Down
247 changes: 247 additions & 0 deletions app/controllers/branch_analytics_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
class BranchAnalyticsController < ApplicationController
before_action :set_survey

def show
@show_deleted = params[:show_deleted] == 'true'
@analytics = {
overview: survey_overview,
by_branch: branch_response_summary,
question_analysis: question_breakdown,
completion_stats: completion_statistics,
charts: chart_data,
deletion_summary: deletion_summary
}
@chart_data_json = chart_data.to_json
end

private

def set_survey
@survey = Survey.find(params[:survey_id])
end

def survey_overview
{
total_responses: responses_scope.count,
total_branches: branches_scope.count,
total_questions: questions_scope.count,
response_rate: calculate_overall_response_rate,
deleted_responses: @survey.responses.only_deleted.count,
deleted_branches: @survey.survey_branches.only_deleted.count,
deleted_questions: @survey.questions.only_deleted.count
}
end

def branch_response_summary
survey_branches_with_associations.map do |branch|
branch_responses = responses_for_branch(branch)

{
role: branch.role.display_name,
branch_id: branch.id,
total_responses: branch_responses.count,
unique_respondents: branch_responses.distinct.count,
questions_count: branch.branch_questions.count,
avg_response_length: calculate_avg_response_length(branch_responses),
completion_rate: calculate_branch_completion_rate(branch),
response_distribution: response_type_distribution(branch)
}
end
end

def question_breakdown
questions_scope.includes(:responses, :branch_questions).map do |question|
{
question_id: question.id,
content: question.content,
question_type: question.question_type,
total_responses: question.responses.where(survey_id: @survey.id).count,
branches: branches_scope.includes(:role).map do |branch|
branch_responses = question.responses.where(survey_id: @survey.id, survey_branch: branch)
{
role: branch.role.display_name,
response_count: branch_responses.count,
sample_responses: branch_responses.limit(3).pluck(:value)
}
end
}
end
end

def completion_statistics
{
response_timeline: response_timeline_data,
most_active_branch: most_active_branch,
least_active_branch: least_active_branch,
average_responses_per_branch: average_responses_per_branch
}
end

def deletion_summary
{
deleted_responses_by_date: deleted_responses_timeline,
deleted_questions_by_type: deleted_questions_by_type,
deleted_branches_by_role: deleted_branches_by_role,
recent_deletions: recent_deletions
}
end

def calculate_overall_response_rate
total_possible = branches_scope.joins(:branch_questions).count
return 0 if total_possible.zero?

(responses_scope.count.to_f / total_possible * 100).round(2)
end

def calculate_avg_response_length(responses)
text_responses = responses.where.not(value: [nil, ''])
return 0 if text_responses.empty?

(text_responses.sum { |r| r.value.to_s.length } / text_responses.count.to_f).round(2)
end

def calculate_branch_completion_rate(branch)
total_questions = branch.branch_questions.count
return 0 if total_questions.zero?

branch_responses = responses_for_branch(branch)

(branch_responses.count.to_f / total_questions * 100).round(2)
end

def response_type_distribution(branch)
responses_for_branch(branch)
.joins(:question)
.group('questions.question_type')
.count
end

def response_timeline_data
responses_scope
.where('created_at >= ?', 30.days.ago)
.group("DATE(created_at)")
.order("DATE(created_at)")
.count
end

def most_active_branch
branch_data = branch_response_summary
return nil if branch_data.empty?

branch_data.max_by { |b| b[:total_responses] }
end

def least_active_branch
branch_data = branch_response_summary
return nil if branch_data.empty?

branch_data.min_by { |b| b[:total_responses] }
end

def average_responses_per_branch
branch_data = branch_response_summary
return 0 if branch_data.empty?

(branch_data.sum { |b| b[:total_responses] } / branch_data.count.to_f).round(2)
end

def chart_data
{
responses_by_branch: responses_by_branch_chart,
completion_rates: completion_rates_chart,
response_timeline: response_timeline_chart,
question_types: question_types_chart,
daily_responses: daily_responses_chart
}
end

def responses_by_branch_chart
survey_branches_with_associations.map do |branch|
[branch.role.display_name, responses_for_branch(branch).count]
end.to_h
end

def completion_rates_chart
survey_branches_with_associations.map do |branch|
completion_rate = calculate_branch_completion_rate(branch).round(1)
[branch.role.display_name, completion_rate]
end.to_h
end

def response_timeline_chart
responses_by_date_range(14)
end

def question_types_chart
questions_scope.group(:question_type).count.transform_keys(&:humanize)
end

def daily_responses_chart
responses_by_date_range(7)
end

# Helper methods for DRY code
def survey_branches_with_associations
@survey_branches_with_associations ||= branches_scope.includes(:role, :branch_questions)
end

def responses_scope
@show_deleted ? @survey.responses.with_deleted : @survey.responses
end

def branches_scope
@show_deleted ? @survey.survey_branches.with_deleted : @survey.survey_branches
end

def questions_scope
@show_deleted ? @survey.questions.with_deleted : @survey.questions
end

def responses_for_branch(branch)
responses_scope.where(survey_branch: branch)
end

def responses_by_date_range(days)
end_date = Date.current
start_date = end_date - (days - 1).days

# Get responses within date range
responses_in_range = responses_scope.where(created_at: start_date.beginning_of_day..end_date.end_of_day)

# Group by date and count
grouped_responses = responses_in_range.group("DATE(created_at)").count

# Fill in missing dates with 0 count
result = {}
(start_date..end_date).each do |date|
date_key = date.strftime("%Y-%m-%d")
result[date_key] = grouped_responses[date] || 0
end

result
end

def deleted_responses_timeline
@survey.responses.only_deleted
.where('deleted_at >= ?', 30.days.ago)
.group("DATE(deleted_at)")
.order("DATE(deleted_at)")
.count
end

def deleted_questions_by_type
@survey.questions.only_deleted.group(:question_type).count.transform_keys(&:humanize)
end

def deleted_branches_by_role
@survey.survey_branches.only_deleted.joins(:role).group('roles.name').count
end

def recent_deletions
{
responses: @survey.responses.only_deleted.order(deleted_at: :desc).limit(5).pluck(:deleted_at, :value),
questions: @survey.questions.only_deleted.order(deleted_at: :desc).limit(5).pluck(:deleted_at, :content),
branches: @survey.survey_branches.only_deleted.includes(:role).order(deleted_at: :desc).limit(5).map { |b| [b.deleted_at, b.role.display_name] }
}
end
end
69 changes: 63 additions & 6 deletions app/controllers/questions_controller.rb
Original file line number Diff line number Diff line change
@@ -1,16 +1,50 @@
class QuestionsController < ApplicationController
before_action :set_survey
before_action :set_question, only: [:edit, :update, :destroy]
before_action :set_question, only: [:edit, :update, :destroy, :restore]
before_action :set_role, only: [:new, :create, :edit, :update, :destroy]
before_action :set_roles, only: [:new, :create, :edit, :update]

def index
@questions = params[:show_deleted] == 'true' ? @survey.questions.with_deleted : @survey.questions
@questions = @questions.distinct

if params[:survey_branch_id].present?
@questions = @questions.joins(:branch_questions)
.where(branch_questions: { survey_branch_id: params[:survey_branch_id] })
.distinct
elsif params[:role_id].present?
@questions = @questions.joins(branch_questions: { survey_branch: :role })
.where(roles: { id: params[:role_id] })
.distinct
end

@questions = @questions.order(:position) if @questions.column_names.include?("position")

respond_to do |format|
format.json do
render json: @questions.as_json(
only: [:id, :content, :question_type, :required],
methods: [:deleted?]
).map do |q|
q['deleted_at'] = @questions.find(q['id']).deleted_at&.strftime('%B %d, %Y') if q['deleted?']
q
end
end
format.html { redirect_to survey_path(@survey) }
end
end

def new
@question = @survey.questions.new
@question = Question.new
end

def create
@question = @survey.questions.new(question_params)
@question = Question.new(question_params.except(:survey_branch_id))
process_options

if @question.save
branch = @survey.survey_branches.find_by(id: params[:question][:survey_branch_id])
BranchQuestion.create!(survey_branch: branch, question: @question) if branch
redirect_to survey_path(@survey), notice: 'Question was successfully created.'
else
render :new, status: :unprocessable_entity
Expand All @@ -35,7 +69,18 @@ def update

def destroy
@question.destroy
redirect_to survey_path(@survey), notice: 'Question was successfully destroyed.'
respond_to do |format|
format.json { head :no_content }
format.html { redirect_to survey_path(@survey), notice: 'Question was successfully soft deleted. It can be restored if needed.' }
end
end

def restore
@question.restore
respond_to do |format|
format.json { head :no_content }
format.html { redirect_to survey_path(@survey), notice: 'Question was successfully restored.' }
end
end

private
Expand All @@ -45,11 +90,23 @@ def set_survey
end

def set_question
@question = @survey.questions.find(params[:id])
if action_name == 'restore'
@question = @survey.questions.with_deleted.find(params[:id])
else
@question = @survey.questions.find(params[:id])
end
end

def set_role
@role = Role.find_by(name: params[:role].downcase) if params[:role].present?
end

def set_roles
@roles = Role.all
end

def question_params
params.require(:question).permit(:content, :question_type, :position, :required, options: [])
params.require(:question).permit(:content, :question_type, :position, :required, options: [], survey_branch_ids: [])
end

def process_options
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/responses_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ def create
private

def response_params
params.require(:response).permit(:survey_id, :question_id, :value)
params.require(:response).permit(:survey_id, :question_id, :value)
end
end
Loading