aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorSebastien Castiel <sebastien@castiel.me>2023-12-08 14:56:51 -0500
committerSebastien Castiel <sebastien@castiel.me>2023-12-08 14:56:51 -0500
commit92a21ff4c5df6e9f771cfbd140835dce3b6d7b0d (patch)
tree33f09b0069550f58e287a4e12a70f20ff1f50cea /src
parent9cc819b0c900d82efb55b6bc8c9faa38d21c0197 (diff)
Migration script, improvements in expenses list
Diffstat (limited to 'src')
-rw-r--r--src/app/groups/[groupId]/expenses/expense-list.tsx30
-rw-r--r--src/app/groups/recent-group-list.tsx22
-rw-r--r--src/lib/api.ts3
-rw-r--r--src/scripts/migrate.ts130
4 files changed, 168 insertions, 17 deletions
diff --git a/src/app/groups/[groupId]/expenses/expense-list.tsx b/src/app/groups/[groupId]/expenses/expense-list.tsx
index 13a85bd..798a5f8 100644
--- a/src/app/groups/[groupId]/expenses/expense-list.tsx
+++ b/src/app/groups/[groupId]/expenses/expense-list.tsx
@@ -1,5 +1,4 @@
'use client'
-import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { getGroupExpenses } from '@/lib/api'
import { cn } from '@/lib/utils'
@@ -7,6 +6,7 @@ import { Participant } from '@prisma/client'
import { ChevronRight } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
+import { Fragment } from 'react'
type Props = {
expenses: Awaited<ReturnType<typeof getGroupExpenses>>
@@ -37,22 +37,32 @@ export function ExpenseList({
}}
>
<div>
- <div className="mb-1">{expense.title}</div>
+ <div className={cn('mb-1', expense.isReimbursement && 'italic')}>
+ {expense.title}
+ </div>
<div className="text-xs text-muted-foreground">
- Paid by{' '}
- <Badge variant="secondary">
- {getParticipant(expense.paidById)?.name}
- </Badge>{' '}
+ Paid by <strong>{getParticipant(expense.paidById)?.name}</strong>{' '}
for{' '}
{expense.paidFor.map((paidFor, index) => (
- <Badge variant="secondary" key={index} className="mr-1 mb-1">
- {participants.find((p) => p.id === paidFor.participantId)?.name}
- </Badge>
+ <Fragment key={index}>
+ {index !== 0 && <>, </>}
+ <strong>
+ {
+ participants.find((p) => p.id === paidFor.participantId)
+ ?.name
+ }
+ </strong>
+ </Fragment>
))}
</div>
</div>
<div className="flex items-center">
- <div className="tabular-nums whitespace-nowrap font-bold">
+ <div
+ className={cn(
+ 'tabular-nums whitespace-nowrap',
+ expense.isReimbursement ? 'italic' : 'font-bold',
+ )}
+ >
{currency} {(expense.amount / 100).toFixed(2)}
</div>
<Button size="icon" variant="link" className="-my-2" asChild>
diff --git a/src/app/groups/recent-group-list.tsx b/src/app/groups/recent-group-list.tsx
index ef43372..92bc743 100644
--- a/src/app/groups/recent-group-list.tsx
+++ b/src/app/groups/recent-group-list.tsx
@@ -2,7 +2,7 @@
import { getRecentGroups } from '@/app/groups/recent-groups-helpers'
import { Button } from '@/components/ui/button'
import { getGroups } from '@/lib/api'
-import { Loader2 } from 'lucide-react'
+import { Calendar, Loader2, Users } from 'lucide-react'
import Link from 'next/link'
import { useEffect, useState } from 'react'
import { z } from 'zod'
@@ -58,14 +58,24 @@ export function RecentGroupList({ getGroupsAction }: Props) {
<li key={group.id}>
<Button variant="outline" className="h-fit w-full py-3" asChild>
<Link href={`/groups/${group.id}`} className="text-base">
- <div className="w-full flex flex-col items-start gap-1">
+ <div className="w-full flex flex-col gap-1">
<div className="text-base">{group.name}</div>
<div className="text-muted-foreground font-normal text-xs">
{details ? (
- <>
- {details._count.participants} participants ยท{' '}
- {details.currency}
- </>
+ <div className="w-full flex items-center justify-between">
+ <div className="flex items-center">
+ <Users className="w-3 h-3 inline mr-1" />
+ <span>{details._count.participants}</span>
+ </div>
+ <div className="flex items-center">
+ <Calendar className="w-3 h-3 inline mx-1" />
+ <span>
+ {details.createdAt.toLocaleDateString('en-US', {
+ dateStyle: 'medium',
+ })}
+ </span>
+ </div>
+ </div>
) : (
<>
<Loader2 className="w-3 h-3 mr-1 inline animate-spin" />
diff --git a/src/lib/api.ts b/src/lib/api.ts
index 33e3eac..b6a8d65 100644
--- a/src/lib/api.ts
+++ b/src/lib/api.ts
@@ -2,7 +2,7 @@ import { getPrisma } from '@/lib/prisma'
import { ExpenseFormValues, GroupFormValues } from '@/lib/schemas'
import { Expense } from '@prisma/client'
-function randomId() {
+export function randomId() {
return Math.random().toString(36).slice(2, 9)
}
@@ -185,6 +185,7 @@ export async function getGroupExpenses(groupId: string) {
return prisma.expense.findMany({
where: { groupId },
include: { paidFor: { include: { participant: true } }, paidBy: true },
+ orderBy: { createdAt: 'desc' },
})
}
diff --git a/src/scripts/migrate.ts b/src/scripts/migrate.ts
new file mode 100644
index 0000000..bf7d93c
--- /dev/null
+++ b/src/scripts/migrate.ts
@@ -0,0 +1,130 @@
+// @ts-nocheck
+import { randomId } from '@/lib/api'
+import { getPrisma } from '@/lib/prisma'
+import { Prisma } from '@prisma/client'
+import { Client } from 'pg'
+
+process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
+
+async function main() {
+ withClient(async (client) => {
+ const prisma = await getPrisma()
+
+ const { rows: groupRows } = await client.query<{
+ id: string
+ name: string
+ currency: string
+ created_at: Date
+ }>('select id, name, currency, created_at from groups')
+
+ for (const groupRow of groupRows) {
+ const participants: Prisma.ParticipantCreateManyInput[] = []
+ const expenses: Prisma.ExpenseCreateManyInput[] = []
+ const expenseParticipants: Prisma.ExpensePaidForCreateManyInput[] = []
+ const participantIdsMapping: Record<number, string> = {}
+ const expenseIdsMapping: Record<number, string> = {}
+
+ const existingGroup = await prisma.group.findUnique({
+ where: { id: groupRow.id },
+ })
+ if (existingGroup) {
+ console.log(`Group ${groupRow.id} already exists, skipping.`)
+ continue
+ }
+
+ const group: Prisma.GroupCreateInput = {
+ id: groupRow.id,
+ name: groupRow.name,
+ currency: groupRow.currency,
+ createdAt: groupRow.created_at,
+ }
+
+ const { rows: participantRows } = await client.query<{
+ id: number
+ created_at: Date
+ name: string
+ }>(
+ 'select id, created_at, name from participants where group_id = $1::text',
+ [groupRow.id],
+ )
+ for (const participantRow of participantRows) {
+ const id = randomId()
+ participantIdsMapping[participantRow.id] = id
+ participants.push({
+ id,
+ groupId: groupRow.id,
+ name: participantRow.name,
+ })
+ }
+
+ const { rows: expenseRows } = await client.query<{
+ id: number
+ created_at: Date
+ description: string
+ amount: number
+ paid_by_participant_id: number
+ is_reimbursement: boolean
+ }>(
+ 'select id, created_at, description, amount, paid_by_participant_id, is_reimbursement from expenses where group_id = $1::text',
+ [groupRow.id],
+ )
+ for (const expenseRow of expenseRows) {
+ const id = randomId()
+ expenseIdsMapping[expenseRow.id] = id
+ expenses.push({
+ id,
+ amount: Math.round(expenseRow.amount * 100),
+ groupId: groupRow.id,
+ title: expenseRow.description,
+ createdAt: expenseRow.created_at,
+ isReimbursement: expenseRow.is_reimbursement === true,
+ paidById: participantIdsMapping[expenseRow.paid_by_participant_id],
+ })
+ }
+
+ if (expenseRows.length > 0) {
+ const { rows: expenseParticipantRows } = await client.query<{
+ expense_id: number
+ participant_id: number
+ }>(
+ 'select expense_id, participant_id from expense_participants where expense_id = any($1::int[]);',
+ [expenseRows.map((row) => row.id)],
+ )
+ for (const expenseParticipantRow of expenseParticipantRows) {
+ expenseParticipants.push({
+ expenseId: expenseIdsMapping[expenseParticipantRow.expense_id],
+ participantId:
+ participantIdsMapping[expenseParticipantRow.participant_id],
+ })
+ }
+ }
+
+ console.log('Creating group:', group)
+ await prisma.group.create({ data: group })
+ console.log('Creating participants:', participants)
+ await prisma.participant.createMany({ data: participants })
+ console.log('Creating expenses:', expenses)
+ await prisma.expense.createMany({ data: expenses })
+ console.log('Creating expenseParticipants:', expenseParticipants)
+ await prisma.expensePaidFor.createMany({ data: expenseParticipants })
+ }
+ })
+}
+
+async function withClient(fn: (client: Client) => void | Promise<void>) {
+ const client = new Client({
+ connectionString: process.env.OLD_POSTGRES_URL,
+ ssl: true,
+ })
+ await client.connect()
+ console.log('Connected.')
+
+ try {
+ await fn(client)
+ } finally {
+ await client.end()
+ console.log('Disconnected.')
+ }
+}
+
+main().catch(console.error)