@@ -261,7 +267,7 @@ defmodule SpendableWeb.Live.Budgets do
scope = socket.assigns.current_scope
selected_month = socket.assigns[:selected_month] || Date.beginning_of_month(Date.utc_today())
summary = Budgets.calculate_month_summary(scope, selected_month, search: socket.assigns[:search])
- listed = maybe_add_credit_cards(summary.budgets, scope, summary.current_month)
+ listed = listed_budgets(summary.budgets, scope, summary.current_month)
socket
|> assign(:spendable, summary.spendable)
@@ -276,29 +282,49 @@ defmodule SpendableWeb.Live.Budgets do
|> assign(:changeset, nil)
end
- # Card debt is not a budget, but it reads as one on this page: a negative balance to cover.
- # It only makes sense against the current month, since it is what is owed right now.
- defp maybe_add_credit_cards(budgets, _scope, false = _current_month_is_selected), do: budgets
+ # A past month has no Spendable figure above the list, so the budget is the only place left to
+ # read what came out of it.
+ defp listed_budgets(budgets, _scope, false = _current_month_is_selected), do: by_type(budgets)
+
+ defp listed_budgets([], _scope, _current_month_is_selected), do: []
- defp maybe_add_credit_cards([spendable | budgets], scope, _current_month_is_selected) do
+ # Card debt is not a budget, but it reads as one on this page: a negative balance to cover, and
+ # no id because there is no row behind it. Spendable is the figure the page opens with, so
+ # listing it again only says the same word twice about two different numbers.
+ defp listed_budgets(budgets, scope, _current_month_is_selected) do
credit_cards = %Budget{
name: "Credit Cards",
type: :envelope,
balance: scope |> Banks.calculate_credit_card_balance() |> Decimal.negate()
}
- [spendable, credit_cards | budgets]
+ [credit_cards | budgets |> Enum.reject(&(&1.name == "Spendable")) |> by_type()]
end
- defp maybe_add_credit_cards([], _scope, _current_month_is_selected), do: []
+ # Envelopes, then what is only tracked, alphabetical inside each. Grouping them by what they are
+ # does the work a heading over each group would, without the headings. Goals go last: a goal is
+ # money going in rather than out, so it is not what the month is about.
+ defp by_type(budgets), do: Enum.sort_by(budgets, &{type_order(&1.type), &1.name})
+
+ defp type_order(:envelope), do: 0
+ defp type_order(:tracking), do: 1
+ defp type_order(:goal), do: 2
defp build_cards(budgets, spent, current_month_is_selected) do
Enum.map(budgets, fn budget ->
spent_here = spent |> Map.get(budget.id, Decimal.new(0)) |> Decimal.abs()
+ credit_cards? = is_nil(budget.id)
+ card = build_budget_card(budget, spent_here, current_month_is_selected)
- budget
- |> build_budget_card(spent_here, current_month_is_selected)
- |> Map.merge(%{budget: budget, pill: pill(budget.type), pill_class: pill_class(budget.type)})
+ # Card debt is not an envelope with something left in it, it is what is owed right now, and
+ # the pill calling it one is only there to satisfy the card it is built from.
+ Map.merge(card, %{
+ budget: budget,
+ label: if(credit_cards?, do: "BALANCE", else: card.label),
+ pill: if(credit_cards?, do: nil, else: pill(budget.type)),
+ pill_class: pill_class(budget.type),
+ editable?: not credit_cards? and budget.name != "Spendable"
+ })
end)
end
diff --git a/lib/spendable_web/live/budgets_test.exs b/lib/spendable_web/live/budgets_test.exs
index 31576412..45c019b0 100644
--- a/lib/spendable_web/live/budgets_test.exs
+++ b/lib/spendable_web/live/budgets_test.exs
@@ -226,6 +226,67 @@ defmodule SpendableWeb.Live.BudgetsTest do
refute html =~ "Credit Cards"
end
+ # Spendable is the figure the page opens with, so a card saying it again is the same word twice
+ # about two different numbers.
+ test "leaves the Spendable card off the current month", %{conn: conn, scope: scope} do
+ {:ok, _transaction} =
+ Transactions.create_transaction(scope, %{
+ "amount" => "-20.00",
+ "date" => Date.utc_today(),
+ "name" => "Groceries"
+ })
+
+ {:ok, view, html} = live(conn, ~p"/budgets")
+
+ assert html =~ "Spendable"
+ refute has_element?(view, "h2", "Spendable")
+ end
+
+ # A past month has no Spendable figure above the list, so the budget is the only place left.
+ test "keeps the Spendable card on a past month", %{conn: conn, scope: scope} do
+ {:ok, _transaction} =
+ Transactions.create_transaction(scope, %{
+ "amount" => "-20.00",
+ "date" => Date.utc_today(),
+ "name" => "Groceries"
+ })
+
+ last_month = Date.utc_today() |> Date.beginning_of_month() |> Date.add(-1)
+
+ {:ok, view, _html} = live(conn, ~p"/budgets")
+
+ render_click(view, "select_month", %{"month" => Date.to_iso8601(last_month)})
+
+ assert has_element?(view, "h2", "Spendable")
+ end
+
+ # Card debt reads the bank accounts and Spendable is whatever is left over. Neither is a card
+ # anyone edits.
+ test "offers no edit on the credit card total", %{conn: conn, scope: scope} do
+ {:ok, _budget} = Budgets.create_budget(scope, %{"name" => "Groceries"})
+
+ {:ok, view, html} = live(conn, ~p"/budgets")
+
+ assert html =~ "BALANCE"
+ assert has_element?(view, ~s(button[aria-label="Edit Groceries"]))
+ refute has_element?(view, ~s(button[aria-label="Edit Credit Cards"]))
+ end
+
+ # Envelopes, then what is only tracked, then goals - the grouping does the work a heading would.
+ test "orders the cards by type, with goals last", %{conn: conn, scope: scope} do
+ {:ok, _goal} = Budgets.create_budget(scope, %{"name" => "Vacation", "type" => "goal"})
+ {:ok, _tracking} = Budgets.create_budget(scope, %{"name" => "Amazon", "type" => "tracking"})
+ {:ok, _envelope} = Budgets.create_budget(scope, %{"name" => "Rent", "type" => "envelope"})
+
+ {:ok, _view, html} = live(conn, ~p"/budgets")
+
+ assert [rent, amazon, vacation] =
+ Enum.map(["Rent", "Amazon", "Vacation"], &(:binary.match(html, &1) |> elem(0)))
+
+ assert rent < amazon
+ assert amazon < vacation
+ end
+
test "filters the list by the search box", %{conn: conn, scope: scope} do
{:ok, _groceries} = Budgets.create_budget(scope, %{"name" => "Groceries"})
{:ok, _rent} = Budgets.create_budget(scope, %{"name" => "Rent"})
diff --git a/lib/spendable_web/live/transactions.ex b/lib/spendable_web/live/transactions.ex
index 752167a6..22b6a49e 100644
--- a/lib/spendable_web/live/transactions.ex
+++ b/lib/spendable_web/live/transactions.ex
@@ -1,6 +1,7 @@
defmodule SpendableWeb.Live.Transactions do
use SpendableWeb, :live_view
+ import SpendableWeb.Utils.AccountLabel
import SpendableWeb.Utils.FormOptions
alias Spendable.Budgets
@@ -75,7 +76,7 @@ defmodule SpendableWeb.Live.Transactions do
:for={{id, transaction} <- @streams.transactions}
id={id}
class={[
- if(transaction.excluded or transaction.transfer_id, do: "opacity-40"),
+ if(transaction.excluded, do: "opacity-40"),
"group flex flex-row items-center gap-x-3 p-2"
]}
>
@@ -111,10 +112,7 @@ defmodule SpendableWeb.Live.Transactions do
class="h-5 w-5 shrink-0 rounded-sm"
/>
- {bank_account(transaction).name}
-
-
- {mask(bank_account(transaction).number)}
+ {account_label(bank_account(transaction).name, bank_account(transaction).number)}