From ab3125b36fde0e9c4bdf64d428e38ec469670249 Mon Sep 17 00:00:00 2001 From: KM Koushik Date: Wed, 27 Mar 2024 21:13:21 +1100 Subject: [PATCH] Add download splitpro data (#35) --- src/pages/account.tsx | 36 +++++++++++++- src/server/api/routers/user.ts | 19 +++++++- src/server/api/services/splitService.ts | 64 +++++++++++++++++++++++++ 3 files changed, 116 insertions(+), 3 deletions(-) diff --git a/src/pages/account.tsx b/src/pages/account.tsx index fa1bceb..7812093 100644 --- a/src/pages/account.tsx +++ b/src/pages/account.tsx @@ -3,7 +3,7 @@ import MainLayout from '~/components/Layout/MainLayout'; import { Button } from '~/components/ui/button'; import Link from 'next/link'; import { UserAvatar } from '~/components/ui/avatar'; -import { Bell, ChevronRight, Download, Github, Star } from 'lucide-react'; +import { Bell, ChevronRight, Download, FileDown, Github, Star } from 'lucide-react'; import { signOut } from 'next-auth/react'; import { AppDrawer } from '~/components/ui/drawer'; import { SubmitFeedback } from '~/components/Account/SubmitFeedback'; @@ -13,9 +13,27 @@ import { type NextPageWithUser } from '~/types'; import { toast } from 'sonner'; import { env } from '~/env'; import { SubscribeNotification } from '~/components/Account/SubscribeNotification'; +import { useState } from 'react'; +import { LoadingSpinner } from '~/components/ui/spinner'; const AccountPage: NextPageWithUser = ({ user }) => { const userQuery = api.user.me.useQuery(); + const downloadQuery = api.user.downloadData.useMutation(); + + const [downloading, setDownloading] = useState(false); + + async function downloadData() { + setDownloading(true); + const data = await downloadQuery.mutateAsync(); + const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = 'splitpro_data.json'; + link.click(); + URL.revokeObjectURL(url); + setDownloading(false); + } return ( <> @@ -136,6 +154,22 @@ const AccountPage: NextPageWithUser = ({ user }) => {

+
diff --git a/src/server/api/routers/user.ts b/src/server/api/routers/user.ts index 0d738f8..8d53629 100644 --- a/src/server/api/routers/user.ts +++ b/src/server/api/routers/user.ts @@ -1,14 +1,20 @@ -import { SplitType } from '@prisma/client'; +import { type Balance, SplitType } from '@prisma/client'; import { boolean, z } from 'zod'; import { createTRPCRouter, protectedProcedure } from '~/server/api/trpc'; import { db } from '~/server/db'; -import { addUserExpense, deleteExpense } from '../services/splitService'; +import { + addUserExpense, + deleteExpense, + getCompleteFriendsDetails, + getCompleteGroupDetails, +} from '../services/splitService'; import { TRPCError } from '@trpc/server'; import { randomUUID } from 'crypto'; import { getDocumentUploadUrl } from '~/server/storage'; import { FILE_SIZE_LIMIT } from '~/lib/constants'; import { sendFeedbackEmail } from '~/server/mailer'; import { pushNotification } from '~/server/notification'; +import { toFixedNumber, toUIString } from '~/utils/numbers'; export const userRouter = createTRPCRouter({ me: protectedProcedure.query(async ({ ctx }) => { @@ -441,4 +447,13 @@ export const userRouter = createTRPCRouter({ }, }); }), + + downloadData: protectedProcedure.mutation(async ({ ctx }) => { + const user = ctx.session.user; + + const friends = await getCompleteFriendsDetails(user.id); + const groups = await getCompleteGroupDetails(user.id); + + return { friends, groups }; + }), }); diff --git a/src/server/api/services/splitService.ts b/src/server/api/services/splitService.ts index 6e240ac..31075bd 100644 --- a/src/server/api/services/splitService.ts +++ b/src/server/api/services/splitService.ts @@ -551,3 +551,67 @@ export async function sendExpensePushNotification(expenseId: string) { await Promise.all(pushNotifications); } + +export async function getCompleteFriendsDetails(userId: number) { + const balances = await db.balance.findMany({ + where: { + userId, + }, + include: { + friend: true, + }, + }); + + const friends = balances.reduce( + (acc, balance) => { + const friendId = balance.friendId; + if (!acc[friendId]) { + acc[friendId] = { + balances: [], + id: balance.friendId, + email: balance.friend.email, + name: balance.friend.name, + }; + } + + if (balance.amount !== 0) { + acc[friendId]?.balances.push({ + currency: balance.currency, + amount: + balance.amount > 0 ? toFixedNumber(balance.amount) : toFixedNumber(balance.amount), + }); + } + + return acc; + }, + {} as Record< + number, + { + id: number; + email?: string | null; + name?: string | null; + balances: { currency: string; amount: number }[]; + } + >, + ); + + return friends; +} + +export async function getCompleteGroupDetails(userId: number) { + const groups = await db.group.findMany({ + where: { + groupUsers: { + some: { + userId, + }, + }, + }, + include: { + groupUsers: true, + groupBalances: true, + }, + }); + + return groups; +}