mirror of
https://github.com/spliit-app/spliit.git
synced 2026-03-07 20:59:05 +01:00
Split unevenly by amount
This commit is contained in:
@@ -60,7 +60,7 @@ export function ExpenseForm({ group, expense, onSubmit, onDelete }: Props) {
|
|||||||
paidBy: expense.paidById,
|
paidBy: expense.paidById,
|
||||||
paidFor: expense.paidFor.map(({ participantId, shares }) => ({
|
paidFor: expense.paidFor.map(({ participantId, shares }) => ({
|
||||||
participant: participantId,
|
participant: participantId,
|
||||||
shares,
|
shares: String(shares / 100) as unknown as number,
|
||||||
})),
|
})),
|
||||||
splitMode: expense.splitMode,
|
splitMode: expense.splitMode,
|
||||||
isReimbursement: expense.isReimbursement,
|
isReimbursement: expense.isReimbursement,
|
||||||
@@ -74,12 +74,18 @@ export function ExpenseForm({ group, expense, onSubmit, onDelete }: Props) {
|
|||||||
paidBy: searchParams.get('from') ?? undefined,
|
paidBy: searchParams.get('from') ?? undefined,
|
||||||
paidFor: [
|
paidFor: [
|
||||||
searchParams.get('to')
|
searchParams.get('to')
|
||||||
? { participant: searchParams.get('to')!, shares: 1 }
|
? { participant: searchParams.get('to')! }
|
||||||
: undefined,
|
: undefined,
|
||||||
],
|
],
|
||||||
isReimbursement: true,
|
isReimbursement: true,
|
||||||
}
|
}
|
||||||
: { title: '', amount: 0, paidFor: [], isReimbursement: false },
|
: {
|
||||||
|
title: '',
|
||||||
|
amount: 0,
|
||||||
|
paidFor: [],
|
||||||
|
isReimbursement: false,
|
||||||
|
splitMode: 'EVENLY',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -202,7 +208,9 @@ export function ExpenseForm({ group, expense, onSubmit, onDelete }: Props) {
|
|||||||
? []
|
? []
|
||||||
: group.participants.map((p) => ({
|
: group.participants.map((p) => ({
|
||||||
participant: p.id,
|
participant: p.id,
|
||||||
shares: 1,
|
shares:
|
||||||
|
paidFor.find((pfor) => pfor.participant === p.id)
|
||||||
|
?.shares ?? ('1' as unknown as number),
|
||||||
}))
|
}))
|
||||||
form.setValue('paidFor', newPaidFor, {
|
form.setValue('paidFor', newPaidFor, {
|
||||||
shouldDirty: true,
|
shouldDirty: true,
|
||||||
@@ -252,7 +260,10 @@ export function ExpenseForm({ group, expense, onSubmit, onDelete }: Props) {
|
|||||||
return checked
|
return checked
|
||||||
? field.onChange([
|
? field.onChange([
|
||||||
...field.value,
|
...field.value,
|
||||||
{ participant: id, shares: 1 },
|
{
|
||||||
|
participant: id,
|
||||||
|
shares: '1',
|
||||||
|
},
|
||||||
])
|
])
|
||||||
: field.onChange(
|
: field.onChange(
|
||||||
field.value?.filter(
|
field.value?.filter(
|
||||||
@@ -271,9 +282,32 @@ export function ExpenseForm({ group, expense, onSubmit, onDelete }: Props) {
|
|||||||
name={`paidFor[${field.value.findIndex(
|
name={`paidFor[${field.value.findIndex(
|
||||||
({ participant }) => participant === id,
|
({ participant }) => participant === id,
|
||||||
)}].shares`}
|
)}].shares`}
|
||||||
render={() => (
|
render={() => {
|
||||||
|
const sharesLabel = (
|
||||||
|
<span
|
||||||
|
className={cn('text-sm', {
|
||||||
|
'text-muted': !field.value?.some(
|
||||||
|
({ participant }) =>
|
||||||
|
participant === id,
|
||||||
|
),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{match(form.getValues().splitMode)
|
||||||
|
.with('BY_SHARES', () => <>share(s)</>)
|
||||||
|
.with('BY_PERCENTAGE', () => <>%</>)
|
||||||
|
.with('BY_AMOUNT', () => (
|
||||||
|
<>{group.currency}</>
|
||||||
|
))
|
||||||
|
.otherwise(() => (
|
||||||
|
<></>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex gap-1 items-baseline">
|
<div className="flex gap-1 items-baseline">
|
||||||
|
{form.getValues().splitMode ===
|
||||||
|
'BY_AMOUNT' && sharesLabel}
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
key={String(
|
key={String(
|
||||||
@@ -302,9 +336,8 @@ export function ExpenseForm({ group, expense, onSubmit, onDelete }: Props) {
|
|||||||
p.participant === id
|
p.participant === id
|
||||||
? {
|
? {
|
||||||
participant: id,
|
participant: id,
|
||||||
shares: Number(
|
shares:
|
||||||
event.target.value,
|
event.target.value,
|
||||||
),
|
|
||||||
}
|
}
|
||||||
: p,
|
: p,
|
||||||
),
|
),
|
||||||
@@ -314,29 +347,17 @@ export function ExpenseForm({ group, expense, onSubmit, onDelete }: Props) {
|
|||||||
step={1}
|
step={1}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<span
|
{[
|
||||||
className={cn('text-sm', {
|
'BY_SHARES',
|
||||||
'text-muted': !field.value?.some(
|
'BY_PERCENTAGE',
|
||||||
({ participant }) =>
|
].includes(
|
||||||
participant === id,
|
form.getValues().splitMode,
|
||||||
),
|
) && sharesLabel}
|
||||||
})}
|
|
||||||
>
|
|
||||||
{match(form.getValues().splitMode)
|
|
||||||
.with('EVENLY', () => <></>)
|
|
||||||
.with('BY_SHARES', () => (
|
|
||||||
<>share(s)</>
|
|
||||||
))
|
|
||||||
.with('BY_PERCENTAGE', () => <>%</>)
|
|
||||||
.with('BY_AMOUNT', () => (
|
|
||||||
<>{group.currency}</>
|
|
||||||
))
|
|
||||||
.exhaustive()}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<FormMessage className="float-right" />
|
<FormMessage className="float-right" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -355,7 +376,8 @@ export function ExpenseForm({ group, expense, onSubmit, onDelete }: Props) {
|
|||||||
Advanced splitting options…
|
Advanced splitting options…
|
||||||
</Button>
|
</Button>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent className="grid sm:grid-cols-2 gap-6 pt-3">
|
<CollapsibleContent>
|
||||||
|
<div className="grid sm:grid-cols-2 gap-6 pt-3">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="splitMode"
|
name="splitMode"
|
||||||
@@ -384,9 +406,9 @@ export function ExpenseForm({ group, expense, onSubmit, onDelete }: Props) {
|
|||||||
<SelectItem value="BY_PERCENTAGE">
|
<SelectItem value="BY_PERCENTAGE">
|
||||||
Unevenly – By percentage
|
Unevenly – By percentage
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
{/* <SelectItem value="BY_AMOUNT">
|
<SelectItem value="BY_AMOUNT">
|
||||||
Unevenly – By amount
|
Unevenly – By amount
|
||||||
</SelectItem> */}
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@@ -396,6 +418,7 @@ export function ExpenseForm({ group, expense, onSubmit, onDelete }: Props) {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -70,13 +70,24 @@ export const expenseFormSchema = z
|
|||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
participant: z.string(),
|
participant: z.string(),
|
||||||
shares: z.number().int(),
|
shares: z.union([
|
||||||
|
z.number(),
|
||||||
|
z.string().transform((value, ctx) => {
|
||||||
|
const valueAsNumber = Number(value)
|
||||||
|
if (Number.isNaN(valueAsNumber))
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: 'Invalid number.',
|
||||||
|
})
|
||||||
|
return Math.round(valueAsNumber * 100)
|
||||||
|
}),
|
||||||
|
]),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.min(1, 'The expense must be paid for at least one participant.')
|
.min(1, 'The expense must be paid for at least one participant.')
|
||||||
.superRefine((paidFor, ctx) => {
|
.superRefine((paidFor, ctx) => {
|
||||||
let sum = 0
|
let sum = 0
|
||||||
for (const { participant, shares } of paidFor) {
|
for (const { shares } of paidFor) {
|
||||||
sum += shares
|
sum += shares
|
||||||
if (shares < 1) {
|
if (shares < 1) {
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
@@ -95,20 +106,21 @@ export const expenseFormSchema = z
|
|||||||
})
|
})
|
||||||
.superRefine((expense, ctx) => {
|
.superRefine((expense, ctx) => {
|
||||||
let sum = 0
|
let sum = 0
|
||||||
for (const { participant, shares } of expense.paidFor) {
|
for (const { shares } of expense.paidFor) {
|
||||||
sum += shares
|
sum +=
|
||||||
|
typeof shares === 'number' ? shares : Math.round(Number(shares) * 100)
|
||||||
}
|
}
|
||||||
switch (expense.splitMode) {
|
switch (expense.splitMode) {
|
||||||
case 'EVENLY':
|
case 'EVENLY':
|
||||||
break // noop
|
break // noop
|
||||||
case 'BY_SHARES':
|
case 'BY_SHARES':
|
||||||
break // noop
|
break // noop
|
||||||
case 'BY_AMOUNT':
|
case 'BY_AMOUNT': {
|
||||||
if (sum !== expense.amount) {
|
if (sum !== expense.amount) {
|
||||||
const detail =
|
const detail =
|
||||||
sum < expense.amount
|
sum < expense.amount
|
||||||
? `${expense.amount - sum} missing`
|
? `${((expense.amount - sum) / 100).toFixed(2)} missing`
|
||||||
: `${sum - expense.amount} surplus`
|
: `${((sum - expense.amount) / 100).toFixed(2)} surplus`
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
code: z.ZodIssueCode.custom,
|
code: z.ZodIssueCode.custom,
|
||||||
message: `Sum of amounts must equal the expense amount (${detail}).`,
|
message: `Sum of amounts must equal the expense amount (${detail}).`,
|
||||||
@@ -116,10 +128,13 @@ export const expenseFormSchema = z
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case 'BY_PERCENTAGE':
|
}
|
||||||
if (sum !== 100) {
|
case 'BY_PERCENTAGE': {
|
||||||
|
if (sum !== 10000) {
|
||||||
const detail =
|
const detail =
|
||||||
sum < 100 ? `${100 - sum}% missing` : `${sum - 100}% surplus`
|
sum < 10000
|
||||||
|
? `${((10000 - sum) / 100).toFixed(0)}% missing`
|
||||||
|
: `${((sum - 10000) / 100).toFixed(0)}% surplus`
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
code: z.ZodIssueCode.custom,
|
code: z.ZodIssueCode.custom,
|
||||||
message: `Sum of percentages must equal 100 (${detail})`,
|
message: `Sum of percentages must equal 100 (${detail})`,
|
||||||
@@ -128,6 +143,7 @@ export const expenseFormSchema = z
|
|||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export type ExpenseFormValues = z.infer<typeof expenseFormSchema>
|
export type ExpenseFormValues = z.infer<typeof expenseFormSchema>
|
||||||
|
|||||||
Reference in New Issue
Block a user