diff options
| author | Sebastien Castiel <sebastien@castiel.me> | 2023-12-08 14:56:51 -0500 |
|---|---|---|
| committer | Sebastien Castiel <sebastien@castiel.me> | 2023-12-08 14:56:51 -0500 |
| commit | 92a21ff4c5df6e9f771cfbd140835dce3b6d7b0d (patch) | |
| tree | 33f09b0069550f58e287a4e12a70f20ff1f50cea /src | |
| parent | 9cc819b0c900d82efb55b6bc8c9faa38d21c0197 (diff) | |
Migration script, improvements in expenses list
Diffstat (limited to 'src')
| -rw-r--r-- | src/app/groups/[groupId]/expenses/expense-list.tsx | 30 | ||||
| -rw-r--r-- | src/app/groups/recent-group-list.tsx | 22 | ||||
| -rw-r--r-- | src/lib/api.ts | 3 | ||||
| -rw-r--r-- | src/scripts/migrate.ts | 130 |
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) |
