1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-07-19 05:09:38 +02:00

Basic transaction categories CRUD actions (inline) (#601)

* Fix dropdown issues and add dummy transaction category modal

* Minor namings tweaks

* Add search type

* Use new menu controller

* Complete basic transaction category inline CRUD actions

* Fix lint error

---------

Co-authored-by: Jakub Kottnauer <jk@jakubkottnauer.com>
This commit is contained in:
Zach Gollwitzer 2024-04-04 17:29:50 -04:00 committed by GitHub
parent 315c4bf1ec
commit d29d465a3c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 254 additions and 101 deletions

View file

@ -0,0 +1,34 @@
class Transactions::CategoriesController < ApplicationController
before_action :set_category, only: [ :update, :destroy ]
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")
end
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
def destroy
@category.destroy!
redirect_to transactions_path, notice: t(".success")
end
private
def set_category
@category = Current.family.transaction_categories.find(params[:id])
end
def category_params
params.require(:transaction_category).permit(:name, :name, :color)
end
end

View file

@ -0,0 +1,2 @@
module Transactions::CategoriesHelper
end

View file

@ -6,11 +6,25 @@ import { Controller } from "@hotwired/stimulus";
* - If you need a form-enabled "select" element, use the "listbox" controller instead.
*/
export default class extends Controller {
static targets = ["button", "content"];
static targets = [
"button",
"content",
"submenu",
"submenuButton",
"submenuContent",
];
static values = {
show: { type: Boolean, default: false },
showSubmenu: { type: Boolean, default: false },
};
initialize() {
this.show = this.showValue;
this.showSubmenu = this.showSubmenuValue;
}
connect() {
this.show = false;
this.contentTarget.classList.add("hidden"); // Initially hide the popover
this.buttonTarget.addEventListener("click", this.toggle);
this.element.addEventListener("keydown", this.handleKeydown);
document.addEventListener("click", this.handleOutsideClick);
@ -38,11 +52,6 @@ export default class extends Controller {
handleKeydown = (event) => {
switch (event.key) {
case " ":
event.preventDefault(); // Prevent the default action to avoid scrolling
if (document.activeElement === this.buttonTarget) {
this.toggle();
}
case "Escape":
this.close();
this.buttonTarget.focus(); // Bring focus back to the button

View file

@ -8,12 +8,25 @@ import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
static classes = ["active"];
static targets = ["option", "button", "list", "input", "buttonText"];
static values = { selected: String };
initialize() {
this.show = false;
const selectedElement = this.optionTargets.find(
(option) => option.dataset.value === this.selectedValue
);
if (selectedElement) {
this.updateAriaAttributesAndClasses(selectedElement);
this.syncButtonTextWithInput();
}
}
connect() {
this.show = false;
this.syncButtonTextWithInput();
this.listTarget.classList.add("hidden");
this.buttonTarget.addEventListener("click", this.toggleList);
if (this.hasButtonTarget) {
this.buttonTarget.addEventListener("click", this.toggleList);
}
this.element.addEventListener("keydown", this.handleKeydown);
document.addEventListener("click", this.handleOutsideClick);
this.element.addEventListener("turbo:load", this.handleTurboLoad);
@ -22,8 +35,15 @@ export default class extends Controller {
disconnect() {
this.element.removeEventListener("keydown", this.handleKeydown);
document.removeEventListener("click", this.handleOutsideClick);
this.buttonTarget.removeEventListener("click", this.toggleList);
this.element.removeEventListener("turbo:load", this.handleTurboLoad);
if (this.hasButtonTarget) {
this.buttonTarget.removeEventListener("click", this.toggleList);
}
}
selectedValueChanged() {
this.syncButtonTextWithInput();
}
handleOutsideClick = (event) => {
@ -42,7 +62,10 @@ export default class extends Controller {
case " ":
case "Enter":
event.preventDefault(); // Prevent the default action to avoid scrolling
if (document.activeElement === this.buttonTarget) {
if (
this.hasButtonTarget &&
document.activeElement === this.buttonTarget
) {
this.toggleList();
} else {
this.selectOption(event);
@ -58,7 +81,9 @@ export default class extends Controller {
break;
case "Escape":
this.close();
this.buttonTarget.focus(); // Bring focus back to the button
if (this.hasButtonTarget) {
this.buttonTarget.focus(); // Bring focus back to the button
}
break;
case "Tab":
this.close();
@ -85,6 +110,8 @@ export default class extends Controller {
}
toggleList = () => {
if (!this.hasButtonTarget) return; // Ensure button target is present before toggling
this.show = !this.show;
this.listTarget.classList.toggle("hidden", !this.show);
this.buttonTarget.setAttribute("aria-expanded", this.show.toString());
@ -99,14 +126,24 @@ export default class extends Controller {
};
close() {
this.show = false;
this.listTarget.classList.add("hidden");
this.buttonTarget.setAttribute("aria-expanded", "false");
if (this.hasButtonTarget) {
this.show = false;
this.listTarget.classList.add("hidden");
this.buttonTarget.setAttribute("aria-expanded", "false");
}
}
selectOption(event) {
const selectedOption =
event.type === "keydown" ? document.activeElement : event.currentTarget;
this.updateAriaAttributesAndClasses(selectedOption);
if (this.inputTarget.value !== selectedOption.getAttribute("data-value")) {
this.updateInputValueAndEmitEvent(selectedOption);
}
this.close(); // Close the list after selection
}
updateAriaAttributesAndClasses(selectedOption) {
this.optionTargets.forEach((option) => {
option.setAttribute("aria-selected", "false");
option.setAttribute("tabindex", "-1");
@ -115,14 +152,15 @@ export default class extends Controller {
selectedOption.classList.add(...this.activeClasses);
selectedOption.setAttribute("aria-selected", "true");
selectedOption.focus();
this.close(); // Close the list after selection
}
updateInputValueAndEmitEvent(selectedOption) {
// Update the hidden input's value
const selectedValue = selectedOption.getAttribute("data-value");
this.inputTarget.value = selectedValue;
this.syncButtonTextWithInput();
// Auto-submit controller listens for this even to auto-submit
// Emit an input event for auto-submit functionality
const inputEvent = new Event("input", {
bubbles: true,
cancelable: true,
@ -134,7 +172,7 @@ export default class extends Controller {
const matchingOption = this.optionTargets.find(
(option) => option.getAttribute("data-value") === this.inputTarget.value
);
if (matchingOption) {
if (matchingOption && this.hasButtonTextTarget) {
this.buttonTextTarget.textContent = matchingOption.textContent.trim();
}
}

View file

@ -3,21 +3,6 @@
* Stimulus controllers to reference our color palette. Mostly used for D3 charts.
*/
export const categoryColors = [
"#e99537",
"#4da568",
"#6471eb",
"#db5a54",
"#df4e92",
"#c44fe9",
"#eb5429",
"#61c9ea",
"#805dee",
"#6ad28a"
]
export const categoryDefaultColor = "#737373"
export default {
transparent: "transparent",
current: "currentColor",

View file

@ -1,20 +1,24 @@
class Transaction::Category < ApplicationRecord
has_many :transactions
has_many :transactions, dependent: :nullify
belongs_to :family
validates :name, :color, :family, presence: true
before_update :clear_internal_category, if: :name_changed?
COLORS = %w[#e99537 #4da568 #6471eb #db5a54 #df4e92 #c44fe9 #eb5429 #61c9ea #805dee #6ad28a]
UNCATEGORIZED_COLOR = "#737373"
DEFAULT_CATEGORIES = [
{ internal_category: "income", color: "#e99537" },
{ internal_category: "food_and_drink", color: "#4da568" },
{ internal_category: "entertainment", color: "#6471eb" },
{ internal_category: "personal_care", color: "#db5a54" },
{ internal_category: "general_services", color: "#df4e92" },
{ internal_category: "auto_and_transport", color: "#c44fe9" },
{ internal_category: "rent_and_utilities", color: "#eb5429" },
{ internal_category: "home_improvement", color: "#61c9ea" }
{ internal_category: "income", color: COLORS[0] },
{ internal_category: "food_and_drink", color: COLORS[1] },
{ internal_category: "entertainment", color: COLORS[2] },
{ internal_category: "personal_care", color: COLORS[3] },
{ internal_category: "general_services", color: COLORS[4] },
{ internal_category: "auto_and_transport", color: COLORS[5] },
{ internal_category: "rent_and_utilities", color: COLORS[6] },
{ internal_category: "home_improvement", color: COLORS[7] }
]
def self.ransackable_attributes(auth_object = nil)

View file

@ -37,7 +37,7 @@
</button>
<div
data-menu-target="content"
class="absolute min-w-[200px] z-10 top-10 right-0 bg-white p-1 rounded-sm shadow-xs border border-alpha-black-25 w-fit"
class="hidden absolute min-w-[200px] z-10 top-10 right-0 bg-white p-1 rounded-sm shadow-xs border border-alpha-black-25 w-fit"
>
<%= link_to edit_settings_path, class: "flex gap-1 items-center hover:bg-gray-50 rounded-md p-2" do %>
<%= lucide_icon("pencil-line", class: "w-5 h-5 text-gray-500 shrink-0") %>

View file

@ -1,11 +1,11 @@
<%# locals: (value: 'all') -%>
<%# locals: (value: 'last_30_days') -%>
<% options = [['7D', 'last_7_days'], ['1M', 'last_30_days'], ["1Y", "last_365_days"], ['All', 'all']] %>
<div data-controller="select" data-select-active-class="bg-alpha-black-50" class="relative">
<div data-controller="select" data-select-active-class="bg-alpha-black-50" class="relative" data-select-selected-value="<%= value %>">
<button type="button" data-select-target="button" class="flex items-center gap-1 w-full border border-alpha-black-100 shadow-xs rounded-lg text-sm p-2 cursor-pointer">
<span data-select-target="buttonText" class="text-gray-900 text-sm"><%= options.find { |option| option[1] == value }[0] %></span>
<%= lucide_icon("chevron-down", class: "w-5 h-5 text-gray-500") %>
</button>
<input type="hidden" name="period" value="<%= value %>" data-select-target="input" data-auto-submit-form-target="auto">
<input type="hidden" name="period" data-select-target="input" data-auto-submit-form-target="auto">
<ul data-select-target="list" class="hidden absolute z-10 top-10 right-0 border border-alpha-black-25 bg-white rounded-lg shadow-xs">
<% options.each do |label, value| %>
<li tabindex="0" data-select-target="option" data-action="click->select#selectOption" data-value="<%= value %>" class="text-sm text-gray-900 rounded-lg cursor-pointer hover:bg-alpha-black-50 px-5 py-1">

View file

@ -1,45 +0,0 @@
<%# locals: (transaction:) %>
<div class="relative" data-controller="menu">
<button data-menu-target="button" class="flex">
<%= render partial: "shared/category_badge", locals: transaction.category.nil? ? {} : { name: transaction.category.name, color: transaction.category.color } %>
</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">
<div class="flex flex-col" data-controller="list-filter">
<div class="grow p-1.5">
<div class="relative flex items-center bg-white border border-gray-200 rounded-lg">
<input placeholder="Search" type="search" class="placeholder:text-sm placeholder:text-gray-500 font-normal h-10 relative pl-10 w-full border-none rounded-lg" data-list-filter-target="input" data-action="list-filter#filter" />
<%= lucide_icon("search", class: "w-5 h-5 text-gray-500 ml-2 absolute inset-0 transform top-1/2 -translate-y-1/2") %>
</div>
</div>
<%= form_with model: transaction, namespace: dom_id(transaction), html: { data: { controller: "auto-submit-form", list_filter_target: "list" }, class: "flex flex-col gap-0.5 p-1.5 mt-0.5 mr-2 max-h-64 overflow-y-scroll scrollbar" } do |form| %>
<div class="py-8 pl-4 mt-0.5 mr-2 text-gray-500 hidden" data-list-filter-target="emptyMessage">
No categories found
</div>
<% Current.family.transaction_categories.each do |category| %>
<% is_selected = category.id == transaction.category.try(:id) %>
<%= content_tag :div, class: ["filterable-item flex items-center hover:bg-gray-25 border-none rounded-lg px-2 py-1 group", { "bg-gray-25": is_selected }], data: { filter_name: category.name } do %>
<%= form.radio_button :category_id, category.id, class: "hidden", data: { auto_submit_form_target: "auto" } %>
<%= label dom_id(transaction), :transaction_category_id, value: category.id, 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: "shared/category_badge", locals: { name: category.name, color: category.color } %>
<% end %>
<button class="ml-auto flex items-center justify-center hover:bg-gray-50 w-8 h-8 rounded-lg invisible group-hover:visible cursor-not-allowed" type="button" disabled>
<%= lucide_icon("more-horizontal", class: "w-5 h-5 text-gray-500") %>
</button>
<% end %>
<% end %>
<% end %>
<hr/>
<div class="p-1.5 w-full">
<button class="cursor-not-allowed flex text-sm font-medium items-center gap-2 text-gray-500 w-full rounded-lg p-2 hover:bg-gray-100" disabled>
<%= lucide_icon("plus", class: "w-5 h-5") %>
Add new
</button>
</div>
</div>
</div>
</div>
</div>

View file

@ -11,7 +11,7 @@
<%= lucide_icon("list-filter", class: "w-5 h-5 text-gray-500") %>
<p class="text-sm font-medium text-gray-900">Filter</p>
</button>
<div data-menu-target="content" class="absolute z-10 top-12 right-0 border border-alpha-black-25 bg-white rounded-lg shadow-xs min-w-[450px]">
<div data-menu-target="content" class="hidden absolute z-10 top-12 right-0 border border-alpha-black-25 bg-white rounded-lg shadow-xs min-w-[450px]">
<div data-controller="tabs" data-tabs-active-class="border-b-2 border-b-black text-gray-900" data-tabs-default-tab-value="txn-account-filter">
<div class="flex items-center px-3 text-sm font-medium text-gray-500 gap-4 border-b border-b-alpha-black-50">
<button class="py-2 border-b-2" type="button" data-id="txn-account-filter" data-tabs-target="btn" data-action="tabs#select">Account</button>

View file

@ -6,7 +6,7 @@
</div>
<% end %>
<div class="w-48">
<%= render partial: "transactions/category_dropdown", locals: { transaction: } %>
<%= render partial: "transactions/categories/menu", locals: { transaction: } %>
</div>
<div>
<p><%= transaction.account.name %></p>

View file

@ -1,4 +1,4 @@
<%# locals: (name: "Uncategorized", color: "#737373") %>
<%# 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 cursor-pointer" style="background-color: <%= background_color %>; border-color: <%= border_color %>; color: <%= color %>"><%= name %></span>

View file

@ -0,0 +1,39 @@
<%# locals: (transaction:) %>
<div class="relative" data-controller="menu">
<button data-menu-target="button" class="flex">
<%= render partial: "transactions/categories/badge", locals: transaction.category.nil? ? {} : { name: transaction.category.name, color: transaction.category.color } %>
</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">
<div class="flex flex-col relative" data-controller="list-filter">
<div class="grow p-1.5">
<div class="relative flex items-center bg-white border border-gray-200 rounded-lg">
<input placeholder="Search" type="search" class="placeholder:text-sm placeholder:text-gray-500 font-normal h-10 relative pl-10 w-full border-none rounded-lg" data-list-filter-target="input" data-action="list-filter#filter" />
<%= lucide_icon("search", class: "w-5 h-5 text-gray-500 ml-2 absolute inset-0 transform top-1/2 -translate-y-1/2") %>
</div>
</div>
<div data-list-filter-target="list" class="flex flex-col gap-0.5 p-1.5 mt-0.5 mr-2 max-h-64 overflow-y-scroll scrollbar">
<div class="pb-2 pl-4 mr-2 text-gray-500 hidden" data-list-filter-target="emptyMessage">
No categories found
</div>
<% sorted_categories = Current.family.transaction_categories.sort_by { |category| category.id == transaction.category_id ? 0 : 1 } %>
<% sorted_categories.each do |category| %>
<%= render partial: "transactions/categories/dropdown/row", locals: { category:, transaction: } %>
<% 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>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,15 @@
<%# 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>

View file

@ -0,0 +1,28 @@
<%# 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 %>

View file

@ -0,0 +1,18 @@
<%# 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 } %>
<% 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 %>

View file

@ -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" do %>
<%= render partial: "shared/category_badge", locals: { name: transaction_category.name, color: transaction_category.color } %>
<%= render partial: "transactions/categories/badge", locals: { name: transaction_category.name, color: transaction_category.color } %>
<%end%>
</div>
<% end %>

View file

@ -1,6 +1,15 @@
---
en:
transactions:
categories:
create:
error: Error creating transaction category
success: New transaction category created successfully
destroy:
success: Transaction category deleted successfully
update:
error: Error updating transaction category
success: Transaction category updated successfully
create:
success: New transaction created successfully
destroy:

View file

@ -11,6 +11,10 @@ Rails.application.routes.draw do
match "search" => "transactions#search", on: :collection, via: [ :get, :post ], as: :search
end
namespace :transactions do
resources :categories
end
resources :accounts, shallow: true do
post :sync, on: :member
resources :valuations

View file

@ -0,0 +1,6 @@
class ChangeTransactionCategoryDeleteBehavior < ActiveRecord::Migration[7.2]
def change
remove_foreign_key :transactions, :transaction_categories, column: :category_id
add_foreign_key :transactions, :transaction_categories, column: :category_id, on_delete: :nullify
end
end

4
db/schema.rb generated
View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.2].define(version: 2024_04_03_192649) do
ActiveRecord::Schema[7.2].define(version: 2024_04_04_112829) do
# These are extensions that must be enabled in order to support this database
enable_extension "pgcrypto"
enable_extension "plpgsql"
@ -253,7 +253,7 @@ ActiveRecord::Schema[7.2].define(version: 2024_04_03_192649) do
add_foreign_key "accounts", "families"
add_foreign_key "transaction_categories", "families"
add_foreign_key "transactions", "accounts", on_delete: :cascade
add_foreign_key "transactions", "transaction_categories", column: "category_id"
add_foreign_key "transactions", "transaction_categories", column: "category_id", on_delete: :nullify
add_foreign_key "users", "families"
add_foreign_key "valuations", "accounts", on_delete: :cascade
end

View file

@ -5,6 +5,8 @@ namespace :demo_data do
family.accounts.delete_all
ExchangeRate.delete_all
family.transaction_categories.delete_all
Transaction::Category.create_default_categories(family)
user = User.find_or_create_by(email: "user@maybe.local") do |u|
u.password = "password"
@ -46,8 +48,6 @@ namespace :demo_data do
puts "Loaded mock exchange rates for last 60 days"
Transaction::Category.create_default_categories(family) if family.transaction_categories.empty?
# ========== Accounts ================
empty_account = Account.create(name: "Demo Empty Account", family: family, accountable: Account::Depository.new, balance: 500, currency: "USD")
multi_currency_checking = Account.create(name: "Demo Multi-Currency Checking", family: family, accountable: Account::Depository.new, balance: 4000, currency: "EUR")

View file

@ -0,0 +1,7 @@
require "test_helper"
class Transactions::CategoriesControllerTest < ActionDispatch::IntegrationTest
# test "the truth" do
# assert true
# end
end