Skip to content

Commit

Permalink
Improve budget handling
Browse files Browse the repository at this point in the history
  • Loading branch information
binary-koan committed Mar 20, 2024
1 parent 18d1355 commit 526627a
Show file tree
Hide file tree
Showing 11 changed files with 78 additions and 47 deletions.
2 changes: 1 addition & 1 deletion app/concepts/money.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def amount_decimal
end

def formatted
"#{sign}#{currency.symbol}#{format('%.2f', amount_decimal.abs)}"
"#{sign}#{currency.symbol}#{format("%.#{currency.decimal_digits}f", amount_decimal.abs)}"
end

def formatted_short
Expand Down
19 changes: 13 additions & 6 deletions app/graphql/mutations/category_update.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,19 @@ def resolve(id:, category_input:)
category_input[:budget_cents] != category.current_budget&.budget_cents ||
category_input[:budget_currency_id] != category.current_budget&.currency_id
)
category.current_budget&.update!(date_to: Time.zone.now.beginning_of_month)
category.category_budgets.create!(
date_from: Time.zone.now.beginning_of_month,
budget_cents: category_input[:budget_cents],
currency_id: category_input[:budget_currency_id]
)
if category.current_budget&.date_from == Time.zone.now.beginning_of_month
category.current_budget.update!(
budget_cents: category_input[:budget_cents],
currency_id: category_input[:budget_currency_id]
)
else
category.current_budget&.update!(date_to: Time.zone.now.beginning_of_month)
category.category_budgets.create!(
date_from: Time.zone.now.beginning_of_month,
budget_cents: category_input[:budget_cents],
currency_id: category_input[:budget_currency_id]
)
end
end
end

Expand Down
12 changes: 8 additions & 4 deletions app/graphql/resolvers/budget_resolver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,17 @@ def resolve(year:, month:, currency_id: nil)
spending_categories = transactions.select(&:expense?).map(&:category).uniq.sort_by { |category| category&.sort_order || -1 }

all_categories = spending_categories.map do |category|
budget = ExchangeRates::ConvertMoney.new([category.current_budget.budget], to_currency: output_currency).call.first if category.current_budget
amount_spent = Monies.new(
transactions.select(&:expense?).select { |transaction| transaction.category == category }.map(&:amount_in_currency),
output_currency
).sum.abs

{
id: "#{year}-#{month}-#{category&.id || "uncategorized"}",
category:,
amount_spent: Monies.new(
transactions.select(&:expense?).select { |transaction| transaction.category == category }.map(&:amount_in_currency),
output_currency
).sum.abs
amount_spent:,
remaining_budget: (budget - amount_spent if budget)
}
end

Expand Down
1 change: 1 addition & 0 deletions app/graphql/types/category_budget_spending_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ class CategoryBudgetSpendingType < Types::BaseObject
field :id, ID, null: false
field :category, Types::CategoryType, null: true
field :amount_spent, Types::MoneyType, null: false
field :remaining_budget, Types::MoneyType, null: true
end
end
2 changes: 1 addition & 1 deletion app/models/category_budget.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ def self.for_date(date)
end

def budget
Money.new(amount_cents: budget_cents, currency:)
MoneyOnDate.new(amount_cents: budget_cents, currency:, date: date_from)
end
end
1 change: 1 addition & 0 deletions schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ type CategoryBudgetSpending {
amountSpent: Money!
category: Category
id: ID!
remainingBudget: Money
}

