diff --git a/app/icons/Filter.tsx b/app/icons/Filter.tsx index 62a9efa..dfa81ce 100644 --- a/app/icons/Filter.tsx +++ b/app/icons/Filter.tsx @@ -1,27 +1,55 @@ -const Filter = () => ( +const Filter = ({ + className, + active = false, +}: { + className?: string; + active?: boolean; +}) => ( - - + {active ? ( + <> + + + + ) : ( + <> + + + + + )} ); diff --git a/app/routes/expenses.tsx b/app/routes/expenses.tsx index 6fd393e..cf27fc7 100644 --- a/app/routes/expenses.tsx +++ b/app/routes/expenses.tsx @@ -30,7 +30,7 @@ export default function ExpensesRoute() { return ( <>
-
+
diff --git a/app/routes/expenses/index.tsx b/app/routes/expenses/index.tsx index 278b757..0d52514 100644 --- a/app/routes/expenses/index.tsx +++ b/app/routes/expenses/index.tsx @@ -148,11 +148,14 @@ export default function ExpensesIndexRoute() {
0 ? "text-error" : "text-success" }`} > - {Math.abs(user.dueAmount)} € + {user.dueAmount !== Math.round(user.dueAmount) + ? Math.abs(user.dueAmount).toFixed(2) + : Math.abs(user.dueAmount)}{" "} + €
{user.username}
diff --git a/app/routes/expenses/list.tsx b/app/routes/expenses/list.tsx index c787b8f..b711a8e 100644 --- a/app/routes/expenses/list.tsx +++ b/app/routes/expenses/list.tsx @@ -1,33 +1,262 @@ -import { Link } from "remix"; +import type { User, Team, Expense } from "@prisma/client"; +import type { LoaderFunction } from "remix"; +import { redirect, useLoaderData, useCatch, Link, Form } from "remix"; +import Filter from "~/icons/Filter"; +import { db } from "~/utils/db.server"; +import { getUser, requireUserId } from "~/utils/session.server"; + +type LoaderData = { + user: (User & { team: Team & { members: User[] } }) | null; + expenses: (Expense & { user: User & { team: Team } })[]; + expensesCount: number; + page: number; + filters: { + description: string | null | undefined; + dateFrom: string | null | undefined; + dateTo: string | null | undefined; + user: string | null | undefined; + }; +}; + +export const loader: LoaderFunction = async ({ request }) => { + const userId = requireUserId(request); + const user = await getUser(request); + if (!user?.id || !userId) { + return redirect("/login"); + } + + const expensesCount = await db.expense.count({ + where: { teamId: user.teamId }, + }); + + const searchParams = new URL(request.url)?.searchParams; + const page = parseInt(searchParams.get("page") || "1", 10); + const description = searchParams.get("description"); + const dateFrom = searchParams.get("dateFrom"); + const dateTo = searchParams.get("dateTo"); + const userIdParam = searchParams.get("user"); + + const filters = { + description: + description && description.length > 0 ? description : undefined, + dateFrom: dateFrom && dateFrom.length > 0 ? dateFrom : undefined, + dateTo: dateTo && dateTo.length > 0 ? dateTo : undefined, + user: userIdParam && userIdParam?.length > 0 ? userIdParam : undefined, + }; + const expensesFilters = { + ...(filters.description && { + description: { contains: filters.description }, + }), + ...((filters.dateFrom || filters.dateTo) && { + createdAt: { + ...(filters.dateFrom && { + gte: new Date(`${filters.dateFrom}T00:00:00+0100`), + }), + ...(filters.dateTo && { + lte: new Date(`${filters.dateTo}T00:00:00+0100`), + }), + }, + }), + ...(filters.user && { userId: filters.user }), + }; + console.log("FILTERS", filters); + + const expenses = await db.expense.findMany({ + where: { + teamId: user.teamId, + ...expensesFilters, + }, + take: 10, + skip: (page - 1) * 10, + orderBy: { + createdAt: "desc", + }, + include: { + user: { + include: { + team: true, + }, + }, + }, + }); + + const data: LoaderData = { + user, + expenses, + expensesCount, + page, + filters, + }; + return data; +}; export default function ListExpensesRoute() { + const data = useLoaderData(); + + const hasFilters = Object.values(data.filters).some( + (value) => value !== undefined && value !== null + ); + return ( -
-
-
-

Work in progress

-

- - This page is under construction. -

- - - - - Back - + <> +

List expenses

+ + + + +
+
+

Filters

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + Clear + + +
+
-
+ +
+ + + + + + + + + + + + {data.expenses?.map((exp) => ( + + + + + + + + ))} + +
DateUserAmountDescription
+ + See + + + {new Intl.DateTimeFormat("it", { + dateStyle: "short", + }).format(new Date(exp.createdAt))} + {exp.user.username}{exp.amount} €{exp.description}
+
+ + {data.expensesCount > 10 && ( +
+ {[...new Array(Math.ceil(data.expensesCount / 10)).keys()].map( + (p) => ( + + {p + 1} + + ) + )} +
+ )} + ); } + +export function CatchBoundary() { + const caught = useCatch(); + + if (caught.status === 401) { + return redirect("/login"); + } + if (caught.status === 404) { + return
There is no data to display.
; + } + throw new Error(`Unexpected caught response with status: ${caught.status}`); +} + +export function ErrorBoundary() { + return
I did a whoopsies.
; +} diff --git a/app/routes/statistics.tsx b/app/routes/statistics.tsx index 07631b4..8968701 100644 --- a/app/routes/statistics.tsx +++ b/app/routes/statistics.tsx @@ -83,8 +83,6 @@ export const loader: LoaderFunction = async ({ request }) => { {} ); - console.log(statsByMonth); - const data: LoaderData = { user, thisMonth: { @@ -116,12 +114,14 @@ export default function ListExpensesRoute() {
Average per month
-
{data.avg} €
+
{data.avg.toFixed(2)} €
This month
-
{data.thisMonth.amount} €
+
+ {data.thisMonth.amount.toFixed(2)} € +
{data.thisMonth.count} expenses