1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-07-18 12:49:38 +02:00

Fix: Filter categories by transaction type in forms (#2082)
Some checks failed
Publish Docker image / ci (push) Has been cancelled
Publish Docker image / Build docker image (push) Has been cancelled

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:
Diego Gasparis Escobedo 2025-04-25 08:18:10 -06:00 committed by GitHub
parent ce83418f0b
commit 71bc51ca15
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 74 additions and 67 deletions

View file

@ -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

View file

@ -247,10 +247,3 @@ class ConcreteProvider < Provider
end
end
```

View file

@ -19,4 +19,4 @@ The codebase uses TailwindCSS v4.x (the newest version) with a custom design sys
- Example 2: use `bg-container` rather than `bg-white`
- Example 3: use `border border-primary` rather than `border border-gray-200`
- Never create new styles in [maybe-design-system.css](mdc:app/assets/tailwind/maybe-design-system.css) or [application.css](mdc:app/assets/tailwind/application.css) without explicitly receiving permission to do so
- Always generate semantic HTML
- Always generate semantic HTML

View file

@ -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.

View file

@ -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)

View file

@ -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

View 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')
}
}
}

View 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>

View file

@ -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>

View file

@ -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">