From 07983c3a7f55ea9af41532f15ba1dc48a3568d1c Mon Sep 17 00:00:00 2001
From: Michael Deol
Date: Wed, 27 Aug 2025 15:17:48 -0600
Subject: [PATCH 01/19] Initial Build
This creates a .ruby-version file for local vs making ruby 3.1.3 a global config setting. I also had to delete the original package-lock.json and node_modules in order to fix a @parcel/watcher "not found" error.
---
.ruby-version | 2 +-
package-lock.json | 1519 ++++++++++++++++++++++++++++++++++++++++-----
2 files changed, 1377 insertions(+), 144 deletions(-)
diff --git a/.ruby-version b/.ruby-version
index c877459..ff365e0 100644
--- a/.ruby-version
+++ b/.ruby-version
@@ -1 +1 @@
-ruby-3.1.3
+3.1.3
diff --git a/package-lock.json b/package-lock.json
index e48c9a4..009921e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,6 +20,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 +91,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,26 +116,364 @@
"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/@parcel/watcher": {
@@ -63,6 +482,7 @@
"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 +512,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.12",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.1.12.tgz",
+ "integrity": "sha512-2PyJ5MGh/6JPS+cEaAq6MGDx3UemkX/mJt+/phm7/VOpycpecwNnHuFZbbgx6TNK/aIjvFOhhTVlappM7tmqvQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@parcel/watcher": "^2.5.1",
+ "@tailwindcss/node": "4.1.12",
+ "@tailwindcss/oxide": "4.1.12",
+ "enhanced-resolve": "^5.18.3",
+ "mri": "^1.2.0",
+ "picocolors": "^1.1.1",
+ "tailwindcss": "4.1.12"
+ },
+ "bin": {
+ "tailwindcss": "dist/index.mjs"
+ }
+ },
+ "node_modules/@tailwindcss/node": {
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.12.tgz",
+ "integrity": "sha512-3hm9brwvQkZFe++SBt+oLjo4OLDtkvlE8q2WalaD/7QWaeM7KEJbAiY/LJZUaCs7Xa8aUu4xy3uoyX4q54UVdQ==",
+ "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.17",
+ "source-map-js": "^1.2.1",
+ "tailwindcss": "4.1.12"
+ }
+ },
+ "node_modules/@tailwindcss/oxide": {
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.12.tgz",
+ "integrity": "sha512-gM5EoKHW/ukmlEtphNwaGx45fGoEmP10v51t9unv55voWh6WrOL19hfuIdo2FjxIaZzw776/BUQg7Pck++cIVw==",
+ "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.12",
+ "@tailwindcss/oxide-darwin-arm64": "4.1.12",
+ "@tailwindcss/oxide-darwin-x64": "4.1.12",
+ "@tailwindcss/oxide-freebsd-x64": "4.1.12",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.12",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.1.12",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.1.12",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.1.12",
+ "@tailwindcss/oxide-linux-x64-musl": "4.1.12",
+ "@tailwindcss/oxide-wasm32-wasi": "4.1.12",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.1.12",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.1.12"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-android-arm64": {
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.12.tgz",
+ "integrity": "sha512-oNY5pq+1gc4T6QVTsZKwZaGpBb2N1H1fsc1GD4o7yinFySqIuRZ2E4NvGasWc6PhYJwGK2+5YT1f9Tp80zUQZQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-arm64": {
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.12.tgz",
+ "integrity": "sha512-cq1qmq2HEtDV9HvZlTtrj671mCdGB93bVY6J29mwCyaMYCP/JaUBXxrQQQm7Qn33AXXASPUb2HFZlWiiHWFytw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-x64": {
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.12.tgz",
+ "integrity": "sha512-6UCsIeFUcBfpangqlXay9Ffty9XhFH1QuUFn0WV83W8lGdX8cD5/+2ONLluALJD5+yJ7k8mVtwy3zMZmzEfbLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-freebsd-x64": {
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.12.tgz",
+ "integrity": "sha512-JOH/f7j6+nYXIrHobRYCtoArJdMJh5zy5lr0FV0Qu47MID/vqJAY3r/OElPzx1C/wdT1uS7cPq+xdYYelny1ww==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.12.tgz",
+ "integrity": "sha512-v4Ghvi9AU1SYgGr3/j38PD8PEe6bRfTnNSUE3YCMIRrrNigCFtHZ2TCm8142X8fcSqHBZBceDx+JlFJEfNg5zQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.12.tgz",
+ "integrity": "sha512-YP5s1LmetL9UsvVAKusHSyPlzSRqYyRB0f+Kl/xcYQSPLEw/BvGfxzbH+ihUciePDjiXwHh+p+qbSP3SlJw+6g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.12.tgz",
+ "integrity": "sha512-V8pAM3s8gsrXcCv6kCHSuwyb/gPsd863iT+v1PGXC4fSL/OJqsKhfK//v8P+w9ThKIoqNbEnsZqNy+WDnwQqCA==",
"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.12",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.12.tgz",
+ "integrity": "sha512-xYfqYLjvm2UQ3TZggTGrwxjYaLB62b1Wiysw/YE3Yqbh86sOMoTn0feF98PonP7LtjsWOWcXEbGqDL7zv0uW8Q==",
+ "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.12",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.12.tgz",
+ "integrity": "sha512-ha0pHPamN+fWZY7GCzz5rKunlv9L5R8kdh+YNvP5awe3LtuXb5nRi/H27GeL2U+TdhDOptU7T6Is7mdwh5Ar3A==",
+ "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.12",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.12.tgz",
+ "integrity": "sha512-4tSyu3dW+ktzdEpuk6g49KdEangu3eCYoqPhWNsZgUhyegEda3M9rG0/j1GV/JjVVsj+lG7jWAyrTlLzd/WEBg==",
+ "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.12",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.12.tgz",
+ "integrity": "sha512-iGLyD/cVP724+FGtMWslhcFyg4xyYyM+5F4hGvKA7eifPkXHRAUDFaimu53fpNg9X8dfP75pXx/zFt/jlNF+lg==",
+ "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.12",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.12.tgz",
+ "integrity": "sha512-NKIh5rzw6CpEodv/++r0hGLlfgT/gFN+5WNdZtvh6wpU2BpGNgdjvj6H2oFc8nCM839QM1YOhjpgbAONUb4IxA==",
"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 +1097,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 +1127,7 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "license": "MIT",
"dependencies": {
"browserslist": "^4.24.4",
"caniuse-lite": "^1.0.30001702",
@@ -251,6 +1151,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 +1160,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.3",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.3.tgz",
+ "integrity": "sha512-cDGv1kkDI4/0e5yON9yM5G/0A5u8sf5TnmdX5C9qHzI9PPu++sQ9zjm1k9NiOrf3riY4OkK0zSGqfvJyJsgCBQ==",
"dev": true,
"funding": [
{
@@ -277,11 +1178,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.30001735",
+ "electron-to-chromium": "^1.5.204",
"node-releases": "^2.0.19",
- "update-browserslist-db": "^1.1.1"
+ "update-browserslist-db": "^1.1.3"
},
"bin": {
"browserslist": "cli.js"
@@ -291,9 +1193,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.30001737",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001737.tgz",
+ "integrity": "sha512-BiloLiXtQNrY5UyF0+1nSJLXUENuhka2pzy2Fx5pGxqavdrxSCW4U6Pn/PoG3Efspi2frRbHpBV2XsrPE6EDlw==",
"dev": true,
"funding": [
{
@@ -308,13 +1210,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 +1235,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 +1243,22 @@
"node": ">=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 +1273,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 +1285,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 +1318,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 +1327,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.210",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.210.tgz",
+ "integrity": "sha512-20kSVv1tyNBN2VFsjCIJZfyvxqo7ylHPrJLME040f/030lzNMA7uQNpxtqJjWSNpccD8/2sqe53EAjrFPvQmjw==",
+ "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 +1359,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 +1397,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 +1407,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 +1420,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 +1434,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 +1443,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 +1461,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 +1471,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 +1481,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 +1494,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 +1512,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 +1532,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 +1586,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 +1776,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 +1800,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 +1860,7 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@@ -698,19 +1872,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
+ "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 +1896,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 +1913,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 +1931,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 +1945,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"
},
@@ -782,6 +1964,7 @@
"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 +1978,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 +1988,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 +1997,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 +2020,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 +2030,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 +2045,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 +2058,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 +2070,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.12",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.12.tgz",
+ "integrity": "sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==",
+ "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 +2113,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 +2126,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 +2135,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 +2157,7 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "license": "MIT",
"dependencies": {
"escalade": "^3.2.0",
"picocolors": "^1.1.1"
@@ -955,6 +2174,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 +2192,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 +2231,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"
}
From 885a7d3ab4546bf3f3369914d67ffe3394ba9a2a Mon Sep 17 00:00:00 2001
From: Michael Deol
Date: Wed, 27 Aug 2025 21:52:19 -0600
Subject: [PATCH 02/19] Added new models and updated seeds
Tests have also been updated but are failing. Tests are still referencing an old schema.
---
app/controllers/surveys_controller.rb | 5 +-
app/models/branch_question.rb | 5 ++
app/models/question.rb | 19 ++++---
app/models/response.rb | 3 +-
app/models/role.rb | 4 ++
app/models/survey.rb | 5 +-
app/models/survey_branch.rb | 7 +++
...224445_create_survey_branches_and_roles.rb | 21 ++++++++
...8034813_remove_survey_id_from_questions.rb | 5 ++
db/schema.rb | 34 ++++++++++--
db/seeds.rb | 52 ++++++++++++++-----
test/controllers/questions_controller_test.rb | 32 ++++++++----
test/controllers/responses_controller_test.rb | 14 +++--
test/controllers/surveys_controller_test.rb | 25 +++++++--
test/models/branch_question_test.rb | 8 +++
test/models/role_test.rb | 7 +++
test/models/survey_branch_test.rb | 10 ++++
test/system/survey_branches_test.rb | 11 ++++
18 files changed, 219 insertions(+), 48 deletions(-)
create mode 100644 app/models/branch_question.rb
create mode 100644 app/models/role.rb
create mode 100644 app/models/survey_branch.rb
create mode 100644 db/migrate/20250827224445_create_survey_branches_and_roles.rb
create mode 100644 db/migrate/20250828034813_remove_survey_id_from_questions.rb
create mode 100644 test/models/branch_question_test.rb
create mode 100644 test/models/role_test.rb
create mode 100644 test/models/survey_branch_test.rb
create mode 100644 test/system/survey_branches_test.rb
diff --git a/app/controllers/surveys_controller.rb b/app/controllers/surveys_controller.rb
index a472b37..bb1ae8f 100644
--- a/app/controllers/surveys_controller.rb
+++ b/app/controllers/surveys_controller.rb
@@ -39,7 +39,10 @@ def destroy
end
def take
- @questions = @survey.questions.order(:position)
+ @survey = Survey.find(params[:id])
+ @role = Role.find_by(name: params[:role])
+ @branch = @survey.survey_branches.find_by(role: @role)
+ @questions = @branch.questions.order(:position)
end
def submit
diff --git a/app/models/branch_question.rb b/app/models/branch_question.rb
new file mode 100644
index 0000000..3e71bd9
--- /dev/null
+++ b/app/models/branch_question.rb
@@ -0,0 +1,5 @@
+
+class BranchQuestion < ApplicationRecord
+ belongs_to :survey_branch
+ belongs_to :question
+end
\ No newline at end of file
diff --git a/app/models/question.rb b/app/models/question.rb
index 0650d31..b12b819 100644
--- a/app/models/question.rb
+++ b/app/models/question.rb
@@ -1,24 +1,23 @@
+
class Question < ApplicationRecord
- belongs_to :survey
+ has_many :branch_questions, dependent: :destroy
+ has_many :survey_branches, through: :branch_questions
has_many :responses, dependent: :destroy
-
+
validates :content, presence: true
validates :question_type, presence: true
-
+
# Define question types
QUESTION_TYPES = ['text', 'long_text', 'multiple_choice', 'checkbox', 'rating'].freeze
-
- # Ensure position is maintained within a survey
- acts_as_list scope: :survey
-
+
# Serialize options as an array
serialize :options, Array
-
+
# Ensure options are present for question types that need them
validate :validate_options_for_question_type
-
+
private
-
+
def validate_options_for_question_type
if ['multiple_choice', 'checkbox'].include?(question_type) && (options.nil? || options.empty?)
errors.add(:options, "can't be blank for #{question_type} questions")
diff --git a/app/models/response.rb b/app/models/response.rb
index a147277..ac64330 100644
--- a/app/models/response.rb
+++ b/app/models/response.rb
@@ -1,6 +1,7 @@
+
class Response < ApplicationRecord
belongs_to :survey
belongs_to :question
-
+
validates :value, presence: true
end
diff --git a/app/models/role.rb b/app/models/role.rb
new file mode 100644
index 0000000..29e11f1
--- /dev/null
+++ b/app/models/role.rb
@@ -0,0 +1,4 @@
+
+class Role < ApplicationRecord
+ has_many :survey_branches, dependent: :destroy
+end
diff --git a/app/models/survey.rb b/app/models/survey.rb
index 07baa57..b9d04b4 100644
--- a/app/models/survey.rb
+++ b/app/models/survey.rb
@@ -1,4 +1,7 @@
+
class Survey < ApplicationRecord
- has_many :questions, dependent: :destroy
+ has_many :survey_branches, dependent: :destroy
+ has_many :branch_questions, through: :survey_branches
+ has_many :questions, through: :branch_questions
has_many :responses, dependent: :destroy
end
diff --git a/app/models/survey_branch.rb b/app/models/survey_branch.rb
new file mode 100644
index 0000000..54c547d
--- /dev/null
+++ b/app/models/survey_branch.rb
@@ -0,0 +1,7 @@
+
+class SurveyBranch < ApplicationRecord
+ belongs_to :survey
+ belongs_to :role
+ has_many :branch_questions, dependent: :destroy
+ has_many :questions, through: :branch_questions
+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/schema.rb b/db/schema.rb
index e823b08..52df819 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,12 +10,20 @@
#
# 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_08_28_034813) 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.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 +31,6 @@
t.datetime "updated_at", null: false
t.text "options"
t.boolean "required"
- t.index ["survey_id"], name: "index_questions_on_survey_id"
end
create_table "responses", force: :cascade do |t|
@@ -36,6 +43,22 @@
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
+ 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.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"
@@ -43,7 +66,10 @@
t.datetime "updated_at", null: false
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", "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..03b5126 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"].map do |role_name|
+ Role.create!(name: role_name.upcase)
+end
# Create engineering team survey
puts "Creating engineering team survey..."
@@ -19,9 +26,14 @@
description: "This survey helps us understand the satisfaction and needs of our engineering team members."
)
+# Create branches for each role
+engineering_survey_branches = 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 +98,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 +113,11 @@
description: "Help us understand what went well and what could be improved in our recent project."
)
+# Create branches for each role
+project_retrospective_survey_branches = 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 +185,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 +200,11 @@
description: "We value your feedback! Please help us improve our products and services."
)
+# Create branches for each role
+customer_survey_branches = 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 +260,12 @@
required: false,
options: ["Competitor A", "Competitor B", "Competitor C", "Competitor D", "None", "Other"]
}
-]
+].map { |attrs| Question.create!(attrs) }
-customer_questions.each do |question_attrs|
- customer_survey.questions.create!(question_attrs)
+customer_survey_branches.each do |branch|
+ customer_questions.each do |question|
+ BranchQuestion.create!(survey_branch: branch, question: question)
+ end
end
puts "Seed data created successfully!"
diff --git a/test/controllers/questions_controller_test.rb b/test/controllers/questions_controller_test.rb
index 0ea6019..f67f534 100644
--- a/test/controllers/questions_controller_test.rb
+++ b/test/controllers/questions_controller_test.rb
@@ -1,28 +1,38 @@
+
require "test_helper"
class QuestionsControllerTest < ActionDispatch::IntegrationTest
+ setup do
+ @survey = surveys(:one)
+ @question = questions(:one)
+ end
+
test "should get new" do
- get questions_new_url
+ get new_survey_question_url(@survey)
assert_response :success
end
- test "should get create" do
- get questions_create_url
- assert_response :success
+ test "should create question" do
+ assert_difference('Question.count') do
+ post survey_questions_url(@survey), params: { question: { content: 'Test?', question_type: 'text' } }
+ end
+ assert_redirected_to survey_path(@survey)
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 destroy question" do
+ assert_difference('Question.count', -1) do
+ delete survey_question_url(@survey, @question)
+ end
+ 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..9da409b 100644
--- a/test/controllers/responses_controller_test.rb
+++ b/test/controllers/responses_controller_test.rb
@@ -1,8 +1,16 @@
+
require "test_helper"
class ResponsesControllerTest < ActionDispatch::IntegrationTest
- test "should get create" do
- get responses_create_url
- assert_response :success
+ setup do
+ @survey = surveys(:one)
+ @question = questions(:one)
+ end
+
+ test "should create response" 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
end
diff --git a/test/controllers/surveys_controller_test.rb b/test/controllers/surveys_controller_test.rb
index 303aa68..6e02ddf 100644
--- a/test/controllers/surveys_controller_test.rb
+++ b/test/controllers/surveys_controller_test.rb
@@ -1,23 +1,38 @@
+
require "test_helper"
class SurveysControllerTest < ActionDispatch::IntegrationTest
+ setup do
+ @survey = surveys(:one)
+ @role = roles(:one)
+ @branch = survey_branches(: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 get create" do
- get surveys_create_url
+ 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 get take survey for role" do
+ get take_survey_url(@survey, role: @role.name)
assert_response :success
+ assert_select 'form' # Should render questions for the branch
end
end
diff --git a/test/models/branch_question_test.rb b/test/models/branch_question_test.rb
new file mode 100644
index 0000000..4a9b0d4
--- /dev/null
+++ b/test/models/branch_question_test.rb
@@ -0,0 +1,8 @@
+require "test_helper"
+
+class BranchQuestionTest < ActiveSupport::TestCase
+ test "associations" do
+ assert BranchQuestion.reflect_on_association(:survey_branch)
+ assert BranchQuestion.reflect_on_association(:question)
+ end
+end
diff --git a/test/models/role_test.rb b/test/models/role_test.rb
new file mode 100644
index 0000000..7bd737a
--- /dev/null
+++ b/test/models/role_test.rb
@@ -0,0 +1,7 @@
+require "test_helper"
+
+class RoleTest < ActiveSupport::TestCase
+ test "associations" do
+ assert Role.reflect_on_association(:survey_branches)
+ end
+end
diff --git a/test/models/survey_branch_test.rb b/test/models/survey_branch_test.rb
new file mode 100644
index 0000000..bdc4a88
--- /dev/null
+++ b/test/models/survey_branch_test.rb
@@ -0,0 +1,10 @@
+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
+end
diff --git a/test/system/survey_branches_test.rb b/test/system/survey_branches_test.rb
new file mode 100644
index 0000000..a031c76
--- /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"
+ select "Data Engineer", from: "Role"
+ assert_text "Question for Data Engineer"
+ refute_text "Question for Frontend Engineer"
+ end
+end
From 3eea0c2d847e9bb0fa7723a1ecbd9fd6a893dacb Mon Sep 17 00:00:00 2001
From: Michael Deol
Date: Thu, 28 Aug 2025 15:06:38 -0600
Subject: [PATCH 03/19] Add fixtures and fix tests
---
app/controllers/questions_controller.rb | 9 ++++++---
app/controllers/surveys_controller.rb | 6 ++++--
test/controllers/surveys_controller_test.rb | 4 +++-
test/fixtures/branch_questions.yml | 11 +++++++++++
test/fixtures/questions.yml | 2 --
test/fixtures/roles.yml | 8 ++++++++
test/fixtures/survey_branches.yml | 14 ++++++++++++++
test/models/question_test.rb | 9 ++++++---
test/models/response_test.rb | 8 +++++---
test/models/survey_test.rb | 10 +++++++---
10 files changed, 64 insertions(+), 17 deletions(-)
create mode 100644 test/fixtures/branch_questions.yml
create mode 100644 test/fixtures/roles.yml
create mode 100644 test/fixtures/survey_branches.yml
diff --git a/app/controllers/questions_controller.rb b/app/controllers/questions_controller.rb
index b7dbcce..37a46ea 100644
--- a/app/controllers/questions_controller.rb
+++ b/app/controllers/questions_controller.rb
@@ -2,15 +2,18 @@ class QuestionsController < ApplicationController
before_action :set_survey
before_action :set_question, only: [:edit, :update, :destroy]
+
def new
- @question = @survey.questions.new
+ @question = Question.new
end
def create
- @question = @survey.questions.new(question_params)
+ @question = Question.new(question_params)
process_options
if @question.save
+ branch = @survey.survey_branches.first # We may want a way to select branch by param/role
+ BranchQuestion.create!(survey_branch: branch, question: @question)
redirect_to survey_path(@survey), notice: 'Question was successfully created.'
else
render :new, status: :unprocessable_entity
@@ -45,7 +48,7 @@ def set_survey
end
def set_question
- @question = @survey.questions.find(params[:id])
+ @question = Question.find(params[:id])
end
def question_params
diff --git a/app/controllers/surveys_controller.rb b/app/controllers/surveys_controller.rb
index bb1ae8f..a0db8b2 100644
--- a/app/controllers/surveys_controller.rb
+++ b/app/controllers/surveys_controller.rb
@@ -40,9 +40,11 @@ def destroy
def take
@survey = Survey.find(params[:id])
- @role = Role.find_by(name: params[:role])
+ @role = Role.find_by(name: params[:role].upcase)
@branch = @survey.survey_branches.find_by(role: @role)
- @questions = @branch.questions.order(:position)
+ @questions = Question.joins(:branch_questions)
+ .where(branch_questions: { survey_branch_id: @branch.id })
+ .order(:position)
end
def submit
diff --git a/test/controllers/surveys_controller_test.rb b/test/controllers/surveys_controller_test.rb
index 6e02ddf..4bdd6fa 100644
--- a/test/controllers/surveys_controller_test.rb
+++ b/test/controllers/surveys_controller_test.rb
@@ -6,6 +6,8 @@ class SurveysControllerTest < ActionDispatch::IntegrationTest
@survey = surveys(:one)
@role = roles(:one)
@branch = survey_branches(:one)
+ @question = questions(:one)
+ @branch_questions = branch_questions(:one)
end
test "should get index" do
@@ -31,7 +33,7 @@ class SurveysControllerTest < ActionDispatch::IntegrationTest
end
test "should get take survey for role" do
- get take_survey_url(@survey, role: @role.name)
+ get take_survey_url(@survey, role: @role.name.upcase)
assert_response :success
assert_select 'form' # Should render questions for the branch
end
diff --git a/test/fixtures/branch_questions.yml b/test/fixtures/branch_questions.yml
new file mode 100644
index 0000000..672918a
--- /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: two
diff --git a/test/fixtures/questions.yml b/test/fixtures/questions.yml
index 57ddecf..2a9d4ed 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
position: 1
two:
- survey: two
content: MyText
question_type: MyString
position: 1
diff --git a/test/fixtures/roles.yml b/test/fixtures/roles.yml
new file mode 100644
index 0000000..2ef51be
--- /dev/null
+++ b/test/fixtures/roles.yml
@@ -0,0 +1,8 @@
+# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
+
+one:
+ name: DATA ENGINEER
+two:
+ name: FRONTEND ENGINEER
+three:
+ name: PRODUCT MANAGER
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/models/question_test.rb b/test/models/question_test.rb
index 50011db..9ade635 100644
--- a/test/models/question_test.rb
+++ b/test/models/question_test.rb
@@ -1,7 +1,10 @@
+
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
end
diff --git a/test/models/response_test.rb b/test/models/response_test.rb
index a2f21b0..5da4ad4 100644
--- a/test/models/response_test.rb
+++ b/test/models/response_test.rb
@@ -1,7 +1,9 @@
+
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
end
diff --git a/test/models/survey_test.rb b/test/models/survey_test.rb
index 0e8738a..0b2f125 100644
--- a/test/models/survey_test.rb
+++ b/test/models/survey_test.rb
@@ -1,7 +1,11 @@
+
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
end
From 1d013801d11969315d46501b2f5b3bcc84e5ca72 Mon Sep 17 00:00:00 2001
From: Michael Deol
Date: Fri, 29 Aug 2025 16:16:09 -0600
Subject: [PATCH 04/19] Start of RoleModal Prompt
---
app/controllers/questions_controller.rb | 8 ++-
app/controllers/surveys_controller.rb | 1 +
app/javascript/components/RoleModal.jsx | 62 +++++++++++++++++++
app/javascript/components/RoleSelect.jsx | 27 ++++++++
app/javascript/components/index.js | 4 +-
app/views/surveys/index.html.erb | 3 +-
test/controllers/questions_controller_test.rb | 3 +-
7 files changed, 103 insertions(+), 5 deletions(-)
create mode 100644 app/javascript/components/RoleModal.jsx
create mode 100644 app/javascript/components/RoleSelect.jsx
diff --git a/app/controllers/questions_controller.rb b/app/controllers/questions_controller.rb
index 37a46ea..45dae4c 100644
--- a/app/controllers/questions_controller.rb
+++ b/app/controllers/questions_controller.rb
@@ -1,7 +1,7 @@
class QuestionsController < ApplicationController
before_action :set_survey
before_action :set_question, only: [:edit, :update, :destroy]
-
+ before_action :set_role, only: [:new, :create, :edit, :update, :destroy]
def new
@question = Question.new
@@ -12,7 +12,7 @@ def create
process_options
if @question.save
- branch = @survey.survey_branches.first # We may want a way to select branch by param/role
+ branch = @survey.survey_branches.find_by(survey_id: @survey.id, role_id: @role.id)
BranchQuestion.create!(survey_branch: branch, question: @question)
redirect_to survey_path(@survey), notice: 'Question was successfully created.'
else
@@ -50,6 +50,10 @@ def set_survey
def set_question
@question = Question.find(params[:id])
end
+
+ def set_role
+ @role = Role.find_by(name: params[:role].upcase) if params[:role].present?
+ end
def question_params
params.require(:question).permit(:content, :question_type, :position, :required, options: [])
diff --git a/app/controllers/surveys_controller.rb b/app/controllers/surveys_controller.rb
index a0db8b2..e388a31 100644
--- a/app/controllers/surveys_controller.rb
+++ b/app/controllers/surveys_controller.rb
@@ -3,6 +3,7 @@ class SurveysController < ApplicationController
def index
@surveys = Survey.all
+ @roles = Role.all
end
def show
diff --git a/app/javascript/components/RoleModal.jsx b/app/javascript/components/RoleModal.jsx
new file mode 100644
index 0000000..b6dea45
--- /dev/null
+++ b/app/javascript/components/RoleModal.jsx
@@ -0,0 +1,62 @@
+import React from "react";
+import RoleSelect from "./RoleSelect";
+import { createRoot } from 'react-dom/client';
+
+const RoleModal = ({ open, roles, value, onSelect, onClose }) => {
+ if (!open) return null;
+ return (
+
+
+
Choose Your Role
+
+
+ Cancel
+
+
+
+ );
+};
+
+// Use a self-executing function to initialize the component
+const initializeRoleModal = () => {
+ const container = document.getElementById('role-modal-root');
+ const roles = JSON.parse(container.dataset.roles);
+ if (container && !container.hasAttribute('data-react-initialized')) {
+
+ // Mark as initialized to prevent double initialization
+ container.setAttribute('data-react-initialized', 'true');
+
+ const root = createRoot(container);
+ document.querySelectorAll(".take-survey-link").forEach(link => {
+ link.addEventListener("click", function(e) {
+ e.preventDefault();
+ const surveyId = this.dataset.surveyId;
+ // Render your React RoleModal, passing surveyId as a prop
+ root.render(
+ {}} // Provide actual handler
+ onClose={() => {}} // Provide actual handler
+ />,
+ document.getElementById("role-modal-root")
+ )
+ });
+ });
+ }
+};
+
+// Try to initialize immediately
+initializeRoleModal();
+
+// Also listen for DOMContentLoaded
+document.addEventListener('DOMContentLoaded', initializeRoleModal);
+
+// Additionally listen for turbo:load event if using Turbo
+document.addEventListener('turbo:load', initializeRoleModal);
+
+export default RoleModal;
diff --git a/app/javascript/components/RoleSelect.jsx b/app/javascript/components/RoleSelect.jsx
new file mode 100644
index 0000000..7a049e9
--- /dev/null
+++ b/app/javascript/components/RoleSelect.jsx
@@ -0,0 +1,27 @@
+import React from "react";
+import { createRoot } from 'react-dom/client';
+
+const RoleSelect = ({ roles, value, onChange, label = "Select Role" }) => {
+ return (
+
+
+ {label}
+
+ onChange(e.target.value)}
+ className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring focus:border-blue-300"
+ >
+ -- Choose a role --
+ {roles.map(role => (
+
+ {role.name}
+
+ ))}
+
+
+ );
+};
+
+export default RoleSelect;
diff --git a/app/javascript/components/index.js b/app/javascript/components/index.js
index ce24380..2d5af9f 100644
--- a/app/javascript/components/index.js
+++ b/app/javascript/components/index.js
@@ -2,6 +2,8 @@
import SurveyForm from "./SurveyForm"
import QuestionList from "./QuestionList"
import TakeSurvey from "./TakeSurvey"
+import RoleModal from "./RoleModal"
+import RoleSelect from "./RoleSelect"
// Export components for use elsewhere if needed
-export { SurveyForm, QuestionList, TakeSurvey }
\ No newline at end of file
+export { SurveyForm, QuestionList, TakeSurvey, RoleModal, RoleSelect }
\ No newline at end of file
diff --git a/app/views/surveys/index.html.erb b/app/views/surveys/index.html.erb
index 2ff14e3..4fc6df7 100644
--- a/app/views/surveys/index.html.erb
+++ b/app/views/surveys/index.html.erb
@@ -23,7 +23,7 @@
<%= link_to "View", survey_path(survey), class: "inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-indigo-700 bg-indigo-100 hover:bg-indigo-200" %>
<%= link_to "Edit", edit_survey_path(survey), class: "inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-yellow-700 bg-yellow-100 hover:bg-yellow-200" %>
- <%= link_to "Take Survey", take_survey_path(survey), class: "inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-green-700 bg-green-100 hover:bg-green-200" %>
+ <%= link_to "Take Survey", "#", class: "take-survey-link inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-green-700 bg-green-100 hover:bg-green-200", data: { survey_id: survey.id } %>
<%= button_to "Delete", survey_path(survey), method: :delete, class: "inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-red-700 bg-red-100 hover:bg-red-200", form: { data: { turbo_confirm: "Are you sure you want to delete this survey?" } } %>
@@ -31,6 +31,7 @@
<% end %>
+
<% else %>
No surveys available. Create your first survey!
diff --git a/test/controllers/questions_controller_test.rb b/test/controllers/questions_controller_test.rb
index f67f534..6e8c3cd 100644
--- a/test/controllers/questions_controller_test.rb
+++ b/test/controllers/questions_controller_test.rb
@@ -5,6 +5,7 @@ class QuestionsControllerTest < ActionDispatch::IntegrationTest
setup do
@survey = surveys(:one)
@question = questions(:one)
+ @role = roles(:one)
end
test "should get new" do
@@ -14,7 +15,7 @@ class QuestionsControllerTest < ActionDispatch::IntegrationTest
test "should create question" do
assert_difference('Question.count') do
- post survey_questions_url(@survey), params: { question: { content: 'Test?', question_type: 'text' } }
+ post survey_questions_url(@survey), params: { question: { content: 'Test?', question_type: 'text' }, role: @role.name }
end
assert_redirected_to survey_path(@survey)
end
From 600799cd11ebf28cc412a72582ff7d62e0dbbab3 Mon Sep 17 00:00:00 2001
From: Michael Deol
Date: Mon, 15 Sep 2025 23:06:47 -0600
Subject: [PATCH 05/19] Take Survey with Role Selected
---
app/controllers/responses_controller.rb | 2 +-
app/controllers/surveys_controller.rb | 9 +-
app/javascript/components/RoleModal.jsx | 39 ++++--
app/javascript/components/TakeSurvey.jsx | 9 +-
app/views/surveys/take.html.erb | 4 +-
mise.local.toml | 2 +
package-lock.json | 158 +++++++++++------------
7 files changed, 121 insertions(+), 102 deletions(-)
create mode 100644 mise.local.toml
diff --git a/app/controllers/responses_controller.rb b/app/controllers/responses_controller.rb
index 5abfa41..ba26acc 100644
--- a/app/controllers/responses_controller.rb
+++ b/app/controllers/responses_controller.rb
@@ -17,6 +17,6 @@ def create
private
def response_params
- params.require(:response).permit(:survey_id, :question_id, :value)
+ params.require(:response).permit(:survey_id, :question_id, :value)
end
end
diff --git a/app/controllers/surveys_controller.rb b/app/controllers/surveys_controller.rb
index e388a31..df44f80 100644
--- a/app/controllers/surveys_controller.rb
+++ b/app/controllers/surveys_controller.rb
@@ -41,7 +41,7 @@ def destroy
def take
@survey = Survey.find(params[:id])
- @role = Role.find_by(name: params[:role].upcase)
+ @role = Role.find(params[:role])
@branch = @survey.survey_branches.find_by(role: @role)
@questions = Question.joins(:branch_questions)
.where(branch_questions: { survey_branch_id: @branch.id })
@@ -49,11 +49,12 @@ def take
end
def submit
- if params[:responses].present?
- params[:responses].each do |response_params|
+ responses = params[:response][:responses]
+ if responses.present?
+ responses.each do |response_params|
@survey.responses.create(
question_id: response_params[:question_id],
- value: response_params[:value],
+ value: response_params[:content],
)
end
redirect_to surveys_path, notice: 'Thank you for completing the survey!'
diff --git a/app/javascript/components/RoleModal.jsx b/app/javascript/components/RoleModal.jsx
index b6dea45..98daec1 100644
--- a/app/javascript/components/RoleModal.jsx
+++ b/app/javascript/components/RoleModal.jsx
@@ -5,7 +5,7 @@ import { createRoot } from 'react-dom/client';
const RoleModal = ({ open, roles, value, onSelect, onClose }) => {
if (!open) return null;
return (
-
+
Choose Your Role
@@ -23,6 +23,7 @@ const RoleModal = ({ open, roles, value, onSelect, onClose }) => {
// Use a self-executing function to initialize the component
const initializeRoleModal = () => {
const container = document.getElementById('role-modal-root');
+ if (!container) return; // Prevents error if element is missing
const roles = JSON.parse(container.dataset.roles);
if (container && !container.hasAttribute('data-react-initialized')) {
@@ -30,21 +31,31 @@ const initializeRoleModal = () => {
container.setAttribute('data-react-initialized', 'true');
const root = createRoot(container);
+
+ let modalOpen = false;
+
document.querySelectorAll(".take-survey-link").forEach(link => {
link.addEventListener("click", function(e) {
- e.preventDefault();
- const surveyId = this.dataset.surveyId;
- // Render your React RoleModal, passing surveyId as a prop
- root.render(
-
{}} // Provide actual handler
- onClose={() => {}} // Provide actual handler
- />,
- document.getElementById("role-modal-root")
- )
+ e.preventDefault();
+ const surveyId = this.dataset.surveyId;
+ // Render your React RoleModal, passing surveyId as a prop
+ root.render(
+ {
+ // Redirect to /surveys/:id/take?role=selectedRole
+ window.location.href = `/surveys/${surveyId}/take?role=${encodeURIComponent(role)}`;
+ root.render(null); // Close modal after selection}} // Provide actual handler
+ }}
+ onClose={() => {
+ modalOpen = false;
+ root.render(null); // Unmount the modal
+
+ }}
+ />,
+ );
});
});
}
diff --git a/app/javascript/components/TakeSurvey.jsx b/app/javascript/components/TakeSurvey.jsx
index 9a7a6d3..f30a8ab 100644
--- a/app/javascript/components/TakeSurvey.jsx
+++ b/app/javascript/components/TakeSurvey.jsx
@@ -96,7 +96,7 @@ const RatingQuestion = ({ question, onChange }) => {
};
const TakeSurvey = (props) => {
- const { survey, questions } = props;
+ const { survey, role, questions } = props;
const [responses, setResponses] = useState({});
const [submitting, setSubmitting] = useState(false);
const [errors, setErrors] = useState([]);
@@ -133,7 +133,7 @@ const TakeSurvey = (props) => {
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
try {
- const response = await fetch(`/surveys/${survey.id}/responses`, {
+ const response = await fetch(`/surveys/${survey.id}/submit`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -142,7 +142,8 @@ const TakeSurvey = (props) => {
body: JSON.stringify({
response: {
survey_id: survey.id,
- question_responses_attributes: formattedResponses
+ role: role,
+ responses: formattedResponses
}
})
});
@@ -387,6 +388,7 @@ const TakeSurvey = (props) => {
// Use a self-executing function to initialize the component
const initializeTakeSurvey = () => {
const container = document.getElementById('take-survey-container');
+ const role = container?.dataset.role;
if (container && !container.hasAttribute('data-react-initialized')) {
const surveyData = JSON.parse(container.dataset.survey || '{}');
const questionsData = JSON.parse(container.dataset.questions || '[]');
@@ -399,6 +401,7 @@ const initializeTakeSurvey = () => {
);
}
diff --git a/app/views/surveys/take.html.erb b/app/views/surveys/take.html.erb
index fdaa4b7..1129146 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="<%= params[:role] %>">
<%= 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/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 009921e..eee7b2d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -792,28 +792,28 @@
"license": "MIT"
},
"node_modules/@tailwindcss/cli": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.1.12.tgz",
- "integrity": "sha512-2PyJ5MGh/6JPS+cEaAq6MGDx3UemkX/mJt+/phm7/VOpycpecwNnHuFZbbgx6TNK/aIjvFOhhTVlappM7tmqvQ==",
+ "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.12",
- "@tailwindcss/oxide": "4.1.12",
+ "@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.12"
+ "tailwindcss": "4.1.13"
},
"bin": {
"tailwindcss": "dist/index.mjs"
}
},
"node_modules/@tailwindcss/node": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.12.tgz",
- "integrity": "sha512-3hm9brwvQkZFe++SBt+oLjo4OLDtkvlE8q2WalaD/7QWaeM7KEJbAiY/LJZUaCs7Xa8aUu4xy3uoyX4q54UVdQ==",
+ "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": {
@@ -821,15 +821,15 @@
"enhanced-resolve": "^5.18.3",
"jiti": "^2.5.1",
"lightningcss": "1.30.1",
- "magic-string": "^0.30.17",
+ "magic-string": "^0.30.18",
"source-map-js": "^1.2.1",
- "tailwindcss": "4.1.12"
+ "tailwindcss": "4.1.13"
}
},
"node_modules/@tailwindcss/oxide": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.12.tgz",
- "integrity": "sha512-gM5EoKHW/ukmlEtphNwaGx45fGoEmP10v51t9unv55voWh6WrOL19hfuIdo2FjxIaZzw776/BUQg7Pck++cIVw==",
+ "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",
@@ -841,24 +841,24 @@
"node": ">= 10"
},
"optionalDependencies": {
- "@tailwindcss/oxide-android-arm64": "4.1.12",
- "@tailwindcss/oxide-darwin-arm64": "4.1.12",
- "@tailwindcss/oxide-darwin-x64": "4.1.12",
- "@tailwindcss/oxide-freebsd-x64": "4.1.12",
- "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.12",
- "@tailwindcss/oxide-linux-arm64-gnu": "4.1.12",
- "@tailwindcss/oxide-linux-arm64-musl": "4.1.12",
- "@tailwindcss/oxide-linux-x64-gnu": "4.1.12",
- "@tailwindcss/oxide-linux-x64-musl": "4.1.12",
- "@tailwindcss/oxide-wasm32-wasi": "4.1.12",
- "@tailwindcss/oxide-win32-arm64-msvc": "4.1.12",
- "@tailwindcss/oxide-win32-x64-msvc": "4.1.12"
+ "@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.12",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.12.tgz",
- "integrity": "sha512-oNY5pq+1gc4T6QVTsZKwZaGpBb2N1H1fsc1GD4o7yinFySqIuRZ2E4NvGasWc6PhYJwGK2+5YT1f9Tp80zUQZQ==",
+ "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"
],
@@ -873,9 +873,9 @@
}
},
"node_modules/@tailwindcss/oxide-darwin-arm64": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.12.tgz",
- "integrity": "sha512-cq1qmq2HEtDV9HvZlTtrj671mCdGB93bVY6J29mwCyaMYCP/JaUBXxrQQQm7Qn33AXXASPUb2HFZlWiiHWFytw==",
+ "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"
],
@@ -890,9 +890,9 @@
}
},
"node_modules/@tailwindcss/oxide-darwin-x64": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.12.tgz",
- "integrity": "sha512-6UCsIeFUcBfpangqlXay9Ffty9XhFH1QuUFn0WV83W8lGdX8cD5/+2ONLluALJD5+yJ7k8mVtwy3zMZmzEfbLg==",
+ "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"
],
@@ -907,9 +907,9 @@
}
},
"node_modules/@tailwindcss/oxide-freebsd-x64": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.12.tgz",
- "integrity": "sha512-JOH/f7j6+nYXIrHobRYCtoArJdMJh5zy5lr0FV0Qu47MID/vqJAY3r/OElPzx1C/wdT1uS7cPq+xdYYelny1ww==",
+ "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"
],
@@ -924,9 +924,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.12.tgz",
- "integrity": "sha512-v4Ghvi9AU1SYgGr3/j38PD8PEe6bRfTnNSUE3YCMIRrrNigCFtHZ2TCm8142X8fcSqHBZBceDx+JlFJEfNg5zQ==",
+ "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"
],
@@ -941,9 +941,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.12.tgz",
- "integrity": "sha512-YP5s1LmetL9UsvVAKusHSyPlzSRqYyRB0f+Kl/xcYQSPLEw/BvGfxzbH+ihUciePDjiXwHh+p+qbSP3SlJw+6g==",
+ "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"
],
@@ -958,9 +958,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.12.tgz",
- "integrity": "sha512-V8pAM3s8gsrXcCv6kCHSuwyb/gPsd863iT+v1PGXC4fSL/OJqsKhfK//v8P+w9ThKIoqNbEnsZqNy+WDnwQqCA==",
+ "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"
],
@@ -975,9 +975,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.12.tgz",
- "integrity": "sha512-xYfqYLjvm2UQ3TZggTGrwxjYaLB62b1Wiysw/YE3Yqbh86sOMoTn0feF98PonP7LtjsWOWcXEbGqDL7zv0uW8Q==",
+ "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"
],
@@ -992,9 +992,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.12.tgz",
- "integrity": "sha512-ha0pHPamN+fWZY7GCzz5rKunlv9L5R8kdh+YNvP5awe3LtuXb5nRi/H27GeL2U+TdhDOptU7T6Is7mdwh5Ar3A==",
+ "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"
],
@@ -1009,9 +1009,9 @@
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.12.tgz",
- "integrity": "sha512-4tSyu3dW+ktzdEpuk6g49KdEangu3eCYoqPhWNsZgUhyegEda3M9rG0/j1GV/JjVVsj+lG7jWAyrTlLzd/WEBg==",
+ "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",
@@ -1039,9 +1039,9 @@
}
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.12.tgz",
- "integrity": "sha512-iGLyD/cVP724+FGtMWslhcFyg4xyYyM+5F4hGvKA7eifPkXHRAUDFaimu53fpNg9X8dfP75pXx/zFt/jlNF+lg==",
+ "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"
],
@@ -1056,9 +1056,9 @@
}
},
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.12.tgz",
- "integrity": "sha512-NKIh5rzw6CpEodv/++r0hGLlfgT/gFN+5WNdZtvh6wpU2BpGNgdjvj6H2oFc8nCM839QM1YOhjpgbAONUb4IxA==",
+ "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": [
"x64"
],
@@ -1160,9 +1160,9 @@
}
},
"node_modules/browserslist": {
- "version": "4.25.3",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.3.tgz",
- "integrity": "sha512-cDGv1kkDI4/0e5yON9yM5G/0A5u8sf5TnmdX5C9qHzI9PPu++sQ9zjm1k9NiOrf3riY4OkK0zSGqfvJyJsgCBQ==",
+ "version": "4.25.4",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.4.tgz",
+ "integrity": "sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==",
"dev": true,
"funding": [
{
@@ -1180,8 +1180,8 @@
],
"license": "MIT",
"dependencies": {
- "caniuse-lite": "^1.0.30001735",
- "electron-to-chromium": "^1.5.204",
+ "caniuse-lite": "^1.0.30001737",
+ "electron-to-chromium": "^1.5.211",
"node-releases": "^2.0.19",
"update-browserslist-db": "^1.1.3"
},
@@ -1193,9 +1193,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001737",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001737.tgz",
- "integrity": "sha512-BiloLiXtQNrY5UyF0+1nSJLXUENuhka2pzy2Fx5pGxqavdrxSCW4U6Pn/PoG3Efspi2frRbHpBV2XsrPE6EDlw==",
+ "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": [
{
@@ -1327,9 +1327,9 @@
}
},
"node_modules/electron-to-chromium": {
- "version": "1.5.210",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.210.tgz",
- "integrity": "sha512-20kSVv1tyNBN2VFsjCIJZfyvxqo7ylHPrJLME040f/030lzNMA7uQNpxtqJjWSNpccD8/2sqe53EAjrFPvQmjw==",
+ "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"
},
@@ -1876,9 +1876,9 @@
"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==",
+ "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"
},
@@ -2070,9 +2070,9 @@
}
},
"node_modules/tailwindcss": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.12.tgz",
- "integrity": "sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==",
+ "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"
},
From 0f57d22798d178cb9927bd57dc7be7f02debfc35 Mon Sep 17 00:00:00 2001
From: Michael Deol
Date: Tue, 16 Sep 2025 13:42:32 -0600
Subject: [PATCH 06/19] Addition of Roles CRUD
---
app/controllers/roles_controller.rb | 49 +++++++++++++++++++++
app/views/layouts/application.html.erb | 2 +-
app/views/roles/_form.html.erb | 19 ++++++++
app/views/roles/edit.html.erb | 5 +++
app/views/roles/index.html.erb | 17 +++++++
app/views/roles/new.html.erb | 5 +++
app/views/roles/show.html.erb | 7 +++
config/routes.rb | 2 +
test/controllers/roles_controller_test.rb | 46 +++++++++++++++++++
test/controllers/surveys_controller_test.rb | 2 +-
10 files changed, 152 insertions(+), 2 deletions(-)
create mode 100644 app/controllers/roles_controller.rb
create mode 100644 app/views/roles/_form.html.erb
create mode 100644 app/views/roles/edit.html.erb
create mode 100644 app/views/roles/index.html.erb
create mode 100644 app/views/roles/new.html.erb
create mode 100644 app/views/roles/show.html.erb
create mode 100644 test/controllers/roles_controller_test.rb
diff --git a/app/controllers/roles_controller.rb b/app/controllers/roles_controller.rb
new file mode 100644
index 0000000..c202624
--- /dev/null
+++ b/app/controllers/roles_controller.rb
@@ -0,0 +1,49 @@
+class RolesController < ApplicationController
+ before_action :set_role, only: [:show, :edit, :update, :destroy]
+
+ def index
+ @roles = Role.all
+ end
+
+ def show
+ end
+
+ def new
+ @role = Role.new
+ end
+
+ def create
+ @role = Role.new(role_params)
+ if @role.save
+ redirect_to @role, notice: 'Role was successfully created.'
+ else
+ render :new, status: :unprocessable_entity
+ end
+ end
+
+ def edit
+ end
+
+ def update
+ if @role.update(role_params)
+ redirect_to @role, notice: 'Role was successfully updated.'
+ else
+ render :edit, status: :unprocessable_entity
+ end
+ end
+
+ def destroy
+ @role.destroy
+ redirect_to roles_url, notice: 'Role was successfully destroyed.'
+ end
+
+ private
+
+ def set_role
+ @role = Role.find(params[:id])
+ end
+
+ def role_params
+ params.require(:role).permit(:name)
+ end
+end
\ No newline at end of file
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
index d650d7b..805d108 100644
--- a/app/views/layouts/application.html.erb
+++ b/app/views/layouts/application.html.erb
@@ -21,7 +21,7 @@
<%= link_to "Surveys", surveys_path, class: "px-3 py-2 rounded-md text-sm font-medium hover:bg-indigo-500" %>
- <%= link_to "New Survey", new_survey_path, class: "px-3 py-2 rounded-md text-sm font-medium hover:bg-indigo-500" %>
+ <%= link_to "Roles", roles_path, class: "px-3 py-2 rounded-md text-sm font-medium hover:bg-indigo-500" %>
diff --git a/app/views/roles/_form.html.erb b/app/views/roles/_form.html.erb
new file mode 100644
index 0000000..3066ee9
--- /dev/null
+++ b/app/views/roles/_form.html.erb
@@ -0,0 +1,19 @@
+<%= form_with(model: role, local: true, class: "space-y-6") do |form| %>
+ <% if role.errors.any? %>
+
+
<%= pluralize(role.errors.count, "error") %> prohibited this role from being saved:
+
+ <% role.errors.full_messages.each do |message| %>
+ <%= message %>
+ <% end %>
+
+
+ <% end %>
+
+ <%= form.label :name, class: "block text-gray-700 font-medium mb-2" %>
+ <%= form.text_field :name, class: "mt-1 block w-full border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" %>
+
+
+ <%= form.submit class: "px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700" %>
+
+<% end %>
\ No newline at end of file
diff --git a/app/views/roles/edit.html.erb b/app/views/roles/edit.html.erb
new file mode 100644
index 0000000..40c9d10
--- /dev/null
+++ b/app/views/roles/edit.html.erb
@@ -0,0 +1,5 @@
+
+
Edit Role
+ <%= render 'form', role: @role %>
+ <%= link_to 'Back', roles_path, class: "inline-block mt-4 px-4 py-2 bg-gray-200 text-gray-700 rounded hover:bg-gray-300" %>
+
\ No newline at end of file
diff --git a/app/views/roles/index.html.erb b/app/views/roles/index.html.erb
new file mode 100644
index 0000000..ac2b72b
--- /dev/null
+++ b/app/views/roles/index.html.erb
@@ -0,0 +1,17 @@
+
+
+
Roles
+ <%= link_to 'New Role', new_role_path, class: "inline-block px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700" %>
+
+
+ <% @roles.each do |role| %>
+
+ <%= link_to role.name, role, class: "hover:underline" %>
+
+ <%= link_to 'Edit', edit_role_path(role), class: "inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-yellow-700 bg-yellow-100 hover:bg-yellow-200" %>
+ <%= link_to 'Destroy', role, method: :delete, data: { confirm: 'Are you sure?' }, class: "inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-red-700 bg-red-100 hover:bg-red-200" %>
+
+
+ <% end %>
+
+
\ No newline at end of file
diff --git a/app/views/roles/new.html.erb b/app/views/roles/new.html.erb
new file mode 100644
index 0000000..c69155b
--- /dev/null
+++ b/app/views/roles/new.html.erb
@@ -0,0 +1,5 @@
+
+
New Role
+ <%= render 'form', role: @role %>
+ <%= link_to 'Back', roles_path, class: "inline-block mt-4 px-4 py-2 bg-gray-200 text-gray-700 rounded hover:bg-gray-300" %>
+
\ No newline at end of file
diff --git a/app/views/roles/show.html.erb b/app/views/roles/show.html.erb
new file mode 100644
index 0000000..2898e48
--- /dev/null
+++ b/app/views/roles/show.html.erb
@@ -0,0 +1,7 @@
+
+
Role: <%= @role.name %>
+
+ <%= link_to 'Edit', edit_role_path(@role), class: "px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700" %>
+ <%= link_to 'Back', roles_path, class: "px-4 py-2 bg-gray-200 text-gray-700 rounded hover:bg-gray-300" %>
+
+
\ No newline at end of file
diff --git a/config/routes.rb b/config/routes.rb
index e4f3933..2368749 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -4,6 +4,8 @@
# Set the root path to the surveys index
root "surveys#index"
+ resources :roles
+
# RESTful routes for surveys
resources :surveys do
# Nested routes for questions
diff --git a/test/controllers/roles_controller_test.rb b/test/controllers/roles_controller_test.rb
new file mode 100644
index 0000000..adf6081
--- /dev/null
+++ b/test/controllers/roles_controller_test.rb
@@ -0,0 +1,46 @@
+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 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 destroy role" do
+ assert_difference('Role.count', -1) do
+ delete role_url(@role)
+ end
+ assert_redirected_to roles_url
+ 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 4bdd6fa..28931ba 100644
--- a/test/controllers/surveys_controller_test.rb
+++ b/test/controllers/surveys_controller_test.rb
@@ -33,7 +33,7 @@ class SurveysControllerTest < ActionDispatch::IntegrationTest
end
test "should get take survey for role" do
- get take_survey_url(@survey, role: @role.name.upcase)
+ get take_survey_url(@survey, role: @role)
assert_response :success
assert_select 'form' # Should render questions for the branch
end
From 794580e13f2d4238106f60d3560078755d61b40b Mon Sep 17 00:00:00 2001
From: Michael Deol
Date: Wed, 17 Sep 2025 13:32:52 -0600
Subject: [PATCH 07/19] Filter Survey 'view' by role/branch, Add branch/role to
question create
---
app/controllers/questions_controller.rb | 34 ++++++++++--
app/javascript/components/QuestionList.jsx | 60 ++++++++++++----------
app/views/questions/_form.html.erb | 5 +-
app/views/surveys/show.html.erb | 18 ++++++-
config/routes.rb | 2 +-
5 files changed, 84 insertions(+), 35 deletions(-)
diff --git a/app/controllers/questions_controller.rb b/app/controllers/questions_controller.rb
index 45dae4c..4674108 100644
--- a/app/controllers/questions_controller.rb
+++ b/app/controllers/questions_controller.rb
@@ -2,18 +2,40 @@ class QuestionsController < ApplicationController
before_action :set_survey
before_action :set_question, only: [:edit, :update, :destroy]
before_action :set_role, only: [:new, :create, :edit, :update, :destroy]
+ before_action :set_roles, only: [:new, :create, :edit, :update]
+
+ def index
+ @questions = @survey.questions
+
+ if params[:survey_branch_id].present?
+ @questions = @questions.joins(:branch_questions)
+ .where(branch_questions: { survey_branch_id: params[:survey_branch_id] })
+ .distinct
+ end
+
+ @questions = @questions.order(:position) if @questions.column_names.include?("position")
+
+ respond_to do |format|
+ format.json do
+ render json: @questions.as_json(
+ only: [:id, :content, :question_type, :required]
+ )
+ end
+ format.html { redirect_to survey_path(@survey) }
+ end
+ end
def new
@question = Question.new
end
def create
- @question = Question.new(question_params)
+ @question = Question.new(question_params.except(:survey_branch_id))
process_options
if @question.save
- branch = @survey.survey_branches.find_by(survey_id: @survey.id, role_id: @role.id)
- BranchQuestion.create!(survey_branch: branch, question: @question)
+ branch = @survey.survey_branches.find_by(id: params[:question][:survey_branch_id])
+ BranchQuestion.create!(survey_branch: branch, question: @question) if branch
redirect_to survey_path(@survey), notice: 'Question was successfully created.'
else
render :new, status: :unprocessable_entity
@@ -54,9 +76,13 @@ def set_question
def set_role
@role = Role.find_by(name: params[:role].upcase) if params[:role].present?
end
+
+ def set_roles
+ @roles = Role.all
+ end
def question_params
- params.require(:question).permit(:content, :question_type, :position, :required, options: [])
+ params.require(:question).permit(:content, :question_type, :position, :required, options: [], survey_branch_ids: [])
end
def process_options
diff --git a/app/javascript/components/QuestionList.jsx b/app/javascript/components/QuestionList.jsx
index 6982a37..3ad6140 100644
--- a/app/javascript/components/QuestionList.jsx
+++ b/app/javascript/components/QuestionList.jsx
@@ -5,24 +5,35 @@ const QuestionList = (props) => {
const [questions, setQuestions] = useState(props.questions || []);
const [loading, setLoading] = useState(false);
- const fetchQuestions = async () => {
+ const fetchQuestions = async (surveyBranchId = "") => {
setLoading(true);
try {
- const response = await fetch(`/surveys/${props.surveyId}/questions.json`);
- if (response.ok) {
- const data = await response.json();
- setQuestions(data);
- }
- } catch (error) {
- console.error('Error fetching questions:', error);
+ const url = `/surveys/${props.surveyId}/questions.json` + (surveyBranchId ? `?survey_branch_id=${encodeURIComponent(surveyBranchId)}` : "");
+ const res = await fetch(url, { headers: { "Accept": "application/json" } });
+ if (!res.ok) throw new Error("Failed to load questions");
+ const data = await res.json();
+ setQuestions(data);
+ } catch (e) {
+ console.error("Error loading questions", e);
} finally {
setLoading(false);
}
};
useEffect(() => {
- if (props.autoload && props.surveyId) {
- fetchQuestions();
+ // initial load
+ fetchQuestions();
+ // wire up filter if provided
+ const selectId = props.filterSelectId;
+ if (selectId) {
+ const sel = document.getElementById(selectId);
+ if (sel) {
+ const handler = (e) => {
+ fetchQuestions(e.target.value || "");
+ };
+ sel.addEventListener("change", handler);
+ return () => sel.removeEventListener("change", handler);
+ }
}
}, [props.surveyId]);
@@ -102,23 +113,18 @@ const QuestionList = (props) => {
// Use a self-executing function to initialize the component
const initializeQuestionList = () => {
const container = document.getElementById('question-list-container');
- if (container && !container.hasAttribute('data-react-initialized')) {
- const surveyId = container.dataset.surveyId;
- const questionsData = JSON.parse(container.dataset.questions || '[]');
- const autoload = container.dataset.autoload === 'true';
-
- // Mark as initialized to prevent double initialization
- container.setAttribute('data-react-initialized', 'true');
-
- const root = createRoot(container);
- root.render(
-
- );
- }
+ if (!container || container.hasAttribute('data-react-initialized')) return;
+ const surveyId = container.dataset.surveyId;
+ const filterSelectId = container.dataset.filterSelectId;
+ container.setAttribute('data-react-initialized', 'true');
+ const root = createRoot(container);
+ root.render(
+
+ );
};
// Try to initialize immediately
diff --git a/app/views/questions/_form.html.erb b/app/views/questions/_form.html.erb
index 5519985..a13ed9e 100644
--- a/app/views/questions/_form.html.erb
+++ b/app/views/questions/_form.html.erb
@@ -63,7 +63,10 @@
Order in which the question appears in the survey.
-
+
+ <%= form.label :role_id, "Role", class: "block text-sm font-medium text-gray-700" %>
+ <%= form.collection_select :survey_branch_id, @survey.survey_branches.includes(:role), :id, ->(sb) { sb.role.name }, { prompt: "Select a branch/role" }, class: "shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md" %>
+
<%= form.check_box :required, class: "focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded" %>
diff --git a/app/views/surveys/show.html.erb b/app/views/surveys/show.html.erb
index 24b305b..b1f2186 100644
--- a/app/views/surveys/show.html.erb
+++ b/app/views/surveys/show.html.erb
@@ -17,11 +17,25 @@
<%= link_to "Add Question", new_survey_question_path(@survey), class: "inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700" %>
+
+
+ Filter by Role
+
+
+ All Roles
+ <% @survey.survey_branches.includes(:role).each do |branch| %>
+ <%= branch.role.name %>
+ <% end %>
+
+
+
+ data-questions="<%= [].to_json %>"
+ data-autoload="false"
+ data-filter-select-id="survey-branch-filter">
diff --git a/config/routes.rb b/config/routes.rb
index 2368749..0cfc9c1 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -9,7 +9,7 @@
# RESTful routes for surveys
resources :surveys do
# Nested routes for questions
- resources :questions, except: [:index, :show]
+ resources :questions, except: [:show]
# Route for taking a survey
member do
From 3b57c1854c3ac0a0df3967d6db22e7c83339a283 Mon Sep 17 00:00:00 2001
From: Michael Deol
Date: Wed, 17 Sep 2025 14:42:47 -0600
Subject: [PATCH 08/19] Fix take survey link in Survey#show view
---
app/controllers/questions_controller.rb | 2 +-
app/controllers/surveys_controller.rb | 1 +
app/javascript/components/QuestionList.jsx | 2 --
app/views/questions/_form.html.erb | 9 ++++++++-
app/views/surveys/index.html.erb | 2 +-
app/views/surveys/show.html.erb | 4 +++-
6 files changed, 14 insertions(+), 6 deletions(-)
diff --git a/app/controllers/questions_controller.rb b/app/controllers/questions_controller.rb
index 4674108..802a9d7 100644
--- a/app/controllers/questions_controller.rb
+++ b/app/controllers/questions_controller.rb
@@ -5,7 +5,7 @@ class QuestionsController < ApplicationController
before_action :set_roles, only: [:new, :create, :edit, :update]
def index
- @questions = @survey.questions
+ @questions = @survey.questions.distinct
if params[:survey_branch_id].present?
@questions = @questions.joins(:branch_questions)
diff --git a/app/controllers/surveys_controller.rb b/app/controllers/surveys_controller.rb
index df44f80..9b25c02 100644
--- a/app/controllers/surveys_controller.rb
+++ b/app/controllers/surveys_controller.rb
@@ -7,6 +7,7 @@ def index
end
def show
+ @roles = Role.all
end
def new
diff --git a/app/javascript/components/QuestionList.jsx b/app/javascript/components/QuestionList.jsx
index 3ad6140..1727ec3 100644
--- a/app/javascript/components/QuestionList.jsx
+++ b/app/javascript/components/QuestionList.jsx
@@ -21,9 +21,7 @@ const QuestionList = (props) => {
};
useEffect(() => {
- // initial load
fetchQuestions();
- // wire up filter if provided
const selectId = props.filterSelectId;
if (selectId) {
const sel = document.getElementById(selectId);
diff --git a/app/views/questions/_form.html.erb b/app/views/questions/_form.html.erb
index a13ed9e..137eefb 100644
--- a/app/views/questions/_form.html.erb
+++ b/app/views/questions/_form.html.erb
@@ -65,7 +65,14 @@
<%= form.label :role_id, "Role", class: "block text-sm font-medium text-gray-700" %>
- <%= form.collection_select :survey_branch_id, @survey.survey_branches.includes(:role), :id, ->(sb) { sb.role.name }, { prompt: "Select a branch/role" }, class: "shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md" %>
+ <% if question.new_record? %>
+ <%= form.collection_select :survey_branch_id, @survey.survey_branches.includes(:role), :id, ->(sb) { sb.role.name }, { prompt: "Select a branch/role" }, class: "shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md" %>
+ <% else %>
+ <% branch = question.survey_branches.find_by(survey_id: @survey.id) %>
+
+ <%= branch&.role&.name || "No role assigned" %>
+
+ <% end %>
diff --git a/app/views/surveys/index.html.erb b/app/views/surveys/index.html.erb
index 4fc6df7..23d1241 100644
--- a/app/views/surveys/index.html.erb
+++ b/app/views/surveys/index.html.erb
@@ -23,7 +23,7 @@
<%= link_to "View", survey_path(survey), class: "inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-indigo-700 bg-indigo-100 hover:bg-indigo-200" %>
<%= link_to "Edit", edit_survey_path(survey), class: "inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-yellow-700 bg-yellow-100 hover:bg-yellow-200" %>
- <%= link_to "Take Survey", "#", class: "take-survey-link inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-green-700 bg-green-100 hover:bg-green-200", data: { survey_id: survey.id } %>
+ <%= link_to "Take Survey", take_survey_path(survey), class: "take-survey-link inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-green-700 bg-green-100 hover:bg-green-200", data: { survey_id: survey.id } %>
<%= button_to "Delete", survey_path(survey), method: :delete, class: "inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-red-700 bg-red-100 hover:bg-red-200", form: { data: { turbo_confirm: "Are you sure you want to delete this survey?" } } %>
diff --git a/app/views/surveys/show.html.erb b/app/views/surveys/show.html.erb
index b1f2186..7db67fb 100644
--- a/app/views/surveys/show.html.erb
+++ b/app/views/surveys/show.html.erb
@@ -6,7 +6,7 @@
<%= link_to "Edit", edit_survey_path(@survey), class: "inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-yellow-700 bg-yellow-100 hover:bg-yellow-200" %>
- <%= link_to "Take Survey", take_survey_path(@survey), class: "inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-green-700 bg-green-100 hover:bg-green-200" %>
+ <%= link_to "Take Survey", "#", class: "take-survey-link inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-green-700 bg-green-100 hover:bg-green-200", data: { survey_id: @survey.id } %>
<%= link_to "Back to Surveys", surveys_path, class: "inline-flex items-center px-3 py-1 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50" %>
@@ -67,5 +67,7 @@
<% end %>
+
+
\ No newline at end of file
From 2321667811bf26eede4afee21f7724ac7dab76f0 Mon Sep 17 00:00:00 2001
From: Michael Deol
Date: Tue, 23 Sep 2025 13:33:21 -0600
Subject: [PATCH 09/19] Updating test coverage
---
app/controllers/surveys_controller.rb | 4 +++
app/models/survey_branch.rb | 2 ++
app/views/questions/_form.html.erb | 22 +++++++++------
test/controllers/questions_controller_test.rb | 15 ++++++++++
test/controllers/responses_controller_test.rb | 28 ++++++++++++++++++-
test/models/question_test.rb | 9 ++++++
test/models/response_test.rb | 9 ++++++
test/models/survey_branch_test.rb | 9 ++++++
8 files changed, 88 insertions(+), 10 deletions(-)
diff --git a/app/controllers/surveys_controller.rb b/app/controllers/surveys_controller.rb
index 9b25c02..4d9c5c7 100644
--- a/app/controllers/surveys_controller.rb
+++ b/app/controllers/surveys_controller.rb
@@ -18,6 +18,10 @@ def create
@survey = Survey.new(survey_params)
if @survey.save
+ # Create a SurveyBranch for each Role
+ Role.find_each do |role|
+ SurveyBranch.create!(survey: @survey, role: role, name: "#{role.name} Branch")
+ end
redirect_to @survey, notice: 'Survey was successfully created.'
else
render :new, status: :unprocessable_entity
diff --git a/app/models/survey_branch.rb b/app/models/survey_branch.rb
index 54c547d..e924bf6 100644
--- a/app/models/survey_branch.rb
+++ b/app/models/survey_branch.rb
@@ -4,4 +4,6 @@ class SurveyBranch < ApplicationRecord
belongs_to :role
has_many :branch_questions, dependent: :destroy
has_many :questions, through: :branch_questions
+
+ validates :role_id, uniqueness: { scope: :survey_id }
end
diff --git a/app/views/questions/_form.html.erb b/app/views/questions/_form.html.erb
index 137eefb..a3ef6c8 100644
--- a/app/views/questions/_form.html.erb
+++ b/app/views/questions/_form.html.erb
@@ -64,15 +64,19 @@
- <%= form.label :role_id, "Role", class: "block text-sm font-medium text-gray-700" %>
- <% if question.new_record? %>
- <%= form.collection_select :survey_branch_id, @survey.survey_branches.includes(:role), :id, ->(sb) { sb.role.name }, { prompt: "Select a branch/role" }, class: "shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md" %>
- <% else %>
- <% branch = question.survey_branches.find_by(survey_id: @survey.id) %>
-
- <%= branch&.role&.name || "No role assigned" %>
-
- <% end %>
+ <%= form.label :survey_branch_ids, "Roles", class: "block text-sm font-medium text-gray-700" %>
+ <%= form.collection_select :survey_branch_ids,
+ @survey.survey_branches.includes(:role),
+ :id,
+ ->(sb) { sb.role.name },
+ { prompt: "Select one or more roles" },
+ {
+ multiple: true,
+ class: "shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md"
+ } %>
+
+ Hold Ctrl (Windows) or Command (Mac) to select multiple roles.
+
diff --git a/test/controllers/questions_controller_test.rb b/test/controllers/questions_controller_test.rb
index 6e8c3cd..8a34a24 100644
--- a/test/controllers/questions_controller_test.rb
+++ b/test/controllers/questions_controller_test.rb
@@ -8,6 +8,21 @@ class QuestionsControllerTest < ActionDispatch::IntegrationTest
@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 new" do
get new_survey_question_url(@survey)
assert_response :success
diff --git a/test/controllers/responses_controller_test.rb b/test/controllers/responses_controller_test.rb
index 9da409b..6d5c9a3 100644
--- a/test/controllers/responses_controller_test.rb
+++ b/test/controllers/responses_controller_test.rb
@@ -7,10 +7,36 @@ class ResponsesControllerTest < ActionDispatch::IntegrationTest
@question = questions(:one)
end
- test "should create response" do
+ 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/models/question_test.rb b/test/models/question_test.rb
index 9ade635..a643db6 100644
--- a/test/models/question_test.rb
+++ b/test/models/question_test.rb
@@ -7,4 +7,13 @@ class QuestionTest < ActiveSupport::TestCase
assert Question.reflect_on_association(:survey_branches)
assert Question.reflect_on_association(:responses)
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
end
diff --git a/test/models/response_test.rb b/test/models/response_test.rb
index 5da4ad4..051d9df 100644
--- a/test/models/response_test.rb
+++ b/test/models/response_test.rb
@@ -6,4 +6,13 @@ class ResponseTest < ActiveSupport::TestCase
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
end
diff --git a/test/models/survey_branch_test.rb b/test/models/survey_branch_test.rb
index bdc4a88..f5a2f88 100644
--- a/test/models/survey_branch_test.rb
+++ b/test/models/survey_branch_test.rb
@@ -7,4 +7,13 @@ class SurveyBranchTest < ActiveSupport::TestCase
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
end
From 8b5c3873dd5e03c1b031f1d999b8b7a312cb18f8 Mon Sep 17 00:00:00 2001
From: Michael Deol
Date: Tue, 23 Sep 2025 20:12:58 -0600
Subject: [PATCH 10/19] Additional Validation and Complete Test Coverage
---
app/controllers/surveys_controller.rb | 2 +-
app/models/role.rb | 2 +
app/models/survey.rb | 2 +
app/views/roles/index.html.erb | 6 +--
test/controllers/roles_controller_test.rb | 14 ++++++
test/controllers/surveys_controller_test.rb | 55 ++++++++++++++++++++-
6 files changed, 75 insertions(+), 6 deletions(-)
diff --git a/app/controllers/surveys_controller.rb b/app/controllers/surveys_controller.rb
index 4d9c5c7..499ec85 100644
--- a/app/controllers/surveys_controller.rb
+++ b/app/controllers/surveys_controller.rb
@@ -55,7 +55,7 @@ def take
def submit
responses = params[:response][:responses]
- if responses.present?
+ if responses.present? && responses.any?
responses.each do |response_params|
@survey.responses.create(
question_id: response_params[:question_id],
diff --git a/app/models/role.rb b/app/models/role.rb
index 29e11f1..b5bcbdc 100644
--- a/app/models/role.rb
+++ b/app/models/role.rb
@@ -1,4 +1,6 @@
class Role < ApplicationRecord
has_many :survey_branches, dependent: :destroy
+
+ validates :name, presence: true
end
diff --git a/app/models/survey.rb b/app/models/survey.rb
index b9d04b4..c77249b 100644
--- a/app/models/survey.rb
+++ b/app/models/survey.rb
@@ -4,4 +4,6 @@ class Survey < ApplicationRecord
has_many :branch_questions, through: :survey_branches
has_many :questions, through: :branch_questions
has_many :responses, dependent: :destroy
+
+ validates :title, presence: true
end
diff --git a/app/views/roles/index.html.erb b/app/views/roles/index.html.erb
index ac2b72b..78a6e4b 100644
--- a/app/views/roles/index.html.erb
+++ b/app/views/roles/index.html.erb
@@ -7,9 +7,9 @@
<% @roles.each do |role| %>
<%= link_to role.name, role, class: "hover:underline" %>
-
- <%= link_to 'Edit', edit_role_path(role), class: "inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-yellow-700 bg-yellow-100 hover:bg-yellow-200" %>
- <%= link_to 'Destroy', role, method: :delete, data: { confirm: 'Are you sure?' }, class: "inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-red-700 bg-red-100 hover:bg-red-200" %>
+
+ <%= link_to "Edit", edit_role_path(role), class: "inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-yellow-700 bg-yellow-100 hover:bg-yellow-200" %>
+ <%= button_to "Delete", role_path(role), method: :delete, class: "inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-red-700 bg-red-100 hover:bg-red-200", form: { data: { turbo_confirm: "Are you sure you want to delete this role?" } } %>
<% end %>
diff --git a/test/controllers/roles_controller_test.rb b/test/controllers/roles_controller_test.rb
index adf6081..a0d8601 100644
--- a/test/controllers/roles_controller_test.rb
+++ b/test/controllers/roles_controller_test.rb
@@ -22,6 +22,13 @@ class RolesControllerTest < ActionDispatch::IntegrationTest
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
@@ -37,6 +44,13 @@ class RolesControllerTest < ActionDispatch::IntegrationTest
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 destroy role" do
assert_difference('Role.count', -1) do
delete role_url(@role)
diff --git a/test/controllers/surveys_controller_test.rb b/test/controllers/surveys_controller_test.rb
index 28931ba..a0d7a4b 100644
--- a/test/controllers/surveys_controller_test.rb
+++ b/test/controllers/surveys_controller_test.rb
@@ -1,4 +1,3 @@
-
require "test_helper"
class SurveysControllerTest < ActionDispatch::IntegrationTest
@@ -32,9 +31,61 @@ class SurveysControllerTest < ActionDispatch::IntegrationTest
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 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 destroy survey" do
+ assert_difference('Survey.count', -1) do
+ delete survey_url(@survey)
+ end
+ 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' # Should render questions for the branch
+ assert_select 'form'
end
end
From 05e9046c481329ba90eaf9ff53df8c6ac45145e9 Mon Sep 17 00:00:00 2001
From: Michael Deol
Date: Wed, 24 Sep 2025 13:18:12 -0600
Subject: [PATCH 11/19] Add System Tests
---
test/fixtures/branch_questions.yml | 2 +-
test/fixtures/questions.yml | 8 ++++----
test/fixtures/surveys.yml | 8 ++++----
test/system/questions_test.rb | 20 ++++++++++++++++++++
test/system/responses_test.rb | 20 ++++++++++++++++++++
test/system/roles_test.rb | 17 +++++++++++++++++
test/system/survey_branches_test.rb | 8 ++++----
test/system/surveys_test.rb | 29 +++++++++++++++++++++++++++++
8 files changed, 99 insertions(+), 13 deletions(-)
create mode 100644 test/system/questions_test.rb
create mode 100644 test/system/responses_test.rb
create mode 100644 test/system/roles_test.rb
create mode 100644 test/system/surveys_test.rb
diff --git a/test/fixtures/branch_questions.yml b/test/fixtures/branch_questions.yml
index 672918a..299135b 100644
--- a/test/fixtures/branch_questions.yml
+++ b/test/fixtures/branch_questions.yml
@@ -8,4 +8,4 @@ two:
question: two
three:
survey_branch: two
- question: two
+ question: one
diff --git a/test/fixtures/questions.yml b/test/fixtures/questions.yml
index 2a9d4ed..bfd0bbc 100644
--- a/test/fixtures/questions.yml
+++ b/test/fixtures/questions.yml
@@ -1,11 +1,11 @@
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
one:
- content: MyText
- question_type: MyString
+ content: Question for DATA ENGINEER
+ question_type: text
position: 1
two:
- content: MyText
- question_type: MyString
+ content: Question for FRONTEND ENGINEER
+ question_type: text
position: 1
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/system/questions_test.rb b/test/system/questions_test.rb
new file mode 100644
index 0000000..c6a1758
--- /dev/null
+++ b/test/system/questions_test.rb
@@ -0,0 +1,20 @@
+require "application_system_test_case"
+
+class QuestionsTest < ApplicationSystemTestCase
+ test "user can add, edit, and remove questions from a survey" do
+ visit surveys_path
+ click_on "Edit", match: :first
+ click_on "Add Question"
+ fill_in "Content", with: "System test question"
+ select "Text", from: "Question type"
+ select "FRONTEND ENGINEER", from: "Roles"
+ click_on "Create Question"
+ assert_text "System test question"
+ click_on "Edit", match: :first
+ fill_in "Content", with: "Updated system test question"
+ click_on "Update Question"
+ assert_text "Updated system test question"
+ click_on "Delete", match: :first
+ assert_no_text "Updated system test question"
+ 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..c32d70d
--- /dev/null
+++ b/test/system/responses_test.rb
@@ -0,0 +1,20 @@
+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"
+ fill_in "Answer", with: "System test response"
+ click_on "Submit"
+ assert_text "Thank you for completing the survey!"
+ 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"
+ click_on "Submit"
+ assert_text "Please answer at least one question"
+ 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..acc6332
--- /dev/null
+++ b/test/system/roles_test.rb
@@ -0,0 +1,17 @@
+require "application_system_test_case"
+
+class RolesTest < ApplicationSystemTestCase
+ test "user can create, edit, and 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 "Delete", match: :first
+ assert_no_text "Updated System Test Role"
+ end
+end
\ No newline at end of file
diff --git a/test/system/survey_branches_test.rb b/test/system/survey_branches_test.rb
index a031c76..1ec1e77 100644
--- a/test/system/survey_branches_test.rb
+++ b/test/system/survey_branches_test.rb
@@ -3,9 +3,9 @@
class SurveyBranchesTest < ApplicationSystemTestCase
test "user can select role and see correct questions" do
visit surveys_path
- click_on "Take Survey"
- select "Data Engineer", from: "Role"
- assert_text "Question for Data Engineer"
- refute_text "Question for Frontend Engineer"
+ 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..28b66e2
--- /dev/null
+++ b/test/system/surveys_test.rb
@@ -0,0 +1,29 @@
+require "application_system_test_case"
+
+class SurveysTest < ApplicationSystemTestCase
+ test "user can create, edit, and 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"
+ click_on "Edit"
+ fill_in "Title", with: "Updated System Test Survey"
+ click_on "Update Survey"
+ assert_text "Updated System Test Survey"
+ click_on "Delete"
+ assert_no_text "Updated System Test Survey"
+ 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
From 8bc6d40ff4b818257e8849d683a8195a6b0ea9f6 Mon Sep 17 00:00:00 2001
From: Michael Deol
Date: Thu, 9 Oct 2025 14:02:45 -0600
Subject: [PATCH 12/19] Fixed all Capybara tests
---
app/controllers/questions_controller.rb | 7 +++--
app/controllers/surveys_controller.rb | 36 ++++++++++++++++------
app/javascript/components/QuestionList.jsx | 9 ++++--
app/javascript/components/SurveyForm.jsx | 13 ++++++--
app/javascript/components/TakeSurvey.jsx | 12 ++++----
test/system/questions_test.rb | 23 +++++++++-----
test/system/responses_test.rb | 27 ++++++++++++----
test/system/roles_test.rb | 8 ++++-
test/system/surveys_test.rb | 11 +++++--
9 files changed, 107 insertions(+), 39 deletions(-)
diff --git a/app/controllers/questions_controller.rb b/app/controllers/questions_controller.rb
index 802a9d7..8251ef5 100644
--- a/app/controllers/questions_controller.rb
+++ b/app/controllers/questions_controller.rb
@@ -60,7 +60,10 @@ def update
def destroy
@question.destroy
- redirect_to survey_path(@survey), notice: 'Question was successfully destroyed.'
+ respond_to do |format|
+ format.json { head :no_content }
+ format.html { redirect_to survey_path(@survey), notice: 'Question was successfully destroyed.' }
+ end
end
private
@@ -70,7 +73,7 @@ def set_survey
end
def set_question
- @question = Question.find(params[:id])
+ @question = @survey.questions.find(params[:id])
end
def set_role
diff --git a/app/controllers/surveys_controller.rb b/app/controllers/surveys_controller.rb
index 499ec85..fde11cb 100644
--- a/app/controllers/surveys_controller.rb
+++ b/app/controllers/surveys_controller.rb
@@ -22,9 +22,15 @@ def create
Role.find_each do |role|
SurveyBranch.create!(survey: @survey, role: role, name: "#{role.name} Branch")
end
- redirect_to @survey, notice: 'Survey was successfully created.'
+ respond_to do |format|
+ format.html { redirect_to @survey, notice: 'Survey was successfully created.' }
+ format.json { render json: { message: 'Survey created successfully' } }
+ end
else
- render :new, status: :unprocessable_entity
+ respond_to do |format|
+ format.html { render :new, status: :unprocessable_entity }
+ format.json { render json: { errors: @survey.errors.full_messages }, status: :unprocessable_entity }
+ end
end
end
@@ -33,12 +39,18 @@ def edit
def update
if @survey.update(survey_params)
- redirect_to @survey, notice: 'Survey was successfully updated.'
+ respond_to do |format|
+ format.html { redirect_to @survey, notice: 'Survey was successfully updated.' }
+ format.json { render json: { success: true, redirect_url: survey_path(@survey) }, status: :ok }
+ end
else
- render :edit, status: :unprocessable_entity
+ respond_to do |format|
+ format.html { render :edit, status: :unprocessable_entity }
+ format.json { render json: { errors: @survey.errors.full_messages }, status: :unprocessable_entity }
+ end
end
end
-
+
def destroy
@survey.destroy
redirect_to surveys_url, notice: 'Survey was successfully destroyed.'
@@ -62,12 +74,18 @@ def submit
value: response_params[:content],
)
end
- redirect_to surveys_path, notice: 'Thank you for completing the survey!'
+ respond_to do |format|
+ format.html { redirect_to surveys_path, notice: 'Thank you for completing the survey!' }
+ format.json { render json: { message: 'Thank you for completing the survey!' }, status: :created }
+ end
else
- redirect_to take_survey_path(@survey), alert: 'Please answer at least one question.'
+ respond_to do |format|
+ format.html { redirect_to take_survey_path(@survey), alert: 'Please answer at least one question.' }
+ format.json { render json: { error: 'Please answer at least one question.' }, status: :unprocessable_entity }
+ end
end
- end
-
+ end
+
private
def set_survey
diff --git a/app/javascript/components/QuestionList.jsx b/app/javascript/components/QuestionList.jsx
index 1727ec3..ec992d8 100644
--- a/app/javascript/components/QuestionList.jsx
+++ b/app/javascript/components/QuestionList.jsx
@@ -40,18 +40,23 @@ const QuestionList = (props) => {
return;
}
- const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
+ const csrfTokenElement = document.querySelector('meta[name="csrf-token"]');
+ const csrfToken = csrfTokenElement ? csrfTokenElement.content : '';
try {
const response = await fetch(`/surveys/${props.surveyId}/questions/${questionId}`, {
method: 'DELETE',
headers: {
- 'X-CSRF-Token': csrfToken
+ 'X-CSRF-Token': csrfToken,
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json'
}
});
if (response.ok) {
setQuestions(questions.filter(q => q.id !== questionId));
+ } else {
+ console.error('Failed to delete question:', response.status);
}
} catch (error) {
console.error('Error deleting question:', error);
diff --git a/app/javascript/components/SurveyForm.jsx b/app/javascript/components/SurveyForm.jsx
index 0842260..207d8e0 100644
--- a/app/javascript/components/SurveyForm.jsx
+++ b/app/javascript/components/SurveyForm.jsx
@@ -9,7 +9,8 @@ const SurveyForm = (props) => {
const handleSubmit = async (e) => {
e.preventDefault();
- const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
+ const csrfTokenElement = document.querySelector('meta[name="csrf-token"]');
+ const csrfToken = csrfTokenElement ? csrfTokenElement.content : '';
const url = props.survey?.id ? `/surveys/${props.survey.id}` : '/surveys';
const method = props.survey?.id ? 'PATCH' : 'POST';
@@ -18,6 +19,7 @@ const SurveyForm = (props) => {
method,
headers: {
'Content-Type': 'application/json',
+ 'Accept': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({
@@ -27,9 +29,14 @@ const SurveyForm = (props) => {
}
})
});
-
+
if (response.ok) {
- window.location.href = props.survey?.id ? `/surveys/${props.survey.id}` : '/surveys';
+ const data = await response.json();
+ if (data.redirect_url) {
+ window.location.href = data.redirect_url;
+ } else {
+ window.location.href = props.survey?.id ? `/surveys/${props.survey.id}` : '/surveys';
+ }
} else {
const data = await response.json();
setErrors(data.errors || ['An error occurred']);
diff --git a/app/javascript/components/TakeSurvey.jsx b/app/javascript/components/TakeSurvey.jsx
index f30a8ab..368d7ab 100644
--- a/app/javascript/components/TakeSurvey.jsx
+++ b/app/javascript/components/TakeSurvey.jsx
@@ -130,13 +130,15 @@ const TakeSurvey = (props) => {
content: responses[questionId]
}));
- const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
-
+ const csrfTokenElement = document.querySelector('meta[name="csrf-token"]');
+ const csrfToken = csrfTokenElement ? csrfTokenElement.content : '';
+
try {
const response = await fetch(`/surveys/${survey.id}/submit`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
+ 'Accept': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({
@@ -153,7 +155,8 @@ const TakeSurvey = (props) => {
window.scrollTo(0, 0);
} else {
const data = await response.json();
- setErrors(data.errors || ['There was an error submitting your response.']);
+ const errorMessages = data.errors || (data.error ? [data.error] : ['There was an error submitting your response.']);
+ setErrors(errorMessages);
}
} catch (error) {
console.error('Error submitting response:', error);
@@ -342,9 +345,6 @@ const TakeSurvey = (props) => {
return (
-
{survey.title}
-
{survey.description}
-
{errors.length > 0 && (
diff --git a/test/system/questions_test.rb b/test/system/questions_test.rb
index c6a1758..baf1081 100644
--- a/test/system/questions_test.rb
+++ b/test/system/questions_test.rb
@@ -3,18 +3,25 @@
class QuestionsTest < ApplicationSystemTestCase
test "user can add, edit, and remove questions from a survey" do
visit surveys_path
- click_on "Edit", match: :first
+ click_on "View", match: :first
click_on "Add Question"
- fill_in "Content", with: "System test question"
- select "Text", from: "Question type"
+ 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"
- click_on "Edit", match: :first
- fill_in "Content", with: "Updated 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 "Updated system test question"
- click_on "Delete", match: :first
- assert_no_text "Updated system test question"
+ assert_text "Question was successfully updated"
+ within("li", text: "Updated system test question") do
+ accept_confirm do
+ click_on "Delete", match: :first
+ end
+ end
+ # Wait for the question to be removed from the DOM
+ assert_no_selector "li", text: "Updated system test question", wait: 10
end
end
\ No newline at end of file
diff --git a/test/system/responses_test.rb b/test/system/responses_test.rb
index c32d70d..34ead7a 100644
--- a/test/system/responses_test.rb
+++ b/test/system/responses_test.rb
@@ -5,16 +5,31 @@ class ResponsesTest < ApplicationSystemTestCase
visit surveys_path
click_on "Take Survey", match: :first
select "FRONTEND ENGINEER", from: "role-select"
- fill_in "Answer", with: "System test response"
- click_on "Submit"
- assert_text "Thank you for completing the survey!"
+
+ # 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"
- click_on "Submit"
- assert_text "Please answer at least one question"
- end
+
+ # 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
index acc6332..634e1e8 100644
--- a/test/system/roles_test.rb
+++ b/test/system/roles_test.rb
@@ -11,7 +11,13 @@ class RolesTest < ApplicationSystemTestCase
fill_in "Name", with: "Updated System Test Role"
click_on "Update Role"
assert_text "Updated System Test Role"
- click_on "Delete", match: :first
+ click_on "Back"
+ # Accept the confirmation alert when clicking 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"
end
end
\ No newline at end of file
diff --git a/test/system/surveys_test.rb b/test/system/surveys_test.rb
index 28b66e2..4cd631f 100644
--- a/test/system/surveys_test.rb
+++ b/test/system/surveys_test.rb
@@ -8,11 +8,18 @@ class SurveysTest < ApplicationSystemTestCase
fill_in "Description", with: "Survey for system test"
click_on "Create Survey"
assert_text "System Test Survey"
- click_on "Edit"
+ 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 "Delete"
+ click_on "Back to Surveys"
+ within("li", text: "Updated System Test Survey") do
+ accept_confirm do
+ click_on "Delete"
+ end
+ end
assert_no_text "Updated System Test Survey"
end
From fd740570560b1523ab23e39b381d0c896a7e8e72 Mon Sep 17 00:00:00 2001
From: Michael Deol
Date: Thu, 9 Oct 2025 14:20:30 -0600
Subject: [PATCH 13/19] Initial start of basic analytics of branches
---
.../branch_analytics_controller.rb | 140 ++++++++++++
app/views/branch_analytics/show.html.erb | 213 ++++++++++++++++++
app/views/surveys/show.html.erb | 1 +
config/routes.rb | 2 +-
4 files changed, 355 insertions(+), 1 deletion(-)
create mode 100644 app/controllers/branch_analytics_controller.rb
create mode 100644 app/views/branch_analytics/show.html.erb
diff --git a/app/controllers/branch_analytics_controller.rb b/app/controllers/branch_analytics_controller.rb
new file mode 100644
index 0000000..e742e20
--- /dev/null
+++ b/app/controllers/branch_analytics_controller.rb
@@ -0,0 +1,140 @@
+class BranchAnalyticsController < ApplicationController
+ before_action :set_survey
+
+ def show
+ @analytics = {
+ overview: survey_overview,
+ by_branch: branch_response_summary,
+ question_analysis: question_breakdown,
+ completion_stats: completion_statistics
+ }
+ end
+
+ private
+
+ def set_survey
+ @survey = Survey.find(params[:survey_id])
+ end
+
+ def survey_overview
+ {
+ total_responses: @survey.responses.count,
+ total_branches: @survey.survey_branches.count,
+ total_questions: @survey.questions.count,
+ response_rate: calculate_overall_response_rate
+ }
+ end
+
+ def branch_response_summary
+ @survey.survey_branches.includes(:role, :branch_questions, branch_questions: { question: :responses }).map do |branch|
+ # Get responses for questions that belong to this branch
+ branch_responses = Response.joins(:question)
+ .joins("JOIN branch_questions ON branch_questions.question_id = questions.id")
+ .where(branch_questions: { survey_branch_id: branch.id })
+ .where(survey_id: @survey.id)
+
+ {
+ role: branch.role.name,
+ branch_id: branch.id,
+ total_responses: branch_responses.count,
+ unique_respondents: branch_responses.distinct.count('responses.id'),
+ questions_count: branch.branch_questions.count,
+ avg_response_length: calculate_avg_response_length(branch_responses),
+ completion_rate: calculate_branch_completion_rate(branch),
+ response_distribution: response_type_distribution(branch)
+ }
+ end
+ end
+
+ def question_breakdown
+ @survey.questions.includes(:responses, :branch_questions, branch_questions: :survey_branch).map do |question|
+ {
+ question_id: question.id,
+ content: question.content,
+ question_type: question.question_type,
+ total_responses: question.responses.where(survey_id: @survey.id).count,
+ branches: question.branch_questions.includes(:survey_branch).map do |branch_question|
+ branch_responses = question.responses.where(survey_id: @survey.id)
+ {
+ role: branch_question.survey_branch.role.name,
+ response_count: branch_responses.count,
+ sample_responses: branch_responses.limit(3).pluck(:value)
+ }
+ end
+ }
+ end
+ end
+
+ def completion_statistics
+ {
+ response_timeline: response_timeline_data,
+ most_active_branch: most_active_branch,
+ least_active_branch: least_active_branch,
+ average_responses_per_branch: average_responses_per_branch
+ }
+ end
+
+ def calculate_overall_response_rate
+ total_possible = @survey.survey_branches.joins(:branch_questions).count
+ return 0 if total_possible.zero?
+
+ (@survey.responses.count.to_f / total_possible * 100).round(2)
+ end
+
+ def calculate_avg_response_length(responses)
+ text_responses = responses.where.not(value: [nil, ''])
+ return 0 if text_responses.empty?
+
+ (text_responses.sum { |r| r.value.to_s.length } / text_responses.count.to_f).round(2)
+ end
+
+ def calculate_branch_completion_rate(branch)
+ total_questions = branch.branch_questions.count
+ return 0 if total_questions.zero?
+
+ branch_responses = Response.joins(:question)
+ .joins("JOIN branch_questions ON branch_questions.question_id = questions.id")
+ .where(branch_questions: { survey_branch_id: branch.id })
+ .where(survey_id: @survey.id)
+
+ (branch_responses.count.to_f / total_questions * 100).round(2)
+ end
+
+ def response_type_distribution(branch)
+ Response.joins(:question)
+ .joins("JOIN branch_questions ON branch_questions.question_id = questions.id")
+ .where(branch_questions: { survey_branch_id: branch.id })
+ .where(survey_id: @survey.id)
+ .group('questions.question_type')
+ .count
+ end
+
+ def response_timeline_data
+ @survey.responses
+ .where('created_at >= ?', 30.days.ago)
+ .group("DATE(created_at)")
+ .order("DATE(created_at)")
+ .count
+ end
+
+ def most_active_branch
+ branch_data = branch_response_summary
+ return nil if branch_data.empty?
+
+ branch_data.max_by { |b| b[:total_responses] }
+ end
+
+ def least_active_branch
+ branch_data = branch_response_summary
+ return nil if branch_data.empty?
+
+ branch_data.min_by { |b| b[:total_responses] }
+ end
+
+ def average_responses_per_branch
+ branch_data = branch_response_summary
+ return 0 if branch_data.empty?
+
+ (branch_data.sum { |b| b[:total_responses] } / branch_data.count.to_f).round(2)
+ end
+end
\ No newline at end of file
diff --git a/app/views/branch_analytics/show.html.erb b/app/views/branch_analytics/show.html.erb
new file mode 100644
index 0000000..a6b0d69
--- /dev/null
+++ b/app/views/branch_analytics/show.html.erb
@@ -0,0 +1,213 @@
+
+
+
+
Analytics for "<%= @survey.title %>"
+
<%= @survey.description %>
+
+ <%= link_to "← Back to Survey", survey_path(@survey), class: "mt-4 inline-flex items-center text-indigo-600 hover:text-indigo-900" %>
+
+
+
+
+
+
+
+
+
+
+ Total Responses
+ <%= @analytics[:overview][:total_responses] %>
+
+
+
+
+
+
+
+
+
+
+
+
+ Active Branches
+ <%= @analytics[:overview][:total_branches] %>
+
+
+
+
+
+
+
+
+
+
+
+
+ Total Questions
+ <%= @analytics[:overview][:total_questions] %>
+
+
+
+
+
+
+
+
+
+
+
+
+ Response Rate
+ <%= @analytics[:overview][:response_rate] %>%
+
+
+
+
+
+
+
+
+
+
+
Branch Comparison
+
Response data breakdown by role/branch
+
+
+
+
+
+ Role
+ Responses
+ Questions
+ Completion Rate
+ Avg Response Length
+
+
+
+ <% @analytics[:by_branch].each do |branch| %>
+
+
+ <%= branch[:role] %>
+
+
+ <%= branch[:total_responses] %>
+ <%= pluralize(branch[:unique_respondents], 'unique respondent') %>
+
+
+ <%= branch[:questions_count] %>
+
+
+
+
+
<%= branch[:completion_rate] %>%
+
+
+
+ <%= branch[:avg_response_length] %> chars
+
+
+ <% end %>
+
+
+
+
+
+
+
+
+
Question Analysis
+
Response breakdown by question across branches
+
+
+ <% @analytics[:question_analysis].each do |question| %>
+
+
+
+
<%= question[:content] %>
+
Type: <%= question[:question_type].humanize %> • Total responses: <%= question[:total_responses] %>
+
+
+
+
+ <% question[:branches].each do |branch| %>
+
+
+ <%= branch[:role] %>
+ <%= pluralize(branch[:response_count], 'response') %>
+
+ <% if branch[:sample_responses].any? %>
+
+
Sample responses:
+ <% branch[:sample_responses].each do |response| %>
+
"<%= truncate(response, length: 50) %>"
+ <% end %>
+
+ <% end %>
+
+ <% end %>
+
+
+ <% end %>
+
+
+
+
+
+
+
Completion Statistics
+
Activity insights and trends
+
+
+
+
+
Most Active Branch
+
+ <% if @analytics[:completion_stats][:most_active_branch] %>
+ <%= @analytics[:completion_stats][:most_active_branch][:role] %>
+ (<%= @analytics[:completion_stats][:most_active_branch][:total_responses] %> responses)
+ <% else %>
+ No data available
+ <% end %>
+
+
+
+
Least Active Branch
+
+ <% if @analytics[:completion_stats][:least_active_branch] %>
+ <%= @analytics[:completion_stats][:least_active_branch][:role] %>
+ (<%= @analytics[:completion_stats][:least_active_branch][:total_responses] %> responses)
+ <% else %>
+ No data available
+ <% end %>
+
+
+
+
Average Responses per Branch
+ <%= @analytics[:completion_stats][:average_responses_per_branch] %>
+
+
+
Survey Created
+ <%= @survey.created_at.strftime("%B %d, %Y") %>
+
+
+
+
+
\ No newline at end of file
diff --git a/app/views/surveys/show.html.erb b/app/views/surveys/show.html.erb
index 7db67fb..42f6b10 100644
--- a/app/views/surveys/show.html.erb
+++ b/app/views/surveys/show.html.erb
@@ -5,6 +5,7 @@
<%= @survey.description %>
+ <%= link_to "Analytics", survey_analytics_path(@survey), class: "inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" %>
<%= link_to "Edit", edit_survey_path(@survey), class: "inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-yellow-700 bg-yellow-100 hover:bg-yellow-200" %>
<%= link_to "Take Survey", "#", class: "take-survey-link inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-green-700 bg-green-100 hover:bg-green-200", data: { survey_id: @survey.id } %>
<%= link_to "Back to Surveys", surveys_path, class: "inline-flex items-center px-3 py-1 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50" %>
diff --git a/config/routes.rb b/config/routes.rb
index 0cfc9c1..780c9be 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -10,7 +10,7 @@
resources :surveys do
# Nested routes for questions
resources :questions, except: [:show]
-
+ get 'analytics', to: 'branch_analytics#show'
# Route for taking a survey
member do
get 'take'
From 0c0b98eb143d01deeeb81ab904d7aac9d7fd2652 Mon Sep 17 00:00:00 2001
From: Michael Deol
Date: Fri, 10 Oct 2025 15:41:24 -0600
Subject: [PATCH 14/19] Add charts to Analytics / Add uniqueness to roles
---
Gemfile | 5 +-
.../branch_analytics_controller.rb | 109 ++++++++++++++----
app/controllers/surveys_controller.rb | 29 ++++-
app/javascript/components/AnalyticsCharts.jsx | 85 ++++++++++++++
app/javascript/components/RoleModal.jsx | 6 +-
app/javascript/components/RoleSelect.jsx | 6 +-
app/javascript/components/TakeSurvey.jsx | 2 +-
app/javascript/components/charts/BarChart.jsx | 57 +++++++++
.../components/charts/ChartColors.js | 28 +++++
.../components/charts/LineChart.jsx | 77 +++++++++++++
app/javascript/components/charts/PieChart.jsx | 44 +++++++
app/javascript/components/index.js | 16 ++-
app/models/response.rb | 1 +
app/models/role.rb | 10 +-
app/models/survey_branch.rb | 10 ++
app/views/branch_analytics/show.html.erb | 67 ++++++++++-
app/views/surveys/take.html.erb | 2 +-
...10202632_add_survey_branch_to_responses.rb | 5 +
...10212936_add_unique_index_to_roles_name.rb | 26 +++++
db/schema.rb | 6 +-
lib/tasks/fix_responses.rake | 49 ++++++++
package-lock.json | 30 +++++
package.json | 8 +-
23 files changed, 633 insertions(+), 45 deletions(-)
create mode 100644 app/javascript/components/AnalyticsCharts.jsx
create mode 100644 app/javascript/components/charts/BarChart.jsx
create mode 100644 app/javascript/components/charts/ChartColors.js
create mode 100644 app/javascript/components/charts/LineChart.jsx
create mode 100644 app/javascript/components/charts/PieChart.jsx
create mode 100644 db/migrate/20251010202632_add_survey_branch_to_responses.rb
create mode 100644 db/migrate/20251010212936_add_unique_index_to_roles_name.rb
create mode 100644 lib/tasks/fix_responses.rake
diff --git a/Gemfile b/Gemfile
index 6473e19..783cfef 100644
--- a/Gemfile
+++ b/Gemfile
@@ -33,7 +33,10 @@ gem "acts_as_list"
# Build JSON APIs with ease [https://github.com/rails/jbuilder]
gem "jbuilder"
-gem 'concurrent-ruby', '1.3.4'
+gem 'concurrent-ruby', '1.3.4'
+
+# Charting library for analytics
+
# Use Redis adapter to run Action Cable in production
# gem "redis", "~> 4.0"
diff --git a/app/controllers/branch_analytics_controller.rb b/app/controllers/branch_analytics_controller.rb
index e742e20..cb6a3ac 100644
--- a/app/controllers/branch_analytics_controller.rb
+++ b/app/controllers/branch_analytics_controller.rb
@@ -6,8 +6,10 @@ def show
overview: survey_overview,
by_branch: branch_response_summary,
question_analysis: question_breakdown,
- completion_stats: completion_statistics
+ completion_stats: completion_statistics,
+ charts: chart_data
}
+ @chart_data_json = chart_data.to_json
end
private
@@ -26,18 +28,15 @@ def survey_overview
end
def branch_response_summary
- @survey.survey_branches.includes(:role, :branch_questions, branch_questions: { question: :responses }).map do |branch|
- # Get responses for questions that belong to this branch
- branch_responses = Response.joins(:question)
- .joins("JOIN branch_questions ON branch_questions.question_id = questions.id")
- .where(branch_questions: { survey_branch_id: branch.id })
- .where(survey_id: @survey.id)
+ @survey.survey_branches.includes(:role, :branch_questions).map do |branch|
+ # Get responses specifically for this survey branch
+ branch_responses = @survey.responses.where(survey_branch: branch)
{
role: branch.role.name,
branch_id: branch.id,
total_responses: branch_responses.count,
- unique_respondents: branch_responses.distinct.count('responses.id'),
+ unique_respondents: branch_responses.distinct.count,
questions_count: branch.branch_questions.count,
avg_response_length: calculate_avg_response_length(branch_responses),
completion_rate: calculate_branch_completion_rate(branch),
@@ -47,16 +46,16 @@ def branch_response_summary
end
def question_breakdown
- @survey.questions.includes(:responses, :branch_questions, branch_questions: :survey_branch).map do |question|
+ @survey.questions.includes(:responses, :branch_questions).map do |question|
{
question_id: question.id,
content: question.content,
question_type: question.question_type,
total_responses: question.responses.where(survey_id: @survey.id).count,
- branches: question.branch_questions.includes(:survey_branch).map do |branch_question|
- branch_responses = question.responses.where(survey_id: @survey.id)
+ branches: @survey.survey_branches.includes(:role).map do |branch|
+ branch_responses = question.responses.where(survey_id: @survey.id, survey_branch: branch)
{
- role: branch_question.survey_branch.role.name,
+ role: branch.role.name,
response_count: branch_responses.count,
sample_responses: branch_responses.limit(3).pluck(:value)
}
@@ -92,21 +91,16 @@ def calculate_branch_completion_rate(branch)
total_questions = branch.branch_questions.count
return 0 if total_questions.zero?
- branch_responses = Response.joins(:question)
- .joins("JOIN branch_questions ON branch_questions.question_id = questions.id")
- .where(branch_questions: { survey_branch_id: branch.id })
- .where(survey_id: @survey.id)
+ branch_responses = @survey.responses.where(survey_branch: branch)
(branch_responses.count.to_f / total_questions * 100).round(2)
end
def response_type_distribution(branch)
- Response.joins(:question)
- .joins("JOIN branch_questions ON branch_questions.question_id = questions.id")
- .where(branch_questions: { survey_branch_id: branch.id })
- .where(survey_id: @survey.id)
- .group('questions.question_type')
- .count
+ @survey.responses.where(survey_branch: branch)
+ .joins(:question)
+ .group('questions.question_type')
+ .count
end
def response_timeline_data
@@ -137,4 +131,75 @@ def average_responses_per_branch
(branch_data.sum { |b| b[:total_responses] } / branch_data.count.to_f).round(2)
end
+
+ def chart_data
+ {
+ responses_by_branch: responses_by_branch_chart,
+ completion_rates: completion_rates_chart,
+ response_timeline: response_timeline_chart,
+ question_types: question_types_chart,
+ daily_responses: daily_responses_chart
+ }
+ end
+
+ def responses_by_branch_chart
+ @survey.survey_branches.includes(:role).map do |branch|
+ [branch.role.name, @survey.responses.where(survey_branch: branch).count]
+ end.to_h
+ end
+
+ def completion_rates_chart
+ @survey.survey_branches.includes(:role).map do |branch|
+ total_questions = branch.branch_questions.count
+ responses_count = @survey.responses.where(survey_branch: branch).count
+ completion_rate = total_questions > 0 ? (responses_count.to_f / total_questions * 100).round(1) : 0
+ [branch.role.name, completion_rate]
+ end.to_h
+ end
+
+ def response_timeline_chart
+ # Manual grouping by day for last 14 days
+ end_date = Date.current
+ start_date = end_date - 13.days
+
+ # Get responses within date range
+ responses_in_range = @survey.responses.where(created_at: start_date.beginning_of_day..end_date.end_of_day)
+
+ # Group by date and count
+ grouped_responses = responses_in_range.group("DATE(created_at)").count
+
+ # Fill in missing dates with 0 count
+ result = {}
+ (start_date..end_date).each do |date|
+ date_key = date.strftime("%Y-%m-%d")
+ result[date_key] = grouped_responses[date] || 0
+ end
+
+ result
+ end
+
+ def question_types_chart
+ @survey.questions.group(:question_type).count.transform_keys(&:humanize)
+ end
+
+ def daily_responses_chart
+ # Manual grouping by day for last 7 days
+ end_date = Date.current
+ start_date = end_date - 6.days
+
+ # Get responses within date range
+ responses_in_range = @survey.responses.where(created_at: start_date.beginning_of_day..end_date.end_of_day)
+
+ # Group by date and count
+ grouped_responses = responses_in_range.group("DATE(created_at)").count
+
+ # Fill in missing dates with 0 count
+ result = {}
+ (start_date..end_date).each do |date|
+ date_key = date.strftime("%Y-%m-%d")
+ result[date_key] = grouped_responses[date] || 0
+ end
+
+ result
+ end
end
\ No newline at end of file
diff --git a/app/controllers/surveys_controller.rb b/app/controllers/surveys_controller.rb
index fde11cb..706044a 100644
--- a/app/controllers/surveys_controller.rb
+++ b/app/controllers/surveys_controller.rb
@@ -58,20 +58,39 @@ def destroy
def take
@survey = Survey.find(params[:id])
- @role = Role.find(params[:role])
+ @role = Role.find_by(id: params[:role])
+
+ if @role.nil?
+ redirect_to survey_path(@survey), alert: 'Invalid role specified.'
+ return
+ end
+
@branch = @survey.survey_branches.find_by(role: @role)
+
+ if @branch.nil?
+ redirect_to survey_path(@survey), alert: 'No questions available for this role.'
+ return
+ end
+
@questions = Question.joins(:branch_questions)
.where(branch_questions: { survey_branch_id: @branch.id })
.order(:position)
end
def submit
- responses = params[:response][:responses]
- if responses.present? && responses.any?
- responses.each do |response_params|
+ responses_data = params[:response][:responses]
+ selected_role_id = params[:response][:role_id] || params[:role_id] # Check both locations
+
+ if responses_data.present? && responses_data.any?
+ # Find the survey branch for the selected role
+ selected_role = Role.find_by(id: selected_role_id)
+ survey_branch = @survey.survey_branches.find_by(role: selected_role) if selected_role
+
+ responses_data.each do |response_params|
@survey.responses.create(
question_id: response_params[:question_id],
value: response_params[:content],
+ survey_branch: survey_branch
)
end
respond_to do |format|
@@ -84,7 +103,7 @@ def submit
format.json { render json: { error: 'Please answer at least one question.' }, status: :unprocessable_entity }
end
end
- end
+ end
private
diff --git a/app/javascript/components/AnalyticsCharts.jsx b/app/javascript/components/AnalyticsCharts.jsx
new file mode 100644
index 0000000..17072f0
--- /dev/null
+++ b/app/javascript/components/AnalyticsCharts.jsx
@@ -0,0 +1,85 @@
+import React from 'react';
+import PieChart from './charts/PieChart';
+import BarChart from './charts/BarChart';
+import LineChart from './charts/LineChart';
+
+const AnalyticsCharts = ({ chartData }) => {
+ if (!chartData) {
+ return Loading charts...
;
+ }
+
+ const {
+ responses_by_branch,
+ completion_rates,
+ response_timeline,
+ question_types,
+ daily_responses
+ } = chartData;
+
+ return (
+
+ {/* Charts Section */}
+
+ {/* Responses by Branch Chart */}
+
+
+
Responses by Role
+
Total responses received per role
+
+
+
+
+ {/* Completion Rates Chart */}
+
+
+
Completion Rates by Role
+
Percentage of questions answered per role
+
+
+
+
+
+
+
+ {/* Timeline Charts */}
+
+ {/* Response Timeline */}
+
+
+
Response Timeline
+
Responses received over the last 14 days
+
+
+
+
+
+
+ {/* Question Types Distribution */}
+
+
+
Question Types
+
Distribution of question types in this survey
+
+
+
+
+
+ {/* Daily Activity Chart */}
+
+
+
Daily Activity
+
Response activity over the last 7 days
+
+
+
+
+
+
+ );
+};
+
+export default AnalyticsCharts;
\ No newline at end of file
diff --git a/app/javascript/components/RoleModal.jsx b/app/javascript/components/RoleModal.jsx
index 98daec1..f2d2705 100644
--- a/app/javascript/components/RoleModal.jsx
+++ b/app/javascript/components/RoleModal.jsx
@@ -44,9 +44,9 @@ const initializeRoleModal = () => {
open={true}
roles={roles} // Provide actual roles array
value={""} // Provide actual value if needed
- onSelect={async (role) => {
- // Redirect to /surveys/:id/take?role=selectedRole
- window.location.href = `/surveys/${surveyId}/take?role=${encodeURIComponent(role)}`;
+ onSelect={async (roleId) => {
+ // Redirect to /surveys/:id/take?role=selectedRoleId
+ window.location.href = `/surveys/${surveyId}/take?role=${roleId}`;
root.render(null); // Close modal after selection}} // Provide actual handler
}}
onClose={() => {
diff --git a/app/javascript/components/RoleSelect.jsx b/app/javascript/components/RoleSelect.jsx
index 7a049e9..385cb65 100644
--- a/app/javascript/components/RoleSelect.jsx
+++ b/app/javascript/components/RoleSelect.jsx
@@ -15,9 +15,9 @@ const RoleSelect = ({ roles, value, onChange, label = "Select Role" }) => {
>
-- Choose a role --
{roles.map(role => (
-
- {role.name}
-
+
+ {role.name}
+
))}
diff --git a/app/javascript/components/TakeSurvey.jsx b/app/javascript/components/TakeSurvey.jsx
index 368d7ab..f751d8d 100644
--- a/app/javascript/components/TakeSurvey.jsx
+++ b/app/javascript/components/TakeSurvey.jsx
@@ -144,7 +144,7 @@ const TakeSurvey = (props) => {
body: JSON.stringify({
response: {
survey_id: survey.id,
- role: role,
+ role_id: role,
responses: formattedResponses
}
})
diff --git a/app/javascript/components/charts/BarChart.jsx b/app/javascript/components/charts/BarChart.jsx
new file mode 100644
index 0000000..6554ccf
--- /dev/null
+++ b/app/javascript/components/charts/BarChart.jsx
@@ -0,0 +1,57 @@
+import React from 'react';
+import { Bar } from 'react-chartjs-2';
+import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend } from 'chart.js';
+import { CHART_COLORS, CHART_OPTIONS } from './ChartColors';
+
+ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend);
+
+const BarChart = ({ data, title, height = 300, suffix = '' }) => {
+ const chartData = {
+ labels: Object.keys(data),
+ datasets: [
+ {
+ data: Object.values(data),
+ backgroundColor: CHART_COLORS.primary,
+ borderColor: CHART_COLORS.primary,
+ borderWidth: 2,
+ },
+ ],
+ };
+
+ const options = {
+ ...CHART_OPTIONS,
+ plugins: {
+ ...CHART_OPTIONS.plugins,
+ title: {
+ display: !!title,
+ text: title,
+ font: {
+ size: 16,
+ weight: 'bold',
+ },
+ padding: 20,
+ },
+ legend: {
+ display: false, // Hide legend for bar charts unless needed
+ },
+ },
+ scales: {
+ y: {
+ beginAtZero: true,
+ ticks: {
+ callback: function(value) {
+ return value + suffix;
+ }
+ }
+ },
+ },
+ };
+
+ return (
+
+
+
+ );
+};
+
+export default BarChart;
\ No newline at end of file
diff --git a/app/javascript/components/charts/ChartColors.js b/app/javascript/components/charts/ChartColors.js
new file mode 100644
index 0000000..a46b41b
--- /dev/null
+++ b/app/javascript/components/charts/ChartColors.js
@@ -0,0 +1,28 @@
+// Consistent color scheme for all charts
+export const CHART_COLORS = {
+ primary: ['#6366f1', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#06b6d4'],
+ gradients: {
+ blue: {
+ backgroundColor: 'rgba(99, 102, 241, 0.2)',
+ borderColor: 'rgba(99, 102, 241, 1)'
+ },
+ green: {
+ backgroundColor: 'rgba(16, 185, 129, 0.2)',
+ borderColor: 'rgba(16, 185, 129, 1)'
+ }
+ }
+};
+
+export const CHART_OPTIONS = {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ legend: {
+ position: 'bottom',
+ labels: {
+ padding: 20,
+ usePointStyle: true
+ }
+ }
+ }
+};
\ No newline at end of file
diff --git a/app/javascript/components/charts/LineChart.jsx b/app/javascript/components/charts/LineChart.jsx
new file mode 100644
index 0000000..02bba68
--- /dev/null
+++ b/app/javascript/components/charts/LineChart.jsx
@@ -0,0 +1,77 @@
+import React from 'react';
+import { Line } from 'react-chartjs-2';
+import { Chart as ChartJS, CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend, Filler } from 'chart.js';
+import { CHART_COLORS, CHART_OPTIONS } from './ChartColors';
+
+ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend, Filler);
+
+const LineChart = ({ data, title, height = 300, fill = false }) => {
+ // Handle both object and array data formats
+ let labels, values;
+ if (Array.isArray(data)) {
+ labels = data.map(item => item[0]);
+ values = data.map(item => item[1]);
+ } else {
+ labels = Object.keys(data);
+ values = Object.values(data);
+ }
+
+ const chartData = {
+ labels: labels,
+ datasets: [
+ {
+ data: values,
+ backgroundColor: fill ? CHART_COLORS.gradients.blue.backgroundColor : 'transparent',
+ borderColor: CHART_COLORS.gradients.blue.borderColor,
+ borderWidth: 3,
+ fill: fill,
+ tension: 0.4,
+ pointBackgroundColor: CHART_COLORS.gradients.blue.borderColor,
+ pointBorderColor: '#fff',
+ pointBorderWidth: 2,
+ pointRadius: 5,
+ pointHoverRadius: 7,
+ },
+ ],
+ };
+
+ const options = {
+ ...CHART_OPTIONS,
+ plugins: {
+ ...CHART_OPTIONS.plugins,
+ title: {
+ display: !!title,
+ text: title,
+ font: {
+ size: 16,
+ weight: 'bold',
+ },
+ padding: 20,
+ },
+ legend: {
+ display: false, // Hide legend for line charts unless needed
+ },
+ },
+ scales: {
+ y: {
+ beginAtZero: true,
+ grid: {
+ color: 'rgba(0, 0, 0, 0.1)',
+ },
+ },
+ x: {
+ grid: {
+ color: 'rgba(0, 0, 0, 0.1)',
+ },
+ },
+ },
+ };
+
+ return (
+
+
+
+ );
+};
+
+export default LineChart;
\ No newline at end of file
diff --git a/app/javascript/components/charts/PieChart.jsx b/app/javascript/components/charts/PieChart.jsx
new file mode 100644
index 0000000..38c9dd2
--- /dev/null
+++ b/app/javascript/components/charts/PieChart.jsx
@@ -0,0 +1,44 @@
+import React from 'react';
+import { Pie } from 'react-chartjs-2';
+import { Chart as ChartJS, ArcElement, Tooltip, Legend } from 'chart.js';
+import { CHART_COLORS, CHART_OPTIONS } from './ChartColors';
+
+ChartJS.register(ArcElement, Tooltip, Legend);
+
+const PieChart = ({ data, title, height = 300 }) => {
+ const chartData = {
+ labels: Object.keys(data),
+ datasets: [
+ {
+ data: Object.values(data),
+ backgroundColor: CHART_COLORS.primary,
+ borderColor: CHART_COLORS.primary,
+ borderWidth: 2,
+ },
+ ],
+ };
+
+ const options = {
+ ...CHART_OPTIONS,
+ plugins: {
+ ...CHART_OPTIONS.plugins,
+ title: {
+ display: !!title,
+ text: title,
+ font: {
+ size: 16,
+ weight: 'bold',
+ },
+ padding: 20,
+ },
+ },
+ };
+
+ return (
+
+ );
+};
+
+export default PieChart;
\ No newline at end of file
diff --git a/app/javascript/components/index.js b/app/javascript/components/index.js
index 2d5af9f..02fc368 100644
--- a/app/javascript/components/index.js
+++ b/app/javascript/components/index.js
@@ -1,9 +1,23 @@
// Import all React components here
+import React from "react"
+import { createRoot } from "react-dom/client"
import SurveyForm from "./SurveyForm"
import QuestionList from "./QuestionList"
import TakeSurvey from "./TakeSurvey"
import RoleModal from "./RoleModal"
import RoleSelect from "./RoleSelect"
+import AnalyticsCharts from "./AnalyticsCharts"
+
+// Initialize React components when DOM is loaded
+document.addEventListener('DOMContentLoaded', () => {
+ // Initialize AnalyticsCharts component
+ const analyticsChartsElement = document.getElementById('analytics-charts');
+ if (analyticsChartsElement) {
+ const chartData = JSON.parse(analyticsChartsElement.dataset.chartData);
+ const root = createRoot(analyticsChartsElement);
+ root.render(
);
+ }
+});
// Export components for use elsewhere if needed
-export { SurveyForm, QuestionList, TakeSurvey, RoleModal, RoleSelect }
\ No newline at end of file
+export { SurveyForm, QuestionList, TakeSurvey, RoleModal, RoleSelect, AnalyticsCharts }
\ No newline at end of file
diff --git a/app/models/response.rb b/app/models/response.rb
index ac64330..a54a94c 100644
--- a/app/models/response.rb
+++ b/app/models/response.rb
@@ -2,6 +2,7 @@
class Response < ApplicationRecord
belongs_to :survey
belongs_to :question
+ belongs_to :survey_branch, optional: true
validates :value, presence: true
end
diff --git a/app/models/role.rb b/app/models/role.rb
index b5bcbdc..50fa059 100644
--- a/app/models/role.rb
+++ b/app/models/role.rb
@@ -2,5 +2,13 @@
class Role < ApplicationRecord
has_many :survey_branches, dependent: :destroy
- validates :name, presence: true
+ validates :name, presence: true, uniqueness: { case_sensitive: false }
+
+ before_validation :normalize_name
+
+ private
+
+ def normalize_name
+ self.name = name.strip.upcase if name.present?
+ end
end
diff --git a/app/models/survey_branch.rb b/app/models/survey_branch.rb
index e924bf6..83ea1a9 100644
--- a/app/models/survey_branch.rb
+++ b/app/models/survey_branch.rb
@@ -4,6 +4,16 @@ class SurveyBranch < ApplicationRecord
belongs_to :role
has_many :branch_questions, dependent: :destroy
has_many :questions, through: :branch_questions
+ has_many :responses, dependent: :destroy
validates :role_id, uniqueness: { scope: :survey_id }
+
+ # Helper method to get completion statistics
+ def completion_stats
+ {
+ total_responses: responses.count,
+ total_questions: branch_questions.count,
+ completion_rate: branch_questions.count > 0 ? (responses.count.to_f / branch_questions.count * 100).round(2) : 0
+ }
+ end
end
diff --git a/app/views/branch_analytics/show.html.erb b/app/views/branch_analytics/show.html.erb
index a6b0d69..e42bdbd 100644
--- a/app/views/branch_analytics/show.html.erb
+++ b/app/views/branch_analytics/show.html.erb
@@ -133,10 +133,45 @@
-
Question Analysis
-
Response breakdown by question across branches
+
+
+
Question Analysis
+
Response breakdown by question across branches
+
+
+ Show Details
+
+
+
+
+
+
+
+
+
+
<%= @analytics[:question_analysis].count %>
+
Total Questions
+
+
+
+ <%= @analytics[:question_analysis].sum { |q| q[:total_responses] } %>
+
+
Total Question Responses
+
+
+
+ <%= @analytics[:question_analysis].group_by { |q| q[:question_type] }.keys.count %>
+
+
Question Types
+
+
-
+
+
<% @analytics[:question_analysis].each do |question| %>
+
+
+
+
+
diff --git a/app/views/surveys/take.html.erb b/app/views/surveys/take.html.erb
index 1129146..88c5542 100644
--- a/app/views/surveys/take.html.erb
+++ b/app/views/surveys/take.html.erb
@@ -9,7 +9,7 @@
+ data-role="<%= @role.id %>">
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/schema.rb b/db/schema.rb
index 52df819..1cad60a 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.0].define(version: 2025_08_28_034813) do
+ActiveRecord::Schema[7.0].define(version: 2025_10_10_212936) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -39,7 +39,9 @@
t.text "value"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.bigint "survey_branch_id"
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
@@ -47,6 +49,7 @@
t.string "name", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.index ["name"], name: "index_roles_on_name", unique: true
end
create_table "survey_branches", force: :cascade do |t|
@@ -69,6 +72,7 @@
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"
diff --git a/lib/tasks/fix_responses.rake b/lib/tasks/fix_responses.rake
new file mode 100644
index 0000000..09fd5f8
--- /dev/null
+++ b/lib/tasks/fix_responses.rake
@@ -0,0 +1,49 @@
+namespace :data do
+ desc "Assign survey branches to existing responses"
+ task fix_response_branches: :environment do
+ puts "Checking existing responses..."
+
+ responses_without_branch = Response.where(survey_branch_id: nil)
+ puts "Found #{responses_without_branch.count} responses without survey_branch_id"
+
+ responses_without_branch.each do |response|
+ puts "Response ID: #{response.id}, Survey: #{response.survey.title}, Question: #{response.question.content[0..50]}"
+
+ # Find survey branches that contain this question
+ survey_branches = response.survey.survey_branches.joins(:branch_questions)
+ .where(branch_questions: { question_id: response.question_id })
+
+ if survey_branches.count == 1
+ # Only one branch contains this question, assign it
+ response.update!(survey_branch: survey_branches.first)
+ puts " -> Assigned to branch: #{survey_branches.first.role.name}"
+ elsif survey_branches.count > 1
+ # Multiple branches contain this question, we need to make an assumption
+ # For now, assign to the first branch (you might want to modify this logic)
+ response.update!(survey_branch: survey_branches.first)
+ puts " -> Multiple branches found, assigned to: #{survey_branches.first.role.name}"
+ else
+ puts " -> WARNING: No branch found for this question!"
+ end
+ end
+
+ puts "Done! Updated #{responses_without_branch.count} responses"
+ end
+
+ desc "Show current response statistics"
+ task show_response_stats: :environment do
+ puts "Response Statistics:"
+ puts "Total responses: #{Response.count}"
+ puts "Responses with survey_branch: #{Response.where.not(survey_branch_id: nil).count}"
+ puts "Responses without survey_branch: #{Response.where(survey_branch_id: nil).count}"
+
+ puts "\nBy Survey:"
+ Survey.includes(:responses, :survey_branches).each do |survey|
+ puts " #{survey.title}:"
+ puts " Total responses: #{survey.responses.count}"
+ survey.survey_branches.includes(:role).each do |branch|
+ puts " #{branch.role.name}: #{survey.responses.where(survey_branch: branch).count} responses"
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index eee7b2d..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": {
@@ -476,6 +478,12 @@
"@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",
@@ -1243,6 +1251,18 @@
"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",
@@ -1960,6 +1980,16 @@
"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",
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"
}
}
From 1f0a6fe62c516c4a7dd29f2f21cc4a1d50abefbd Mon Sep 17 00:00:00 2001
From: Michael Deol
Date: Fri, 10 Oct 2025 15:46:30 -0600
Subject: [PATCH 15/19] DRY up some of the logic
---
.../branch_analytics_controller.rb | 60 ++++++++-----------
1 file changed, 26 insertions(+), 34 deletions(-)
diff --git a/app/controllers/branch_analytics_controller.rb b/app/controllers/branch_analytics_controller.rb
index cb6a3ac..eb4faa3 100644
--- a/app/controllers/branch_analytics_controller.rb
+++ b/app/controllers/branch_analytics_controller.rb
@@ -28,9 +28,8 @@ def survey_overview
end
def branch_response_summary
- @survey.survey_branches.includes(:role, :branch_questions).map do |branch|
- # Get responses specifically for this survey branch
- branch_responses = @survey.responses.where(survey_branch: branch)
+ survey_branches_with_associations.map do |branch|
+ branch_responses = responses_for_branch(branch)
{
role: branch.role.name,
@@ -91,16 +90,16 @@ def calculate_branch_completion_rate(branch)
total_questions = branch.branch_questions.count
return 0 if total_questions.zero?
- branch_responses = @survey.responses.where(survey_branch: branch)
+ branch_responses = responses_for_branch(branch)
(branch_responses.count.to_f / total_questions * 100).round(2)
end
def response_type_distribution(branch)
- @survey.responses.where(survey_branch: branch)
- .joins(:question)
- .group('questions.question_type')
- .count
+ responses_for_branch(branch)
+ .joins(:question)
+ .group('questions.question_type')
+ .count
end
def response_timeline_data
@@ -143,39 +142,20 @@ def chart_data
end
def responses_by_branch_chart
- @survey.survey_branches.includes(:role).map do |branch|
- [branch.role.name, @survey.responses.where(survey_branch: branch).count]
+ survey_branches_with_associations.map do |branch|
+ [branch.role.name, responses_for_branch(branch).count]
end.to_h
end
def completion_rates_chart
- @survey.survey_branches.includes(:role).map do |branch|
- total_questions = branch.branch_questions.count
- responses_count = @survey.responses.where(survey_branch: branch).count
- completion_rate = total_questions > 0 ? (responses_count.to_f / total_questions * 100).round(1) : 0
+ survey_branches_with_associations.map do |branch|
+ completion_rate = calculate_branch_completion_rate(branch).round(1)
[branch.role.name, completion_rate]
end.to_h
end
def response_timeline_chart
- # Manual grouping by day for last 14 days
- end_date = Date.current
- start_date = end_date - 13.days
-
- # Get responses within date range
- responses_in_range = @survey.responses.where(created_at: start_date.beginning_of_day..end_date.end_of_day)
-
- # Group by date and count
- grouped_responses = responses_in_range.group("DATE(created_at)").count
-
- # Fill in missing dates with 0 count
- result = {}
- (start_date..end_date).each do |date|
- date_key = date.strftime("%Y-%m-%d")
- result[date_key] = grouped_responses[date] || 0
- end
-
- result
+ responses_by_date_range(14)
end
def question_types_chart
@@ -183,9 +163,21 @@ def question_types_chart
end
def daily_responses_chart
- # Manual grouping by day for last 7 days
+ responses_by_date_range(7)
+ end
+
+ # Helper methods for DRY code
+ def survey_branches_with_associations
+ @survey_branches_with_associations ||= @survey.survey_branches.includes(:role, :branch_questions)
+ end
+
+ def responses_for_branch(branch)
+ @survey.responses.where(survey_branch: branch)
+ end
+
+ def responses_by_date_range(days)
end_date = Date.current
- start_date = end_date - 6.days
+ start_date = end_date - (days - 1).days
# Get responses within date range
responses_in_range = @survey.responses.where(created_at: start_date.beginning_of_day..end_date.end_of_day)
From 525d90f3e589f98d24a93a9ee4bd70bfe9ec64f0 Mon Sep 17 00:00:00 2001
From: Michael Deol
Date: Fri, 10 Oct 2025 15:59:41 -0600
Subject: [PATCH 16/19] Updated Roles CSS to match Surveys, Updated Analytics
React load on turbo vs DOM
---
app/javascript/components/index.js | 20 ++++++++++--
app/views/roles/edit.html.erb | 18 ++++++++---
app/views/roles/index.html.erb | 49 +++++++++++++++++++++---------
app/views/roles/new.html.erb | 18 ++++++++---
app/views/roles/show.html.erb | 16 +++++++---
5 files changed, 90 insertions(+), 31 deletions(-)
diff --git a/app/javascript/components/index.js b/app/javascript/components/index.js
index 02fc368..4f2fe32 100644
--- a/app/javascript/components/index.js
+++ b/app/javascript/components/index.js
@@ -8,15 +8,29 @@ import RoleModal from "./RoleModal"
import RoleSelect from "./RoleSelect"
import AnalyticsCharts from "./AnalyticsCharts"
-// Initialize React components when DOM is loaded
-document.addEventListener('DOMContentLoaded', () => {
- // Initialize AnalyticsCharts component
+// Initialize React components
+function initializeAnalyticsCharts() {
const analyticsChartsElement = document.getElementById('analytics-charts');
if (analyticsChartsElement) {
const chartData = JSON.parse(analyticsChartsElement.dataset.chartData);
const root = createRoot(analyticsChartsElement);
root.render( );
}
+}
+
+// Initialize React components when DOM is loaded (for initial page load)
+document.addEventListener('DOMContentLoaded', initializeAnalyticsCharts);
+
+// Initialize React components when Turbo navigates to a new page
+document.addEventListener('turbo:load', initializeAnalyticsCharts);
+
+// Clean up when navigating away (optional, helps prevent memory leaks)
+document.addEventListener('turbo:before-cache', () => {
+ const analyticsChartsElement = document.getElementById('analytics-charts');
+ if (analyticsChartsElement) {
+ // Clear the element to prevent issues with Turbo caching
+ analyticsChartsElement.innerHTML = '';
+ }
});
// Export components for use elsewhere if needed
diff --git a/app/views/roles/edit.html.erb b/app/views/roles/edit.html.erb
index 40c9d10..48d5c97 100644
--- a/app/views/roles/edit.html.erb
+++ b/app/views/roles/edit.html.erb
@@ -1,5 +1,15 @@
-
-
Edit Role
- <%= render 'form', role: @role %>
- <%= link_to 'Back', roles_path, class: "inline-block mt-4 px-4 py-2 bg-gray-200 text-gray-700 rounded hover:bg-gray-300" %>
+
+
+
+
Edit Role
+
Update the role information.
+
+
+ <%= link_to 'Back', roles_path, class: "inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" %>
+
+
+
+
+ <%= render 'form', role: @role %>
+
\ No newline at end of file
diff --git a/app/views/roles/index.html.erb b/app/views/roles/index.html.erb
index 78a6e4b..663ab11 100644
--- a/app/views/roles/index.html.erb
+++ b/app/views/roles/index.html.erb
@@ -1,17 +1,36 @@
-
-
-
Roles
- <%= link_to 'New Role', new_role_path, class: "inline-block px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700" %>
+
+
+
+
Roles
+
A list of all available roles.
+
+
+ <%= link_to "New Role", new_role_path, class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" %>
+
-
- <% @roles.each do |role| %>
-
- <%= link_to role.name, role, class: "hover:underline" %>
-
- <%= link_to "Edit", edit_role_path(role), class: "inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-yellow-700 bg-yellow-100 hover:bg-yellow-200" %>
- <%= button_to "Delete", role_path(role), method: :delete, class: "inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-red-700 bg-red-100 hover:bg-red-200", form: { data: { turbo_confirm: "Are you sure you want to delete this role?" } } %>
-
-
- <% end %>
-
+
+ <% if @roles.any? %>
+
+
+ <% @roles.each do |role| %>
+
+
+
+
<%= link_to role.name, role %>
+
+
+ <%= link_to "View", role_path(role), class: "inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-indigo-700 bg-indigo-100 hover:bg-indigo-200" %>
+ <%= link_to "Edit", edit_role_path(role), class: "inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-yellow-700 bg-yellow-100 hover:bg-yellow-200" %>
+ <%= button_to "Delete", role_path(role), method: :delete, class: "inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-red-700 bg-red-100 hover:bg-red-200", form: { data: { turbo_confirm: "Are you sure you want to delete this role?" } } %>
+
+
+
+ <% end %>
+
+
+ <% else %>
+
+ <% end %>
\ No newline at end of file
diff --git a/app/views/roles/new.html.erb b/app/views/roles/new.html.erb
index c69155b..e8d0fc8 100644
--- a/app/views/roles/new.html.erb
+++ b/app/views/roles/new.html.erb
@@ -1,5 +1,15 @@
-
-
New Role
- <%= render 'form', role: @role %>
- <%= link_to 'Back', roles_path, class: "inline-block mt-4 px-4 py-2 bg-gray-200 text-gray-700 rounded hover:bg-gray-300" %>
+
+
+
+
New Role
+
Create a new role for survey branching.
+
+
+ <%= link_to 'Back', roles_path, class: "inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" %>
+
+
+
+
+ <%= render 'form', role: @role %>
+
\ No newline at end of file
diff --git a/app/views/roles/show.html.erb b/app/views/roles/show.html.erb
index 2898e48..8a6a77a 100644
--- a/app/views/roles/show.html.erb
+++ b/app/views/roles/show.html.erb
@@ -1,7 +1,13 @@
-
-
Role: <%= @role.name %>
-
- <%= link_to 'Edit', edit_role_path(@role), class: "px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700" %>
- <%= link_to 'Back', roles_path, class: "px-4 py-2 bg-gray-200 text-gray-700 rounded hover:bg-gray-300" %>
+
+
+
+
+
Role: <%= @role.name %>
+
+
+ <%= link_to 'Edit', edit_role_path(@role), class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" %>
+ <%= link_to 'Back', roles_path, class: "inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" %>
+
+
\ No newline at end of file
From acf0370cf7b2f4fa391938076989bb1e5cb10b03 Mon Sep 17 00:00:00 2001
From: Michael Deol
Date: Fri, 10 Oct 2025 21:59:28 -0600
Subject: [PATCH 17/19] normalize roles
---
.../branch_analytics_controller.rb | 8 +-
app/controllers/questions_controller.rb | 6 +-
app/controllers/surveys_controller.rb | 5 +-
app/javascript/components/QuestionList.jsx | 4 +-
app/javascript/components/RoleSelect.jsx | 6 +-
app/models/role.rb | 13 ++-
app/views/questions/_form.html.erb | 2 +-
app/views/roles/index.html.erb | 2 +-
app/views/roles/show.html.erb | 2 +-
app/views/surveys/show.html.erb | 4 +-
db/seeds.rb | 93 +++++++++++++++++--
lib/tasks/fix_responses.rake | 6 +-
test/fixtures/roles.yml | 8 +-
test/system/questions_test.rb | 2 +-
test/system/responses_test.rb | 4 +-
test/system/survey_branches_test.rb | 2 +-
test/system/surveys_test.rb | 2 +-
17 files changed, 132 insertions(+), 37 deletions(-)
diff --git a/app/controllers/branch_analytics_controller.rb b/app/controllers/branch_analytics_controller.rb
index eb4faa3..bf7c156 100644
--- a/app/controllers/branch_analytics_controller.rb
+++ b/app/controllers/branch_analytics_controller.rb
@@ -32,7 +32,7 @@ def branch_response_summary
branch_responses = responses_for_branch(branch)
{
- role: branch.role.name,
+ role: branch.role.display_name,
branch_id: branch.id,
total_responses: branch_responses.count,
unique_respondents: branch_responses.distinct.count,
@@ -54,7 +54,7 @@ def question_breakdown
branches: @survey.survey_branches.includes(:role).map do |branch|
branch_responses = question.responses.where(survey_id: @survey.id, survey_branch: branch)
{
- role: branch.role.name,
+ role: branch.role.display_name,
response_count: branch_responses.count,
sample_responses: branch_responses.limit(3).pluck(:value)
}
@@ -143,14 +143,14 @@ def chart_data
def responses_by_branch_chart
survey_branches_with_associations.map do |branch|
- [branch.role.name, responses_for_branch(branch).count]
+ [branch.role.display_name, responses_for_branch(branch).count]
end.to_h
end
def completion_rates_chart
survey_branches_with_associations.map do |branch|
completion_rate = calculate_branch_completion_rate(branch).round(1)
- [branch.role.name, completion_rate]
+ [branch.role.display_name, completion_rate]
end.to_h
end
diff --git a/app/controllers/questions_controller.rb b/app/controllers/questions_controller.rb
index 8251ef5..d9f8b94 100644
--- a/app/controllers/questions_controller.rb
+++ b/app/controllers/questions_controller.rb
@@ -11,6 +11,10 @@ def index
@questions = @questions.joins(:branch_questions)
.where(branch_questions: { survey_branch_id: params[:survey_branch_id] })
.distinct
+ elsif params[:role_id].present?
+ @questions = @questions.joins(branch_questions: { survey_branch: :role })
+ .where(roles: { id: params[:role_id] })
+ .distinct
end
@questions = @questions.order(:position) if @questions.column_names.include?("position")
@@ -77,7 +81,7 @@ def set_question
end
def set_role
- @role = Role.find_by(name: params[:role].upcase) if params[:role].present?
+ @role = Role.find_by(name: params[:role].downcase) if params[:role].present?
end
def set_roles
diff --git a/app/controllers/surveys_controller.rb b/app/controllers/surveys_controller.rb
index 706044a..1cfc1f5 100644
--- a/app/controllers/surveys_controller.rb
+++ b/app/controllers/surveys_controller.rb
@@ -3,11 +3,12 @@ class SurveysController < ApplicationController
def index
@surveys = Survey.all
- @roles = Role.all
+ @roles = Role.all.map { |role| { id: role.id, name: role.display_name } }
end
def show
- @roles = Role.all
+ @roles = Role.all.map { |role| { id: role.id, name: role.display_name } }
+ @all_roles = Role.all
end
def new
diff --git a/app/javascript/components/QuestionList.jsx b/app/javascript/components/QuestionList.jsx
index ec992d8..06e189e 100644
--- a/app/javascript/components/QuestionList.jsx
+++ b/app/javascript/components/QuestionList.jsx
@@ -5,10 +5,10 @@ const QuestionList = (props) => {
const [questions, setQuestions] = useState(props.questions || []);
const [loading, setLoading] = useState(false);
- const fetchQuestions = async (surveyBranchId = "") => {
+ const fetchQuestions = async (roleId = "") => {
setLoading(true);
try {
- const url = `/surveys/${props.surveyId}/questions.json` + (surveyBranchId ? `?survey_branch_id=${encodeURIComponent(surveyBranchId)}` : "");
+ const url = `/surveys/${props.surveyId}/questions.json` + (roleId ? `?role_id=${encodeURIComponent(roleId)}` : "");
const res = await fetch(url, { headers: { "Accept": "application/json" } });
if (!res.ok) throw new Error("Failed to load questions");
const data = await res.json();
diff --git a/app/javascript/components/RoleSelect.jsx b/app/javascript/components/RoleSelect.jsx
index 385cb65..4c7abb8 100644
--- a/app/javascript/components/RoleSelect.jsx
+++ b/app/javascript/components/RoleSelect.jsx
@@ -15,9 +15,9 @@ const RoleSelect = ({ roles, value, onChange, label = "Select Role" }) => {
>
-- Choose a role --
{roles.map(role => (
-
- {role.name}
-
+
+ {role.name}
+
))}
diff --git a/app/models/role.rb b/app/models/role.rb
index 50fa059..bb41ba7 100644
--- a/app/models/role.rb
+++ b/app/models/role.rb
@@ -6,9 +6,20 @@ class Role < ApplicationRecord
before_validation :normalize_name
+ def display_name
+ return nil unless name.present?
+
+ # Common acronyms that should stay uppercase
+ acronyms = %w[QA UI UX API CEO CTO VP IT HR AI ML]
+
+ name.titleize.split.map do |word|
+ acronyms.include?(word.upcase) ? word.upcase : word
+ end.join(' ')
+ end
+
private
def normalize_name
- self.name = name.strip.upcase if name.present?
+ self.name = name.strip.downcase if name.present?
end
end
diff --git a/app/views/questions/_form.html.erb b/app/views/questions/_form.html.erb
index a3ef6c8..39f111c 100644
--- a/app/views/questions/_form.html.erb
+++ b/app/views/questions/_form.html.erb
@@ -68,7 +68,7 @@
<%= form.collection_select :survey_branch_ids,
@survey.survey_branches.includes(:role),
:id,
- ->(sb) { sb.role.name },
+ ->(sb) { sb.role.display_name },
{ prompt: "Select one or more roles" },
{
multiple: true,
diff --git a/app/views/roles/index.html.erb b/app/views/roles/index.html.erb
index 663ab11..ba13928 100644
--- a/app/views/roles/index.html.erb
+++ b/app/views/roles/index.html.erb
@@ -16,7 +16,7 @@
-
<%= link_to role.name, role %>
+ <%= link_to role.display_name, role %>
<%= link_to "View", role_path(role), class: "inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-indigo-700 bg-indigo-100 hover:bg-indigo-200" %>
diff --git a/app/views/roles/show.html.erb b/app/views/roles/show.html.erb
index 8a6a77a..5ad7f31 100644
--- a/app/views/roles/show.html.erb
+++ b/app/views/roles/show.html.erb
@@ -2,7 +2,7 @@
-
Role: <%= @role.name %>
+ Role: <%= @role.display_name %>
<%= link_to 'Edit', edit_role_path(@role), class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" %>
diff --git a/app/views/surveys/show.html.erb b/app/views/surveys/show.html.erb
index 42f6b10..ab83213 100644
--- a/app/views/surveys/show.html.erb
+++ b/app/views/surveys/show.html.erb
@@ -25,8 +25,8 @@
All Roles
- <% @survey.survey_branches.includes(:role).each do |branch| %>
- <%= branch.role.name %>
+ <% @all_roles.each do |role| %>
+ <%= role.display_name %>
<% end %>
diff --git a/db/seeds.rb b/db/seeds.rb
index 03b5126..4eed8a1 100644
--- a/db/seeds.rb
+++ b/db/seeds.rb
@@ -15,8 +15,8 @@
BranchQuestion.destroy_all
# Create roles
-roles = ["Data Engineer", "Frontend Engineer", "QA Engineer", "UX Designer", "Backend Engineer", "Product Manager"].map do |role_name|
- Role.create!(name: role_name.upcase)
+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
@@ -26,8 +26,9 @@
description: "This survey helps us understand the satisfaction and needs of our engineering team members."
)
-# Create branches for each role
-engineering_survey_branches = roles.map do |role|
+# 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
@@ -113,8 +114,9 @@
description: "Help us understand what went well and what could be improved in our recent project."
)
-# Create branches for each role
-project_retrospective_survey_branches = roles.map do |role|
+# 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
@@ -200,8 +202,14 @@
description: "We value your feedback! Please help us improve our products and services."
)
-# Create branches for each role
-customer_survey_branches = roles.map do |role|
+# 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
@@ -268,4 +276,73 @@
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) }
+
+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/lib/tasks/fix_responses.rake b/lib/tasks/fix_responses.rake
index 09fd5f8..ebd1f1e 100644
--- a/lib/tasks/fix_responses.rake
+++ b/lib/tasks/fix_responses.rake
@@ -16,12 +16,12 @@ namespace :data do
if survey_branches.count == 1
# Only one branch contains this question, assign it
response.update!(survey_branch: survey_branches.first)
- puts " -> Assigned to branch: #{survey_branches.first.role.name}"
+ puts " -> Assigned to branch: #{survey_branches.first.role.display_name}"
elsif survey_branches.count > 1
# Multiple branches contain this question, we need to make an assumption
# For now, assign to the first branch (you might want to modify this logic)
response.update!(survey_branch: survey_branches.first)
- puts " -> Multiple branches found, assigned to: #{survey_branches.first.role.name}"
+ puts " -> Multiple branches found, assigned to: #{survey_branches.first.role.display_name}"
else
puts " -> WARNING: No branch found for this question!"
end
@@ -42,7 +42,7 @@ namespace :data do
puts " #{survey.title}:"
puts " Total responses: #{survey.responses.count}"
survey.survey_branches.includes(:role).each do |branch|
- puts " #{branch.role.name}: #{survey.responses.where(survey_branch: branch).count} responses"
+ puts " #{branch.role.display_name}: #{survey.responses.where(survey_branch: branch).count} responses"
end
end
end
diff --git a/test/fixtures/roles.yml b/test/fixtures/roles.yml
index 2ef51be..5ca0ab4 100644
--- a/test/fixtures/roles.yml
+++ b/test/fixtures/roles.yml
@@ -1,8 +1,10 @@
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
one:
- name: DATA ENGINEER
+ name: data engineer
two:
- name: FRONTEND ENGINEER
+ name: frontend engineer
three:
- name: PRODUCT MANAGER
+ name: product manager
+four:
+ name: qa engineer
diff --git a/test/system/questions_test.rb b/test/system/questions_test.rb
index baf1081..55b15ac 100644
--- a/test/system/questions_test.rb
+++ b/test/system/questions_test.rb
@@ -7,7 +7,7 @@ class QuestionsTest < ApplicationSystemTestCase
click_on "Add Question"
fill_in "Question Text", with: "System test question"
select "text", from: "Question type"
- select "FRONTEND ENGINEER", from: "Roles"
+ select "Frontend Engineer", from: "Roles"
click_on "Create Question"
assert_text "System test question"
within("li", text: "System test question") do
diff --git a/test/system/responses_test.rb b/test/system/responses_test.rb
index 34ead7a..a549dcd 100644
--- a/test/system/responses_test.rb
+++ b/test/system/responses_test.rb
@@ -4,7 +4,7 @@ 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"
+ select "Frontend Engineer", from: "role-select"
# Wait for the survey form to load
assert_selector "form", wait: 5
@@ -21,7 +21,7 @@ class ResponsesTest < ApplicationSystemTestCase
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"
+ select "Frontend Engineer", from: "role-select"
# Wait for the survey form to load
assert_selector "form", wait: 5
diff --git a/test/system/survey_branches_test.rb b/test/system/survey_branches_test.rb
index 1ec1e77..d71c896 100644
--- a/test/system/survey_branches_test.rb
+++ b/test/system/survey_branches_test.rb
@@ -4,7 +4,7 @@ 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"
+ select "Data Engineer", from: "role-select"
assert_text "Question for DATA ENGINEER"
refute_text "Question for FRONTEND ENGINEER"
end
diff --git a/test/system/surveys_test.rb b/test/system/surveys_test.rb
index 4cd631f..e328d1b 100644
--- a/test/system/surveys_test.rb
+++ b/test/system/surveys_test.rb
@@ -26,7 +26,7 @@ class SurveysTest < ApplicationSystemTestCase
test "user can take a survey" do
visit surveys_path
click_on "Take Survey", match: :first
- select "FRONTEND ENGINEER", from: "role-select"
+ 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"
From 3932fd442bb464093b844f85bb35a4dc1ffcf81a Mon Sep 17 00:00:00 2001
From: Michael Deol
Date: Mon, 13 Oct 2025 14:12:00 -0600
Subject: [PATCH 18/19] Soft Delete All Models, Fix UI Bug Question Creation
---
.../branch_analytics_controller.rb | 78 +++++-
app/controllers/questions_controller.rb | 29 +-
app/controllers/roles_controller.rb | 23 +-
app/controllers/surveys_controller.rb | 27 +-
app/javascript/components/QuestionList.jsx | 136 ++++++++--
app/javascript/controllers/index.js | 8 +-
.../controllers/question_form_controller.js | 10 +-
app/models/branch_question.rb | 2 +
app/models/concerns/soft_deletable.rb | 49 ++++
app/models/question.rb | 6 +-
app/models/response.rb | 2 +
app/models/role.rb | 26 +-
app/models/survey.rb | 6 +-
app/models/survey_branch.rb | 6 +-
app/views/branch_analytics/show.html.erb | 129 ++++++++-
app/views/roles/index.html.erb | 37 ++-
app/views/roles/show.html.erb | 65 ++++-
app/views/surveys/index.html.erb | 54 +++-
app/views/surveys/show.html.erb | 16 ++
config/routes.rb | 19 +-
...1013192304_add_deleted_at_to_all_models.rb | 17 ++
db/schema.rb | 14 +-
.../branch_analytics_controller_test.rb | 111 ++++++++
test/controllers/questions_controller_test.rb | 186 ++++++++++++-
test/controllers/roles_controller_test.rb | 57 +++-
test/controllers/surveys_controller_test.rb | 28 +-
test/models/branch_question_test.rb | 39 +++
test/models/question_test.rb | 161 +++++++++++
test/models/response_test.rb | 35 +++
test/models/role_test.rb | 118 +++++++++
test/models/survey_branch_test.rb | 35 +++
test/models/survey_test.rb | 41 +++
test/system/questions_test.rb | 249 +++++++++++++++++-
test/system/roles_test.rb | 22 +-
test/system/surveys_test.rb | 21 +-
35 files changed, 1747 insertions(+), 115 deletions(-)
create mode 100644 app/models/concerns/soft_deletable.rb
create mode 100644 db/migrate/20251013192304_add_deleted_at_to_all_models.rb
create mode 100644 test/controllers/branch_analytics_controller_test.rb
diff --git a/app/controllers/branch_analytics_controller.rb b/app/controllers/branch_analytics_controller.rb
index bf7c156..a2e98bd 100644
--- a/app/controllers/branch_analytics_controller.rb
+++ b/app/controllers/branch_analytics_controller.rb
@@ -2,12 +2,14 @@ class BranchAnalyticsController < ApplicationController
before_action :set_survey
def show
+ @show_deleted = params[:show_deleted] == 'true'
@analytics = {
overview: survey_overview,
by_branch: branch_response_summary,
question_analysis: question_breakdown,
completion_stats: completion_statistics,
- charts: chart_data
+ charts: chart_data,
+ deletion_summary: deletion_summary
}
@chart_data_json = chart_data.to_json
end
@@ -20,10 +22,13 @@ def set_survey
def survey_overview
{
- total_responses: @survey.responses.count,
- total_branches: @survey.survey_branches.count,
- total_questions: @survey.questions.count,
- response_rate: calculate_overall_response_rate
+ total_responses: responses_scope.count,
+ total_branches: branches_scope.count,
+ total_questions: questions_scope.count,
+ response_rate: calculate_overall_response_rate,
+ deleted_responses: @survey.responses.only_deleted.count,
+ deleted_branches: @survey.survey_branches.only_deleted.count,
+ deleted_questions: @survey.questions.only_deleted.count
}
end
@@ -45,13 +50,13 @@ def branch_response_summary
end
def question_breakdown
- @survey.questions.includes(:responses, :branch_questions).map do |question|
+ questions_scope.includes(:responses, :branch_questions).map do |question|
{
question_id: question.id,
content: question.content,
question_type: question.question_type,
total_responses: question.responses.where(survey_id: @survey.id).count,
- branches: @survey.survey_branches.includes(:role).map do |branch|
+ branches: branches_scope.includes(:role).map do |branch|
branch_responses = question.responses.where(survey_id: @survey.id, survey_branch: branch)
{
role: branch.role.display_name,
@@ -72,11 +77,20 @@ def completion_statistics
}
end
+ def deletion_summary
+ {
+ deleted_responses_by_date: deleted_responses_timeline,
+ deleted_questions_by_type: deleted_questions_by_type,
+ deleted_branches_by_role: deleted_branches_by_role,
+ recent_deletions: recent_deletions
+ }
+ end
+
def calculate_overall_response_rate
- total_possible = @survey.survey_branches.joins(:branch_questions).count
+ total_possible = branches_scope.joins(:branch_questions).count
return 0 if total_possible.zero?
- (@survey.responses.count.to_f / total_possible * 100).round(2)
+ (responses_scope.count.to_f / total_possible * 100).round(2)
end
def calculate_avg_response_length(responses)
@@ -103,7 +117,7 @@ def response_type_distribution(branch)
end
def response_timeline_data
- @survey.responses
+ responses_scope
.where('created_at >= ?', 30.days.ago)
.group("DATE(created_at)")
.order("DATE(created_at)")
@@ -159,7 +173,7 @@ def response_timeline_chart
end
def question_types_chart
- @survey.questions.group(:question_type).count.transform_keys(&:humanize)
+ questions_scope.group(:question_type).count.transform_keys(&:humanize)
end
def daily_responses_chart
@@ -168,11 +182,23 @@ def daily_responses_chart
# Helper methods for DRY code
def survey_branches_with_associations
- @survey_branches_with_associations ||= @survey.survey_branches.includes(:role, :branch_questions)
+ @survey_branches_with_associations ||= branches_scope.includes(:role, :branch_questions)
+ end
+
+ def responses_scope
+ @show_deleted ? @survey.responses.with_deleted : @survey.responses
+ end
+
+ def branches_scope
+ @show_deleted ? @survey.survey_branches.with_deleted : @survey.survey_branches
+ end
+
+ def questions_scope
+ @show_deleted ? @survey.questions.with_deleted : @survey.questions
end
def responses_for_branch(branch)
- @survey.responses.where(survey_branch: branch)
+ responses_scope.where(survey_branch: branch)
end
def responses_by_date_range(days)
@@ -180,7 +206,7 @@ def responses_by_date_range(days)
start_date = end_date - (days - 1).days
# Get responses within date range
- responses_in_range = @survey.responses.where(created_at: start_date.beginning_of_day..end_date.end_of_day)
+ responses_in_range = responses_scope.where(created_at: start_date.beginning_of_day..end_date.end_of_day)
# Group by date and count
grouped_responses = responses_in_range.group("DATE(created_at)").count
@@ -194,4 +220,28 @@ def responses_by_date_range(days)
result
end
+
+ def deleted_responses_timeline
+ @survey.responses.only_deleted
+ .where('deleted_at >= ?', 30.days.ago)
+ .group("DATE(deleted_at)")
+ .order("DATE(deleted_at)")
+ .count
+ end
+
+ def deleted_questions_by_type
+ @survey.questions.only_deleted.group(:question_type).count.transform_keys(&:humanize)
+ end
+
+ def deleted_branches_by_role
+ @survey.survey_branches.only_deleted.joins(:role).group('roles.name').count
+ end
+
+ def recent_deletions
+ {
+ responses: @survey.responses.only_deleted.order(deleted_at: :desc).limit(5).pluck(:deleted_at, :value),
+ questions: @survey.questions.only_deleted.order(deleted_at: :desc).limit(5).pluck(:deleted_at, :content),
+ branches: @survey.survey_branches.only_deleted.includes(:role).order(deleted_at: :desc).limit(5).map { |b| [b.deleted_at, b.role.display_name] }
+ }
+ end
end
\ No newline at end of file
diff --git a/app/controllers/questions_controller.rb b/app/controllers/questions_controller.rb
index d9f8b94..e106faa 100644
--- a/app/controllers/questions_controller.rb
+++ b/app/controllers/questions_controller.rb
@@ -1,11 +1,12 @@
class QuestionsController < ApplicationController
before_action :set_survey
- before_action :set_question, only: [:edit, :update, :destroy]
+ before_action :set_question, only: [:edit, :update, :destroy, :restore]
before_action :set_role, only: [:new, :create, :edit, :update, :destroy]
before_action :set_roles, only: [:new, :create, :edit, :update]
def index
- @questions = @survey.questions.distinct
+ @questions = params[:show_deleted] == 'true' ? @survey.questions.with_deleted : @survey.questions
+ @questions = @questions.distinct
if params[:survey_branch_id].present?
@questions = @questions.joins(:branch_questions)
@@ -22,8 +23,12 @@ def index
respond_to do |format|
format.json do
render json: @questions.as_json(
- only: [:id, :content, :question_type, :required]
- )
+ only: [:id, :content, :question_type, :required],
+ methods: [:deleted?]
+ ).map do |q|
+ q['deleted_at'] = @questions.find(q['id']).deleted_at&.strftime('%B %d, %Y') if q['deleted?']
+ q
+ end
end
format.html { redirect_to survey_path(@survey) }
end
@@ -66,7 +71,15 @@ def destroy
@question.destroy
respond_to do |format|
format.json { head :no_content }
- format.html { redirect_to survey_path(@survey), notice: 'Question was successfully destroyed.' }
+ format.html { redirect_to survey_path(@survey), notice: 'Question was successfully soft deleted. It can be restored if needed.' }
+ end
+ end
+
+ def restore
+ @question.restore
+ respond_to do |format|
+ format.json { head :no_content }
+ format.html { redirect_to survey_path(@survey), notice: 'Question was successfully restored.' }
end
end
@@ -77,7 +90,11 @@ def set_survey
end
def set_question
- @question = @survey.questions.find(params[:id])
+ if action_name == 'restore'
+ @question = @survey.questions.with_deleted.find(params[:id])
+ else
+ @question = @survey.questions.find(params[:id])
+ end
end
def set_role
diff --git a/app/controllers/roles_controller.rb b/app/controllers/roles_controller.rb
index c202624..8ee5cb6 100644
--- a/app/controllers/roles_controller.rb
+++ b/app/controllers/roles_controller.rb
@@ -1,8 +1,14 @@
class RolesController < ApplicationController
- before_action :set_role, only: [:show, :edit, :update, :destroy]
+ before_action :set_role, only: [:show, :edit, :update, :destroy, :restore]
def index
- @roles = Role.all
+ @roles = params[:show_deleted] == 'true' ? Role.with_deleted : Role.all
+ @show_deleted = params[:show_deleted] == 'true'
+ end
+
+ def deleted
+ @roles = Role.only_deleted
+ render :index
end
def show
@@ -34,13 +40,22 @@ def update
def destroy
@role.destroy
- redirect_to roles_url, notice: 'Role was successfully destroyed.'
+ redirect_to roles_url, notice: 'Role was successfully soft deleted. It can be restored if needed.'
+ end
+
+ def restore
+ @role.restore
+ redirect_to roles_url, notice: 'Role was successfully restored.'
end
private
def set_role
- @role = Role.find(params[:id])
+ if action_name == 'restore'
+ @role = Role.with_deleted.find(params[:id])
+ else
+ @role = Role.find(params[:id])
+ end
end
def role_params
diff --git a/app/controllers/surveys_controller.rb b/app/controllers/surveys_controller.rb
index 1cfc1f5..d7d0629 100644
--- a/app/controllers/surveys_controller.rb
+++ b/app/controllers/surveys_controller.rb
@@ -1,11 +1,17 @@
class SurveysController < ApplicationController
- before_action :set_survey, only: [:show, :edit, :update, :destroy, :take, :submit]
+ before_action :set_survey, only: [:show, :edit, :update, :destroy, :take, :submit, :restore]
def index
- @surveys = Survey.all
+ @surveys = params[:show_deleted] == 'true' ? Survey.with_deleted : Survey.all
+ @show_deleted = params[:show_deleted] == 'true'
@roles = Role.all.map { |role| { id: role.id, name: role.display_name } }
end
+ def deleted
+ @surveys = Survey.only_deleted
+ render :index
+ end
+
def show
@roles = Role.all.map { |role| { id: role.id, name: role.display_name } }
@all_roles = Role.all
@@ -54,7 +60,12 @@ def update
def destroy
@survey.destroy
- redirect_to surveys_url, notice: 'Survey was successfully destroyed.'
+ redirect_to surveys_url, notice: 'Survey was successfully soft deleted. It can be restored if needed.'
+ end
+
+ def restore
+ @survey.restore
+ redirect_to surveys_url, notice: 'Survey was successfully restored.'
end
def take
@@ -107,11 +118,15 @@ def submit
end
private
-
+
def set_survey
- @survey = Survey.find(params[:id])
+ if action_name == 'restore'
+ @survey = Survey.with_deleted.find(params[:id])
+ else
+ @survey = Survey.find(params[:id])
+ end
end
-
+
def survey_params
params.require(:survey).permit(:title, :description)
end
diff --git a/app/javascript/components/QuestionList.jsx b/app/javascript/components/QuestionList.jsx
index 06e189e..faecbf7 100644
--- a/app/javascript/components/QuestionList.jsx
+++ b/app/javascript/components/QuestionList.jsx
@@ -4,11 +4,16 @@ import { createRoot } from 'react-dom/client';
const QuestionList = (props) => {
const [questions, setQuestions] = useState(props.questions || []);
const [loading, setLoading] = useState(false);
+ const [showDeleted, setShowDeleted] = useState(false);
- const fetchQuestions = async (roleId = "") => {
+ const fetchQuestions = async (roleId = "", includeDeleted = false) => {
setLoading(true);
try {
- const url = `/surveys/${props.surveyId}/questions.json` + (roleId ? `?role_id=${encodeURIComponent(roleId)}` : "");
+ const params = new URLSearchParams();
+ if (roleId) params.append('role_id', roleId);
+ if (includeDeleted) params.append('show_deleted', 'true');
+
+ const url = `/surveys/${props.surveyId}/questions.json` + (params.toString() ? `?${params.toString()}` : "");
const res = await fetch(url, { headers: { "Accept": "application/json" } });
if (!res.ok) throw new Error("Failed to load questions");
const data = await res.json();
@@ -21,19 +26,51 @@ const QuestionList = (props) => {
};
useEffect(() => {
- fetchQuestions();
+ fetchQuestions("", showDeleted);
+
+ // Handle role filter
const selectId = props.filterSelectId;
if (selectId) {
const sel = document.getElementById(selectId);
if (sel) {
const handler = (e) => {
- fetchQuestions(e.target.value || "");
+ fetchQuestions(e.target.value || "", showDeleted);
};
sel.addEventListener("change", handler);
return () => sel.removeEventListener("change", handler);
}
}
- }, [props.surveyId]);
+ }, [props.surveyId, showDeleted]);
+
+ useEffect(() => {
+ // Handle tab filtering
+ const tabs = document.querySelectorAll('.question-filter-tab');
+ const handleTabClick = (e) => {
+ const filter = e.target.dataset.filter;
+ const newShowDeleted = filter === 'all';
+
+ // Update tab styling
+ tabs.forEach(tab => {
+ if (tab === e.target) {
+ tab.className = 'border-indigo-500 text-indigo-600 whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm question-filter-tab';
+ } else {
+ tab.className = 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm question-filter-tab';
+ }
+ });
+
+ setShowDeleted(newShowDeleted);
+ };
+
+ tabs.forEach(tab => {
+ tab.addEventListener('click', handleTabClick);
+ });
+
+ return () => {
+ tabs.forEach(tab => {
+ tab.removeEventListener('click', handleTabClick);
+ });
+ };
+ }, []);
const handleDelete = async (questionId) => {
if (!confirm('Are you sure you want to delete this question?')) {
@@ -54,7 +91,10 @@ const QuestionList = (props) => {
});
if (response.ok) {
- setQuestions(questions.filter(q => q.id !== questionId));
+ // Refresh the list to show updated status
+ const roleSelect = document.getElementById(props.filterSelectId);
+ const currentRoleId = roleSelect ? roleSelect.value : "";
+ fetchQuestions(currentRoleId, showDeleted);
} else {
console.error('Failed to delete question:', response.status);
}
@@ -63,6 +103,37 @@ const QuestionList = (props) => {
}
};
+ const handleRestore = async (questionId) => {
+ if (!confirm('Are you sure you want to restore this question?')) {
+ return;
+ }
+
+ const csrfTokenElement = document.querySelector('meta[name="csrf-token"]');
+ const csrfToken = csrfTokenElement ? csrfTokenElement.content : '';
+
+ try {
+ const response = await fetch(`/surveys/${props.surveyId}/questions/${questionId}/restore`, {
+ method: 'PATCH',
+ headers: {
+ 'X-CSRF-Token': csrfToken,
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json'
+ }
+ });
+
+ if (response.ok) {
+ // Refresh the list to show updated status
+ const roleSelect = document.getElementById(props.filterSelectId);
+ const currentRoleId = roleSelect ? roleSelect.value : "";
+ fetchQuestions(currentRoleId, showDeleted);
+ } else {
+ console.error('Failed to restore question:', response.status);
+ }
+ } catch (error) {
+ console.error('Error restoring question:', error);
+ }
+ };
+
const getQuestionTypeLabel = (type) => {
return type.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase());
};
@@ -82,29 +153,50 @@ const QuestionList = (props) => {
return (
{questions.map(question => (
-
+
-
- {question.content}
-
+
+
+ {question.content}
+
+ {question['deleted?'] && (
+
+ Deleted
+
+ )}
+
Type: {getQuestionTypeLabel(question.question_type)}
+ {question['deleted?'] && question.deleted_at && (
+
Deleted on {question.deleted_at}
+ )}
-
- Edit
-
-
handleDelete(question.id)}
- className="inline-flex items-center px-2 py-1 border border-transparent text-xs font-medium rounded text-red-700 bg-red-100 hover:bg-red-200"
- >
- Delete
-
+ {question['deleted?'] ? (
+
handleRestore(question.id)}
+ className="inline-flex items-center px-2 py-1 border border-transparent text-xs font-medium rounded text-green-700 bg-green-100 hover:bg-green-200"
+ >
+ Restore
+
+ ) : (
+ <>
+
+ Edit
+
+
handleDelete(question.id)}
+ className="inline-flex items-center px-2 py-1 border border-transparent text-xs font-medium rounded text-red-700 bg-red-100 hover:bg-red-200"
+ >
+ Delete
+
+ >
+ )}
diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js
index 808d70a..228afa0 100644
--- a/app/javascript/controllers/index.js
+++ b/app/javascript/controllers/index.js
@@ -3,9 +3,7 @@
import { application } from "./application"
// Manually import all controllers
-// Add your controllers here as needed
-// import HelloController from "./hello_controller"
-// application.register("hello", HelloController)
+import QuestionFormController from "./question_form_controller"
-// This is a placeholder for future controller registrations
-// When you create a new controller, import it above and register it here
\ No newline at end of file
+// Register controllers
+application.register("question-form", QuestionFormController)
\ No newline at end of file
diff --git a/app/javascript/controllers/question_form_controller.js b/app/javascript/controllers/question_form_controller.js
index 5496cd8..fc5ebcb 100644
--- a/app/javascript/controllers/question_form_controller.js
+++ b/app/javascript/controllers/question_form_controller.js
@@ -9,8 +9,14 @@ export default class extends Controller {
}
toggleOptionsField() {
- const questionType = document.getElementById('question_question_type').value
- const optionsField = document.getElementById('options-field')
+ const questionTypeSelect = this.element.querySelector('select[name="question[question_type]"]')
+ const questionType = questionTypeSelect ? questionTypeSelect.value : null
+
+ if (!this.hasOptionsFieldTarget) {
+ return
+ }
+
+ const optionsField = this.optionsFieldTarget
if (['multiple_choice', 'checkbox'].includes(questionType)) {
optionsField.classList.remove('hidden')
diff --git a/app/models/branch_question.rb b/app/models/branch_question.rb
index 3e71bd9..f486f01 100644
--- a/app/models/branch_question.rb
+++ b/app/models/branch_question.rb
@@ -1,5 +1,7 @@
class BranchQuestion < ApplicationRecord
+ include SoftDeletable
+
belongs_to :survey_branch
belongs_to :question
end
\ No newline at end of file
diff --git a/app/models/concerns/soft_deletable.rb b/app/models/concerns/soft_deletable.rb
new file mode 100644
index 0000000..674b08d
--- /dev/null
+++ b/app/models/concerns/soft_deletable.rb
@@ -0,0 +1,49 @@
+module SoftDeletable
+ extend ActiveSupport::Concern
+
+ included do
+ # Default scope to exclude soft deleted records
+ default_scope { where(deleted_at: nil) }
+
+ # Scope to include soft deleted records
+ scope :with_deleted, -> { unscope(where: :deleted_at) }
+ scope :only_deleted, -> { unscope(where: :deleted_at).where.not(deleted_at: nil) }
+ end
+
+ # Override destroy to perform soft delete
+ def destroy
+ return false unless valid?
+
+ run_callbacks :destroy do
+ update_column(:deleted_at, Time.current)
+ end
+
+ self
+ end
+
+ # Hard delete - actually remove from database
+ def destroy!
+ with_deleted { super }
+ end
+
+ # Check if record is soft deleted
+ def deleted?
+ deleted_at.present?
+ end
+
+ # Restore a soft deleted record
+ def restore
+ update_column(:deleted_at, nil)
+ end
+
+ # Override persisted? to account for soft delete
+ def persisted?
+ super && !deleted?
+ end
+
+ private
+
+ def with_deleted
+ self.class.unscoped { yield }
+ end
+end
\ No newline at end of file
diff --git a/app/models/question.rb b/app/models/question.rb
index b12b819..776c4a1 100644
--- a/app/models/question.rb
+++ b/app/models/question.rb
@@ -1,8 +1,10 @@
class Question < ApplicationRecord
- has_many :branch_questions, dependent: :destroy
+ include SoftDeletable
+
+ has_many :branch_questions
has_many :survey_branches, through: :branch_questions
- has_many :responses, dependent: :destroy
+ has_many :responses
validates :content, presence: true
validates :question_type, presence: true
diff --git a/app/models/response.rb b/app/models/response.rb
index a54a94c..d35c88a 100644
--- a/app/models/response.rb
+++ b/app/models/response.rb
@@ -1,5 +1,7 @@
class Response < ApplicationRecord
+ include SoftDeletable
+
belongs_to :survey
belongs_to :question
belongs_to :survey_branch, optional: true
diff --git a/app/models/role.rb b/app/models/role.rb
index bb41ba7..408d8cd 100644
--- a/app/models/role.rb
+++ b/app/models/role.rb
@@ -1,6 +1,8 @@
class Role < ApplicationRecord
- has_many :survey_branches, dependent: :destroy
+ include SoftDeletable
+
+ has_many :survey_branches
validates :name, presence: true, uniqueness: { case_sensitive: false }
@@ -17,6 +19,28 @@ def display_name
end.join(' ')
end
+ # Check what data would be affected by deletion
+ def deletion_impact
+ branches = survey_branches.includes(:responses, :branch_questions)
+ total_responses = branches.sum { |branch| branch.responses.count }
+ total_questions = branches.sum { |branch| branch.branch_questions.count }
+ affected_surveys = branches.map(&:survey).uniq
+
+ {
+ survey_branches: branches.count,
+ total_responses: total_responses,
+ total_questions: total_questions,
+ affected_surveys: affected_surveys.count,
+ survey_titles: affected_surveys.map(&:title)
+ }
+ end
+
+ # Get human-readable deletion status
+ def deletion_status
+ return "Can be soft deleted safely" unless deleted?
+ "Already deleted on #{deleted_at.strftime('%B %d, %Y at %I:%M %p')}"
+ end
+
private
def normalize_name
diff --git a/app/models/survey.rb b/app/models/survey.rb
index c77249b..eaa7f9d 100644
--- a/app/models/survey.rb
+++ b/app/models/survey.rb
@@ -1,9 +1,11 @@
class Survey < ApplicationRecord
- has_many :survey_branches, dependent: :destroy
+ include SoftDeletable
+
+ has_many :survey_branches
has_many :branch_questions, through: :survey_branches
has_many :questions, through: :branch_questions
- has_many :responses, dependent: :destroy
+ has_many :responses
validates :title, presence: true
end
diff --git a/app/models/survey_branch.rb b/app/models/survey_branch.rb
index 83ea1a9..64f6fbe 100644
--- a/app/models/survey_branch.rb
+++ b/app/models/survey_branch.rb
@@ -1,10 +1,12 @@
class SurveyBranch < ApplicationRecord
+ include SoftDeletable
+
belongs_to :survey
belongs_to :role
- has_many :branch_questions, dependent: :destroy
+ has_many :branch_questions
has_many :questions, through: :branch_questions
- has_many :responses, dependent: :destroy
+ has_many :responses
validates :role_id, uniqueness: { scope: :survey_id }
diff --git a/app/views/branch_analytics/show.html.erb b/app/views/branch_analytics/show.html.erb
index e42bdbd..7524994 100644
--- a/app/views/branch_analytics/show.html.erb
+++ b/app/views/branch_analytics/show.html.erb
@@ -5,6 +5,20 @@
<%= @survey.description %>
<%= link_to "← Back to Survey", survey_path(@survey), class: "mt-4 inline-flex items-center text-indigo-600 hover:text-indigo-900" %>
+
+
+
+
+ <%= link_to survey_analytics_path(@survey),
+ class: "#{@show_deleted ? 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' : 'border-indigo-500 text-indigo-600'} whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm" do %>
+ Active Data Only
+ <% end %>
+ <%= link_to survey_analytics_path(@survey, show_deleted: true),
+ class: "#{@show_deleted ? 'border-indigo-500 text-indigo-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'} whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm" do %>
+ Include Deleted Data
+ <% end %>
+
+
@@ -19,8 +33,15 @@
- Total Responses
- <%= @analytics[:overview][:total_responses] %>
+
+ <%= @show_deleted ? "Total Responses (+ Deleted)" : "Total Responses" %>
+
+
+ <%= @analytics[:overview][:total_responses] %>
+ <% if @show_deleted && @analytics[:overview][:deleted_responses] > 0 %>
+ (+<%= @analytics[:overview][:deleted_responses] %> deleted)
+ <% end %>
+
@@ -37,8 +58,15 @@
- Active Branches
- <%= @analytics[:overview][:total_branches] %>
+
+ <%= @show_deleted ? "Total Branches (+ Deleted)" : "Active Branches" %>
+
+
+ <%= @analytics[:overview][:total_branches] %>
+ <% if @show_deleted && @analytics[:overview][:deleted_branches] > 0 %>
+ (+<%= @analytics[:overview][:deleted_branches] %> deleted)
+ <% end %>
+
@@ -55,8 +83,15 @@
- Total Questions
- <%= @analytics[:overview][:total_questions] %>
+
+ <%= @show_deleted ? "Total Questions (+ Deleted)" : "Total Questions" %>
+
+
+ <%= @analytics[:overview][:total_questions] %>
+ <% if @show_deleted && @analytics[:overview][:deleted_questions] > 0 %>
+ (+<%= @analytics[:overview][:deleted_questions] %> deleted)
+ <% end %>
+
@@ -130,6 +165,88 @@
+ <% if @show_deleted %>
+
+
+
+
Deletion Analytics
+
Insights into deleted data patterns
+
+
+
+
+
+
+
Recent Deletions
+
+ <% if @analytics[:deletion_summary][:recent_deletions][:responses].any? %>
+
+
Responses
+
+ <% @analytics[:deletion_summary][:recent_deletions][:responses].each do |deleted_at, value| %>
+
+ <%= truncate(value, length: 30) %>
+ <%= deleted_at.strftime('%m/%d/%y') %>
+
+ <% end %>
+
+
+ <% end %>
+
+ <% if @analytics[:deletion_summary][:recent_deletions][:questions].any? %>
+
+
Questions
+
+ <% @analytics[:deletion_summary][:recent_deletions][:questions].each do |deleted_at, content| %>
+
+ <%= truncate(content, length: 30) %>
+ <%= deleted_at.strftime('%m/%d/%y') %>
+
+ <% end %>
+
+
+ <% end %>
+
+
+
+
+
+
Deletion Patterns
+
+ <% if @analytics[:deletion_summary][:deleted_questions_by_type].any? %>
+
+
Deleted Questions by Type
+
+ <% @analytics[:deletion_summary][:deleted_questions_by_type].each do |type, count| %>
+
+ <%= type %>
+ <%= count %>
+
+ <% end %>
+
+
+ <% end %>
+
+ <% if @analytics[:deletion_summary][:deleted_branches_by_role].any? %>
+
+
Deleted Branches by Role
+
+ <% @analytics[:deletion_summary][:deleted_branches_by_role].each do |role, count| %>
+
+ <%= role.humanize %>
+ <%= count %>
+
+ <% end %>
+
+
+ <% end %>
+
+
+
+
+
+ <% end %>
+
diff --git a/app/views/roles/index.html.erb b/app/views/roles/index.html.erb
index ba13928..d27251b 100644
--- a/app/views/roles/index.html.erb
+++ b/app/views/roles/index.html.erb
@@ -1,11 +1,18 @@
-
-
-
Roles
-
A list of all available roles.
+
+
+
+
Roles
+
A list of all available roles.
+
+
+ <%= link_to "New Role", new_role_path, class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" %>
+
-
- <%= link_to "New Role", new_role_path, class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" %>
+
+
+ <%= link_to "Active Roles", roles_path, class: @show_deleted ? "px-3 py-2 text-sm font-medium text-gray-500 hover:text-gray-700" : "px-3 py-2 text-sm font-medium text-indigo-600 border-b-2 border-indigo-600" %>
+ <%= link_to "All Roles", roles_path(show_deleted: true), class: @show_deleted ? "px-3 py-2 text-sm font-medium text-indigo-600 border-b-2 border-indigo-600" : "px-3 py-2 text-sm font-medium text-gray-500 hover:text-gray-700" %>
@@ -17,11 +24,25 @@
<%= link_to role.display_name, role %>
+ <% if role.deleted? %>
+
+
+
+
+
+ Deleted on <%= role.deleted_at.strftime('%B %d, %Y') %>
+
+
+ <% end %>
<%= link_to "View", role_path(role), class: "inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-indigo-700 bg-indigo-100 hover:bg-indigo-200" %>
- <%= link_to "Edit", edit_role_path(role), class: "inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-yellow-700 bg-yellow-100 hover:bg-yellow-200" %>
- <%= button_to "Delete", role_path(role), method: :delete, class: "inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-red-700 bg-red-100 hover:bg-red-200", form: { data: { turbo_confirm: "Are you sure you want to delete this role?" } } %>
+ <% unless role.deleted? %>
+ <%= link_to "Edit", edit_role_path(role), class: "inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-yellow-700 bg-yellow-100 hover:bg-yellow-200" %>
+ <%= button_to "Delete", role_path(role), method: :delete, class: "inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-red-700 bg-red-100 hover:bg-red-200", form: { data: { turbo_confirm: "Are you sure you want to soft delete this role? It can be restored later." } } %>
+ <% else %>
+ <%= button_to "Restore", restore_role_path(role), method: :patch, class: "inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-green-700 bg-green-100 hover:bg-green-200", form: { data: { turbo_confirm: "Are you sure you want to restore this role?" } } %>
+ <% end %>
diff --git a/app/views/roles/show.html.erb b/app/views/roles/show.html.erb
index 5ad7f31..bcd597d 100644
--- a/app/views/roles/show.html.erb
+++ b/app/views/roles/show.html.erb
@@ -2,12 +2,73 @@
-
Role: <%= @role.display_name %>
+
+ Role: <%= @role.display_name %>
+ <% if @role.deleted? %>
+
+ Soft Deleted
+
+ <% end %>
+
- <%= link_to 'Edit', edit_role_path(@role), class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" %>
+ <% unless @role.deleted? %>
+ <%= link_to 'Edit', edit_role_path(@role), class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" %>
+ <% else %>
+ <%= button_to 'Restore', restore_role_path(@role), method: :patch, class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500", form: { data: { turbo_confirm: "Are you sure you want to restore this role?" } } %>
+ <% end %>
<%= link_to 'Back', roles_path, class: "inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" %>
+
+
+
Role Information
+
+
+
+
Name
+ <%= @role.display_name %>
+
+
+
Survey Branches
+ <%= @role.survey_branches.count %>
+
+
+
+
+
+ <% if @role.deleted? %>
+
+
+
+
+
+
+
+
Soft Deleted
+
+
This role was soft deleted on <%= @role.deleted_at.strftime('%B %d, %Y at %I:%M %p') %>. The data is preserved and can be restored.
+
+
+
+
+
+ <% else %>
+
+
+
+
+
+
+
+
Soft Delete Available
+
+
This role can be soft deleted, preserving all associated survey data. Soft deleted roles can be restored later if needed.
+
+
+
+
+
+ <% end %>
\ No newline at end of file
diff --git a/app/views/surveys/index.html.erb b/app/views/surveys/index.html.erb
index 23d1241..d338e7d 100644
--- a/app/views/surveys/index.html.erb
+++ b/app/views/surveys/index.html.erb
@@ -1,11 +1,27 @@
-
-
-
Surveys
-
A list of all available surveys.
+
+
+
+
Surveys
+
A list of all available surveys.
+
+
+ <%= link_to "New Survey", new_survey_path, class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" %>
+
-
- <%= link_to "New Survey", new_survey_path, class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" %>
+
+
+
+
+ <%= link_to surveys_path,
+ class: "#{@show_deleted ? 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' : 'border-indigo-500 text-indigo-600'} whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm" do %>
+ Active Surveys
+ <% end %>
+ <%= link_to surveys_path(show_deleted: true),
+ class: "#{@show_deleted ? 'border-indigo-500 text-indigo-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'} whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm" do %>
+ All Surveys
+ <% end %>
+
@@ -13,18 +29,32 @@
<% @surveys.each do |survey| %>
-
+
-
<%= survey.title %>
+
+
<%= survey.title %>
+ <% if survey.deleted? %>
+
+ Deleted
+
+ <% end %>
+
<%= truncate(survey.description, length: 100) %>
<%= pluralize(survey.questions.count, 'question') %>
+ <% if survey.deleted? %>
+
Deleted on <%= survey.deleted_at.strftime('%B %d, %Y') %>
+ <% end %>
- <%= link_to "View", survey_path(survey), class: "inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-indigo-700 bg-indigo-100 hover:bg-indigo-200" %>
- <%= link_to "Edit", edit_survey_path(survey), class: "inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-yellow-700 bg-yellow-100 hover:bg-yellow-200" %>
- <%= link_to "Take Survey", take_survey_path(survey), class: "take-survey-link inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-green-700 bg-green-100 hover:bg-green-200", data: { survey_id: survey.id } %>
- <%= button_to "Delete", survey_path(survey), method: :delete, class: "inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-red-700 bg-red-100 hover:bg-red-200", form: { data: { turbo_confirm: "Are you sure you want to delete this survey?" } } %>
+ <% if survey.deleted? %>
+ <%= button_to "Restore", restore_survey_path(survey), method: :patch, class: "inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-green-700 bg-green-100 hover:bg-green-200", form: { data: { turbo_confirm: "Are you sure you want to restore this survey?" } } %>
+ <% else %>
+ <%= link_to "View", survey_path(survey), class: "inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-indigo-700 bg-indigo-100 hover:bg-indigo-200" %>
+ <%= link_to "Edit", edit_survey_path(survey), class: "inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-yellow-700 bg-yellow-100 hover:bg-yellow-200" %>
+ <%= link_to "Take Survey", take_survey_path(survey), class: "take-survey-link inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-green-700 bg-green-100 hover:bg-green-200", data: { survey_id: survey.id } %>
+ <%= button_to "Delete", survey_path(survey), method: :delete, class: "inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-red-700 bg-red-100 hover:bg-red-200", form: { data: { turbo_confirm: "Are you sure you want to delete this survey?" } } %>
+ <% end %>
diff --git a/app/views/surveys/show.html.erb b/app/views/surveys/show.html.erb
index ab83213..2e10658 100644
--- a/app/views/surveys/show.html.erb
+++ b/app/views/surveys/show.html.erb
@@ -18,6 +18,22 @@
<%= link_to "Add Question", new_survey_question_path(@survey), class: "inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700" %>
+
+
+
+
+ Active Questions
+
+
+ All Questions
+
+
+
+
Filter by Role
diff --git a/config/routes.rb b/config/routes.rb
index 780c9be..d9b1728 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -4,15 +4,26 @@
# Set the root path to the surveys index
root "surveys#index"
- resources :roles
-
- # 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: [: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/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 1cad60a..4ba7810 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.0].define(version: 2025_10_10_212936) 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"
@@ -19,6 +19,8 @@
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
@@ -31,6 +33,8 @@
t.datetime "updated_at", null: false
t.text "options"
t.boolean "required"
+ t.datetime "deleted_at"
+ t.index ["deleted_at"], name: "index_questions_on_deleted_at"
end
create_table "responses", force: :cascade do |t|
@@ -40,6 +44,8 @@
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"
@@ -49,6 +55,8 @@
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
@@ -58,6 +66,8 @@
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
@@ -67,6 +77,8 @@
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 "branch_questions", "questions"
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 8a34a24..e7dd350 100644
--- a/test/controllers/questions_controller_test.rb
+++ b/test/controllers/questions_controller_test.rb
@@ -35,6 +35,164 @@ class QuestionsControllerTest < ActionDispatch::IntegrationTest
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 edit_survey_question_url(@survey, @question)
assert_response :success
@@ -45,10 +203,32 @@ class QuestionsControllerTest < ActionDispatch::IntegrationTest
assert_redirected_to survey_path(@survey)
end
- test "should destroy question" do
- assert_difference('Question.count', -1) do
- delete survey_question_url(@survey, @question)
+ 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/roles_controller_test.rb b/test/controllers/roles_controller_test.rb
index a0d8601..7c5ebc2 100644
--- a/test/controllers/roles_controller_test.rb
+++ b/test/controllers/roles_controller_test.rb
@@ -51,10 +51,59 @@ class RolesControllerTest < ActionDispatch::IntegrationTest
assert_not_equal "", @role.name
end
- test "should destroy role" do
- assert_difference('Role.count', -1) do
- delete role_url(@role)
- 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 a0d7a4b..ee84cb9 100644
--- a/test/controllers/surveys_controller_test.rb
+++ b/test/controllers/surveys_controller_test.rb
@@ -57,10 +57,32 @@ class SurveysControllerTest < ActionDispatch::IntegrationTest
assert_not_equal '', @survey.title
end
- test "should destroy survey" do
- assert_difference('Survey.count', -1) do
- delete survey_url(@survey)
+ 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
diff --git a/test/models/branch_question_test.rb b/test/models/branch_question_test.rb
index 4a9b0d4..1da4ef2 100644
--- a/test/models/branch_question_test.rb
+++ b/test/models/branch_question_test.rb
@@ -5,4 +5,43 @@ class BranchQuestionTest < ActiveSupport::TestCase
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 a643db6..b180050 100644
--- a/test/models/question_test.rb
+++ b/test/models/question_test.rb
@@ -8,6 +8,47 @@ class QuestionTest < ActiveSupport::TestCase
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?
@@ -16,4 +57,124 @@ class QuestionTest < ActiveSupport::TestCase
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 051d9df..e016c51 100644
--- a/test/models/response_test.rb
+++ b/test/models/response_test.rb
@@ -15,4 +15,39 @@ class ResponseTest < ActiveSupport::TestCase
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
index 7bd737a..42878ae 100644
--- a/test/models/role_test.rb
+++ b/test/models/role_test.rb
@@ -4,4 +4,122 @@ 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
index f5a2f88..3b0e8a6 100644
--- a/test/models/survey_branch_test.rb
+++ b/test/models/survey_branch_test.rb
@@ -16,4 +16,39 @@ class SurveyBranchTest < ActiveSupport::TestCase
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 0b2f125..61a163b 100644
--- a/test/models/survey_test.rb
+++ b/test/models/survey_test.rb
@@ -8,4 +8,45 @@ class SurveyTest < ActiveSupport::TestCase
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
index 55b15ac..566a355 100644
--- a/test/system/questions_test.rb
+++ b/test/system/questions_test.rb
@@ -1,13 +1,17 @@
require "application_system_test_case"
class QuestionsTest < ApplicationSystemTestCase
- test "user can add, edit, and remove questions from a survey" do
+ 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"
+ select "Frontend Engineer", from: "Roles"
click_on "Create Question"
assert_text "System test question"
within("li", text: "System test question") do
@@ -16,12 +20,251 @@ class QuestionsTest < ApplicationSystemTestCase
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
- # Wait for the question to be removed from the DOM
+ # 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/roles_test.rb b/test/system/roles_test.rb
index 634e1e8..f5d1262 100644
--- a/test/system/roles_test.rb
+++ b/test/system/roles_test.rb
@@ -1,7 +1,7 @@
require "application_system_test_case"
class RolesTest < ApplicationSystemTestCase
- test "user can create, edit, and delete a role" do
+ 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"
@@ -12,12 +12,30 @@ class RolesTest < ApplicationSystemTestCase
click_on "Update Role"
assert_text "Updated System Test Role"
click_on "Back"
- # Accept the confirmation alert when clicking Delete
+
+ # 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/surveys_test.rb b/test/system/surveys_test.rb
index e328d1b..9842080 100644
--- a/test/system/surveys_test.rb
+++ b/test/system/surveys_test.rb
@@ -1,7 +1,7 @@
require "application_system_test_case"
class SurveysTest < ApplicationSystemTestCase
- test "user can create, edit, and delete a survey" do
+ 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"
@@ -15,12 +15,31 @@ class SurveysTest < ApplicationSystemTestCase
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
From d227763910d6bb3986e80a75b7d506947e4eb6d3 Mon Sep 17 00:00:00 2001
From: Michael Deol
Date: Mon, 13 Oct 2025 14:35:56 -0600
Subject: [PATCH 19/19] minor cleanup
---
Gemfile | 5 ---
Gemfile.lock | 1 -
app/javascript/components/RoleModal.jsx | 7 ++--
lib/tasks/fix_responses.rake | 49 -------------------------
4 files changed, 3 insertions(+), 59 deletions(-)
delete mode 100644 lib/tasks/fix_responses.rake
diff --git a/Gemfile b/Gemfile
index 783cfef..bc34f1f 100644
--- a/Gemfile
+++ b/Gemfile
@@ -33,11 +33,6 @@ gem "acts_as_list"
# Build JSON APIs with ease [https://github.com/rails/jbuilder]
gem "jbuilder"
-gem 'concurrent-ruby', '1.3.4'
-
-# Charting library for analytics
-
-
# Use Redis adapter to run Action Cable in production
# gem "redis", "~> 4.0"
diff --git a/Gemfile.lock b/Gemfile.lock
index 770fab4..fe8f982 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -251,7 +251,6 @@ DEPENDENCIES
acts_as_list
bootsnap
capybara
- concurrent-ruby (= 1.3.4)
cssbundling-rails
debug
foreman (~> 0.88.1)
diff --git a/app/javascript/components/RoleModal.jsx b/app/javascript/components/RoleModal.jsx
index f2d2705..4cac869 100644
--- a/app/javascript/components/RoleModal.jsx
+++ b/app/javascript/components/RoleModal.jsx
@@ -38,16 +38,15 @@ const initializeRoleModal = () => {
link.addEventListener("click", function(e) {
e.preventDefault();
const surveyId = this.dataset.surveyId;
- // Render your React RoleModal, passing surveyId as a prop
root.render(
{
// Redirect to /surveys/:id/take?role=selectedRoleId
window.location.href = `/surveys/${surveyId}/take?role=${roleId}`;
- root.render(null); // Close modal after selection}} // Provide actual handler
+ root.render(null); // Close modal after selection
}}
onClose={() => {
modalOpen = false;
diff --git a/lib/tasks/fix_responses.rake b/lib/tasks/fix_responses.rake
deleted file mode 100644
index ebd1f1e..0000000
--- a/lib/tasks/fix_responses.rake
+++ /dev/null
@@ -1,49 +0,0 @@
-namespace :data do
- desc "Assign survey branches to existing responses"
- task fix_response_branches: :environment do
- puts "Checking existing responses..."
-
- responses_without_branch = Response.where(survey_branch_id: nil)
- puts "Found #{responses_without_branch.count} responses without survey_branch_id"
-
- responses_without_branch.each do |response|
- puts "Response ID: #{response.id}, Survey: #{response.survey.title}, Question: #{response.question.content[0..50]}"
-
- # Find survey branches that contain this question
- survey_branches = response.survey.survey_branches.joins(:branch_questions)
- .where(branch_questions: { question_id: response.question_id })
-
- if survey_branches.count == 1
- # Only one branch contains this question, assign it
- response.update!(survey_branch: survey_branches.first)
- puts " -> Assigned to branch: #{survey_branches.first.role.display_name}"
- elsif survey_branches.count > 1
- # Multiple branches contain this question, we need to make an assumption
- # For now, assign to the first branch (you might want to modify this logic)
- response.update!(survey_branch: survey_branches.first)
- puts " -> Multiple branches found, assigned to: #{survey_branches.first.role.display_name}"
- else
- puts " -> WARNING: No branch found for this question!"
- end
- end
-
- puts "Done! Updated #{responses_without_branch.count} responses"
- end
-
- desc "Show current response statistics"
- task show_response_stats: :environment do
- puts "Response Statistics:"
- puts "Total responses: #{Response.count}"
- puts "Responses with survey_branch: #{Response.where.not(survey_branch_id: nil).count}"
- puts "Responses without survey_branch: #{Response.where(survey_branch_id: nil).count}"
-
- puts "\nBy Survey:"
- Survey.includes(:responses, :survey_branches).each do |survey|
- puts " #{survey.title}:"
- puts " Total responses: #{survey.responses.count}"
- survey.survey_branches.includes(:role).each do |branch|
- puts " #{branch.role.display_name}: #{survey.responses.where(survey_branch: branch).count} responses"
- end
- end
- end
-end
\ No newline at end of file