mirror of
https://github.com/spliit-app/spliit.git
synced 2025-12-06 09:29:39 +01:00
First version
This commit is contained in:
155
src/lib/api.ts
Normal file
155
src/lib/api.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { getPrisma } from '@/lib/prisma'
|
||||
import { ExpenseFormValues, GroupFormValues } from '@/lib/schemas'
|
||||
import { Expense } from '@prisma/client'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
export async function createGroup(groupFormValues: GroupFormValues) {
|
||||
return getPrisma().group.create({
|
||||
data: {
|
||||
id: uuidv4(),
|
||||
name: groupFormValues.name,
|
||||
participants: {
|
||||
createMany: {
|
||||
data: groupFormValues.participants.map(({ name }) => ({
|
||||
id: uuidv4(),
|
||||
name,
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
include: { participants: true },
|
||||
})
|
||||
}
|
||||
|
||||
export async function createExpense(
|
||||
expenseFormValues: ExpenseFormValues,
|
||||
groupId: string,
|
||||
): Promise<Expense> {
|
||||
const group = await getGroup(groupId)
|
||||
if (!group) throw new Error(`Invalid group ID: ${groupId}`)
|
||||
|
||||
for (const participant of [
|
||||
expenseFormValues.paidBy,
|
||||
...expenseFormValues.paidFor,
|
||||
]) {
|
||||
if (!group.participants.some((p) => p.id === participant))
|
||||
throw new Error(`Invalid participant ID: ${participant}`)
|
||||
}
|
||||
|
||||
return getPrisma().expense.create({
|
||||
data: {
|
||||
id: uuidv4(),
|
||||
groupId,
|
||||
amount: expenseFormValues.amount,
|
||||
title: expenseFormValues.title,
|
||||
paidById: expenseFormValues.paidBy,
|
||||
paidFor: {
|
||||
createMany: {
|
||||
data: expenseFormValues.paidFor.map((paidFor) => ({
|
||||
participantId: paidFor,
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function updateExpense(
|
||||
groupId: string,
|
||||
expenseId: string,
|
||||
expenseFormValues: ExpenseFormValues,
|
||||
) {
|
||||
const group = await getGroup(groupId)
|
||||
if (!group) throw new Error(`Invalid group ID: ${groupId}`)
|
||||
|
||||
const existingExpense = await getExpense(groupId, expenseId)
|
||||
if (!existingExpense) throw new Error(`Invalid expense ID: ${expenseId}`)
|
||||
|
||||
for (const participant of [
|
||||
expenseFormValues.paidBy,
|
||||
...expenseFormValues.paidFor,
|
||||
]) {
|
||||
if (!group.participants.some((p) => p.id === participant))
|
||||
throw new Error(`Invalid participant ID: ${participant}`)
|
||||
}
|
||||
|
||||
return getPrisma().expense.update({
|
||||
where: { id: expenseId },
|
||||
data: {
|
||||
amount: expenseFormValues.amount,
|
||||
title: expenseFormValues.title,
|
||||
paidById: expenseFormValues.paidBy,
|
||||
paidFor: {
|
||||
connectOrCreate: expenseFormValues.paidFor.map((paidFor) => ({
|
||||
where: {
|
||||
expenseId_participantId: { expenseId, participantId: paidFor },
|
||||
},
|
||||
create: { participantId: paidFor },
|
||||
})),
|
||||
deleteMany: existingExpense.paidFor.filter(
|
||||
(paidFor) =>
|
||||
!expenseFormValues.paidFor.some(
|
||||
(pf) => pf === paidFor.participantId,
|
||||
),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function updateGroup(
|
||||
groupId: string,
|
||||
groupFormValues: GroupFormValues,
|
||||
) {
|
||||
const existingGroup = await getGroup(groupId)
|
||||
if (!existingGroup) throw new Error('Invalid group ID')
|
||||
|
||||
return getPrisma().group.update({
|
||||
where: { id: groupId },
|
||||
data: {
|
||||
name: groupFormValues.name,
|
||||
participants: {
|
||||
deleteMany: existingGroup.participants.filter(
|
||||
(p) => !groupFormValues.participants.some((p2) => p2.id === p.id),
|
||||
),
|
||||
updateMany: groupFormValues.participants
|
||||
.filter((participant) => participant.id !== undefined)
|
||||
.map((participant) => ({
|
||||
where: { id: participant.id },
|
||||
data: {
|
||||
name: participant.name,
|
||||
},
|
||||
})),
|
||||
createMany: {
|
||||
data: groupFormValues.participants
|
||||
.filter((participant) => participant.id === undefined)
|
||||
.map((participant) => ({
|
||||
id: uuidv4(),
|
||||
name: participant.name,
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function getGroup(groupId: string) {
|
||||
return getPrisma().group.findUnique({
|
||||
where: { id: groupId },
|
||||
include: { participants: true },
|
||||
})
|
||||
}
|
||||
|
||||
export async function getGroupExpenses(groupId: string) {
|
||||
return getPrisma().expense.findMany({
|
||||
where: { groupId },
|
||||
include: { paidFor: { include: { participant: true } }, paidBy: true },
|
||||
})
|
||||
}
|
||||
|
||||
export async function getExpense(groupId: string, expenseId: string) {
|
||||
return getPrisma().expense.findUnique({
|
||||
where: { id: expenseId },
|
||||
include: { paidBy: true, paidFor: true },
|
||||
})
|
||||
}
|
||||
17
src/lib/prisma.ts
Normal file
17
src/lib/prisma.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
let prisma: PrismaClient
|
||||
|
||||
export function getPrisma() {
|
||||
if (!prisma) {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
prisma = new PrismaClient()
|
||||
} else {
|
||||
if (!(global as any).prisma) {
|
||||
;(global as any).prisma = new PrismaClient()
|
||||
}
|
||||
prisma = (global as any).prisma
|
||||
}
|
||||
}
|
||||
return prisma
|
||||
}
|
||||
50
src/lib/schemas.ts
Normal file
50
src/lib/schemas.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import * as z from 'zod'
|
||||
|
||||
export const groupFormSchema = z
|
||||
.object({
|
||||
name: z
|
||||
.string()
|
||||
.min(2, 'Enter at least two characters.')
|
||||
.max(50, 'Enter at most 50 characters.'),
|
||||
participants: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.string().optional(),
|
||||
name: z
|
||||
.string()
|
||||
.min(2, 'Enter at least two characters.')
|
||||
.max(50, 'Enter at most 50 characters.'),
|
||||
}),
|
||||
)
|
||||
.min(1),
|
||||
})
|
||||
.superRefine(({ participants }, ctx) => {
|
||||
participants.forEach((participant, i) => {
|
||||
participants.slice(0, i).forEach((otherParticipant) => {
|
||||
if (otherParticipant.name === participant.name) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
message: 'Another participant already has this name.',
|
||||
path: ['participants', i, 'name'],
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
export type GroupFormValues = z.infer<typeof groupFormSchema>
|
||||
|
||||
export const expenseFormSchema = z.object({
|
||||
title: z
|
||||
.string({ required_error: 'Please enter a title.' })
|
||||
.min(2, 'Enter at least two characters.'),
|
||||
amount: z.coerce
|
||||
.number({ required_error: 'You must enter an amount.' })
|
||||
.min(0.01, 'The amount must be higher than 0.01.'),
|
||||
paidBy: z.string({ required_error: 'You must select a participant.' }),
|
||||
paidFor: z
|
||||
.array(z.string())
|
||||
.min(1, 'The expense must be paid for at least 1 participant.'),
|
||||
})
|
||||
|
||||
export type ExpenseFormValues = z.infer<typeof expenseFormSchema>
|
||||
6
src/lib/utils.ts
Normal file
6
src/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
Reference in New Issue
Block a user