feat: add income based balances
This commit is contained in:
parent
a34358bfa0
commit
60b551801a
|
|
@ -61,6 +61,13 @@ function validateTeamId(teamId: unknown) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateIncome(income: unknown) {
|
||||||
|
if (typeof income !== "number") {
|
||||||
|
console.log(income, typeof income);
|
||||||
|
return "Income must be a positive number or zero";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type ActionData = {
|
type ActionData = {
|
||||||
formError?: string;
|
formError?: string;
|
||||||
success?: string;
|
success?: string;
|
||||||
|
|
@ -69,12 +76,14 @@ type ActionData = {
|
||||||
teamId: string | undefined;
|
teamId: string | undefined;
|
||||||
password: string | undefined;
|
password: string | undefined;
|
||||||
confirmPassword: string | undefined;
|
confirmPassword: string | undefined;
|
||||||
|
avgIncome: string | undefined;
|
||||||
};
|
};
|
||||||
fields?: {
|
fields?: {
|
||||||
password?: string;
|
password?: string;
|
||||||
confirmPassword?: string;
|
confirmPassword?: string;
|
||||||
teamId?: string;
|
teamId?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
|
avgIncome?: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -91,6 +100,7 @@ export const action: ActionFunction = async ({ request }) => {
|
||||||
const confirmPassword = form.get("confirmPassword");
|
const confirmPassword = form.get("confirmPassword");
|
||||||
const icon = form.get("icon") ?? (user ? user.username[0] : undefined);
|
const icon = form.get("icon") ?? (user ? user.username[0] : undefined);
|
||||||
const teamId = form.get("teamId");
|
const teamId = form.get("teamId");
|
||||||
|
const avgIncome = form.get("avgIncome");
|
||||||
|
|
||||||
const fields = {
|
const fields = {
|
||||||
icon: typeof icon === "string" ? icon : undefined,
|
icon: typeof icon === "string" ? icon : undefined,
|
||||||
|
|
@ -98,6 +108,12 @@ export const action: ActionFunction = async ({ request }) => {
|
||||||
confirmPassword:
|
confirmPassword:
|
||||||
typeof confirmPassword === "string" ? confirmPassword : undefined,
|
typeof confirmPassword === "string" ? confirmPassword : undefined,
|
||||||
teamId: typeof teamId === "string" ? teamId : undefined,
|
teamId: typeof teamId === "string" ? teamId : undefined,
|
||||||
|
avgIncome:
|
||||||
|
typeof avgIncome === "number"
|
||||||
|
? Math.round(avgIncome)
|
||||||
|
: typeof avgIncome === "string"
|
||||||
|
? parseInt(avgIncome, 10)
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
const fieldErrors = {
|
const fieldErrors = {
|
||||||
password: validatePassword(password),
|
password: validatePassword(password),
|
||||||
|
|
@ -107,14 +123,19 @@ export const action: ActionFunction = async ({ request }) => {
|
||||||
),
|
),
|
||||||
icon: validateIcon(icon),
|
icon: validateIcon(icon),
|
||||||
teamId: validateTeamId(teamId),
|
teamId: validateTeamId(teamId),
|
||||||
|
avgIncome: validateIncome(fields.avgIncome),
|
||||||
};
|
};
|
||||||
if (Object.values(fieldErrors).some(Boolean))
|
if (Object.values(fieldErrors).some(Boolean))
|
||||||
return badRequest({ fieldErrors, fields });
|
return badRequest({ fieldErrors, fields });
|
||||||
|
|
||||||
const nonEmptyFields = Object.entries(fields).reduce((acc, [key, value]) => {
|
const nonEmptyFields = Object.entries(fields).reduce((acc, [key, value]) => {
|
||||||
if (typeof value === "string" && key !== "confirmPassword")
|
if (
|
||||||
|
(typeof value === "string" && key !== "confirmPassword") ||
|
||||||
|
(typeof value === "number" && key === "avgIncome")
|
||||||
|
)
|
||||||
return { ...acc, [key]: value ?? undefined };
|
return { ...acc, [key]: value ?? undefined };
|
||||||
else return acc;
|
|
||||||
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
const userUpdated = await updateUser({
|
const userUpdated = await updateUser({
|
||||||
id: user.id,
|
id: user.id,
|
||||||
|
|
@ -299,6 +320,48 @@ export default function AccountPreferencesRoute() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="form-control mb-3">
|
||||||
|
<label htmlFor="avgIncome-input" className="label">
|
||||||
|
<span className="label-text">Average monthly income</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="avgIncome-input"
|
||||||
|
name="avgIncome"
|
||||||
|
className={`input input-bordered${
|
||||||
|
Boolean(actionData?.fieldErrors?.avgIncome) ? " input-error" : ""
|
||||||
|
}`}
|
||||||
|
defaultValue={
|
||||||
|
actionData?.fields?.avgIncome ?? data.user?.avgIncome ?? 0
|
||||||
|
}
|
||||||
|
aria-invalid={Boolean(actionData?.fieldErrors?.avgIncome)}
|
||||||
|
aria-describedby={
|
||||||
|
actionData?.fieldErrors?.avgIncome ? "avgIncome-error" : undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{actionData?.fieldErrors?.avgIncome && (
|
||||||
|
<div className="alert alert-error mt-2" role="alert">
|
||||||
|
<div className="flex-1">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
className="w-6 h-6 mx-2 stroke-current"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<label id="avgIncome-error">
|
||||||
|
{actionData?.fieldErrors.avgIncome}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className="form-control mb-3">
|
<div className="form-control mb-3">
|
||||||
<label htmlFor="teamId-input" className="label">
|
<label htmlFor="teamId-input" className="label">
|
||||||
<span className="label-text">Team</span>
|
<span className="label-text">Team</span>
|
||||||
|
|
@ -310,7 +373,7 @@ export default function AccountPreferencesRoute() {
|
||||||
className={`input input-bordered${
|
className={`input input-bordered${
|
||||||
Boolean(actionData?.fieldErrors?.teamId) ? " input-error" : ""
|
Boolean(actionData?.fieldErrors?.teamId) ? " input-error" : ""
|
||||||
}`}
|
}`}
|
||||||
defaultValue={actionData?.fields?.teamId}
|
defaultValue={actionData?.fields?.teamId ?? data.user?.teamId}
|
||||||
aria-invalid={Boolean(actionData?.fieldErrors?.teamId)}
|
aria-invalid={Boolean(actionData?.fieldErrors?.teamId)}
|
||||||
aria-describedby={
|
aria-describedby={
|
||||||
actionData?.fieldErrors?.teamId ? "teamid-error" : undefined
|
actionData?.fieldErrors?.teamId ? "teamid-error" : undefined
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ type LoaderData = {
|
||||||
count: number;
|
count: number;
|
||||||
spent: number;
|
spent: number;
|
||||||
dueAmount: number;
|
dueAmount: number;
|
||||||
|
avgIncome: number;
|
||||||
}[];
|
}[];
|
||||||
totalExpenses: {
|
totalExpenses: {
|
||||||
count: number;
|
count: number;
|
||||||
|
|
@ -52,19 +53,33 @@ export const loader: LoaderFunction = async ({ request }) => {
|
||||||
icon: m.icon,
|
icon: m.icon,
|
||||||
count: teamExpenses.find((e) => e.userId === m.id)?._count?._all ?? 0,
|
count: teamExpenses.find((e) => e.userId === m.id)?._count?._all ?? 0,
|
||||||
spent: teamExpenses.find((e) => e.userId === m.id)?._sum?.amount ?? 0,
|
spent: teamExpenses.find((e) => e.userId === m.id)?._sum?.amount ?? 0,
|
||||||
|
avgIncome: m.avgIncome ?? 0,
|
||||||
dueAmount: 0,
|
dueAmount: 0,
|
||||||
}));
|
}));
|
||||||
let totalExpenses = expensesByUser.reduce(
|
let totalExpenses = expensesByUser.reduce(
|
||||||
(acc, { count, spent }) => ({
|
(acc, { count, spent, avgIncome }) => ({
|
||||||
count: acc.count + count,
|
count: acc.count + count,
|
||||||
amount: acc.amount + spent,
|
amount: acc.amount + spent,
|
||||||
|
incomes: acc.incomes + avgIncome,
|
||||||
}),
|
}),
|
||||||
{ count: 0, amount: 0 }
|
{ count: 0, amount: 0, incomes: 0 }
|
||||||
);
|
);
|
||||||
|
|
||||||
const avgPerUser = totalExpenses.amount / user.team.members.length;
|
const avgPerUser = totalExpenses.amount / user.team.members.length;
|
||||||
|
const quotaPerUser = expensesByUser.reduce(
|
||||||
|
(acc: { [key: string]: number }, userData) => {
|
||||||
|
acc[userData.id] =
|
||||||
|
(userData.avgIncome / totalExpenses.incomes) * totalExpenses.amount;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
let teamCounts = expensesByUser.map((userData) => ({
|
let teamCounts = expensesByUser.map((userData) => ({
|
||||||
...userData,
|
...userData,
|
||||||
dueAmount: avgPerUser - userData.spent,
|
dueAmount: user.team.balanceByIncome
|
||||||
|
? quotaPerUser[userData.id] - userData.spent
|
||||||
|
: avgPerUser - userData.spent,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const data: LoaderData = {
|
const data: LoaderData = {
|
||||||
|
|
@ -123,7 +138,7 @@ export default function ExpensesIndexRoute() {
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2 md:col-span-1 card shadow-lg compact side bg-base-100">
|
<div className="col-span-2 md:col-span-1 card shadow-lg compact side bg-base-100">
|
||||||
<div className="flex-column items-center card-body !py-6">
|
<div className="flex-column items-center card-body !py-6">
|
||||||
<h2 className="card-title">Who needs to pay who</h2>
|
<h2 className="card-title">Team balance</h2>
|
||||||
<div className="w-full shadow stats grid-cols-2 grid-flow-row-dense">
|
<div className="w-full shadow stats grid-cols-2 grid-flow-row-dense">
|
||||||
{data.teamCounts?.map((user) => (
|
{data.teamCounts?.map((user) => (
|
||||||
<div className="stat col-span-1" key={user.id}>
|
<div className="stat col-span-1" key={user.id}>
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ function validateConfirmPassword(confirmPassword: unknown, password: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateIcon(icon: unknown) {
|
function validateIcon(icon: unknown) {
|
||||||
if (typeof icon !== "string" || icon.length > 2) {
|
if (typeof icon !== "string") {
|
||||||
return `Icons must be a single character, e.g. "A" or "😎"`;
|
return `Icons must be a single character, e.g. "A" or "😎"`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -70,9 +70,7 @@ export const action: ActionFunction = async ({ request }) => {
|
||||||
const username = form.get("username");
|
const username = form.get("username");
|
||||||
const password = form.get("password");
|
const password = form.get("password");
|
||||||
const confirmPassword = form.get("confirmPassword");
|
const confirmPassword = form.get("confirmPassword");
|
||||||
const icon =
|
const icon = form.get("icon");
|
||||||
form.get("icon") ??
|
|
||||||
(typeof username === "string" ? username[0] : undefined);
|
|
||||||
const teamId = form.get("teamId");
|
const teamId = form.get("teamId");
|
||||||
const redirectTo = form.get("redirectTo") || "/expenses";
|
const redirectTo = form.get("redirectTo") || "/expenses";
|
||||||
if (
|
if (
|
||||||
|
|
@ -80,6 +78,7 @@ export const action: ActionFunction = async ({ request }) => {
|
||||||
typeof password !== "string" ||
|
typeof password !== "string" ||
|
||||||
typeof confirmPassword !== "string" ||
|
typeof confirmPassword !== "string" ||
|
||||||
typeof teamId !== "string" ||
|
typeof teamId !== "string" ||
|
||||||
|
typeof icon !== "string" ||
|
||||||
typeof redirectTo !== "string"
|
typeof redirectTo !== "string"
|
||||||
) {
|
) {
|
||||||
return badRequest({
|
return badRequest({
|
||||||
|
|
@ -87,12 +86,12 @@ export const action: ActionFunction = async ({ request }) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const fields = { username, password, confirmPassword, teamId };
|
const fields = { username, password, icon, confirmPassword, teamId };
|
||||||
const fieldErrors = {
|
const fieldErrors = {
|
||||||
username: validateUsername(username),
|
username: validateUsername(username),
|
||||||
password: validatePassword(password),
|
password: validatePassword(password),
|
||||||
confirmPassword: validateConfirmPassword(confirmPassword, password),
|
confirmPassword: validateConfirmPassword(confirmPassword, password),
|
||||||
icon: validateIcon(icon),
|
icon: validateIcon(icon ?? ""),
|
||||||
teamId: validateTeamId(teamId),
|
teamId: validateTeamId(teamId),
|
||||||
};
|
};
|
||||||
if (Object.values(fieldErrors).some(Boolean))
|
if (Object.values(fieldErrors).some(Boolean))
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import type { LoaderFunction } from "remix";
|
import type { LoaderFunction, ActionFunction } from "remix";
|
||||||
import { useLoaderData, useCatch, redirect } from "remix";
|
import { useLoaderData, useCatch, redirect, Link, Form } from "remix";
|
||||||
import type { Team, User } from "@prisma/client";
|
import type { Team, User } from "@prisma/client";
|
||||||
import { getUser } from "~/utils/session.server";
|
|
||||||
import Header from "~/components/Header";
|
import Header from "~/components/Header";
|
||||||
|
import { db } from "~/utils/db.server";
|
||||||
|
import { requireUserId, getUser } from "~/utils/session.server";
|
||||||
|
|
||||||
type LoaderData = {
|
type LoaderData = {
|
||||||
user: User & {
|
user: User & {
|
||||||
|
|
@ -21,9 +22,43 @@ export const loader: LoaderFunction = async ({ request }) => {
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const action: ActionFunction = async ({ request }) => {
|
||||||
|
const form = await request.formData();
|
||||||
|
if (form.get("_method") === "patch") {
|
||||||
|
const userId = await requireUserId(request);
|
||||||
|
const user = await getUser(request);
|
||||||
|
|
||||||
|
const balanceByIncomeField = form.get("balanceByIncome");
|
||||||
|
const balanceByIncome =
|
||||||
|
typeof balanceByIncomeField === "boolean"
|
||||||
|
? balanceByIncomeField
|
||||||
|
: typeof balanceByIncomeField === "string"
|
||||||
|
? Boolean(balanceByIncomeField)
|
||||||
|
: false;
|
||||||
|
|
||||||
|
const team = await db.team.findUnique({
|
||||||
|
where: { id: user?.teamId },
|
||||||
|
});
|
||||||
|
if (!team) {
|
||||||
|
throw new Response("Can't update what does not exist", { status: 404 });
|
||||||
|
}
|
||||||
|
if (user?.teamId !== team.id) {
|
||||||
|
throw new Response("Pssh, nice try. That's not your expense", {
|
||||||
|
status: 401,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await db.team.update({ where: { id: team.id }, data: { balanceByIncome } });
|
||||||
|
return redirect("/expenses");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export default function JokesIndexRoute() {
|
export default function JokesIndexRoute() {
|
||||||
const data = useLoaderData<LoaderData>();
|
const data = useLoaderData<LoaderData>();
|
||||||
|
|
||||||
|
const allUsersHaveSetAvgIncome = data.user.team.members?.every(
|
||||||
|
(m) => m.avgIncome && m.avgIncome > 0
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header user={data.user} route="/account" />
|
<Header user={data.user} route="/account" />
|
||||||
|
|
@ -52,6 +87,64 @@ export default function JokesIndexRoute() {
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
<div className="mt-10">
|
||||||
|
<h2 className="mb-4 text-xl">Balance based on income</h2>
|
||||||
|
<p>
|
||||||
|
To have an equal split based on everyone's income, you can select
|
||||||
|
this option. If of two people, one earns 1.5 times as much as the
|
||||||
|
other, then his or her share in the common expenses will be 1.5
|
||||||
|
times as much as the other's.
|
||||||
|
</p>
|
||||||
|
<Form method="post">
|
||||||
|
<input type="hidden" name="_method" value="patch" />
|
||||||
|
<fieldset disabled={!allUsersHaveSetAvgIncome}>
|
||||||
|
<div className="form-control mt-4">
|
||||||
|
<label className="cursor-pointer label justify-start">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="balanceByIncome"
|
||||||
|
defaultChecked={data.user.team.balanceByIncome ?? false}
|
||||||
|
className="checkbox checkbox-primary"
|
||||||
|
/>
|
||||||
|
<span className="label-text ml-2">
|
||||||
|
Enable balance based on income
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
{!allUsersHaveSetAvgIncome ? (
|
||||||
|
<div className="alert shadow-lg">
|
||||||
|
<div>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
className="stroke-info-content flex-shrink-0 w-6 h-6"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<span>
|
||||||
|
Not all users from this team set their average income.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex-none">
|
||||||
|
<Link to="/account/manage" className="btn btn-sm">
|
||||||
|
Check in the Account page
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button type="submit" className="mt-6 btn btn-primary">
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ type LoginForm = {
|
||||||
type RegisterForm = {
|
type RegisterForm = {
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
icon: string;
|
icon?: string;
|
||||||
teamId: string;
|
teamId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -19,6 +19,7 @@ type UpdateUserForm = {
|
||||||
password?: string;
|
password?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
teamId?: string;
|
teamId?: string;
|
||||||
|
avgIncome?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function register({
|
export async function register({
|
||||||
|
|
@ -38,7 +39,12 @@ export async function register({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const user = await db.user.create({
|
const user = await db.user.create({
|
||||||
data: { username, passwordHash, icon: icon ?? username[0], teamId },
|
data: {
|
||||||
|
username,
|
||||||
|
passwordHash,
|
||||||
|
icon: icon && icon.length > 0 ? icon : username[0],
|
||||||
|
teamId,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
@ -62,6 +68,7 @@ export async function updateUser({ id, ...data }: UpdateUserForm) {
|
||||||
...(data?.icon ? { icon: data.icon } : {}),
|
...(data?.icon ? { icon: data.icon } : {}),
|
||||||
...(data?.password ? { passwordHash } : {}),
|
...(data?.password ? { passwordHash } : {}),
|
||||||
...(data?.teamId ? { teamId: data.teamId } : {}),
|
...(data?.teamId ? { teamId: data.teamId } : {}),
|
||||||
|
...(data?.avgIncome ? { avgIncome: data.avgIncome } : {}),
|
||||||
},
|
},
|
||||||
where: { id },
|
where: { id },
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,9 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npm run build:css && npm run build:worker && remix build",
|
"build": "npm run build:css && npm run build:worker && remix build",
|
||||||
"build:css": "tailwindcss -o ./app/tailwind.css",
|
"build:css": "tailwindcss -o ./app/tailwind.css",
|
||||||
"build:worker": "esbuild ./app/entry.worker.tsx --outfile=./public/entry.worker.js --minify --bundle --format=esm --define:process.env.NODE_ENV='\"production\"'",
|
"build:worker": "esbuild ./app/entry.worker.tsx --outfile=./build/entry.worker.js --minify --bundle --format=esm --define:process.env.NODE_ENV='\"production\"'",
|
||||||
"dev": "concurrently \"npm run dev:css\" \"npm run dev:worker\" \"remix dev\"",
|
"dev": "concurrently \"npm run dev:css\" \"npm run dev:worker\" \"remix dev\"",
|
||||||
"dev:worker": "esbuild ./app/entry.worker.tsx --outfile=./public/entry.worker.js --bundle --format=esm --define:process.env.NODE_ENV='\"development\"' --watch",
|
"dev:worker": "esbuild ./app/entry.worker.tsx --outfile=./build/entry.worker.js --bundle --format=esm --define:process.env.NODE_ENV='\"development\"' --watch",
|
||||||
"dev:css": "tailwindcss -o ./app/tailwind.css --watch",
|
"dev:css": "tailwindcss -o ./app/tailwind.css --watch",
|
||||||
"postinstall": "remix setup node",
|
"postinstall": "remix setup node",
|
||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
|
|
|
||||||
5
prisma/migrations/20220221154720_income/migration.sql
Normal file
5
prisma/migrations/20220221154720_income/migration.sql
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Team" ADD COLUMN "balanceByIncome" BOOLEAN DEFAULT false;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" ADD COLUMN "avgIncome" INTEGER;
|
||||||
|
|
@ -15,6 +15,7 @@ model Team {
|
||||||
icon String
|
icon String
|
||||||
description String?
|
description String?
|
||||||
members User[]
|
members User[]
|
||||||
|
balanceByIncome Boolean? @default(value: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
|
|
@ -28,6 +29,7 @@ model User {
|
||||||
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||||
expenses Expense[]
|
expenses Expense[]
|
||||||
theme String?
|
theme String?
|
||||||
|
avgIncome Int?
|
||||||
}
|
}
|
||||||
|
|
||||||
model Expense {
|
model Expense {
|
||||||
|
|
|
||||||
|
|
@ -2,29 +2,29 @@ import { PrismaClient } from "@prisma/client";
|
||||||
const db = new PrismaClient();
|
const db = new PrismaClient();
|
||||||
|
|
||||||
async function seed() {
|
async function seed() {
|
||||||
const famiglia = await db.team.create({
|
const team = await db.team.create({
|
||||||
data: {
|
data: {
|
||||||
id: "Famiglia",
|
id: "Family",
|
||||||
description: "La mia famiglia",
|
description: "My family",
|
||||||
icon: "♥️",
|
icon: "♥️",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const nicola = await db.user.create({
|
const user1 = await db.user.create({
|
||||||
data: {
|
data: {
|
||||||
username: "nicola",
|
username: "user1",
|
||||||
passwordHash:
|
passwordHash:
|
||||||
"$2b$10$K7L1OJ45/4Y2nIvhRVpCe.FSmhDdWoXehVzJptJ/op0lSsvqNu/1u",
|
"$2b$10$K7L1OJ45/4Y2nIvhRVpCe.FSmhDdWoXehVzJptJ/op0lSsvqNu/1u", // twixrox
|
||||||
teamId: famiglia.id,
|
teamId: team.id,
|
||||||
icon: "🧑💻",
|
icon: "🧑💻",
|
||||||
theme: "dark",
|
theme: "dark",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const shahra = await db.user.create({
|
const user2 = await db.user.create({
|
||||||
data: {
|
data: {
|
||||||
username: "shahra",
|
username: "user2",
|
||||||
passwordHash:
|
passwordHash:
|
||||||
"$2b$10$K7L1OJ45/4Y2nIvhRVpCe.FSmhDdWoXehVzJptJ/op0lSsvqNu/1u",
|
"$2b$10$K7L1OJ45/4Y2nIvhRVpCe.FSmhDdWoXehVzJptJ/op0lSsvqNu/1u",
|
||||||
teamId: famiglia.id,
|
teamId: team.id,
|
||||||
icon: "💃",
|
icon: "💃",
|
||||||
theme: "emerald",
|
theme: "emerald",
|
||||||
},
|
},
|
||||||
|
|
@ -32,43 +32,43 @@ async function seed() {
|
||||||
|
|
||||||
const expenses = [
|
const expenses = [
|
||||||
{
|
{
|
||||||
description: "Spesa",
|
description: "Groceries",
|
||||||
amount: 100,
|
amount: 100,
|
||||||
userId: nicola.id,
|
userId: user1.id,
|
||||||
teamId: famiglia.id,
|
teamId: team.id,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "Spesa",
|
description: "Groceries",
|
||||||
amount: 70,
|
amount: 70,
|
||||||
userId: shahra.id,
|
userId: user2.id,
|
||||||
teamId: famiglia.id,
|
teamId: team.id,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "Affitto",
|
description: "Rent",
|
||||||
amount: 500,
|
amount: 500,
|
||||||
userId: shahra.id,
|
userId: user2.id,
|
||||||
teamId: famiglia.id,
|
teamId: team.id,
|
||||||
},
|
},
|
||||||
|
|
||||||
// transaction between users
|
// transaction between users
|
||||||
{
|
{
|
||||||
description: "Affitto",
|
description: "Rent",
|
||||||
amount: 250,
|
amount: 250,
|
||||||
userId: nicola.id,
|
userId: user1.id,
|
||||||
teamId: famiglia.id,
|
teamId: team.id,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "Affitto",
|
description: "Rent",
|
||||||
amount: -250,
|
amount: -250,
|
||||||
userId: shahra.id,
|
userId: user2.id,
|
||||||
teamId: famiglia.id,
|
teamId: team.id,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
description: "Cena",
|
description: "Dinner out",
|
||||||
amount: 50,
|
amount: 50,
|
||||||
userId: nicola.id,
|
userId: user1.id,
|
||||||
teamId: famiglia.id,
|
teamId: team.id,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue