Skip to content

Commit 375e0c6

Browse files
committed
feat: Store default splitting options on the server
This commit refactors the "default splitting options" feature to be stored on the server-side instead of in the client's localStorage. This provides a more consistent user experience, as the setting will now persist across different devices and browsers for all members of a group. The following changes were made: - The Group model in prisma.schema.prisma has been updated with a new defaultSplittingOptions JSON field. - A database migration has been added to apply this schema change. - The ExpenseForm component in expense-form.tsx has been updated to: - Fetch the default splitting options from the group object, which is populated from the database. - Use a new tRPC mutation, updateDefaultSplittingOptions, to persist the user's chosen default options back to the server. - All logic related to localStorage for this feature has been removed. Fixes #327
1 parent d3b151e commit 375e0c6

File tree

5 files changed

+76
-52
lines changed

5 files changed

+76
-52
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
-- AlterTable
2+
ALTER TABLE "Group" ADD COLUMN IF NOT EXISTS "defaultSplittingOptions" JSONB;
3+

prisma/schema.prisma

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ model Group {
1717
information String? @db.Text
1818
currency String @default("$")
1919
currencyCode String?
20+
defaultSplittingOptions Json?
2021
participants Participant[]
2122
expenses Expense[]
2223
activities Activity[]

src/app/groups/[groupId]/expenses/expense-form.tsx

Lines changed: 40 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import {
5151
formatCurrency,
5252
getCurrencyFromGroup,
5353
} from '@/lib/utils'
54+
import { trpc } from '@/trpc/client'
5455
import { AppRouterOutput } from '@/trpc/routers/_app'
5556
import { zodResolver } from '@hookform/resolvers/zod'
5657
import { RecurrenceRule } from '@prisma/client'
@@ -85,69 +86,31 @@ const getDefaultSplittingOptions = (
8586
})),
8687
}
8788

88-
if (typeof localStorage === 'undefined') return defaultValue
89-
const defaultSplitMode = localStorage.getItem(
90-
`${group.id}-defaultSplittingOptions`,
91-
)
92-
if (defaultSplitMode === null) return defaultValue
93-
const parsedDefaultSplitMode = JSON.parse(
94-
defaultSplitMode,
95-
) as SplittingOptions
89+
const dbOptions = group.defaultSplittingOptions as SplittingOptions | null
90+
if (!dbOptions) {
91+
return defaultValue
92+
}
9693

