mirror of
https://github.com/hanzoai/captable.git
synced 2026-10-16 14:21:29 +00:00
fix: merge conflicts
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "BankAccountTypeEnum" AS ENUM ('CHECKING', 'SAVINGS');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "BankAccount" (
|
||||
"id" TEXT NOT NULL,
|
||||
"beneficiaryName" TEXT NOT NULL,
|
||||
"beneficiaryAddress" TEXT NOT NULL,
|
||||
"bankName" TEXT NOT NULL,
|
||||
"bankAddress" TEXT NOT NULL,
|
||||
"accountNumber" TEXT NOT NULL,
|
||||
"routingNumber" TEXT NOT NULL,
|
||||
"accountType" "BankAccountTypeEnum" NOT NULL DEFAULT 'CHECKING',
|
||||
"swiftCode" TEXT,
|
||||
"primary" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"companyId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "BankAccount_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "BankAccount_companyId_idx" ON "BankAccount"("companyId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "BankAccount_companyId_accountNumber_key" ON "BankAccount"("companyId", "accountNumber");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "BankAccount_companyId_primary_key" ON "BankAccount"("companyId", "primary");
|
||||
@@ -0,0 +1,7 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Option" ADD COLUMN "cliffYears" INTEGER NOT NULL DEFAULT 0,
|
||||
ADD COLUMN "vestingYears" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Share" ADD COLUMN "cliffYears" INTEGER NOT NULL DEFAULT 0,
|
||||
ADD COLUMN "vestingYears" INTEGER NOT NULL DEFAULT 0;
|
||||
+38
-1
@@ -157,10 +157,42 @@ model Company {
|
||||
billingCustomers BillingCustomer[]
|
||||
customRoles CustomRole[]
|
||||
apiKeys ApiKey[]
|
||||
BankAccount BankAccount[]
|
||||
|
||||
@@unique([publicId])
|
||||
}
|
||||
|
||||
enum BankAccountTypeEnum {
|
||||
CHECKING
|
||||
SAVINGS
|
||||
}
|
||||
|
||||
model BankAccount {
|
||||
id String @id @default(cuid())
|
||||
beneficiaryName String
|
||||
beneficiaryAddress String
|
||||
bankName String
|
||||
bankAddress String
|
||||
accountNumber String // iban or account number
|
||||
routingNumber String // ABA or routing number
|
||||
accountType BankAccountTypeEnum @default(CHECKING)
|
||||
|
||||
// International bank information
|
||||
swiftCode String? // BIC or SWIFT code
|
||||
|
||||
primary Boolean @default(false)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
companyId String
|
||||
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([companyId, accountNumber])
|
||||
@@unique([companyId, primary], name: "unique_primary_account")
|
||||
@@index([companyId])
|
||||
}
|
||||
|
||||
enum MemberStatusEnum {
|
||||
ACTIVE
|
||||
INACTIVE
|
||||
@@ -668,8 +700,11 @@ model Share {
|
||||
debtCancelled Float? // Amount of debt cancelled
|
||||
otherContributions Float? // Other contributions
|
||||
|
||||
cliffYears Int @default(0) // 0 means immediate vesting, 1 means vesting starts after 1 year
|
||||
vestingYears Int @default(0) // 0 means immediate vesting, 1 means vesting over 1 year
|
||||
vestingSchedule VestingScheduleEnum
|
||||
companyLegends ShareLegendsEnum[]
|
||||
|
||||
companyLegends ShareLegendsEnum[]
|
||||
|
||||
issueDate DateTime
|
||||
rule144Date DateTime?
|
||||
@@ -718,6 +753,8 @@ model Option {
|
||||
|
||||
type OptionTypeEnum
|
||||
status OptionStatusEnum @default(DRAFT)
|
||||
cliffYears Int @default(0) // 0 means immediate vesting, 1 means vesting starts after 1 year
|
||||
vestingYears Int @default(0) // 0 means immediate vesting, 1 means vesting over 1 year
|
||||
vestingSchedule VestingScheduleEnum
|
||||
|
||||
issueDate DateTime
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import { pushModal } from "@/components/modals";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { RiAddLine } from "@remixicon/react";
|
||||
|
||||
const CtaButton = () => {
|
||||
return (
|
||||
<Button
|
||||
onClick={() => {
|
||||
pushModal("BankAccountModal", {
|
||||
title: "Add a bank account",
|
||||
subtitle: "Add a bank account to receive funds",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<RiAddLine className="inline-block h-5 w-5" />
|
||||
Add a bank account
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default CtaButton;
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import type { BankAccount } from "@prisma/client";
|
||||
|
||||
const BankAccountsTable = () => {
|
||||
return <>Table</>;
|
||||
};
|
||||
|
||||
export default BankAccountsTable;
|
||||
@@ -0,0 +1,60 @@
|
||||
import EmptyState from "@/components/common/empty-state";
|
||||
import { UnAuthorizedState } from "@/components/ui/un-authorized-state";
|
||||
import { serverAccessControl } from "@/lib/rbac/access-control";
|
||||
import { api } from "@/trpc/server";
|
||||
import { RiBankFill } from "@remixicon/react";
|
||||
import type { Metadata } from "next";
|
||||
import { Fragment } from "react";
|
||||
import CtaButton from "./components/cta-button";
|
||||
import BankAccountsTable from "./components/table";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Bank accounts",
|
||||
};
|
||||
const ApiSettingsPage = async () => {
|
||||
const { allow } = await serverAccessControl();
|
||||
|
||||
const data = await allow(api.bankAccounts.getAll.query(), [
|
||||
"bank-accounts",
|
||||
"read",
|
||||
]);
|
||||
|
||||
if (!data) {
|
||||
return <UnAuthorizedState />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{data.bankAccounts.length === 0 ? (
|
||||
<EmptyState
|
||||
bordered={false}
|
||||
icon={<RiBankFill />}
|
||||
title="Bank accounts"
|
||||
subtitle="Add a bank account to receive funds"
|
||||
>
|
||||
<CtaButton />
|
||||
</EmptyState>
|
||||
) : (
|
||||
<div className="flex flex-col gap-y-3">
|
||||
<div className="flex items-center justify-between gap-y-3 ">
|
||||
<div className="gap-y-3">
|
||||
<h3 className="font-medium">Bank accounts</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Add and manage bank account to receive funds
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<CtaButton />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* <BankAccountsTable accounts={data.bankAccounts} /> */}
|
||||
<BankAccountsTable />
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApiSettingsPage;
|
||||
@@ -13,7 +13,7 @@ const BillingPage = async () => {
|
||||
]);
|
||||
|
||||
return (
|
||||
<PageLayout title="Billing" description="manage your billing">
|
||||
<PageLayout title="Billing" description="Manage payments and billing">
|
||||
<PlanDetails products={products} subscription={subscription} />
|
||||
</PageLayout>
|
||||
);
|
||||
|
||||
@@ -26,7 +26,7 @@ const EmptyState = ({
|
||||
bordered = true,
|
||||
subtitle,
|
||||
children,
|
||||
error,
|
||||
error = false,
|
||||
}: EmptyStateProps) => {
|
||||
return (
|
||||
<div role="alert" className="overflow-hidden">
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import Modal from "@/components/common/push-modal";
|
||||
|
||||
type ShareClassType = {
|
||||
title: string | React.ReactNode;
|
||||
subtitle: string | React.ReactNode;
|
||||
};
|
||||
|
||||
export const BankAccountModal = ({ title, subtitle }: ShareClassType) => {
|
||||
return (
|
||||
<Modal size="xl" title={title} subtitle={subtitle}>
|
||||
Please clone this file and implement the modal.
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { createPushModal } from "pushmodal";
|
||||
import { BankAccountModal } from "./bank-account-modal";
|
||||
import { DocumentUploadModal } from "./document-upload-modal";
|
||||
import { EquityPlanModal } from "./equity-pan/equity-plan-modal";
|
||||
import { ExistingSafeModal } from "./existing-safe-modal";
|
||||
@@ -35,9 +36,12 @@ export const { pushModal, popModal, ModalProvider } = createPushModal({
|
||||
EquityPlanModal,
|
||||
IssueShareModal,
|
||||
IssueStockOptionModal,
|
||||
AddEsignDocumentModal,
|
||||
BankAccountModal,
|
||||
|
||||
// Safe modals
|
||||
NewSafeModal,
|
||||
ExistingSafeModal,
|
||||
AddEsignDocumentModal,
|
||||
InvestorModal,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
RiAccountCircleLine,
|
||||
RiBankCardFill,
|
||||
RiBankCardLine,
|
||||
RiBankFill,
|
||||
RiBankLine,
|
||||
RiBuildingFill,
|
||||
RiBuildingLine,
|
||||
RiGroup2Fill,
|
||||
@@ -41,6 +43,26 @@ const companyNav = [
|
||||
icon: RiShieldUserLine,
|
||||
activeIcon: RiShieldUserFill,
|
||||
},
|
||||
|
||||
{
|
||||
name: "Billing",
|
||||
href: "/settings/billing",
|
||||
icon: RiBankCardLine,
|
||||
activeIcon: RiBankCardFill,
|
||||
},
|
||||
|
||||
{
|
||||
name: "API Keys",
|
||||
href: "/settings/api",
|
||||
icon: RiTerminalBoxLine,
|
||||
activeIcon: RiTerminalBoxFill,
|
||||
},
|
||||
{
|
||||
name: "Bank Accounts",
|
||||
href: "/settings/bank-accounts",
|
||||
icon: RiBankLine,
|
||||
activeIcon: RiBankFill,
|
||||
},
|
||||
];
|
||||
|
||||
const accountNav = [
|
||||
@@ -57,12 +79,6 @@ const accountNav = [
|
||||
activeIcon: RiLock2Fill,
|
||||
},
|
||||
|
||||
{
|
||||
name: "API Keys",
|
||||
href: "/settings/api",
|
||||
icon: RiTerminalBoxLine,
|
||||
activeIcon: RiTerminalBoxFill,
|
||||
},
|
||||
{
|
||||
name: "Notifications",
|
||||
href: "/settings/notifications",
|
||||
@@ -80,25 +96,16 @@ export function SettingsSidebar({ isBillingEnabled }: SettingsSidebarProps) {
|
||||
const { data } = useSession();
|
||||
const companyPublicId = data?.user.companyPublicId;
|
||||
|
||||
const sideNavData = [
|
||||
...companyNav,
|
||||
...(isBillingEnabled
|
||||
? [
|
||||
{
|
||||
name: "Billing",
|
||||
href: "/settings/billing",
|
||||
icon: RiBankCardLine,
|
||||
activeIcon: RiBankCardFill,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
return (
|
||||
<ul className="text-sm">
|
||||
{sideNavData.map((item) => {
|
||||
{companyNav.map((item) => {
|
||||
const href = `/${companyPublicId}${item.href}`;
|
||||
const isActive = currentPath === href;
|
||||
|
||||
if (item.name === "Billing" && !isBillingEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<li key={item.name} className="rounded py-1">
|
||||
<NavLink
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { RiProhibitedLine } from "@remixicon/react";
|
||||
import { RiDoorLockFill } from "@remixicon/react";
|
||||
import EmptyState, { type EmptyStateProps } from "../common/empty-state";
|
||||
|
||||
export function UnAuthorizedState(props: Partial<EmptyStateProps>) {
|
||||
return (
|
||||
<EmptyState
|
||||
bordered={false}
|
||||
icon={<RiProhibitedLine />}
|
||||
title="Un Authorized"
|
||||
subtitle="you don't have access to view this content."
|
||||
error={true}
|
||||
icon={<RiDoorLockFill />}
|
||||
title="Unauthorized"
|
||||
subtitle="You are not authorized to access this content"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -7,5 +7,6 @@ export const SUBJECTS = [
|
||||
"documents",
|
||||
"company",
|
||||
"api-keys",
|
||||
"bank-accounts",
|
||||
] as const;
|
||||
export type TSubjects = (typeof SUBJECTS)[number];
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createTRPCRouter } from "@/trpc/api/trpc";
|
||||
import { apiKeyRouter } from "../routers/api-key/router";
|
||||
import { auditRouter } from "../routers/audit-router/router";
|
||||
import { authRouter } from "../routers/auth/router";
|
||||
import { bankAccountsRouter } from "../routers/bank-accounts/router";
|
||||
import { billingRouter } from "../routers/billing-router/router";
|
||||
import { bucketRouter } from "../routers/bucket-router/router";
|
||||
import { commonRouter } from "../routers/common/router";
|
||||
@@ -52,6 +53,7 @@ export const appRouter = createTRPCRouter({
|
||||
billing: billingRouter,
|
||||
rbac: rbacRouter,
|
||||
apiKey: apiKeyRouter,
|
||||
bankAccounts: bankAccountsRouter,
|
||||
});
|
||||
|
||||
// export type definition of API
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { generatePublicId } from "@/common/id";
|
||||
import { createApiToken, createSecureHash } from "@/lib/crypto";
|
||||
import { createTRPCRouter, withAccessControl } from "@/trpc/api/trpc";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import z from "zod";
|
||||
|
||||
export const bankAccountsRouter = createTRPCRouter({
|
||||
getAll: withAccessControl
|
||||
.meta({ policies: { "bank-accounts": { allow: ["read"] } } })
|
||||
.query(async ({ ctx }) => {
|
||||
const {
|
||||
db,
|
||||
membership: { companyId },
|
||||
} = ctx;
|
||||
|
||||
const bankAccounts = await db.bankAccount.findMany({
|
||||
where: {
|
||||
companyId,
|
||||
},
|
||||
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
|
||||
select: {
|
||||
id: true,
|
||||
bankName: true,
|
||||
accountNumber: true,
|
||||
primary: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
bankAccounts,
|
||||
};
|
||||
}),
|
||||
|
||||
create: withAccessControl
|
||||
.meta({ policies: { "bank-accounts": { allow: ["create"] } } })
|
||||
.mutation(async ({ ctx }) => {
|
||||
// const {
|
||||
// db,
|
||||
// membership: { companyId, memberId },
|
||||
// } = ctx;
|
||||
// TODO // Implement create mutation
|
||||
}),
|
||||
|
||||
delete: withAccessControl
|
||||
.input(z.object({ id: z.string() }))
|
||||
.meta({ policies: { "bank-accounts": { allow: ["delete"] } } })
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
// const {
|
||||
// db,
|
||||
// membership: { memberId, companyId },
|
||||
// } = ctx;
|
||||
// TODO // Implement delete mutation
|
||||
}),
|
||||
});
|
||||
Reference in New Issue
Block a user