"""
Expand Down
7 changes: 5 additions & 2 deletions web/src/components/budgets/BudgetGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ interface BudgetGroupProps {
color?: CategoryColor
budget?: { amountDecimal: number; formatted: string } | null | false
total: { amountDecimal: number; formatted: string }
remaining?: { amountDecimal: number; formatted: string } | null
}>
}

Expand Down Expand Up @@ -52,7 +53,7 @@ const BudgetGroup: Component<BudgetGroupProps> = (props) => {
</Show>

<For each={props.items}>
{({ categoryId, indicator, name, color, total, budget }) => (
{({ categoryId, indicator, name, color, total, remaining, budget }) => (
<div class="flex items-center pb-4">
<CategoryIndicator class="mr-3 h-6 w-6" {...indicator} />
<div class="min-w-0 flex-1">
Expand All @@ -73,6 +74,8 @@ const BudgetGroup: Component<BudgetGroupProps> = (props) => {
style={{
width: budget
? `${Math.min(total.amountDecimal / budget.amountDecimal, 1) * 100}%`
: total.amountDecimal > 0
? "100%"
: 0
}}
/>
Expand All @@ -92,7 +95,7 @@ const BudgetGroup: Component<BudgetGroupProps> = (props) => {
{((total.amountDecimal / budget.amountDecimal) * 100).toFixed(0)}% spent
</span>
<span class="ml-auto text-xs text-gray-600">
{budget.formatted} budget
{remaining?.formatted} left / {budget.formatted} budget
</span>
</>
) : (
Expand Down
27 changes: 16 additions & 11 deletions web/src/components/budgets/Budgets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,21 @@ export const Budgets: Component<{
({ category }) => category?.id || null
)
})}
items={props.data.budget.regularCategories.categories.map(({ category, amountSpent }) => ({
indicator: {
transactionsFilter={transactionsFilter()}
items={props.data.budget.regularCategories.categories.map(
({ category, amountSpent, remainingBudget }) => ({
indicator: {
color: category?.color as CategoryColor,
icon: category?.icon ? namedIcons[category.icon] : undefined
},
name: category?.name || "Uncategorized",
color: category?.color as CategoryColor,
icon: category?.icon ? namedIcons[category.icon] : undefined
},
name: category?.name || "Uncategorized",
color: category?.color as CategoryColor,
budget: category?.budget?.budget,
total: amountSpent,
categoryId: category?.id || null
}))}
budget: category?.budget?.budget,
total: amountSpent,
remaining: remainingBudget,
categoryId: category?.id || null
})
)}
/>

<BudgetGroup
Expand All @@ -68,7 +72,7 @@ export const Budgets: Component<{
})}
transactionsFilter={transactionsFilter()}
items={props.data.budget.irregularCategories.categories.map(
({ category, amountSpent }) => ({
({ category, amountSpent, remainingBudget }) => ({
indicator: {
color: category?.color as CategoryColor,
icon: category?.icon ? namedIcons[category.icon] : undefined
Expand All @@ -77,6 +81,7 @@ export const Budgets: Component<{
color: category?.color as CategoryColor,
budget: category?.budget?.budget,
total: amountSpent,
remaining: remainingBudget,
categoryId: category?.id || null
})
)}
Expand Down
43 changes: 22 additions & 21 deletions web/src/components/categories/CategoryForm.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createForm, Form, getValue } from "@modular-forms/solid"
import { repeat } from "lodash"
import { Component } from "solid-js"
import { Component, Show } from "solid-js"
import { CreateCategoryMutationVariables, FullCategoryFragment } from "../../graphql-types"
import { useCurrenciesQuery } from "../../graphql/queries/currenciesQuery"
import { Button } from "../base/Button"
Expand Down Expand Up @@ -32,22 +32,21 @@ const CategoryForm: Component<{
color: props.category?.color,
icon: props.category?.icon,
budget: props.category?.budget?.budget.amountDecimal.toString(),
budgetCurrencyId: props.category?.budget?.currency.id,
budgetCurrencyId: props.category?.budget?.currency.id || "",
isRegular: props.category?.isRegular != null ? props.category.isRegular : true
}
})

const selectedCurrency = () =>
currencies()?.currencies.find((currency) => currency.id === getValue(form, "budgetCurrencyId"))

const onSave = (data: CategoryFormValues) => {
const onSave = ({ budget, budgetCurrencyId, ...data }: CategoryFormValues) => {
props.onSave(
{
...data,
budgetCents: data.budget
? Math.floor(
parseFloat(data.budget || "0") * 10 ** (selectedCurrency()?.decimalDigits || 0)
)
budgetCurrencyId: budgetCurrencyId || null,
budgetCents: budget
? Math.floor(parseFloat(budget || "0") * 10 ** (selectedCurrency()?.decimalDigits || 0))
: null,
isRegular: Boolean(data.isRegular)
},
Expand Down Expand Up @@ -83,26 +82,28 @@ const CategoryForm: Component<{
of={form}
label="Budget Currency"
name="budgetCurrencyId"
options={
options={[{ value: "", content: "None" }].concat(
currencies()?.currencies?.map((currency) => ({
value: currency.id,
content: `${currency.code} (${currency.name})`
})) || []
}
)}
/>

<FormInputGroup
of={form}
label="Budget"
name="budget"
inputmode="decimal"
before={<InputAddon>{selectedCurrency()?.symbol}</InputAddon>}
step={
selectedCurrency()?.decimalDigits
? `0.${repeat("0", selectedCurrency()!.decimalDigits - 1)}1`
: "1"
}
/>
<Show when={selectedCurrency()}>
<FormInputGroup
of={form}
label="Budget"
name="budget"
inputmode="decimal"
before={<InputAddon>{selectedCurrency()?.symbol}</InputAddon>}
step={
selectedCurrency()?.decimalDigits
? `0.${repeat("0", selectedCurrency()!.decimalDigits - 1)}1`
: "1"
}
/>
</Show>

<FormSwitch of={form} label="Regular" name="isRegular" />

Expand Down
3 changes: 2 additions & 1 deletion web/src/graphql-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ export type CategoryBudgetSpending = {
amountSpent: Money;
category?: Maybe<Category>;
id: Scalars['ID']['output'];
remainingBudget?: Maybe<Money>;
};

/** Autogenerated input type of CategoryCreate */
Expand Down Expand Up @@ -934,7 +935,7 @@ export type BudgetQueryVariables = Exact<{
}>;


export type BudgetQuery = { __typename?: 'Query', budget: { __typename?: 'MonthBudget', id: string, month: number, income: { __typename?: 'Money', amountDecimal: number, formatted: string }, totalSpending: { __typename?: 'Money', amountDecimal: number, formatted: string }, difference: { __typename?: 'Money', amountDecimal: number, formatted: string }, regularCategories: { __typename?: 'CategoryBudgetGroup', totalSpending: { __typename?: 'Money', amountDecimal: number, formatted: string }, categories: Array<{ __typename?: 'CategoryBudgetSpending', id: string, category?: { __typename?: 'Category', id: string, name: string, color: string, icon: string, isRegular: boolean, budget?: { __typename?: 'CategoryBudget', budget: { __typename?: 'Money', amountDecimal: number, formatted: string } } | null } | null, amountSpent: { __typename?: 'Money', amountDecimal: number, formatted: string } }> }, irregularCategories: { __typename?: 'CategoryBudgetGroup', totalSpending: { __typename?: 'Money', amountDecimal: number, formatted: string }, categories: Array<{ __typename?: 'CategoryBudgetSpending', id: string, category?: { __typename?: 'Category', id: string, name: string, color: string, icon: string, isRegular: boolean, budget?: { __typename?: 'CategoryBudget', budget: { __typename?: 'Money', amountDecimal: number, formatted: string } } | null } | null, amountSpent: { __typename?: 'Money', amountDecimal: number, formatted: string } }> } }, income: { __typename?: 'TransactionConnection', nodes: Array<{ __typename?: 'Transaction', id: string, memo: string, amount?: { __typename?: 'Money', formatted: string, amountDecimal: number } | null }> } };
export type BudgetQuery = { __typename?: 'Query', budget: { __typename?: 'MonthBudget', id: string, month: number, income: { __typename?: 'Money', amountDecimal: number, formatted: string }, totalSpending: { __typename?: 'Money', amountDecimal: number, formatted: string }, difference: { __typename?: 'Money', amountDecimal: number, formatted: string }, regularCategories: { __typename?: 'CategoryBudgetGroup', totalSpending: { __typename?: 'Money', amountDecimal: number, formatted: string }, categories: Array<{ __typename?: 'CategoryBudgetSpending', id: string, category?: { __typename?: 'Category', id: string, name: string, color: string, icon: string, isRegular: boolean, budget?: { __typename?: 'CategoryBudget', budget: { __typename?: 'Money', amountDecimal: number, formatted: string } } | null } | null, amountSpent: { __typename?: 'Money', amountDecimal: number, formatted: string }, remainingBudget?: { __typename?: 'Money', amountDecimal: number, formatted: string } | null }> }, irregularCategories: { __typename?: 'CategoryBudgetGroup', totalSpending: { __typename?: 'Money', amountDecimal: number, formatted: string }, categories: Array<{ __typename?: 'CategoryBudgetSpending', id: string, category?: { __typename?: 'Category', id: string, name: string, color: string, icon: string, isRegular: boolean, budget?: { __typename?: 'CategoryBudget', budget: { __typename?: 'Money', amountDecimal: number, formatted: string } } | null } | null, amountSpent: { __typename?: 'Money', amountDecimal: number, formatted: string }, remainingBudget?: { __typename?: 'Money', amountDecimal: number, formatted: string } | null }> } }, income: { __typename?: 'TransactionConnection', nodes: Array<{ __typename?: 'Transaction', id: string, memo: string, amount?: { __typename?: 'Money', formatted: string, amountDecimal: number } | null }> } };

export type CategoriesQueryVariables = Exact<{
today: Scalars['ISO8601Date']['input'];
Expand Down
8 changes: 8 additions & 0 deletions web/src/graphql/queries/budgetQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ export const BUDGET_QUERY = gql`
amountDecimal
formatted
}
remainingBudget {
amountDecimal
formatted
}
}
}
irregularCategories {
Expand All @@ -75,6 +79,10 @@ export const BUDGET_QUERY = gql`
amountDecimal
formatted
}
remainingBudget {
amountDecimal
formatted
}
}
}
}
Expand Down

0 comments on commit 526627a

Please sign in to comment.