diff --git a/app/components/layout/header.tsx b/app/components/layout/header.tsx index 14d2796..1436892 100644 --- a/app/components/layout/header.tsx +++ b/app/components/layout/header.tsx @@ -152,7 +152,7 @@ function UserDropdown() { className="relative rounded-full border border-primary bg-secondary p-1" >
-
+
-
- -
BACKER
-
+ {/*
+
BACKER
Home @@ -216,11 +214,9 @@ function UserDropdown() { Backed Projects -
+
*/}
- -
CREATOR
-
+
DASHBOARD
- Created Projects + Companies - Start a New Project + Create a New Company
diff --git a/app/components/ui/button.tsx b/app/components/ui/button.tsx index 36601c5..2a4e0b9 100644 --- a/app/components/ui/button.tsx +++ b/app/components/ui/button.tsx @@ -14,6 +14,8 @@ const buttonVariants = cva( 'bg-destructive text-destructive-foreground hover:bg-destructive/80', outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', + outline_2: + 'border border-input bg-background hover:bg-primary hover:text-primary-foreground', secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', ghost: 'hover:bg-accent hover:text-accent-foreground', @@ -21,6 +23,7 @@ const buttonVariants = cva( }, size: { default: 'h-10 px-4 py-2', + full: 'w-full h-full', wide: 'px-24 py-5', sm: 'h-9 rounded-md px-3', lg: 'h-11 rounded-md px-8', diff --git a/app/root.tsx b/app/root.tsx index 428f086..c00273d 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -207,10 +207,6 @@ function App() { const data = useLoaderData() const nonce = useNonce() const theme = useTheme() - // const matches = useMatches() - // const isOnCompanyPage = !!matches.find( - // m => m.id === 'routes/c.$companyId+/_layout_company', - // ) useToast(data.toast) @@ -219,12 +215,10 @@ function App() {
- {/* {!isOnCompanyPage && ( */}
- {/* )} */}
diff --git a/app/routes/_marketing+/index.tsx b/app/routes/_marketing+/index.tsx index f25d1bb..747cbc6 100644 --- a/app/routes/_marketing+/index.tsx +++ b/app/routes/_marketing+/index.tsx @@ -1,13 +1,26 @@ -import { type MetaFunction } from '@remix-run/node' +import { + type LoaderFunctionArgs, + type MetaFunction, + redirect, +} from '@remix-run/node' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '#app/components/ui/tooltip.tsx' +import { getUserId } from '#app/utils/auth.server.ts' import { cn } from '#app/utils/misc.tsx' import { logos } from './logos/logos.ts' +export async function loader({ request }: LoaderFunctionArgs) { + const userId = await getUserId(request) + + if (userId) return redirect('/studio') + + return null +} + export const meta: MetaFunction = () => [{ title: 'Epic Notes' }] // Tailwind Grid cell classes lookup diff --git a/app/routes/studio+/_layout_studio.tsx b/app/routes/studio+/_layout_studio.tsx index 9d748bb..a07aa4a 100644 --- a/app/routes/studio+/_layout_studio.tsx +++ b/app/routes/studio+/_layout_studio.tsx @@ -6,7 +6,7 @@ export default function LayoutStudio() { <> -
+
diff --git a/app/routes/studio+/c.new.tsx b/app/routes/studio+/c.new.tsx new file mode 100644 index 0000000..b626f8a --- /dev/null +++ b/app/routes/studio+/c.new.tsx @@ -0,0 +1,119 @@ +import { getFormProps, getInputProps, useForm } from '@conform-to/react' +import { getZodConstraint, parseWithZod } from '@conform-to/zod' +import { + type LoaderFunctionArgs, + type ActionFunctionArgs, +} from '@remix-run/node' +import { Form, json, useActionData } from '@remix-run/react' +import { z } from 'zod' +import { Field } from '#app/components/forms' +import { requireUserId } from '#app/utils/auth.server' +import { prisma } from '#app/utils/db.server' +import { redirectWithToast } from '#app/utils/toast.server' + +export async function loader({ request }: LoaderFunctionArgs) { + await requireUserId(request) + + return null +} + +const StudioCompanyNewSchema = z.object({ + name: z.string().min(4).max(60), +}) + +export async function action({ request }: ActionFunctionArgs) { + const userId = await requireUserId(request) + + const formData = await request.formData() + + const submission = await parseWithZod(formData, { + schema: StudioCompanyNewSchema.superRefine(async (data, ctx) => { + const existingCompany = await prisma.user.findFirst({ + where: { + id: userId, + userCompanies: { + some: { + company: { + name: data.name, + }, + isOwner: true, + }, + }, + }, + select: { + id: true, + }, + }) + + if (existingCompany) { + ctx.addIssue({ + path: ['name'], + code: z.ZodIssueCode.custom, + message: 'A company with this name already exists', + }) + return + } + }), + async: true, + }) + + if (submission.status !== 'success') { + return json( + { result: submission.reply() }, + { status: submission.status === 'error' ? 400 : 200 }, + ) + } + + const { name } = submission.value + + const company = await prisma.company.create({ + data: { + name, + users: { + create: { + userId: userId, + isOwner: true, + }, + }, + }, + select: { + id: true, + name: true, + }, + }) + + return redirectWithToast(`/c/${company.id}`, { + type: 'success', + title: 'Company Created!', + description: `You've created ${company.name} company!`, + }) +} + +export default function StudioCompanyNew() { + const actionData = useActionData() + + const [form, fields] = useForm({ + id: 'company-new', + constraint: getZodConstraint(StudioCompanyNewSchema), + lastResult: actionData?.result, + onValidate({ formData }) { + return parseWithZod(formData, { schema: StudioCompanyNewSchema }) + }, + }) + + return ( +
+ + + ) +} diff --git a/app/routes/studio+/c.tsx b/app/routes/studio+/c.tsx new file mode 100644 index 0000000..73bfdcd --- /dev/null +++ b/app/routes/studio+/c.tsx @@ -0,0 +1,21 @@ +import { type LoaderFunctionArgs } from '@remix-run/node' +import { Outlet } from '@remix-run/react' +import { requireUserId } from '#app/utils/auth.server' + +export async function loader({ request }: LoaderFunctionArgs) { + await requireUserId(request) + + return null +} + +export default function StudioCompanies() { + return ( +
+

Unknown Route

+ +
+ +
+
+ ) +} diff --git a/app/routes/studio+/company.new.tsx b/app/routes/studio+/company.new.tsx deleted file mode 100644 index d1b5f64..0000000 --- a/app/routes/studio+/company.new.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { - type LoaderFunctionArgs, - type ActionFunctionArgs, -} from '@remix-run/node' -import { requireUserId } from '#app/utils/auth.server' -import { prisma } from '#app/utils/db.server' -import { redirectWithToast } from '#app/utils/toast.server' - -export async function loader({ request }: LoaderFunctionArgs) { - await requireUserId(request) - - return null -} - -export async function action({ request }: ActionFunctionArgs) { - const userId = await requireUserId(request) - - const company = await prisma.company.create({ - data: { - name: 'test', - users: { - create: { - userId: userId, - isOwner: true, - }, - }, - }, - select: { - id: true, - name: true, - }, - }) - - return redirectWithToast(`/c/${company.id}`, { - type: 'success', - title: 'Company Created!', - description: `You've created ${company.name} company!`, - }) -} - -export default function CompanyNew() { - return ( -
-

Unknown Route

-
- ) -} diff --git a/app/routes/studio+/index.tsx b/app/routes/studio+/index.tsx index 4cbea83..7709aba 100644 --- a/app/routes/studio+/index.tsx +++ b/app/routes/studio+/index.tsx @@ -1,7 +1,9 @@ import { type LoaderFunctionArgs, json } from '@remix-run/node' -import { useLoaderData } from '@remix-run/react' +import { Link, useLoaderData } from '@remix-run/react' import { logoutAndRedirect, requireUserId } from '#app/utils/auth.server' import { prisma } from '#app/utils/db.server' +import { Button } from '#app/components/ui/button' +import { Spacer } from '#app/components/spacer' export async function loader({ request }: LoaderFunctionArgs) { const userId = await requireUserId(request) @@ -42,13 +44,25 @@ export default function StudioIndex() { const data = useLoaderData() return ( -
-

Unknown Route

-
{data.user.name}
+ <> +

Studio

+ +
+ + + +
Hello
+
Hello
+
Hello
+
+
{data.user.userCompanies.map(userCompany => (
- {userCompany.company.name} : {userCompany.isOwner} :{' '} + {userCompany.company.name} :{' '} + {userCompany.isOwner ? 'true' : 'false'} :{' '} {userCompany.Role.map(role => (
{role.name} :{' '} @@ -63,6 +77,6 @@ export default function StudioIndex() {
))}
-
+ ) } diff --git a/app/utils/user.ts b/app/utils/user.ts index 0958e63..7592480 100644 --- a/app/utils/user.ts +++ b/app/utils/user.ts @@ -25,7 +25,7 @@ export function useUser() { } type Action = 'create' | 'read' | 'update' | 'delete' -type Entity = 'user' | 'note' +type Entity = 'user' | 'note' | 'company' type Access = 'own' | 'any' | 'own,any' | 'any,own' export type PermissionString = | `${Action}:${Entity}` diff --git a/package-lock.json b/package-lock.json index 81aade4..f95cd31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@epic-web/totp": "^1.1.1", "@nasa-gcn/remix-seo": "^2.0.0", "@paralleldrive/cuid2": "^2.2.2", - "@prisma/client": "^5.10.0-dev.1", + "@prisma/client": "^5.9.1", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", @@ -24,11 +24,11 @@ "@radix-ui/react-toast": "^1.1.5", "@radix-ui/react-tooltip": "^1.0.7", "@react-email/components": "0.0.14", - "@remix-run/css-bundle": "^2.5.1", - "@remix-run/express": "^2.5.1", - "@remix-run/node": "^2.5.1", - "@remix-run/react": "^2.5.1", - "@remix-run/server-runtime": "^2.5.1", + "@remix-run/css-bundle": "^2.6.0", + "@remix-run/express": "^2.6.0", + "@remix-run/node": "^2.6.0", + "@remix-run/react": "^2.6.0", + "@remix-run/server-runtime": "^2.6.0", "@sentry/profiling-node": "^1.3.5", "@sentry/remix": "^7.99.0", "address": "^2.0.1", @@ -76,14 +76,14 @@ }, "devDependencies": { "@faker-js/faker": "^8.4.0", - "@playwright/test": "^1.41.1", - "@remix-run/dev": "^2.5.1", - "@remix-run/eslint-config": "^2.5.1", - "@remix-run/serve": "^2.5.1", - "@remix-run/testing": "^2.5.1", + "@playwright/test": "^1.41.2", + "@remix-run/dev": "^2.6.0", + "@remix-run/eslint-config": "^2.6.0", + "@remix-run/serve": "^2.6.0", + "@remix-run/testing": "^2.6.0", "@sly-cli/sly": "^1.8.0", "@testing-library/jest-dom": "^6.4.1", - "@testing-library/react": "^14.2.0", + "@testing-library/react": "^14.2.1", "@testing-library/user-event": "^14.5.2", "@total-typescript/ts-reset": "^0.5.1", "@types/bcryptjs": "^2.4.6", @@ -95,9 +95,9 @@ "@types/fs-extra": "^11.0.4", "@types/glob": "^8.1.0", "@types/morgan": "^1.9.9", - "@types/node": "^20.11.14", + "@types/node": "^20.11.16", "@types/qrcode": "^1.5.5", - "@types/react": "^18.2.50", + "@types/react": "^18.2.51", "@types/react-dom": "^18.2.18", "@types/set-cookie-parser": "^2.4.7", "@types/source-map-support": "^0.5.10", @@ -116,7 +116,7 @@ "prettier": "^3.2.4", "prettier-plugin-sql": "^0.18.0", "prettier-plugin-tailwindcss": "^0.5.11", - "prisma": "^5.10.0-dev.1", + "prisma": "^5.9.1", "remix-flat-routes": "^0.6.4", "rimraf": "^5.0.5", "tsx": "^4.7.0", @@ -1987,12 +1987,12 @@ } }, "node_modules/@playwright/test": { - "version": "1.41.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.41.1.tgz", - "integrity": "sha512-9g8EWTjiQ9yFBXc6HjCWe41msLpxEX0KhmfmPl9RPLJdfzL4F0lg2BdJ91O9azFdl11y1pmpwdjBiSxvqc+btw==", + "version": "1.41.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.41.2.tgz", + "integrity": "sha512-qQB9h7KbibJzrDpkXkYvsmiDJK14FULCCZgEcoe2AvFAS64oCirWTwzTlAYEbKaRxWs5TFesE1Na6izMv3HfGg==", "dev": true, "dependencies": { - "playwright": "1.41.1" + "playwright": "1.41.2" }, "bin": { "playwright": "cli.js" @@ -2002,9 +2002,9 @@ } }, "node_modules/@prisma/client": { - "version": "5.10.0-dev.1", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.10.0-dev.1.tgz", - "integrity": "sha512-Y3BcweUn4+wpR+IB0HvssbVjubRLsPxUZhrSY1b6lJWkwUt0Db2W6bWYkRDEJBZzI9A8q5b463MJkrVRVNzZVg==", + "version": "5.9.1", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.9.1.tgz", + "integrity": "sha512-caSOnG4kxcSkhqC/2ShV7rEoWwd3XrftokxJqOCMVvia4NYV/TPtJlS9C2os3Igxw/Qyxumj9GBQzcStzECvtQ==", "hasInstallScript": true, "engines": { "node": ">=16.13" @@ -2019,22 +2019,22 @@ } }, "node_modules/@prisma/debug": { - "version": "5.10.0-dev.1", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.10.0-dev.1.tgz", - "integrity": "sha512-LZZI+iyw2gBQkxgEBD9OOf9YDPlvmHQI4GOruqAkEzHs+CCQKRWu/QiepppkLWLaj2INbncQJPiSTm5UOk7THw==", + "version": "5.9.1", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.9.1.tgz", + "integrity": "sha512-yAHFSFCg8KVoL0oRUno3m60GAjsUKYUDkQ+9BA2X2JfVR3kRVSJFc/GpQ2fSORi4pSHZR9orfM4UC9OVXIFFTA==", "dev": true }, "node_modules/@prisma/engines": { - "version": "5.10.0-dev.1", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.10.0-dev.1.tgz", - "integrity": "sha512-zIu9K3K0CXAiqWJ63y6HTRKkgh8qylOaMXg6QvlRjPArm1nByHM9Qv5VOPyvIy5QYng8zPUIMi0AhPdtBWLe9g==", + "version": "5.9.1", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.9.1.tgz", + "integrity": "sha512-gkdXmjxQ5jktxWNdDA5aZZ6R8rH74JkoKq6LD5mACSvxd2vbqWeWIOV0Py5wFC8vofOYShbt6XUeCIUmrOzOnQ==", "dev": true, "hasInstallScript": true, "dependencies": { - "@prisma/debug": "5.10.0-dev.1", + "@prisma/debug": "5.9.1", "@prisma/engines-version": "5.9.0-32.23fdc5965b1e05fc54e5f26ed3de66776b93de64", - "@prisma/fetch-engine": "5.10.0-dev.1", - "@prisma/get-platform": "5.10.0-dev.1" + "@prisma/fetch-engine": "5.9.1", + "@prisma/get-platform": "5.9.1" } }, "node_modules/@prisma/engines-version": { @@ -2044,23 +2044,23 @@ "dev": true }, "node_modules/@prisma/fetch-engine": { - "version": "5.10.0-dev.1", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.10.0-dev.1.tgz", - "integrity": "sha512-4sRlPxxnSHMACgLYv8Fa9yvOPQfIws0QpjpOyIe6i87O8J2JOkcptNao4VVjQt5CVPQQllAnuRkBRBQq78kTcQ==", + "version": "5.9.1", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.9.1.tgz", + "integrity": "sha512-l0goQOMcNVOJs1kAcwqpKq3ylvkD9F04Ioe1oJoCqmz05mw22bNAKKGWuDd3zTUoUZr97va0c/UfLNru+PDmNA==", "dev": true, "dependencies": { - "@prisma/debug": "5.10.0-dev.1", + "@prisma/debug": "5.9.1", "@prisma/engines-version": "5.9.0-32.23fdc5965b1e05fc54e5f26ed3de66776b93de64", - "@prisma/get-platform": "5.10.0-dev.1" + "@prisma/get-platform": "5.9.1" } }, "node_modules/@prisma/get-platform": { - "version": "5.10.0-dev.1", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.10.0-dev.1.tgz", - "integrity": "sha512-X1oHa6Tssm12iQUM3UOglmZYiZU9DsS0rCVb3YpViRweIWDvQM2G/9Zdc/y1lns2+gUAWKMZQ4LEYPSs/uA40w==", + "version": "5.9.1", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.9.1.tgz", + "integrity": "sha512-6OQsNxTyhvG+T2Ksr8FPFpuPeL4r9u0JF0OZHUBI/Uy9SS43sPyAIutt4ZEAyqWQt104ERh70EZedkHZKsnNbg==", "dev": true, "dependencies": { - "@prisma/debug": "5.10.0-dev.1" + "@prisma/debug": "5.9.1" } }, "node_modules/@radix-ui/primitive": { @@ -3037,17 +3037,17 @@ } }, "node_modules/@remix-run/css-bundle": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@remix-run/css-bundle/-/css-bundle-2.5.1.tgz", - "integrity": "sha512-QPeNvgD7fj4NmXB9CuGM5Mp0ZtM43dMeda8Ik3AUoQOMgMWb0d4jK4Cye6eoTGwJOron6XISKh0mq8MorucWEQ==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@remix-run/css-bundle/-/css-bundle-2.6.0.tgz", + "integrity": "sha512-c3I1FVsWeBoA3c92Fwa1sufxAlosovfE2V7hwjc9HeQHA3DAHe18RXfgU6my/IvpItkIvaEgQPuoWEA6Dh8VDQ==", "engines": { "node": ">=18.0.0" } }, "node_modules/@remix-run/dev": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@remix-run/dev/-/dev-2.5.1.tgz", - "integrity": "sha512-IrYhWANubH+WM62Wz55n8NWT5kBqfbyytXDFlP0VoZLrr1IpJf2GtaT4IA+CODMaNoeXeMACOD5Tw5/Y2bO5lA==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@remix-run/dev/-/dev-2.6.0.tgz", + "integrity": "sha512-wf5DoKxBwz3/84FNyFM6NKvQIOEv+Ukwj9DjXrDs6YLI6oSqw2XsJCxWN4lAbOxXuK37pBt1WAE8LzEMuyowsw==", "dev": true, "dependencies": { "@babel/core": "^7.21.8", @@ -3060,9 +3060,9 @@ "@babel/types": "^7.22.5", "@mdx-js/mdx": "^2.3.0", "@npmcli/package-json": "^4.0.1", - "@remix-run/node": "2.5.1", - "@remix-run/router": "1.14.2", - "@remix-run/server-runtime": "2.5.1", + "@remix-run/node": "2.6.0", + "@remix-run/router": "1.15.0", + "@remix-run/server-runtime": "2.6.0", "@types/mdx": "^2.0.5", "@vanilla-extract/integration": "^6.2.0", "arg": "^5.0.1", @@ -3111,9 +3111,10 @@ "node": ">=18.0.0" }, "peerDependencies": { - "@remix-run/serve": "^2.5.1", + "@remix-run/serve": "^2.6.0", "typescript": "^5.1.0", - "vite": "^5.0.0" + "vite": "^5.0.0", + "wrangler": "^3.24.0" }, "peerDependenciesMeta": { "@remix-run/serve": { @@ -3124,6 +3125,9 @@ }, "vite": { "optional": true + }, + "wrangler": { + "optional": true } } }, @@ -3714,9 +3718,9 @@ } }, "node_modules/@remix-run/eslint-config": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@remix-run/eslint-config/-/eslint-config-2.5.1.tgz", - "integrity": "sha512-PpLj0QSd2NZ12KdTA2QYPd/FK3Szu9Np7kTmx26VxDZJTzQYSgGb5i2O9uby+j2sD68zR/+EAYaIcOlq66ekJw==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@remix-run/eslint-config/-/eslint-config-2.6.0.tgz", + "integrity": "sha512-WIYyCl8qHNDyZy05ggzl/x3wbnLtmHWOaeSqvtshYbvBsKpyLT0xVQNWyj0XEzk6hWWk+93b0yQ5ihumjM4Y1Q==", "dev": true, "dependencies": { "@babel/core": "^7.21.8", @@ -3751,11 +3755,11 @@ } }, "node_modules/@remix-run/express": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@remix-run/express/-/express-2.5.1.tgz", - "integrity": "sha512-ISaf2hzHxDTS1hNsOovRoSfIQC29m4ogzXvBC6xp2BuJj0K8R0yQ4RFD4+qUFEUnS2n6MyWyjFQRhOC6PhQhRw==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@remix-run/express/-/express-2.6.0.tgz", + "integrity": "sha512-sAb0eoMwqP4yhDCnT5H6Db0svfzBuRbuPuPSbQCtuLRWkEGmIhWN7vKA0IqaxsUA09qKwQQiKQdupra55KfCyA==", "dependencies": { - "@remix-run/node": "2.5.1" + "@remix-run/node": "2.6.0" }, "engines": { "node": ">=18.0.0" @@ -3771,11 +3775,11 @@ } }, "node_modules/@remix-run/node": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@remix-run/node/-/node-2.5.1.tgz", - "integrity": "sha512-UI442xzHAiokmsfrYOabMQB024+IizmRhZBGcNa42QjJWsNqogy1bNwYhzEpB6oQEB1wF3vwOKK1AD1/iYA/9A==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@remix-run/node/-/node-2.6.0.tgz", + "integrity": "sha512-bWemy3g258Kdqi+4OxIEZ7QS64T96jNK6a7NdlPXGJZqeLpxM5NmlCl/slSdx52oTi9r5Xoz1Tm4uR37nD1/Xw==", "dependencies": { - "@remix-run/server-runtime": "2.5.1", + "@remix-run/server-runtime": "2.6.0", "@remix-run/web-fetch": "^4.4.2", "@remix-run/web-file": "^3.1.0", "@remix-run/web-stream": "^1.1.0", @@ -3797,14 +3801,14 @@ } }, "node_modules/@remix-run/react": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@remix-run/react/-/react-2.5.1.tgz", - "integrity": "sha512-MNXHLj4Iu9Iyi+3uY61JZJ1Rtx2nM/z11j9AtwQdEADkh1/t9GruhtT/8VLplToOl0qWZKItboWScKf6uRQsrw==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@remix-run/react/-/react-2.6.0.tgz", + "integrity": "sha512-m/Ph6bryny7wrmrQyXQMvIiW+cBLrU/MepcLGFPvTVVwvfeiGBgXRiYZJ6yPNsfrmHFaS83d+Ja/Mx4N4zUWcg==", "dependencies": { - "@remix-run/router": "1.14.2", - "@remix-run/server-runtime": "2.5.1", - "react-router": "6.21.3", - "react-router-dom": "6.21.3" + "@remix-run/router": "1.15.0", + "@remix-run/server-runtime": "2.6.0", + "react-router": "6.22.0", + "react-router-dom": "6.22.0" }, "engines": { "node": ">=18.0.0" @@ -3821,21 +3825,21 @@ } }, "node_modules/@remix-run/router": { - "version": "1.14.2", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.14.2.tgz", - "integrity": "sha512-ACXpdMM9hmKZww21yEqWwiLws/UPLhNKvimN8RrYSqPSvB3ov7sLvAcfvaxePeLvccTQKGdkDIhLYApZVDFuKg==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.0.tgz", + "integrity": "sha512-HOil5aFtme37dVQTB6M34G95kPM3MMuqSmIRVCC52eKV+Y/tGSqw9P3rWhlAx6A+mz+MoX+XxsGsNJbaI5qCgQ==", "engines": { "node": ">=14.0.0" } }, "node_modules/@remix-run/serve": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@remix-run/serve/-/serve-2.5.1.tgz", - "integrity": "sha512-r1IWfirwkLrxADd8uFUIpLR1wMU8VeRI4ED4SpbhrKwqODLrYtv5irzjei+r/w0y0Oob8DMHnYxg03UY4T7ejg==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@remix-run/serve/-/serve-2.6.0.tgz", + "integrity": "sha512-OIvGWaruFLCMLpemovitE8WnxRD/4TBPDTsgC/pSxcjDfuXv3nZk0nhAcmfklPSnxBCCct50cbtEN1xcrPqeyw==", "dev": true, "dependencies": { - "@remix-run/express": "2.5.1", - "@remix-run/node": "2.5.1", + "@remix-run/express": "2.6.0", + "@remix-run/node": "2.6.0", "chokidar": "^3.5.3", "compression": "^1.7.4", "express": "^4.17.1", @@ -3863,11 +3867,11 @@ } }, "node_modules/@remix-run/server-runtime": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@remix-run/server-runtime/-/server-runtime-2.5.1.tgz", - "integrity": "sha512-bP31jrVbYTJ2eP5sxZfDgT1YyXzDlzsfMxGYVzpaoLCYDJAekq1QpHLLXKGOXhmyb46O9rdhlQKfwD6WpAxr3A==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@remix-run/server-runtime/-/server-runtime-2.6.0.tgz", + "integrity": "sha512-qFXDl4pK55njBLuvyRn5AkI/hu8fEU3t1XFKv0Syivx0nmUVpWMW25Uzi1pkX/chF1VIxCVrZ8KuQ1rcrKj+DQ==", "dependencies": { - "@remix-run/router": "1.14.2", + "@remix-run/router": "1.15.0", "@types/cookie": "^0.6.0", "@web3-storage/multipart-parser": "^1.0.0", "cookie": "^0.6.0", @@ -3887,15 +3891,15 @@ } }, "node_modules/@remix-run/testing": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@remix-run/testing/-/testing-2.5.1.tgz", - "integrity": "sha512-No7e7/Kvu7NoXrXEsSKVEP9YFLeUpkGME54f3R4jAd2mvLySY5f87iFVub3ZhdgqLQOxLuJfLPrdaz1Ml2hSDg==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@remix-run/testing/-/testing-2.6.0.tgz", + "integrity": "sha512-hjIQxSke6ohccJrKKlB9j36W7pXesTR7+nkq68+A+NAZWt6wYCB+iSOKveqmtsmGiXnSOfFMhZcNNrMJjyRrLw==", "dev": true, "dependencies": { - "@remix-run/node": "2.5.1", - "@remix-run/react": "2.5.1", - "@remix-run/router": "1.14.2", - "react-router-dom": "6.21.3" + "@remix-run/node": "2.6.0", + "@remix-run/react": "2.6.0", + "@remix-run/router": "1.15.0", + "react-router-dom": "6.22.0" }, "engines": { "node": ">=18.0.0" @@ -5308,9 +5312,9 @@ "dev": true }, "node_modules/@testing-library/react": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.2.0.tgz", - "integrity": "sha512-7uBnPHyOG6nDGCzv8SLeJbSa33ZoYw7swYpSLIgJvBALdq7l9zPNk33om4USrxy1lKTxXaVfufzLmq83WNfWIw==", + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.2.1.tgz", + "integrity": "sha512-sGdjws32ai5TLerhvzThYFbpnF9XtL65Cjf+gB0Dhr29BGqK+mAeN7SURSdu+eqgET4ANcWoC7FQpkaiGvBr+A==", "dev": true, "dependencies": { "@babel/runtime": "^7.12.5", @@ -5610,9 +5614,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.11.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.14.tgz", - "integrity": "sha512-w3yWCcwULefjP9DmDDsgUskrMoOy5Z8MiwKHr1FvqGPtx7CvJzQvxD7eKpxNtklQxLruxSXWddyeRtyud0RcXQ==", + "version": "20.11.16", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.16.tgz", + "integrity": "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -5646,9 +5650,9 @@ "dev": true }, "node_modules/@types/react": { - "version": "18.2.50", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.50.tgz", - "integrity": "sha512-y0XIDJkqp9HynS1VBktZG9mUziHTK5WZTAFDP/UfzSq+poV1drUKsr4VkjMyHTbqMz26BwgLZVYdx/EgPm7EkQ==", + "version": "18.2.51", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.51.tgz", + "integrity": "sha512-XeoMaU4CzyjdRr3c4IQQtiH7Rpo18V07rYZUucEZQwOUEtGgTXv7e6igQiQ+xnV6MbMe1qjEmKdgMNnfppnXfg==", "dev": true, "dependencies": { "@types/prop-types": "*", @@ -15141,12 +15145,12 @@ } }, "node_modules/playwright": { - "version": "1.41.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.41.1.tgz", - "integrity": "sha512-gdZAWG97oUnbBdRL3GuBvX3nDDmUOuqzV/D24dytqlKt+eI5KbwusluZRGljx1YoJKZ2NRPaeWiFTeGZO7SosQ==", + "version": "1.41.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.41.2.tgz", + "integrity": "sha512-v0bOa6H2GJChDL8pAeLa/LZC4feoAMbSQm1/jF/ySsWWoaNItvrMP7GEkvEEFyCTUYKMxjQKaTSg5up7nR6/8A==", "dev": true, "dependencies": { - "playwright-core": "1.41.1" + "playwright-core": "1.41.2" }, "bin": { "playwright": "cli.js" @@ -15159,9 +15163,9 @@ } }, "node_modules/playwright-core": { - "version": "1.41.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.41.1.tgz", - "integrity": "sha512-/KPO5DzXSMlxSX77wy+HihKGOunh3hqndhqeo/nMxfigiKzogn8kfL0ZBDu0L1RKgan5XHCPmn6zXd2NUJgjhg==", + "version": "1.41.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.41.2.tgz", + "integrity": "sha512-VaTvwCA4Y8kxEe+kfm2+uUUw5Lubf38RxF7FpBxLPmGe5sdNkSg5e3ChEigaGrX7qdqT3pt2m/98LiyvU2x6CA==", "dev": true, "bin": { "playwright-core": "cli.js" @@ -15599,13 +15603,13 @@ } }, "node_modules/prisma": { - "version": "5.10.0-dev.1", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.10.0-dev.1.tgz", - "integrity": "sha512-11ppGQks5MPTAqYQjYXS5BHdvrPNP62FjqmM6Y6omP4zdcTKj11RJ+wPWwWE71OK/doY4+YnnbIaXDtUSHDAMA==", + "version": "5.9.1", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.9.1.tgz", + "integrity": "sha512-Hy/8KJZz0ELtkw4FnG9MS9rNWlXcJhf98Z2QMqi0QiVMoS8PzsBkpla0/Y5hTlob8F3HeECYphBjqmBxrluUrQ==", "dev": true, "hasInstallScript": true, "dependencies": { - "@prisma/engines": "5.10.0-dev.1" + "@prisma/engines": "5.9.1" }, "bin": { "prisma": "build/index.js" @@ -16132,11 +16136,11 @@ } }, "node_modules/react-router": { - "version": "6.21.3", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.21.3.tgz", - "integrity": "sha512-a0H638ZXULv1OdkmiK6s6itNhoy33ywxmUFT/xtSoVyf9VnC7n7+VT4LjVzdIHSaF5TIh9ylUgxMXksHTgGrKg==", + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.0.tgz", + "integrity": "sha512-q2yemJeg6gw/YixRlRnVx6IRJWZD6fonnfZhN1JIOhV2iJCPeRNSH3V1ISwHf+JWcESzLC3BOLD1T07tmO5dmg==", "dependencies": { - "@remix-run/router": "1.14.2" + "@remix-run/router": "1.15.0" }, "engines": { "node": ">=14.0.0" @@ -16146,12 +16150,12 @@ } }, "node_modules/react-router-dom": { - "version": "6.21.3", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.21.3.tgz", - "integrity": "sha512-kNzubk7n4YHSrErzjLK72j0B5i969GsuCGazRl3G6j1zqZBLjuSlYBdVdkDOgzGdPIffUOc9nmgiadTEVoq91g==", + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.22.0.tgz", + "integrity": "sha512-z2w+M4tH5wlcLmH3BMMOMdrtrJ9T3oJJNsAlBJbwk+8Syxd5WFJ7J5dxMEW0/GEXD1BBis4uXRrNIz3mORr0ag==", "dependencies": { - "@remix-run/router": "1.14.2", - "react-router": "6.21.3" + "@remix-run/router": "1.15.0", + "react-router": "6.22.0" }, "engines": { "node": ">=14.0.0" diff --git a/package.json b/package.json index 186588c..e6a1318 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "@epic-web/totp": "^1.1.1", "@nasa-gcn/remix-seo": "^2.0.0", "@paralleldrive/cuid2": "^2.2.2", - "@prisma/client": "^5.10.0-dev.1", + "@prisma/client": "^5.9.1", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", @@ -56,11 +56,11 @@ "@radix-ui/react-toast": "^1.1.5", "@radix-ui/react-tooltip": "^1.0.7", "@react-email/components": "0.0.14", - "@remix-run/css-bundle": "^2.5.1", - "@remix-run/express": "^2.5.1", - "@remix-run/node": "^2.5.1", - "@remix-run/react": "^2.5.1", - "@remix-run/server-runtime": "^2.5.1", + "@remix-run/css-bundle": "^2.6.0", + "@remix-run/express": "^2.6.0", + "@remix-run/node": "^2.6.0", + "@remix-run/react": "^2.6.0", + "@remix-run/server-runtime": "^2.6.0", "@sentry/profiling-node": "^1.3.5", "@sentry/remix": "^7.99.0", "address": "^2.0.1", @@ -108,14 +108,14 @@ }, "devDependencies": { "@faker-js/faker": "^8.4.0", - "@playwright/test": "^1.41.1", - "@remix-run/dev": "^2.5.1", - "@remix-run/eslint-config": "^2.5.1", - "@remix-run/serve": "^2.5.1", - "@remix-run/testing": "^2.5.1", + "@playwright/test": "^1.41.2", + "@remix-run/dev": "^2.6.0", + "@remix-run/eslint-config": "^2.6.0", + "@remix-run/serve": "^2.6.0", + "@remix-run/testing": "^2.6.0", "@sly-cli/sly": "^1.8.0", "@testing-library/jest-dom": "^6.4.1", - "@testing-library/react": "^14.2.0", + "@testing-library/react": "^14.2.1", "@testing-library/user-event": "^14.5.2", "@total-typescript/ts-reset": "^0.5.1", "@types/bcryptjs": "^2.4.6", @@ -127,9 +127,9 @@ "@types/fs-extra": "^11.0.4", "@types/glob": "^8.1.0", "@types/morgan": "^1.9.9", - "@types/node": "^20.11.14", + "@types/node": "^20.11.16", "@types/qrcode": "^1.5.5", - "@types/react": "^18.2.50", + "@types/react": "^18.2.51", "@types/react-dom": "^18.2.18", "@types/set-cookie-parser": "^2.4.7", "@types/source-map-support": "^0.5.10", @@ -148,7 +148,7 @@ "prettier": "^3.2.4", "prettier-plugin-sql": "^0.18.0", "prettier-plugin-tailwindcss": "^0.5.11", - "prisma": "^5.10.0-dev.1", + "prisma": "^5.9.1", "remix-flat-routes": "^0.6.4", "rimraf": "^5.0.5", "tsx": "^4.7.0", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d05cdb9..efdcd64 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -19,15 +19,16 @@ model User { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - image UserImage? - password Password? + password Password? + image UserImage? + platformStatus PlatformStatus? @relation(fields: [platformStatusId], references: [id]) + platformStatusId Int? + notes Note[] roles Role[] sessions Session[] userCompanies UserCompany[] connections Connection[] - transactions Transaction[] - invoices Invoice[] } model Note { @@ -172,93 +173,185 @@ model Connection { @@unique([providerName, providerId]) } +model PlatformStatus { + id Int @id @default(autoincrement()) + key String @unique + label String? + color String? + + createdAt DateTime @default(now()) + updatedAt DateTime? @updatedAt + + companies Company[] + users User[] +} + +model TransactionStatus { + id Int @id @default(autoincrement()) + key String @unique + label String? + color String? + + createdAt DateTime @default(now()) + updatedAt DateTime? @updatedAt + + // ! Maybe remove transactions from here? I would hardly ever use them to query + transactions Transaction[] + saleInvoices SaleInvoice[] + purchaseBills PurchaseBill[] +} + // * Account specific models // * This is for a simple personal finance app // Company model represents a business entity or organization that the accounting system will handle. model Company { - id String @id @default(cuid()) - name String // Name of the company or business entity. + id String @id @default(cuid()) + name String // Name of the company or business entity. + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + platformStatus PlatformStatus? @relation(fields: [platformStatusId], references: [id]) + platformStatusId Int? + users UserCompany[] // Users associated with the company. accounts Account[] // Financial accounts belonging to the company. - invoices Invoice[] // Invoices generated by the company. + saleInvoices SaleInvoice[] // Invoices generated by the company. transactions Transaction[] // Financial transactions recorded for the company. - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + PurchaseBill PurchaseBill[] // Bills received by the company. } // UserCompany is a join table that allows users to be associated with multiple companies. // It serves as a many-to-many relationship between User and Company models. model UserCompany { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) + isOwner Boolean @default(false) + user User @relation(fields: [userId], references: [id]) userId String company Company @relation(fields: [companyId], references: [id]) companyId String - isOwner Boolean @default(false) Role Role[] + + transactions Transaction[] + saleInvoices SaleInvoice[] + purchaseBills PurchaseBill[] } // Account model represents financial ledgers which are used to record transactions // and keep track of the financial status of various aspects of the company such as assets, liabilities, revenue, etc. model Account { - id Int @id @default(autoincrement()) - company Company @relation(fields: [companyId], references: [id]) - companyId String - name String // Name of the financial ledger/account. - balance Float // Current balance of the account. - type String // Type of the account, e.g., revenue, expense. + id Int @id @default(autoincrement()) + name String // Name of the financial ledger/account. + balance Float // Current balance of the account. + type String // Type of the account, e.g., revenue, expense. + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + company Company @relation(fields: [companyId], references: [id]) + companyId String + transactions Transaction[] // Transactions associated with this account. - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + PurchaseBill PurchaseBill[] // Bills associated with this account. + SaleInvoice SaleInvoice[] // Invoices associated with this account. } // Transaction model records all the financial activities which affect accounts, defined by a change in balance. model Transaction { id Int @id @default(autoincrement()) - company Company @relation(fields: [companyId], references: [id]) - companyId String date DateTime @default(now()) - account Account @relation(fields: [accountId], references: [id]) - accountId Int description String // Describes the purpose of the transaction. amount Float // The monetary value of the transaction. - type String // Type can include 'sale', 'purchase', etc. + type String? // Type can include 'sale', 'purchase', etc. category String // Categorizes the transaction for accounting purposes. - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - User User? @relation(fields: [userId], references: [id]) - userId String? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + company Company @relation(fields: [companyId], references: [id]) + companyId String + account Account @relation(fields: [accountId], references: [id]) + accountId Int + issuedBy UserCompany? @relation(fields: [issuedById], references: [id]) + issuedById Int? + transactionStatus TransactionStatus? @relation(fields: [transactionStatusId], references: [id]) + transactionStatusId Int? } // Invoice model represents bills to be sent to clients or received from suppliers, // which can include one or more payable items, and their total has an impact on the company's finances. -model Invoice { - id Int @id @default(autoincrement()) - company Company @relation(fields: [companyId], references: [id]) - companyId String - dateIssued DateTime @default(now()) +model SaleInvoice { + id Int @id @default(autoincrement()) + dateIssued DateTime @default(now()) dueDate DateTime // The date by which the invoice should be paid. - items InvoiceItem[] // Line items detailing individual charges on the invoice. totalAmount Float // Total monetary value to be paid / received for the invoice. - status String // Status could be 'pending', 'paid', 'overdue', etc. - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - User User? @relation(fields: [userId], references: [id]) - userId String? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + company Company @relation(fields: [companyId], references: [id]) + companyId String + issuedTo Account @relation(fields: [issuedToId], references: [id]) + issuedToId Int + issuedBy UserCompany? @relation(fields: [issuedById], references: [id]) + issuedById Int? + transactionStatus TransactionStatus? @relation(fields: [transactionStatusId], references: [id]) + transactionStatusId Int? + + items InvoiceItem[] // Line items detailing individual charges on the invoice. } // InvoiceItem model holds the details of each item or service listed in an invoice. // Each item has a price and quantity which contribute to the invoice's total. model InvoiceItem { - id Int @id @default(autoincrement()) - invoice Invoice @relation(fields: [invoiceId], references: [id]) + id Int @id @default(autoincrement()) + invoice SaleInvoice @relation(fields: [invoiceId], references: [id]) invoiceId Int - description String // Details of what the item or service is. + description String? // Details of what the item or service is. quantity Int // Quantity of the item or service provided. price Float // Price per unit of the item or service. - total Float @default(0) // Total price for the line item. - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + total Float @default(0) // Total price for the line item. + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +// PurchaseBill model for purchase transactions from vendors +model PurchaseBill { + id String @id @default(cuid()) + companyId String + company Company @relation(fields: [companyId], references: [id]) + dateIssued DateTime @default(now()) + dueDate DateTime // Due date for payment. + totalAmount Float // Total amount of the bill. + vendor String? // Vendor identifier or name. + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + issuedTo Account @relation(fields: [issuedToId], references: [id]) // Account to which the bill is issued. + issuedToId Int + + // Optional: Relate to a user who manages the bill + issuedBy UserCompany? @relation(fields: [issuedById], references: [id]) + issuedById Int? + transactionStatus TransactionStatus? @relation(fields: [transactionStatusId], references: [id]) + transactionStatusId Int? + + purchaseBillItems PurchaseBillItem[] // Individual items on the bill. +} + +// PurchaseBillItem model for line items on a purchase bill +model PurchaseBillItem { + id Int @id @default(autoincrement()) + purchaseBill PurchaseBill @relation(fields: [purchaseBillId], references: [id]) + purchaseBillId String + description String? // Description of the item or service. + quantity Int // Quantity of the item. + price Float // Unit price of the item. + total Float // Line item total (quantity * price). + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt }