diff --git a/app/controllers/transactions/categories/deletions_controller.rb b/app/controllers/transactions/categories/deletions_controller.rb new file mode 100644 index 00000000..ee868abe --- /dev/null +++ b/app/controllers/transactions/categories/deletions_controller.rb @@ -0,0 +1,24 @@ +class Transactions::Categories::DeletionsController < ApplicationController + before_action :set_category + before_action :set_replacement_category, only: :create + + def new + end + + def create + @category.replace_and_destroy! @replacement_category + + redirect_back_or_to transactions_path, notice: t(".success") + end + + private + def set_category + @category = Current.family.transaction_categories.find(params[:transaction_category_id]) + end + + def set_replacement_category + if params[:replacement_category_id].present? + @replacement_category = Current.family.transaction_categories.find(params[:replacement_category_id]) + end + end +end diff --git a/app/controllers/transactions/categories_controller.rb b/app/controllers/transactions/categories_controller.rb index 1ce3a8f1..b01e4439 100644 --- a/app/controllers/transactions/categories_controller.rb +++ b/app/controllers/transactions/categories_controller.rb @@ -1,37 +1,45 @@ class Transactions::CategoriesController < ApplicationController - before_action :set_category, only: [ :update, :destroy ] + before_action :set_category, only: %i[ edit update ] + before_action :set_transaction, only: :create def index + @categories = Current.family.transaction_categories.alphabetically + end + + def new + @category = Current.family.transaction_categories.new color: Transaction::Category::COLORS.sample end def create - if Current.family.transaction_categories.create(category_params) - redirect_to transactions_path, notice: t(".success") - else - render transactions_path, status: :unprocessable_entity, notice: t(".error") + Transaction::Category.transaction do + category = Current.family.transaction_categories.create!(category_params) + @transaction.update!(category_id: category.id) if @transaction end + + redirect_back_or_to transactions_path, notice: t(".success") + end + + def edit end def update - if @category.update(category_params) - redirect_to transactions_path, notice: t(".success") - else - render transactions_path, status: :unprocessable_entity, notice: t(".error") - end - end + @category.update! category_params - def destroy - @category.destroy! - redirect_to transactions_path, notice: t(".success") + redirect_back_or_to transactions_path, notice: t(".success") end private + def set_category + @category = Current.family.transaction_categories.find(params[:id]) + end - def set_category - @category = Current.family.transaction_categories.find(params[:id]) - end + def set_transaction + if params[:transaction_id].present? + @transaction = Current.family.transactions.find(params[:transaction_id]) + end + end - def category_params - params.require(:transaction_category).permit(:name, :color) - end + def category_params + params.require(:transaction_category).permit(:name, :color) + end end diff --git a/app/controllers/transactions/merchants_controller.rb b/app/controllers/transactions/merchants_controller.rb index 600d07b3..2d0dd2ce 100644 --- a/app/controllers/transactions/merchants_controller.rb +++ b/app/controllers/transactions/merchants_controller.rb @@ -11,9 +11,9 @@ class Transactions::MerchantsController < ApplicationController def create if Current.family.transaction_merchants.create(merchant_params) - redirect_to transactions_merchants_path, notice: t(".success") + redirect_to transaction_merchants_path, notice: t(".success") else - render transactions_merchants_path, status: :unprocessable_entity, notice: t(".error") + render transaction_merchants_path, status: :unprocessable_entity, notice: t(".error") end end @@ -22,15 +22,15 @@ class Transactions::MerchantsController < ApplicationController def update if @merchant.update(merchant_params) - redirect_to transactions_merchants_path, notice: t(".success") + redirect_to transaction_merchants_path, notice: t(".success") else - render transactions_merchants_path, status: :unprocessable_entity, notice: t(".error") + render transaction_merchants_path, status: :unprocessable_entity, notice: t(".error") end end def destroy @merchant.destroy! - redirect_to transactions_merchants_path, notice: t(".success") + redirect_to transaction_merchants_path, notice: t(".success") end private diff --git a/app/controllers/transactions_controller.rb b/app/controllers/transactions_controller.rb index 9bb985b3..36dbe312 100644 --- a/app/controllers/transactions_controller.rb +++ b/app/controllers/transactions_controller.rb @@ -85,7 +85,12 @@ class TransactionsController < ApplicationController def update respond_to do |format| - sync_start_date = [ @transaction.date, Date.parse(transaction_params[:date]) ].compact.min + sync_start_date = if transaction_params[:date] + [ @transaction.date, Date.parse(transaction_params[:date]) ].compact.min + else + @transaction.date + end + if @transaction.update(transaction_params) @transaction.account.sync_later(sync_start_date) diff --git a/app/helpers/menus_helper.rb b/app/helpers/menus_helper.rb new file mode 100644 index 00000000..9576d54f --- /dev/null +++ b/app/helpers/menus_helper.rb @@ -0,0 +1,21 @@ +module MenusHelper + def contextual_menu(&block) + tag.div class: "relative cursor-pointer", data: { controller: "menu" } do + concat contextual_menu_icon + concat contextual_menu_content(&block) + end + end + + private + def contextual_menu_icon + tag.button class: "flex hover:bg-gray-100 p-2 rounded", data: { menu_target: "button" } do + lucide_icon "more-horizontal", class: "w-5 h-5 text-gray-500" + end + end + + def contextual_menu_content(&block) + tag.div class: "absolute z-10 top-10 right-0 border border-alpha-black-25 bg-white rounded-lg shadow-xs hidden", data: { menu_target: "content" } do + capture(&block) + end + end +end diff --git a/app/helpers/transactions/categories_helper.rb b/app/helpers/transactions/categories_helper.rb index 5ce10ffb..19d77a23 100644 --- a/app/helpers/transactions/categories_helper.rb +++ b/app/helpers/transactions/categories_helper.rb @@ -1,2 +1,7 @@ module Transactions::CategoriesHelper + def null_category + Transaction::Category.new \ + name: "Uncategorized", + color: Transaction::Category::UNCATEGORIZED_COLOR + end end diff --git a/app/javascript/controllers/category_deletion_controller.js b/app/javascript/controllers/category_deletion_controller.js new file mode 100644 index 00000000..a07b4619 --- /dev/null +++ b/app/javascript/controllers/category_deletion_controller.js @@ -0,0 +1,30 @@ +import { Controller } from "@hotwired/stimulus"; + +export default class extends Controller { + static targets = [ "replacementCategoryField", "submitButton" ] + static classes = [ "dangerousAction", "safeAction" ] + static values = { + submitTextWhenReplacing: String, + submitTextWhenNotReplacing: String + } + + updateSubmitButton() { + if (this.replacementCategoryFieldTarget.value) { + this.submitButtonTarget.value = this.submitTextWhenReplacingValue + this.#markSafe() + } else { + this.submitButtonTarget.value = this.submitTextWhenNotReplacingValue + this.#markDangerous() + } + } + + #markSafe() { + this.submitButtonTarget.classList.remove(...this.dangerousActionClasses) + this.submitButtonTarget.classList.add(...this.safeActionClasses) + } + + #markDangerous() { + this.submitButtonTarget.classList.remove(...this.safeActionClasses) + this.submitButtonTarget.classList.add(...this.dangerousActionClasses) + } +} diff --git a/app/javascript/controllers/color_select_controller.js b/app/javascript/controllers/color_select_controller.js new file mode 100644 index 00000000..e18bb84f --- /dev/null +++ b/app/javascript/controllers/color_select_controller.js @@ -0,0 +1,59 @@ +import { Controller } from "@hotwired/stimulus"; + +export default class extends Controller { + static targets = [ "input", "decoration" ] + static values = { selection: String } + + connect() { + this.#renderOptions() + } + + select({ target }) { + this.selectionValue = target.dataset.value + } + + selectionValueChanged() { + this.#options.forEach(option => { + if (option.dataset.value === this.selectionValue) { + this.#check(option) + this.inputTarget.value = this.selectionValue + } else { + this.#uncheck(option) + } + }) + } + + #renderOptions() { + this.#options.forEach(option => option.style.backgroundColor = option.dataset.value) + } + + #check(option) { + option.setAttribute("aria-checked", "true") + option.style.boxShadow = `0px 0px 0px 4px ${hexToRGBA(option.dataset.value, 0.2)}` + this.decorationTarget.style.backgroundColor = option.dataset.value + } + + #uncheck(option) { + option.setAttribute("aria-checked", "false") + option.style.boxShadow = "none" + } + + get #options() { + return Array.from(this.element.querySelectorAll("[role='radio']")) + } +} + +function hexToRGBA(hex, alpha = 1) { + hex = hex.replace(/^#/, ''); + + if (hex.length === 8) { + alpha = parseInt(hex.slice(6, 8), 16) / 255; + hex = hex.slice(0, 6); + } + + let r = parseInt(hex.slice(0, 2), 16); + let g = parseInt(hex.slice(2, 4), 16); + let b = parseInt(hex.slice(4, 6), 16); + + return `rgba(${r}, ${g}, ${b}, ${alpha})`; +} diff --git a/app/models/transaction/category.rb b/app/models/transaction/category.rb index 97439f68..d884a3bd 100644 --- a/app/models/transaction/category.rb +++ b/app/models/transaction/category.rb @@ -46,6 +46,13 @@ class Transaction::Category < ApplicationRecord self.insert_all(categories) end + def replace_and_destroy!(replacement) + transaction do + transactions.update_all category_id: replacement&.id + destroy! + end + end + private def clear_internal_category diff --git a/app/views/accounts/index.html.erb b/app/views/accounts/index.html.erb index c1b42f35..f844067d 100644 --- a/app/views/accounts/index.html.erb +++ b/app/views/accounts/index.html.erb @@ -63,6 +63,6 @@ <% else %> <%= previous_setting("Billing", settings_billing_path) %> <% end %> - <%= next_setting("Categories", transactions_categories_path) %> + <%= next_setting("Categories", transaction_categories_path) %> diff --git a/app/views/accounts/new.html.erb b/app/views/accounts/new.html.erb index c6cd4f9c..e7e000f5 100644 --- a/app/views/accounts/new.html.erb +++ b/app/views/accounts/new.html.erb @@ -1,6 +1,6 @@