diff --git a/Gemfile b/Gemfile index fcb23d2..08dace4 100644 --- a/Gemfile +++ b/Gemfile @@ -5,14 +5,14 @@ ruby '3.4.4' gem 'puma' -gem 'rails', '~> 8.0' gem 'bootsnap', require: false gem 'importmap-rails' -gem 'turbo-rails' +gem 'rails', '~> 8.0' gem 'stimulus-rails' +gem 'turbo-rails' -gem 'solid_queue', '~> 1.1' gem 'mission_control-jobs' +gem 'solid_queue', '~> 1.1' gem 'propshaft' @@ -29,19 +29,19 @@ gem 'haml-rails' gem 'ransack' group :development, :test do + gem 'database_cleaner' gem 'debug', platforms: %i[mri mingw x64_mingw] gem 'rspec-rails' gem 'simplecov', require: false - gem 'database_cleaner' end group :development do - gem 'web-console' gem 'claude-on-rails' gem 'rubocop', require: false + gem 'rubocop-performance', require: false gem 'rubocop-rails', require: false gem 'rubocop-rspec', require: false - gem 'rubocop-performance', require: false + gem 'web-console' end group :production do diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb index 636eb43..228bf72 100644 --- a/app/channels/application_cable/connection.rb +++ b/app/channels/application_cable/connection.rb @@ -9,9 +9,9 @@ def connect private def set_current_user - if session = Session.find_by(id: cookies.signed[:session_id]) - self.current_user = session.user - end + return unless session = Session.find_by(id: cookies.signed[:session_id]) + + self.current_user = session.user end end end diff --git a/app/controllers/categories_controller.rb b/app/controllers/categories_controller.rb index 6ee56c0..679e0eb 100644 --- a/app/controllers/categories_controller.rb +++ b/app/controllers/categories_controller.rb @@ -8,8 +8,7 @@ def index end # GET /categories/1 or /categories/1.json - def show - end + def show; end # GET /categories/new def new @@ -17,8 +16,7 @@ def new end # GET /categories/1/edit - def edit - end + def edit; end # POST /categories or /categories.json def create @@ -88,7 +86,7 @@ def merge @old_category.destroy end redirect_to merge_categories_path, notice: 'Category was successfully merged.' - rescue ActiveRecord::RecordInvalid => e + rescue ActiveRecord::RecordInvalid redirect_to merge_categories_path, alert: 'There was an error. Category could not be merged.' end diff --git a/app/controllers/charts_controller.rb b/app/controllers/charts_controller.rb index 7b82f3a..aabd58a 100644 --- a/app/controllers/charts_controller.rb +++ b/app/controllers/charts_controller.rb @@ -26,8 +26,8 @@ def show private def calculate_monthly_profit_loss(year) - months = (1..12).map { |m| m.to_s } - data = months.map do |month| + months = (1..12).map(&:to_s) + months.map do |month| entries = current_user.entries .by_year(year) .by_month(month) @@ -44,14 +44,12 @@ def calculate_monthly_profit_loss(year) profit_loss: profit_loss } end - - data end def calculate_daily_expenses(year) start_date = Date.parse("#{year}-01-01") end_date = Date.parse("#{year}-12-31") - + # Get all expense entries for the year expense_entries = current_user.entries .by_year(year) @@ -59,7 +57,7 @@ def calculate_daily_expenses(year) .by_untracked(false) .group(:date) .select('date, COUNT(*) as count, SUM(amount) as total') - + # Convert to hash for easy lookup expenses_by_date = expense_entries.each_with_object({}) do |entry, hash| hash[entry.date.to_s] = { @@ -67,17 +65,17 @@ def calculate_daily_expenses(year) total: entry.total.to_f } end - + # Build complete year data with zeros for days without expenses daily_data = {} (start_date..end_date).each do |date| date_key = date.to_s daily_data[date_key] = expenses_by_date[date_key] || { count: 0, total: 0.0 } end - + # Calculate max expense for scaling - max_expense = daily_data.values.map { |d| d[:total] }.max || 0 - + max_expense = daily_data.values.map { |d| d[:total] }.max || 0 # rubocop:disable Rails/Pluck + { daily_expenses: daily_data, max_expense: max_expense, diff --git a/app/controllers/concerns/authentication.rb b/app/controllers/concerns/authentication.rb index 50f8d74..dedc44d 100644 --- a/app/controllers/concerns/authentication.rb +++ b/app/controllers/concerns/authentication.rb @@ -7,8 +7,8 @@ module Authentication end class_methods do - def allow_unauthenticated_access(**options) - skip_before_action :require_authentication, **options + def allow_unauthenticated_access(**) + skip_before_action(:require_authentication, **) end end diff --git a/app/controllers/entries_controller.rb b/app/controllers/entries_controller.rb index 822eada..04c4ad8 100644 --- a/app/controllers/entries_controller.rb +++ b/app/controllers/entries_controller.rb @@ -15,8 +15,7 @@ def index end # GET /entries/1 or /entries/1.json - def show - end + def show; end # GET /entries/new def new @@ -84,7 +83,7 @@ def set_entry # Only allow a list of trusted parameters through. def entry_params params.require(:entry).permit(:date, :amount, :notes, :category_name, :income, :untracked, :user_id, - :category_id, tag_attributes: [:id, :name, :_destroy]) + :category_id, tag_attributes: %i[id name _destroy]) end def search_params diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb index 075fb7a..d06dd01 100644 --- a/app/controllers/passwords_controller.rb +++ b/app/controllers/passwords_controller.rb @@ -2,25 +2,23 @@ class PasswordsController < ApplicationController allow_unauthenticated_access before_action :set_user_by_token, only: %i[edit update] - def new - end + def new; end + + def edit; end def create - if user = User.find_by(email_address: params[:email_address]) - # In a real application without email, you might display this to the user or use SMS - # reset_url = edit_password_url(user.password_reset_token) - # Rails.logger.info "Password reset URL for #{user.email_address}: #{reset_url}" - flash[:notice] = 'Please contact an administrator to reset your password. ' - else - flash[:notice] = 'Please contact an administrator to reset your password.' - end + flash[:notice] = if User.find_by(email_address: params[:email_address]) + # In a real application without email, you might display this to the user or use SMS + # reset_url = edit_password_url(user.password_reset_token) + # Rails.logger.info "Password reset URL for #{user.email_address}: #{reset_url}" + 'Please contact an administrator to reset your password. ' + else + 'Please contact an administrator to reset your password.' + end redirect_to new_session_path end - def edit - end - def update if @user.update(params.permit(:password, :password_confirmation)) redirect_to new_session_path, notice: 'Password has been reset.' diff --git a/app/controllers/pwa_controller.rb b/app/controllers/pwa_controller.rb index ccd2312..239d29d 100644 --- a/app/controllers/pwa_controller.rb +++ b/app/controllers/pwa_controller.rb @@ -1,14 +1,14 @@ class PwaController < ApplicationController allow_unauthenticated_access - + def manifest - render json: render_to_string(template: "pwa/manifest", formats: :json), - content_type: "application/manifest+json" + render json: render_to_string(template: 'pwa/manifest', formats: :json), + content_type: 'application/manifest+json' end - + def service_worker - render file: Rails.public_path.join("service-worker.js"), - content_type: "application/javascript", + render file: Rails.public_path.join('service-worker.js'), + content_type: 'application/javascript', layout: false end -end \ No newline at end of file +end diff --git a/app/controllers/recurrables_controller.rb b/app/controllers/recurrables_controller.rb index ed38a5f..8c39d07 100644 --- a/app/controllers/recurrables_controller.rb +++ b/app/controllers/recurrables_controller.rb @@ -7,8 +7,7 @@ def index end # GET /recurrables/1 - def show - end + def show; end # GET /recurrables/new def new @@ -63,6 +62,6 @@ def set_recurrable # Only allow a list of trusted parameters through. def recurrable_params params.require(:recurrable).permit(:name, :day_of_month, :amount, :notes, :category_id, - tag_attributes: [:id, :name, :_destroy]) + tag_attributes: %i[id name _destroy]) end end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 6b2107e..bb4c2f8 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -1,11 +1,10 @@ class SessionsController < ApplicationController allow_unauthenticated_access only: %i[new create] - rate_limit to: 10, within: 3.minutes, only: :create, with: -> { + rate_limit to: 10, within: 3.minutes, only: :create, with: lambda { redirect_to new_session_url, alert: 'Try again later.' } - def new - end + def new; end def create if user = User.authenticate_by(params.permit(:email_address, :password)) diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index 6bd21c3..8145615 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -7,8 +7,7 @@ def index end # GET /tags/1 - def show - end + def show; end # GET /tags/new def new @@ -16,8 +15,7 @@ def new end # GET /tags/1/edit - def edit - end + def edit; end # POST /tags def create diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 67b7e1f..b1238e3 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,12 +1,14 @@ class UsersController < ApplicationController - allow_unauthenticated_access only: [:new, :create] - before_action :set_user, only: [:edit, :update] + allow_unauthenticated_access only: %i[new create] + before_action :set_user, only: %i[edit update] def new redirect_to root_path if authenticated? @user = User.new end + def edit; end + def create @user = User.new(user_params) @@ -18,9 +20,6 @@ def create end end - def edit - end - def update # Allow updating without providing current password if @user.update(user_update_params) @@ -43,9 +42,7 @@ def user_params def user_update_params # Only permit password fields if they are present permitted = [:email_address] - if params[:user][:password].present? - permitted += [:password, :password_confirmation] - end + permitted += %i[password password_confirmation] if params[:user][:password].present? params.require(:user).permit(permitted) end end diff --git a/app/models/category.rb b/app/models/category.rb index a2f38a1..e9db491 100644 --- a/app/models/category.rb +++ b/app/models/category.rb @@ -1,7 +1,7 @@ class Category < ApplicationRecord has_and_belongs_to_many :users - def self.ransackable_attributes(auth_object = nil) + def self.ransackable_attributes(_auth_object = nil) ['name'] end end diff --git a/app/models/concerns/taggable.rb b/app/models/concerns/taggable.rb index 0eb34cf..c51e510 100644 --- a/app/models/concerns/taggable.rb +++ b/app/models/concerns/taggable.rb @@ -13,12 +13,12 @@ def autosave_associated_records_for_tag self.tag = tag elsif tag.marked_for_destruction? self.tag = nil - self.save + save elsif tag.new_record? && Tag.find_by(name: tag.name).present? self.tag = Tag.find_by(name: tag.name) elsif tag.changed? && tag.name.blank? self.tag = nil - self.save + save elsif tag.changed? self.tag = Tag.find_or_create_by(name: tag.name) end diff --git a/app/models/entry.rb b/app/models/entry.rb index ef4dc4e..954ac4a 100644 --- a/app/models/entry.rb +++ b/app/models/entry.rb @@ -5,21 +5,21 @@ class Entry < ApplicationRecord belongs_to :category scope :by_year, ->(year) { where("strftime('%Y', date) = ?", year.to_s) if year.present? } - scope :by_month, ->(month) { + scope :by_month, lambda { |month| where("ltrim(strftime('%m', date), '0') = ?", month.to_s) unless month.blank? || month.to_i.zero? } scope :by_income, ->(income) { joins(:category).where(categories: { income: income }) if !!income == income } - scope :by_untracked, ->(untracked) { + scope :by_untracked, lambda { |untracked| joins(:category).where(categories: { untracked: untracked }) if !!untracked == untracked } scope :by_category_id, ->(category_id) { where(category_id: category_id) if category_id.present? } scope :by_tag_id, ->(tag_id) { where(tag_id: tag_id) if tag_id.present? } - def self.ransackable_attributes(auth_object = nil) - ['amount', 'date', 'notes'] + def self.ransackable_attributes(_auth_object = nil) + %w[amount date notes] end - def self.ransackable_associations(auth_object = nil) + def self.ransackable_associations(_auth_object = nil) ['category'] end diff --git a/app/models/user.rb b/app/models/user.rb index 64429c2..4a1d71d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -24,9 +24,9 @@ def tags end def remove_data - self.tags.destroy_all - self.entries.destroy_all - self.categories.destroy_all + tags.destroy_all + entries.destroy_all + categories.destroy_all end def password_reset_token diff --git a/app/services/demo_data_generator.rb b/app/services/demo_data_generator.rb index 16c6887..9b72f26 100644 --- a/app/services/demo_data_generator.rb +++ b/app/services/demo_data_generator.rb @@ -4,14 +4,14 @@ module DemoDataGenerator def random_amount(base, variance = 0.2) min = base * (1 - variance) max = base * (1 + variance) - (rand * (max - min) + min).round(2) + ((rand * (max - min)) + min).round(2) end def maybe_add_tag(tags, probability = 0.15) rand < probability ? tags.values.sample : nil end - def maybe_add_notes(category_name, amount = nil) + def maybe_add_notes(category_name, _amount = nil) category_notes = { 'Salary' => ['Monthly salary deposit', 'Regular paycheck', 'Direct deposit', 'Salary payment'], 'Freelance' => ['Project payment', 'Client invoice', 'Consulting work', 'Contract payment', 'Freelance gig'], @@ -29,12 +29,10 @@ def maybe_add_notes(category_name, amount = nil) } # 60% chance of having notes - if rand < 0.6 - notes_array = category_notes[category_name] || ['Payment', 'Purchase', 'Transaction'] - notes_array.sample - else - nil - end + return unless rand < 0.6 + + notes_array = category_notes[category_name] || %w[Payment Purchase Transaction] + notes_array.sample end def generate_daily_expenses(user, categories, tags, date) diff --git a/app/views/charts/show.html.haml b/app/views/charts/show.html.haml index c7cd9c3..ff8012a 100644 --- a/app/views/charts/show.html.haml +++ b/app/views/charts/show.html.haml @@ -51,4 +51,4 @@ 'heatmap-data-value': @heatmap_data.to_json } } - %svg#expense-heatmap{ width: '100%', height: '180', viewBox: '0 0 954 180' } \ No newline at end of file + %svg#expense-heatmap{ width: '100%', height: '180', viewBox: '0 0 954 180' } diff --git a/config/routes.rb b/config/routes.rb index 9bef0e4..413a604 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,7 +2,7 @@ mount MissionControl::Jobs::Engine, at: '/jobs' get 'up' => 'rails/health#show', as: :rails_health_check - + # PWA routes get '/manifest.json', to: 'pwa#manifest' get '/service-worker.js', to: 'pwa#service_worker' @@ -33,10 +33,10 @@ get '/signup', to: 'users#new', as: :signup post '/signup', to: 'users#create' - resources :users, only: [:edit, :update] + resources :users, only: %i[edit update] # Root routes - constraints lambda { |req| Session.find_by(id: req.cookie_jar.signed[:session_id]).present? } do + constraints ->(req) { Session.find_by(id: req.cookie_jar.signed[:session_id]).present? } do root 'entries#new', as: :authenticated_root end diff --git a/db/seeds.rb b/db/seeds.rb index aefd028..8cabf57 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -173,17 +173,17 @@ # Holiday shopping in November/December if [11, 12].include?(month_start.month) (month_start..month_end).each do |date| - if rand < 0.15 # 15% chance each day - Entry.create!( - user: demo_user, - category: categories[:shopping], - date: date, - amount: random_amount(120, 0.6), - notes: 'Holiday shopping', - tag: tags[:gift] - ) - entry_count += 1 - end + next unless rand < 0.15 # 15% chance each day + + Entry.create!( + user: demo_user, + category: categories[:shopping], + date: date, + amount: random_amount(120, 0.6), + notes: 'Holiday shopping', + tag: tags[:gift] + ) + entry_count += 1 end end @@ -194,29 +194,29 @@ vacation_days.times do |day| vacation_date = vacation_start + day.days - if vacation_date <= month_end && vacation_date >= month_start - # Hotel - Entry.create!( - user: demo_user, - category: categories[:entertainment], - date: vacation_date, - amount: random_amount(150, 0.3), - notes: 'Hotel', - tag: tags[:vacation] - ) - entry_count += 1 - - # Dining out more - Entry.create!( - user: demo_user, - category: categories[:restaurants], - date: vacation_date, - amount: random_amount(80, 0.4), - notes: 'Vacation dining', - tag: tags[:vacation] - ) - entry_count += 1 - end + next unless vacation_date <= month_end && vacation_date >= month_start + + # Hotel + Entry.create!( + user: demo_user, + category: categories[:entertainment], + date: vacation_date, + amount: random_amount(150, 0.3), + notes: 'Hotel', + tag: tags[:vacation] + ) + entry_count += 1 + + # Dining out more + Entry.create!( + user: demo_user, + category: categories[:restaurants], + date: vacation_date, + amount: random_amount(80, 0.4), + notes: 'Vacation dining', + tag: tags[:vacation] + ) + entry_count += 1 end end