mirror of
https://github.com/spliit-app/spliit.git
synced 2026-02-16 12:36:13 +01:00
Merge branch 'group-currency-code' of github.com:whimcomp/spliit into whimcomp-group-currency-code
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { Balances } from '@/lib/balances'
|
||||
import { Currency } from '@/lib/currency'
|
||||
import { cn, formatCurrency } from '@/lib/utils'
|
||||
import { Participant } from '@prisma/client'
|
||||
import { useLocale } from 'next-intl'
|
||||
@@ -6,7 +7,7 @@ import { useLocale } from 'next-intl'
|
||||
type Props = {
|
||||
balances: Balances
|
||||
participants: Participant[]
|
||||
currency: string
|
||||
currency: Currency
|
||||
}
|
||||
|
||||
export function BalancesList({ balances, participants, currency }: Props) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
CardTitle,
|
||||
} from '@/components/ui/card'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { getCurrencyFromGroup } from '@/lib/utils'
|
||||
import { trpc } from '@/trpc/client'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { Fragment, useEffect } from 'react'
|
||||
@@ -47,7 +48,7 @@ export default function BalancesAndReimbursements() {
|
||||
<BalancesList
|
||||
balances={balancesData.balances}
|
||||
participants={group?.participants}
|
||||
currency={group?.currency}
|
||||
currency={getCurrencyFromGroup(group)}
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
@@ -66,7 +67,7 @@ export default function BalancesAndReimbursements() {
|
||||
<ReimbursementList
|
||||
reimbursements={balancesData.reimbursements}
|
||||
participants={group?.participants}
|
||||
currency={group?.currency}
|
||||
currency={getCurrencyFromGroup(group)}
|
||||
groupId={groupId}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
'use client'
|
||||
import { Money } from '@/components/money'
|
||||
import { getBalances } from '@/lib/balances'
|
||||
import { Currency } from '@/lib/currency'
|
||||
import { useActiveUser } from '@/lib/hooks'
|
||||
import { useTranslations } from 'next-intl'
|
||||
|
||||
type Props = {
|
||||
groupId: string
|
||||
currency: string
|
||||
currency: Currency
|
||||
expense: Parameters<typeof getBalances>[0][number]
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
import { ToastAction } from '@/components/ui/toast'
|
||||
import { useToast } from '@/components/ui/use-toast'
|
||||
import { useMediaQuery } from '@/lib/hooks'
|
||||
import { formatCurrency, formatDate, formatFileSize } from '@/lib/utils'
|
||||
import { formatCurrency, formatDate, formatFileSize, getCurrencyFromGroup } from '@/lib/utils'
|
||||
import { trpc } from '@/trpc/client'
|
||||
import { ChevronRight, FileQuestion, Loader2, Receipt } from 'lucide-react'
|
||||
import { useLocale, useTranslations } from 'next-intl'
|
||||
@@ -202,7 +202,7 @@ function ReceiptDialogContent() {
|
||||
receiptInfo.amount ? (
|
||||
<>
|
||||
{formatCurrency(
|
||||
group.currency,
|
||||
getCurrencyFromGroup(group),
|
||||
receiptInfo.amount,
|
||||
locale,
|
||||
true,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { CategoryIcon } from '@/app/groups/[groupId]/expenses/category-icon'
|
||||
import { DocumentsCount } from '@/app/groups/[groupId]/expenses/documents-count'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { getGroupExpenses } from '@/lib/api'
|
||||
import { Currency } from '@/lib/currency'
|
||||
import { cn, formatCurrency, formatDate } from '@/lib/utils'
|
||||
import { ChevronRight } from 'lucide-react'
|
||||
import { useLocale, useTranslations } from 'next-intl'
|
||||
@@ -45,7 +46,7 @@ function Participants({
|
||||
|
||||
type Props = {
|
||||
expense: Expense
|
||||
currency: string
|
||||
currency: Currency
|
||||
groupId: string
|
||||
participantCount: number
|
||||
}
|
||||
|
||||
@@ -41,12 +41,18 @@ import {
|
||||
expenseFormSchema,
|
||||
} from '@/lib/schemas'
|
||||
import { calculateShare } from '@/lib/totals'
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
amountAsDecimal,
|
||||
amountAsMinorUnits,
|
||||
cn,
|
||||
formatCurrency,
|
||||
getCurrencyFromGroup,
|
||||
} from '@/lib/utils'
|
||||
import { AppRouterOutput } from '@/trpc/routers/_app'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { RecurrenceRule } from '@prisma/client'
|
||||
import { Save } from 'lucide-react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { useLocale, useTranslations } from 'next-intl'
|
||||
import Link from 'next/link'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
import { useEffect, useState } from 'react'
|
||||
@@ -155,6 +161,7 @@ export function ExpenseForm({
|
||||
runtimeFeatureFlags: RuntimeFeatureFlags
|
||||
}) {
|
||||
const t = useTranslations('ExpenseForm')
|
||||
const locale = useLocale()
|
||||
const isCreate = expense === undefined
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
@@ -172,18 +179,25 @@ export function ExpenseForm({
|
||||
return field?.value as RecurrenceRule
|
||||
}
|
||||
const defaultSplittingOptions = getDefaultSplittingOptions(group)
|
||||
const groupCurrency = getCurrencyFromGroup(group)
|
||||
const form = useForm<ExpenseFormValues>({
|
||||
resolver: zodResolver(expenseFormSchema),
|
||||
defaultValues: expense
|
||||
? {
|
||||
title: expense.title,
|
||||
expenseDate: expense.expenseDate ?? new Date(),
|
||||
amount: String(expense.amount / 100) as unknown as number, // hack
|
||||
amount: String(
|
||||
amountAsDecimal(expense.amount, groupCurrency),
|
||||
) as unknown as number, // hack
|
||||
category: expense.categoryId,
|
||||
paidBy: expense.paidById,
|
||||
paidFor: expense.paidFor.map(({ participantId, shares }) => ({
|
||||
participant: participantId,
|
||||
shares: String(shares / 100) as unknown as number,
|
||||
shares: String(
|
||||
expense.splitMode === 'BY_AMOUNT'
|
||||
? amountAsDecimal(shares, groupCurrency)
|
||||
: shares / 100,
|
||||
) as unknown as number,
|
||||
})),
|
||||
splitMode: expense.splitMode,
|
||||
saveDefaultSplittingOptions: false,
|
||||
@@ -197,7 +211,10 @@ export function ExpenseForm({
|
||||
title: t('reimbursement'),
|
||||
expenseDate: new Date(),
|
||||
amount: String(
|
||||
(Number(searchParams.get('amount')) || 0) / 100,
|
||||
amountAsDecimal(
|
||||
Number(searchParams.get('amount')) || 0,
|
||||
groupCurrency,
|
||||
),
|
||||
) as unknown as number, // hack
|
||||
category: 1, // category with Id 1 is Payment
|
||||
paidBy: searchParams.get('from') ?? undefined,
|
||||
@@ -250,6 +267,16 @@ export function ExpenseForm({
|
||||
|
||||
const submit = async (values: ExpenseFormValues) => {
|
||||
await persistDefaultSplittingOptions(group.id, values)
|
||||
|
||||
// Store monetary amounts in minor units (cents)
|
||||
values.amount = amountAsMinorUnits(values.amount, groupCurrency)
|
||||
values.paidFor = values.paidFor.map(({ participant, shares }) => ({
|
||||
participant,
|
||||
shares:
|
||||
values.splitMode === 'BY_AMOUNT'
|
||||
? amountAsMinorUnits(shares, groupCurrency)
|
||||
: shares,
|
||||
}))
|
||||
return onSubmit(values, activeUserId ?? undefined)
|
||||
}
|
||||
|
||||
@@ -303,7 +330,9 @@ export function ExpenseForm({
|
||||
return {
|
||||
...participant,
|
||||
shares: String(
|
||||
Number(amountPerRemaining.toFixed(2)),
|
||||
Number(
|
||||
amountPerRemaining.toFixed(groupCurrency.decimal_digits),
|
||||
),
|
||||
) as unknown as number,
|
||||
}
|
||||
}
|
||||
@@ -644,11 +673,14 @@ export function ExpenseForm({
|
||||
) &&
|
||||
!form.watch('isReimbursement') && (
|
||||
<span className="text-muted-foreground ml-2">
|
||||
({group.currency}{' '}
|
||||
{(
|
||||
(
|
||||
{formatCurrency(
|
||||
groupCurrency,
|
||||
calculateShare(id, {
|
||||
amount:
|
||||
Number(form.watch('amount')) * 100, // Convert to cents
|
||||
amount: amountAsMinorUnits(
|
||||
Number(form.watch('amount')),
|
||||
groupCurrency,
|
||||
), // Convert to cents
|
||||
paidFor: field.value.map(
|
||||
({ participant, shares }) => ({
|
||||
participant: {
|
||||
@@ -658,10 +690,14 @@ export function ExpenseForm({
|
||||
},
|
||||
shares:
|
||||
form.watch('splitMode') ===
|
||||
'BY_PERCENTAGE' ||
|
||||
form.watch('splitMode') ===
|
||||
'BY_AMOUNT'
|
||||
'BY_PERCENTAGE'
|
||||
? Number(shares) * 100 // Convert percentage to basis points (e.g., 50% -> 5000)
|
||||
: form.watch('splitMode') ===
|
||||
'BY_AMOUNT'
|
||||
? amountAsMinorUnits(
|
||||
shares,
|
||||
groupCurrency,
|
||||
)
|
||||
: shares,
|
||||
expenseId: '',
|
||||
participantId: '',
|
||||
@@ -670,8 +706,9 @@ export function ExpenseForm({
|
||||
splitMode: form.watch('splitMode'),
|
||||
isReimbursement:
|
||||
form.watch('isReimbursement'),
|
||||
}) / 100
|
||||
).toFixed(2)}
|
||||
}),
|
||||
locale,
|
||||
)}
|
||||
)
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { getGroupExpensesAction } from '@/app/groups/[groupId]/expenses/expense-
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { SearchBar } from '@/components/ui/search-bar'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { getCurrencyFromGroup } from '@/lib/utils'
|
||||
import { trpc } from '@/trpc/client'
|
||||
import dayjs, { type Dayjs } from 'dayjs'
|
||||
import { useTranslations } from 'next-intl'
|
||||
@@ -170,7 +171,7 @@ const ExpenseListForSearch = ({
|
||||
<ExpenseCard
|
||||
key={expense.id}
|
||||
expense={expense}
|
||||
currency={group.currency}
|
||||
currency={getCurrencyFromGroup(group)}
|
||||
groupId={groupId}
|
||||
participantCount={group.participants.length}
|
||||
/>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { formatAmountAsDecimal, getCurrencyFromGroup } from '@/lib/utils'
|
||||
import { Parser } from '@json2csv/plainjs'
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
import contentDisposition from 'content-disposition'
|
||||
@@ -30,6 +31,7 @@ export async function GET(
|
||||
id: true,
|
||||
name: true,
|
||||
currency: true,
|
||||
currencyCode: true,
|
||||
expenses: {
|
||||
select: {
|
||||
expenseDate: true,
|
||||
@@ -91,12 +93,14 @@ export async function GET(
|
||||
})),
|
||||
]
|
||||
|
||||
const currency = getCurrencyFromGroup(group)
|
||||
|
||||
const expenses = group.expenses.map((expense) => ({
|
||||
date: formatDate(expense.expenseDate),
|
||||
title: expense.title,
|
||||
categoryName: expense.category?.name || '',
|
||||
currency: group.currency,
|
||||
amount: (expense.amount / 100).toFixed(2),
|
||||
currency: group.currencyCode ?? group.currency,
|
||||
amount: formatAmountAsDecimal(expense.amount, currency),
|
||||
isReimbursement: expense.isReimbursement ? 'Yes' : 'No',
|
||||
splitMode: splitModeLabel[expense.splitMode],
|
||||
...Object.fromEntries(
|
||||
@@ -113,10 +117,10 @@ export async function GET(
|
||||
)
|
||||
|
||||
const isPaidByParticipant = expense.paidById === participant.id
|
||||
const participantAmountShare = +(
|
||||
((expense.amount / totalShares) * participantShare) /
|
||||
100
|
||||
).toFixed(2)
|
||||
const participantAmountShare = +formatAmountAsDecimal(
|
||||
(expense.amount / totalShares) * participantShare,
|
||||
currency,
|
||||
)
|
||||
|
||||
return [
|
||||
participant.name,
|
||||
|
||||
@@ -12,6 +12,7 @@ export async function GET(
|
||||
id: true,
|
||||
name: true,
|
||||
currency: true,
|
||||
currencyCode: true,
|
||||
expenses: {
|
||||
select: {
|
||||
createdAt: true,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Reimbursement } from '@/lib/balances'
|
||||
import { Currency } from '@/lib/currency'
|
||||
import { formatCurrency } from '@/lib/utils'
|
||||
import { Participant } from '@prisma/client'
|
||||
import { useLocale, useTranslations } from 'next-intl'
|
||||
@@ -8,7 +9,7 @@ import Link from 'next/link'
|
||||
type Props = {
|
||||
reimbursements: Reimbursement[]
|
||||
participants: Participant[]
|
||||
currency: string
|
||||
currency: Currency
|
||||
groupId: string
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Currency } from '@/lib/currency'
|
||||
import { formatCurrency } from '@/lib/utils'
|
||||
import { useLocale, useTranslations } from 'next-intl'
|
||||
|
||||
type Props = {
|
||||
totalGroupSpendings: number
|
||||
currency: string
|
||||
currency: Currency
|
||||
}
|
||||
|
||||
export function TotalsGroupSpending({ totalGroupSpendings, currency }: Props) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
'use client'
|
||||
import { Currency } from '@/lib/currency'
|
||||
import { cn, formatCurrency } from '@/lib/utils'
|
||||
import { useLocale, useTranslations } from 'next-intl'
|
||||
|
||||
@@ -7,7 +8,7 @@ export function TotalsYourShare({
|
||||
currency,
|
||||
}: {
|
||||
totalParticipantShare?: number
|
||||
currency: string
|
||||
currency: Currency
|
||||
}) {
|
||||
const locale = useLocale()
|
||||
const t = useTranslations('Stats.Totals')
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
'use client'
|
||||
import { Currency } from '@/lib/currency'
|
||||
import { cn, formatCurrency } from '@/lib/utils'
|
||||
import { useLocale, useTranslations } from 'next-intl'
|
||||
|
||||
@@ -7,7 +8,7 @@ export function TotalsYourSpendings({
|
||||
currency,
|
||||
}: {
|
||||
totalParticipantSpendings?: number
|
||||
currency: string
|
||||
currency: Currency
|
||||
}) {
|
||||
const locale = useLocale()
|
||||
const t = useTranslations('Stats.Totals')
|
||||
|
||||
@@ -4,6 +4,7 @@ import { TotalsYourShare } from '@/app/groups/[groupId]/stats/totals-your-share'
|
||||
import { TotalsYourSpendings } from '@/app/groups/[groupId]/stats/totals-your-spending'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { useActiveUser } from '@/lib/hooks'
|
||||
import { getCurrencyFromGroup } from '@/lib/utils'
|
||||
import { trpc } from '@/trpc/client'
|
||||
import { useCurrentGroup } from '../current-group-context'
|
||||
|
||||
@@ -33,21 +34,23 @@ export function Totals() {
|
||||
totalParticipantSpendings,
|
||||
} = data
|
||||
|
||||
const currency = getCurrencyFromGroup(group)
|
||||
|
||||
return (
|
||||
<>
|
||||
<TotalsGroupSpending
|
||||
totalGroupSpendings={totalGroupSpendings}
|
||||
currency={group.currency}
|
||||
currency={currency}
|
||||
/>
|
||||
{participantId && (
|
||||
<>
|
||||
<TotalsYourSpendings
|
||||
totalParticipantSpendings={totalParticipantSpendings}
|
||||
currency={group.currency}
|
||||
currency={currency}
|
||||
/>
|
||||
<TotalsYourShare
|
||||
totalParticipantShare={totalParticipantShare}
|
||||
currency={group.currency}
|
||||
currency={currency}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user