Skip to content

Commit

Permalink
Add "save as default splitting options" feature (#120)
Browse files Browse the repository at this point in the history
* Add "save as default splitting options" feature

* Fix type issue

* Run autoformatter
  • Loading branch information
jantuomi committed Mar 9, 2024
1 parent e07d237 commit b67a0be
Show file tree
Hide file tree
Showing 2 changed files with 121 additions and 10 deletions.
124 changes: 114 additions & 10 deletions src/components/expense-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ import {
} from '@/components/ui/select'
import { getCategories, getExpense, getGroup, randomId } from '@/lib/api'
import { RuntimeFeatureFlags } from '@/lib/featureFlags'
import { ExpenseFormValues, expenseFormSchema } from '@/lib/schemas'
import {
ExpenseFormValues,
SplittingOptions,
expenseFormSchema,
} from '@/lib/schemas'
import { cn } from '@/lib/utils'
import { zodResolver } from '@hookform/resolvers/zod'
import { Save } from 'lucide-react'
Expand Down Expand Up @@ -67,6 +71,78 @@ const enforceCurrencyPattern = (value: string) =>
// remove all non-numeric and non-dot characters
.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({
group,
expense,
Expand All @@ -86,6 +162,7 @@ export function ExpenseForm({
}
return field?.value
}
const defaultSplittingOptions = getDefaultSplittingOptions(group)
const form = useForm<ExpenseFormValues>({
resolver: zodResolver(expenseFormSchema),
defaultValues: expense
Expand All @@ -100,6 +177,7 @@ export function ExpenseForm({
shares: String(shares / 100) as unknown as number,
})),
splitMode: expense.splitMode,
saveDefaultSplittingOptions: false,
isReimbursement: expense.isReimbursement,
documents: expense.documents,
}
Expand All @@ -121,7 +199,8 @@ export function ExpenseForm({
: undefined,
],
isReimbursement: true,
splitMode: 'EVENLY',
splitMode: defaultSplittingOptions.splitMode,
saveDefaultSplittingOptions: false,
documents: [],
}
: {
Expand All @@ -134,13 +213,11 @@ export function ExpenseForm({
? Number(searchParams.get('categoryId'))
: 0, // category with Id 0 is General
// paid for all, split evenly
paidFor: group.participants.map(({ id }) => ({
participant: id,
shares: '1' as unknown as number,
})),
paidFor: defaultSplittingOptions.paidFor,
paidBy: getSelectedPayer(),
isReimbursement: false,
splitMode: 'EVENLY',
splitMode: defaultSplittingOptions.splitMode,
saveDefaultSplittingOptions: false,
documents: searchParams.get('imageUrl')
? [
{
Expand All @@ -155,9 +232,14 @@ export function ExpenseForm({
})
const [isCategoryLoading, setCategoryLoading] = useState(false)

const submit = async (values: ExpenseFormValues) => {
await persistDefaultSplittingOptions(group.id, values)
return onSubmit(values)
}

return (
<Form {...form}>
<form onSubmit={form.handleSubmit((values) => onSubmit(values))}>
<form onSubmit={form.handleSubmit(submit)}>
<Card>
<CardHeader>
<CardTitle>
Expand Down Expand Up @@ -511,7 +593,10 @@ export function ExpenseForm({
)}
/>

<Collapsible className="mt-5">
<Collapsible
className="mt-5"
defaultOpen={form.getValues().splitMode !== 'EVENLY'}
>
<CollapsibleTrigger asChild>
<Button variant="link" className="-mx-4">
Advanced splitting options…
Expand All @@ -523,7 +608,7 @@ export function ExpenseForm({
control={form.control}
name="splitMode"
render={({ field }) => (
<FormItem className="sm:order-2">
<FormItem>
<FormLabel>Split mode</FormLabel>
<FormControl>
<Select
Expand Down Expand Up @@ -559,6 +644,25 @@ export function ExpenseForm({
</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>
</CollapsibleContent>
</Collapsible>
Expand Down
7 changes: 7 additions & 0 deletions src/lib/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export const expenseFormSchema = z
Object.values(SplitMode) as any,
)
.default('EVENLY'),
saveDefaultSplittingOptions: z.boolean(),
isReimbursement: z.boolean(),
documents: z
.array(
Expand Down Expand Up @@ -160,3 +161,9 @@ export const expenseFormSchema = z
})

export type ExpenseFormValues = z.infer<typeof expenseFormSchema>

export type SplittingOptions = {
// Used for saving default splitting options in localStorage
splitMode: SplitMode
paidFor: ExpenseFormValues['paidFor'] | null
}

0 comments on commit b67a0be

Please sign in to comment.