From c67282f38e93782cfe88c3bd0fba3c57ee1af321 Mon Sep 17 00:00:00 2001 From: Ricard Mallafre Date: Sun, 31 Mar 2024 15:12:49 +0200 Subject: [PATCH 1/2] fix(groups): properly upsert group We were removing all the links between users and groups when creating a new group but we should only delete those that belong to the current group being processed. --- src/server/api/routers/groups/groups.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/server/api/routers/groups/groups.ts b/src/server/api/routers/groups/groups.ts index 86139b8..16c108a 100644 --- a/src/server/api/routers/groups/groups.ts +++ b/src/server/api/routers/groups/groups.ts @@ -97,6 +97,7 @@ export const groupsRouter = createTRPCRouter({ await db.usersGroups.deleteMany({ where: { + groupId: group.id, userId: { notIn: [user.id, ...members], }, From fdb5484cbb944fa529dc5cbad93459c8df8e0081 Mon Sep 17 00:00:00 2001 From: Ricard Mallafre Date: Sun, 31 Mar 2024 16:27:42 +0200 Subject: [PATCH 2/2] feat(groups): show recent groups activity --- .../expenses/new/group-expense.client.tsx | 2 +- src/app/groups/[groupId]/group-activity.tsx | 111 ++++++++------- src/app/groups/[groupId]/group-list.tsx | 10 +- src/app/groups/my-groups.tsx | 8 +- src/app/groups/page.tsx | 14 +- src/app/groups/recent-group-activity.tsx | 9 -- src/app/groups/recent-groups-activity.tsx | 46 ++++++ src/server/api/routers/groups/groups.ts | 133 +++++++++++++++--- 8 files changed, 252 insertions(+), 81 deletions(-) delete mode 100644 src/app/groups/recent-group-activity.tsx create mode 100644 src/app/groups/recent-groups-activity.tsx diff --git a/src/app/groups/[groupId]/expenses/new/group-expense.client.tsx b/src/app/groups/[groupId]/expenses/new/group-expense.client.tsx index c634d57..366e5e7 100644 --- a/src/app/groups/[groupId]/expenses/new/group-expense.client.tsx +++ b/src/app/groups/[groupId]/expenses/new/group-expense.client.tsx @@ -224,7 +224,7 @@ export default function GroupExpenseForm({ group, user, expense }: GroupExpenseF }} /> -
+
{expense ? ( { diff --git a/src/app/groups/[groupId]/group-activity.tsx b/src/app/groups/[groupId]/group-activity.tsx index b01db64..690a21c 100644 --- a/src/app/groups/[groupId]/group-activity.tsx +++ b/src/app/groups/[groupId]/group-activity.tsx @@ -3,7 +3,14 @@ import currencySymbolMap from 'currency-symbol-map/map'; import { MoveRight } from 'lucide-react'; import Link from 'next/link'; import DateDisplay from '~/app/_components/date-display'; -import { GroupList, GroupListBody, GroupListItem, GroupListTitle } from '~/app/groups/[groupId]/group-list'; +import { + GroupList, + GroupListBody, + GroupListItem, + GroupListItemBody, + GroupListItemTitle, + GroupListTitle, +} from '~/app/groups/[groupId]/group-list'; import RegisterSettlement from '~/app/groups/[groupId]/register-settlement.client'; import { Avatar, AvatarFallback, AvatarImage } from '~/components/ui/avatar'; import type { RouterOutputs } from '~/trpc/shared'; @@ -39,12 +46,14 @@ export default function RecentGroupActivity({ ); } -function SharedTransactionView({ +export function SharedTransactionView({ sharedTransaction, user, + title, }: { user: RouterOutputs['users']['get']; sharedTransaction: RouterOutputs['groups']['expenses']['recent'][number]; + title?: string; }) { const userPaid = sharedTransaction.TransactionSplit.some((s) => s.user.id === user.id && s.paid > 0); const payers = sharedTransaction.TransactionSplit.filter((s) => s.paid > 0) @@ -59,40 +68,45 @@ function SharedTransactionView({ return ( -
- {payers.reverse().map((p) => ( - - - - - - - ))} -
-
-

- {formatter.format(payersNames)} paid{' '} - {sharedTransaction.transaction.amount / 100} - {currencySymbolMap[user.currency ?? 'EUR']} for{' '} - {sharedTransaction.transaction.description} -

-
- + {title ? {title} : null} + +
+ {payers.reverse().map((p) => ( + + + + + + + ))}
-
+
+

+ {formatter.format(payersNames)} paid{' '} + {sharedTransaction.transaction.amount / 100} + {currencySymbolMap[user.currency ?? 'EUR']} for{' '} + {sharedTransaction.transaction.description} +

+
+ +
+
+ ); } -function RegisteredSettlementView({ +export function RegisteredSettlementView({ settlement, user, group, + title, }: { group: Exclude; user: RouterOutputs['users']['get']; settlement: RouterOutputs['groups']['settlements']['recent'][number]; + title?: string; }) { const parts: string[] = []; if (settlement.from.id === user.id) { @@ -112,33 +126,36 @@ function RegisteredSettlementView({ return ( -
- - - - - - -
-

- {settlement.amount / 100} {currencySymbolMap[user.currency ?? 'EUR']} -

- + {title ? {title} : null} + +
+ + + + + + +
+

+ {settlement.amount / 100} {currencySymbolMap[user.currency ?? 'EUR']} +

+ +
+ + + + + +
- - - - - - -
-
-

{parts.join(' ')}

-
- +
+

{parts.join(' ')}

+
+ +
-
+ ); diff --git a/src/app/groups/[groupId]/group-list.tsx b/src/app/groups/[groupId]/group-list.tsx index db54023..81f64dc 100644 --- a/src/app/groups/[groupId]/group-list.tsx +++ b/src/app/groups/[groupId]/group-list.tsx @@ -32,5 +32,13 @@ export function GroupListBody({ children }: { children: React.ReactNode }) { } export function GroupListItem({ children }: { children: React.ReactNode }) { - return
{children}
; + return
{children}
; +} + +export function GroupListItemTitle({ children }: { children: React.ReactNode }) { + return

{children}

; +} + +export function GroupListItemBody({ children }: { children: React.ReactNode }) { + return
{children}
; } diff --git a/src/app/groups/my-groups.tsx b/src/app/groups/my-groups.tsx index ada04eb..b103281 100644 --- a/src/app/groups/my-groups.tsx +++ b/src/app/groups/my-groups.tsx @@ -2,11 +2,13 @@ import { UsersRound } from 'lucide-react'; import Link from 'next/link'; import { Avatar, AvatarFallback, AvatarImage } from '~/components/ui/avatar'; import { cn } from '~/lib/utils'; -import { api } from '~/trpc/server'; +import type { RouterOutputs } from '~/trpc/shared'; -export default async function MyGroups() { - const groups = await api.groups.all.query(); +export type MyGroupsProps = { + groups: RouterOutputs['groups']['all']['get']; +}; +export default async function MyGroups({ groups }: MyGroupsProps) { return (
diff --git a/src/app/groups/page.tsx b/src/app/groups/page.tsx index d22d58f..f6e9453 100644 --- a/src/app/groups/page.tsx +++ b/src/app/groups/page.tsx @@ -1,9 +1,17 @@ import Link from 'next/link'; import MyGroups from '~/app/groups/my-groups'; -import RecentGroupActivity from '~/app/groups/recent-group-activity'; +import RecentGroupsActivity from '~/app/groups/recent-groups-activity'; import { Button } from '~/components/ui/button'; +import { api } from '~/trpc/server'; export default async function GroupsPage() { + const [user, groups, expenses, settlements] = await Promise.all([ + api.users.get.query(), + api.groups.all.get.query(), + api.groups.all.expenses.recent.query(), + api.groups.all.settlements.recent.query(), + ]); + return ( <>
@@ -12,8 +20,8 @@ export default async function GroupsPage() {
- - + +
); diff --git a/src/app/groups/recent-group-activity.tsx b/src/app/groups/recent-group-activity.tsx deleted file mode 100644 index 3c4531c..0000000 --- a/src/app/groups/recent-group-activity.tsx +++ /dev/null @@ -1,9 +0,0 @@ -export default function RecentGroupActivity() { - return ( -
-
-

Recent activity

-
-
- ); -} diff --git a/src/app/groups/recent-groups-activity.tsx b/src/app/groups/recent-groups-activity.tsx new file mode 100644 index 0000000..a2b6b50 --- /dev/null +++ b/src/app/groups/recent-groups-activity.tsx @@ -0,0 +1,46 @@ +import { RegisteredSettlementView, SharedTransactionView } from '~/app/groups/[groupId]/group-activity'; +import { GroupList, GroupListBody, GroupListTitle } from '~/app/groups/[groupId]/group-list'; +import type { RouterOutputs } from '~/trpc/shared'; + +export type RecentGroupsActivityProps = { + groups: RouterOutputs['groups']['all']['get']; + user: RouterOutputs['users']['get']; + expenses: RouterOutputs['groups']['all']['expenses']['recent']; + settlements: RouterOutputs['groups']['all']['settlements']['recent']; +}; + +export default function RecentGroupsActivity({ user, groups, expenses, settlements }: RecentGroupsActivityProps) { + const settlementsListItems = settlements.map((s) => ({ + date: s.date, + id: `settlement-${s.id}`, + component: ( + g.id === s.groupId)?.name} + {...{ settlement: s, user, group: groups.find((g) => g.id === s.group.id)! }} + /> + ), + })); + const sharedTransactionsListItems = expenses.map((e) => ({ + date: e.transaction.date, + id: `expense-${e.id}`, + component: ( + g.id === e.groupId)?.name} + key={e.id} + {...{ sharedTransaction: e, user }} + /> + ), + })); + + const allItems = [...settlementsListItems, ...sharedTransactionsListItems].sort( + (a, b) => b.date.getTime() - a.date.getTime(), + ); + + return ( + + Recent activity + {allItems.slice(0, 8).map((item) => item.component)} + + ); +} diff --git a/src/server/api/routers/groups/groups.ts b/src/server/api/routers/groups/groups.ts index 16c108a..00496dd 100644 --- a/src/server/api/routers/groups/groups.ts +++ b/src/server/api/routers/groups/groups.ts @@ -1,3 +1,4 @@ +import { settings } from '.eslintrc.cjs'; import { TRPCError } from '@trpc/server'; import { log } from 'next-axiom'; import { z } from 'zod'; @@ -53,23 +54,6 @@ export const groupsRouter = createTRPCRouter({ }); }), - all: privateProcedure.query(async ({ ctx: { db, user } }) => { - return db.group.findMany({ - where: { - UserGroup: { - some: { - user, - }, - }, - }, - select: { - id: true, - name: true, - description: true, - }, - }); - }), - upsert: privateProcedure .input( z.object({ @@ -249,6 +233,121 @@ export const groupsRouter = createTRPCRouter({ expenses: groupExpensesRouter, settlements: groupSettlementsRouter, + + all: createTRPCRouter({ + get: privateProcedure.query(async ({ ctx: { db, user } }) => { + return db.group.findMany({ + where: { + UserGroup: { + some: { + user, + }, + }, + }, + include: { + UserGroup: { + include: { + user: { + select: { + id: true, + username: true, + firstName: true, + lastName: true, + imageUrl: true, + }, + }, + }, + }, + }, + }); + }), + + expenses: createTRPCRouter({ + recent: privateProcedure.query(async ({ ctx: { db, user } }) => { + return db.sharedTransaction.findMany({ + where: { + group: { + UserGroup: { + some: { + user, + }, + }, + }, + }, + select: { + id: true, + groupId: true, + createdById: true, + transactionId: true, + transaction: true, + TransactionSplit: { + include: { + user: { + select: { + id: true, + username: true, + firstName: true, + lastName: true, + imageUrl: true, + }, + }, + }, + }, + }, + orderBy: { + transaction: { + date: 'desc', + }, + }, + }); + }), + }), + + settlements: createTRPCRouter({ + recent: privateProcedure.query(async ({ ctx: { db, user } }) => { + return db.settlement.findMany({ + where: { + group: { + UserGroup: { + some: { + user, + }, + }, + }, + }, + include: { + from: { + select: { + id: true, + username: true, + firstName: true, + lastName: true, + imageUrl: true, + }, + }, + to: { + select: { + id: true, + username: true, + firstName: true, + lastName: true, + imageUrl: true, + }, + }, + group: { + select: { + id: true, + name: true, + }, + }, + }, + orderBy: { + date: 'desc', + }, + }); + }), + }), + }), }); export async function assertUserInGroup({ groupId, userId }: { groupId: string; userId: string }) {