From 4c5f8263bcb06bede9b7fdfa00f091fea65a8593 Mon Sep 17 00:00:00 2001 From: Jose Farias <31393016+josefarias@users.noreply.github.com> Date: Thu, 2 May 2024 07:24:31 -0600 Subject: [PATCH] Implement transaction category management (#688) * Singularize "transaction" in transaction-nested paths * Refactor category badge partial * Let modal content define its width * Add contectual menu to transactions index * Add null_category helper * Implement category edits * Fix inline transaction category badges * Fix typos in system test paths * Add missing translations * Add decoration to color select controller * Wire up transaction category creation * Fix indent in color-select-controller * Add button for clearing category from transaction * Implement category deletions * Fix existing modal sizes * Use null_category in a single place * Remove anemic method in category deletion controller * reassign_and_destroy -> reassign_transactions_then_destroy * Fix i18n * Remove destroy action from CategoriesController callbacks * transactions_merchant -> transaction_merchant * reassign_transactions_then_destroy -> replace_and_destroy * Add transaction category CRUD tests * Add presence check for transaction_id * Check replacement_category_id presence * Test Transaction::Category#replace_and_destroy! --- .../categories/deletions_controller.rb | 24 +++++++ .../transactions/categories_controller.rb | 48 +++++++------ .../transactions/merchants_controller.rb | 10 +-- app/controllers/transactions_controller.rb | 7 +- app/helpers/menus_helper.rb | 21 ++++++ app/helpers/transactions/categories_helper.rb | 5 ++ .../category_deletion_controller.js | 30 ++++++++ .../controllers/color_select_controller.js | 59 +++++++++++++++ app/models/transaction/category.rb | 7 ++ app/views/accounts/index.html.erb | 2 +- app/views/accounts/new.html.erb | 2 +- app/views/pages/changelog.html.erb | 2 +- app/views/settings/_nav.html.erb | 6 +- app/views/shared/_modal.html.erb | 2 +- app/views/transactions/_transaction.html.erb | 2 +- .../transactions/categories/_badge.html.erb | 14 ++-- .../transactions/categories/_form.html.erb | 38 ++++++++++ .../transactions/categories/_menu.html.erb | 31 +++++--- .../transactions/categories/_row.html.erb | 23 ++++++ .../categories/deletions/new.html.erb | 33 +++++++++ .../categories/dropdown/_edit.html.erb | 15 ---- .../categories/dropdown/_form.html.erb | 28 -------- .../categories/dropdown/_row.html.erb | 29 +++++--- .../transactions/categories/edit.html.erb | 10 +++ .../transactions/categories/index.html.erb | 29 +++++--- .../transactions/categories/new.html.erb | 10 +++ app/views/transactions/index.html.erb | 10 +++ .../transactions/merchants/_form.html.erb | 2 +- .../transactions/merchants/_list.html.erb | 4 +- .../transactions/merchants/index.html.erb | 8 +-- app/views/transactions/new.html.erb | 2 +- app/views/transactions/rules/index.html.erb | 2 +- .../search_form/_category_filter.html.erb | 2 +- config/locales/views/transaction/en.yml | 38 ++++++++-- config/routes.rb | 20 +++--- .../categories/deletions_controller_test.rb | 40 +++++++++++ .../categories_controller_test.rb | 72 ++++++++++++++++++- .../transactions/merchants_controller_test.rb | 16 ++--- test/models/transaction/category_test.rb | 16 +++++ test/system/settings_test.rb | 6 +- 40 files changed, 580 insertions(+), 145 deletions(-) create mode 100644 app/controllers/transactions/categories/deletions_controller.rb create mode 100644 app/helpers/menus_helper.rb create mode 100644 app/javascript/controllers/category_deletion_controller.js create mode 100644 app/javascript/controllers/color_select_controller.js create mode 100644 app/views/transactions/categories/_form.html.erb create mode 100644 app/views/transactions/categories/_row.html.erb create mode 100644 app/views/transactions/categories/deletions/new.html.erb delete mode 100644 app/views/transactions/categories/dropdown/_edit.html.erb delete mode 100644 app/views/transactions/categories/dropdown/_form.html.erb create mode 100644 app/views/transactions/categories/edit.html.erb create mode 100644 app/views/transactions/categories/new.html.erb create mode 100644 test/controllers/transactions/categories/deletions_controller_test.rb 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 @@

<%= t(".title") %>

<%= modal do %> -
+
<% if @account.accountable.blank? %>
<%= t ".select_accountable_type" %> diff --git a/app/views/pages/changelog.html.erb b/app/views/pages/changelog.html.erb index ba35a34e..700b358e 100644 --- a/app/views/pages/changelog.html.erb +++ b/app/views/pages/changelog.html.erb @@ -9,7 +9,7 @@
- <%= previous_setting("Rules", transactions_rules_path) %> + <%= previous_setting("Rules", transaction_rules_path) %> <%= next_setting("Feedback", feedback_path) %>
diff --git a/app/views/settings/_nav.html.erb b/app/views/settings/_nav.html.erb index c78f9239..838684f7 100644 --- a/app/views/settings/_nav.html.erb +++ b/app/views/settings/_nav.html.erb @@ -47,13 +47,13 @@ diff --git a/app/views/shared/_modal.html.erb b/app/views/shared/_modal.html.erb index a1a7b7d7..7d6ae1d1 100644 --- a/app/views/shared/_modal.html.erb +++ b/app/views/shared/_modal.html.erb @@ -1,6 +1,6 @@ <%# locals: (content:, classes:) -%> <%= turbo_frame_tag "modal" do %> - +
<%= content %>
diff --git a/app/views/transactions/_transaction.html.erb b/app/views/transactions/_transaction.html.erb index 811b9f17..ad2c4b6b 100644 --- a/app/views/transactions/_transaction.html.erb +++ b/app/views/transactions/_transaction.html.erb @@ -13,7 +13,7 @@ <% else %> <%= render partial: "transactions/transaction_name", locals: { name: transaction.name } %>
- <%= render partial: "transactions/categories/badge", locals: transaction.category.nil? ? {} : { name: transaction.category.name, color: transaction.category.color } %> + <%= render partial: "transactions/categories/badge", locals: { category: transaction.category } %>
<% end %>
diff --git a/app/views/transactions/categories/_badge.html.erb b/app/views/transactions/categories/_badge.html.erb index 4d4825f1..bc4a2918 100644 --- a/app/views/transactions/categories/_badge.html.erb +++ b/app/views/transactions/categories/_badge.html.erb @@ -1,4 +1,10 @@ -<%# locals: (name: "Uncategorized", color: Transaction::Category::UNCATEGORIZED_COLOR) %> -<% background_color = "color-mix(in srgb, #{color} 5%, white)" %> -<% border_color = "color-mix(in srgb, #{color} 10%, white)" %> -<%= name %> +<%# locals: (category:) %> +<% category ||= null_category %> + + + <%= category.name %> + diff --git a/app/views/transactions/categories/_form.html.erb b/app/views/transactions/categories/_form.html.erb new file mode 100644 index 00000000..0e12b4e7 --- /dev/null +++ b/app/views/transactions/categories/_form.html.erb @@ -0,0 +1,38 @@ +<%= form_with model: category, data: { turbo: false } do |form| %> +
+
+ + <%= form.text_field :name, + value: category.name, + autofocus: "", + required: true, + placeholder: "Enter Category name", + class: "rounded-lg w-full focus:ring-black focus:border-transparent placeholder:text-gray-500 pl-6" %> +
+ +
+ <%= form.hidden_field :color, data: { color_select_target: "input" } %> + +
    + <% Transaction::Category::COLORS.each do |color| %> + + <% end %> +
+
+ +
+ <%= hidden_field_tag :transaction_id, params[:transaction_id] %> + + <% if category.persisted? %> + <%= form.submit t(".update") %> + <% else %> + <%= form.submit t(".create") %> + <% end %> +
+
+<% end %> diff --git a/app/views/transactions/categories/_menu.html.erb b/app/views/transactions/categories/_menu.html.erb index c78a10ab..c7ec508f 100644 --- a/app/views/transactions/categories/_menu.html.erb +++ b/app/views/transactions/categories/_menu.html.erb @@ -1,7 +1,7 @@ <%# locals: (transaction:) %>
diff --git a/app/views/transactions/categories/_row.html.erb b/app/views/transactions/categories/_row.html.erb new file mode 100644 index 00000000..b724f746 --- /dev/null +++ b/app/views/transactions/categories/_row.html.erb @@ -0,0 +1,23 @@ +
+ <%= render partial: "transactions/categories/badge", locals: { category: row } %> + + <%= contextual_menu do %> +
+ <%= link_to edit_transaction_category_path(row), + class: "block w-full py-2 px-3 space-x-2 text-gray-900 hover:bg-gray-50 flex items-center rounded-lg", + data: { turbo_frame: :modal } do %> + <%= lucide_icon "pencil-line", class: "w-5 h-5 text-gray-500" %> + + <%= t(".edit") %> + <% end %> + + <%= link_to new_transaction_category_deletion_path(row), + class: "block w-full py-2 px-3 space-x-2 text-red-600 hover:bg-red-50 flex items-center rounded-lg", + data: { turbo_frame: :modal } do %> + <%= lucide_icon "trash-2", class: "w-5 h-5" %> + + <%= t(".delete") %> + <% end %> +
+ <% end %> +
diff --git a/app/views/transactions/categories/deletions/new.html.erb b/app/views/transactions/categories/deletions/new.html.erb new file mode 100644 index 00000000..ef18a4bf --- /dev/null +++ b/app/views/transactions/categories/deletions/new.html.erb @@ -0,0 +1,33 @@ +<%= modal do %> +
+
+
+

<%= t(".delete_category") %>

+ <%= lucide_icon "x", class: "w-5 h-5 text-gray-500", data: { action: "click->modal#close" } %> +
+ +

+ <%= t(".explanation", category_name: @category.name) %> +

+
+ + <%= form_with url: transaction_category_deletions_path(@category), + data: { + turbo: false, + controller: "category-deletion", + category_deletion_dangerous_action_class: "form-field__submit bg-white text-red-600 border hover:bg-red-50", + category_deletion_safe_action_class: "form-field__submit border border-transparent", + category_deletion_submit_text_when_not_replacing_value: t(".delete_and_leave_uncategorized", category_name: @category.name), + category_deletion_submit_text_when_replacing_value: t(".delete_and_recategorize", category_name: @category.name) } do |f| %> + <%= f.collection_select :replacement_category_id, + Current.family.transaction_categories.alphabetically.without(@category), + :id, :name, + { prompt: t(".replacement_category_prompt"), label: t(".category") }, + { data: { category_deletion_target: "replacementCategoryField", action: "category-deletion#updateSubmitButton" } } %> + + <%= f.submit t(".delete_and_leave_uncategorized", category_name: @category.name), + class: "form-field__submit bg-white text-red-600 border hover:bg-red-50", + data: { category_deletion_target: "submitButton" } %> + <% end %> +
+<% end %> diff --git a/app/views/transactions/categories/dropdown/_edit.html.erb b/app/views/transactions/categories/dropdown/_edit.html.erb deleted file mode 100644 index 7a7806ad..00000000 --- a/app/views/transactions/categories/dropdown/_edit.html.erb +++ /dev/null @@ -1,15 +0,0 @@ -<%# locals: (category:) %> -
-
- <%= render partial: "transactions/categories/dropdown/form", locals: { category: } %> -
-
- <%= button_to transactions_category_path(category), - method: :delete, - class: "flex text-sm font-medium items-center gap-2 text-red-600 w-full rounded-lg p-2 hover:bg-gray-100", - data: { turbo: false } do %> - <%= lucide_icon("trash-2", class: "w-5 h-5") %> Delete category - <% end %> -
-
-
diff --git a/app/views/transactions/categories/dropdown/_form.html.erb b/app/views/transactions/categories/dropdown/_form.html.erb deleted file mode 100644 index 87111231..00000000 --- a/app/views/transactions/categories/dropdown/_form.html.erb +++ /dev/null @@ -1,28 +0,0 @@ -<%# locals: (category: nil) %> -<%= form_with url: category ? transactions_category_path(category) : transactions_categories_path, method: category ? :patch : :post, scope: :transaction_category, html: { class: "text-sm font-semibold leading-6 text-gray-900" }, data: { turbo: false } do |form| %> -
-
-
- <%= form.text_field :name, value: category&.name, placeholder: "Enter Category name", class: "text-sm font-normal placeholder:text-gray-500 h-10 relative pl-6 w-full border-none rounded-lg" %> -
-
-
- <%= form.hidden_field :color, data: { select_target: "input" } %> -
    - <% Transaction::Category::COLORS.each do |color| %> -
  • -
    -
  • - <% end %> -
-
-
-
-
-
- <%= form.button "Create category", class: "flex text-sm font-medium items-center gap-2 text-gray-900 w-full rounded-lg p-2 hover:bg-gray-100" do %> - <%= lucide_icon("plus", class: "w-5 h-5") %> <%= category.nil? ? "Create" : "Update" %> category - <% end %> -
-
-<% end %> diff --git a/app/views/transactions/categories/dropdown/_row.html.erb b/app/views/transactions/categories/dropdown/_row.html.erb index ff39c60c..136a4843 100644 --- a/app/views/transactions/categories/dropdown/_row.html.erb +++ b/app/views/transactions/categories/dropdown/_row.html.erb @@ -1,18 +1,31 @@ <%# locals: (category:, transaction:) %> <% is_selected = transaction.category_id == category.id %> + <%= content_tag :div, class: ["filterable-item flex justify-between items-center border-none rounded-lg px-2 py-1 group w-full", { "bg-gray-25": is_selected }], data: { filter_name: category.name } do %> <%= button_to transaction_path(transaction, transaction: { category_id: category.id }), method: :patch, class: "flex w-full items-center gap-1.5 cursor-pointer" do %> <%= lucide_icon("check", class: "w-5 h-5 text-gray-500") if is_selected %> - <%= render partial: "transactions/categories/badge", locals: { name: category.name, color: category.color } %> + <%= render partial: "transactions/categories/badge", locals: { category: category } %> <% end %> -
- - + <% end %> <% end %> diff --git a/app/views/transactions/categories/edit.html.erb b/app/views/transactions/categories/edit.html.erb new file mode 100644 index 00000000..d835e76f --- /dev/null +++ b/app/views/transactions/categories/edit.html.erb @@ -0,0 +1,10 @@ +<%= modal do %> +
+
+

<%= t(".edit") %>

+ <%= lucide_icon "x", class: "w-5 h-5 text-gray-500", data: { action: "click->modal#close" } %> +
+ + <%= render "form", category: @category %> +
+<% end %> diff --git a/app/views/transactions/categories/index.html.erb b/app/views/transactions/categories/index.html.erb index 3a564eae..972c201b 100644 --- a/app/views/transactions/categories/index.html.erb +++ b/app/views/transactions/categories/index.html.erb @@ -1,15 +1,28 @@ <% content_for :sidebar do %> <%= render "settings/nav" %> <% end %> -
-

Categories

+
+
+

<%= t(".categories") %>

+ + <%= link_to new_transaction_category_path, class: "rounded-lg bg-gray-900 text-white flex items-center gap-1 justify-center hover:bg-gray-700 px-3 py-2", data: { turbo_frame: :modal } do %> + <%= lucide_icon "plus", class: "w-5 h-5" %> +

<%= t(".new") %>

+ <% end %> +
+
-
-

Transaction categories coming soon...

+
+

<%= t(".categories") %> ยท <%= @categories.size %>

+ +
+ <%= render collection: @categories, partial: "transactions/categories/row" %> +
-
+ +
<%= previous_setting("Accounts", accounts_path) %> - <%= next_setting("Merchants", transactions_merchants_path) %> -
-
+ <%= next_setting("Merchants", transaction_merchants_path) %> + +
diff --git a/app/views/transactions/categories/new.html.erb b/app/views/transactions/categories/new.html.erb new file mode 100644 index 00000000..aad6f15b --- /dev/null +++ b/app/views/transactions/categories/new.html.erb @@ -0,0 +1,10 @@ +<%= modal do %> +
+
+

<%= t(".new_category") %>

+ <%= lucide_icon "x", class: "w-5 h-5 text-gray-500", data: { action: "click->modal#close" } %> +
+ + <%= render "form", category: @category %> +
+<% end %> diff --git a/app/views/transactions/index.html.erb b/app/views/transactions/index.html.erb index 829b6575..5184296e 100644 --- a/app/views/transactions/index.html.erb +++ b/app/views/transactions/index.html.erb @@ -3,6 +3,16 @@

Transactions

+ <%= contextual_menu do %> +
+ <%= link_to transaction_categories_path, + class: "block w-full py-2 px-3 space-x-2 text-gray-900 hover:bg-gray-50 flex items-center rounded-lg font-normal" do %> + <%= lucide_icon "tags", class: "w-5 h-5 text-gray-500" %> + <%= t(".edit_categories") %> + <% end %> +
+ <% end %> + <%= link_to new_transaction_path, class: "rounded-lg bg-gray-900 text-white flex items-center gap-1 justify-center hover:bg-gray-700 px-3 py-2", data: { turbo_frame: :modal } do %> <%= lucide_icon("plus", class: "w-5 h-5") %>

New transaction

diff --git a/app/views/transactions/merchants/_form.html.erb b/app/views/transactions/merchants/_form.html.erb index 76c21cb5..68526568 100644 --- a/app/views/transactions/merchants/_form.html.erb +++ b/app/views/transactions/merchants/_form.html.erb @@ -1,6 +1,6 @@ <% is_editing = @merchant.id.present? %>
- <%= form_with model: @merchant, url: is_editing ? transactions_merchant_path(@merchant) : transactions_merchants_path, method: is_editing ? :patch : :post, scope: :transaction_merchant, data: { turbo: false } do |f| %> + <%= form_with model: @merchant, url: is_editing ? transaction_merchant_path(@merchant) : transaction_merchants_path, method: is_editing ? :patch : :post, scope: :transaction_merchant, data: { turbo: false } do |f| %>
<%= render partial: "transactions/merchants/avatar", locals: { merchant: } %> diff --git a/app/views/transactions/merchants/_list.html.erb b/app/views/transactions/merchants/_list.html.erb index c3bf656e..af4bca7b 100644 --- a/app/views/transactions/merchants/_list.html.erb +++ b/app/views/transactions/merchants/_list.html.erb @@ -13,13 +13,13 @@