Skip to content

Commit b4a953f

Browse files
authored
add edit expense (#150)
* add edit expense * add edit expense changes * fix build * fix edit not working on exact match * set share to zero * fix deleting user on edit not working * fix auto load to expense page * make split share nice
1 parent e00a6b2 commit b4a953f

File tree

16 files changed

+747
-189
lines changed

16 files changed

+747
-189
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-- AlterTable
2+
ALTER TABLE "Expense" ADD COLUMN "updatedBy" INTEGER;
3+
4+
-- AddForeignKey
5+
ALTER TABLE "Expense" ADD CONSTRAINT "Expense_updatedBy_fkey" FOREIGN KEY ("updatedBy") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;

prisma/schema.prisma

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ model User {
6262
paidExpenses Expense[] @relation("PaidByUser")
6363
addedExpenses Expense[] @relation("AddedByUser")
6464
deletedExpenses Expense[] @relation("DeletedByUser")
65+
updatedExpenses Expense[] @relation("UpdatedByUser")
6566
}
6667

6768
model VerificationToken {
@@ -149,10 +150,12 @@ model Expense {
149150
groupId Int?
150151
deletedAt DateTime?
151152
deletedBy Int?
153+
updatedBy Int?
152154
group Group? @relation(fields: [groupId], references: [id], onDelete: Cascade)
153155
paidByUser User @relation(name: "PaidByUser", fields: [paidBy], references: [id], onDelete: Cascade)
154156
addedByUser User @relation(name: "AddedByUser", fields: [addedBy], references: [id], onDelete: Cascade)
155157
deletedByUser User? @relation(name: "DeletedByUser", fields: [deletedBy], references: [id], onDelete: Cascade)
158+
updatedByUser User? @relation(name: "UpdatedByUser", fields: [updatedBy], references: [id], onDelete: SetNull)
156159
expenseParticipants ExpenseParticipant[]
157160
expenseNotes ExpenseNote[]
158161

src/components/AddExpense/AddExpensePage.tsx

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -106,13 +106,13 @@ const categories = {
106106
},
107107
};
108108

109-
export const AddExpensePage: React.FC<{
109+
export const AddOrEditExpensePage: React.FC<{
110110
isStorageConfigured: boolean;
111111
enableSendingInvites: boolean;
112-
}> = ({ isStorageConfigured, enableSendingInvites }) => {
112+
expenseId?: string;
113+
}> = ({ isStorageConfigured, enableSendingInvites, expenseId }) => {
113114
const [date, setDate] = React.useState<Date | undefined>(new Date());
114115
const [open, setOpen] = React.useState(false);
115-
const [amtStr, setAmountStr] = React.useState('');
116116

117117
const showFriends = useAddExpenseStore((s) => s.showFriends);
118118
const amount = useAddExpenseStore((s) => s.amount);
@@ -122,13 +122,20 @@ export const AddExpensePage: React.FC<{
122122
const category = useAddExpenseStore((s) => s.category);
123123
const description = useAddExpenseStore((s) => s.description);
124124
const isFileUploading = useAddExpenseStore((s) => s.isFileUploading);
125+
const amtStr = useAddExpenseStore((s) => s.amountStr);
125126

126-
const { setCurrency, setCategory, setDescription, setAmount, resetState } = useAddExpenseStore(
127-
(s) => s.actions,
128-
);
127+
const {
128+
setCurrency,
129+
setCategory,
130+
setDescription,
131+
setAmount,
132+
setAmountStr,
133+
resetState,
134+
setSplitScreenOpen,
135+
} = useAddExpenseStore((s) => s.actions);
129136

130-
const addExpenseMutation = api.user.addExpense.useMutation();
131-
const addGroupExpenseMutation = api.group.addExpense.useMutation();
137+
const addExpenseMutation = api.user.addOrEditExpense.useMutation();
138+
const addGroupExpenseMutation = api.group.addOrEditExpense.useMutation();
132139
const updateProfile = api.user.updateUserDetail.useMutation();
133140

134141
const router = useRouter();
@@ -140,11 +147,17 @@ export const AddExpensePage: React.FC<{
140147
}
141148

142149
function addExpense() {
143-
const { group, paidBy, splitType, fileKey } = useAddExpenseStore.getState();
150+
const { group, paidBy, splitType, fileKey, canSplitScreenClosed } =
151+
useAddExpenseStore.getState();
144152
if (!paidBy) {
145153
return;
146154
}
147155

156+
if (!canSplitScreenClosed) {
157+
setSplitScreenOpen(true);
158+
return;
159+
}
160+
148161
if (group) {
149162
addGroupExpenseMutation.mutate(
150163
{
@@ -161,12 +174,13 @@ export const AddExpensePage: React.FC<{
161174
category,
162175
fileKey,
163176
expenseDate: date,
177+
expenseId,
164178
},
165179
{
166180
onSuccess: (d) => {
167181
if (d) {
168182
router
169-
.push(`/groups/${group.id}/expenses/${d?.id}`)
183+
.push(`/groups/${group.id}/expenses/${d?.id ?? expenseId}`)
170184
.then(() => resetState())
171185
.catch(console.error);
172186
}
@@ -176,6 +190,7 @@ export const AddExpensePage: React.FC<{
176190
} else {
177191
addExpenseMutation.mutate(
178192
{
193+
expenseId,
179194
name: description,
180195
currency,
181196
amount,
@@ -191,10 +206,9 @@ export const AddExpensePage: React.FC<{
191206
},
192207
{
193208
onSuccess: (d) => {
194-
resetState();
195209
if (participants[1] && d) {
196210
router
197-
.push(`/balances/${participants[1]?.id}/expenses/${d?.id}`)
211+
.push(`expenses/${d?.id ?? expenseId}`)
198212
.then(() => resetState())
199213
.catch(console.error);
200214
}
@@ -237,7 +251,7 @@ export const AddExpensePage: React.FC<{
237251
Save
238252
</Button>{' '}
239253
</div>
240-
<UserInput />
254+
<UserInput isEditing={!!expenseId} />
241255
{showFriends || (participants.length === 1 && !group) ? (
242256
<SelectUserOrGroup enableSendingInvites={enableSendingInvites} />
243257
) : (

src/components/AddExpense/SplitTypeSection.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ export const SplitTypeSection: React.FC = () => {
1313
const currentUser = useAddExpenseStore((s) => s.currentUser);
1414
const canSplitScreenClosed = useAddExpenseStore((s) => s.canSplitScreenClosed);
1515
const splitType = useAddExpenseStore((s) => s.splitType);
16+
const splitScreenOpen = useAddExpenseStore((s) => s.splitScreenOpen);
1617

17-
const { setPaidBy } = useAddExpenseStore((s) => s.actions);
18+
const { setPaidBy, setSplitScreenOpen } = useAddExpenseStore((s) => s.actions);
1819

1920
return (
2021
<div className="mt-4 flex items-center justify-center text-[16px] text-gray-400">
@@ -63,6 +64,8 @@ export const SplitTypeSection: React.FC = () => {
6364
dismissible={false}
6465
actionTitle="Save"
6566
actionDisabled={!canSplitScreenClosed}
67+
open={splitScreenOpen}
68+
onOpenChange={(open) => setSplitScreenOpen(open)}
6669
>
6770
<SplitExpenseForm />
6871
</AppDrawer>

src/components/AddExpense/UserInput.tsx

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import { z } from 'zod';
44
import { api } from '~/utils/api';
55
import Router from 'next/router';
66

7-
export const UserInput: React.FC = () => {
7+
export const UserInput: React.FC<{
8+
isEditing?: boolean;
9+
}> = ({ isEditing }) => {
810
const {
911
setNameOrEmail,
1012
removeLastParticipant,
@@ -85,17 +87,20 @@ export const UserInput: React.FC = () => {
8587
<input
8688
type="email"
8789
placeholder={
88-
group
89-
? 'Press delete to remove group'
90-
: participants.length > 1
91-
? 'Add more friends'
92-
: 'Search friends, groups or add email'
90+
isEditing && !!group
91+
? 'Cannot change group while editing'
92+
: group
93+
? 'Press delete to remove group'
94+
: participants.length > 1
95+
? 'Add more friends'
96+
: 'Search friends, groups or add email'
9397
}
9498
value={nameOrEmail}
9599
onChange={(e) => setNameOrEmail(e.target.value)}
96100
onKeyDown={handleKeyDown}
97101
className="min-w-[100px] flex-grow bg-transparent outline-none placeholder:text-sm focus:ring-0"
98102
autoFocus
103+
disabled={isEditing && !!group}
99104
/>
100105
</div>
101106
);

src/components/Expense/ExpensePage.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ type ExpenseDetailsProps = {
1818
addedByUser: User;
1919
paidByUser: User;
2020
deletedByUser: User | null;
21+
updatedByUser: User | null;
2122
};
2223
storagePublicUrl?: string;
2324
};
@@ -42,6 +43,12 @@ const ExpenseDetails: React.FC<ExpenseDetailsProps> = ({ user, expense, storageP
4243
{!isSameDay(expense.expenseDate, expense.createdAt) ? (
4344
<p className="text-sm text-gray-500">{format(expense.expenseDate, 'dd MMM yyyy')}</p>
4445
) : null}
46+
{expense.updatedByUser ? (
47+
<p className=" text-sm text-gray-500">
48+
Edited by {expense.updatedByUser?.name ?? expense.updatedByUser?.email} on{' '}
49+
{format(expense.updatedAt, 'dd MMM yyyy')}
50+
</p>
51+
) : null}
4552
{expense.deletedByUser ? (
4653
<p className=" text-sm text-orange-600">
4754
Deleted by {expense.deletedByUser.name ?? expense.addedByUser.email} on{' '}

src/components/Friend/Settleup.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export const SettleUp: React.FC<{
3030
setAmount(toFixedNumber(Math.abs(balance.amount)).toString());
3131
}
3232

33-
const addExpenseMutation = api.user.addExpense.useMutation();
33+
const addExpenseMutation = api.user.addOrEditExpense.useMutation();
3434
const utils = api.useUtils();
3535

3636
function saveExpense() {

src/pages/add.tsx

Lines changed: 63 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,32 @@
11
import Head from 'next/head';
22
import { useRouter } from 'next/router';
33
import React, { useEffect } from 'react';
4-
import { AddExpensePage } from '~/components/AddExpense/AddExpensePage';
4+
import { AddOrEditExpensePage } from '~/components/AddExpense/AddExpensePage';
55
import MainLayout from '~/components/Layout/MainLayout';
66
import { env } from '~/env';
77
import { isStorageConfigured } from '~/server/storage';
8-
import { useAddExpenseStore } from '~/store/addStore';
8+
import { calculateSplitShareBasedOnAmount, useAddExpenseStore } from '~/store/addStore';
99
import { type NextPageWithUser } from '~/types';
1010
import { api } from '~/utils/api';
11+
import { toFixedNumber, toInteger } from '~/utils/numbers';
1112

1213
// 🧾
1314

1415
const AddPage: NextPageWithUser<{
1516
isStorageConfigured: boolean;
1617
enableSendingInvites: boolean;
1718
}> = ({ user, isStorageConfigured, enableSendingInvites }) => {
18-
const { setCurrentUser, setGroup, setParticipants, setCurrency } = useAddExpenseStore(
19-
(s) => s.actions,
20-
);
19+
const {
20+
setCurrentUser,
21+
setGroup,
22+
setParticipants,
23+
setCurrency,
24+
setAmount,
25+
setDescription,
26+
setPaidBy,
27+
setAmountStr,
28+
setSplitType,
29+
} = useAddExpenseStore((s) => s.actions);
2130
const currentUser = useAddExpenseStore((s) => s.currentUser);
2231

2332
useEffect(() => {
@@ -32,11 +41,11 @@ const AddPage: NextPageWithUser<{
3241
}, []);
3342

3443
const router = useRouter();
35-
const { friendId, groupId } = router.query;
44+
const { friendId, groupId, expenseId } = router.query;
3645

3746
const _groupId = parseInt(groupId as string);
3847
const _friendId = parseInt(friendId as string);
39-
48+
const _expenseId = expenseId as string;
4049
const groupQuery = api.group.getGroupDetails.useQuery(
4150
{ groupId: _groupId },
4251
{ enabled: !!_groupId },
@@ -47,6 +56,11 @@ const AddPage: NextPageWithUser<{
4756
{ enabled: !!_friendId },
4857
);
4958

59+
const expenseQuery = api.user.getExpenseDetails.useQuery(
60+
{ expenseId: _expenseId },
61+
{ enabled: !!_expenseId, refetchOnWindowFocus: false },
62+
);
63+
5064
useEffect(() => {
5165
// Set group
5266
if (groupId && !groupQuery.isLoading && groupQuery.data && currentUser) {
@@ -69,16 +83,56 @@ const AddPage: NextPageWithUser<{
6983
// eslint-disable-next-line react-hooks/exhaustive-deps
7084
}, [friendId, friendQuery.isLoading, friendQuery.data, currentUser]);
7185

86+
useEffect(() => {
87+
if (_expenseId && expenseQuery.data) {
88+
console.log(
89+
'expenseQuery.data 123',
90+
expenseQuery.data.expenseParticipants,
91+
expenseQuery.data.splitType,
92+
calculateSplitShareBasedOnAmount(
93+
toFixedNumber(expenseQuery.data.amount),
94+
expenseQuery.data.expenseParticipants.map((ep) => ({
95+
...ep.user,
96+
amount: toFixedNumber(ep.amount),
97+
})),
98+
expenseQuery.data.splitType,
99+
expenseQuery.data.paidByUser,
100+
),
101+
);
102+
expenseQuery.data.group && setGroup(expenseQuery.data.group);
103+
setParticipants(
104+
calculateSplitShareBasedOnAmount(
105+
toFixedNumber(expenseQuery.data.amount),
106+
expenseQuery.data.expenseParticipants.map((ep) => ({
107+
...ep.user,
108+
amount: toFixedNumber(ep.amount),
109+
})),
110+
expenseQuery.data.splitType,
111+
expenseQuery.data.paidByUser,
112+
),
113+
);
114+
setCurrency(expenseQuery.data.currency);
115+
setAmountStr(toFixedNumber(expenseQuery.data.amount).toString());
116+
setDescription(expenseQuery.data.name);
117+
setPaidBy(expenseQuery.data.paidByUser);
118+
setAmount(toFixedNumber(expenseQuery.data.amount));
119+
setSplitType(expenseQuery.data.splitType);
120+
useAddExpenseStore.setState({ showFriends: false });
121+
}
122+
// eslint-disable-next-line react-hooks/exhaustive-deps
123+
}, [_expenseId, expenseQuery.data]);
124+
72125
return (
73126
<>
74127
<Head>
75128
<title>Add Expense</title>
76129
</Head>
77130
<MainLayout hideAppBar>
78-
{currentUser ? (
79-
<AddExpensePage
131+
{currentUser && (!_expenseId || expenseQuery.data) ? (
132+
<AddOrEditExpensePage
80133
isStorageConfigured={isStorageConfigured}
81134
enableSendingInvites={enableSendingInvites}
135+
expenseId={_expenseId}
82136
/>
83137
) : (
84138
<div></div>
@@ -93,8 +147,6 @@ AddPage.auth = true;
93147
export default AddPage;
94148

95149
export async function getServerSideProps() {
96-
console.log('isStorageConfigured', isStorageConfigured());
97-
98150
return {
99151
props: {
100152
isStorageConfigured: !!isStorageConfigured(),

src/pages/balances/[friendId]/expenses/[expenseId].tsx

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ import MainLayout from '~/components/Layout/MainLayout';
33
import { api } from '~/utils/api';
44
import Link from 'next/link';
55
import { useRouter } from 'next/router';
6-
import { ChevronLeftIcon, Trash, Trash2 } from 'lucide-react';
6+
import { ChevronLeftIcon, PencilIcon, Trash, Trash2 } from 'lucide-react';
77
import ExpenseDetails from '~/components/Expense/ExpensePage';
88
import { DeleteExpense } from '~/components/Expense/DeleteExpense';
99
import { type NextPageWithUser } from '~/types';
1010
import { env } from '~/env';
11+
import { Button } from '~/components/ui/button';
1112

1213
const ExpensesPage: NextPageWithUser<{ storagePublicUrl?: string }> = ({
1314
user,
@@ -35,11 +36,18 @@ const ExpensesPage: NextPageWithUser<{ storagePublicUrl?: string }> = ({
3536
</div>
3637
}
3738
actions={
38-
<DeleteExpense
39-
expenseId={expenseId}
40-
friendId={friendId}
41-
groupId={expenseQuery.data?.groupId ?? undefined}
42-
/>
39+
<div className="flex items-center gap-1">
40+
<DeleteExpense
41+
expenseId={expenseId}
42+
friendId={friendId}
43+
groupId={expenseQuery.data?.groupId ?? undefined}
44+
/>
45+
<Link href={`/add?expenseId=${expenseId}`}>
46+
<Button variant="ghost">
47+
<PencilIcon className="mr-1 h-4 w-4" />
48+
</Button>
49+
</Link>
50+
</div>
4351
}
4452
>
4553
{expenseQuery.data ? (

0 commit comments

Comments
 (0)