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

+ + +
+
+ ); +}; + +// 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 ( +
+ + +
+ ); +}; + +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] %>">
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" %>
+
+ + +
+
+ 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" %> +
    + + +
    +
    +
    +
    +
    +
    + R +
    +
    +
    +
    +
    Total Responses
    +
    <%= @analytics[:overview][:total_responses] %>
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    + B +
    +
    +
    +
    +
    Active Branches
    +
    <%= @analytics[:overview][:total_branches] %>
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    + Q +
    +
    +
    +
    +
    Total Questions
    +
    <%= @analytics[:overview][:total_questions] %>
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    + % +
    +
    +
    +
    +
    Response Rate
    +
    <%= @analytics[:overview][:response_rate] %>%
    +
    +
    +
    +
    +
    +
    + + +
    +
    +

    Branch Comparison

    +

    Response data breakdown by role/branch

    +
    +
    + + + + + + + + + + + + <% @analytics[:by_branch].each do |branch| %> + + + + + + + + <% end %> + +
    RoleResponsesQuestionsCompletion RateAvg Response Length
    +
    <%= branch[:role] %>
    +
    +
    <%= branch[:total_responses] %>
    +
    <%= pluralize(branch[:unique_respondents], 'unique respondent') %>
    +
    + <%= branch[:questions_count] %> + +
    +
    +
    +
    + <%= branch[:completion_rate] %>% +
    +
    + <%= branch[:avg_response_length] %> chars +
    +
    +
    + + +
    +
    +

    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" }) => { > {roles.map(role => ( - + ))}
    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

    +
    + +
    + + +
    +
    +
    <%= @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
    +
    +
    -
    + +