diff --git a/.cursor/rules/project-conventions.mdc b/.cursor/rules/project-conventions.mdc index 17cee2e0..33906c22 100644 --- a/.cursor/rules/project-conventions.mdc +++ b/.cursor/rules/project-conventions.mdc @@ -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 - diff --git a/.cursor/rules/project-design.mdc b/.cursor/rules/project-design.mdc index b2d13e9f..4b60f2f9 100644 --- a/.cursor/rules/project-design.mdc +++ b/.cursor/rules/project-design.mdc @@ -247,10 +247,3 @@ class ConcreteProvider < Provider end end ``` - - - - - - - diff --git a/.cursor/rules/ui-ux-design-guidelines.mdc b/.cursor/rules/ui-ux-design-guidelines.mdc index d497a887..cabe097f 100644 --- a/.cursor/rules/ui-ux-design-guidelines.mdc +++ b/.cursor/rules/ui-ux-design-guidelines.mdc @@ -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 \ No newline at end of file +- Always generate semantic HTML diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 629bfe6a..45c1b426 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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. diff --git a/app/controllers/transactions_controller.rb b/app/controllers/transactions_controller.rb index 77044542..72980dc9 100644 --- a/app/controllers/transactions_controller.rb +++ b/app/controllers/transactions_controller.rb @@ -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) diff --git a/app/helpers/forms_helper.rb b/app/helpers/forms_helper.rb index 52b3bb5f..dfa5c3b5 100644 --- a/app/helpers/forms_helper.rb +++ b/app/helpers/forms_helper.rb @@ -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 diff --git a/app/javascript/controllers/transaction_form_controller.js b/app/javascript/controllers/transaction_form_controller.js new file mode 100644 index 00000000..d4ad50b1 --- /dev/null +++ b/app/javascript/controllers/transaction_form_controller.js @@ -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') + } + } +} diff --git a/app/views/shared/_transaction_type_tabs.html.erb b/app/views/shared/_transaction_type_tabs.html.erb new file mode 100644 index 00000000..0c55b6cc --- /dev/null +++ b/app/views/shared/_transaction_type_tabs.html.erb @@ -0,0 +1,24 @@ +
+ <% 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 %> +
diff --git a/app/views/transactions/_form.html.erb b/app/views/transactions/_form.html.erb index cbe9e717..19c9acaa 100644 --- a/app/views/transactions/_form.html.erb +++ b/app/views/transactions/_form.html.erb @@ -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 %>
-
- <%= 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 %> -
+ <%= 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" %>
<%= 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 %>
@@ -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 %>
diff --git a/app/views/transfers/_form.html.erb b/app/views/transfers/_form.html.erb index 8b276bef..26344579 100644 --- a/app/views/transfers/_form.html.erb +++ b/app/views/transfers/_form.html.erb @@ -7,22 +7,7 @@ <% end %>
-
- <%= 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 %> -
+ <%= render "shared/transaction_type_tabs", active_tab: "transfer" %>