feat: add expense detail ui
This commit is contained in:
parent
0c354b069a
commit
8afc80539b
|
|
@ -42,44 +42,46 @@ const Header = ({ user, route }: Props) => {
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-none hidden lg:block">
|
<div className="flex-none hidden lg:block">
|
||||||
<ul className="menu horizontal">
|
{user && (
|
||||||
<li>
|
<ul className="menu horizontal">
|
||||||
<Link to="/expenses" className="btn btn-ghost rounded-btn">
|
<li>
|
||||||
<MoneyAdd className="lg:mr-2" />
|
<Link to="/expenses" className="btn btn-ghost rounded-btn">
|
||||||
<span
|
<MoneyAdd className="lg:mr-2" />
|
||||||
className={`hidden lg:inline-block ${
|
<span
|
||||||
route === "/expenses" ? "underline" : ""
|
className={`hidden lg:inline-block ${
|
||||||
}`}
|
route === "/expenses" ? "underline" : ""
|
||||||
>
|
}`}
|
||||||
Expenses
|
>
|
||||||
</span>
|
Expenses
|
||||||
</Link>
|
</span>
|
||||||
</li>
|
</Link>
|
||||||
<li>
|
</li>
|
||||||
<Link to="/team" className="btn btn-ghost rounded-btn">
|
<li>
|
||||||
<People className="lg:mr-2" />
|
<Link to="/team" className="btn btn-ghost rounded-btn">
|
||||||
<span
|
<People className="lg:mr-2" />
|
||||||
className={`hidden lg:inline-block ${
|
<span
|
||||||
route === "/team" ? "underline" : ""
|
className={`hidden lg:inline-block ${
|
||||||
}`}
|
route === "/team" ? "underline" : ""
|
||||||
>
|
}`}
|
||||||
Your team
|
>
|
||||||
</span>
|
Your team
|
||||||
</Link>
|
</span>
|
||||||
</li>
|
</Link>
|
||||||
<li>
|
</li>
|
||||||
<Link to="/statistics" className="btn btn-ghost rounded-btn">
|
<li>
|
||||||
<Percentage className="lg:mr-2" />
|
<Link to="/statistics" className="btn btn-ghost rounded-btn">
|
||||||
<span
|
<Percentage className="lg:mr-2" />
|
||||||
className={`hidden lg:inline-block ${
|
<span
|
||||||
route === "/statistics" ? "underline" : ""
|
className={`hidden lg:inline-block ${
|
||||||
}`}
|
route === "/statistics" ? "underline" : ""
|
||||||
>
|
}`}
|
||||||
Statistics
|
>
|
||||||
</span>
|
Statistics
|
||||||
</Link>
|
</span>
|
||||||
</li>
|
</Link>
|
||||||
</ul>
|
</li>
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-none">
|
<div className="flex-none">
|
||||||
{user ? (
|
{user ? (
|
||||||
|
|
@ -154,42 +156,57 @@ const Header = ({ user, route }: Props) => {
|
||||||
<span className="text-lg font-bold">Explit</span>
|
<span className="text-lg font-bold">Explit</span>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li className="items-start">
|
{user ? (
|
||||||
<Link to="/expenses" className="btn btn-ghost rounded-btn">
|
<>
|
||||||
<MoneyAdd className="mr-2" />
|
<li className="items-start">
|
||||||
<span
|
<Link to="/expenses" className="btn btn-ghost rounded-btn">
|
||||||
className={`inline-block ${
|
<MoneyAdd className="mr-2" />
|
||||||
route === "/expenses" ? "underline" : ""
|
<span
|
||||||
}`}
|
className={`inline-block ${
|
||||||
|
route === "/expenses" ? "underline" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Expenses
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li className="items-start">
|
||||||
|
<Link to="/team" className="btn btn-ghost rounded-btn">
|
||||||
|
<People className="mr-2" />
|
||||||
|
<span
|
||||||
|
className={`inline-block ${
|
||||||
|
route === "/team" ? "underline" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Your team
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li className="items-start">
|
||||||
|
<Link to="/statistics" className="btn btn-ghost rounded-btn">
|
||||||
|
<Percentage className="mr-2" />
|
||||||
|
<span
|
||||||
|
className={`inline-block ${
|
||||||
|
route === "/statistics" ? "underline" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Statistics
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<li className="items-start">
|
||||||
|
<Link
|
||||||
|
to="/login"
|
||||||
|
className="btn btn-ghost rounded-btn"
|
||||||
|
title="Login"
|
||||||
>
|
>
|
||||||
Expenses
|
<LoginSVG />
|
||||||
</span>
|
<span className="inline-block ml-1">Login</span>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li className="items-start">
|
)}
|
||||||
<Link to="/team" className="btn btn-ghost rounded-btn">
|
|
||||||
<People className="mr-2" />
|
|
||||||
<span
|
|
||||||
className={`inline-block ${
|
|
||||||
route === "/team" ? "underline" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Your team
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
<li className="items-start">
|
|
||||||
<Link to="/statistics" className="btn btn-ghost rounded-btn">
|
|
||||||
<Percentage className="mr-2" />
|
|
||||||
<span
|
|
||||||
className={`inline-block ${
|
|
||||||
route === "/statistics" ? "underline" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Statistics
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import type { Expense, User } from "@prisma/client";
|
||||||
import { db } from "~/utils/db.server";
|
import { db } from "~/utils/db.server";
|
||||||
import { requireUserId, getUserId } from "~/utils/session.server";
|
import { requireUserId, getUserId } from "~/utils/session.server";
|
||||||
|
|
||||||
type LoaderData = { expense: Expense; user: User; isOwner: boolean };
|
type LoaderData = { expense: Expense & { user: User }; isOwner: boolean };
|
||||||
|
|
||||||
export const meta: MetaFunction = ({
|
export const meta: MetaFunction = ({
|
||||||
data,
|
data,
|
||||||
|
|
@ -38,23 +38,17 @@ export const loader: LoaderFunction = async ({ request, params }) => {
|
||||||
|
|
||||||
const expense = await db.expense.findUnique({
|
const expense = await db.expense.findUnique({
|
||||||
where: { id: params.expenseId },
|
where: { id: params.expenseId },
|
||||||
|
include: {
|
||||||
|
user: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
if (!expense) {
|
if (!expense) {
|
||||||
throw new Response("What an expense! Not found.", {
|
throw new Response("What an expense! Not found.", {
|
||||||
status: 404,
|
status: 404,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const expenseUser = await db.user.findUnique({
|
|
||||||
where: { id: expense.userId },
|
|
||||||
});
|
|
||||||
if (!expenseUser) {
|
|
||||||
throw new Response("Oupsie! Not found.", {
|
|
||||||
status: 500,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const data: LoaderData = {
|
const data: LoaderData = {
|
||||||
expense,
|
expense,
|
||||||
user: expenseUser,
|
|
||||||
isOwner: userId === expense.userId,
|
isOwner: userId === expense.userId,
|
||||||
};
|
};
|
||||||
return data;
|
return data;
|
||||||
|
|
@ -84,18 +78,96 @@ export default function ExpenseRoute() {
|
||||||
const data = useLoaderData<LoaderData>();
|
const data = useLoaderData<LoaderData>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="card shadow-lg bg-base-100 my-4">
|
||||||
<p>Description: {data.expense.description}</p>
|
<div className="flex-col items-center card-body p-4">
|
||||||
<p>Amount: {data.expense.amount}€</p>
|
<div className="flex flex-row w-full items-center">
|
||||||
<p>User: {data.user.username}</p>
|
<Link to="/expenses" className="btn btn-ghost mr-4">
|
||||||
{data.isOwner && (
|
<svg
|
||||||
<form method="post">
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<input type="hidden" name="_method" value="delete" />
|
fill="none"
|
||||||
<button type="submit" className="button">
|
viewBox="0 0 24 24"
|
||||||
Delete
|
className="inline-block w-6 h-6 mr-2 stroke-current rotate-180"
|
||||||
</button>
|
>
|
||||||
</form>
|
<path
|
||||||
)}
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M9 5l7 7-7 7"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
Back
|
||||||
|
</Link>
|
||||||
|
<h2 className="card-title mb-0">Expense details</h2>
|
||||||
|
</div>
|
||||||
|
<dl className="w-full py-6 px-3 my-4 rounded-box bg-base-200">
|
||||||
|
<div className="bg-base-200 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||||
|
<dt className="text-sm font-medium text-base-400">Description</dt>
|
||||||
|
<dd className="mt-1 text-sm text-base-900 sm:mt-0 sm:col-span-2">
|
||||||
|
{data.expense.description}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div className="bg-base-200 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||||
|
<dt className="text-sm font-medium text-base-400">Amount</dt>
|
||||||
|
<dd className="mt-1 text-sm text-base-900 sm:mt-0 sm:col-span-2">
|
||||||
|
{data.expense.amount} €
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div className="bg-base-200 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 items-center">
|
||||||
|
<dt className="text-sm font-medium text-base-400">User</dt>
|
||||||
|
<dd className="mt-2 text-sm text-base-900 sm:mt-0 sm:col-span-2 flex items-center">
|
||||||
|
<div className="rounded-full w-10 h-10 inline-flex justify-center items-center bg-white text-3xl mr-4">
|
||||||
|
{data.expense.user.icon ?? data.expense.user.username[0]}
|
||||||
|
</div>
|
||||||
|
{data.expense.user.username}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div className="bg-base-200 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||||
|
<dt className="text-sm font-medium text-base-400">Date</dt>
|
||||||
|
<dd className="mt-1 text-sm text-base-900 sm:mt-0 sm:col-span-2">
|
||||||
|
{new Intl.DateTimeFormat("it", {
|
||||||
|
dateStyle: "short",
|
||||||
|
timeStyle: "short",
|
||||||
|
}).format(new Date(data.expense.createdAt))}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
{data.isOwner && (
|
||||||
|
<>
|
||||||
|
<div className="flex justify-center align-center mt-6">
|
||||||
|
<label
|
||||||
|
htmlFor="delete-expense-modal"
|
||||||
|
className="btn btn-error modal-button"
|
||||||
|
>
|
||||||
|
Delete expense
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="delete-expense-modal"
|
||||||
|
className="modal-toggle"
|
||||||
|
/>
|
||||||
|
<div className="modal">
|
||||||
|
<div className="modal-box absolute left-[10%] right-[10%] top-[40%] w-[80%] rounded-lg">
|
||||||
|
<p>
|
||||||
|
Do you really want to delete your expense? Its data will be
|
||||||
|
permanently deleted.
|
||||||
|
</p>
|
||||||
|
<div className="modal-action">
|
||||||
|
<Form method="post">
|
||||||
|
<input type="hidden" name="_method" value="delete" />
|
||||||
|
<button type="submit" className="btn btn-error">
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</Form>
|
||||||
|
<label htmlFor="delete-expense-modal" className="btn">
|
||||||
|
Cancel
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ export const loader: LoaderFunction = async ({ request }) => {
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function JokesIndexRoute() {
|
export default function ExpensesIndexRoute() {
|
||||||
const data = useLoaderData<LoaderData>();
|
const data = useLoaderData<LoaderData>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -88,28 +88,33 @@ export default function JokesIndexRoute() {
|
||||||
<h2 className="card-title">Last expenses</h2>
|
<h2 className="card-title">Last expenses</h2>
|
||||||
<ul className="list-none shadow-inner w-full rounded-lg p-4 my-4 mx-0 max-h-48 overflow-x-scroll">
|
<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) => (
|
{data.lastExpenses?.map((exp) => (
|
||||||
<li className="flex w-full items-center mb-3" key={exp.id}>
|
<li key={exp.id}>
|
||||||
<div className="rounded-full w-10 h-10 inline-flex justify-center items-center bg-white text-3xl">
|
<Link
|
||||||
{exp.user.icon ?? exp.user.username[0]}
|
className="flex w-full items-center mb-3"
|
||||||
</div>
|
to={`/expenses/${exp.id}`}
|
||||||
<div className="font-bold w-16 ml-2 text-right">
|
>
|
||||||
<span
|
<div className="rounded-full w-10 h-10 inline-flex justify-center items-center bg-white text-3xl">
|
||||||
className={`${
|
{exp.user.icon ?? exp.user.username[0]}
|
||||||
exp.amount > 0 ? "text-error" : "text-success"
|
</div>
|
||||||
}`}
|
<div className="font-bold w-16 ml-2 text-right">
|
||||||
>
|
<span
|
||||||
{-exp.amount} €
|
className={`${
|
||||||
</span>
|
exp.amount > 0 ? "text-error" : "text-success"
|
||||||
</div>
|
}`}
|
||||||
<div className="grow ml-3 flex flex-col justify-center items-start">
|
>
|
||||||
<span className="text-xs opacity-50">
|
{-exp.amount} €
|
||||||
{new Intl.DateTimeFormat("it", {
|
</span>
|
||||||
dateStyle: "short",
|
</div>
|
||||||
timeStyle: "short",
|
<div className="grow ml-3 flex flex-col justify-center items-start">
|
||||||
}).format(new Date(exp.createdAt))}
|
<span className="text-xs opacity-50">
|
||||||
</span>
|
{new Intl.DateTimeFormat("it", {
|
||||||
<span className="font-bold">{exp.description}</span>
|
dateStyle: "short",
|
||||||
</div>
|
timeStyle: "short",
|
||||||
|
}).format(new Date(exp.createdAt))}
|
||||||
|
</span>
|
||||||
|
<span className="font-bold">{exp.description}</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
@ -125,7 +130,7 @@ export default function JokesIndexRoute() {
|
||||||
{data.teamCounts?.map((user) => (
|
{data.teamCounts?.map((user) => (
|
||||||
<div className="stat col-span-1">
|
<div className="stat col-span-1">
|
||||||
<div className="stat-figure text-info">
|
<div className="stat-figure text-info">
|
||||||
<div className="rounded-full shrink-0 w-12 h-12 inline-flex justify-center items-center bg-white text-3xl">
|
<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">
|
||||||
{user.icon ?? user.username[0]}
|
{user.icon ?? user.username[0]}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -140,9 +145,7 @@ export default function JokesIndexRoute() {
|
||||||
<div className="stat-desc text-info">
|
<div className="stat-desc text-info">
|
||||||
{user.dueAmount > 0
|
{user.dueAmount > 0
|
||||||
? `${user.id === data.user.id ? "You owe" : "Owes"} others`
|
? `${user.id === data.user.id ? "You owe" : "Owes"} others`
|
||||||
: `Others owe ${
|
: `Others owe ${user.id === data.user.id ? "you" : ""}`}
|
||||||
user.id === data.user.id ? "you" : "him/her"
|
|
||||||
}`}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue