diff --git a/.gitignore b/.gitignore index ac1323f..20773d6 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,4 @@ # Ignore key files for decrypting credentials and more. /config/credentials/*.key +TODO.org diff --git a/.ruby-version b/.ruby-version index 4d54dad..c4e41f9 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -4.0.2 +4.0.3 diff --git a/Dockerfile b/Dockerfile index ed518fc..0c838b7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,13 +2,13 @@ # check=error=true # This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand: -# docker build -t active_core_minimal . -# docker run -d -p 80:80 -e RAILS_MASTER_KEY= --name active_core_minimal active_core_minimal +# docker build -t active-core . +# docker run -d -p 80:80 -e RAILS_MASTER_KEY= --name active-core active-core # For a containerized dev environment, see Dev Containers: https://guides.rubyonrails.org/getting_started_with_devcontainer.html # Make sure RUBY_VERSION matches the Ruby version in .ruby-version -ARG RUBY_VERSION=4.0.2 +ARG RUBY_VERSION=4.0.3 FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base # Rails app lives here diff --git a/Gemfile.lock b/Gemfile.lock index 5ffc5d2..9392815 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -75,7 +75,7 @@ GEM securerandom (>= 0.3) tzinfo (~> 2.0, >= 2.0.5) uri (>= 0.13.1) - addressable (2.8.9) + addressable (2.9.0) public_suffix (>= 2.0.2, < 8.0) ast (2.4.3) base64 (0.3.0) @@ -83,7 +83,7 @@ GEM bcrypt_pbkdf (1.1.2) bigdecimal (3.3.1) bindex (0.8.1) - bootsnap (1.23.0) + bootsnap (1.24.1) msgpack (~> 1.2) brakeman (8.0.4) racc @@ -110,7 +110,7 @@ GEM dotenv (3.2.0) drb (2.2.3) ed25519 (1.4.0) - erb (6.0.2) + erb (6.0.4) erubi (1.13.1) et-orbi (1.4.0) tzinfo @@ -118,7 +118,6 @@ GEM ffi (1.17.4-aarch64-linux-musl) ffi (1.17.4-arm-linux-gnu) ffi (1.17.4-arm-linux-musl) - ffi (1.17.4-arm64-darwin) ffi (1.17.4-x86_64-linux-gnu) ffi (1.17.4-x86_64-linux-musl) fugit (1.12.1) @@ -136,7 +135,7 @@ GEM activesupport (>= 6.0.0) railties (>= 6.0.0) io-console (0.8.2) - irb (1.17.0) + irb (1.18.0) pp (>= 0.6.0) prism (>= 1.3.0) rdoc (>= 4.0.0) @@ -144,7 +143,7 @@ GEM jbuilder (2.14.1) actionview (>= 7.0.0) activesupport (>= 7.0.0) - json (2.19.3) + json (2.19.4) kamal (2.11.0) activesupport (>= 7.0) base64 (~> 0.2) @@ -173,11 +172,11 @@ GEM mini_magick (5.3.1) logger mini_mime (1.1.5) - minitest (6.0.2) + minitest (6.0.5) drb (~> 2.0) prism (~> 1.5) msgpack (1.8.0) - net-imap (0.6.3) + net-imap (0.6.4) date net-protocol net-pop (0.1.2) @@ -192,31 +191,31 @@ GEM net-protocol net-ssh (7.3.2) nio4r (2.7.5) - nokogiri (1.19.2-aarch64-linux-gnu) + nokogiri (1.19.3-aarch64-linux-gnu) racc (~> 1.4) - nokogiri (1.19.2-aarch64-linux-musl) + nokogiri (1.19.3-aarch64-linux-musl) racc (~> 1.4) - nokogiri (1.19.2-arm-linux-gnu) + nokogiri (1.19.3-arm-linux-gnu) racc (~> 1.4) - nokogiri (1.19.2-arm-linux-musl) + nokogiri (1.19.3-arm-linux-musl) racc (~> 1.4) - nokogiri (1.19.2-arm64-darwin) + nokogiri (1.19.3-x86_64-linux-gnu) racc (~> 1.4) - nokogiri (1.19.2-x86_64-linux-gnu) + nokogiri (1.19.3-x86_64-linux-musl) racc (~> 1.4) nokogiri (1.19.2-x86_64-linux-musl) racc (~> 1.4) ostruct (0.6.3) - pagy (43.4.4) + pagy (43.5.3) json uri yaml - parallel (1.27.0) + parallel (2.1.0) parser (3.3.11.1) ast (~> 2.4.1) racc pdf-core (0.10.0) - phonelib (0.10.17) + phonelib (0.10.18) pp (0.6.3) prettyprint prawn (2.5.0) @@ -227,7 +226,7 @@ GEM prawn (>= 1.3.0, < 3.0.0) prettyprint (0.2.0) prism (1.9.0) - propshaft (1.3.1) + propshaft (1.3.2) actionpack (>= 7.0.0) activesupport (>= 7.0.0) rack @@ -235,12 +234,12 @@ GEM date stringio public_suffix (7.0.5) - puma (7.2.0) + puma (8.0.1) nio4r (~> 2.0) raabro (1.4.0) racc (1.8.1) - rack (3.2.5) - rack-session (2.1.1) + rack (3.2.6) + rack-session (2.1.2) base64 (>= 0.1.0) rack (>= 3.0.0) rack-test (2.2.0) @@ -281,20 +280,20 @@ GEM tsort (>= 0.2) zeitwerk (~> 2.6) rainbow (3.1.1) - rake (13.3.1) + rake (13.4.2) rdoc (7.2.0) erb psych (>= 4.0.0) tsort - regexp_parser (2.11.3) + regexp_parser (2.12.0) reline (0.6.3) io-console (~> 0.5) rexml (3.4.4) - rubocop (1.86.0) + rubocop (1.86.1) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) - parallel (~> 1.10) + parallel (>= 1.10) parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 2.9.3, < 3.0) @@ -324,7 +323,7 @@ GEM logger rubyzip (3.2.2) securerandom (0.4.1) - selenium-webdriver (4.41.0) + selenium-webdriver (4.43.0) base64 (~> 0.2) logger (~> 1.4) rexml (~> 3.2, >= 3.2.5) @@ -346,13 +345,12 @@ GEM fugit (~> 1.11) railties (>= 7.1) thor (>= 1.3.1) - sqlite3 (2.9.2-aarch64-linux-gnu) - sqlite3 (2.9.2-aarch64-linux-musl) - sqlite3 (2.9.2-arm-linux-gnu) - sqlite3 (2.9.2-arm-linux-musl) - sqlite3 (2.9.2-arm64-darwin) - sqlite3 (2.9.2-x86_64-linux-gnu) - sqlite3 (2.9.2-x86_64-linux-musl) + sqlite3 (2.9.3-aarch64-linux-gnu) + sqlite3 (2.9.3-aarch64-linux-musl) + sqlite3 (2.9.3-arm-linux-gnu) + sqlite3 (2.9.3-arm-linux-musl) + sqlite3 (2.9.3-x86_64-linux-gnu) + sqlite3 (2.9.3-x86_64-linux-musl) sshkit (1.25.0) base64 logger @@ -366,16 +364,14 @@ GEM tailwindcss-rails (4.4.0) railties (>= 7.0.0) tailwindcss-ruby (~> 4.0) - tailwindcss-ruby (4.2.1) - tailwindcss-ruby (4.2.1-aarch64-linux-gnu) - tailwindcss-ruby (4.2.1-aarch64-linux-musl) - tailwindcss-ruby (4.2.1-arm64-darwin) - tailwindcss-ruby (4.2.1-x86_64-linux-gnu) - tailwindcss-ruby (4.2.1-x86_64-linux-musl) + tailwindcss-ruby (4.2.4) + tailwindcss-ruby (4.2.4-aarch64-linux-gnu) + tailwindcss-ruby (4.2.4-aarch64-linux-musl) + tailwindcss-ruby (4.2.4-x86_64-linux-gnu) + tailwindcss-ruby (4.2.4-x86_64-linux-musl) thor (1.5.0) thruster (0.1.20) thruster (0.1.20-aarch64-linux) - thruster (0.1.20-arm64-darwin) thruster (0.1.20-x86_64-linux) timeout (0.6.1) tsort (0.2.0) @@ -411,8 +407,6 @@ PLATFORMS aarch64-linux-musl arm-linux-gnu arm-linux-musl - arm64-darwin-24 - x86_64-linux x86_64-linux-gnu x86_64-linux-musl @@ -461,16 +455,17 @@ CHECKSUMS activerecord (8.1.3) sha256=8003be7b2466ba0a2a670e603eeb0a61dd66058fccecfc49901e775260ac70ab activestorage (8.1.3) sha256=0564ce9309143951a67615e1bb4e090ee54b8befed417133cae614479b46384d activesupport (8.1.3) sha256=21a5e0dfbd4c3ddd9e1317ec6a4d782fa226e7867dc70b0743acda81a1dca20e - addressable (2.8.9) sha256=cc154fcbe689711808a43601dee7b980238ce54368d23e127421753e46895485 + addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383 base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b bcrypt (3.1.22) sha256=1f0072e88c2d705d94aff7f2c5cb02eb3f1ec4b8368671e19112527489f29032 bcrypt_pbkdf (1.1.2) sha256=c2414c23ce66869b3eb9f643d6a3374d8322dfb5078125c82792304c10b94cf6 bigdecimal (3.3.1) sha256=eaa01e228be54c4f9f53bf3cc34fe3d5e845c31963e7fcc5bedb05a4e7d52218 bindex (0.8.1) sha256=7b1ecc9dc539ed8bccfc8cb4d2732046227b09d6f37582ff12e50a5047ceb17e - bootsnap (1.23.0) sha256=c1254f458d58558b58be0f8eb8f6eec2821456785b7cdd1e16248e2020d3f214 + bootsnap (1.24.1) sha256=d7faea1dc24aa5b22dacc049c9236b64ebf60b14dd49c615e15d8402375d39ef brakeman (8.0.4) sha256=7bf921fa9638544835df9aa7b3e720a9a72c0267f34f92135955edd80d4dcf6f builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f + bundler (4.0.11) sha256=5bcec0fb78302e48d02ee46f10ee6e6942be647ba5b44a6d1ddfda9a240ce785 bundler-audit (0.9.3) sha256=81c8766c71e47d0d28a0f98c7eed028539f21a6ea3cd8f685eb6f42333c9b4e9 capybara (3.40.0) sha256=42dba720578ea1ca65fd7a41d163dd368502c191804558f6e0f71b391054aeef concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab @@ -481,14 +476,13 @@ CHECKSUMS dotenv (3.2.0) sha256=e375b83121ea7ca4ce20f214740076129ab8514cd81378161f11c03853fe619d drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373 ed25519 (1.4.0) sha256=16e97f5198689a154247169f3453ef4cfd3f7a47481fde0ae33206cdfdcac506 - erb (6.0.2) sha256=9fe6264d44f79422c87490a1558479bd0e7dad4dd0e317656e67ea3077b5242b + erb (6.0.4) sha256=38e3803694be357fe2bfe312487c74beaf9fb4e5beb3e22498952fe1645b95d9 erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9 et-orbi (1.4.0) sha256=6c7e3c90779821f9e3b324c5e96fda9767f72995d6ae435b96678a4f3e2de8bc ffi (1.17.4-aarch64-linux-gnu) sha256=b208f06f91ffd8f5e1193da3cae3d2ccfc27fc36fba577baf698d26d91c080df ffi (1.17.4-aarch64-linux-musl) sha256=9286b7a615f2676245283aef0a0a3b475ae3aae2bb5448baace630bb77b91f39 ffi (1.17.4-arm-linux-gnu) sha256=d6dbddf7cb77bf955411af5f187a65b8cd378cb003c15c05697f5feee1cb1564 ffi (1.17.4-arm-linux-musl) sha256=9d4838ded0465bef6e2426935f6bcc93134b6616785a84ffd2a3d82bc3cf6f95 - ffi (1.17.4-arm64-darwin) sha256=19071aaf1419251b0a46852abf960e77330a3b334d13a4ab51d58b31a937001b ffi (1.17.4-x86_64-linux-gnu) sha256=9d3db14c2eae074b382fa9c083fe95aec6e0a1451da249eab096c34002bc752d ffi (1.17.4-x86_64-linux-musl) sha256=3fdf9888483de005f8ef8d1cf2d3b20d86626af206cbf780f6a6a12439a9c49e fugit (1.12.1) sha256=5898f478ede9b415f0804e42b8f3fd53f814bd85eebffceebdbc34e1107aaf68 @@ -497,9 +491,9 @@ CHECKSUMS image_processing (1.14.0) sha256=754cc169c9c262980889bec6bfd325ed1dafad34f85242b5a07b60af004742fb importmap-rails (2.2.3) sha256=7101be2a4dc97cf1558fb8f573a718404c5f6bcfe94f304bf1f39e444feeb16a io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc - irb (1.17.0) sha256=168c4ddb93d8a361a045c41d92b2952c7a118fa73f23fe14e55609eb7a863aae + irb (1.18.0) sha256=de9454a0703a54704b9811a5ef31a60c86949fbf4013fcf244fabc7c775248e3 jbuilder (2.14.1) sha256=4eb26376ff60ef100cb4fd6fd7533cd271f9998327e86adf20fd8c0e69fabb42 - json (2.19.3) sha256=289b0bb53052a1fa8c34ab33cc750b659ba14a5c45f3fcf4b18762dc67c78646 + json (2.19.4) sha256=670a7d333fb3b18ca5b29cb255eb7bef099e40d88c02c80bd42a3f30fe5239ac kamal (2.11.0) sha256=1408864425e0dec7e0a14d712a3b13f614e9f3a425b7661d3f9d287a51d7dd75 language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87 @@ -510,9 +504,9 @@ CHECKSUMS matrix (0.4.3) sha256=a0d5ab7ddcc1973ff690ab361b67f359acbb16958d1dc072b8b956a286564c5b mini_magick (5.3.1) sha256=29395dfd76badcabb6403ee5aff6f681e867074f8f28ce08d78661e9e4a351c4 mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef - minitest (6.0.2) sha256=db6e57956f6ecc6134683b4c87467d6dd792323c7f0eea7b93f66bd284adbc3d + minitest (6.0.5) sha256=f007d7246bf4feea549502842cd7c6aba8851cdc9c90ba06de9c476c0d01155c msgpack (1.8.0) sha256=e64ce0212000d016809f5048b48eb3a65ffb169db22238fb4b72472fecb2d732 - net-imap (0.6.3) sha256=9bab75f876596d09ee7bf911a291da478e0cd6badc54dfb82874855ccc82f2ad + net-imap (0.6.4) sha256=9a5598c67a3022c284d98430ef1d4948e7dbdb62596f61081ea8ca933270a02b net-pop (0.1.2) sha256=848b4e982013c15b2f0382792268763b748cce91c9e91e36b0f27ed26420dff3 net-protocol (0.2.2) sha256=aa73e0cba6a125369de9837b8d8ef82a61849360eba0521900e2c3713aa162a8 net-scp (4.1.0) sha256=a99b0b92a1e5d360b0de4ffbf2dc0c91531502d3d4f56c28b0139a7c093d1a5d @@ -520,32 +514,31 @@ CHECKSUMS net-smtp (0.5.1) sha256=ed96a0af63c524fceb4b29b0d352195c30d82dd916a42f03c62a3a70e5b70736 net-ssh (7.3.2) sha256=65029e213c380e20e5fd92ece663934ab0a0fe888e0cd7cc6a5b664074362dd4 nio4r (2.7.5) sha256=6c90168e48fb5f8e768419c93abb94ba2b892a1d0602cb06eef16d8b7df1dca1 - nokogiri (1.19.2-aarch64-linux-gnu) sha256=c34d5c8208025587554608e98fd88ab125b29c80f9352b821964e9a5d5cfbd19 - nokogiri (1.19.2-aarch64-linux-musl) sha256=7f6b4b0202d507326841a4f790294bf75098aef50c7173443812e3ac5cb06515 - nokogiri (1.19.2-arm-linux-gnu) sha256=b7fa1139016f3dc850bda1260988f0d749934a939d04ef2da13bec060d7d5081 - nokogiri (1.19.2-arm-linux-musl) sha256=61114d44f6742ff72194a1b3020967201e2eb982814778d130f6471c11f9828c - nokogiri (1.19.2-arm64-darwin) sha256=58d8ea2e31a967b843b70487a44c14c8ba1866daa1b9da9be9dbdf1b43dee205 - nokogiri (1.19.2-x86_64-linux-gnu) sha256=fa8feca882b73e871a9845f3817a72e9734c8e974bdc4fbad6e4bc6e8076b94f - nokogiri (1.19.2-x86_64-linux-musl) sha256=93128448e61a9383a30baef041bf1f5817e22f297a1d400521e90294445069a8 + nokogiri (1.19.3-aarch64-linux-gnu) sha256=46b89e5d7b9e844c2ee360794240c6ea2a4e6fa0c5892a4ed487db621224b639 + nokogiri (1.19.3-aarch64-linux-musl) sha256=8392dfdcd21be7a94dbbe9ccc138dea01b97b24cb2dc02a114ca98bfb1d9a0b7 + nokogiri (1.19.3-arm-linux-gnu) sha256=3919d5ffc334ad778a4a9eb88fda7dcb8b1fb58c8a52ac640c6dcd2f038e774f + nokogiri (1.19.3-arm-linux-musl) sha256=9ce1cb6346bb9c67b1550eb537aa183ead91e4b6eadb2f36ade02d8dd2a79fb6 + nokogiri (1.19.3-x86_64-linux-gnu) sha256=2f5078620fe12e83669b5b17311b32532a8153d02eee7ad06948b926d6080976 + nokogiri (1.19.3-x86_64-linux-musl) sha256=248c906d2166eca5efb56d52fdee5f9a1f51d69a72e2b64fdac647b4ce39ea3f ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912 - pagy (43.4.4) sha256=b41a57328a0aabfd222266a89e9de3dc3a735c17bd57f8113829c95fece5bef6 - parallel (1.27.0) sha256=4ac151e1806b755fb4e2dc2332cbf0e54f2e24ba821ff2d3dcf86bf6dc4ae130 + pagy (43.5.3) sha256=f9d73e690648d484706661dcb815647775cf8330fcc5c6e62ec87b9df431290b + parallel (2.1.0) sha256=b35258865c2e31134c5ecb708beaaf6772adf9d5efae28e93e99260877b09356 parser (3.3.11.1) sha256=d17ace7aabe3e72c3cc94043714be27cc6f852f104d81aa284c2281aecc65d54 pdf-core (0.10.0) sha256=0a5d101e2063c01e3f941e1ee47cbb97f1adfc1395b58372f4f65f1300f3ce91 - phonelib (0.10.17) sha256=8c97b6abc1877a8313ef32f9438cd35e24a943f9a70666af85eb69ab81caf4e3 + phonelib (0.10.18) sha256=2096b127bfbd4fef58eddb4dc916bb285495855b6673648719dff76cd8c32007 pp (0.6.3) sha256=2951d514450b93ccfeb1df7d021cae0da16e0a7f95ee1e2273719669d0ab9df6 prawn (2.5.0) sha256=f4e20e3b4f30bf5b9ae37dad15eb421831594553aa930b2391b0fa0a99c43cb6 prawn-table (0.2.2) sha256=336d46e39e003f77bf973337a958af6a68300b941c85cb22288872dc2b36addb prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193 prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85 - propshaft (1.3.1) sha256=9acc664ef67e819ffa3d95bd7ad4c3623ea799110c5f4dee67fa7e583e74c392 + propshaft (1.3.2) sha256=1d56a3e56a92c21bfc29caf07406b5386b00d4c47ddf357cf989a5a234b1389e psych (5.3.1) sha256=eb7a57cef10c9d70173ff74e739d843ac3b2c019a003de48447b2963d81b1974 public_suffix (7.0.5) sha256=1a8bb08f1bbea19228d3bed6e5ed908d1cb4f7c2726d18bd9cadf60bc676f623 - puma (7.2.0) sha256=bf8ef4ab514a4e6d4554cb4326b2004eba5036ae05cf765cfe51aba9706a72a8 + puma (8.0.1) sha256=7b94e50c07655718c1fb8ae41a11fc06c7d61293208b3aa608ff71a46d3ad37c raabro (1.4.0) sha256=d4fa9ff5172391edb92b242eed8be802d1934b1464061ae5e70d80962c5da882 racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f - rack (3.2.5) sha256=4cbd0974c0b79f7a139b4812004a62e4c60b145cba76422e288ee670601ed6d3 - rack-session (2.1.1) sha256=0b6dc07dea7e4b583f58a48e8b806d4c9f1c6c9214ebc202ec94562cbea2e4e9 + rack (3.2.6) sha256=5ed78e1f73b2e25679bec7d45ee2d4483cc4146eb1be0264fc4d94cb5ef212c2 + rack-session (2.1.2) sha256=595434f8c0c3473ae7d7ac56ecda6cc6dfd9d37c0b2b5255330aa1576967ffe8 rack-test (2.2.0) sha256=005a36692c306ac0b4a9350355ee080fd09ddef1148a5f8b2ac636c720f5c463 rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868 rails (8.1.3) sha256=6d017ba5348c98fc909753a8169b21d44de14d2a0b92d140d1a966834c3c9cd3 @@ -554,12 +547,12 @@ CHECKSUMS rails-i18n (8.1.0) sha256=52d5fd6c0abef28d84223cc05647f6ae0fd552637a1ede92deee9545755b6cf3 railties (8.1.3) sha256=913eb0e0cb520aac687ffd74916bd726d48fa21f47833c6292576ef6a286de22 rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a - rake (13.3.1) sha256=8c9e89d09f66a26a01264e7e3480ec0607f0c497a861ef16063604b1b08eb19c + rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701 rdoc (7.2.0) sha256=8650f76cd4009c3b54955eb5d7e3a075c60a57276766ebf36f9085e8c9f23192 - regexp_parser (2.11.3) sha256=ca13f381a173b7a93450e53459075c9b76a10433caadcb2f1180f2c741fc55a4 + regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835 rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142 - rubocop (1.86.0) sha256=4ff1186fe16ebe9baff5e7aad66bb0ad4cabf5cdcd419f773146dbba2565d186 + rubocop (1.86.1) sha256=44415f3f01d01a21e01132248d2fd0867572475b566ca188a0a42133a08d4531 rubocop-ast (1.49.1) sha256=4412f3ee70f6fe4546cc489548e0f6fcf76cafcfa80fa03af67098ffed755035 rubocop-performance (1.26.1) sha256=cd19b936ff196df85829d264b522fd4f98b6c89ad271fa52744a8c11b8f71834 rubocop-rails (2.34.3) sha256=10d37989024865ecda8199f311f3faca990143fbac967de943f88aca11eb9ad2 @@ -568,31 +561,28 @@ CHECKSUMS ruby-vips (2.3.0) sha256=e685ec02c13969912debbd98019e50492e12989282da5f37d05f5471442f5374 rubyzip (3.2.2) sha256=c0ed99385f0625415c8f05bcae33fe649ed2952894a95ff8b08f26ca57ea5b3c securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1 - selenium-webdriver (4.41.0) sha256=cdc1173cd55cf186022cea83156cc2d0bec06d337e039b02ad25d94e41bedd22 + selenium-webdriver (4.43.0) sha256=a634377b964b701c6ac0a009ce3a08fa34ec1e1e7fe9a6d57e3088d14529a65c solid_cable (3.0.12) sha256=a168a54731a455d5627af48d8441ea3b554b8c1f6e6cd6074109de493e6b0460 solid_cache (1.0.10) sha256=bc05a2fb3ac78a6f43cbb5946679cf9db67dd30d22939ededc385cb93e120d41 solid_queue (1.4.0) sha256=e6a18d196f0b27cb6e3c77c5b31258b05fb634f8ed64fb1866ed164047216c2a - sqlite3 (2.9.2-aarch64-linux-gnu) sha256=eeb86db55645b85327ba75129e3614658d974bf4da8fdc87018a0d42c59f6e42 - sqlite3 (2.9.2-aarch64-linux-musl) sha256=4feff91fb8c2b13688da34b5627c9d1ed9cedb3ee87a7114ec82209147f07a6d - sqlite3 (2.9.2-arm-linux-gnu) sha256=1ee2eb06b5301aaf5ce343a6e88d99ac932d95202d7b350f0e7b6d8d588580d7 - sqlite3 (2.9.2-arm-linux-musl) sha256=8ca0de6aceede968de0394e22e95d549834c4d8e318f69a92a52f049878a0057 - sqlite3 (2.9.2-arm64-darwin) sha256=d15bd9609a05f9d54930babe039585efc8cadd57517c15b64ec7dfa75158a5e9 - sqlite3 (2.9.2-x86_64-linux-gnu) sha256=dce83ffcb7e72f9f7aeb6e5404f15d277a45332fe18ccce8a8b3ed51e8d23aee - sqlite3 (2.9.2-x86_64-linux-musl) sha256=e8dd906a613f13b60f6d47ae9dda376384d9de1ab3f7e3f2fdf2fd18a871a2d7 + sqlite3 (2.9.3-aarch64-linux-gnu) sha256=ca6dd1cf6c037ccc8d3e5837190cc61ef15466092014951235641b5c4c8ab4ee + sqlite3 (2.9.3-aarch64-linux-musl) sha256=ff017a36c463d02e9f0be7a6224521371128024e6a05ed16994afa5c037afbba + sqlite3 (2.9.3-arm-linux-gnu) sha256=fd8b74337a66bdaf746b97d65e6c9a2faff803c8f72d6b107fb880972815d072 + sqlite3 (2.9.3-arm-linux-musl) sha256=792ae9a786bb37dbdc4c443c527bc91df423aac10e472f76d5cf5a9ac6d51980 + sqlite3 (2.9.3-x86_64-linux-gnu) sha256=85200a10c6cf5c60085fcca411a3168c5fba8fda3e2b1b0109ec277d7c226d46 + sqlite3 (2.9.3-x86_64-linux-musl) sha256=b6d0437046d9180335dea1aa0592802e65c4f7b57409d63f14408211bf28536b sshkit (1.25.0) sha256=c8c6543cdb60f91f1d277306d585dd11b6a064cb44eab0972827e4311ff96744 stimulus-rails (1.3.4) sha256=765676ffa1f33af64ce026d26b48e8ffb2e0b94e0f50e9119e11d6107d67cb06 stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1 tailwindcss-rails (4.4.0) sha256=efa2961351a52acebe616e645a81a30bb4f27fde46cc06ce7688d1cd1131e916 - tailwindcss-ruby (4.2.1) sha256=95886a1e24b42d76792c787d34e47098b53cb3b5a6363845bca4486f52b2e66a - tailwindcss-ruby (4.2.1-aarch64-linux-gnu) sha256=de457ddfc999c6bbbe1a59fbc11eb2168d619f6e0cb72d8d3334d372b331e36f - tailwindcss-ruby (4.2.1-aarch64-linux-musl) sha256=e6ed27704263201f8366316354aa45f9016cc9378ce8fac46fbbe65fafd4da5e - tailwindcss-ruby (4.2.1-arm64-darwin) sha256=bcf222fb8542cf5433925623e5e7b257897fbb8291a2350daae870a32f2eeb91 - tailwindcss-ruby (4.2.1-x86_64-linux-gnu) sha256=201d0e5e5d4aba52cae4ee4bd1acd497d2790c83e7f15da964aab8ec93876831 - tailwindcss-ruby (4.2.1-x86_64-linux-musl) sha256=79fa48ad51e533545f9fdbb04227e1342a65a42c2bd1314118b95473d5612007 + tailwindcss-ruby (4.2.4) sha256=f3025ba442aa1436168a6df07cf44f9f43f0124a69b5375db1d359ad39c12446 + tailwindcss-ruby (4.2.4-aarch64-linux-gnu) sha256=8f73d4faf9e36ef9a4f0691cbab7b63cd0ecaf955c7878ee6a96033a29f30e8c + tailwindcss-ruby (4.2.4-aarch64-linux-musl) sha256=e96e9f5ba4743179d3731a91ea0b002383915f8bf7d6a23c9ef82b0e146a6abf + tailwindcss-ruby (4.2.4-x86_64-linux-gnu) sha256=8bac2ad4a1a1d7e1ff387499f322491a4bcbac622f12e396222cdd29a3499917 + tailwindcss-ruby (4.2.4-x86_64-linux-musl) sha256=a54ec6b5e7b3903328950ef7314825d10f43d581488fbb3f53c9172d40e478b7 thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73 thruster (0.1.20) sha256=c05f2fbcae527bbe093a6e6d84fb12d9d680617e7c162325d9b97e8e9d4b5201 thruster (0.1.20-aarch64-linux) sha256=754f1701061235235165dde31e7a3bc87ec88066a02981ff4241fcda0d76d397 - thruster (0.1.20-arm64-darwin) sha256=630cf8c273f562063b92ea5ccd7a721d7ba6130cc422c823727f4744f6d0770e thruster (0.1.20-x86_64-linux) sha256=d579f252bf67aee6ba6d957e48f566b72e019d7657ba2f267a5db1e4d91d2479 timeout (0.6.1) sha256=78f57368a7e7bbadec56971f78a3f5ecbcfb59b7fcbb0a3ed6ddc08a5094accb tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f @@ -612,4 +602,4 @@ CHECKSUMS zeitwerk (2.7.5) sha256=d8da92128c09ea6ec62c949011b00ed4a20242b255293dd66bf41545398f73dd BUNDLED WITH - 4.0.9 + 4.0.11 diff --git a/app/assets/images/icons/access.svg b/app/assets/images/icons/access.svg new file mode 100644 index 0000000..6728946 --- /dev/null +++ b/app/assets/images/icons/access.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/icons/contact_page.svg b/app/assets/images/icons/contact_page.svg deleted file mode 100644 index 1d888c7..0000000 --- a/app/assets/images/icons/contact_page.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/assets/images/icons/login.svg b/app/assets/images/icons/login.svg new file mode 100644 index 0000000..7bb310c --- /dev/null +++ b/app/assets/images/icons/login.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/icons/monitoring.svg b/app/assets/images/icons/monitoring.svg new file mode 100644 index 0000000..0e91965 --- /dev/null +++ b/app/assets/images/icons/monitoring.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/icons/other.svg b/app/assets/images/icons/other.svg deleted file mode 100644 index 6980875..0000000 --- a/app/assets/images/icons/other.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/assets/images/icons/person_add.svg b/app/assets/images/icons/person_add.svg deleted file mode 100644 index 3158153..0000000 --- a/app/assets/images/icons/person_add.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/assets/images/icons/person_search.svg b/app/assets/images/icons/person_search.svg new file mode 100644 index 0000000..a7435a0 --- /dev/null +++ b/app/assets/images/icons/person_search.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/icons/shopping_bag.svg b/app/assets/images/icons/shopping_bag.svg new file mode 100644 index 0000000..bd94678 --- /dev/null +++ b/app/assets/images/icons/shopping_bag.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/icons/sticky_note_2.svg b/app/assets/images/icons/sticky_note_2.svg deleted file mode 100644 index db6f242..0000000 --- a/app/assets/images/icons/sticky_note_2.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/assets/images/icons/view.svg b/app/assets/images/icons/view.svg deleted file mode 100644 index f744edd..0000000 --- a/app/assets/images/icons/view.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/assets/tailwind/application.css b/app/assets/tailwind/application.css index 9b30276..bc6f361 100644 --- a/app/assets/tailwind/application.css +++ b/app/assets/tailwind/application.css @@ -3,5 +3,5 @@ @source not "./daisyui{,*}.mjs"; @plugin "./daisyui.mjs" { - themes: all; + themes: corporate --default, business --prefersdark, light, dark, dim; } diff --git a/app/controllers/access_logs_controller.rb b/app/controllers/access_logs_controller.rb index 7ed3b6c..f3e803b 100644 --- a/app/controllers/access_logs_controller.rb +++ b/app/controllers/access_logs_controller.rb @@ -1,10 +1,33 @@ class AccessLogsController < ApplicationController + include Filterable + + before_action :require_admin + before_action :set_access_log, only: [ :destroy ] + def index + @total_accesses = AccessLog.count + @pagy, @access_logs = pagy( + AccessLog + .apply_filters(filter_params) + .includes(:member, :discipline, :checkin_by_user) + ) end def new + @access_log = AccessLog.new end - def create + def destroy + @access_log.destroy + turbo_refresh_or_redirect_to access_logs_path, status: :see_other, notice: "Accesso annullato con successo." end + + private + def set_access_log + @access_log = AccessLog.find(params[:id]) + end + + def filter_params + params.permit(:query, :sort, :status, :discipline_id) + end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index a40ee79..3d7cf4b 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,5 +1,5 @@ class ApplicationController < ActionController::Base - include Themable, Authentication + include Themable, Localizable, Authentication include Pagy::Method @@ -8,4 +8,19 @@ class ApplicationController < ActionController::Base # Changes to the importmap will invalidate the etag for HTML responses stale_when_importmap_changes + + private + + def turbo_refresh_or_redirect_to(fallback_path, options = {}) + respond_to do |format| + format.turbo_stream do + flash[:notice] = options[:notice] if options[:notice] + flash[:alert] = options[:alert] if options[:alert] + + render turbo_stream: turbo_stream.refresh(request_id: nil) + end + + format.html { redirect_to fallback_path, **options } + end + end end diff --git a/app/controllers/concerns/filterable.rb b/app/controllers/concerns/filterable.rb new file mode 100644 index 0000000..3b73847 --- /dev/null +++ b/app/controllers/concerns/filterable.rb @@ -0,0 +1,16 @@ +module Filterable + extend ActiveSupport::Concern + + included do + helper_method :filtering?, :active_filters + end + + private + def filtering? + active_filters.any? + end + + def active_filters + request.query_parameters.except(:sort, :commit, :page).reject { |_, v| v.blank? } + end +end diff --git a/app/controllers/concerns/filterable_actions.rb b/app/controllers/concerns/filterable_actions.rb deleted file mode 100644 index 4db6548..0000000 --- a/app/controllers/concerns/filterable_actions.rb +++ /dev/null @@ -1,31 +0,0 @@ -module FilterableActions - extend ActiveSupport::Concern - - included do - helper_method :current_filter_params - end - - def filter_and_paginate(scope, filter_namespace: :query) - model_class = scope.model - - @available_filters = model_class.available_filters - @available_sorts = model_class.available_sorts - - flat_params = extract_flat_params(model_class, filter_namespace) - filtered_scope = scope.merge(model_class.apply_filters(flat_params)) - - pagy(filtered_scope) - end - - private - def extract_flat_params(model_class, namespace) - permitted_keys = model_class.available_filters.map { |f| f[:key] } - namespace_params = params.fetch(namespace, {}).permit(permitted_keys) - sort_params = params.permit(:sort, :direction) - namespace_params.merge(sort_params) - end - - def current_filter_params - @_current_filter_params ||= params.fetch(:query, {}).to_unsafe_h - end -end diff --git a/app/controllers/concerns/localizable.rb b/app/controllers/concerns/localizable.rb new file mode 100644 index 0000000..8f3071d --- /dev/null +++ b/app/controllers/concerns/localizable.rb @@ -0,0 +1,14 @@ +module Localizable + extend ActiveSupport::Concern + + included do + around_action :switch_locale + end + + private + def switch_locale(&action) + locale = current_user&.locale || I18n.default_locale + + I18n.with_locale(locale, &action) + end +end diff --git a/app/controllers/concerns/safe_date_parsing.rb b/app/controllers/concerns/safe_date_parsing.rb new file mode 100644 index 0000000..ae0668f --- /dev/null +++ b/app/controllers/concerns/safe_date_parsing.rb @@ -0,0 +1,44 @@ +module SafeDateParsing + extend ActiveSupport::Concern + + included do + helper_method :parse_month_param, :parse_date_param + end + + private + def parse_month_param(month_string, fallback: Date.current) + return fallback if month_string.blank? + + normalized_string = month_string.to_s.strip + + unless normalized_string.match?(/\A\d{4}-\d{2}\z/) + Rails.logger.warn("[SafeDateParsing] Formato mese non valido o manomesso: #{normalized_string.inspect}") + return fallback + end + + begin + Date.strptime("#{normalized_string}-01", "%Y-%m-%d") + rescue Date::Error => e + Rails.logger.warn("[SafeDateParsing] Errore logico (Date::Error) per il mese #{normalized_string}: #{e.message}") + fallback + end + end + + def parse_date_param(date_string, fallback: Date.current) + return fallback if date_string.blank? + + normalized_string = date_string.to_s.strip + + unless normalized_string.match?(/\A\d{4}-\d{2}-\d{2}\z/) + Rails.logger.warn("[SafeDateParsing] Formato data non valido o manomesso: #{normalized_string.inspect}") + return fallback + end + + begin + Date.strptime(normalized_string, "%Y-%m-%d") + rescue Date::Error => e + Rails.logger.warn("[SafeDateParsing] Errore logico (Date::Error) per la data #{normalized_string}: #{e.message}") + fallback + end + end +end diff --git a/app/controllers/concerns/themable.rb b/app/controllers/concerns/themable.rb index ed0f3ec..931726d 100644 --- a/app/controllers/concerns/themable.rb +++ b/app/controllers/concerns/themable.rb @@ -2,13 +2,11 @@ module Themable extend ActiveSupport::Concern included do - before_action :load_theme + helper_method :current_theme end private - def load_theme - return @theme = current_user.theme_or_default if current_user - - @theme = "light" + def current_theme + current_user&.theme || "corporate" end end diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index dec9e61..99defe2 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -2,13 +2,18 @@ class DashboardController < ApplicationController def index @daily_cash = DailyCash.current - @recent_sales = Sale.kept - .includes(:member, :product, :user) - .order(created_at: :desc) - .limit(10) - - @expiring_count = Subscription.kept - .where(end_date: Date.current..7.days.from_now) - .count + @today_accesses_count = AccessLog.where(entered_at: Time.current.beginning_of_day..Time.current.end_of_day).count + + @expiring_subscriptions = Subscription.kept + .includes(:member) + .where(end_date: Date.current..7.days.from_now) + .order(end_date: :asc) + .limit(5) + + @expiring_count = Subscription.kept.where(end_date: Date.current..7.days.from_now).count + + @recent_accesses = AccessLog.includes(:member, :discipline) + .order(entered_at: :desc) + .limit(5) end end diff --git a/app/controllers/disciplines/members_controller.rb b/app/controllers/disciplines/members_controller.rb index 6d945c1..85eb0a5 100644 --- a/app/controllers/disciplines/members_controller.rb +++ b/app/controllers/disciplines/members_controller.rb @@ -1,20 +1,24 @@ class Disciplines::MembersController < ApplicationController + include Filterable + before_action :set_discipline def index - product_ids = @discipline.product_ids + @products = @discipline.products.kept + + @query = @discipline.recent_subscriptions + .apply_filters(filter_params) + .includes(:product, member: [ :subscriptions ]) - all_subs = Subscription.kept - .where(product_id: product_ids) - .where("end_date >= ?", 30.days.ago) - .includes(:member, :product) - @subscriptions = all_subs.group_by(&:member_id) - .map { |_, subs| subs.max_by(&:end_date) } - .sort_by(&:end_date) + @pagy, @subscriptions = pagy(@query) end private def set_discipline @discipline = Discipline.find(params[:discipline_id]) end + + def filter_params + params.permit(:query, :sort, :state, :product_id, :membership_status, :med_cert) + end end diff --git a/app/controllers/disciplines_controller.rb b/app/controllers/disciplines_controller.rb index 913b6ee..820d8e7 100644 --- a/app/controllers/disciplines_controller.rb +++ b/app/controllers/disciplines_controller.rb @@ -1,8 +1,17 @@ class DisciplinesController < ApplicationController + include Filterable + before_action :set_discipline, only: [ :show, :edit, :update, :destroy ] + layout "modal", only: [ :new, :create, :edit, :update ] + def index - @pagy, @disciplines = pagy(Discipline.kept.order(:name)) + @total_active_disciplines = Discipline.kept.count + @pagy, @disciplines = pagy( + Discipline + .apply_filters(filter_params) + .includes(:products) + ) end def show @@ -14,37 +23,33 @@ def new requires_medical_certificate: true, requires_membership: true ) - - render layout: "modal" end def create @discipline = Discipline.new(discipline_params) if @discipline.save - redirect_to disciplines_path, notice: t(".created", default: "Disciplina creata con successo.") + turbo_refresh_or_redirect_to disciplines_path, notice: "Disciplina creata con successo." else - render :new, layout: "modal", status: :unprocessable_entity + render :new, status: :unprocessable_entity end end - def edit - render layout: "modal" - end + def edit; end def update if @discipline.update(discipline_params) - redirect_to disciplines_path, notice: t(".updated", default: "Disciplina aggiornata.") + turbo_refresh_or_redirect_to disciplines_path, notice: "Disciplina aggiornata." else - render :edit, layout: "modal", status: :unprocessable_entity + render :edit, status: :unprocessable_entity end end def destroy if @discipline.discard! - redirect_to disciplines_path, notice: t(".discarded", default: "Disciplina archiviata.") + turbo_refresh_or_redirect_to disciplines_path, notice: "Disciplina archiviata." else - redirect_to disciplines_path, alert: t(".error", default: "Impossibile archiviare.") + turbo_refresh_or_redirect_to disciplines_path, alert: "Impossibile archiviare." end end @@ -56,4 +61,8 @@ def set_discipline def discipline_params params.require(:discipline).permit(:name, :requires_medical_certificate, :requires_membership) end + + def filter_params + params.permit(:query, :sort) + end end diff --git a/app/controllers/kiosk/access_logs_controller.rb b/app/controllers/kiosk/access_logs_controller.rb new file mode 100644 index 0000000..6b58619 --- /dev/null +++ b/app/controllers/kiosk/access_logs_controller.rb @@ -0,0 +1,39 @@ +class Kiosk::AccessLogsController < Kiosk::BaseController + before_action :set_discipline + before_action :set_discipline_access_log, only: [ :destroy ] + before_action :set_member, only: [ :create ] + + def create + @access_log = @discipline.access_logs.build(member: @member, checkin_by_user: current_user) + + if @access_log.save + if @access_log.status == "ok" + flash[:success] = "Check-in registrato per #{@member.first_name}" + else + flash[:warning] = "Check-in forzato per #{@member.first_name} (Verificare anagrafica)" + end + else + flash[:error] = @access_log.errors.full_messages.to_sentence + end + + redirect_to kiosk_discipline_path(@discipline) + end + + def destroy + @access_log.destroy + redirect_to kiosk_discipline_path(@discipline), notice: "Check-in annullato per #{@access_log.member.first_name}" + end + + private + def set_discipline + @discipline = Discipline.find(params[:discipline_id]) + end + + def set_discipline_access_log + @access_log = @discipline.access_logs.find(params[:id]) + end + + def set_member + @member = Member.find(params[:member_id]) + end +end diff --git a/app/controllers/kiosk/base_controller.rb b/app/controllers/kiosk/base_controller.rb new file mode 100644 index 0000000..113dd83 --- /dev/null +++ b/app/controllers/kiosk/base_controller.rb @@ -0,0 +1,3 @@ +class Kiosk::BaseController < ApplicationController + layout "kiosk" +end diff --git a/app/controllers/kiosk/disciplines_controller.rb b/app/controllers/kiosk/disciplines_controller.rb new file mode 100644 index 0000000..728119e --- /dev/null +++ b/app/controllers/kiosk/disciplines_controller.rb @@ -0,0 +1,20 @@ +class Kiosk::DisciplinesController < Kiosk::BaseController + def index + @disciplines = Discipline.kept.order(:name) + end + + def show + @discipline = Discipline.kept.find(params[:id]) + + @today_accesses = @discipline + .access_logs + .today + .includes(:member) + .order(entered_at: :desc) + + @pending_members = Member.kept + .with_active_subscription_for(@discipline) + .without_recent_checkin_for(@discipline) + .order(:first_name, :last_name) + end +end diff --git a/app/controllers/kiosk/member_searches_controller.rb b/app/controllers/kiosk/member_searches_controller.rb new file mode 100644 index 0000000..c9491cc --- /dev/null +++ b/app/controllers/kiosk/member_searches_controller.rb @@ -0,0 +1,14 @@ +class Kiosk::MemberSearchesController < Kiosk::BaseController + layout false + + def index + @discipline = Discipline.find(params[:discipline_id]) + + if params[:query].present? + @members = Member.search_text(params[:query]).limit(10) + @checked_in_ids = @discipline.access_logs.recent_for_kiosk.pluck(:member_id) + else + @members = Member.none + end + end +end diff --git a/app/controllers/members/access_logs_controller.rb b/app/controllers/members/access_logs_controller.rb index bd75939..b682ce7 100644 --- a/app/controllers/members/access_logs_controller.rb +++ b/app/controllers/members/access_logs_controller.rb @@ -1,14 +1,12 @@ class Members::AccessLogsController < ApplicationController before_action :set_member - # TODO def index - @access_logs = @member.access_logs.order(created_at: :desc).limit(100) + @pagy, @access_logs = pagy(@member.access_logs.order(created_at: :desc)) end private - - def set_member - @member = Member.find(params[:member_id]) - end + def set_member + @member = Member.find(params[:member_id]) + end end diff --git a/app/controllers/members/sales_controller.rb b/app/controllers/members/sales_controller.rb index 93f3b98..8cfdedf 100644 --- a/app/controllers/members/sales_controller.rb +++ b/app/controllers/members/sales_controller.rb @@ -1,15 +1,22 @@ -class Members::SalesController < ApplicationController +class Members::SalesController < MembersController before_action :require_admin before_action :set_member def index - @sales = @member.sales.kept - .includes(:product, :user) - .order(sold_on: :desc, created_at: :desc) + @query = @member.sales + .apply_filters(filter_params) + .includes(:product, :user, subscription: [ :product, :sales ]) + + @pagy, @sales = pagy(@query) + @total_amount_cents = @query.sum(:amount_cents) end private def set_member @member = Member.find(params[:member_id]) end + + def filter_params + params.permit(:query, :sort, :product_id, :payment_method, :state) + end end diff --git a/app/controllers/members/searches_controller.rb b/app/controllers/members/searches_controller.rb new file mode 100644 index 0000000..89894a7 --- /dev/null +++ b/app/controllers/members/searches_controller.rb @@ -0,0 +1,7 @@ +class Members::SearchesController < ApplicationController + layout false + + def index + @members = params[:query].present? ? Member.search_text(params[:query]).limit(10) : Member.none + end +end diff --git a/app/controllers/members/subscriptions_controller.rb b/app/controllers/members/subscriptions_controller.rb index 4114df6..7207230 100644 --- a/app/controllers/members/subscriptions_controller.rb +++ b/app/controllers/members/subscriptions_controller.rb @@ -2,9 +2,8 @@ class Members::SubscriptionsController < ApplicationController before_action :set_member def index - @subscriptions = @member.subscriptions.kept - .includes(:product, :sale) - .order(end_date: :desc) + query = @member.subscriptions.kept.includes(:product, :sales).order(end_date: :desc) + @pagy, @subscriptions = pagy(query) end private diff --git a/app/controllers/members_controller.rb b/app/controllers/members_controller.rb index c1cc21b..e452c4a 100644 --- a/app/controllers/members_controller.rb +++ b/app/controllers/members_controller.rb @@ -1,57 +1,47 @@ class MembersController < ApplicationController - include FilterableActions + include Filterable - before_action :set_member, only: [ :edit, :update, :destroy ] + before_action :set_member, only: [ :show, :edit, :update, :destroy ] - def index - base_scope = Member.includes(:subscriptions) + layout "modal", only: [ :new, :edit ] - @pagy, @members = filter_and_paginate(base_scope) + def index + @total_active_members = Member.kept.count + @pagy, @members = pagy( + Member + .apply_filters(filter_params) + .includes(subscriptions: [ :product, :sales ]) + ) end def show - @member = Member.includes(subscriptions: :product, sales: []) - .find(params[:id]) - end - - def renewal_info @member = Member.find(params[:id]) - product = Product.find_by(id: params[:product_id]) - - is_manual_input = params[:ref_date].present? - - reference_date = is_manual_input ? Date.parse(params[:ref_date]) : Date.current - - if product - info = RenewalCalculator.new(@member, product, reference_date, manual_override: is_manual_input).call - render json: info - else - render json: {}, status: :bad_request - end + @active_subscriptions = @member.subscriptions.kept + .includes(:product, :access_logs) + .select { |s| (s.end_date.nil? || s.end_date >= Date.current) && !s.out_of_entries? } + .sort_by { |s| s.start_date || Date.current } + @recent_sales = @member.recent_sales end def new @member = Member.new - render layout: "modal" end def create @member = Member.new(member_params) if @member.save - redirect_to @member, notice: t(".created", default: "Socio creato con successo.") + turbo_refresh_or_redirect_to @member, notice: t(".created", default: "Socio creato con successo.") else render :new, layout: "modal", status: :unprocessable_entity end end - def edit - render layout: "modal" - end + def edit; end def update if @member.update(member_params) - redirect_to @member, notice: t(".updated", default: "Socio aggiornato con successo.") + turbo_refresh_or_redirect_to @member, notice: t(".updated", default: "Socio aggiornato con successo.") else render :edit, layout: "modal", status: :unprocessable_entity end @@ -59,7 +49,7 @@ def update def destroy if @member.discard! - redirect_to members_path, status: :see_other, notice: t(".discarded", default: "Socio archiviato correttamente.") + turbo_refresh_or_redirect_to members_path, status: :see_other, notice: t(".discarded", default: "Socio archiviato correttamente.") else redirect_to members_path, status: :see_other, alert: t(".discard_error", default: "Impossibile archiviare il socio.") end @@ -72,16 +62,13 @@ def set_member def member_params params.require(:member).permit( - :first_name, - :last_name, - :fiscal_code, - :birth_date, - :email_address, - :phone, - :address, - :city, - :zip_code, + :first_name, :last_name, :fiscal_code, :birth_date, + :email_address, :phone, :address, :city, :zip_code, :medical_certificate_expiry ) end + + def filter_params + params.permit(:query, :sort, :membership_status, :med_cert) + end end diff --git a/app/controllers/preferences/base_controller.rb b/app/controllers/preferences/base_controller.rb deleted file mode 100644 index daa658f..0000000 --- a/app/controllers/preferences/base_controller.rb +++ /dev/null @@ -1,10 +0,0 @@ -class Preferences::BaseController < ApplicationController - protected - def update_preference!(key, value) - current_user.update!(preferences: current_user.preferences.merge(key => value)) - end - - def render_preference(key) - render json: { key => current_user.preferences[key] } - end -end diff --git a/app/controllers/preferences/languages_controller.rb b/app/controllers/preferences/languages_controller.rb index 24ad889..ca87a07 100644 --- a/app/controllers/preferences/languages_controller.rb +++ b/app/controllers/preferences/languages_controller.rb @@ -1,12 +1,7 @@ -class Preferences::LanguagesController < Preferences::BaseController +class Preferences::LanguagesController < ApplicationController def update - lang = params.require(:language) # Il parametro dal form può restare "language" - allowed = I18n.available_locales.map(&:to_s) - return head :bad_request unless allowed.include?(lang) + current_user.update(locale: params.require(:language)) - I18n.locale = lang - - update_preference!("locale", lang) - render_preference("locale") + redirect_back_or_to root_path end end diff --git a/app/controllers/preferences/themes_controller.rb b/app/controllers/preferences/themes_controller.rb index 6bf5523..bbcb2d7 100644 --- a/app/controllers/preferences/themes_controller.rb +++ b/app/controllers/preferences/themes_controller.rb @@ -1,14 +1,7 @@ -class Preferences::ThemesController < Preferences::BaseController - def show - render_preference("theme") - end - +class Preferences::ThemesController < ApplicationController def update - theme = params.require(:theme) - allowed = UserPreferences::ALLOWED_THEMES - return head :bad_request unless allowed.include?(theme) + current_user.update(theme: params.require(:theme)) - update_preference!("theme", theme) - render_preference("theme") + redirect_back_or_to root_path end end diff --git a/app/controllers/products_controller.rb b/app/controllers/products_controller.rb index ff96a75..6548240 100644 --- a/app/controllers/products_controller.rb +++ b/app/controllers/products_controller.rb @@ -1,8 +1,18 @@ class ProductsController < ApplicationController + include Filterable + + before_action :require_admin before_action :set_product, only: [ :show, :edit, :update, :destroy ] + layout "modal", only: [ :new, :create, :edit, :update ] + def index - @pagy, @products = pagy(Product.kept.includes(:disciplines).order(:name)) + @total_active_products = Product.kept.count + @pagy, @products = pagy( + Product + .apply_filters(filter_params) + .includes(:disciplines) + ) end def show; end @@ -15,42 +25,37 @@ def new accounting_category: :institutional, duration_days: 30 ) - - render layout: "modal" end def create @product = Product.new(product_params) if @product.save - redirect_to products_path, notice: t(".created", default: "Prodotto creato correttamente.") + turbo_refresh_or_redirect_to products_path, notice: t(".created", default: "Prodotto creato correttamente.") else - render :new, layout: "modal", status: :unprocessable_entity + render :new, status: :unprocessable_entity end end - def edit - render layout: "modal" - end + def edit; end def update if @product.update(product_params) - redirect_to products_path, notice: t(".updated", default: "Prodotto aggiornato.") + turbo_refresh_or_redirect_to products_path, notice: t(".updated", default: "Prodotto aggiornato.") else - render :edit, layout: "modal", status: :unprocessable_entity + render :edit, status: :unprocessable_entity end end def destroy if @product.discard! - redirect_to products_path, notice: t(".discarded", default: "Prodotto archiviato.") + turbo_refresh_or_redirect_to products_path, notice: t(".discarded", default: "Prodotto archiviato.") else redirect_to products_path, alert: t(".error", default: "Impossibile archiviare.") end end private - def set_product @product = Product.find(params[:id]) end @@ -61,7 +66,12 @@ def product_params :price, :duration_days, :accounting_category, + :entry_limit, discipline_ids: [] ) end + + def filter_params + params.permit(:query, :sort) + end end diff --git a/app/controllers/receipt_counters_controller.rb b/app/controllers/receipt_counters_controller.rb deleted file mode 100644 index 5b0c2fb..0000000 --- a/app/controllers/receipt_counters_controller.rb +++ /dev/null @@ -1,10 +0,0 @@ -class ReceiptCountersController < ApplicationController - def index - end - - def edit - end - - def update - end -end diff --git a/app/controllers/reports_controller.rb b/app/controllers/reports_controller.rb index adb7f52..8e0af0a 100644 --- a/app/controllers/reports_controller.rb +++ b/app/controllers/reports_controller.rb @@ -1,26 +1,41 @@ class ReportsController < ApplicationController + include Filterable, SafeDateParsing + def index - @date = params[:date] ? Date.parse(params[:date]) : Date.current + @date = parse_month_param(params[:month]) @month_range = @date.beginning_of_month..@date.end_of_month - # 1. QUERY UNICA: Scarichiamo tutte le vendite cash del mese in un colpo solo. - # Usiamo .includes(:member) se nella vista mostrassimo i nomi, ma qui servono solo i totali. - monthly_sales = Sale.kept - .where(sold_on: @month_range, payment_method: :cash) - .order(:created_at) + stats_query = Sale.kept + .where(sold_on: @month_range, payment_method: :cash) + .group(:sold_on) + .pluck( + :sold_on, + Arel.sql("SUM(CASE WHEN CAST(strftime('%H', datetime(created_at, 'localtime')) AS INTEGER) < 14 THEN amount_cents ELSE 0 END)"), + Arel.sql("SUM(CASE WHEN CAST(strftime('%H', datetime(created_at, 'localtime')) AS INTEGER) >= 14 THEN amount_cents ELSE 0 END)"), + Arel.sql("SUM(amount_cents)"), + Arel.sql("COUNT(id)") + ) - # 2. RAGGRUPPAMENTO: Creiamo un Hash { Data => [Sale, Sale...], Data2 => [...] } - sales_by_date = monthly_sales.group_by(&:sold_on) + stats_by_date = stats_query.each_with_object({}) do |(date, morning, afternoon, total, count), hash| + hash[date] = { + morning: morning || 0, + afternoon: afternoon || 0, + total: total || 0, + count: count || 0 + } + end - # 3. MAPPING: Creiamo i DailyCash passando i dati già pronti @daily_reports = @month_range.map do |date| - # Passiamo l'array di vendite per quel giorno (o array vuoto se nil) - DailyCash.for(date, sales: sales_by_date[date] || []) - end + DailyCash.for(date, stats: stats_by_date[date] || { morning: 0, afternoon: 0, total: 0, count: 0 }) + end.reverse + + @monthly_total = @daily_reports.sum(&:total_cents) / 100.0 + + @keys = params.slice(:month).permit!.to_h.reject { |_, v| v.blank? || v == Date.current.strftime("%Y-%m") } end def show - @date = Date.parse(params[:date]) + @date = parse_date_param(params[:date]) case params[:report_type] when "daily_cash" diff --git a/app/controllers/sales_controller.rb b/app/controllers/sales_controller.rb index 4a7c38e..33f1000 100644 --- a/app/controllers/sales_controller.rb +++ b/app/controllers/sales_controller.rb @@ -1,12 +1,18 @@ class SalesController < ApplicationController + include Filterable + before_action :require_admin, only: [ :index ] before_action :set_sale, only: [ :show, :destroy ] + layout -> { turbo_frame_request_id == "pos_form_frame" ? false : "modal" }, only: [ :new, :create ] + def index - scope = Sale.kept - .includes(:member, :user) - .order(sold_on: :desc, created_at: :desc) - @pagy, @sales = pagy(scope) + @total_active_sales = Sale.kept.count + @pagy, @sales = pagy( + Sale + .apply_filters(filter_params) + .includes(:member, :user) + ) end def show @@ -17,20 +23,13 @@ def show send_data pdf.render, filename: "ricevuta_#{@sale.id}_#{@sale.member.last_name}.pdf", type: "application/pdf", - disposition: "inline" + disposition: "_blank" end end end def new - @sale = Sale.new(sold_on: Date.current, user: current_user) - @sale.build_subscription(start_date: Date.current) - - if params[:member_id] - @sale.member = Member.find(params[:member_id]) - end - - setup_renewal_data if params[:renew_subscription_id] + @sale = build_draft(sale_params_for_build) end def create @@ -40,8 +39,18 @@ def create if @sale.save redirect_to sale_path(@sale), notice: t(".created", default: "Vendita registrata con successo.") else - @sale.build_subscription(start_date: Date.current) if @sale.subscription.nil? - render :new, status: :unprocessable_entity + @sale = build_draft(sale_params, existing_sale: @sale) + + respond_to do |format| + format.turbo_stream do + render turbo_stream: turbo_stream.replace( + "pos_form_frame", + template: "sales/new", + layout: false + ), status: :unprocessable_entity + end + format.html { render :new, status: :unprocessable_entity } + end end end @@ -58,29 +67,42 @@ def set_sale @sale = Sale.find(params[:id]) end - def setup_renewal_data - return unless @sale.member && params[:renew_subscription_id] - - old_sub = @sale.member.subscriptions.find(params[:renew_subscription_id]) + def build_draft(sale_params, existing_sale: nil) + context = params.to_unsafe_h.deep_symbolize_keys - @sale.product = old_sub.product - @sale.amount = old_sub.product.price || 0 + if context[:manual_start_date].present? + context[:sale] ||= {} + context[:sale][:subscription_attributes] ||= {} + context[:sale][:subscription_attributes][:start_date] = context[:manual_start_date] + end - dates = RenewalCalculator.new(@sale.member, @sale.product, Date.current).call + PosDraftBuilder.new( + sale_params: sale_params, + context_params: context, + existing_sale: existing_sale + ).build + end - @sale.subscription.start_date = dates[:start_date] - @sale.subscription.end_date = dates[:end_date] + def sale_params_for_build + params.has_key?(:sale) ? sale_params : {} end def sale_params + permitted_sub_attrs = [ :start_date ] + + if current_user.admin? + permitted_sub_attrs << :end_date + permitted_sub_attrs << :agreed_price + end + params.require(:sale).permit( - :member_id, - :product_id, - :amount, - :payment_method, - :sold_on, - :notes, - subscription_attributes: [ :start_date ] + :member_id, :product_id, :amount, :payment_method, + :sold_on, :notes, :subscription_id, + subscription_attributes: permitted_sub_attrs ) end + + def filter_params + params.permit(:query, :sort, :state, :period, :payment_method, :accounting_category, :operator_id) + end end diff --git a/app/controllers/subscriptions_controller.rb b/app/controllers/subscriptions_controller.rb index 3ba63d9..858437c 100644 --- a/app/controllers/subscriptions_controller.rb +++ b/app/controllers/subscriptions_controller.rb @@ -1,6 +1,22 @@ class SubscriptionsController < ApplicationController + before_action :require_admin, only: [ :edit, :update ] before_action :set_subscription, only: [ :edit, :update, :destroy ] + layout "modal", only: [ :edit, :update ] + + + def index + @subscriptions = Subscription.kept.includes(:member, :product) + + if params[:filter] == "expiring" + @subscriptions = @subscriptions.where(end_date: Date.current..7.days.from_now).order(:end_date) + else + @subscriptions = @subscriptions.order(created_at: :desc) + end + + @pagy, @subscriptions = pagy(@subscriptions) + end + def edit; end def update @@ -25,12 +41,6 @@ def set_subscription end def subscription_params - permitted = [ :start_date ] - - if current_user.respond_to?(:admin?) && current_user.admin? - permitted << :end_date - end - - params.require(:subscription).permit(permitted) + params.require(:subscription).permit([ :start_date, :end_date, :entry_limit ]) end end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 7c0bb86..895e6bb 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,33 +1,37 @@ class UsersController < ApplicationController + include Filterable + before_action :set_user, only: [ :show, :edit, :update, :destroy ] + layout "modal", only: [ :new, :create, :edit, :update ] + def index - @pagy, @users = pagy(User.kept.order(role: :desc, last_name: :asc)) + @total_active_users = User.kept.count + @pagy, @users = pagy( + User + .apply_filters(filter_params) + ) end def show @sales_count = @user.sales.count - @checkins_count = @user.checkins_performed.count end def new @user = User.new(role: :staff) - render layout: "modal" end def create @user = User.new(user_params) if @user.save - redirect_to users_path, notice: t(".created", default: "Utente creato con successo.") + turbo_refresh_or_redirect_to users_path, notice: t(".created", default: "Utente creato con successo.") else - render :new, layout: "modal", status: :unprocessable_entity + render :new, status: :unprocessable_entity end end - def edit - render layout: "modal" - end + def edit; end def update upd_params = user_params @@ -37,22 +41,17 @@ def update end if @user.update(upd_params) - redirect_to users_path, notice: t(".updated", default: "Profilo utente aggiornato.") + turbo_refresh_or_redirect_to users_path, notice: t(".updated", default: "Profilo utente aggiornato.") else - render :edit, layout: "modal", status: :unprocessable_entity + render :edit, status: :unprocessable_entity end end def destroy - if @user == current_user - redirect_to users_path, alert: "Non puoi archiviare il tuo stesso account." - return - end - - if @user.discard! - redirect_to users_path, notice: t(".discarded", default: "Utente archiviato.") + if @user != current_user && @user.discard! + turbo_refresh_or_redirect_to users_path, status: :see_other, notice: t(".discarded", default: "Utente archiviato.") else - redirect_to users_path, alert: t(".error", default: "Impossibile archiviare utente.") + turbo_refresh_or_redirect_to users_path, status: :see_other, alert: t(".error", default: "Impossibile archiviare utente.") end end @@ -63,15 +62,15 @@ def set_user def user_params permitted_params = [ - :first_name, - :last_name, - :username, - :email_address, - :password, - :password_confirmation ] - + :first_name, :last_name, :username, + :email_address, :password, :password_confirmation + ] permitted_params << :role if current_user&.admin? params.require(:user).permit(permitted_params) end + + def filter_params + params.permit(:query, :sort).merge(role: params[:role]) + end end diff --git a/app/helpers/access_logs_helper.rb b/app/helpers/access_logs_helper.rb new file mode 100644 index 0000000..f2e88b8 --- /dev/null +++ b/app/helpers/access_logs_helper.rb @@ -0,0 +1,32 @@ +module AccessLogsHelper + def access_log_status_color(status) + case status.to_sym + when :ok then "success" + when :warning then "warning" + when :error then "error" + else "base-content" + end + end + + def access_log_status_icon(status) + case status.to_sym + when :ok then "success" + when :warning then "warning" + when :error then "error" + else "help" + end + end + + def access_log_status_label(status) + case status.to_sym + when :ok then "Consentito" + when :warning then "Avviso" + when :error then "Negato" + else "Sconosciuto" + end + end + + def access_log_activity_name(log) + log.discipline&.name || "Accesso Generico" + end +end diff --git a/app/helpers/filters_helper.rb b/app/helpers/filters_helper.rb new file mode 100644 index 0000000..07882ff --- /dev/null +++ b/app/helpers/filters_helper.rb @@ -0,0 +1,37 @@ +module FiltersHelper + def humanize_filter_key(key) + case key.to_s + when "product_id" then "Corso" + when "state" then "Stato" + when "query" then "Ricerca" + when "med_cert" then "Certificato Medico" + else key.to_s.humanize + end + end + + def humanize_filter_value(key, value) + case key.to_s + when "product_id" + Product.find_by(id: value)&.name || "Sconosciuto" + when "state" + value.to_s == "kept" ? "Attivi" : "Archiviati" + else + value.to_s + end + end + + def state_filters + [ + [ "Attivi", "kept" ], + [ "Archiviati", "discarded" ] + ] + end + + def filtered_results_counter(pagy) + return unless filtering? + + testo = "Trovati #{pagy.count} risultati" + + content_tag :div, testo, class: "mb-4 text-sm font-medium text-base-content/70" + end +end diff --git a/app/helpers/flash_helper.rb b/app/helpers/flash_helper.rb index 133333b..ea5c950 100644 --- a/app/helpers/flash_helper.rb +++ b/app/helpers/flash_helper.rb @@ -1,21 +1,21 @@ module FlashHelper FLASH_CLASSES = { - notice: "alert-success text-white", + notice: "alert-success text-white", success: "alert-success text-white", - alert: "alert-error text-white", - error: "alert-error text-white", - warning: "alert-warning text-black", - info: "alert-info text-white" - }.freeze + alert: "alert-error text-white", + error: "alert-error text-white", + warning: "alert-warning text-black" + }.with_indifferent_access.freeze def flash_class(type) - FLASH_CLASSES[type.to_sym] || "alert-info text-white" + FLASH_CLASSES[type] || "alert-info text-white" end - def flash_icon(type) + def flash_icon_name(type) case type.to_sym when :alert, :error then "error" when :notice, :success then "success" + when :warning then "warning" else "info" end end diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb index b39c8e0..5d1e227 100644 --- a/app/helpers/icons_helper.rb +++ b/app/helpers/icons_helper.rb @@ -1,43 +1,31 @@ module IconsHelper - def icon(name, size: 20, **options) - # Percorso del file SVG + def icon(name, classes: "size-5", **options) filename = Rails.root.join("app/assets/images/icons/#{name}.svg") - # Fallback se l'icona non esiste: mostra un quadratino con l'iniziale unless File.exist?(filename) return content_tag(:span, name.to_s.first.upcase, - class: "icon-placeholder inline-flex items-center justify-center bg-base-300 rounded text-xs font-bold select-none #{options[:class]}", - style: "width: #{size}px; height: #{size}px;") + class: "inline-flex items-center justify-center bg-base-300 rounded text-[10px] font-bold select-none #{classes}") end - # Cache key: include il nome del file e la data di modifica (mtime) - # Se modifichi il file SVG, la cache si aggiorna da sola. - cache_key = [ "icon_svg_v1", name, File.mtime(filename) ] - - svg_content = Rails.cache.fetch(cache_key) do + svg_content = Rails.cache.fetch([ "icon_svg_fast", name, File.mtime(filename) ]) do File.read(filename) end - # Parsing con Nokogiri per modificare le dimensioni e le classi al volo - doc = Nokogiri::HTML::DocumentFragment.parse(svg_content) - svg = doc.at_css("svg") - - # Imposta dimensioni - svg["width"] = size.to_s - svg["height"] = size.to_s + doc = svg_content.dup - # Aggiungi classi CSS (merge con quelle esistenti nell'SVG se ce ne sono) - if options[:class].present? - existing_class = svg["class"] || "" - svg["class"] = "#{existing_class} #{options[:class]}".strip + if classes.present? + if doc.include?('class="') + doc.sub!('class="', "class=\"#{classes} ") + else + doc.sub!("