mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-18 20:59:39 +02:00
Fix: Filter categories by transaction type in forms (#2082)
Changed transaction form to only display relevant categories based on transaction type (income or expense), improving usability and preventing misclassification. Created a shared transaction type tabs component for consistent navigation between expense, income, and transfer forms, providing a better user experience and reducing code duplication. Signed-off-by: Zach Gollwitzer <zach@maybe.co> Co-authored-by: Zach Gollwitzer <zach@maybe.co>
This commit is contained in:
parent
ce83418f0b
commit
71bc51ca15
10 changed files with 74 additions and 67 deletions
|
@ -117,4 +117,3 @@ end
|
|||
- Enforce `null` checks, unique indexes, and other simple validations in the DB
|
||||
- ActiveRecord validations _may_ mirror the DB level ones, but not 100% necessary. These are for convenience when error handling in forms. Always prefer client-side form validation when possible.
|
||||
- Complex validations and business logic should remain in ActiveRecord
|
||||
|
||||
|
|
|
@ -247,10 +247,3 @@ class ConcreteProvider < Provider
|
|||
end
|
||||
end
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ It means so much that you're interested in contributing to Maybe! Seriously. Tha
|
|||
|
||||
## House Rules
|
||||
|
||||
- Before contributing, familiarize yourself with our project conventions. You should read through our [Project Conventions Rule](https://github.com/maybe-finance/maybe/.cursor/rules/project-conventions.mdc), which is intended for LLMs, but is also an excellent primer on how we write code for Maybe.
|
||||
- Before contributing, familiarize yourself with our project conventions. You should read through our [Project Conventions Rule](https://github.com/maybe-finance/maybe/.cursor/rules/project-conventions.mdc), which is intended for LLMs, but is also an excellent primer on how we write code for Maybe.
|
||||
- While totally optional, consider using Cursor + VSCode as it will automatically apply our project conventions to your code via the `.cursor/rules` directory.
|
||||
- Before contributing, please check if it already exists in [issues](https://github.com/maybe-finance/maybe/issues) or [PRs](https://github.com/maybe-finance/maybe/pulls)
|
||||
- Given the speed at which we're moving on the codebase, we don't assign issues or "give" issues to anyone.
|
||||
|
|
|
@ -3,6 +3,12 @@ class TransactionsController < ApplicationController
|
|||
|
||||
before_action :store_params!, only: :index
|
||||
|
||||
def new
|
||||
super
|
||||
@income_categories = Current.family.categories.incomes.alphabetically
|
||||
@expense_categories = Current.family.categories.expenses.alphabetically
|
||||
end
|
||||
|
||||
def index
|
||||
@q = search_params
|
||||
transactions_query = Current.family.transactions.active.search(@q)
|
||||
|
|
|
@ -10,29 +10,13 @@ module FormsHelper
|
|||
render partial: "shared/modal_form", locals: { title:, subtitle:, content:, overflow_visible: }
|
||||
end
|
||||
|
||||
def radio_tab_tag(form:, name:, value:, label:, icon:, checked: false, disabled: false, class: nil)
|
||||
form.label name, for: form.field_id(name, value), class: "group has-disabled:cursor-not-allowed" do
|
||||
concat radio_tab_contents(label:, icon:, class:)
|
||||
concat form.radio_button(name, value, checked:, disabled:, class: "hidden")
|
||||
end
|
||||
end
|
||||
|
||||
def period_select(form:, selected:, classes: "border border-secondary bg-container-inset rounded-lg text-sm pr-7 cursor-pointer text-primary focus:outline-hidden focus:ring-0")
|
||||
periods_for_select = Period.all.map { |period| [ period.label_short, period.key ] }
|
||||
|
||||
form.select(:period, periods_for_select, { selected: selected.key }, class: classes, data: { "auto-submit-form-target": "auto" })
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def currencies_for_select
|
||||
Money::Currency.all_instances.sort_by { |currency| [ currency.priority, currency.name ] }
|
||||
end
|
||||
|
||||
private
|
||||
def radio_tab_contents(label:, icon:, class: nil)
|
||||
tag.div(class: "flex px-4 py-1 rounded-lg items-center space-x-2 justify-center text-sm md:text-normal text-subdued group-has-checked:bg-surface group-has-checked:text-primary group-has-checked:shadow-sm") do
|
||||
concat lucide_icon(icon, class: "w-5 h-5")
|
||||
concat tag.span(label, class: "group-has-checked:font-semibold")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
22
app/javascript/controllers/transaction_form_controller.js
Normal file
22
app/javascript/controllers/transaction_form_controller.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
import {Controller} from "@hotwired/stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["expenseCategories", "incomeCategories"]
|
||||
|
||||
connect() {
|
||||
this.updateCategories()
|
||||
}
|
||||
|
||||
updateCategories(event) {
|
||||
const natureField = this.element.querySelector('input[name="account_entry[nature]"]:checked')
|
||||
const natureValue = natureField ? natureField.value : 'outflow'
|
||||
|
||||
if (natureValue === 'inflow') {
|
||||
this.expenseCategoriesTarget.classList.add('hidden')
|
||||
this.incomeCategoriesTarget.classList.remove('hidden')
|
||||
} else {
|
||||
this.expenseCategoriesTarget.classList.remove('hidden')
|
||||
this.incomeCategoriesTarget.classList.add('hidden')
|
||||
}
|
||||
}
|
||||
}
|
24
app/views/shared/_transaction_type_tabs.html.erb
Normal file
24
app/views/shared/_transaction_type_tabs.html.erb
Normal file
|
@ -0,0 +1,24 @@
|
|||
<fieldset class="bg-gray-50 rounded-lg p-1 grid grid-flow-col justify-stretch gap-x-2">
|
||||
<% active_tab = local_assigns[:active_tab] || 'expense' %>
|
||||
|
||||
<%= link_to new_transaction_path(nature: 'outflow'),
|
||||
data: { turbo_frame: :modal },
|
||||
class: "flex px-4 py-1 rounded-lg items-center space-x-2 justify-center text-sm md:text-normal text-subdued #{active_tab == 'expense' ? 'bg-container text-gray-800 shadow-sm' : 'hover:bg-container hover:text-gray-800 hover:shadow-sm'}" do %>
|
||||
<%= lucide_icon "minus-circle", class: "w-4 h-4 md:w-5 md:h-5" %>
|
||||
<%= tag.span t("shared.transaction_tabs.expense") %>
|
||||
<% end %>
|
||||
|
||||
<%= link_to new_transaction_path(nature: 'inflow'),
|
||||
data: { turbo_frame: :modal },
|
||||
class: "flex px-4 py-1 rounded-lg items-center space-x-2 justify-center text-sm md:text-normal text-subdued #{active_tab == 'income' ? 'bg-container text-gray-800 shadow-sm' : 'hover:bg-container hover:text-gray-800 hover:shadow-sm'}" do %>
|
||||
<%= lucide_icon "plus-circle", class: "w-4 h-4 md:w-5 md:h-5" %>
|
||||
<%= tag.span t("shared.transaction_tabs.income") %>
|
||||
<% end %>
|
||||
|
||||
<%= link_to new_transfer_path,
|
||||
data: { turbo_frame: :modal },
|
||||
class: "flex px-4 py-1 rounded-lg items-center space-x-2 justify-center text-sm md:text-normal text-subdued #{active_tab == 'transfer' ? 'bg-container text-gray-800 shadow-sm' : 'hover:bg-container hover:text-gray-800 hover:shadow-sm'}" do %>
|
||||
<%= lucide_icon "arrow-right-left", class: "w-4 h-4 md:w-5 md:h-5" %>
|
||||
<%= tag.span t("shared.transaction_tabs.transfer") %>
|
||||
<% end %>
|
||||
</fieldset>
|
|
@ -1,25 +1,18 @@
|
|||
<%= styled_form_with model: @entry, url: transactions_path, class: "space-y-4 text-subdued" do |f| %>
|
||||
|
||||
<%= styled_form_with model: @entry, url: transactions_path, class: "space-y-4 text-subdued", data: { controller: "transaction-form" } do |f| %>
|
||||
<% if entry.errors.any? %>
|
||||
<%= render "shared/form_errors", model: entry %>
|
||||
<% end %>
|
||||
|
||||
<section>
|
||||
<fieldset class="bg-container rounded-lg p-1 grid grid-flow-col justify-stretch gap-x-1 md:gap-x-2">
|
||||
<%= radio_tab_tag form: f, name: :nature, value: :outflow, label: t(".expense"), icon: "minus-circle", checked: params[:nature] == "outflow" || params[:nature].nil?, class: "text-xs md:text-sm" %>
|
||||
<%= radio_tab_tag form: f, name: :nature, value: :inflow, label: t(".income"), icon: "plus-circle", checked: params[:nature] == "inflow", class: "text-xs md:text-sm" %>
|
||||
<%= link_to new_transfer_path, data: { turbo_frame: :modal }, class: "flex px-4 py-1 rounded-lg items-center space-x-2 justify-center text-subdued text-sm md:text-normal group-has-checked:bg-container group-has-checked:text-gray-800 group-has-checked:shadow-sm group-has-checked:text-sm" do %>
|
||||
<%= lucide_icon "arrow-right-left", class: "w-4 h-4 md:w-5 md:h-5" %>
|
||||
<%= tag.span t(".transfer") %>
|
||||
<% end %>
|
||||
</fieldset>
|
||||
<%= render "shared/transaction_type_tabs", active_tab: params[:nature] == "inflow" ? "income" : "expense" %>
|
||||
|
||||
<%= f.hidden_field :nature, value: params[:nature] || "outflow", data: { "transaction-form-target": "natureField" } %>
|
||||
<%= f.hidden_field :entryable_type, value: "Transaction" %>
|
||||
</section>
|
||||
|
||||
<section class="space-y-2 overflow-hidden">
|
||||
<%= f.text_field :name, label: t(".description"), placeholder: t(".description_placeholder"), required: true %>
|
||||
|
||||
<%= f.hidden_field :entryable_type, value: "Transaction" %>
|
||||
|
||||
<% if @entry.account_id %>
|
||||
<%= f.hidden_field :account_id %>
|
||||
<% else %>
|
||||
|
@ -28,7 +21,8 @@
|
|||
|
||||
<%= f.money_field :amount, label: t(".amount"), required: true %>
|
||||
<%= f.fields_for :entryable do |ef| %>
|
||||
<%= ef.collection_select :category_id, Current.family.categories.alphabetically, :id, :name, { prompt: t(".category_prompt"), label: t(".category") } %>
|
||||
<% categories = params[:nature] == "inflow" ? @income_categories : @expense_categories %>
|
||||
<%= ef.collection_select :category_id, categories, :id, :name, { prompt: t(".category_prompt"), label: t(".category") } %>
|
||||
<% end %>
|
||||
<%= f.date_field :date, label: t(".date"), required: true, min: Entry.min_supported_date, max: Date.current, value: Date.current %>
|
||||
</section>
|
||||
|
@ -36,19 +30,19 @@
|
|||
<%= disclosure t(".details"), default_open: false do %>
|
||||
<%= f.fields_for :entryable do |ef| %>
|
||||
<%= ef.select :tag_ids,
|
||||
Current.family.tags.alphabetically.pluck(:name, :id),
|
||||
{
|
||||
include_blank: t(".none"),
|
||||
multiple: true,
|
||||
label: t(".tags_label"),
|
||||
container_class: "h-40"
|
||||
} %>
|
||||
Current.family.tags.alphabetically.pluck(:name, :id),
|
||||
{
|
||||
include_blank: t(".none"),
|
||||
multiple: true,
|
||||
label: t(".tags_label"),
|
||||
container_class: "h-40"
|
||||
} %>
|
||||
<% end %>
|
||||
<%= f.text_area :notes,
|
||||
label: t(".note_label"),
|
||||
placeholder: t(".note_placeholder"),
|
||||
rows: 5,
|
||||
"data-auto-submit-form-target": "auto" %>
|
||||
label: t(".note_label"),
|
||||
placeholder: t(".note_placeholder"),
|
||||
rows: 5,
|
||||
"data-auto-submit-form-target": "auto" %>
|
||||
<% end %>
|
||||
|
||||
<section>
|
||||
|
|
|
@ -7,22 +7,7 @@
|
|||
<% end %>
|
||||
|
||||
<section>
|
||||
<fieldset class="bg-container rounded-lg p-1 grid grid-flow-col justify-stretch gap-x-1">
|
||||
<%= link_to new_transaction_path(nature: "expense"), data: { turbo_frame: :modal }, class: "flex px-4 py-1 rounded-lg items-center space-x-2 justify-center text-subdued" do %>
|
||||
<%= lucide_icon "minus-circle", class: "w-4 h-4" %>
|
||||
<%= tag.span t(".expense") %>
|
||||
<% end %>
|
||||
|
||||
<%= link_to new_transaction_path(nature: "income"), data: { turbo_frame: :modal }, class: "flex px-4 py-1 rounded-lg items-center space-x-2 justify-center text-subdued" do %>
|
||||
<%= lucide_icon "plus-circle", class: "w-4 h-4" %>
|
||||
<%= tag.span t(".income") %>
|
||||
<% end %>
|
||||
|
||||
<%= tag.div class: "flex px-4 py-1 rounded-lg items-center space-x-2 justify-center bg-container text-primary shadow-sm" do %>
|
||||
<%= lucide_icon "arrow-right-left", class: "w-4 h-4" %>
|
||||
<%= tag.span t(".transfer") %>
|
||||
<% end %>
|
||||
</fieldset>
|
||||
<%= render "shared/transaction_type_tabs", active_tab: "transfer" %>
|
||||
</section>
|
||||
|
||||
<section class="space-y-2">
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue