Add support for group income (= negative expenses) (#158)

* Allow negative amount for expenses to be entered

- an expense becomes an income
- this does not affect calculations, i.e. an income can be split just like an expense

* Incomes should not be reimbursements

when entering a negative number
- deselect 'isReimbursement'
- hide reimbursement checkbox

* Change captions when entering a negative number

- "expense" becomes "income"
- "paid" becomes "received"

* Format incomes on expense list

- replace "paid by" with "received by"

* Format incomes on "Stats" tab

- a group's or participants balance might be negative
- in this case "spendings" will be "earnings" (display accordingly)
- always display positive numbers
- for active user: highlight spendings/earnings in red/green

* Fix typo

---------

Co-authored-by: Sebastien Castiel <sebastien@castiel.me>
This commit is contained in:
Stefan Hynst
2024-05-30 04:20:04 +02:00
committed by GitHub
parent 3887efd9ee
commit 0c05499107
6 changed files with 75 additions and 55 deletions

View File

@@ -38,7 +38,8 @@ export function ExpenseCard({ expense, currency, groupId }: Props) {
{expense.title}
</div>
<div className="text-xs text-muted-foreground">
Paid by <strong>{expense.paidBy.name}</strong> for{' '}
{expense.amount > 0 ? 'Paid by ' : 'Received by '}
<strong>{expense.paidBy.name}</strong> for{' '}
{expense.paidFor.map((paidFor, index) => (
<Fragment key={index}>
{index !== 0 && <>, </>}

View File

@@ -6,11 +6,12 @@ type Props = {
}
export function TotalsGroupSpending({ totalGroupSpendings, currency }: Props) {
const balance = totalGroupSpendings < 0 ? 'earnings' : 'spendings'
return (
<div>
<div className="text-muted-foreground">Total group spendings</div>
<div className="text-muted-foreground">Total group {balance}</div>
<div className="text-lg">
{formatCurrency(currency, totalGroupSpendings)}
{formatCurrency(currency, Math.abs(totalGroupSpendings))}
</div>
</div>
)

View File

@@ -1,7 +1,7 @@
'use client'
import { getGroup, getGroupExpenses } from '@/lib/api'
import { getTotalActiveUserShare } from '@/lib/totals'
import { formatCurrency } from '@/lib/utils'
import { cn, formatCurrency } from '@/lib/utils'
import { useEffect, useState } from 'react'
type Props = {
@@ -26,8 +26,13 @@ export function TotalsYourShare({ group, expenses }: Props) {
return (
<div>
<div className="text-muted-foreground">Your total share</div>
<div className="text-lg">
{formatCurrency(currency, totalActiveUserShare)}
<div
className={cn(
'text-lg',
totalActiveUserShare < 0 ? 'text-green-600' : 'text-red-600',
)}
>
{formatCurrency(currency, Math.abs(totalActiveUserShare))}
</div>
</div>
)

View File

@@ -2,7 +2,7 @@
import { getGroup, getGroupExpenses } from '@/lib/api'
import { useActiveUser } from '@/lib/hooks'
import { getTotalActiveUserPaidFor } from '@/lib/totals'
import { formatCurrency } from '@/lib/utils'
import { cn, formatCurrency } from '@/lib/utils'
type Props = {
group: NonNullable<Awaited<ReturnType<typeof getGroup>>>
@@ -17,13 +17,19 @@ export function TotalsYourSpendings({ group, expenses }: Props) {
? 0
: getTotalActiveUserPaidFor(activeUser, expenses)
const currency = group.currency
const balance = totalYourSpendings < 0 ? 'earnings' : 'spendings'
return (
<div>
<div className="text-muted-foreground">Total you paid for</div>
<div className="text-muted-foreground">Your total {balance}</div>
<div className="text-lg">
{formatCurrency(currency, totalYourSpendings)}
<div
className={cn(
'text-lg',
totalYourSpendings < 0 ? 'text-green-600' : 'text-red-600',
)}
>
{formatCurrency(currency, Math.abs(totalYourSpendings))}
</div>
</div>
)