97-
if (parsedDefaultSplitMode.paidFor === null) {
98-
parsedDefaultSplitMode.paidFor = defaultValue.paidFor
94+
if (dbOptions.paidFor === null) {
95+
dbOptions.paidFor = defaultValue.paidFor
9996
}
10097

101-
// if there is a participant in the default options that does not exist anymore,
102-
// remove the stale default splitting options
103-
for (const parsedPaidFor of parsedDefaultSplitMode.paidFor) {
104-
if (
105-
!group.participants.some(({ id }) => id === parsedPaidFor.participant)
106-
) {
107-
localStorage.removeItem(`${group.id}-defaultSplittingOptions`)
98+
for (const paidFor of dbOptions.paidFor) {
99+
if (!group.participants.some(({ id }) => id === paidFor.participant)) {
108100
return defaultValue
109101
}
110102
}
111103

112104
return {
113-
splitMode: parsedDefaultSplitMode.splitMode,
114-
paidFor: parsedDefaultSplitMode.paidFor.map((paidFor) => ({
115-
participant: paidFor.participant,
116-
shares: (paidFor.shares / 100).toString() as any, // Convert to string for consistent schema handling
105+
splitMode: dbOptions.splitMode,
106+
paidFor: dbOptions.paidFor.map((p) => ({
107+
participant: p.participant,
108+
shares: String(p.shares / 100) as unknown as number,
117109
})),
118110
}
119111
}
120112

121-
async function persistDefaultSplittingOptions(
122-
groupId: string,
123-
expenseFormValues: ExpenseFormValues,
124-
) {
125-
if (localStorage && expenseFormValues.saveDefaultSplittingOptions) {
126-
const computePaidFor = (): SplittingOptions['paidFor'] => {
127-
if (expenseFormValues.splitMode === 'EVENLY') {
128-
return expenseFormValues.paidFor.map(({ participant }) => ({
129-
participant,
130-
shares: 100,
131-
}))
132-
} else if (expenseFormValues.splitMode === 'BY_AMOUNT') {
133-
return null
134-
} else {
135-
return expenseFormValues.paidFor
136-
}
137-
}
138-
139-
const splittingOptions = {
140-
splitMode: expenseFormValues.splitMode,
141-
paidFor: computePaidFor(),
142-
} satisfies SplittingOptions
143-
144-
localStorage.setItem(
145-
`${groupId}-defaultSplittingOptions`,
146-
JSON.stringify(splittingOptions),
147-
)
148-
}
149-
}
150-
113+
// removed localStorage persistence in favor of server-side persistence
151114
export function ExpenseForm({
152115
group,
153116
categories,
@@ -270,10 +233,35 @@ export function ExpenseForm({
270233
})
271234
const [isCategoryLoading, setCategoryLoading] = useState(false)
272235
const activeUserId = useActiveUser(group.id)
236+
const { mutateAsync: updateDefaultSplittingOptions } =
237+
trpc.groups.updateDefaultSplittingOptions.useMutation()
238+
const utils = trpc.useUtils()
273239

274240
const submit = async (values: ExpenseFormValues) => {
275-
await persistDefaultSplittingOptions(group.id, values)
241+
if (values.saveDefaultSplittingOptions) {
242+
const computePaidFor = (): SplittingOptions['paidFor'] => {
243+
if (values.splitMode === 'EVENLY') {
244+
return values.paidFor.map(({ participant }) => ({
245+
participant,
246+
shares: '100' as unknown as number,
247+
}))
248+
} else if (values.splitMode === 'BY_AMOUNT') {
249+
return null
250+
}
251+
return values.paidFor
252+
}
253+
254+
const splittingOptions = {
255+
splitMode: values.splitMode,
256+
paidFor: computePaidFor(),
257+
} satisfies SplittingOptions
276258

259+
await updateDefaultSplittingOptions({
260+
groupId: group.id,
261+
defaultSplittingOptions: splittingOptions,
262+
})
263+
await utils.groups.invalidate()
264+
}
277265
// Store monetary amounts in minor units (cents)
278266
values.amount = amountAsMinorUnits(values.amount, groupCurrency)
279267
values.paidFor = values.paidFor.map(({ participant, shares }) => ({

src/trpc/routers/groups/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { groupStatsRouter } from '@/trpc/routers/groups/stats'
88
import { updateGroupProcedure } from '@/trpc/routers/groups/update.procedure'
99
import { getGroupDetailsProcedure } from './getDetails.procedure'
1010
import { listGroupsProcedure } from './list.procedure'
11+
import { updateDefaultSplittingOptionsProcedure } from './updateDefaultSplittingOptions.procedure'
1112

1213
export const groupsRouter = createTRPCRouter({
1314
expenses: groupExpensesRouter,
@@ -20,4 +21,5 @@ export const groupsRouter = createTRPCRouter({
2021
list: listGroupsProcedure,
2122
create: createGroupProcedure,
2223
update: updateGroupProcedure,
24+
updateDefaultSplittingOptions: updateDefaultSplittingOptionsProcedure,
2325
})
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { prisma } from '@/lib/prisma'
2+
import { baseProcedure } from '@/trpc/init'
3+
import { SplitMode } from '@prisma/client'
4+
import { z } from 'zod'
5+
6+
export const updateDefaultSplittingOptionsProcedure = baseProcedure
7+
.input(
8+
z.object({
9+
groupId: z.string().min(1),
10+
defaultSplittingOptions: z.object({
11+
splitMode: z.enum(
12+
Object.values(SplitMode) as [SplitMode, ...SplitMode[]],
13+
),
14+
paidFor: z
15+
.array(
16+
z.object({
17+
participant: z.string(),
18+
shares: z.number(),
19+
}),
20+
)
21+
.nullable(),
22+
}),
23+
}),
24+
)
25+
.mutation(async ({ input: { groupId, defaultSplittingOptions } }) => {
26+
await prisma.group.update({
27+
where: { id: groupId },
28+
data: { defaultSplittingOptions },
29+
})
30+
})

0 commit comments

Comments
 (0)