mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-19 13:19:39 +02:00
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!
This commit is contained in:
parent
dc024d63b0
commit
4c5f8263bc
40 changed files with 580 additions and 145 deletions
|
@ -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
|
|
@ -1,36 +1,44 @@
|
|||
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_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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
21
app/helpers/menus_helper.rb
Normal file
21
app/helpers/menus_helper.rb
Normal file
|
@ -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
|
|
@ -1,2 +1,7 @@
|
|||
module Transactions::CategoriesHelper
|
||||
def null_category
|
||||
Transaction::Category.new \
|
||||
name: "Uncategorized",
|
||||
color: Transaction::Category::UNCATEGORIZED_COLOR
|
||||
end
|
||||
end
|
||||
|
|
30
app/javascript/controllers/category_deletion_controller.js
Normal file
30
app/javascript/controllers/category_deletion_controller.js
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
59
app/javascript/controllers/color_select_controller.js
Normal file
59
app/javascript/controllers/color_select_controller.js
Normal file
|
@ -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})`;
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -63,6 +63,6 @@
|
|||
<% else %>
|
||||
<%= previous_setting("Billing", settings_billing_path) %>
|
||||
<% end %>
|
||||
<%= next_setting("Categories", transactions_categories_path) %>
|
||||
<%= next_setting("Categories", transaction_categories_path) %>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<h1 class="text-3xl font-semibold font-display"><%= t(".title") %></h1>
|
||||
<%= modal do %>
|
||||
<div class="flex flex-col min-h-[530px]" data-controller="list-keyboard-navigation">
|
||||
<div class="flex flex-col min-h-[530px] w-screen max-w-xl" data-controller="list-keyboard-navigation">
|
||||
<% if @account.accountable.blank? %>
|
||||
<div class="border-b border-alpha-black-25 p-4 text-gray-400">
|
||||
<%= t ".select_accountable_type" %>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between gap-4">
|
||||
<%= previous_setting("Rules", transactions_rules_path) %>
|
||||
<%= previous_setting("Rules", transaction_rules_path) %>
|
||||
<%= next_setting("Feedback", feedback_path) %>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -47,13 +47,13 @@
|
|||
</div>
|
||||
<ul class="space-y-1">
|
||||
<li>
|
||||
<%= sidebar_link_to t(".categories_label"), transactions_categories_path, icon: "tags" %>
|
||||
<%= sidebar_link_to t(".categories_label"), transaction_categories_path, icon: "tags" %>
|
||||
</li>
|
||||
<li>
|
||||
<%= sidebar_link_to t(".merchants_label"), transactions_merchants_path, icon: "store" %>
|
||||
<%= sidebar_link_to t(".merchants_label"), transaction_merchants_path, icon: "store" %>
|
||||
</li>
|
||||
<li>
|
||||
<%= sidebar_link_to t(".rules_label"), transactions_rules_path, icon: "list-checks" %>
|
||||
<%= sidebar_link_to t(".rules_label"), transaction_rules_path, icon: "list-checks" %>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<%# locals: (content:, classes:) -%>
|
||||
<%= turbo_frame_tag "modal" do %>
|
||||
<dialog class="bg-white border border-alpha-black-25 rounded-2xl max-h-[648px] max-w-[580px] w-full shadow-xs h-fit <%= classes %>" data-controller="modal" data-action="click->modal#clickOutside">
|
||||
<dialog class="bg-white border border-alpha-black-25 rounded-2xl max-h-[648px] max-w-[580px] w-min-content shadow-xs h-fit <%= classes %>" data-controller="modal" data-action="click->modal#clickOutside">
|
||||
<div class="flex flex-col">
|
||||
<%= content %>
|
||||
</div>
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
<% else %>
|
||||
<%= render partial: "transactions/transaction_name", locals: { name: transaction.name } %>
|
||||
<div class="w-36">
|
||||
<%= 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 } %>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="ml-auto">
|
||||
|
|
|
@ -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)" %>
|
||||
<span class="border text-sm font-medium px-2.5 py-1 rounded-full truncate" style="background-color: <%= background_color %>; border-color: <%= border_color %>; color: <%= color %>"><%= name %></span>
|
||||
<%# locals: (category:) %>
|
||||
<% category ||= null_category %>
|
||||
|
||||
<span class="border text-sm font-medium px-2.5 py-1 rounded-full cursor-pointer content-center"
|
||||
style="
|
||||
background-color: color-mix(in srgb, <%= category.color %> 5%, white);
|
||||
border-color: color-mix(in srgb, <%= category.color %> 10%, white);
|
||||
color: <%= category.color %>;">
|
||||
<%= category.name %>
|
||||
</span>
|
||||
|
|
38
app/views/transactions/categories/_form.html.erb
Normal file
38
app/views/transactions/categories/_form.html.erb
Normal file
|
@ -0,0 +1,38 @@
|
|||
<%= form_with model: category, data: { turbo: false } do |form| %>
|
||||
<div class="flex flex-col space-y-4 w-96" data-controller="color-select" data-color-select-selection-value="<%= category.color %>">
|
||||
<fieldset class="relative">
|
||||
<span data-color-select-target="decoration" class="pointer-events-none absolute inset-y-3.5 left-3 flex items-center pl-1 block w-1 rounded-lg"></span>
|
||||
<%= 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" %>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<%= form.hidden_field :color, data: { color_select_target: "input" } %>
|
||||
|
||||
<ul role="radiogroup" class="flex justify-between items-center py-2">
|
||||
<% Transaction::Category::COLORS.each do |color| %>
|
||||
<li tabindex="0"
|
||||
role="radio"
|
||||
data-action="click->color-select#select keydown.enter->color-select#select keydown.space->color-select#select"
|
||||
data-value="<%= color %>"
|
||||
class="flex shrink-0 justify-center items-center w-5 h-5 cursor-pointer hover:bg-gray-200 rounded-full">
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</fieldset>
|
||||
|
||||
<section>
|
||||
<%= hidden_field_tag :transaction_id, params[:transaction_id] %>
|
||||
|
||||
<% if category.persisted? %>
|
||||
<%= form.submit t(".update") %>
|
||||
<% else %>
|
||||
<%= form.submit t(".create") %>
|
||||
<% end %>
|
||||
</section>
|
||||
</div>
|
||||
<% end %>
|
|
@ -1,7 +1,7 @@
|
|||
<%# locals: (transaction:) %>
|
||||
<div class="relative" data-controller="menu">
|
||||
<button data-menu-target="button cursor-pointer" class="flex">
|
||||
<%= 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 } %>
|
||||
</button>
|
||||
<div data-menu-target="content" class="absolute z-10 hidden w-screen mt-2 max-w-min cursor-default">
|
||||
<div class="w-64 text-sm font-semibold leading-6 text-gray-900 bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
|
||||
|
@ -22,16 +22,25 @@
|
|||
<% end %>
|
||||
</div>
|
||||
<hr>
|
||||
<div data-controller="menu" class="relative p-1.5 w-full">
|
||||
<button data-menu-target="button" class="flex text-sm font-medium items-center gap-2 text-gray-500 w-full rounded-lg p-2 hover:bg-gray-100">
|
||||
<%= lucide_icon("plus", class: "w-5 h-5") %>
|
||||
Add new
|
||||
</button>
|
||||
<div data-menu-target="content" class="hidden absolute bottom-14 right-0">
|
||||
<div class="w-96 text-sm font-semibold leading-6 text-gray-900 bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
|
||||
<%= render partial: "transactions/categories/dropdown/form" %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative p-1.5 w-full">
|
||||
<%= link_to new_transaction_category_path(transaction_id: transaction),
|
||||
class: "flex text-sm font-medium items-center gap-2 text-gray-500 w-full rounded-lg p-2 hover:bg-gray-100",
|
||||
data: { turbo_frame: "modal" } do %>
|
||||
<%= lucide_icon "plus", class: "w-5 h-5" %>
|
||||
|
||||
<%= t(".add_new") %>
|
||||
<% end %>
|
||||
|
||||
<% if transaction.category %>
|
||||
<%= button_to transaction_path(transaction),
|
||||
method: :patch,
|
||||
params: { transaction: { category_id: nil } },
|
||||
class: "flex text-sm font-medium items-center gap-2 text-gray-500 w-full rounded-lg p-2 hover:bg-gray-100" do %>
|
||||
<%= lucide_icon "minus", class: "w-5 h-5" %>
|
||||
|
||||
<%= t(".clear") %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
23
app/views/transactions/categories/_row.html.erb
Normal file
23
app/views/transactions/categories/_row.html.erb
Normal file
|
@ -0,0 +1,23 @@
|
|||
<div class="flex justify-between mx-4 py-5 border-b last:border-b-0 border-alpha-black-50">
|
||||
<%= render partial: "transactions/categories/badge", locals: { category: row } %>
|
||||
|
||||
<%= contextual_menu do %>
|
||||
<div class="w-48 p-1 text-sm leading-6 text-gray-900 bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
|
||||
<%= 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" %>
|
||||
|
||||
<span><%= t(".edit") %></span>
|
||||
<% 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" %>
|
||||
|
||||
<span><%= t(".delete") %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
33
app/views/transactions/categories/deletions/new.html.erb
Normal file
33
app/views/transactions/categories/deletions/new.html.erb
Normal file
|
@ -0,0 +1,33 @@
|
|||
<%= modal do %>
|
||||
<article class="mx-auto p-4 w-screen max-w-md">
|
||||
<div class="space-y-2">
|
||||
<header class="flex justify-between">
|
||||
<h2 class="font-medium text-xl"><%= t(".delete_category") %></h2>
|
||||
<%= lucide_icon "x", class: "w-5 h-5 text-gray-500", data: { action: "click->modal#close" } %>
|
||||
</header>
|
||||
|
||||
<p class="text-gray-500 font-light">
|
||||
<%= t(".explanation", category_name: @category.name) %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<%= 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 %>
|
||||
</article>
|
||||
<% end %>
|
|
@ -1,15 +0,0 @@
|
|||
<%# locals: (category:) %>
|
||||
<div class="w-96 text-sm font-semibold leading-6 text-gray-900 bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
|
||||
<div class="flex flex-col">
|
||||
<%= render partial: "transactions/categories/dropdown/form", locals: { category: } %>
|
||||
<hr>
|
||||
<div class="p-1.5 w-full">
|
||||
<%= 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 %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -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| %>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col p-1.5 gap-1.5">
|
||||
<div class="relative flex items-center border border-gray-200 rounded-lg">
|
||||
<%= 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" %>
|
||||
</div>
|
||||
<div class="p-2 overflow-x-auto">
|
||||
<div data-controller="select" data-select-active-class="bg-gray-200" data-select-selected-value="<%= category&.color || Transaction::Category::COLORS[0] %>">
|
||||
<%= form.hidden_field :color, data: { select_target: "input" } %>
|
||||
<ul data-select-target="list" class="flex gap-2 items-center">
|
||||
<% Transaction::Category::COLORS.each do |color| %>
|
||||
<li tabindex="0" data-select-target="option" data-action="click->select#selectOption" data-value="<%= color %>" class="flex shrink-0 justify-center items-center w-6 h-6 cursor-pointer hover:bg-gray-200 rounded-full">
|
||||
<div style="background-color: <%= color %>" class="shrink-0 w-4 h-4 rounded-full"></div>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="p-1.5 w-full">
|
||||
<%= 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 %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
|
@ -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 %>
|
||||
<span class="w-5 h-5">
|
||||
<%= lucide_icon("check", class: "w-5 h-5 text-gray-500") if is_selected %>
|
||||
</span>
|
||||
<%= render partial: "transactions/categories/badge", locals: { name: category.name, color: category.color } %>
|
||||
<%= render partial: "transactions/categories/badge", locals: { category: category } %>
|
||||
<% end %>
|
||||
|
||||
<%= contextual_menu do %>
|
||||
<div class="w-48 p-1 text-sm leading-6 text-gray-900 bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
|
||||
<%= link_to edit_transaction_category_path(category),
|
||||
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" %>
|
||||
|
||||
<span><%= t(".edit") %></span>
|
||||
<% end %>
|
||||
|
||||
<%= link_to new_transaction_category_deletion_path(category),
|
||||
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" %>
|
||||
|
||||
<span><%= t(".delete") %></span>
|
||||
<% end %>
|
||||
<div data-controller="menu">
|
||||
<button data-menu-target="button" type="button" class="flex items-center justify-center hover:bg-gray-50 w-8 h-8 rounded-lg">
|
||||
<%= lucide_icon("more-horizontal", class: "w-5 h-5 text-gray-500") %>
|
||||
</button>
|
||||
<div data-menu-target="content" class="absolute z-30 hidden w-screen mt-2 max-w-min">
|
||||
<%= render partial: "transactions/categories/dropdown/edit", locals: { category: } %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
|
10
app/views/transactions/categories/edit.html.erb
Normal file
10
app/views/transactions/categories/edit.html.erb
Normal file
|
@ -0,0 +1,10 @@
|
|||
<%= modal do %>
|
||||
<article class="mx-auto w-full p-4 space-y-4">
|
||||
<header class="flex justify-between">
|
||||
<h2 class="font-medium text-xl"><%= t(".edit") %></h2>
|
||||
<%= lucide_icon "x", class: "w-5 h-5 text-gray-500", data: { action: "click->modal#close" } %>
|
||||
</header>
|
||||
|
||||
<%= render "form", category: @category %>
|
||||
</article>
|
||||
<% end %>
|
|
@ -1,15 +1,28 @@
|
|||
<% content_for :sidebar do %>
|
||||
<%= render "settings/nav" %>
|
||||
<% end %>
|
||||
<div class="space-y-4">
|
||||
<h1 class="text-gray-900 text-xl font-medium mb-4">Categories</h1>
|
||||
<section class="space-y-4">
|
||||
<header class="flex items-center justify-between">
|
||||
<h1 class="text-gray-900 text-xl font-medium"><%= t(".categories") %></h1>
|
||||
|
||||
<%= 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" %>
|
||||
<p><%= t(".new") %></p>
|
||||
<% end %>
|
||||
</header>
|
||||
|
||||
<div class="bg-white shadow-xs border border-alpha-black-25 rounded-xl p-4">
|
||||
<div class="flex justify-center items-center py-20">
|
||||
<p class="text-gray-500">Transaction categories coming soon...</p>
|
||||
<div class="rounded-xl bg-gray-25 p-1">
|
||||
<h2 class="uppercase px-4 py-2 text-gray-500 text-xs"><%= t(".categories") %> · <%= @categories.size %></h2>
|
||||
|
||||
<div class="border border-alpha-gray-100 rounded-lg bg-white shadow-xs">
|
||||
<%= render collection: @categories, partial: "transactions/categories/row" %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between gap-4">
|
||||
</div>
|
||||
|
||||
<footer class="flex justify-between gap-4">
|
||||
<%= previous_setting("Accounts", accounts_path) %>
|
||||
<%= next_setting("Merchants", transactions_merchants_path) %>
|
||||
</div>
|
||||
</div>
|
||||
<%= next_setting("Merchants", transaction_merchants_path) %>
|
||||
</footer>
|
||||
</section>
|
||||
|
|
10
app/views/transactions/categories/new.html.erb
Normal file
10
app/views/transactions/categories/new.html.erb
Normal file
|
@ -0,0 +1,10 @@
|
|||
<%= modal do %>
|
||||
<article class="mx-auto w-full p-4 space-y-4">
|
||||
<header class="flex justify-between">
|
||||
<h2 class="font-medium text-xl"><%= t(".new_category") %></h2>
|
||||
<%= lucide_icon "x", class: "w-5 h-5 text-gray-500", data: { action: "click->modal#close" } %>
|
||||
</header>
|
||||
|
||||
<%= render "form", category: @category %>
|
||||
</article>
|
||||
<% end %>
|
|
@ -3,6 +3,16 @@
|
|||
<h1 class="text-xl">Transactions</h1>
|
||||
<div class="flex items-center gap-5">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= contextual_menu do %>
|
||||
<div class="w-48 p-1 text-sm leading-6 text-gray-900 bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
|
||||
<%= 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" %>
|
||||
<span class="text-black"><%= t(".edit_categories") %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% 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") %>
|
||||
<p>New transaction</p>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<% is_editing = @merchant.id.present? %>
|
||||
<div data-controller="merchant-avatar">
|
||||
<%= 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| %>
|
||||
<section class="space-y-4">
|
||||
<div class="w-fit m-auto">
|
||||
<%= render partial: "transactions/merchants/avatar", locals: { merchant: } %>
|
||||
|
|
|
@ -13,13 +13,13 @@
|
|||
</button>
|
||||
<div data-menu-target="content" class="absolute z-10 top-10 right-0 border border-alpha-black-25 bg-white rounded-lg shadow-xs w-48 hidden">
|
||||
<div class="border-t border-b border-alpha-black-100 p-1">
|
||||
<%= button_to edit_transactions_merchant_path(merchant),
|
||||
<%= button_to edit_transaction_merchant_path(merchant),
|
||||
method: :get,
|
||||
class: "flex w-full gap-1 items-center text-sm hover:bg-gray-50 rounded-lg px-3 py-2",
|
||||
data: { turbo_frame: "modal" } do %>
|
||||
<%= lucide_icon("pencil-line", class: "w-5 h-5 mr-2") %> <%= t(".edit") %>
|
||||
<% end %>
|
||||
<%= button_to transactions_merchant_path(merchant),
|
||||
<%= button_to transaction_merchant_path(merchant),
|
||||
method: :delete,
|
||||
class: "flex w-full gap-1 items-center text-sm text-red-600 hover:text-red-800 hover:bg-gray-50 rounded-lg px-3 py-2",
|
||||
data: {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-xl font-medium text-gray-900"><%= t(".title") %></h1>
|
||||
<%= link_to new_transactions_merchant_path, class: "flex text-white text-sm font-medium items-center gap-1 bg-gray-900 rounded-lg p-2", data: { turbo_frame: "modal" } do %>
|
||||
<%= link_to new_transaction_merchant_path, class: "flex text-white text-sm font-medium items-center gap-1 bg-gray-900 rounded-lg p-2", data: { turbo_frame: "modal" } do %>
|
||||
<%= lucide_icon("plus", class: "w-5 h-5") %>
|
||||
<span><%= t(".new_short") %></span>
|
||||
<% end %>
|
||||
|
@ -14,7 +14,7 @@
|
|||
<div class="flex justify-center items-center py-20">
|
||||
<div class="text-center flex flex-col items-center max-w-[300px]">
|
||||
<p class="text-gray-900 mb-1 font-medium text-sm"><%= t(".empty") %></p>
|
||||
<%= link_to new_transactions_merchant_path, class: "w-fit flex text-white text-sm font-medium items-center gap-1 bg-gray-900 rounded-lg p-2", data: { turbo_frame: "modal" } do %>
|
||||
<%= link_to new_transaction_merchant_path, class: "w-fit flex text-white text-sm font-medium items-center gap-1 bg-gray-900 rounded-lg p-2", data: { turbo_frame: "modal" } do %>
|
||||
<%= lucide_icon("plus", class: "w-5 h-5") %>
|
||||
<span><%= t(".new_long") %></span>
|
||||
<% end %>
|
||||
|
@ -32,7 +32,7 @@
|
|||
<% end %>
|
||||
</div>
|
||||
<div class="flex justify-between gap-4">
|
||||
<%= previous_setting("Categories", transactions_categories_path) %>
|
||||
<%= next_setting("Rules", transactions_rules_path) %>
|
||||
<%= previous_setting("Categories", transaction_categories_path) %>
|
||||
<%= next_setting("Rules", transaction_rules_path) %>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<%= modal do %>
|
||||
<article class="mx-auto w-full p-4 space-y-4">
|
||||
<article class="mx-auto p-4 space-y-4 w-screen max-w-xl">
|
||||
<header class="flex justify-between">
|
||||
<h2 class="font-medium text-xl">New transaction</h2>
|
||||
<%= lucide_icon "x", class: "w-5 h-5 text-gray-500", data: { action: "click->modal#close" } %>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between gap-4">
|
||||
<%= previous_setting("Merchants", transactions_merchants_path) %>
|
||||
<%= previous_setting("Merchants", transaction_merchants_path) %>
|
||||
<%= next_setting("What's New", changelog_path) %>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<div class="filterable-item flex items-center gap-2 p-2" data-filter-name="<%= transaction_category.name %>">
|
||||
<%= form.check_box :category_id_in, { "data-auto-submit-form-target": "auto", multiple: true, class: "rounded-sm border-gray-300 text-indigo-600 shadow-xs focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50" }, transaction_category.id, nil %>
|
||||
<%= form.label :category_id_in, transaction_category.name, value: transaction_category.id, class: "text-sm text-gray-900 cursor-pointer" do %>
|
||||
<%= render partial: "transactions/categories/badge", locals: { name: transaction_category.name, color: transaction_category.color } %>
|
||||
<%= render partial: "transactions/categories/badge", locals: { category: transaction_category } %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
|
@ -3,12 +3,40 @@ en:
|
|||
transactions:
|
||||
categories:
|
||||
create:
|
||||
error: Error creating transaction category
|
||||
success: New transaction category created successfully
|
||||
destroy:
|
||||
deletions:
|
||||
create:
|
||||
success: Transaction category deleted successfully
|
||||
new:
|
||||
category: Category
|
||||
delete_and_leave_uncategorized: Delete "%{category_name}" and leave uncategorized
|
||||
delete_and_recategorize: Delete "%{category_name}" and assign new category
|
||||
delete_category: Delete category?
|
||||
explanation: By deleting this category, every transaction that has the "%{category_name}"
|
||||
category will be uncategorized. Instead of leaving them uncategorized,
|
||||
you can also assign a new category below.
|
||||
replacement_category_prompt: Select category
|
||||
dropdown:
|
||||
row:
|
||||
delete: Delete category
|
||||
edit: Edit category
|
||||
edit:
|
||||
edit: Edit category
|
||||
form:
|
||||
create: Create category
|
||||
update: Update
|
||||
index:
|
||||
categories: Categories
|
||||
new: New
|
||||
menu:
|
||||
add_new: Add new
|
||||
clear: Clear
|
||||
new:
|
||||
new_category: New category
|
||||
row:
|
||||
delete: Delete category
|
||||
edit: Edit category
|
||||
update:
|
||||
error: Error updating transaction category
|
||||
success: Transaction category updated successfully
|
||||
create:
|
||||
success: New transaction created successfully
|
||||
|
@ -27,6 +55,8 @@ en:
|
|||
income: Income
|
||||
submit: Add transaction
|
||||
transfer: Transfer
|
||||
index:
|
||||
edit_categories: Edit categories
|
||||
merchants:
|
||||
create:
|
||||
error: Error creating merchant
|
||||
|
|
|
@ -21,17 +21,19 @@ Rails.application.routes.draw do
|
|||
resource :security, only: %i[show update]
|
||||
end
|
||||
|
||||
namespace :transactions do
|
||||
resources :categories
|
||||
|
||||
# TODO: These are *placeholders*
|
||||
# Uncomment `only` and add the necessary actions as they are implemented.
|
||||
resources :rules, only: [ :index ]
|
||||
resources :merchants, only: %i[index new create edit update destroy]
|
||||
end
|
||||
|
||||
resources :transactions do
|
||||
match "search" => "transactions#search", on: :collection, via: [ :get, :post ], as: :search
|
||||
|
||||
collection do
|
||||
scope module: :transactions do
|
||||
resources :categories, as: :transaction_categories do
|
||||
resources :deletions, only: %i[ new create ], module: :categories
|
||||
end
|
||||
|
||||
resources :rules, only: %i[ index ], as: :transaction_rules
|
||||
resources :merchants, only: %i[ index new create edit update destroy ], as: :transaction_merchants
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
resources :accounts, shallow: true do
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
require "test_helper"
|
||||
|
||||
class Transactions::Categories::DeletionsControllerTest < ActionDispatch::IntegrationTest
|
||||
setup do
|
||||
sign_in users(:family_admin)
|
||||
@category = transaction_categories(:food_and_drink)
|
||||
end
|
||||
|
||||
test "new" do
|
||||
get new_transaction_category_deletion_url(@category)
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "create with replacement" do
|
||||
replacement_category = transaction_categories(:income)
|
||||
|
||||
assert_not_empty @category.transactions
|
||||
|
||||
assert_difference "Transaction::Category.count", -1 do
|
||||
assert_difference "replacement_category.transactions.count", @category.transactions.count do
|
||||
post transaction_category_deletions_url(@category),
|
||||
params: { replacement_category_id: replacement_category.id }
|
||||
end
|
||||
end
|
||||
|
||||
assert_redirected_to transactions_url
|
||||
end
|
||||
|
||||
test "create without replacement" do
|
||||
assert_not_empty @category.transactions
|
||||
|
||||
assert_difference "Transaction::Category.count", -1 do
|
||||
assert_difference "Transaction.where(category: nil).count", @category.transactions.count do
|
||||
post transaction_category_deletions_url(@category)
|
||||
end
|
||||
end
|
||||
|
||||
assert_redirected_to transactions_url
|
||||
end
|
||||
end
|
|
@ -1,7 +1,73 @@
|
|||
require "test_helper"
|
||||
|
||||
class Transactions::CategoriesControllerTest < ActionDispatch::IntegrationTest
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
setup do
|
||||
sign_in users(:family_admin)
|
||||
end
|
||||
|
||||
test "index" do
|
||||
get transaction_categories_url
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "new" do
|
||||
get new_transaction_category_url
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "create" do
|
||||
color = Transaction::Category::COLORS.sample
|
||||
|
||||
assert_difference "Transaction::Category.count", +1 do
|
||||
post transaction_categories_url, params: {
|
||||
transaction_category: {
|
||||
name: "New Category",
|
||||
color: color } }
|
||||
end
|
||||
|
||||
new_category = Transaction::Category.order(:created_at).last
|
||||
|
||||
assert_redirected_to transactions_url
|
||||
assert_equal "New Category", new_category.name
|
||||
assert_equal color, new_category.color
|
||||
end
|
||||
|
||||
test "create and assign to transaction" do
|
||||
color = Transaction::Category::COLORS.sample
|
||||
|
||||
assert_difference "Transaction::Category.count", +1 do
|
||||
post transaction_categories_url, params: {
|
||||
transaction_id: transactions(:checking_one).id,
|
||||
transaction_category: {
|
||||
name: "New Category",
|
||||
color: color } }
|
||||
end
|
||||
|
||||
new_category = Transaction::Category.order(:created_at).last
|
||||
|
||||
assert_redirected_to transactions_url
|
||||
assert_equal "New Category", new_category.name
|
||||
assert_equal color, new_category.color
|
||||
assert_equal transactions(:checking_one).reload.category, new_category
|
||||
end
|
||||
|
||||
test "edit" do
|
||||
get edit_transaction_category_url(transaction_categories(:food_and_drink))
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "update" do
|
||||
new_color = Transaction::Category::COLORS.without(transaction_categories(:income).color).sample
|
||||
|
||||
assert_changes -> { transaction_categories(:income).name }, to: "New Name" do
|
||||
assert_changes -> { transaction_categories(:income).reload.color }, to: new_color do
|
||||
patch transaction_category_url(transaction_categories(:income)), params: {
|
||||
transaction_category: {
|
||||
name: "New Name",
|
||||
color: new_color } }
|
||||
end
|
||||
end
|
||||
|
||||
assert_redirected_to transactions_url
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,33 +7,33 @@ class Transactions::MerchantsControllerTest < ActionDispatch::IntegrationTest
|
|||
end
|
||||
|
||||
test "index" do
|
||||
get transactions_merchants_path
|
||||
get transaction_merchants_path
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "new" do
|
||||
get new_transactions_merchant_path
|
||||
get new_transaction_merchant_path
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "should create merchant" do
|
||||
assert_difference("Transaction::Merchant.count") do
|
||||
post transactions_merchants_url, params: { transaction_merchant: { name: "new merchant", color: "#000000" } }
|
||||
post transaction_merchants_url, params: { transaction_merchant: { name: "new merchant", color: "#000000" } }
|
||||
end
|
||||
|
||||
assert_redirected_to transactions_merchants_path
|
||||
assert_redirected_to transaction_merchants_path
|
||||
end
|
||||
|
||||
test "should update merchant" do
|
||||
patch transactions_merchant_url(@merchant), params: { transaction_merchant: { name: "new name", color: "#000000" } }
|
||||
assert_redirected_to transactions_merchants_path
|
||||
patch transaction_merchant_url(@merchant), params: { transaction_merchant: { name: "new name", color: "#000000" } }
|
||||
assert_redirected_to transaction_merchants_path
|
||||
end
|
||||
|
||||
test "should destroy merchant" do
|
||||
assert_difference("Transaction::Merchant.count", -1) do
|
||||
delete transactions_merchant_url(@merchant)
|
||||
delete transaction_merchant_url(@merchant)
|
||||
end
|
||||
|
||||
assert_redirected_to transactions_merchants_path
|
||||
assert_redirected_to transaction_merchants_path
|
||||
end
|
||||
end
|
||||
|
|
|
@ -32,4 +32,20 @@ class Transaction::CategoryTest < ActiveSupport::TestCase
|
|||
category.update_attribute(:color, "#000")
|
||||
end
|
||||
end
|
||||
|
||||
test "replacing and destroying" do
|
||||
transctions = transaction_categories(:food_and_drink).transactions.to_a
|
||||
|
||||
transaction_categories(:food_and_drink).replace_and_destroy!(transaction_categories(:income))
|
||||
|
||||
assert_equal transaction_categories(:income), transactions.map { |t| t.reload.category }.uniq.first
|
||||
end
|
||||
|
||||
test "replacing with nil should nullify the category" do
|
||||
transactions = transaction_categories(:food_and_drink).transactions.to_a
|
||||
|
||||
transaction_categories(:food_and_drink).replace_and_destroy!(nil)
|
||||
|
||||
assert_nil transactions.map { |t| t.reload.category }.uniq.first
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,9 +11,9 @@ class SettingsTest < ApplicationSystemTestCase
|
|||
[ "Security", "Security", settings_security_path ],
|
||||
[ "Billing", "Billing", settings_billing_path ],
|
||||
[ "Accounts", "Accounts", accounts_path ],
|
||||
[ "Categories", "Categories", transactions_categories_path ],
|
||||
[ "Merchants", "Merchants", transactions_merchants_path ],
|
||||
[ "Rules", "Rules", transactions_rules_path ],
|
||||
[ "Categories", "Categories", transaction_categories_path ],
|
||||
[ "Merchants", "Merchants", transaction_merchants_path ],
|
||||
[ "Rules", "Rules", transaction_rules_path ],
|
||||
[ "What's New", "What's New", changelog_path ],
|
||||
[ "Feedback", "Feedback", feedback_path ],
|
||||
[ "Invite friends", "Invite friends", invites_path ]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue