mirror of
https://github.com/spliit-app/spliit.git
synced 2026-03-05 20:26:11 +01:00
Add "save as default splitting options" feature (#120)
* Add "save as default splitting options" feature * Fix type issue * Run autoformatter
This commit is contained in:
@@ -35,7 +35,11 @@ import {
|
|||||||
} from '@/components/ui/select'
|
} from '@/components/ui/select'
|
||||||
import { getCategories, getExpense, getGroup, randomId } from '@/lib/api'
|
import { getCategories, getExpense, getGroup, randomId } from '@/lib/api'
|
||||||
import { RuntimeFeatureFlags } from '@/lib/featureFlags'
|
import { RuntimeFeatureFlags } from '@/lib/featureFlags'
|
||||||
import { ExpenseFormValues, expenseFormSchema } from '@/lib/schemas'
|
import {
|
||||||
|
ExpenseFormValues,
|
||||||
|
SplittingOptions,
|
||||||
|
expenseFormSchema,
|
||||||
|
} from '@/lib/schemas'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { zodResolver } from '@hookform/resolvers/zod'
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
import { Save } from 'lucide-react'
|
import { Save } from 'lucide-react'
|
||||||
@@ -67,6 +71,78 @@ const enforceCurrencyPattern = (value: string) =>
|
|||||||
// remove all non-numeric and non-dot characters
|
// remove all non-numeric and non-dot characters
|
||||||
.replace(/[^\d.]/g, '')
|
.replace(/[^\d.]/g, '')
|
||||||
|
|
||||||
|
const getDefaultSplittingOptions = (group: Props['group']) => {
|
||||||
|
const defaultValue = {
|
||||||
|
splitMode: 'EVENLY' as const,
|
||||||
|
paidFor: group.participants.map(({ id }) => ({
|
||||||
|
participant: id,
|
||||||
|
shares: '1' as unknown as number,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof localStorage === 'undefined') return defaultValue
|
||||||
|
const defaultSplitMode = localStorage.getItem(
|
||||||
|
`${group.id}-defaultSplittingOptions`,
|
||||||
|
)
|
||||||
|
if (defaultSplitMode === null) return defaultValue
|
||||||
|
const parsedDefaultSplitMode = JSON.parse(
|
||||||
|
defaultSplitMode,
|
||||||
|
) as SplittingOptions
|
||||||
|
|
||||||
|
if (parsedDefaultSplitMode.paidFor === null) {
|
||||||
|
parsedDefaultSplitMode.paidFor = defaultValue.paidFor
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there is a participant in the default options that does not exist anymore,
|
||||||
|
// remove the stale default splitting options
|
||||||
|
for (const parsedPaidFor of parsedDefaultSplitMode.paidFor) {
|
||||||
|
if (
|
||||||
|
!group.participants.some(({ id }) => id === parsedPaidFor.participant)
|
||||||
|
) {
|
||||||
|
localStorage.removeItem(`${group.id}-defaultSplittingOptions`)
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
splitMode: parsedDefaultSplitMode.splitMode,
|
||||||
|
paidFor: parsedDefaultSplitMode.paidFor.map((paidFor) => ({
|
||||||
|
participant: paidFor.participant,
|
||||||
|
shares: String(paidFor.shares / 100) as unknown as number,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function persistDefaultSplittingOptions(
|
||||||
|
groupId: string,
|
||||||
|
expenseFormValues: ExpenseFormValues,
|
||||||
|
) {
|
||||||
|
if (localStorage && expenseFormValues.saveDefaultSplittingOptions) {
|
||||||
|
const computePaidFor = (): SplittingOptions['paidFor'] => {
|
||||||
|
if (expenseFormValues.splitMode === 'EVENLY') {
|
||||||
|
return expenseFormValues.paidFor.map(({ participant }) => ({
|
||||||
|
participant,
|
||||||
|
shares: '100' as unknown as number,
|
||||||
|
}))
|
||||||
|
} else if (expenseFormValues.splitMode === 'BY_AMOUNT') {
|
||||||
|
return null
|
||||||
|
} else {
|
||||||
|
return expenseFormValues.paidFor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const splittingOptions = {
|
||||||
|
splitMode: expenseFormValues.splitMode,
|
||||||
|
paidFor: computePaidFor(),
|
||||||
|
} satisfies SplittingOptions
|
||||||
|
|
||||||
|
localStorage.setItem(
|
||||||
|
`${groupId}-defaultSplittingOptions`,
|
||||||
|
JSON.stringify(splittingOptions),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function ExpenseForm({
|
export function ExpenseForm({
|
||||||
group,
|
group,
|
||||||
expense,
|
expense,
|
||||||
@@ -86,6 +162,7 @@ export function ExpenseForm({
|
|||||||
}
|
}
|
||||||
return field?.value
|
return field?.value
|
||||||
}
|
}
|
||||||
|
const defaultSplittingOptions = getDefaultSplittingOptions(group)
|
||||||
const form = useForm<ExpenseFormValues>({
|
const form = useForm<ExpenseFormValues>({
|
||||||
resolver: zodResolver(expenseFormSchema),
|
resolver: zodResolver(expenseFormSchema),
|
||||||
defaultValues: expense
|
defaultValues: expense
|
||||||
@@ -100,6 +177,7 @@ export function ExpenseForm({
|
|||||||
shares: String(shares / 100) as unknown as number,
|
shares: String(shares / 100) as unknown as number,
|
||||||
})),
|
})),
|
||||||
splitMode: expense.splitMode,
|
splitMode: expense.splitMode,
|
||||||
|
saveDefaultSplittingOptions: false,
|
||||||
isReimbursement: expense.isReimbursement,
|
isReimbursement: expense.isReimbursement,
|
||||||
documents: expense.documents,
|
documents: expense.documents,
|
||||||
}
|
}
|
||||||
@@ -121,7 +199,8 @@ export function ExpenseForm({
|
|||||||
: undefined,
|
: undefined,
|
||||||
],
|
],
|
||||||
isReimbursement: true,
|
isReimbursement: true,
|
||||||
splitMode: 'EVENLY',
|
splitMode: defaultSplittingOptions.splitMode,
|
||||||
|
saveDefaultSplittingOptions: false,
|
||||||
documents: [],
|
documents: [],
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
@@ -134,13 +213,11 @@ export function ExpenseForm({
|
|||||||
? Number(searchParams.get('categoryId'))
|
? Number(searchParams.get('categoryId'))
|
||||||
: 0, // category with Id 0 is General
|
: 0, // category with Id 0 is General
|
||||||
// paid for all, split evenly
|
// paid for all, split evenly
|
||||||
paidFor: group.participants.map(({ id }) => ({
|
paidFor: defaultSplittingOptions.paidFor,
|
||||||
participant: id,
|
|
||||||
shares: '1' as unknown as number,
|
|
||||||
})),
|
|
||||||
paidBy: getSelectedPayer(),
|
paidBy: getSelectedPayer(),
|
||||||
isReimbursement: false,
|
isReimbursement: false,
|
||||||
splitMode: 'EVENLY',
|
splitMode: defaultSplittingOptions.splitMode,
|
||||||
|
saveDefaultSplittingOptions: false,
|
||||||
documents: searchParams.get('imageUrl')
|
documents: searchParams.get('imageUrl')
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
@@ -155,9 +232,14 @@ export function ExpenseForm({
|
|||||||
})
|
})
|
||||||
const [isCategoryLoading, setCategoryLoading] = useState(false)
|
const [isCategoryLoading, setCategoryLoading] = useState(false)
|
||||||
|
|
||||||
|
const submit = async (values: ExpenseFormValues) => {
|
||||||
|
await persistDefaultSplittingOptions(group.id, values)
|
||||||
|
return onSubmit(values)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit((values) => onSubmit(values))}>
|
<form onSubmit={form.handleSubmit(submit)}>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>
|
<CardTitle>
|
||||||
@@ -511,7 +593,10 @@ export function ExpenseForm({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Collapsible className="mt-5">
|
<Collapsible
|
||||||
|
className="mt-5"
|
||||||
|
defaultOpen={form.getValues().splitMode !== 'EVENLY'}
|
||||||
|
>
|
||||||
<CollapsibleTrigger asChild>
|
<CollapsibleTrigger asChild>
|
||||||
<Button variant="link" className="-mx-4">
|
<Button variant="link" className="-mx-4">
|
||||||
Advanced splitting options…
|
Advanced splitting options…
|
||||||
@@ -523,7 +608,7 @@ export function ExpenseForm({
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="splitMode"
|
name="splitMode"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="sm:order-2">
|
<FormItem>
|
||||||
<FormLabel>Split mode</FormLabel>
|
<FormLabel>Split mode</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Select
|
<Select
|
||||||
@@ -559,6 +644,25 @@ export function ExpenseForm({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="saveDefaultSplittingOptions"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row gap-2 items-center space-y-0 pt-2">
|
||||||
|
<FormControl>
|
||||||
|
<Checkbox
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<div>
|
||||||
|
<FormLabel>
|
||||||
|
Save as default splitting options
|
||||||
|
</FormLabel>
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ export const expenseFormSchema = z
|
|||||||
Object.values(SplitMode) as any,
|
Object.values(SplitMode) as any,
|
||||||
)
|
)
|
||||||
.default('EVENLY'),
|
.default('EVENLY'),
|
||||||
|
saveDefaultSplittingOptions: z.boolean(),
|
||||||
isReimbursement: z.boolean(),
|
isReimbursement: z.boolean(),
|
||||||
documents: z
|
documents: z
|
||||||
.array(
|
.array(
|
||||||
@@ -160,3 +161,9 @@ export const expenseFormSchema = z
|
|||||||
})
|
})
|
||||||
|
|
||||||
export type ExpenseFormValues = z.infer<typeof expenseFormSchema>
|
export type ExpenseFormValues = z.infer<typeof expenseFormSchema>
|
||||||
|
|
||||||
|
export type SplittingOptions = {
|
||||||
|
// Used for saving default splitting options in localStorage
|
||||||
|
splitMode: SplitMode
|
||||||
|
paidFor: ExpenseFormValues['paidFor'] | null
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user