Build a Finance SaaS Platform -17 (Transaction Page)

Add Transaction Page

Add app/(dashboard)/transactions/page.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
"use client";

import { Loader2, Plus } from "lucide-react";

import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";

import { columns } from "./columns";
import { DataTable } from "@/components/data-table";
import { useNewTransaction } from "@/features/transactions/hooks/use-new-transaction";
import { useBulkDeleteTransactions } from "@/features/transactions/api/use-bulk-delete-transactions";
import { useGetTransactions } from "@/features/transactions/api/use-get-transactions";

const TransactionsPage = ()=> {
const newTransaction = useNewTransaction();
const deleteTransaction = useBulkDeleteTransactions();
const TransactionQuery = useGetTransactions();
const Transactions = TransactionQuery.data || [];

const isDisabled = TransactionQuery.isLoading || deleteTransaction.isPending;

if( TransactionQuery.isLoading) {
return (
<div className="max-w-screen-2xl max-auto w-full pb-10 -mt-24">
<Card className="border-none drop-shadow-sm">
<CardHeader>
<Skeleton className="h-8 w-48" />
</CardHeader>
<CardContent>
<div className="h-[500px] w-full flex items-center">
<Loader2 className="size-6 text-slate-300 animate-spin" />
</div>
</CardContent>
</Card>
</div>
)
};

return (
<div className="max-w-screen-2xl mx-auto w-full pb-10 -mt-24">
<Card className="border-none drop-shadow-sm">
<CardHeader className="gap-y-2 lg:flex-row lg:items-center lg:justify-between">
<CardTitle className="text-xl line-clamp-1">
Transaction History
</CardTitle>
<Button onClick={newTransaction.onOpen} size="sm">
<Plus className="size-4 mr-2" />
Add New
</Button>
</CardHeader>
<CardContent>
<DataTable
filterKey = "name"
columns={columns}
data={ Transactions }
onDelete={(rows)=>{
const ids = rows.map((r) => r.id)
deleteTransaction.mutate({ ids });
}}
disabled={isDisabled}
/>
</CardContent>
</Card>
</div>
);
};

export default TransactionsPage;

Add Column and Column Components

Add app/(dashboard)/transactions/account-column.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { useOpenAccount } from "@/features/accounts/hooks/use-open-account";

import { cn } from "@/lib/utils";

type Props = {
account: string;
accountId: string;
}

export const AccountColumn = ({ account, accountId }: Props) =>{
const { onOpen: onOpenAccount } = useOpenAccount();

const onClick = () => {
onOpenAccount(accountId);
}

return (
<div
onClick={onClick}
className="flex items-center course-pointer hover:underline"
>
{account}
</div>
)
}

Add app/(dashboard)/transactions/category-column.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import { TriangleAlert } from "lucide-react";

import { useOpenCategory } from "@/features/categories/hooks/use-open-category";
import { useOpenTransaction } from "@/features/transactions/hooks/use-open-transaction";

import { cn } from "@/lib/utils";

type Props = {
id: string;
category: string | null;
categoryId: string| null;
}

export const CategoryColumn = ({ id, category, categoryId }: Props) =>{
const { onOpen: onOpenCategory } = useOpenCategory();
const { onOpen: onOpenTransaction } = useOpenTransaction();

const onClick = () => {
if (categoryId){
onOpenCategory(categoryId);
} else {
onOpenTransaction(id);
}
};

return (
<div
onClick={onClick}
className={cn("flex items-center course-pointer hover:underline",
!category && "text-rose-500",
)}
>
{!category && <TriangleAlert className="mr-2 size-4 shrink-0" />}
{category || "Uncategorized"}
</div>
)
}

Add app/(dashboard)/transactions/actions.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
"use client"

import { Edit, MoreHorizontal, Trash } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"

import { useOpenTransaction } from "@/features/transactions/hooks/use-open-transaction";
import { useDeleteTransaction } from "@/features/transactions/api/use-delete-transaction";

import { useConfirm } from "@/hooks/use-confirm";
type Props = {
id: string;
}

export const Actions = ({ id }: Props) => {
const [ConfirmDialog, confirm] = useConfirm(
"Are you sure?",
"You are about to delete this transaction"
);

const deleteMutation = useDeleteTransaction(id);
const { onOpen } = useOpenTransaction();

const handleDelete = async () => {
const ok = await confirm();

if (ok) {
deleteMutation.mutate();
}
}

return (
<>
<ConfirmDialog />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="size-8 p-0">
<MoreHorizontal className="size-4"/>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
disabled={deleteMutation.isPending}
onClick={() => onOpen(id)}
>
<Edit className="size-4 mr-2"/>
Edit
</DropdownMenuItem>
<DropdownMenuItem
disabled={deleteMutation.isPending}
onClick={handleDelete}
>
<Trash className="size-4 mr-2"/>
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</>
);
}

Add app/(dashboard)/transactions/columns.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
"use client"
import { InferResponseType } from "hono";
import { ColumnDef } from "@tanstack/react-table";
import { ArrowUpDown } from "lucide-react";
import { format } from "date-fns";

import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Badge } from "@/components/ui/badge";

import { Actions } from "./actions";
import { client } from "@/lib/hono";
import { formatCurrency } from "@/lib/utils";
import { AccountColumn } from "./account-column";
import { CategoryColumn } from "./category-column";

export type ResponsType = InferResponseType<typeof client.api.transactions.$get, 200>["data"][0];

export const columns: ColumnDef<ResponsType>[] = [
{
id: "select",
header: ({ table }) => (
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
/>
),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: "date",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Date
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
)
},
cell: ({ row }) => {
const date = row.getValue("date") as Date;

return (
<span>
{format(date, "dd MMMM,yyyy")}
</span>
)
}
},
{
accessorKey: "category",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Category
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
)
},
cell: ({ row }) => {
return (
<CategoryColumn
id = {row.original.id}
category={row.original.category}
categoryId={row.original.categoryId}
/>
);
}
},
{
accessorKey: "payee",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Payee
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
)
}
},
{
accessorKey: "amount",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Amount
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
)
},
cell: ({ row }) => {
const amount = parseFloat(row.getValue("amount"));

return (
<Badge
variant={amount > 0 ? "primary" : "destructive"}
className="text-xs font-medium px-3.5 py-2.5"
>
{formatCurrency(amount)}
</Badge>
)
}
},
{
accessorKey: "account",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Account
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
)
},
cell: ({ row }) => {
return (
<AccountColumn
account= { row.original.account}
accountId = { row.original.accountId }
/>
)
}
},
{
id: "actions",
cell: ({ row }) => <Actions id={row.original.id} />
}
]

请我喝杯咖啡吧~

支付宝
微信