diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 5dae40c..77b562b 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -2,7 +2,7 @@ const vitestFiles = ['app/**/__tests__/**/*', 'app/**/*.{spec,test}.*'] const testFiles = ['**/tests/**', ...vitestFiles] const appFiles = ['app/**'] -/** @type {import('@types/eslint').Linter.BaseConfig} */ +/** @type {import('@types/eslint').Linter.Config} */ module.exports = { extends: [ '@remix-run/eslint-config', diff --git a/app/components/account-ledger.tsx b/app/components/account-ledger.tsx new file mode 100644 index 0000000..1f9bc19 --- /dev/null +++ b/app/components/account-ledger.tsx @@ -0,0 +1,86 @@ +const people = [ + { + name: 'Lindsay Walton', + title: 'Front-end Developer', + email: 'lindsay.walton@example.com', + role: 'Member', + }, + { + name: 'Lindsay Walton', + title: 'Front-end Developer', + email: 'lindsay.walton@example.com', + role: 'Member', + }, + { + name: 'Lindsay Walton', + title: 'Front-end Developer', + email: 'lindsay.walton@example.com', + role: 'Member', + }, + { + name: 'Lindsay Walton', + title: 'Front-end Developer', + email: 'lindsay.walton@example.com', + role: 'Member', + }, + // More people... +] + +export default function AccountLedger() { + return ( +
+
+
+ + + + + + + + + + + {people.map(person => ( + + + + + + + ))} + +
+ Name + + Title + + Email + + Role +
+ {person.name} + + {person.title} + + {person.email} + + {person.role} +
+
+
+
+ ) +} diff --git a/app/root.tsx b/app/root.tsx index f9726b6..0c902cc 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -44,7 +44,6 @@ export const links: LinksFunction = () => { // Preload svg sprite as a resource to avoid render blocking { rel: 'preload', href: iconsHref, as: 'image' }, // Preload CSS as a resource to avoid render blocking - { rel: 'preload', href: tailwindStyleSheetUrl, as: 'style' }, { rel: 'mask-icon', href: '/favicons/mask-icon.svg' }, { rel: 'alternate icon', diff --git a/app/routes/c.$companyId.sales+/__sale-editor.server.tsx b/app/routes/c.$companyId.sales+/__sale-editor.server.tsx new file mode 100644 index 0000000..a68bcb7 --- /dev/null +++ b/app/routes/c.$companyId.sales+/__sale-editor.server.tsx @@ -0,0 +1,98 @@ +import { parseWithZod } from '@conform-to/zod' +import { json, type ActionFunctionArgs, redirect } from '@remix-run/node' +import { z } from 'zod' +import { prisma } from '#app/utils/db.server' +import { requireCompanyUserWithRBAC } from '#app/utils/permissions.server' +import { CompanySalesEditorSchema } from './__sale-editor' + +export async function action({ request, params }: ActionFunctionArgs) { + const user = await requireCompanyUserWithRBAC({ + request, + companyId: params.companyId!, + permission: 'create:company-sale', + select: { + userCompanies: { + select: { + id: true, + }, + }, + }, + }) + + const formData = await request.formData() + + const submission = await parseWithZod(formData, { + schema: CompanySalesEditorSchema.superRefine(async (data, ctx) => { + const checkName = await prisma.company.findFirst({ + where: { + accounts: { + some: { + name: data.name, + }, + }, + }, + }) + + if (checkName) { + ctx.addIssue({ + path: ['name'], + code: z.ZodIssueCode.custom, + message: 'Name already exists', + }) + } + }), + async: true, + }) + + if (submission.status !== 'success') { + return json( + { result: submission.reply() }, + { status: submission.status === 'error' ? 400 : 200 }, + ) + } + + const { + id: saleId, + name, + uniqueId, + email, + phone, + address, + city, + state, + country, + zip, + } = submission.value + + await prisma.account.upsert({ + where: { + id: saleId ?? '__new_sale__', + }, + create: { + name, + uniqueId, + email, + phone, + address, + city, + state, + country, + zip, + companyId: params.companyId!, + createdById: user.userCompanies[0].id, + }, + update: { + name, + uniqueId, + email, + phone, + address, + city, + state, + country, + zip, + }, + }) + + return redirect(`/c/${params.companyId}/sales`) +} diff --git a/app/routes/c.$companyId.sales+/__sale-editor.tsx b/app/routes/c.$companyId.sales+/__sale-editor.tsx index 32d2e88..1d5f438 100644 --- a/app/routes/c.$companyId.sales+/__sale-editor.tsx +++ b/app/routes/c.$companyId.sales+/__sale-editor.tsx @@ -1,162 +1,59 @@ import { getFormProps, getInputProps, useForm } from '@conform-to/react' import { getZodConstraint, parseWithZod } from '@conform-to/zod' -import { type Account, type SaleInvoice } from '@prisma/client' -import { - json, - type ActionFunctionArgs, - type SerializeFrom, -} from '@remix-run/node' -import { Form, redirect, useActionData } from '@remix-run/react' +import { type SaleInvoice } from '@prisma/client' +import { type SerializeFrom } from '@remix-run/node' +import { Form, useActionData } from '@remix-run/react' import { z } from 'zod' -import { Field, MinimalField } from '#app/components/forms' +import { Field } from '#app/components/forms' import { StatusButton } from '#app/components/ui/status-button' -import { prisma } from '#app/utils/db.server' import { useIsPending } from '#app/utils/misc' -import { requireCompanyUserWithRBAC } from '#app/utils/permissions.server' +import { type action } from './__sale-editor.server' // TODO: Make a better component to handle country, state, and city inputs // ? Maybe a select component with api data -const CompanySaleEditorSchema = z.object({ +export const CompanySalesEditorSchema = z.object({ id: z.string().optional(), - invoiceNumber: z.number().optional(), - dueDate: z.string().optional(), - onCredit: z.boolean().optional(), - dateIssued: z.string().optional(), - issuedToId: z.string(), - issuedById: z.string(), - totalAmount: z.number(), - transactionStatusKey: z.string(), - description: z.string().min(4).max(80).optional(), + name: z.string().min(3).max(40), + uniqueId: z.string().min(4).max(24).optional(), + email: z.string().email().optional(), + phone: z.string().min(7).max(15).optional(), + address: z.string().min(4).max(40).optional(), + country: z.string().min(4).max(24).optional(), + city: z.string().min(4).max(24).optional(), + state: z.string().min(4).max(24).optional(), + zip: z.string().min(4).max(12).optional(), + // description: z.string().min(4).max(80).optional(), }) -export async function action({ request, params }: ActionFunctionArgs) { - const user = await requireCompanyUserWithRBAC({ - request, - companyId: params.companyId!, - permission: 'create:company-sale', - select: { - userCompanies: { - select: { - id: true, - }, - }, - }, - }) - - const formData = await request.formData() - - const submission = await parseWithZod(formData, { - schema: CompanySaleEditorSchema.superRefine(async (data, ctx) => { - const checkName = await prisma.company.findFirst({ - where: { - accounts: { - some: { - name: data.name, - }, - }, - }, - }) - - if (checkName) { - ctx.addIssue({ - path: ['name'], - code: z.ZodIssueCode.custom, - message: 'Name already exists', - }) - } - }), - async: true, - }) - - if (submission.status !== 'success') { - return json( - { result: submission.reply() }, - { status: submission.status === 'error' ? 400 : 200 }, - ) - } - - const { - id: accountId, - name, - uniqueId, - email, - phone, - address, - city, - state, - country, - zip, - } = submission.value - - await prisma.account.upsert({ - where: { - id: accountId ?? '__new_account__', - }, - create: { - name, - uniqueId, - email, - phone, - address, - city, - state, - country, - zip, - companyId: params.companyId!, - createdById: user.userCompanies[0].id, - }, - update: { - name, - uniqueId, - email, - phone, - address, - city, - state, - country, - zip, - }, - }) - - return redirect(`/c/${params.companyId}/customers`) +const person = { + name: 'Lindsay Walton', + title: 'Front-end Developer', + email: 'lindsay.walton@example.com', + role: 'Member', } export function SaleEditor({ - account, sale, }: { - account?: SerializeFrom> sale?: SerializeFrom< Pick< SaleInvoice, - | 'id' - | 'invoiceNumber' - | 'dueDate' - | 'onCredit' - | 'dateIssued' - | 'issuedToId' - | 'issuedById' - | 'totalAmount' - | 'createdAt' - | 'updatedAt' - | 'transactionStatusKey' - | 'description' - > & { issuedTo: Pick } + 'id' | 'totalAmount' | 'dateIssued' | 'dueDate' | 'onCredit' + > > }) { const actionData = useActionData() const isPending = useIsPending() const [form, fields] = useForm({ - id: 'company-sale-editor-form', - constraint: getZodConstraint(CompanySaleEditorSchema), + id: 'company-sales-editor-form', + constraint: getZodConstraint(CompanySalesEditorSchema), lastResult: actionData?.result, onValidate({ formData }) { - return parseWithZod(formData, { schema: CompanySaleEditorSchema }) + return parseWithZod(formData, { schema: CompanySalesEditorSchema }) }, defaultValue: { - ...account, ...sale, }, shouldRevalidate: 'onBlur', @@ -167,89 +64,36 @@ export function SaleEditor({