diff --git a/app/(auth)/budgets/edit/[id]/loading.tsx b/app/(auth)/budgets/edit/[id]/loading.tsx new file mode 100644 index 0000000..8c4aa13 --- /dev/null +++ b/app/(auth)/budgets/edit/[id]/loading.tsx @@ -0,0 +1,5 @@ +import FormSkeleton from "@/components/skeletons/form"; + +export default function Loading() { + return ; +} diff --git a/app/(auth)/budgets/page.tsx b/app/(auth)/budgets/page.tsx index 046d080..c8e0fa2 100644 --- a/app/(auth)/budgets/page.tsx +++ b/app/(auth)/budgets/page.tsx @@ -1,40 +1,24 @@ import AddButton from "@/components/add-button"; -import BudgetsList from "@/components/budgets/list"; -import EmptyList from "@/components/empty-list"; +import { BudgetsContainer } from "@/components/budgets/container"; import PageTitle from "@/components/page-title"; -import { Card, CardContent } from "@/components/ui/card"; -import { getBudgets, getExpensesByCategory } from "@/lib/dal"; -import { getCurrentMonthRange } from "@/lib/utils"; +import BudgetsSkeleton from "@/components/skeletons/budgets"; import type { Metadata } from "next"; +import { Suspense } from "react"; export const metadata: Metadata = { title: "Budgets" }; export default async function BudgetsPage() { - const currentDate = new Date(); - const monthRange = getCurrentMonthRange(currentDate); - - const [budgets, expenses] = await Promise.all([ - getBudgets(), - getExpensesByCategory(monthRange.start, monthRange.end) - ]); - return ( <>
- {budgets.length === 0 ? ( - - - - - - ) : ( - - )} + }> + + ); } diff --git a/app/(auth)/dashboard/page.tsx b/app/(auth)/dashboard/page.tsx index 84a7202..2fe408e 100644 --- a/app/(auth)/dashboard/page.tsx +++ b/app/(auth)/dashboard/page.tsx @@ -1,22 +1,11 @@ import { Card, CardContent, CardHeader } from "@/components/ui/card"; -import { getExpensesByDateRange, getFirstExpense } from "@/lib/dal"; -import { - buildChartData, - calculateTotalAmount, - createDateFromDay, - getCurrentMonthRange, - parseSelectedDay, - parseSelectedMonth -} from "@/lib/utils"; -import FormattedAmount from "@/components/formatted-amount"; import ExpensesList from "@/components/expenses/list"; -import DailyBarChart from "@/components/daily-bar-chart"; -import { format, getDate } from "date-fns"; -import MonthSelect from "@/components/month-select"; -import { filterExpensesByDay } from "@/lib/utils"; +import Chart from "@/components/dashboard/chart"; import type { Metadata } from "next"; import EmptyList from "@/components/empty-list"; import PageTitle from "@/components/page-title"; +import ChartHeader from "@/components/dashboard/chart-header"; +import { getDashboardData } from "@/lib/dashboard-data"; export const metadata: Metadata = { title: "Dashboard" @@ -25,60 +14,33 @@ export const metadata: Metadata = { export default async function Page({ searchParams }: { - searchParams: Promise<{ [key: string]: string | string[] | undefined }>; + searchParams: Promise<{ day?: string; month?: string }>; }) { const params = await searchParams; - const currentDate = parseSelectedMonth(params.month) || new Date(); - const monthRange = getCurrentMonthRange(currentDate); - const selectedDayParam = parseSelectedDay(params.day); - const highlightedDate = selectedDayParam - ? createDateFromDay(selectedDayParam, currentDate) - : null; - - const [monthlyExpenses, firstExpense] = await Promise.all([ - getExpensesByDateRange(monthRange.start, currentDate), - getFirstExpense() - ]); - - const totalSpent = calculateTotalAmount(monthlyExpenses); - let selectedExpenses = monthlyExpenses; - let amountSpent = totalSpent / getDate(currentDate); - - if (selectedDayParam) { - const dayExpenses = filterExpensesByDay(monthlyExpenses, selectedDayParam); - const dayTotal = calculateTotalAmount(dayExpenses); - - selectedExpenses = dayExpenses ?? []; - amountSpent = dayTotal ?? 0; - } - - const chartData = buildChartData(monthlyExpenses, getDate(monthRange.end)); + const { + totalSpent, + amountSpent, + selectedExpenses, + highlightedDate, + chartData, + monthSelectOptions + } = await getDashboardData(params.month, params.day); return ( <> -
- - -
-
-

- {highlightedDate === null - ? "Spent/day" - : format(highlightedDate, "d MMM yyyy")} -

- -
+
- +
diff --git a/app/(auth)/expenses/edit/[id]/loading.tsx b/app/(auth)/expenses/edit/[id]/loading.tsx new file mode 100644 index 0000000..8c4aa13 --- /dev/null +++ b/app/(auth)/expenses/edit/[id]/loading.tsx @@ -0,0 +1,5 @@ +import FormSkeleton from "@/components/skeletons/form"; + +export default function Loading() { + return ; +} diff --git a/app/(auth)/expenses/page.tsx b/app/(auth)/expenses/page.tsx index b974b01..447bd7f 100644 --- a/app/(auth)/expenses/page.tsx +++ b/app/(auth)/expenses/page.tsx @@ -1,8 +1,15 @@ import type { Metadata } from "next"; -import { notFound } from "next/navigation"; import PageTitle from "@/components/page-title"; import AddButton from "@/components/add-button"; -import PaginatedExpensesList from "@/components/expenses/paginated-list"; +import ExpensesListWrapper from "@/components/expenses/list-wrapper"; +import { Suspense } from "react"; +import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card"; +import SearchBar from "@/components/expenses/search-bar"; +import { getExpensesPages } from "@/lib/dal"; +import PaginationControls from "@/components/pagination-controls"; +import ExpensesSkeleton from "@/components/skeletons/expenses"; + +const PAGE_SIZE = 15; export const metadata: Metadata = { title: "Expenses" @@ -11,15 +18,13 @@ export const metadata: Metadata = { export default async function Page({ searchParams }: { - searchParams: Promise<{ page: string; query: string }>; + searchParams: Promise<{ page?: string; query?: string }>; }) { const params = await searchParams; - const page = params.page ? Number(params.page) : 1; - const query = params.query; + const page = Number(params.page) || 1; + const query = params.query || ""; - if (isNaN(page) || page < 1) { - notFound(); - } + const totalPages = await getExpensesPages(query, PAGE_SIZE); return ( <> @@ -27,7 +32,21 @@ export default async function Page({ - + + + + + + }> + + + + {totalPages > 1 && ( + + + + )} + ); } diff --git a/components/budgets/container.tsx b/components/budgets/container.tsx new file mode 100644 index 0000000..6ee5bed --- /dev/null +++ b/components/budgets/container.tsx @@ -0,0 +1,29 @@ +import BudgetsList from "@/components/budgets/list"; +import EmptyList from "@/components/empty-list"; +import { Card, CardContent } from "@/components/ui/card"; +import { getBudgets, getExpensesByCategory } from "@/lib/dal"; +import { getCurrentMonthRange } from "@/lib/utils"; + +export async function BudgetsContainer() { + const currentDate = new Date(); + const monthRange = getCurrentMonthRange(currentDate); + + const [budgets, expenses] = await Promise.all([ + getBudgets(), + getExpensesByCategory(monthRange.start, monthRange.end) + ]); + + return ( + <> + {budgets.length === 0 ? ( + + + + + + ) : ( + + )} + + ); +} diff --git a/components/dashboard/chart-header.tsx b/components/dashboard/chart-header.tsx new file mode 100644 index 0000000..ead1c73 --- /dev/null +++ b/components/dashboard/chart-header.tsx @@ -0,0 +1,36 @@ +import FormattedAmount from "@/components/formatted-amount"; +import { format } from "date-fns"; +import MonthSelect from "@/components/dashboard/month-select"; + +type Props = { + totalSpent: number; + amountSpent: number; + highlightedDate: Date | null; + monthSelectOptions: { value: string; label: string }[]; +}; + +export default async function ChartHeader({ + totalSpent, + amountSpent, + highlightedDate, + monthSelectOptions +}: Props) { + return ( + <> +
+ + +
+
+

+ {highlightedDate === null ? "Spent/day" : format(highlightedDate, "d MMM yyyy")} +

+ +
+ + ); +} diff --git a/components/daily-bar-chart.tsx b/components/dashboard/chart.tsx similarity index 96% rename from components/daily-bar-chart.tsx rename to components/dashboard/chart.tsx index 6f04f5c..f2e40e8 100644 --- a/components/daily-bar-chart.tsx +++ b/components/dashboard/chart.tsx @@ -17,7 +17,7 @@ function formatToK(num: number) { return num.toString(); } -export default function DailyBarChart({ chartData }: Props) { +export default function Chart({ chartData }: Props) { const searchParams = useSearchParams(); const router = useRouter(); diff --git a/components/month-select.tsx b/components/dashboard/month-select.tsx similarity index 60% rename from components/month-select.tsx rename to components/dashboard/month-select.tsx index 666f7f7..0aa9d80 100644 --- a/components/month-select.tsx +++ b/components/dashboard/month-select.tsx @@ -8,38 +8,22 @@ import { SelectTrigger, SelectValue } from "@/components/ui/select"; -import { useEffect, useState } from "react"; -import { buildMonthSelectOptions } from "@/lib/utils"; -import { format } from "date-fns"; -const now = new Date(); -const defaultOptions = [ - { - label: format(now, "MMM yyyy"), - value: format(now, "yyyy-MM") - } -]; +type Props = { + options: { + value: string; + label: string; + }[]; +}; -export default function MonthSelect({ startDate }: { startDate?: Date }) { - const [options, setOptions] = - useState>(defaultOptions); +export default function MonthSelect({ options }: Props) { const searchParams = useSearchParams(); const router = useRouter(); const selectedMonth = searchParams.get("month"); - useEffect(() => { - if (!startDate) { - return; - } - const monthOptions = buildMonthSelectOptions(startDate); - setOptions(monthOptions); - }, [startDate]); - const handleChange = (value: string) => { const params = new URLSearchParams(); - if (value !== defaultOptions[0].value) { - params.set("month", value); - } + params.set("month", value); router.push(`?${params.toString()}`); }; diff --git a/components/expenses/list-wrapper.tsx b/components/expenses/list-wrapper.tsx new file mode 100644 index 0000000..d8cc2f2 --- /dev/null +++ b/components/expenses/list-wrapper.tsx @@ -0,0 +1,21 @@ +import { getPaginatedExpenses } from "@/lib/dal"; +import EmptyList from "@/components/empty-list"; +import ExpensesList from "@/components/expenses/list"; + +export default async function ExpensesListWrapper({ + query, + currentPage, + pageSize +}: { + query?: string; + currentPage: number; + pageSize: number; +}) { + const expenses = await getPaginatedExpenses(currentPage, pageSize, query); + + if (expenses.length === 0) { + return ; + } + + return ; +} diff --git a/components/expenses/paginated-list.tsx b/components/expenses/paginated-list.tsx deleted file mode 100644 index aa246b5..0000000 --- a/components/expenses/paginated-list.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { notFound } from "next/navigation"; -import { getPaginatedExpenses } from "@/lib/dal"; -import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card"; -import EmptyList from "@/components/empty-list"; -import ExpensesList from "@/components/expenses/list"; -import PaginationControls from "@/components/pagination-controls"; -import SearchBar from "@/components/expenses/search-bar"; - -export default async function PaginatedExpensesList({ - query, - page -}: { - query: string; - page: number; -}) { - const expensesData = await getPaginatedExpenses(page, 15, query); - - const { expenses, totalPages, currentPage, hasNextPage, hasPreviousPage } = - expensesData; - - if (currentPage > totalPages && totalPages > 0) { - notFound(); - } - - return ( - - - - - - {expenses.length === 0 ? : } - - {totalPages > 1 && ( - - - - )} - - ); -} diff --git a/components/pagination-controls.tsx b/components/pagination-controls.tsx index 04da72a..3555b67 100644 --- a/components/pagination-controls.tsx +++ b/components/pagination-controls.tsx @@ -11,22 +11,17 @@ import { PaginationPrevious } from "@/components/ui/pagination"; -type PaginationControlsProps = { - currentPage: number; +type Props = { totalPages: number; - hasNextPage: boolean; - hasPreviousPage: boolean; }; -export default function PaginationControls({ - currentPage, - totalPages, - hasNextPage, - hasPreviousPage -}: PaginationControlsProps) { +export default function PaginationControls({ totalPages }: Props) { const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); + const currentPage = Number(searchParams.get("page")) || 1; + const hasNextPage = currentPage < totalPages; + const hasPreviousPage = currentPage > 1; const createPageURL = (page: number) => { const params = new URLSearchParams(searchParams.toString()); @@ -78,12 +73,10 @@ export default function PaginationControls({ { - if (!hasPreviousPage) { - e.preventDefault(); - return; - } e.preventDefault(); - navigateToPage(currentPage - 1); + if (hasPreviousPage) { + navigateToPage(currentPage - 1); + } }} aria-disabled={!hasPreviousPage} className={!hasPreviousPage ? "pointer-events-none opacity-50" : ""} @@ -113,12 +106,10 @@ export default function PaginationControls({ { - if (!hasNextPage) { - e.preventDefault(); - return; - } e.preventDefault(); - navigateToPage(currentPage + 1); + if (hasNextPage) { + navigateToPage(currentPage + 1); + } }} aria-disabled={!hasNextPage} className={!hasNextPage ? "pointer-events-none opacity-50" : ""} diff --git a/components/skeletons/budgets.tsx b/components/skeletons/budgets.tsx new file mode 100644 index 0000000..df97905 --- /dev/null +++ b/components/skeletons/budgets.tsx @@ -0,0 +1,37 @@ +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Skeleton } from "@/components/ui/skeleton"; + +export default function BudgetsSkeleton() { + return ( +
+ + + + + + + +
+ + + +
+
+
+ + + + + + + +
+ + + +
+
+
+
+ ); +} diff --git a/components/skeletons/expenses.tsx b/components/skeletons/expenses.tsx new file mode 100644 index 0000000..1b68d8b --- /dev/null +++ b/components/skeletons/expenses.tsx @@ -0,0 +1,31 @@ +import { Skeleton } from "@/components/ui/skeleton"; + +export default function ExpensesSkeleton() { + return ( +
+
+ +
+ + + +
+
+
+ +
+ +
+
+
+ +
+ + + + +
+
+
+ ); +} diff --git a/components/skeletons/form.tsx b/components/skeletons/form.tsx new file mode 100644 index 0000000..fef1912 --- /dev/null +++ b/components/skeletons/form.tsx @@ -0,0 +1,46 @@ +import { + Card, + CardAction, + CardContent, + CardFooter, + CardHeader, + CardTitle +} from "@/components/ui/card"; +import { Skeleton } from "@/components/ui/skeleton"; + +export default function FormSkeleton() { + return ( + + + + + + + + + + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+ + +
+
+
+ ); +} diff --git a/lib/dal.ts b/lib/dal.ts index b5463b7..08cfb7d 100644 --- a/lib/dal.ts +++ b/lib/dal.ts @@ -108,61 +108,54 @@ export async function getExpensesByCategory( } } +export async function getExpensesPages(query: string, pageSize: number): Promise { + const session = await verifySession(); + + try { + const data = await prisma.expense.count({ + where: { + userId: session.userId, + item: { startsWith: query, mode: "insensitive" } + } + }); + + return Math.ceil(data / pageSize); + } catch (error) { + throw error; + } +} export async function getPaginatedExpenses( - page: number = 1, - pageSize: number = 15, - startsWith: string = "" -): Promise<{ - expenses: ExpenseWithColor[]; - totalPages: number; - currentPage: number; - hasNextPage: boolean; - hasPreviousPage: boolean; -}> { + page: number, + pageSize: number, + query: string = "" +): Promise { const session = await verifySession(); try { - const validPage = Math.max(1, page); - const skip = (validPage - 1) * pageSize; + const skip = (page - 1) * pageSize; - const [data, totalCount] = await Promise.all([ - prisma.expense.findMany({ - where: { - userId: session.userId, - item: { startsWith, mode: "insensitive" } - }, - select: { - id: true, - item: true, - value: true, - category: { - select: { - color: true - } - }, - createdAt: true + const data = await prisma.expense.findMany({ + where: { + userId: session.userId, + item: { startsWith: query, mode: "insensitive" } + }, + select: { + id: true, + item: true, + value: true, + category: { + select: { + color: true + } }, - orderBy: { createdAt: "desc" }, - skip, - take: pageSize - }), - prisma.expense.count({ - where: { - userId: session.userId, - item: { startsWith } - } - }) - ]); - - const totalPages = Math.ceil(totalCount / pageSize); + createdAt: true + }, + orderBy: { createdAt: "desc" }, + skip, + take: pageSize + }); - return { - expenses: data, - totalPages, - currentPage: validPage, - hasNextPage: validPage < totalPages, - hasPreviousPage: validPage > 1 - }; + return data; } catch (error) { throw error; } diff --git a/lib/dashboard-data.ts b/lib/dashboard-data.ts new file mode 100644 index 0000000..4b22c14 --- /dev/null +++ b/lib/dashboard-data.ts @@ -0,0 +1,50 @@ +import { getExpensesByDateRange, getFirstExpense } from "@/lib/dal"; +import { + parseSelectedMonth, + parseSelectedDay, + getCurrentMonthRange, + filterExpensesByDay, + calculateTotalAmount, + createDateFromDay, + buildChartData, + buildMonthSelectOptions +} from "@/lib/utils"; +import { getDate } from "date-fns"; + +export async function getDashboardData(month?: string, day?: string) { + const currentDate = parseSelectedMonth(month) || new Date(); + const selectedDayParam = parseSelectedDay(day); + const monthRange = getCurrentMonthRange(currentDate); + + const [monthlyExpenses, firstExpense] = await Promise.all([ + getExpensesByDateRange(monthRange.start, currentDate), + getFirstExpense() + ]); + + const totalSpent = calculateTotalAmount(monthlyExpenses); + + const selectedExpenses = selectedDayParam + ? filterExpensesByDay(monthlyExpenses, selectedDayParam) + : monthlyExpenses; + + const amountSpent = selectedDayParam + ? calculateTotalAmount(selectedExpenses) + : totalSpent / getDate(currentDate); + + const highlightedDate = selectedDayParam + ? createDateFromDay(selectedDayParam, currentDate) + : null; + + const monthSelectOptions = buildMonthSelectOptions(firstExpense?.createdAt); + const chartData = buildChartData(monthlyExpenses, getDate(monthRange.end)); + + return { + totalSpent, + amountSpent, + selectedExpenses, + highlightedDate, + chartData, + monthSelectOptions, + monthRange + }; +} diff --git a/lib/utils/chart.ts b/lib/utils/chart.ts index d3362b3..e606dc5 100644 --- a/lib/utils/chart.ts +++ b/lib/utils/chart.ts @@ -26,10 +26,10 @@ export function buildChartData( return chartData; } -export function buildMonthSelectOptions(date: Date) { +export function buildMonthSelectOptions(date?: Date) { const now = new Date(); - const startYear = getYear(date); - const startMonth = getMonth(date); + const startYear = getYear(date || now); + const startMonth = getMonth(date || now); const endYear = getYear(now); const endMonth = getMonth(now); diff --git a/next.config.ts b/next.config.ts index e9ffa30..7a07296 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,7 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + reactCompiler: true }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index 0061adb..ea3ba06 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,6 +53,7 @@ "@types/react": "19", "@types/react-dom": "19", "@types/supertest": "6.0.3", + "babel-plugin-react-compiler": "^1.0.0", "dotenv": "17.2.3", "eslint": "9", "eslint-config-next": "15.5.4", @@ -85,6 +86,40 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@date-fns/tz": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.4.1.tgz", @@ -4643,6 +4678,16 @@ "node": ">= 0.4" } }, + "node_modules/babel-plugin-react-compiler": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz", + "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", diff --git a/package.json b/package.json index 38fb7a9..fe5e867 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "@types/react": "19", "@types/react-dom": "19", "@types/supertest": "6.0.3", + "babel-plugin-react-compiler": "^1.0.0", "dotenv": "17.2.3", "eslint": "9", "eslint-config-next": "15.5.4", diff --git a/tests/integration/expenses.test.ts b/tests/integration/expenses.test.ts index 441e4be..c674c64 100644 --- a/tests/integration/expenses.test.ts +++ b/tests/integration/expenses.test.ts @@ -4,7 +4,8 @@ import { getExpenseById, getPaginatedExpenses, getExpensesByDateRange, - getExpensesByCategory + getExpensesByCategory, + getExpensesPages } from "@/lib/dal"; import { encrypt } from "@/lib/auth/session"; @@ -49,29 +50,23 @@ describe("expenses", () => { }); mockGet.mockReturnValue({ value: token }); - const result = await getPaginatedExpenses(); + const result = await getPaginatedExpenses(1, 5); - expect(result).toMatchObject({ - currentPage: 1, - expenses: [ - { - item: "Fruits", - value: 100, - category: { - color: foodCategory.color - } - }, - { - item: "Meat", - value: 50, - category: { - color: foodCategory.color - } - } - ], - hasNextPage: false, - hasPreviousPage: false, - totalPages: 1 + expect(result).toHaveLength(2); + + expect(result[0]).toMatchObject({ + item: "Fruits", + value: 100, + category: { + color: foodCategory.color + } + }); + expect(result[1]).toMatchObject({ + item: "Meat", + value: 50, + category: { + color: foodCategory.color + } }); }); @@ -106,28 +101,21 @@ describe("expenses", () => { mockGet.mockReturnValue({ value: token }); const result = await getPaginatedExpenses(2, 5); + expect(result).toHaveLength(2); - expect(result).toMatchObject({ - currentPage: 2, - expenses: [ - { - item: "Item6", - value: 60, - category: { - color: foodCategory.color - } - }, - { - item: "Item7", - value: 70, - category: { - color: foodCategory.color - } - } - ], - hasNextPage: false, - hasPreviousPage: true, - totalPages: 2 + expect(result[0]).toMatchObject({ + item: "Item6", + value: 60, + category: { + color: foodCategory.color + } + }); + expect(result[1]).toMatchObject({ + item: "Item7", + value: 70, + category: { + color: foodCategory.color + } }); }); @@ -165,14 +153,12 @@ describe("expenses", () => { }); mockGet.mockReturnValue({ value: token }); - const result = await getPaginatedExpenses(); + const result = await getPaginatedExpenses(1, 5); - expect(result).toMatchObject({ - currentPage: 1, - expenses: [{ item: "Item1", value: 100, category: { color: category1.color } }], - hasNextPage: false, - hasPreviousPage: false, - totalPages: 1 + expect(result[0]).toMatchObject({ + item: "Item1", + value: 100, + category: { color: category1.color } }); }); @@ -190,15 +176,72 @@ describe("expenses", () => { }); mockGet.mockReturnValue({ value: token }); - const result = await getPaginatedExpenses(); + const result = await getPaginatedExpenses(1, 5); - expect(result).toEqual({ - currentPage: 1, - expenses: [], - hasNextPage: false, - hasPreviousPage: false, - totalPages: 0 + expect(result).toEqual([]); + }); + }); + + describe("#getExpensesPages", () => { + it("should return number of expenses pages", async () => { + const user = await prisma.user.create({ + data: { + email: "test@example.com", + password: { create: { hash: "hashed" } } + } }); + + const foodCategory = await prisma.category.create({ + data: { name: "Food", color: "#FF0000", userId: user.id } + }); + + await prisma.expense.createMany({ + data: [ + { + value: 100, + userId: user.id, + categoryId: foodCategory.id, + item: "Item Fruits" + }, + { + value: 90, + userId: user.id, + categoryId: foodCategory.id, + item: "Item Cookies" + }, + { value: 70, userId: user.id, categoryId: foodCategory.id, item: "Vegetables" }, + { value: 50, userId: user.id, categoryId: foodCategory.id, item: "Meat" } + ] + }); + + const token = await encrypt({ + userId: user.id, + expiresAt: new Date(Date.now() + 1000000) + }); + mockGet.mockReturnValue({ value: token }); + + const result = await getExpensesPages("ite", 2); + + expect(result).toEqual(1); + }); + + it("should return zero when user have no expenses", async () => { + const user = await prisma.user.create({ + data: { + email: "test@example.com", + password: { create: { hash: "hashed" } } + } + }); + + const token = await encrypt({ + userId: user.id, + expiresAt: new Date(Date.now() + 1000000) + }); + mockGet.mockReturnValue({ value: token }); + + const result = await getExpensesPages("", 5); + + expect(result).toEqual(0); }); });