\ No newline at end of file
diff --git a/app/views/surveys/take.html.erb b/app/views/surveys/take.html.erb
index fdaa4b7..88c5542 100644
--- a/app/views/surveys/take.html.erb
+++ b/app/views/surveys/take.html.erb
@@ -8,12 +8,14 @@
+ data-questions="<%= @questions.to_json %>"
+ data-role="<%= @role.id %>">
<%= form_with(url: submit_survey_path(@survey), method: :post, class: "space-y-6") do |form| %>
+ <%= hidden_field_tag :role, params[:role] %>
<% if @questions.any? %>
<% @questions.each do |question| %>
diff --git a/config/routes.rb b/config/routes.rb
index e4f3933..d9b1728 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -4,13 +4,26 @@
# Set the root path to the surveys index
root "surveys#index"
- # RESTful routes for surveys
+ resources :roles do
+ member do
+ patch 'restore'
+ end
+ collection do
+ get 'deleted'
+ end
+ end
+
resources :surveys do
# Nested routes for questions
- resources :questions, except: [:index, :show]
-
+ resources :questions, except: [:show] do
+ member do
+ patch 'restore'
+ end
+ end
+ get 'analytics', to: 'branch_analytics#show'
# Route for taking a survey
member do
+ patch 'restore'
get 'take'
post 'submit'
end
diff --git a/db/migrate/20250827224445_create_survey_branches_and_roles.rb b/db/migrate/20250827224445_create_survey_branches_and_roles.rb
new file mode 100644
index 0000000..967ee9a
--- /dev/null
+++ b/db/migrate/20250827224445_create_survey_branches_and_roles.rb
@@ -0,0 +1,21 @@
+class CreateSurveyBranchesAndRoles < ActiveRecord::Migration[7.0]
+ def change
+ create_table :roles do |t|
+ t.string :name, null: false, unique: true
+ t.timestamps
+ end
+
+ create_table :survey_branches do |t|
+ t.references :survey, null: false, foreign_key: true
+ t.references :role, null: false, foreign_key: true
+ t.string :name, null: false
+ t.timestamps
+ end
+
+ create_table :branch_questions do |t|
+ t.references :survey_branch, null: false, foreign_key: true
+ t.references :question, null: false, foreign_key: true
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20250828034813_remove_survey_id_from_questions.rb b/db/migrate/20250828034813_remove_survey_id_from_questions.rb
new file mode 100644
index 0000000..5553146
--- /dev/null
+++ b/db/migrate/20250828034813_remove_survey_id_from_questions.rb
@@ -0,0 +1,5 @@
+class RemoveSurveyIdFromQuestions < ActiveRecord::Migration[7.0]
+ def change
+ remove_column :questions, :survey_id, :bigint
+ end
+end
diff --git a/db/migrate/20251010202632_add_survey_branch_to_responses.rb b/db/migrate/20251010202632_add_survey_branch_to_responses.rb
new file mode 100644
index 0000000..6d0232d
--- /dev/null
+++ b/db/migrate/20251010202632_add_survey_branch_to_responses.rb
@@ -0,0 +1,5 @@
+class AddSurveyBranchToResponses < ActiveRecord::Migration[7.0]
+ def change
+ add_reference :responses, :survey_branch, null: true, foreign_key: true
+ end
+end
\ No newline at end of file
diff --git a/db/migrate/20251010212936_add_unique_index_to_roles_name.rb b/db/migrate/20251010212936_add_unique_index_to_roles_name.rb
new file mode 100644
index 0000000..5665bdd
--- /dev/null
+++ b/db/migrate/20251010212936_add_unique_index_to_roles_name.rb
@@ -0,0 +1,26 @@
+class AddUniqueIndexToRolesName < ActiveRecord::Migration[7.0]
+ def up
+ # First, normalize all existing role names to uppercase
+ execute <<-SQL
+ UPDATE roles SET name = UPPER(name);
+ SQL
+
+ # Remove any duplicate roles that may exist after normalization
+ # Keep the first occurrence of each name and remove duplicates
+ execute <<-SQL
+ DELETE FROM roles
+ WHERE id NOT IN (
+ SELECT MIN(id)
+ FROM (SELECT id, name FROM roles) AS r
+ GROUP BY name
+ );
+ SQL
+
+ # Add the unique index to prevent future duplicates
+ add_index :roles, :name, unique: true
+ end
+
+ def down
+ remove_index :roles, :name
+ end
+end
diff --git a/db/migrate/20251013192304_add_deleted_at_to_all_models.rb b/db/migrate/20251013192304_add_deleted_at_to_all_models.rb
new file mode 100644
index 0000000..28a107f
--- /dev/null
+++ b/db/migrate/20251013192304_add_deleted_at_to_all_models.rb
@@ -0,0 +1,17 @@
+class AddDeletedAtToAllModels < ActiveRecord::Migration[7.0]
+ def change
+ add_column :roles, :deleted_at, :datetime
+ add_column :surveys, :deleted_at, :datetime
+ add_column :questions, :deleted_at, :datetime
+ add_column :survey_branches, :deleted_at, :datetime
+ add_column :responses, :deleted_at, :datetime
+ add_column :branch_questions, :deleted_at, :datetime
+
+ add_index :roles, :deleted_at
+ add_index :surveys, :deleted_at
+ add_index :questions, :deleted_at
+ add_index :survey_branches, :deleted_at
+ add_index :responses, :deleted_at
+ add_index :branch_questions, :deleted_at
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index e823b08..4ba7810 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,12 +10,22 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.0].define(version: 2025_03_17_163201) do
+ActiveRecord::Schema[7.0].define(version: 2025_10_13_192304) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
+ create_table "branch_questions", force: :cascade do |t|
+ t.bigint "survey_branch_id", null: false
+ t.bigint "question_id", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.datetime "deleted_at"
+ t.index ["deleted_at"], name: "index_branch_questions_on_deleted_at"
+ t.index ["question_id"], name: "index_branch_questions_on_question_id"
+ t.index ["survey_branch_id"], name: "index_branch_questions_on_survey_branch_id"
+ end
+
create_table "questions", force: :cascade do |t|
- t.bigint "survey_id", null: false
t.text "content"
t.string "question_type"
t.integer "position"
@@ -23,7 +33,8 @@
t.datetime "updated_at", null: false
t.text "options"
t.boolean "required"
- t.index ["survey_id"], name: "index_questions_on_survey_id"
+ t.datetime "deleted_at"
+ t.index ["deleted_at"], name: "index_questions_on_deleted_at"
end
create_table "responses", force: :cascade do |t|
@@ -32,18 +43,49 @@
t.text "value"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.bigint "survey_branch_id"
+ t.datetime "deleted_at"
+ t.index ["deleted_at"], name: "index_responses_on_deleted_at"
t.index ["question_id"], name: "index_responses_on_question_id"
+ t.index ["survey_branch_id"], name: "index_responses_on_survey_branch_id"
t.index ["survey_id"], name: "index_responses_on_survey_id"
end
+ create_table "roles", force: :cascade do |t|
+ t.string "name", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.datetime "deleted_at"
+ t.index ["deleted_at"], name: "index_roles_on_deleted_at"
+ t.index ["name"], name: "index_roles_on_name", unique: true
+ end
+
+ create_table "survey_branches", force: :cascade do |t|
+ t.bigint "survey_id", null: false
+ t.bigint "role_id", null: false
+ t.string "name", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.datetime "deleted_at"
+ t.index ["deleted_at"], name: "index_survey_branches_on_deleted_at"
+ t.index ["role_id"], name: "index_survey_branches_on_role_id"
+ t.index ["survey_id"], name: "index_survey_branches_on_survey_id"
+ end
+
create_table "surveys", force: :cascade do |t|
t.string "title"
t.text "description"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.datetime "deleted_at"
+ t.index ["deleted_at"], name: "index_surveys_on_deleted_at"
end
- add_foreign_key "questions", "surveys"
+ add_foreign_key "branch_questions", "questions"
+ add_foreign_key "branch_questions", "survey_branches"
add_foreign_key "responses", "questions"
+ add_foreign_key "responses", "survey_branches"
add_foreign_key "responses", "surveys"
+ add_foreign_key "survey_branches", "roles"
+ add_foreign_key "survey_branches", "surveys"
end
diff --git a/db/seeds.rb b/db/seeds.rb
index 3831ede..4eed8a1 100644
--- a/db/seeds.rb
+++ b/db/seeds.rb
@@ -8,9 +8,16 @@
# Clear existing data
puts "Clearing existing data..."
-Response.destroy_all
-Question.destroy_all
+Role.destroy_all
Survey.destroy_all
+SurveyBranch.destroy_all
+Question.destroy_all
+BranchQuestion.destroy_all
+
+# Create roles
+roles = ["Data Engineer", "Frontend Engineer", "QA Engineer", "UX Designer", "Backend Engineer", "Product Manager", "UI Designer", "API Developer"].map do |role_name|
+ Role.create!(name: role_name)
+end
# Create engineering team survey
puts "Creating engineering team survey..."
@@ -19,9 +26,15 @@
description: "This survey helps us understand the satisfaction and needs of our engineering team members."
)
+# Create branches only for engineering roles
+engineering_roles = roles.select { |role| role.name.downcase.include?('engineer') || role.name.downcase.include?('developer') }
+engineering_survey_branches = engineering_roles.map do |role|
+ SurveyBranch.create!(survey: engineering_survey, role: role, name: "#{role.name} Branch")
+end
+
# Create questions for the engineering survey
puts "Creating questions for engineering survey..."
-questions = [
+engineering_survey_questions = [
{
content: "How satisfied are you with the current development process?",
question_type: "rating",
@@ -86,10 +99,12 @@
required: false,
options: ["Jira", "Trello", "Asana", "GitHub Projects", "ClickUp", "Monday.com", "Notion", "Linear", "Other"]
}
-]
+].map { |attrs| Question.create!(attrs) }
-questions.each do |question_attrs|
- engineering_survey.questions.create!(question_attrs)
+engineering_survey_branches.each do |branch|
+ engineering_survey_questions.each do |question|
+ BranchQuestion.create!(survey_branch: branch, question: question)
+ end
end
# Create project retrospective survey
@@ -99,6 +114,12 @@
description: "Help us understand what went well and what could be improved in our recent project."
)
+# Create branches for project-involved roles (exclude pure design roles for this survey)
+project_roles = roles.reject { |role| role.name.downcase.include?('designer') }
+project_retrospective_survey_branches = project_roles.map do |role|
+ SurveyBranch.create!(survey: retrospective_survey, role: role, name: "#{role.name} Branch")
+end
+
# Create questions for the retrospective survey
puts "Creating questions for retrospective survey..."
retro_questions = [
@@ -166,10 +187,12 @@
required: false,
options: ["Technical skills", "Communication", "Problem-solving", "Time management", "Leadership", "Collaboration", "Adaptability", "Domain knowledge"]
}
-]
+].map { |attrs| Question.create!(attrs) }
-retro_questions.each do |question_attrs|
- retrospective_survey.questions.create!(question_attrs)
+project_retrospective_survey_branches.each do |branch|
+ retro_questions.each do |question|
+ BranchQuestion.create!(survey_branch: branch, question: question)
+ end
end
# Create customer feedback survey
@@ -179,6 +202,17 @@
description: "We value your feedback! Please help us improve our products and services."
)
+# Create branches for customer-facing roles
+customer_facing_roles = roles.select { |role|
+ role.name.downcase.include?('product') ||
+ role.name.downcase.include?('frontend') ||
+ role.name.downcase.include?('ux') ||
+ role.name.downcase.include?('ui')
+}
+customer_survey_branches = customer_facing_roles.map do |role|
+ SurveyBranch.create!(survey: customer_survey, role: role, name: "#{role.name} Branch")
+end
+
# Create questions for the customer survey
puts "Creating questions for customer survey..."
customer_questions = [
@@ -234,10 +268,81 @@
required: false,
options: ["Competitor A", "Competitor B", "Competitor C", "Competitor D", "None", "Other"]
}
-]
+].map { |attrs| Question.create!(attrs) }
+
+customer_survey_branches.each do |branch|
+ customer_questions.each do |question|
+ BranchQuestion.create!(survey_branch: branch, question: question)
+ end
+end
+
+# Create QA-specific survey
+puts "Creating QA and testing survey..."
+qa_survey = Survey.create!(
+ title: "Quality Assurance & Testing Practices Survey",
+ description: "Help us understand our testing processes and quality assurance practices."
+)
+
+# Create branches only for QA roles
+qa_roles = roles.select { |role| role.name.downcase.include?('qa') || role.name.downcase.include?('engineer') }
+qa_survey_branches = qa_roles.map do |role|
+ SurveyBranch.create!(survey: qa_survey, role: role, name: "#{role.name} Branch")
+end
+
+# Create QA-specific questions
+puts "Creating questions for QA survey..."
+qa_questions = [
+ {
+ content: "How confident are you in our current testing coverage?",
+ question_type: "rating",
+ position: 1,
+ required: true
+ },
+ {
+ content: "Which types of testing do you regularly perform?",
+ question_type: "checkbox",
+ position: 2,
+ required: true,
+ options: ["Unit Testing", "Integration Testing", "End-to-End Testing", "Manual Testing", "Performance Testing", "Security Testing", "Accessibility Testing", "API Testing"]
+ },
+ {
+ content: "What testing tools do you use most frequently?",
+ question_type: "checkbox",
+ position: 3,
+ required: false,
+ options: ["Jest", "Cypress", "Selenium", "Postman", "JMeter", "SonarQube", "TestRail", "Jira", "Other"]
+ },
+ {
+ content: "How would you rate the quality of our bug reporting process?",
+ question_type: "rating",
+ position: 4,
+ required: true
+ },
+ {
+ content: "What are the biggest challenges in maintaining test quality?",
+ question_type: "long_text",
+ position: 5,
+ required: false
+ },
+ {
+ content: "How often do you encounter flaky tests?",
+ question_type: "multiple_choice",
+ position: 6,
+ required: true,
+ options: ["Never", "Rarely", "Sometimes", "Often", "Very often"]
+ },
+ {
+ content: "What improvements would help you be more effective in testing?",
+ question_type: "text",
+ position: 7,
+ required: false
+ }
+].map { |attrs| Question.create!(attrs) }
-customer_questions.each do |question_attrs|
- customer_survey.questions.create!(question_attrs)
+qa_survey_branches.each do |branch|
+ qa_questions.each do |question|
+ BranchQuestion.create!(survey_branch: branch, question: question)
+ end
end
puts "Seed data created successfully!"
diff --git a/mise.local.toml b/mise.local.toml
new file mode 100644
index 0000000..d94afe5
--- /dev/null
+++ b/mise.local.toml
@@ -0,0 +1,2 @@
+[tools]
+ruby = "3.1.3"
diff --git a/package-lock.json b/package-lock.json
index e48c9a4..fc41494 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,8 +8,10 @@
"dependencies": {
"@hotwired/stimulus": "^3.2.2",
"@hotwired/turbo-rails": "^8.0.0",
+ "chart.js": "^4.5.0",
"esbuild": "^0.19.11",
"react": "^18.2.0",
+ "react-chartjs-2": "^5.3.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
@@ -20,6 +22,70 @@
"tailwindcss": "^4.0.15"
}
},
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz",
+ "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz",
+ "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz",
+ "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz",
+ "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/@esbuild/darwin-arm64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz",
@@ -27,6 +93,23 @@
"cpu": [
"arm64"
],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz",
+ "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
"optional": true,
"os": [
"darwin"
@@ -35,34 +118,379 @@
"node": ">=12"
}
},
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz",
+ "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz",
+ "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz",
+ "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz",
+ "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz",
+ "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz",
+ "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==",
+ "cpu": [
+ "loong64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz",
+ "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==",
+ "cpu": [
+ "mips64el"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz",
+ "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz",
+ "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz",
+ "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz",
+ "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz",
+ "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz",
+ "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz",
+ "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz",
+ "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz",
+ "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz",
+ "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/@hotwired/stimulus": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@hotwired/stimulus/-/stimulus-3.2.2.tgz",
- "integrity": "sha512-eGeIqNOQpXoPAIP7tC1+1Yc1yl1xnwYqg+3mzqxyrbE5pg5YFBZcA6YoTiByJB6DKAEsiWtl6tjTJS4IYtbB7A=="
+ "integrity": "sha512-eGeIqNOQpXoPAIP7tC1+1Yc1yl1xnwYqg+3mzqxyrbE5pg5YFBZcA6YoTiByJB6DKAEsiWtl6tjTJS4IYtbB7A==",
+ "license": "MIT"
},
"node_modules/@hotwired/turbo": {
"version": "8.0.13",
"resolved": "https://registry.npmjs.org/@hotwired/turbo/-/turbo-8.0.13.tgz",
"integrity": "sha512-M7qXUqcGab6G5PKOiwhgbByTtrPgKPFCTMNQ52QhzUEXEqmp0/ApEguUesh/FPiUjrmFec+3lq98KsWnYY2C7g==",
+ "license": "MIT",
"engines": {
"node": ">= 14"
}
},
"node_modules/@hotwired/turbo-rails": {
- "version": "8.0.13",
- "resolved": "https://registry.npmjs.org/@hotwired/turbo-rails/-/turbo-rails-8.0.13.tgz",
- "integrity": "sha512-6SCnnOSzhtaJ0pNkAjncZxjtKsK3sP/vPEkCnTXBXSHkr+vF7DTZkOlwjhms1DbbQNTsjCsBoKvzSMbh/omSCQ==",
+ "version": "8.0.16",
+ "resolved": "https://registry.npmjs.org/@hotwired/turbo-rails/-/turbo-rails-8.0.16.tgz",
+ "integrity": "sha512-Yxiy2x+N3eOIEDokvLzSrd08aI5RDKnFYDQFl2J/LuMEWTtPdY7oNP0F/gv/sSe5AV23Lwz4FitG/uNFXNM5tA==",
+ "license": "MIT",
"dependencies": {
"@hotwired/turbo": "^8.0.13",
- "@rails/actioncable": "^7.0"
+ "@rails/actioncable": ">=7.0"
+ }
+ },
+ "node_modules/@isaacs/fs-minipass": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
+ "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^7.0.4"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
}
},
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.30",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz",
+ "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@kurkle/color": {
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
+ "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
+ "license": "MIT"
+ },
"node_modules/@parcel/watcher": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
"integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
"dev": true,
"hasInstallScript": true,
+ "license": "MIT",
"dependencies": {
"detect-libc": "^1.0.3",
"is-glob": "^4.0.3",
@@ -92,104 +520,582 @@
"@parcel/watcher-win32-x64": "2.5.1"
}
},
- "node_modules/@parcel/watcher-darwin-arm64": {
- "version": "2.5.1",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
- "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
+ "node_modules/@parcel/watcher-android-arm64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz",
+ "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-darwin-arm64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
+ "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-darwin-x64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz",
+ "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-freebsd-x64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz",
+ "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm-glibc": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz",
+ "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm-musl": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz",
+ "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm64-glibc": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz",
+ "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm64-musl": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz",
+ "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-x64-glibc": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
+ "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-x64-musl": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz",
+ "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-win32-arm64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz",
+ "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-win32-ia32": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz",
+ "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-win32-x64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
+ "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@rails/actioncable": {
+ "version": "8.0.201",
+ "resolved": "https://registry.npmjs.org/@rails/actioncable/-/actioncable-8.0.201.tgz",
+ "integrity": "sha512-WiXZodvnK7u+wlu72DZydfV75x14HhzXI84sto9xcdsW1DMOHK+jYwQuuE/Wh/hKH5yajFIw/3DUP6MHDeGrbA==",
+ "license": "MIT"
+ },
+ "node_modules/@tailwindcss/cli": {
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.1.13.tgz",
+ "integrity": "sha512-KEu/iL4CYBzGza/2yZBLXqjCCZB/eRWkRLP8Vg2kkEWk4usC8HLGJW0QAhLS7U5DsAWumsisxgabuppE6NinLw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@parcel/watcher": "^2.5.1",
+ "@tailwindcss/node": "4.1.13",
+ "@tailwindcss/oxide": "4.1.13",
+ "enhanced-resolve": "^5.18.3",
+ "mri": "^1.2.0",
+ "picocolors": "^1.1.1",
+ "tailwindcss": "4.1.13"
+ },
+ "bin": {
+ "tailwindcss": "dist/index.mjs"
+ }
+ },
+ "node_modules/@tailwindcss/node": {
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.13.tgz",
+ "integrity": "sha512-eq3ouolC1oEFOAvOMOBAmfCIqZBJuvWvvYWh5h5iOYfe1HFC6+GZ6EIL0JdM3/niGRJmnrOc+8gl9/HGUaaptw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.4",
+ "enhanced-resolve": "^5.18.3",
+ "jiti": "^2.5.1",
+ "lightningcss": "1.30.1",
+ "magic-string": "^0.30.18",
+ "source-map-js": "^1.2.1",
+ "tailwindcss": "4.1.13"
+ }
+ },
+ "node_modules/@tailwindcss/oxide": {
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.13.tgz",
+ "integrity": "sha512-CPgsM1IpGRa880sMbYmG1s4xhAy3xEt1QULgTJGQmZUeNgXFR7s1YxYygmJyBGtou4SyEosGAGEeYqY7R53bIA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "detect-libc": "^2.0.4",
+ "tar": "^7.4.3"
+ },
+ "engines": {
+ "node": ">= 10"
+ },
+ "optionalDependencies": {
+ "@tailwindcss/oxide-android-arm64": "4.1.13",
+ "@tailwindcss/oxide-darwin-arm64": "4.1.13",
+ "@tailwindcss/oxide-darwin-x64": "4.1.13",
+ "@tailwindcss/oxide-freebsd-x64": "4.1.13",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.13",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.1.13",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.1.13",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.1.13",
+ "@tailwindcss/oxide-linux-x64-musl": "4.1.13",
+ "@tailwindcss/oxide-wasm32-wasi": "4.1.13",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.1.13",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.1.13"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-android-arm64": {
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.13.tgz",
+ "integrity": "sha512-BrpTrVYyejbgGo57yc8ieE+D6VT9GOgnNdmh5Sac6+t0m+v+sKQevpFVpwX3pBrM2qKrQwJ0c5eDbtjouY/+ew==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-arm64": {
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.13.tgz",
+ "integrity": "sha512-YP+Jksc4U0KHcu76UhRDHq9bx4qtBftp9ShK/7UGfq0wpaP96YVnnjFnj3ZFrUAjc5iECzODl/Ts0AN7ZPOANQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-x64": {
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.13.tgz",
+ "integrity": "sha512-aAJ3bbwrn/PQHDxCto9sxwQfT30PzyYJFG0u/BWZGeVXi5Hx6uuUOQEI2Fa43qvmUjTRQNZnGqe9t0Zntexeuw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-freebsd-x64": {
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.13.tgz",
+ "integrity": "sha512-Wt8KvASHwSXhKE/dJLCCWcTSVmBj3xhVhp/aF3RpAhGeZ3sVo7+NTfgiN8Vey/Fi8prRClDs6/f0KXPDTZE6nQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.13.tgz",
+ "integrity": "sha512-mbVbcAsW3Gkm2MGwA93eLtWrwajz91aXZCNSkGTx/R5eb6KpKD5q8Ueckkh9YNboU8RH7jiv+ol/I7ZyQ9H7Bw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.13.tgz",
+ "integrity": "sha512-wdtfkmpXiwej/yoAkrCP2DNzRXCALq9NVLgLELgLim1QpSfhQM5+ZxQQF8fkOiEpuNoKLp4nKZ6RC4kmeFH0HQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.13.tgz",
+ "integrity": "sha512-hZQrmtLdhyqzXHB7mkXfq0IYbxegaqTmfa1p9MBj72WPoDD3oNOh1Lnxf6xZLY9C3OV6qiCYkO1i/LrzEdW2mg==",
"cpu": [
"arm64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
- "darwin"
+ "linux"
],
"engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
+ "node": ">= 10"
}
},
- "node_modules/@rails/actioncable": {
- "version": "7.2.201",
- "resolved": "https://registry.npmjs.org/@rails/actioncable/-/actioncable-7.2.201.tgz",
- "integrity": "sha512-wsTdWoZ5EfG5k3t7ORdyQF0ZmDEgN4aVPCanHAiNEwCROqibSZMXXmCbH7IDJUVri4FOeAVwwbPINI7HVHPKBw=="
+ "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.13.tgz",
+ "integrity": "sha512-uaZTYWxSXyMWDJZNY1Ul7XkJTCBRFZ5Fo6wtjrgBKzZLoJNrG+WderJwAjPzuNZOnmdrVg260DKwXCFtJ/hWRQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
},
- "node_modules/@tailwindcss/cli": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.1.1.tgz",
- "integrity": "sha512-e+k52zGEjaClimmt/85UwF43NIvB0r0PLXRId2QYUwmbExuU3XPhfBzitdrEKZOTM1goH1Vs4wrNxnqDn8ITQA==",
+ "node_modules/@tailwindcss/oxide-linux-x64-musl": {
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.13.tgz",
+ "integrity": "sha512-oXiPj5mi4Hdn50v5RdnuuIms0PVPI/EG4fxAfFiIKQh5TgQgX7oSuDWntHW7WNIi/yVLAiS+CRGW4RkoGSSgVQ==",
+ "cpu": [
+ "x64"
+ ],
"dev": true,
- "dependencies": {
- "@parcel/watcher": "^2.5.1",
- "@tailwindcss/node": "4.1.1",
- "@tailwindcss/oxide": "4.1.1",
- "enhanced-resolve": "^5.18.1",
- "mri": "^1.2.0",
- "picocolors": "^1.1.1",
- "tailwindcss": "4.1.1"
- },
- "bin": {
- "tailwindcss": "dist/index.mjs"
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
}
},
- "node_modules/@tailwindcss/node": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.1.tgz",
- "integrity": "sha512-xvlh4pvfG/bkv0fEtJDABAm1tjtSmSyi2QmS4zyj1EKNI1UiOYiUq1IphSwDsNJ5vJ9cWEGs4rJXpUdCN2kujQ==",
+ "node_modules/@tailwindcss/oxide-wasm32-wasi": {
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.13.tgz",
+ "integrity": "sha512-+LC2nNtPovtrDwBc/nqnIKYh/W2+R69FA0hgoeOn64BdCX522u19ryLh3Vf3F8W49XBcMIxSe665kwy21FkhvA==",
+ "bundleDependencies": [
+ "@napi-rs/wasm-runtime",
+ "@emnapi/core",
+ "@emnapi/runtime",
+ "@tybys/wasm-util",
+ "@emnapi/wasi-threads",
+ "tslib"
+ ],
+ "cpu": [
+ "wasm32"
+ ],
"dev": true,
+ "license": "MIT",
+ "optional": true,
"dependencies": {
- "enhanced-resolve": "^5.18.1",
- "jiti": "^2.4.2",
- "lightningcss": "1.29.2",
- "tailwindcss": "4.1.1"
+ "@emnapi/core": "^1.4.5",
+ "@emnapi/runtime": "^1.4.5",
+ "@emnapi/wasi-threads": "^1.0.4",
+ "@napi-rs/wasm-runtime": "^0.2.12",
+ "@tybys/wasm-util": "^0.10.0",
+ "tslib": "^2.8.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
}
},
- "node_modules/@tailwindcss/oxide": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.1.tgz",
- "integrity": "sha512-7+YBgnPQ4+jv6B6WVOerJ6WOzDzNJXrRKDts674v6TKAqFqYRr9+EBtSziO7nNcwQ8JtoZNMeqA+WJDjtCM/7w==",
+ "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.13.tgz",
+ "integrity": "sha512-dziTNeQXtoQ2KBXmrjCxsuPk3F3CQ/yb7ZNZNA+UkNTeiTGgfeh+gH5Pi7mRncVgcPD2xgHvkFCh/MhZWSgyQg==",
+ "cpu": [
+ "arm64"
+ ],
"dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
"engines": {
"node": ">= 10"
- },
- "optionalDependencies": {
- "@tailwindcss/oxide-android-arm64": "4.1.1",
- "@tailwindcss/oxide-darwin-arm64": "4.1.1",
- "@tailwindcss/oxide-darwin-x64": "4.1.1",
- "@tailwindcss/oxide-freebsd-x64": "4.1.1",
- "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.1",
- "@tailwindcss/oxide-linux-arm64-gnu": "4.1.1",
- "@tailwindcss/oxide-linux-arm64-musl": "4.1.1",
- "@tailwindcss/oxide-linux-x64-gnu": "4.1.1",
- "@tailwindcss/oxide-linux-x64-musl": "4.1.1",
- "@tailwindcss/oxide-win32-arm64-msvc": "4.1.1",
- "@tailwindcss/oxide-win32-x64-msvc": "4.1.1"
}
},
- "node_modules/@tailwindcss/oxide-darwin-arm64": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.1.tgz",
- "integrity": "sha512-dI0QbdMWBvLB3MtaTKetzUKG9CUUQow8JSP4Nm+OxVokeZ+N+f1OmZW/hW1LzMxpx9RQCBgSRL+IIvKRat5Wdg==",
+ "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.13.tgz",
+ "integrity": "sha512-3+LKesjXydTkHk5zXX01b5KMzLV1xl2mcktBJkje7rhFUpUlYJy7IMOLqjIRQncLTa1WZZiFY/foAeB5nmaiTw==",
"cpu": [
- "arm64"
+ "x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
- "darwin"
+ "win32"
],
"engines": {
"node": ">= 10"
}
},
+ "node_modules/@tailwindcss/oxide/node_modules/detect-libc": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
+ "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=8"
}
@@ -199,6 +1105,7 @@
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
@@ -228,6 +1135,7 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "license": "MIT",
"dependencies": {
"browserslist": "^4.24.4",
"caniuse-lite": "^1.0.30001702",
@@ -251,6 +1159,7 @@
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"fill-range": "^7.1.1"
},
@@ -259,9 +1168,9 @@
}
},
"node_modules/browserslist": {
- "version": "4.24.4",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz",
- "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==",
+ "version": "4.25.4",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.4.tgz",
+ "integrity": "sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==",
"dev": true,
"funding": [
{
@@ -277,11 +1186,12 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "license": "MIT",
"dependencies": {
- "caniuse-lite": "^1.0.30001688",
- "electron-to-chromium": "^1.5.73",
+ "caniuse-lite": "^1.0.30001737",
+ "electron-to-chromium": "^1.5.211",
"node-releases": "^2.0.19",
- "update-browserslist-db": "^1.1.1"
+ "update-browserslist-db": "^1.1.3"
},
"bin": {
"browserslist": "cli.js"
@@ -291,9 +1201,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001707",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz",
- "integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==",
+ "version": "1.0.30001741",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001741.tgz",
+ "integrity": "sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==",
"dev": true,
"funding": [
{
@@ -308,13 +1218,15 @@
"type": "github",
"url": "https://github.com/sponsors/ai"
}
- ]
+ ],
+ "license": "CC-BY-4.0"
},
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
@@ -331,6 +1243,7 @@
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
@@ -338,11 +1251,34 @@
"node": ">=8"
}
},
+ "node_modules/chart.js": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz",
+ "integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@kurkle/color": "^0.3.0"
+ },
+ "engines": {
+ "pnpm": ">=8"
+ }
+ },
+ "node_modules/chownr": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
+ "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"dev": true,
+ "license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.1",
@@ -357,6 +1293,7 @@
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
@@ -368,21 +1305,22 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/concurrently": {
- "version": "9.1.2",
- "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.1.2.tgz",
- "integrity": "sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==",
+ "version": "9.2.1",
+ "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz",
+ "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "chalk": "^4.1.2",
- "lodash": "^4.17.21",
- "rxjs": "^7.8.1",
- "shell-quote": "^1.8.1",
- "supports-color": "^8.1.1",
- "tree-kill": "^1.2.2",
- "yargs": "^17.7.2"
+ "chalk": "4.1.2",
+ "rxjs": "7.8.2",
+ "shell-quote": "1.8.3",
+ "supports-color": "8.1.1",
+ "tree-kill": "1.2.2",
+ "yargs": "17.7.2"
},
"bin": {
"conc": "dist/bin/concurrently.js",
@@ -400,6 +1338,7 @@
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
"dev": true,
+ "license": "Apache-2.0",
"bin": {
"detect-libc": "bin/detect-libc.js"
},
@@ -408,22 +1347,25 @@
}
},
"node_modules/electron-to-chromium": {
- "version": "1.5.129",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.129.tgz",
- "integrity": "sha512-JlXUemX4s0+9f8mLqib/bHH8gOHf5elKS6KeWG3sk3xozb/JTq/RLXIv8OKUWiK4Ah00Wm88EFj5PYkFr4RUPA==",
- "dev": true
+ "version": "1.5.214",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.214.tgz",
+ "integrity": "sha512-TpvUNdha+X3ybfU78NoQatKvQEm1oq3lf2QbnmCEdw+Bd9RuIAY+hJTvq1avzHM0f7EJfnH3vbCnbzKzisc/9Q==",
+ "dev": true,
+ "license": "ISC"
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/enhanced-resolve": {
- "version": "5.18.1",
- "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
- "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==",
+ "version": "5.18.3",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
+ "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.4",
"tapable": "^2.2.0"
@@ -437,6 +1379,7 @@
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz",
"integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==",
"hasInstallScript": true,
+ "license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
@@ -474,6 +1417,7 @@
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=6"
}
@@ -483,6 +1427,7 @@
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"to-regex-range": "^5.0.1"
},
@@ -495,6 +1440,7 @@
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
"integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": "*"
},
@@ -508,6 +1454,7 @@
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true,
+ "license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
@@ -516,13 +1463,15 @@
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
- "dev": true
+ "dev": true,
+ "license": "ISC"
},
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=8"
}
@@ -532,6 +1481,7 @@
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=0.10.0"
}
@@ -541,6 +1491,7 @@
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=8"
}
@@ -550,6 +1501,7 @@
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"is-extglob": "^2.1.1"
},
@@ -562,15 +1514,17 @@
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/jiti": {
- "version": "2.4.2",
- "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
- "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==",
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz",
+ "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==",
"dev": true,
+ "license": "MIT",
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
@@ -578,13 +1532,15 @@
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
},
"node_modules/lightningcss": {
- "version": "1.29.2",
- "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz",
- "integrity": "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==",
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
+ "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==",
"dev": true,
+ "license": "MPL-2.0",
"dependencies": {
"detect-libc": "^2.0.3"
},
@@ -596,26 +1552,48 @@
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
- "lightningcss-darwin-arm64": "1.29.2",
- "lightningcss-darwin-x64": "1.29.2",
- "lightningcss-freebsd-x64": "1.29.2",
- "lightningcss-linux-arm-gnueabihf": "1.29.2",
- "lightningcss-linux-arm64-gnu": "1.29.2",
- "lightningcss-linux-arm64-musl": "1.29.2",
- "lightningcss-linux-x64-gnu": "1.29.2",
- "lightningcss-linux-x64-musl": "1.29.2",
- "lightningcss-win32-arm64-msvc": "1.29.2",
- "lightningcss-win32-x64-msvc": "1.29.2"
+ "lightningcss-darwin-arm64": "1.30.1",
+ "lightningcss-darwin-x64": "1.30.1",
+ "lightningcss-freebsd-x64": "1.30.1",
+ "lightningcss-linux-arm-gnueabihf": "1.30.1",
+ "lightningcss-linux-arm64-gnu": "1.30.1",
+ "lightningcss-linux-arm64-musl": "1.30.1",
+ "lightningcss-linux-x64-gnu": "1.30.1",
+ "lightningcss-linux-x64-musl": "1.30.1",
+ "lightningcss-win32-arm64-msvc": "1.30.1",
+ "lightningcss-win32-x64-msvc": "1.30.1"
}
},
"node_modules/lightningcss-darwin-arm64": {
- "version": "1.29.2",
- "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.2.tgz",
- "integrity": "sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==",
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz",
+ "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==",
"cpu": [
"arm64"
],
"dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz",
+ "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
@@ -628,25 +1606,189 @@
"url": "https://opencollective.com/parcel"
}
},
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz",
+ "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz",
+ "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz",
+ "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz",
+ "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz",
+ "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz",
+ "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz",
+ "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz",
+ "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
"node_modules/lightningcss/node_modules/detect-libc": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
- "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
+ "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
"dev": true,
+ "license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
- "node_modules/lodash": {
- "version": "4.17.21",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
- "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
- "dev": true
- },
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
@@ -654,11 +1796,22 @@
"loose-envify": "cli.js"
}
},
+ "node_modules/magic-string": {
+ "version": "0.30.18",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.18.tgz",
+ "integrity": "sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"braces": "^3.0.3",
"picomatch": "^2.3.1"
@@ -667,11 +1820,51 @@
"node": ">=8.6"
}
},
+ "node_modules/minipass": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/minizlib": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz",
+ "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "minipass": "^7.1.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/mkdirp": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
+ "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "mkdirp": "dist/cjs/src/bin.js"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/mri": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
"integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=4"
}
@@ -687,6 +1880,7 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@@ -698,19 +1892,22 @@
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/node-releases": {
- "version": "2.0.19",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
- "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
- "dev": true
+ "version": "2.0.20",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.20.tgz",
+ "integrity": "sha512-7gK6zSXEH6neM212JgfYFXe+GmZQM+fia5SsusuBIUgnPheLFBmIPhtFoAQRj8/7wASYQnbDlHPVwY0BefoFgA==",
+ "dev": true,
+ "license": "MIT"
},
"node_modules/normalize-range": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
"integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=0.10.0"
}
@@ -719,13 +1916,15 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
- "dev": true
+ "dev": true,
+ "license": "ISC"
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=8.6"
},
@@ -734,9 +1933,9 @@
}
},
"node_modules/postcss": {
- "version": "8.5.3",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
- "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"dev": true,
"funding": [
{
@@ -752,8 +1951,9 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "license": "MIT",
"dependencies": {
- "nanoid": "^3.3.8",
+ "nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@@ -765,12 +1965,14 @@
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/react": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -778,10 +1980,21 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-chartjs-2": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz",
+ "integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "chart.js": "^4.1.1",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@@ -795,6 +2008,7 @@
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=0.10.0"
}
@@ -804,6 +2018,7 @@
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
"dev": true,
+ "license": "Apache-2.0",
"dependencies": {
"tslib": "^2.1.0"
}
@@ -812,15 +2027,17 @@
"version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
}
},
"node_modules/shell-quote": {
- "version": "1.8.2",
- "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz",
- "integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==",
+ "version": "1.8.3",
+ "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
+ "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">= 0.4"
},
@@ -833,6 +2050,7 @@
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
+ "license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
@@ -842,6 +2060,7 @@
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
@@ -856,6 +2075,7 @@
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
@@ -868,6 +2088,7 @@
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
@@ -879,18 +2100,42 @@
}
},
"node_modules/tailwindcss": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.1.tgz",
- "integrity": "sha512-QNbdmeS979Efzim2g/bEvfuh+fTcIdp1y7gA+sb6OYSW74rt7Cr7M78AKdf6HqWT3d5AiTb7SwTT3sLQxr4/qw==",
- "dev": true
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz",
+ "integrity": "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==",
+ "dev": true,
+ "license": "MIT"
},
"node_modules/tapable": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
- "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz",
+ "integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/tar": {
+ "version": "7.4.3",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz",
+ "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@isaacs/fs-minipass": "^4.0.0",
+ "chownr": "^3.0.0",
+ "minipass": "^7.1.2",
+ "minizlib": "^3.0.1",
+ "mkdirp": "^3.0.1",
+ "yallist": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
}
},
"node_modules/to-regex-range": {
@@ -898,6 +2143,7 @@
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"is-number": "^7.0.0"
},
@@ -910,6 +2156,7 @@
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
"dev": true,
+ "license": "MIT",
"bin": {
"tree-kill": "cli.js"
}
@@ -918,7 +2165,8 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
- "dev": true
+ "dev": true,
+ "license": "0BSD"
},
"node_modules/update-browserslist-db": {
"version": "1.1.3",
@@ -939,6 +2187,7 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "license": "MIT",
"dependencies": {
"escalade": "^3.2.0",
"picocolors": "^1.1.1"
@@ -955,6 +2204,7 @@
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
@@ -972,15 +2222,27 @@
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"dev": true,
+ "license": "ISC",
"engines": {
"node": ">=10"
}
},
+ "node_modules/yallist": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
+ "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/yargs": {
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"cliui": "^8.0.1",
"escalade": "^3.1.1",
@@ -999,6 +2261,7 @@
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"dev": true,
+ "license": "ISC",
"engines": {
"node": ">=12"
}
diff --git a/package.json b/package.json
index b64ad04..7a6b5f7 100644
--- a/package.json
+++ b/package.json
@@ -4,22 +4,24 @@
"dependencies": {
"@hotwired/stimulus": "^3.2.2",
"@hotwired/turbo-rails": "^8.0.0",
+ "chart.js": "^4.5.0",
"esbuild": "^0.19.11",
"react": "^18.2.0",
+ "react-chartjs-2": "^5.3.0",
"react-dom": "^18.2.0"
},
"scripts": {
- "build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=/assets",
+ "build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=/assets --loader:.js=jsx --loader:.jsx=jsx",
"build:css": "npx @tailwindcss/cli -i ./app/assets/stylesheets/application.tailwind.css -o ./app/assets/builds/application.css --minify",
- "watch": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=/assets --watch",
+ "watch": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=/assets --watch --loader:.js=jsx --loader:.jsx=jsx",
"watch:css": "npx @tailwindcss/cli -i ./app/assets/stylesheets/application.tailwind.css -o ./app/assets/builds/application.css --watch",
"dev": "concurrently \"npm run watch\" \"npm run watch:css\""
},
"devDependencies": {
+ "@tailwindcss/cli": "^4.0.15",
"autoprefixer": "^10.4.21",
"concurrently": "^9.1.2",
"postcss": "^8.5.3",
- "@tailwindcss/cli": "^4.0.15",
"tailwindcss": "^4.0.15"
}
}
diff --git a/test/controllers/branch_analytics_controller_test.rb b/test/controllers/branch_analytics_controller_test.rb
new file mode 100644
index 0000000..e0ed6ef
--- /dev/null
+++ b/test/controllers/branch_analytics_controller_test.rb
@@ -0,0 +1,111 @@
+require "test_helper"
+
+class BranchAnalyticsControllerTest < ActionDispatch::IntegrationTest
+ setup do
+ @survey = surveys(:one)
+ @question = questions(:one)
+ @branch = survey_branches(:one)
+
+ # Create some test responses
+ 3.times do |i|
+ Response.create!(
+ survey: @survey,
+ question: @question,
+ survey_branch: @branch,
+ value: "Test response #{i + 1}"
+ )
+ end
+
+ # Soft delete some items for testing
+ @survey.responses.limit(1).each(&:destroy)
+ @survey.questions.limit(1).each(&:destroy)
+ end
+
+ test "should show analytics with active data only" do
+ get survey_analytics_path(@survey)
+ assert_response :success
+
+ # Should show only active data counts
+ assert_select "dd", text: /#{@survey.responses.count}/
+ assert_select "dd", text: /#{@survey.questions.count}/
+
+ # Should not show deleted indicators
+ assert_select "span.text-red-500", count: 0
+ end
+
+ test "should show analytics with deleted data included" do
+ get survey_analytics_path(@survey, show_deleted: true)
+ assert_response :success
+
+ # Should show total data counts including deleted
+ total_responses = @survey.responses.with_deleted.count
+ total_questions = @survey.questions.with_deleted.count
+
+ assert_select "dd", text: /#{total_responses}/
+ assert_select "dd", text: /#{total_questions}/
+
+ # Should show deleted indicators if there are deleted items
+ if @survey.responses.only_deleted.count > 0
+ assert_select "span.text-red-500", text: /deleted/
+ end
+ end
+
+ test "should show deletion analytics section when viewing deleted data" do
+ get survey_analytics_path(@survey, show_deleted: true)
+ assert_response :success
+
+ # Should show deletion analytics section
+ assert_select "h3", text: "Deletion Analytics"
+ assert_select "h4", text: "Recent Deletions"
+ assert_select "h4", text: "Deletion Patterns"
+ end
+
+ test "should not show deletion analytics section when viewing active data only" do
+ get survey_analytics_path(@survey)
+ assert_response :success
+
+ # Should not show deletion analytics section
+ assert_select "h3", text: "Deletion Analytics", count: 0
+ end
+
+ test "analytics tabs should work correctly" do
+ get survey_analytics_path(@survey)
+ assert_response :success
+
+ # Should show "Active Data Only" as active tab
+ assert_select "a.border-indigo-500", text: "Active Data Only"
+ assert_select "a.border-transparent", text: "Include Deleted Data"
+
+ get survey_analytics_path(@survey, show_deleted: true)
+ assert_response :success
+
+ # Should show "Include Deleted Data" as active tab
+ assert_select "a.border-indigo-500", text: "Include Deleted Data"
+ assert_select "a.border-transparent", text: "Active Data Only"
+ end
+
+ test "should handle surveys with no deleted data gracefully" do
+ # Create a clean survey with no deleted data
+ clean_survey = Survey.create!(title: "Clean Survey", description: "No deleted data")
+ Role.find_each do |role|
+ SurveyBranch.create!(survey: clean_survey, role: role, name: "#{role.name} Branch")
+ end
+
+ get survey_analytics_path(clean_survey, show_deleted: true)
+ assert_response :success
+
+ # Should show zero deleted counts
+ assert_select "span.text-red-500", count: 0
+ end
+
+ test "should calculate response rates correctly with deleted data" do
+ get survey_analytics_path(@survey, show_deleted: true)
+ assert_response :success
+
+ # Response rate should use the correct scope
+ total_possible = @survey.survey_branches.with_deleted.joins(:branch_questions).count
+ expected_rate = total_possible.zero? ? 0 : (@survey.responses.with_deleted.count.to_f / total_possible * 100).round(2)
+
+ assert_select "dd", text: "#{expected_rate}%"
+ end
+end
\ No newline at end of file
diff --git a/test/controllers/questions_controller_test.rb b/test/controllers/questions_controller_test.rb
index 0ea6019..e7dd350 100644
--- a/test/controllers/questions_controller_test.rb
+++ b/test/controllers/questions_controller_test.rb
@@ -1,28 +1,234 @@
+
require "test_helper"
class QuestionsControllerTest < ActionDispatch::IntegrationTest
- test "should get new" do
- get questions_new_url
+ setup do
+ @survey = surveys(:one)
+ @question = questions(:one)
+ @role = roles(:one)
+ end
+
+ test "should get index as html" do
+ get survey_questions_url(@survey)
+ assert_response :redirect
+ assert_redirected_to survey_path(@survey)
+ assert_equal "text/html", @response.media_type
+ end
+
+ test "should get index as json" do
+ get survey_questions_url(@survey, format: :json)
assert_response :success
+ assert_equal "application/json", @response.media_type
+ body = JSON.parse(@response.body)
+ assert body.is_a?(Array)
end
- test "should get create" do
- get questions_create_url
+ test "should get new" do
+ get new_survey_question_url(@survey)
assert_response :success
end
+ test "should create question" do
+ assert_difference('Question.count') do
+ post survey_questions_url(@survey), params: { question: { content: 'Test?', question_type: 'text' }, role: @role.name }
+ end
+ assert_redirected_to survey_path(@survey)
+ end
+
+ test "should create text question via controller" do
+ assert_difference('Question.count') do
+ post survey_questions_url(@survey), params: {
+ question: {
+ content: 'What is your name?',
+ question_type: 'text',
+ position: 1,
+ required: true
+ },
+ role: @role.name
+ }
+ end
+
+ question = Question.last
+ assert_equal 'What is your name?', question.content
+ assert_equal 'text', question.question_type
+ assert_equal 1, question.position
+ assert question.required?
+ assert_redirected_to survey_path(@survey)
+ end
+
+ test "should create long_text question via controller" do
+ assert_difference('Question.count') do
+ post survey_questions_url(@survey), params: {
+ question: {
+ content: 'Please describe your experience:',
+ question_type: 'long_text',
+ position: 2
+ },
+ role: @role.name
+ }
+ end
+
+ question = Question.last
+ assert_equal 'Please describe your experience:', question.content
+ assert_equal 'long_text', question.question_type
+ assert_redirected_to survey_path(@survey)
+ end
+
+ test "should create multiple_choice question via controller" do
+ assert_difference('Question.count') do
+ post survey_questions_url(@survey), params: {
+ question: {
+ content: 'What is your favorite color?',
+ question_type: 'multiple_choice',
+ options: "Red\nBlue\nGreen\nYellow",
+ position: 3
+ },
+ role: @role.name
+ }
+ end
+
+ question = Question.last
+ assert_equal 'What is your favorite color?', question.content
+ assert_equal 'multiple_choice', question.question_type
+ assert_equal ['Red', 'Blue', 'Green', 'Yellow'], question.options
+ assert_redirected_to survey_path(@survey)
+ end
+
+ test "should create checkbox question via controller" do
+ assert_difference('Question.count') do
+ post survey_questions_url(@survey), params: {
+ question: {
+ content: 'Which languages do you speak?',
+ question_type: 'checkbox',
+ options: "English\nSpanish\nFrench\nGerman",
+ position: 4
+ },
+ role: @role.name
+ }
+ end
+
+ question = Question.last
+ assert_equal 'Which languages do you speak?', question.content
+ assert_equal 'checkbox', question.question_type
+ assert_equal ['English', 'Spanish', 'French', 'German'], question.options
+ assert_redirected_to survey_path(@survey)
+ end
+
+ test "should create rating question via controller" do
+ assert_difference('Question.count') do
+ post survey_questions_url(@survey), params: {
+ question: {
+ content: 'Rate your experience from 1 to 5:',
+ question_type: 'rating',
+ position: 5
+ },
+ role: @role.name
+ }
+ end
+
+ question = Question.last
+ assert_equal 'Rate your experience from 1 to 5:', question.content
+ assert_equal 'rating', question.question_type
+ assert_redirected_to survey_path(@survey)
+ end
+
+ test "should not create multiple_choice question without options" do
+ assert_no_difference('Question.count') do
+ post survey_questions_url(@survey), params: {
+ question: {
+ content: 'What is your favorite color?',
+ question_type: 'multiple_choice',
+ position: 1
+ },
+ role: @role.name
+ }
+ end
+
+ assert_response :unprocessable_entity
+ end
+
+ test "should not create checkbox question without options" do
+ assert_no_difference('Question.count') do
+ post survey_questions_url(@survey), params: {
+ question: {
+ content: 'Which languages do you speak?',
+ question_type: 'checkbox',
+ position: 1
+ },
+ role: @role.name
+ }
+ end
+
+ assert_response :unprocessable_entity
+ end
+
+ test "should create all question types successfully" do
+ Question::QUESTION_TYPES.each_with_index do |question_type, index|
+ params = {
+ question: {
+ content: "Test #{question_type} question?",
+ question_type: question_type,
+ position: index + 1
+ },
+ role: @role.name
+ }
+
+ # Add options for types that require them
+ if ['multiple_choice', 'checkbox'].include?(question_type)
+ params[:question][:options] = "Option 1\nOption 2\nOption 3"
+ end
+
+ assert_difference('Question.count') do
+ post survey_questions_url(@survey), params: params
+ end
+
+ assert_redirected_to survey_path(@survey), "Failed to create #{question_type} question"
+
+ question = Question.last
+ assert_equal question_type, question.question_type, "Question type not set correctly for #{question_type}"
+
+ if ['multiple_choice', 'checkbox'].include?(question_type)
+ assert_equal ['Option 1', 'Option 2', 'Option 3'], question.options, "Options not set correctly for #{question_type}"
+ end
+ end
+ end
+
test "should get edit" do
- get questions_edit_url
+ get edit_survey_question_url(@survey, @question)
assert_response :success
end
- test "should get update" do
- get questions_update_url
- assert_response :success
+ test "should update question" do
+ patch survey_question_url(@survey, @question), params: { question: { content: 'Updated?' } }
+ assert_redirected_to survey_path(@survey)
end
- test "should get destroy" do
- get questions_destroy_url
- assert_response :success
+ test "should soft delete question" do
+ # Should not change total count (including deleted)
+ assert_no_difference('Question.with_deleted.count') do
+ # But should decrease active count
+ assert_difference('Question.count', -1) do
+ delete survey_question_url(@survey, @question)
+ end
+ end
+
+ @question.reload
+ assert @question.deleted?
+ assert_redirected_to survey_path(@survey)
+ end
+
+ test "should restore question" do
+ # First soft delete the question
+ @question.destroy
+ assert @question.deleted?
+
+ # Then restore it
+ assert_difference('Question.count', 1) do
+ patch restore_survey_question_url(@survey, @question)
+ end
+
+ @question.reload
+ assert_not @question.deleted?
+ assert_redirected_to survey_path(@survey)
end
end
diff --git a/test/controllers/responses_controller_test.rb b/test/controllers/responses_controller_test.rb
index 0200b59..6d5c9a3 100644
--- a/test/controllers/responses_controller_test.rb
+++ b/test/controllers/responses_controller_test.rb
@@ -1,8 +1,42 @@
+
require "test_helper"
class ResponsesControllerTest < ActionDispatch::IntegrationTest
- test "should get create" do
- get responses_create_url
+ setup do
+ @survey = surveys(:one)
+ @question = questions(:one)
+ end
+
+ test "should create response with html" do
+ assert_difference('Response.count') do
+ post survey_responses_url(@survey), params: { response: { survey_id: @survey.id, question_id: @question.id, value: 'Test answer' } }
+ end
+ assert_redirected_to surveys_path
+ end
+
+ test "should create response with json" do
+ assert_difference('Response.count') do
+ post survey_responses_url(@survey, format: :json), params: { response: { survey_id: @survey.id, question_id: @question.id, value: 'Test answer' } }
+ end
assert_response :success
+ assert_equal "application/json", @response.media_type
+ end
+
+ test "should not create response with html if invalid" do
+ assert_no_difference('Response.count') do
+ post survey_responses_url(@survey), params: { response: { survey_id: @survey.id, question_id: @question.id, value: nil } }
+ end
+ assert_redirected_to take_survey_path(@survey)
+ assert_equal "There was an error recording your response.", flash[:alert]
+ end
+
+ test "should not create response with json if invalid" do
+ assert_no_difference('Response.count') do
+ post survey_responses_url(@survey, format: :json), params: { response: { survey_id: @survey.id, question_id: @question.id, value: nil } }
+ end
+ assert_response :unprocessable_entity
+ assert_equal "application/json", @response.media_type
+ body = JSON.parse(@response.body)
+ assert body["error"].present?
end
end
diff --git a/test/controllers/roles_controller_test.rb b/test/controllers/roles_controller_test.rb
new file mode 100644
index 0000000..7c5ebc2
--- /dev/null
+++ b/test/controllers/roles_controller_test.rb
@@ -0,0 +1,109 @@
+require "test_helper"
+
+class RolesControllerTest < ActionDispatch::IntegrationTest
+ setup do
+ @role = Role.create!(name: "TestRole")
+ end
+
+ test "should get index" do
+ get roles_url
+ assert_response :success
+ end
+
+ test "should get new" do
+ get new_role_url
+ assert_response :success
+ end
+
+ test "should create role" do
+ assert_difference('Role.count') do
+ post roles_url, params: { role: { name: "AnotherRole" } }
+ end
+ assert_redirected_to role_url(Role.last)
+ end
+
+ test "should not create role with invalid params" do
+ assert_no_difference('Role.count') do
+ post roles_url, params: { role: { name: "" } }
+ end
+ assert_response :unprocessable_entity
+ end
+
+ test "should show role" do
+ get role_url(@role)
+ assert_response :success
+ end
+
+ test "should get edit" do
+ get edit_role_url(@role)
+ assert_response :success
+ end
+
+ test "should update role" do
+ patch role_url(@role), params: { role: { name: "UpdatedRole" } }
+ assert_redirected_to role_url(@role)
+ end
+
+ test "should not update role with invalid params" do
+ patch role_url(@role), params: { role: { name: "" } }
+ assert_response :unprocessable_entity
+ @role.reload
+ assert_not_equal "", @role.name
+ end
+
+ test "should soft delete role" do
+ initial_count = Role.count
+
+ delete role_url(@role)
+
+ # Role count should decrease (due to default scope)
+ assert_equal initial_count - 1, Role.count
+
+ # But should still exist with with_deleted scope
+ assert Role.with_deleted.exists?(@role.id)
+
+ assert_redirected_to roles_url
+ assert flash[:notice].include?('soft deleted')
+ end
+
+ test "should restore soft deleted role" do
+ @role.destroy # Soft delete first
+ initial_count = Role.count
+
+ patch restore_role_url(@role)
+
+ # Role count should increase (restored to active)
+ assert_equal initial_count + 1, Role.count
+
+ assert_redirected_to roles_url
+ assert_equal 'Role was successfully restored.', flash[:notice]
+
+ # Role should no longer be deleted
+ @role.reload
+ assert_not @role.deleted?
+ end
+
+ test "index should show active roles by default" do
+ deleted_role = Role.create!(name: "DeletedRole")
+ deleted_role.destroy
+
+ get roles_url
+
+ assert_response :success
+ # Verify by checking that deleted roles are not in default scope
+ assert_not Role.exists?(deleted_role.id)
+ assert Role.with_deleted.exists?(deleted_role.id)
+ end
+
+ test "index should show all roles when show_deleted is true" do
+ deleted_role = Role.create!(name: "DeletedRole")
+ deleted_role.destroy
+
+ get roles_url(show_deleted: true)
+
+ assert_response :success
+ # Response should be successful and the controller handles the show_deleted param
+ assert_not Role.exists?(deleted_role.id) # Not in default scope
+ assert Role.with_deleted.exists?(deleted_role.id) # But exists when including deleted
+ end
+end
\ No newline at end of file
diff --git a/test/controllers/surveys_controller_test.rb b/test/controllers/surveys_controller_test.rb
index 303aa68..ee84cb9 100644
--- a/test/controllers/surveys_controller_test.rb
+++ b/test/controllers/surveys_controller_test.rb
@@ -1,23 +1,113 @@
require "test_helper"
class SurveysControllerTest < ActionDispatch::IntegrationTest
+ setup do
+ @survey = surveys(:one)
+ @role = roles(:one)
+ @branch = survey_branches(:one)
+ @question = questions(:one)
+ @branch_questions = branch_questions(:one)
+ end
+
test "should get index" do
- get surveys_index_url
+ get surveys_url
assert_response :success
end
test "should get show" do
- get surveys_show_url
+ get survey_url(@survey)
assert_response :success
end
test "should get new" do
- get surveys_new_url
+ get new_survey_url
+ assert_response :success
+ end
+
+ test "should create survey" do
+ assert_difference('Survey.count') do
+ post surveys_url, params: { survey: { title: 'New Survey', description: 'Test' } }
+ end
+ assert_redirected_to survey_path(Survey.last)
+ end
+
+ test "should not create survey with invalid params" do
+ assert_no_difference('Survey.count') do
+ post surveys_url, params: { survey: { title: '', description: '' } }
+ end
+ assert_response :unprocessable_entity
+ end
+
+ test "should get edit" do
+ get edit_survey_url(@survey)
assert_response :success
end
- test "should get create" do
- get surveys_create_url
+ test "should update survey" do
+ patch survey_url(@survey), params: { survey: { title: 'Updated Title', description: 'Updated Description' } }
+ assert_redirected_to survey_path(@survey)
+ @survey.reload
+ assert_equal 'Updated Title', @survey.title
+ end
+
+ test "should not update survey with invalid params" do
+ patch survey_url(@survey), params: { survey: { title: '', description: '' } }
+ assert_response :unprocessable_entity
+ @survey.reload
+ assert_not_equal '', @survey.title
+ end
+
+ test "should soft delete survey" do
+ # Should not change total count (including deleted)
+ assert_no_difference('Survey.with_deleted.count') do
+ # But should decrease active count
+ assert_difference('Survey.count', -1) do
+ delete survey_url(@survey)
+ end
+ end
+
+ @survey.reload
+ assert @survey.deleted?
+ assert_redirected_to surveys_url
+ end
+
+ test "should restore survey" do
+ # First soft delete the survey
+ @survey.destroy
+ assert @survey.deleted?
+
+ # Then restore it
+ assert_difference('Survey.count', 1) do
+ patch restore_survey_url(@survey)
+ end
+
+ @survey.reload
+ assert_not @survey.deleted?
+ assert_redirected_to surveys_url
+ end
+
+ test "should submit survey responses" do
+ responses = [
+ { question_id: @question.id, content: "Test answer" }
+ ]
+ assert_difference('Response.count') do
+ post submit_survey_url(@survey), params: { response: { responses: responses } }
+ end
+ assert_redirected_to surveys_path
+ follow_redirect!
+ assert_match /Thank you for completing the survey!/, response.body
+ end
+
+ test "should not submit survey responses if none provided" do
+ assert_no_difference('Response.count') do
+ post submit_survey_url(@survey), params: { response: { responses: nil } }
+ end
+ assert_redirected_to take_survey_path(@survey)
+ end
+
+ test "should get take survey for role" do
+ get take_survey_url(@survey, role: @role)
assert_response :success
+ assert_select 'form'
end
end
diff --git a/test/fixtures/branch_questions.yml b/test/fixtures/branch_questions.yml
new file mode 100644
index 0000000..299135b
--- /dev/null
+++ b/test/fixtures/branch_questions.yml
@@ -0,0 +1,11 @@
+# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
+
+one:
+ survey_branch: one
+ question: one
+two:
+ survey_branch: two
+ question: two
+three:
+ survey_branch: two
+ question: one
diff --git a/test/fixtures/questions.yml b/test/fixtures/questions.yml
index 57ddecf..bfd0bbc 100644
--- a/test/fixtures/questions.yml
+++ b/test/fixtures/questions.yml
@@ -1,13 +1,11 @@
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
one:
- survey: one
- content: MyText
- question_type: MyString
+ content: Question for DATA ENGINEER
+ question_type: text
position: 1
two:
- survey: two
- content: MyText
- question_type: MyString
+ content: Question for FRONTEND ENGINEER
+ question_type: text
position: 1
diff --git a/test/fixtures/roles.yml b/test/fixtures/roles.yml
new file mode 100644
index 0000000..5ca0ab4
--- /dev/null
+++ b/test/fixtures/roles.yml
@@ -0,0 +1,10 @@
+# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
+
+one:
+ name: data engineer
+two:
+ name: frontend engineer
+three:
+ name: product manager
+four:
+ name: qa engineer
diff --git a/test/fixtures/survey_branches.yml b/test/fixtures/survey_branches.yml
new file mode 100644
index 0000000..c6e6587
--- /dev/null
+++ b/test/fixtures/survey_branches.yml
@@ -0,0 +1,14 @@
+# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
+
+one:
+ survey: one
+ role: one
+ name: Data Engineer Branch
+two:
+ survey: one
+ role: two
+ name: Frontend Engineer Branch
+three:
+ survey: one
+ role: three
+ name: Product Manager Branch
diff --git a/test/fixtures/surveys.yml b/test/fixtures/surveys.yml
index 4d4de9a..3479a31 100644
--- a/test/fixtures/surveys.yml
+++ b/test/fixtures/surveys.yml
@@ -1,9 +1,9 @@
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
one:
- title: MyString
- description: MyText
+ title: Survey One
+ description: Survey One Description
two:
- title: MyString
- description: MyText
+ title: Survey Two
+ description: Survey Two Description
diff --git a/test/models/branch_question_test.rb b/test/models/branch_question_test.rb
new file mode 100644
index 0000000..1da4ef2
--- /dev/null
+++ b/test/models/branch_question_test.rb
@@ -0,0 +1,47 @@
+require "test_helper"
+
+class BranchQuestionTest < ActiveSupport::TestCase
+ test "associations" do
+ assert BranchQuestion.reflect_on_association(:survey_branch)
+ assert BranchQuestion.reflect_on_association(:question)
+ end
+
+ test "soft delete functionality" do
+ survey = Survey.create!(title: "Test Survey")
+ role = Role.create!(name: "Test Role")
+ survey_branch = SurveyBranch.create!(survey: survey, role: role, name: "Test Branch")
+ question = Question.create!(content: "Test Question", question_type: "text", position: 1)
+ branch_question = BranchQuestion.create!(survey_branch: survey_branch, question: question)
+
+ # Test soft delete
+ branch_question.destroy
+ assert branch_question.deleted?
+ assert branch_question.deleted_at.present?
+
+ # Test restore
+ branch_question.restore
+ assert_not branch_question.deleted?
+ assert_nil branch_question.deleted_at
+ end
+
+ test "default scope excludes soft deleted records" do
+ survey = Survey.create!(title: "Test Survey")
+ role = Role.create!(name: "Test Role")
+ survey_branch = SurveyBranch.create!(survey: survey, role: role, name: "Test Branch")
+ question = Question.create!(content: "Test Question", question_type: "text", position: 1)
+ branch_question = BranchQuestion.create!(survey_branch: survey_branch, question: question)
+ branch_question_id = branch_question.id
+
+ # BranchQuestion should be visible in default scope
+ assert BranchQuestion.exists?(branch_question_id)
+
+ # Soft delete the branch_question
+ branch_question.destroy
+
+ # BranchQuestion should not be visible in default scope
+ assert_not BranchQuestion.exists?(branch_question_id)
+
+ # But should be visible with with_deleted scope
+ assert BranchQuestion.with_deleted.exists?(branch_question_id)
+ end
+end
diff --git a/test/models/question_test.rb b/test/models/question_test.rb
index 50011db..b180050 100644
--- a/test/models/question_test.rb
+++ b/test/models/question_test.rb
@@ -1,7 +1,180 @@
+
require "test_helper"
class QuestionTest < ActiveSupport::TestCase
- # test "the truth" do
- # assert true
- # end
+ test "associations" do
+ assert Question.reflect_on_association(:branch_questions)
+ assert Question.reflect_on_association(:survey_branches)
+ assert Question.reflect_on_association(:responses)
+ end
+
+ test "soft delete functionality" do
+ question = Question.create!(content: "Test Question", question_type: "text", position: 1)
+
+ # Test soft delete
+ question.destroy
+ assert question.deleted?
+ assert question.deleted_at.present?
+
+ # Test restore
+ question.restore
+ assert_not question.deleted?
+ assert_nil question.deleted_at
+ end
+
+ test "default scope excludes soft deleted records" do
+ question = Question.create!(content: "Test Question for Scoping", question_type: "text", position: 1)
+ question_id = question.id
+
+ # Question should be visible in default scope
+ assert Question.exists?(question_id)
+
+ # Soft delete the question
+ question.destroy
+
+ # Question should not be visible in default scope
+ assert_not Question.exists?(question_id)
+
+ # But should be visible with with_deleted scope
+ assert Question.with_deleted.exists?(question_id)
+ end
+
+ test "can find only deleted records" do
+ active_question = Question.create!(content: "Active Question", question_type: "text", position: 1)
+ deleted_question = Question.create!(content: "Deleted Question", question_type: "text", position: 2)
+ deleted_question.destroy
+
+ deleted_questions = Question.only_deleted
+ assert_includes deleted_questions, deleted_question
+ assert_not_includes deleted_questions, active_question
+ end
+
+ test "validate options for multiple_choice question type" do
+ question = Question.new(content: "Choose one", question_type: "multiple_choice", options: nil)
+ assert_not question.valid?
+ assert_includes question.errors[:options], "can't be blank for multiple_choice questions"
+
+ question.options = ["A", "B", "C"]
+ assert question.valid?
+ end
+
+ test "can create text question" do
+ question = Question.new(
+ content: "What is your name?",
+ question_type: "text",
+ position: 1
+ )
+
+ assert question.valid?, "Text question should be valid: #{question.errors.full_messages}"
+ assert question.save, "Text question should save successfully"
+ end
+
+ test "can create long_text question" do
+ question = Question.new(
+ content: "Please describe your experience:",
+ question_type: "long_text",
+ position: 1
+ )
+
+ assert question.valid?, "Long text question should be valid: #{question.errors.full_messages}"
+ assert question.save, "Long text question should save successfully"
+ end
+
+ test "can create multiple_choice question with options" do
+ question = Question.new(
+ content: "What is your favorite color?",
+ question_type: "multiple_choice",
+ options: ["Red", "Blue", "Green", "Yellow"],
+ position: 1
+ )
+
+ assert question.valid?, "Multiple choice question should be valid: #{question.errors.full_messages}"
+ assert question.save, "Multiple choice question should save successfully"
+ assert_equal ["Red", "Blue", "Green", "Yellow"], question.options
+ end
+
+ test "cannot create multiple_choice question without options" do
+ question = Question.new(
+ content: "What is your favorite color?",
+ question_type: "multiple_choice",
+ position: 1
+ )
+
+ assert_not question.valid?, "Multiple choice question without options should be invalid"
+ assert_includes question.errors[:options], "can't be blank for multiple_choice questions"
+ end
+
+ test "can create checkbox question with options" do
+ question = Question.new(
+ content: "Which languages do you speak?",
+ question_type: "checkbox",
+ options: ["English", "Spanish", "French", "German"],
+ position: 1
+ )
+
+ assert question.valid?, "Checkbox question should be valid: #{question.errors.full_messages}"
+ assert question.save, "Checkbox question should save successfully"
+ assert_equal ["English", "Spanish", "French", "German"], question.options
+ end
+
+ test "cannot create checkbox question without options" do
+ question = Question.new(
+ content: "Which languages do you speak?",
+ question_type: "checkbox",
+ position: 1
+ )
+
+ assert_not question.valid?, "Checkbox question without options should be invalid"
+ assert_includes question.errors[:options], "can't be blank for checkbox questions"
+ end
+
+ test "can create rating question" do
+ question = Question.new(
+ content: "Rate your experience from 1 to 5:",
+ question_type: "rating",
+ position: 1
+ )
+
+ assert question.valid?, "Rating question should be valid: #{question.errors.full_messages}"
+ assert question.save, "Rating question should save successfully"
+ end
+
+ test "validates question_type is one of allowed types" do
+ valid_types = Question::QUESTION_TYPES
+
+ valid_types.each do |type|
+ question = Question.new(
+ content: "Test question",
+ question_type: type,
+ position: 1
+ )
+
+ # Add options for types that require them
+ if ['multiple_choice', 'checkbox'].include?(type)
+ question.options = ["Option 1", "Option 2"]
+ end
+
+ assert question.valid?, "Question with type '#{type}' should be valid: #{question.errors.full_messages}"
+ end
+ end
+
+ test "requires content to be present" do
+ question = Question.new(
+ question_type: "text",
+ position: 1
+ )
+
+ assert_not question.valid?, "Question without content should be invalid"
+ assert_includes question.errors[:content], "can't be blank"
+ end
+
+ test "requires question_type to be present" do
+ question = Question.new(
+ content: "Test question",
+ position: 1
+ )
+
+ assert_not question.valid?, "Question without question_type should be invalid"
+ assert_includes question.errors[:question_type], "can't be blank"
+ end
end
diff --git a/test/models/response_test.rb b/test/models/response_test.rb
index a2f21b0..e016c51 100644
--- a/test/models/response_test.rb
+++ b/test/models/response_test.rb
@@ -1,7 +1,53 @@
+
require "test_helper"
class ResponseTest < ActiveSupport::TestCase
- # test "the truth" do
- # assert true
- # end
+ test "associations" do
+ assert Response.reflect_on_association(:survey)
+ assert Response.reflect_on_association(:question)
+ end
+
+ test "value must be present" do
+ response = Response.new(survey: Survey.new, question: Question.new, value: nil)
+ assert_not response.valid?
+ assert_includes response.errors[:value], "can't be blank"
+
+ response.value = "Some answer"
+ assert response.valid?
+ end
+
+ test "soft delete functionality" do
+ survey = Survey.create!(title: "Test Survey")
+ question = Question.create!(content: "Test Question", question_type: "text", position: 1)
+ response = Response.create!(survey: survey, question: question, value: "Test Answer")
+
+ # Test soft delete
+ response.destroy
+ assert response.deleted?
+ assert response.deleted_at.present?
+
+ # Test restore
+ response.restore
+ assert_not response.deleted?
+ assert_nil response.deleted_at
+ end
+
+ test "default scope excludes soft deleted records" do
+ survey = Survey.create!(title: "Test Survey")
+ question = Question.create!(content: "Test Question", question_type: "text", position: 1)
+ response = Response.create!(survey: survey, question: question, value: "Test Answer for Scoping")
+ response_id = response.id
+
+ # Response should be visible in default scope
+ assert Response.exists?(response_id)
+
+ # Soft delete the response
+ response.destroy
+
+ # Response should not be visible in default scope
+ assert_not Response.exists?(response_id)
+
+ # But should be visible with with_deleted scope
+ assert Response.with_deleted.exists?(response_id)
+ end
end
diff --git a/test/models/role_test.rb b/test/models/role_test.rb
new file mode 100644
index 0000000..42878ae
--- /dev/null
+++ b/test/models/role_test.rb
@@ -0,0 +1,125 @@
+require "test_helper"
+
+class RoleTest < ActiveSupport::TestCase
+ test "associations" do
+ assert Role.reflect_on_association(:survey_branches)
+ end
+
+ test "soft delete functionality" do
+ role = Role.create!(name: "test role")
+
+ # Test soft delete
+ role.destroy
+ assert role.deleted?
+ assert role.deleted_at.present?
+
+ # Test restore
+ role.restore
+ assert_not role.deleted?
+ assert_nil role.deleted_at
+ end
+
+ test "default scope excludes soft deleted records" do
+ role = Role.create!(name: "test role for scoping")
+ role_id = role.id
+
+ # Role should be visible in default scope
+ assert Role.exists?(role_id)
+
+ # Soft delete the role
+ role.destroy
+
+ # Role should not be visible in default scope
+ assert_not Role.exists?(role_id)
+
+ # But should be visible with with_deleted scope
+ assert Role.with_deleted.exists?(role_id)
+ end
+
+ test "deletion_impact returns correct data" do
+ # Create a role with survey branch
+ survey = surveys(:one)
+ role = Role.create!(name: "role for impact test")
+ SurveyBranch.create!(survey: survey, role: role, name: "Impact Test Branch")
+
+ impact = role.deletion_impact
+
+ assert impact.key?(:survey_branches)
+ assert impact.key?(:total_responses)
+ assert impact.key?(:total_questions)
+ assert impact.key?(:affected_surveys)
+ assert impact.key?(:survey_titles)
+
+ assert_kind_of Integer, impact[:survey_branches]
+ assert_kind_of Integer, impact[:total_responses]
+ assert_kind_of Integer, impact[:total_questions]
+ assert_kind_of Integer, impact[:affected_surveys]
+ assert_kind_of Array, impact[:survey_titles]
+
+ assert_equal 1, impact[:survey_branches]
+ assert_equal 1, impact[:affected_surveys]
+ end
+
+ test "deletion_status returns appropriate message" do
+ # Test role with no data
+ empty_role = Role.create!(name: "empty role")
+ assert_equal "Can be soft deleted safely", empty_role.deletion_status
+
+ # Test deleted role
+ deleted_role = Role.create!(name: "deleted role")
+ deleted_role.destroy
+ status = deleted_role.deletion_status
+ assert status.include?("Already deleted on")
+ end
+
+ test "soft delete preserves data" do
+ # Create a role with survey branch
+ survey = surveys(:one)
+ role = Role.create!(name: "test role with data")
+ branch = SurveyBranch.create!(survey: survey, role: role, name: "Test Branch")
+
+ branch_id = branch.id
+
+ # Soft delete the role
+ role.destroy
+
+ # Role should be soft deleted but data preserved
+ assert role.deleted?
+
+ # Survey branch should still exist (not cascaded)
+ assert SurveyBranch.exists?(branch_id)
+ end
+
+ test "with_deleted scope includes all records" do
+ initial_count = Role.count
+ initial_with_deleted_count = Role.with_deleted.count
+
+ Role.create!(name: "active role")
+ deleted_role = Role.create!(name: "deleted role")
+ deleted_role.destroy
+
+ # Default scope should show initial + 1 active role
+ assert_equal initial_count + 1, Role.count
+
+ # with_deleted should show initial + 2 (both active and deleted)
+ assert_equal initial_with_deleted_count + 2, Role.with_deleted.count
+ end
+
+ test "only_deleted scope shows only soft deleted records" do
+ Role.create!(name: "active role")
+ deleted_role = Role.create!(name: "deleted role")
+ deleted_role.destroy
+
+ # only_deleted should show only the deleted one
+ deleted_roles = Role.only_deleted
+ assert deleted_roles.any? { |r| r.id == deleted_role.id }
+ end
+
+ test "persisted returns false for soft deleted records" do
+ role = Role.create!(name: "test role")
+ assert role.persisted?
+
+ role.destroy
+ assert_not role.persisted?
+ end
+end
diff --git a/test/models/survey_branch_test.rb b/test/models/survey_branch_test.rb
new file mode 100644
index 0000000..3b0e8a6
--- /dev/null
+++ b/test/models/survey_branch_test.rb
@@ -0,0 +1,54 @@
+require "test_helper"
+
+class SurveyBranchTest < ActiveSupport::TestCase
+ test "associations" do
+ assert SurveyBranch.reflect_on_association(:survey)
+ assert SurveyBranch.reflect_on_association(:role)
+ assert SurveyBranch.reflect_on_association(:branch_questions)
+ assert SurveyBranch.reflect_on_association(:questions)
+ end
+
+ test "role_id must be unique per survey" do
+ survey = Survey.create!(title: "Test Survey")
+ role = Role.create!(name: "Test Role")
+ SurveyBranch.create!(survey: survey, role: role, name: "Test Branch")
+ duplicate = SurveyBranch.new(survey: survey, role: role, name: "Duplicate Branch")
+ assert_not duplicate.valid?
+ assert_includes duplicate.errors[:role_id], "has already been taken"
+ end
+
+ test "soft delete functionality" do
+ survey = Survey.create!(title: "Test Survey")
+ role = Role.create!(name: "Test Role")
+ survey_branch = SurveyBranch.create!(survey: survey, role: role, name: "Test Branch")
+
+ # Test soft delete
+ survey_branch.destroy
+ assert survey_branch.deleted?
+ assert survey_branch.deleted_at.present?
+
+ # Test restore
+ survey_branch.restore
+ assert_not survey_branch.deleted?
+ assert_nil survey_branch.deleted_at
+ end
+
+ test "default scope excludes soft deleted records" do
+ survey = Survey.create!(title: "Test Survey")
+ role = Role.create!(name: "Test Role")
+ survey_branch = SurveyBranch.create!(survey: survey, role: role, name: "Test Branch for Scoping")
+ survey_branch_id = survey_branch.id
+
+ # SurveyBranch should be visible in default scope
+ assert SurveyBranch.exists?(survey_branch_id)
+
+ # Soft delete the survey_branch
+ survey_branch.destroy
+
+ # SurveyBranch should not be visible in default scope
+ assert_not SurveyBranch.exists?(survey_branch_id)
+
+ # But should be visible with with_deleted scope
+ assert SurveyBranch.with_deleted.exists?(survey_branch_id)
+ end
+end
diff --git a/test/models/survey_test.rb b/test/models/survey_test.rb
index 0e8738a..61a163b 100644
--- a/test/models/survey_test.rb
+++ b/test/models/survey_test.rb
@@ -1,7 +1,52 @@
+
require "test_helper"
class SurveyTest < ActiveSupport::TestCase
- # test "the truth" do
- # assert true
- # end
+ test "associations" do
+ assert Survey.reflect_on_association(:survey_branches)
+ assert Survey.reflect_on_association(:branch_questions)
+ assert Survey.reflect_on_association(:questions)
+ assert Survey.reflect_on_association(:responses)
+ end
+
+ test "soft delete functionality" do
+ survey = Survey.create!(title: "Test Survey", description: "Test Description")
+
+ # Test soft delete
+ survey.destroy
+ assert survey.deleted?
+ assert survey.deleted_at.present?
+
+ # Test restore
+ survey.restore
+ assert_not survey.deleted?
+ assert_nil survey.deleted_at
+ end
+
+ test "default scope excludes soft deleted records" do
+ survey = Survey.create!(title: "Test Survey for Scoping", description: "Test Description")
+ survey_id = survey.id
+
+ # Survey should be visible in default scope
+ assert Survey.exists?(survey_id)
+
+ # Soft delete the survey
+ survey.destroy
+
+ # Survey should not be visible in default scope
+ assert_not Survey.exists?(survey_id)
+
+ # But should be visible with with_deleted scope
+ assert Survey.with_deleted.exists?(survey_id)
+ end
+
+ test "can find only deleted records" do
+ active_survey = Survey.create!(title: "Active Survey", description: "Active Description")
+ deleted_survey = Survey.create!(title: "Deleted Survey", description: "Deleted Description")
+ deleted_survey.destroy
+
+ deleted_surveys = Survey.only_deleted
+ assert_includes deleted_surveys, deleted_survey
+ assert_not_includes deleted_surveys, active_survey
+ end
end
diff --git a/test/system/questions_test.rb b/test/system/questions_test.rb
new file mode 100644
index 0000000..566a355
--- /dev/null
+++ b/test/system/questions_test.rb
@@ -0,0 +1,270 @@
+require "application_system_test_case"
+
+class QuestionsTest < ApplicationSystemTestCase
+ setup do
+ @survey = surveys(:one)
+ end
+
+ test "user can add, edit, and soft delete questions from a survey" do
+ visit surveys_path
+ click_on "View", match: :first
+ click_on "Add Question"
+ fill_in "Question Text", with: "System test question"
+ select "text", from: "Question type"
+ select "Frontend Engineer", from: "Roles"
+ click_on "Create Question"
+ assert_text "System test question"
+ within("li", text: "System test question") do
+ click_on "Edit", match: :first
+ end
+ fill_in "Question Text", with: "Updated system test question"
+ click_on "Update Question"
+ assert_text "Question was successfully updated"
+
+ # Test soft delete
+ within("li", text: "Updated system test question") do
+ accept_confirm do
+ click_on "Delete", match: :first
+ end
+ end
+ # Question should disappear from active view
+ assert_no_selector "li", text: "Updated system test question", wait: 10
+
+ # Test that deleted question appears in "All Questions" tab
+ click_on "All Questions"
+ assert_text "Updated system test question"
+ assert_text "Deleted"
+
+ # Test restore functionality
+ within("li", text: "Updated system test question") do
+ accept_confirm do
+ click_on "Restore"
+ end
+ end
+
+ # Switch back to active questions and verify it's restored
+ click_on "Active Questions"
+ assert_text "Updated system test question"
+ assert_no_text "Deleted"
+ end
+
+ test "user can create text question" do
+ visit survey_path(@survey)
+ click_on "Add Question"
+
+ fill_in "Question Text", with: "What is your name?"
+ select "Text", from: "Question type"
+ fill_in "Position", with: "1"
+ select "Frontend Engineer", from: "Roles"
+ check "Required"
+
+ click_on "Create Question"
+
+ assert_text "Question was successfully created"
+ assert_text "What is your name?"
+
+ # Verify the question was created correctly
+ question = Question.last
+ assert_equal "What is your name?", question.content
+ assert_equal "text", question.question_type
+ assert_equal 1, question.position
+ assert question.required?
+ end
+
+ test "user can create long text question" do
+ visit survey_path(@survey)
+ click_on "Add Question"
+
+ fill_in "Question Text", with: "Please describe your experience in detail:"
+ select "Long text", from: "Question type"
+ fill_in "Position", with: "2"
+ select "Frontend Engineer", from: "Roles"
+
+ click_on "Create Question"
+
+ assert_text "Question was successfully created"
+ assert_text "Please describe your experience in detail:"
+
+ question = Question.last
+ assert_equal "Please describe your experience in detail:", question.content
+ assert_equal "long_text", question.question_type
+ assert_equal 2, question.position
+ end
+
+ test "user can create multiple choice question with options field appearing" do
+ visit survey_path(@survey)
+ click_on "Add Question"
+
+ fill_in "Question Text", with: "What is your favorite programming language?"
+ select "Multiple choice", from: "Question type"
+
+ # Verify options field appears when multiple choice is selected
+ assert_selector "textarea[name='question[options]']", visible: true
+
+ fill_in "Options (one per line)", with: "JavaScript\nPython\nRuby\nGo"
+ fill_in "Position", with: "3"
+ select "Frontend Engineer", from: "Roles"
+
+ click_on "Create Question"
+
+ assert_text "Question was successfully created"
+ assert_text "What is your favorite programming language?"
+
+ question = Question.last
+ assert_equal "What is your favorite programming language?", question.content
+ assert_equal "multiple_choice", question.question_type
+ assert_equal ["JavaScript", "Python", "Ruby", "Go"], question.options
+ assert_equal 3, question.position
+ end
+
+ test "user can create checkbox question with options field appearing" do
+ visit survey_path(@survey)
+ click_on "Add Question"
+
+ fill_in "Question Text", with: "Which frameworks have you used?"
+ select "Checkbox", from: "Question type"
+
+ # Verify options field appears when checkbox is selected
+ assert_selector "textarea[name='question[options]']", visible: true
+
+ fill_in "Options (one per line)", with: "React\nVue\nAngular\nSvelte"
+ fill_in "Position", with: "4"
+ select "Frontend Engineer", from: "Roles"
+
+ click_on "Create Question"
+
+ assert_text "Question was successfully created"
+ assert_text "Which frameworks have you used?"
+
+ question = Question.last
+ assert_equal "Which frameworks have you used?", question.content
+ assert_equal "checkbox", question.question_type
+ assert_equal ["React", "Vue", "Angular", "Svelte"], question.options
+ assert_equal 4, question.position
+ end
+
+ test "user can create rating question" do
+ visit survey_path(@survey)
+ click_on "Add Question"
+
+ fill_in "Question Text", with: "Rate your overall satisfaction (1-5):"
+ select "Rating", from: "Question type"
+ fill_in "Position", with: "5"
+ select "Frontend Engineer", from: "Roles"
+
+ click_on "Create Question"
+
+ assert_text "Question was successfully created"
+ assert_text "Rate your overall satisfaction (1-5):"
+
+ question = Question.last
+ assert_equal "Rate your overall satisfaction (1-5):", question.content
+ assert_equal "rating", question.question_type
+ assert_equal 5, question.position
+ end
+
+ test "options field visibility toggles correctly based on question type" do
+ visit survey_path(@survey)
+ click_on "Add Question"
+
+ # Initially, when text is selected, options should be hidden
+ select "Text", from: "Question type"
+ assert_selector "textarea[name='question[options]']", visible: false
+
+ # When switching to multiple choice, options should appear
+ select "Multiple choice", from: "Question type"
+ assert_selector "textarea[name='question[options]']", visible: true
+
+ # When switching to checkbox, options should remain visible
+ select "Checkbox", from: "Question type"
+ assert_selector "textarea[name='question[options]']", visible: true
+
+ # When switching to rating, options should be hidden
+ select "Rating", from: "Question type"
+ assert_selector "textarea[name='question[options]']", visible: false
+
+ # When switching to long text, options should remain hidden
+ select "Long text", from: "Question type"
+ assert_selector "textarea[name='question[options]']", visible: false
+
+ # Switch back to multiple choice to verify it shows again
+ select "Multiple choice", from: "Question type"
+ assert_selector "textarea[name='question[options]']", visible: true
+ end
+
+ test "validation errors prevent creation of multiple choice question without options" do
+ visit survey_path(@survey)
+ click_on "Add Question"
+
+ fill_in "Question Text", with: "Choose your favorite color"
+ select "Multiple choice", from: "Question type"
+ # Intentionally leave options field empty
+ fill_in "Position", with: "1"
+ select "Frontend Engineer", from: "Roles"
+
+ click_on "Create Question"
+
+ # Should show validation error
+ assert_text "can't be blank for multiple_choice questions"
+ assert_current_path new_survey_question_path(@survey)
+ end
+
+ test "validation errors prevent creation of checkbox question without options" do
+ visit survey_path(@survey)
+ click_on "Add Question"
+
+ fill_in "Question Text", with: "Select all that apply"
+ select "Checkbox", from: "Question type"
+ # Intentionally leave options field empty
+ fill_in "Position", with: "1"
+ select "Frontend Engineer", from: "Roles"
+
+ click_on "Create Question"
+
+ # Should show validation error
+ assert_text "can't be blank for checkbox questions"
+ assert_current_path new_survey_question_path(@survey)
+ end
+
+ test "user can create all question types successfully" do
+ question_types = [
+ { type: "Text", content: "What is your name?", db_type: "text" },
+ { type: "Long text", content: "Describe your experience:", db_type: "long_text" },
+ { type: "Multiple choice", content: "Choose one:", db_type: "multiple_choice", options: "Option A\nOption B\nOption C" },
+ { type: "Checkbox", content: "Select all:", db_type: "checkbox", options: "Choice 1\nChoice 2\nChoice 3" },
+ { type: "Rating", content: "Rate this (1-5):", db_type: "rating" }
+ ]
+
+ question_types.each_with_index do |q_data, index|
+ visit survey_path(@survey)
+ click_on "Add Question"
+
+ fill_in "Question Text", with: q_data[:content]
+ select q_data[:type], from: "Question type"
+
+ if q_data[:options]
+ # Wait for options field to appear and fill it
+ assert_selector "textarea[name='question[options]']", visible: true
+ fill_in "Options (one per line)", with: q_data[:options]
+ end
+
+ fill_in "Position", with: (index + 1).to_s
+ select "Frontend Engineer", from: "Roles"
+
+ click_on "Create Question"
+
+ assert_text "Question was successfully created"
+ assert_text q_data[:content]
+
+ # Verify in database
+ question = Question.where(content: q_data[:content]).last
+ assert_not_nil question, "Question with content '#{q_data[:content]}' was not created"
+ assert_equal q_data[:db_type], question.question_type
+
+ if q_data[:options]
+ expected_options = q_data[:options].split("\n")
+ assert_equal expected_options, question.options
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/test/system/responses_test.rb b/test/system/responses_test.rb
new file mode 100644
index 0000000..a549dcd
--- /dev/null
+++ b/test/system/responses_test.rb
@@ -0,0 +1,35 @@
+require "application_system_test_case"
+
+class ResponsesTest < ApplicationSystemTestCase
+ test "user can submit a response to a survey" do
+ visit surveys_path
+ click_on "Take Survey", match: :first
+ select "Frontend Engineer", from: "role-select"
+
+ # Wait for the survey form to load
+ assert_selector "form", wait: 5
+ assert_selector "input[type='text'], textarea", wait: 5
+
+ all("input[type='text'], textarea").each_with_index do |field, index|
+ field.set("Test response #{index + 1}")
+ end
+
+ click_on "Submit Response"
+ assert_text "Thank you for completing the survey!", wait: 10
+ end
+
+ test "user sees validation error when submitting incomplete response" do
+ visit surveys_path
+ click_on "Take Survey", match: :first
+ select "Frontend Engineer", from: "role-select"
+
+ # Wait for the survey form to load
+ assert_selector "form", wait: 5
+
+ # Submit without answering any questions (bypasses client validation if no required questions)
+ click_on "Submit Response"
+
+ # Should now show the server error message
+ assert_text "Please answer at least one question", wait: 10
+ end
+end
\ No newline at end of file
diff --git a/test/system/roles_test.rb b/test/system/roles_test.rb
new file mode 100644
index 0000000..f5d1262
--- /dev/null
+++ b/test/system/roles_test.rb
@@ -0,0 +1,41 @@
+require "application_system_test_case"
+
+class RolesTest < ApplicationSystemTestCase
+ test "user can create, edit, and soft delete a role" do
+ visit roles_path
+ click_on "New Role"
+ fill_in "Name", with: "System Test Role"
+ click_on "Create Role"
+ assert_text "System Test Role"
+ click_on "Edit", match: :first
+ fill_in "Name", with: "Updated System Test Role"
+ click_on "Update Role"
+ assert_text "Updated System Test Role"
+ click_on "Back"
+
+ # Test soft delete
+ within("li", text: "Updated System Test Role") do
+ accept_confirm do
+ click_on "Delete", match: :first
+ end
+ end
+ assert_no_text "Updated System Test Role"
+
+ # Test that deleted role appears in "All Roles" tab
+ click_on "All Roles"
+ assert_text "Updated System Test Role"
+ assert_text "Deleted"
+
+ # Test restore functionality
+ within("li", text: "Updated System Test Role") do
+ accept_confirm do
+ click_on "Restore"
+ end
+ end
+
+ # Switch back to active roles and verify it's restored
+ click_on "Active Roles"
+ assert_text "Updated System Test Role"
+ assert_no_text "Deleted"
+ end
+end
\ No newline at end of file
diff --git a/test/system/survey_branches_test.rb b/test/system/survey_branches_test.rb
new file mode 100644
index 0000000..d71c896
--- /dev/null
+++ b/test/system/survey_branches_test.rb
@@ -0,0 +1,11 @@
+require "application_system_test_case"
+
+class SurveyBranchesTest < ApplicationSystemTestCase
+ test "user can select role and see correct questions" do
+ visit surveys_path
+ click_on "Take Survey", match: :first
+ select "Data Engineer", from: "role-select"
+ assert_text "Question for DATA ENGINEER"
+ refute_text "Question for FRONTEND ENGINEER"
+ end
+end
diff --git a/test/system/surveys_test.rb b/test/system/surveys_test.rb
new file mode 100644
index 0000000..9842080
--- /dev/null
+++ b/test/system/surveys_test.rb
@@ -0,0 +1,55 @@
+require "application_system_test_case"
+
+class SurveysTest < ApplicationSystemTestCase
+ test "user can create, edit, and soft delete a survey" do
+ visit surveys_path
+ click_on "New Survey"
+ fill_in "Title", with: "System Test Survey"
+ fill_in "Description", with: "Survey for system test"
+ click_on "Create Survey"
+ assert_text "System Test Survey"
+ within("li", text: "System Test Survey") do
+ click_on "Edit"
+ end
+ fill_in "Title", with: "Updated System Test Survey"
+ click_on "Update Survey"
+ assert_text "Updated System Test Survey"
+ click_on "Back to Surveys"
+
+ # Test soft delete
+ within("li", text: "Updated System Test Survey") do
+ accept_confirm do
+ click_on "Delete"
+ end
+ end
+ assert_no_text "Updated System Test Survey"
+
+ # Test that deleted survey appears in "All Surveys" tab
+ click_on "All Surveys"
+ assert_text "Updated System Test Survey"
+ assert_text "Deleted"
+
+ # Test restore functionality
+ within("li", text: "Updated System Test Survey") do
+ accept_confirm do
+ click_on "Restore"
+ end
+ end
+
+ # Switch back to active surveys and verify it's restored
+ click_on "Active Surveys"
+ assert_text "Updated System Test Survey"
+ assert_no_text "Deleted"
+ end
+
+ test "user can take a survey" do
+ visit surveys_path
+ click_on "Take Survey", match: :first
+ select "Frontend Engineer", from: "role-select"
+ assert_text "Question"
+ fill_in "Question for FRONTEND ENGINEER", with: "Test answer"
+ fill_in "Question for DATA ENGINEER", with: "Test answer"
+ click_on "Submit"
+ assert_text "Thank you for completing the survey!", wait: 10
+ end
+end
\ No newline at end of file