2022-02-10 10:44:44 +01:00
|
|
|
import type { LoaderFunction } from "remix";
|
2022-02-13 21:25:00 +01:00
|
|
|
import { useLoaderData, Link, useCatch, redirect } from "remix";
|
|
|
|
|
import type { Expense, User } from "@prisma/client";
|
2022-02-10 10:44:44 +01:00
|
|
|
import { db } from "~/utils/db.server";
|
2022-02-13 21:25:00 +01:00
|
|
|
import { getUser } from "~/utils/session.server";
|
|
|
|
|
import Group from "~/icons/Group";
|
2022-02-10 10:44:44 +01:00
|
|
|
|
2022-02-14 19:19:12 +01:00
|
|
|
type LoaderData = {
|
|
|
|
|
lastExpenses: (Expense & { user: User })[];
|
|
|
|
|
user: User;
|
|
|
|
|
teamCounts: {
|
|
|
|
|
id: string;
|
|
|
|
|
username: string;
|
|
|
|
|
icon: string;
|
|
|
|
|
count: number;
|
|
|
|
|
spent: number;
|
|
|
|
|
dueAmount: number;
|
|
|
|
|
}[];
|
|
|
|
|
totalExpenses: {
|
|
|
|
|
count: number;
|
|
|
|
|
amount: number;
|
|
|
|
|
};
|
|
|
|
|
};
|
2022-02-10 10:44:44 +01:00
|
|
|
|
2022-02-13 21:25:00 +01:00
|
|
|
export const loader: LoaderFunction = async ({ request }) => {
|
|
|
|
|
const user = await getUser(request);
|
|
|
|
|
if (!user) {
|
|
|
|
|
return redirect("/login");
|
|
|
|
|
}
|
2022-02-10 10:44:44 +01:00
|
|
|
const lastExpenses = await db.expense.findMany({
|
2022-02-13 21:25:00 +01:00
|
|
|
include: {
|
|
|
|
|
user: true,
|
|
|
|
|
},
|
2022-02-14 11:49:10 +01:00
|
|
|
take: 6,
|
2022-02-10 10:44:44 +01:00
|
|
|
orderBy: { createdAt: "desc" },
|
2022-02-13 21:25:00 +01:00
|
|
|
where: { teamId: user.teamId },
|
2022-02-10 10:44:44 +01:00
|
|
|
});
|
|
|
|
|
|
2022-02-14 19:19:12 +01:00
|
|
|
let teamExpenses = await db.expense.groupBy({
|
|
|
|
|
by: ["userId"],
|
|
|
|
|
_count: {
|
|
|
|
|
_all: true,
|
|
|
|
|
},
|
|
|
|
|
_sum: {
|
|
|
|
|
amount: true,
|
|
|
|
|
},
|
|
|
|
|
where: { user: { teamId: user.teamId } },
|
|
|
|
|
});
|
|
|
|
|
let expensesByUser = user.team.members.map((m) => ({
|
|
|
|
|
id: m.id,
|
|
|
|
|
username: m.username,
|
|
|
|
|
icon: m.icon,
|
|
|
|
|
count: teamExpenses.find((e) => e.userId === m.id)?._count?._all ?? 0,
|
|
|
|
|
spent: teamExpenses.find((e) => e.userId === m.id)?._sum?.amount ?? 0,
|
|
|
|
|
dueAmount: 0,
|
|
|
|
|
}));
|
|
|
|
|
let totalExpenses = expensesByUser.reduce(
|
|
|
|
|
(acc, { count, spent }) => ({
|
|
|
|
|
count: acc.count + count,
|
|
|
|
|
amount: acc.amount + spent,
|
|
|
|
|
}),
|
|
|
|
|
{ count: 0, amount: 0 }
|
|
|
|
|
);
|
|
|
|
|
const avgPerUser = totalExpenses.amount / user.team.members.length;
|
|
|
|
|
let teamCounts = expensesByUser.map((userData) => ({
|
|
|
|
|
...userData,
|
|
|
|
|
dueAmount: avgPerUser - userData.spent,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const data: LoaderData = {
|
|
|
|
|
lastExpenses,
|
|
|
|
|
user,
|
|
|
|
|
totalExpenses,
|
|
|
|
|
teamCounts,
|
|
|
|
|
};
|
2022-02-10 10:44:44 +01:00
|
|
|
return data;
|
|
|
|
|
};
|
|
|
|
|
|
2022-02-14 20:48:34 +01:00
|
|
|
export default function ExpensesIndexRoute() {
|
2022-02-10 10:44:44 +01:00
|
|
|
const data = useLoaderData<LoaderData>();
|
|
|
|
|
|
|
|
|
|
return (
|
2022-02-14 13:23:06 +01:00
|
|
|
<div className="grid grid-cols-2 gap-4">
|
2022-02-14 11:49:10 +01:00
|
|
|
<div className="col-span-2 md:col-span-1 card shadow-lg compact side bg-base-100 order-last md:order-none">
|
|
|
|
|
<div className="flex-column items-center card-body !py-6">
|
2022-02-13 21:25:00 +01:00
|
|
|
<h2 className="card-title">Last expenses</h2>
|
2022-02-14 11:49:10 +01:00
|
|
|
<ul className="list-none shadow-inner w-full rounded-lg p-4 my-4 mx-0 max-h-48 overflow-x-scroll">
|
|
|
|
|
{data.lastExpenses?.map((exp) => (
|
2022-02-14 20:48:34 +01:00
|
|
|
<li key={exp.id}>
|
|
|
|
|
<Link
|
|
|
|
|
className="flex w-full items-center mb-3"
|
|
|
|
|
to={`/expenses/${exp.id}`}
|
|
|
|
|
>
|
|
|
|
|
<div className="rounded-full w-10 h-10 inline-flex justify-center items-center bg-white text-3xl">
|
|
|
|
|
{exp.user.icon ?? exp.user.username[0]}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="font-bold w-16 ml-2 text-right">
|
|
|
|
|
<span
|
|
|
|
|
className={`${
|
|
|
|
|
exp.amount > 0 ? "text-error" : "text-success"
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
{-exp.amount} €
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="grow ml-3 flex flex-col justify-center items-start">
|
|
|
|
|
<span className="text-xs opacity-50">
|
|
|
|
|
{new Intl.DateTimeFormat("it", {
|
|
|
|
|
dateStyle: "short",
|
|
|
|
|
timeStyle: "short",
|
|
|
|
|
}).format(new Date(exp.createdAt))}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="font-bold">{exp.description}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</Link>
|
2022-02-14 11:49:10 +01:00
|
|
|
</li>
|
|
|
|
|
))}
|
|
|
|
|
</ul>
|
2022-02-13 21:25:00 +01:00
|
|
|
<Link to="list" className="btn">
|
|
|
|
|
<span>See all</span>
|
|
|
|
|
</Link>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2022-02-14 11:49:10 +01:00
|
|
|
<div className="col-span-2 md:col-span-1 card shadow-lg compact side bg-base-100">
|
2022-02-14 19:19:12 +01:00
|
|
|
<div className="flex-column items-center card-body !py-6">
|
2022-02-13 21:25:00 +01:00
|
|
|
<h2 className="card-title">Who needs to pay who</h2>
|
2022-02-14 20:02:47 +01:00
|
|
|
<div className="w-full shadow stats grid-cols-2 grid-flow-row-dense">
|
2022-02-14 19:19:12 +01:00
|
|
|
{data.teamCounts?.map((user) => (
|
2022-02-19 18:11:55 +01:00
|
|
|
<div className="stat col-span-1" key={user.id}>
|
2022-02-14 20:02:47 +01:00
|
|
|
<div className="stat-figure text-info">
|
2022-02-14 20:48:34 +01:00
|
|
|
<div className="rounded-full shrink-0 w-4 sm:w-10 h-4 sm:h-10 inline-flex justify-center items-center bg-white text-3xl">
|
2022-02-14 19:19:12 +01:00
|
|
|
{user.icon ?? user.username[0]}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2022-02-14 20:02:47 +01:00
|
|
|
<div
|
|
|
|
|
className={`stat-value ${
|
|
|
|
|
user.dueAmount > 0 ? "text-error" : "text-success"
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
{Math.abs(user.dueAmount)} €
|
|
|
|
|
</div>
|
|
|
|
|
<div className="stat-title text-lg">{user.username}</div>
|
|
|
|
|
<div className="stat-desc text-info">
|
|
|
|
|
{user.dueAmount > 0
|
|
|
|
|
? `${user.id === data.user.id ? "You owe" : "Owes"} others`
|
2022-02-14 20:48:34 +01:00
|
|
|
: `Others owe ${user.id === data.user.id ? "you" : ""}`}
|
2022-02-14 19:19:12 +01:00
|
|
|
</div>
|
2022-02-14 20:02:47 +01:00
|
|
|
</div>
|
2022-02-14 19:19:12 +01:00
|
|
|
))}
|
2022-02-14 20:02:47 +01:00
|
|
|
</div>
|
2022-02-13 21:25:00 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2022-02-14 11:49:10 +01:00
|
|
|
<div className="card shadow-lg compact side md:bg-base-100 order-first md:order-none">
|
2022-02-13 21:25:00 +01:00
|
|
|
<div className="flex-row items-center justify-center space-x-4 card-body">
|
2022-02-14 11:49:10 +01:00
|
|
|
<Link
|
|
|
|
|
to="new"
|
|
|
|
|
className="btn btn-primary flex-wrap py-3 h-auto w-full"
|
|
|
|
|
>
|
2022-02-13 21:25:00 +01:00
|
|
|
<svg
|
|
|
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
|
|
|
fill="none"
|
|
|
|
|
viewBox="0 0 24 24"
|
2022-02-14 11:49:10 +01:00
|
|
|
className="inline-block lg:mr-2 w-6 h-6 stroke-current rotate-45"
|
2022-02-13 21:25:00 +01:00
|
|
|
>
|
|
|
|
|
<path
|
2022-02-14 14:30:42 +01:00
|
|
|
strokeLinecap="round"
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
strokeWidth="2"
|
2022-02-13 21:25:00 +01:00
|
|
|
d="M6 18L18 6M6 6l12 12"
|
|
|
|
|
></path>
|
|
|
|
|
</svg>
|
2022-02-14 11:49:10 +01:00
|
|
|
<span className="block w-full mt-2 lg:mt-0 lg:w-auto lg:inline-block">
|
2022-02-14 13:23:06 +01:00
|
|
|
New
|
2022-02-14 11:49:10 +01:00
|
|
|
</span>
|
2022-02-13 21:25:00 +01:00
|
|
|
</Link>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2022-02-14 11:49:10 +01:00
|
|
|
<div className="card shadow-lg compact side md:bg-base-100 order-first md:order-none">
|
2022-02-13 21:25:00 +01:00
|
|
|
<div className="flex-row items-center justify-center space-x-4 card-body">
|
2022-02-14 11:49:10 +01:00
|
|
|
<Link
|
2022-02-14 14:30:42 +01:00
|
|
|
to="transfer"
|
2022-02-14 11:49:10 +01:00
|
|
|
className="btn btn-primary flex-wrap py-3 h-auto w-full"
|
|
|
|
|
>
|
|
|
|
|
<Group className="inline-block lg:mr-2 w-6 h-6 stroke-current" />
|
|
|
|
|
<span className="block w-full mt-2 lg:mt-0 lg:w-auto lg:inline-block">
|
|
|
|
|
Trasfer
|
|
|
|
|
</span>
|
2022-02-13 21:25:00 +01:00
|
|
|
</Link>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2022-02-10 10:44:44 +01:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function CatchBoundary() {
|
|
|
|
|
const caught = useCatch();
|
|
|
|
|
|
|
|
|
|
if (caught.status === 404) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="error-container">There are no expenses to display.</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
throw new Error(`Unexpected caught response with status: ${caught.status}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function ErrorBoundary() {
|
|
|
|
|
return <div className="error-container">I did a whoopsies.</div>;
|
|
|
|
|
}
|