Skip to content

Commit

Permalink
Merge pull request #331 from captableinc/feat/create-share-page
Browse files Browse the repository at this point in the history
feat: Allocate shares to stakeholders
  • Loading branch information
dahal authored May 31, 2024
2 parents b000aac + 21427cf commit 69cf85d
Show file tree
Hide file tree
Showing 31 changed files with 2,086 additions and 44 deletions.
6 changes: 4 additions & 2 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
".next",
"dist",
"public/pdf.worker.min.js",
"./prisma/enums.ts"
"./prisma/enums.ts",
"./src/components/ui/simple-multi-select.tsx"
]
},
"linter": {
Expand All @@ -25,7 +26,8 @@
".next",
"dist",
"public/pdf.worker.min.js",
"./prisma/enums.ts"
"./prisma/enums.ts",
"./src/components/ui/simple-multi-select.tsx"
]
},
"formatter": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
Warnings:
- A unique constraint covering the columns `[companyId,certificateId]` on the table `Share` will be added. If there are existing duplicate values, this will fail.
*/
-- CreateIndex
CREATE UNIQUE INDEX "Share_companyId_certificateId_key" ON "Share"("companyId", "certificateId");
1 change: 1 addition & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,7 @@ model Share {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([companyId, certificateId])
@@index([companyId])
@@index([shareClassId])
@@index([stakeholderId])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,7 @@ const OptionsPage = async () => {
>
<OptionModal
title="Create an option"
subtitle={
<Tldr
message="Please fill in the details to create an option. If you need help, click the link below."
cta={{
label: "Learn more",
href: "https://captable.inc/help/stakeholder-options",
}}
/>
}
subtitle="Please fill in the details to create an option."
trigger={
<Button size="lg">
<RiAddFill className="mr-2 h-5 w-5" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,69 @@
import EmptyState from "@/components/common/empty-state";
import Tldr from "@/components/common/tldr";
import { ShareModal } from "@/components/securities/shares/share-modal";
import ShareTable from "@/components/securities/shares/share-table";
import { Button } from "@/components/ui/button";
import { RiPieChartFill } from "@remixicon/react";
import { type Metadata } from "next";
import { Card } from "@/components/ui/card";
import { api } from "@/trpc/server";
import { RiAddFill, RiPieChartFill } from "@remixicon/react";
import type { Metadata } from "next";

export const metadata: Metadata = {
title: "Cap table",
title: "Captable | Shares",
};

const SharesPage = () => {
const SharesPage = async () => {
const shares = await api.securities.getShares.query();

if (shares?.data?.length === 0) {
return (
<EmptyState
icon={<RiPieChartFill />}
title="You have not issued any shares"
subtitle="Please click the button below to start issuing shares."
>
<ShareModal
size="4xl"
title="Create a share"
subtitle="Please fill in the details to create and issue a share."
trigger={
<Button size="lg">
<RiAddFill className="mr-2 h-5 w-5" />
Create a share
</Button>
}
/>
</EmptyState>
);
}

return (
<EmptyState
icon={<RiPieChartFill />}
title="Work in progress."
subtitle="This page is not yet available."
>
<Button size="lg">Coming soon...</Button>
</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">Shares</h3>
<p className="mt-1 text-sm text-muted-foreground">
Issue shares to stakeholders
</p>
</div>
<div>
<ShareModal
size="4xl"
title="Create a share"
subtitle="Please fill in the details to create and issue a share."
trigger={
<Button>
<RiAddFill className="mr-2 h-5 w-5" />
Create a share
</Button>
}
/>
</div>
</div>
<Card className="mx-auto mt-3 w-[28rem] sm:w-[38rem] md:w-full">
<ShareTable shares={shares.data} />
</Card>
</div>
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CompanyForm } from "@/components/onboarding/company-form";
import { api } from "@/trpc/server";
import { type Metadata } from "next";
import type { Metadata } from "next";

export const metadata: Metadata = {
title: "Company",
Expand Down
31 changes: 31 additions & 0 deletions src/components/common/LoadingSpinner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { cn } from "@/lib/utils";

export interface ISVGProps extends React.SVGProps<SVGSVGElement> {
size?: number;
className?: string;
}

export const LoadingSpinner = ({
size = 24,
className,
...props
}: ISVGProps) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
{...props}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={cn("animate-spin", className)}
>
<title>Loading</title>
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
</svg>
);
};
1 change: 1 addition & 0 deletions src/components/onboarding/company-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export const CompanyForm = ({ type, data }: CompanyFormProps) => {
state: data?.company.state ?? "",
streetAddress: data?.company.streetAddress ?? "",
zipcode: data?.company.zipcode ?? "",
country: data?.company.country ?? "",
},
},
});
Expand Down
16 changes: 1 addition & 15 deletions src/components/securities/options/steps/vesting-details.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"use client";

import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import {
Form,
Expand Down Expand Up @@ -31,6 +30,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { NumericFormat } from "react-number-format";
import { z } from "zod";
import { EmptySelect } from "../../shared/EmptySelect";

const formSchema = z.object({
equityPlanId: z.string(),
Expand All @@ -46,20 +46,6 @@ interface VestingDetailsProps {
equityPlans: RouterOutputs["equityPlan"]["getPlans"];
}

interface EmptySelectProps {
title: string;
description: string;
}

function EmptySelect({ title, description }: EmptySelectProps) {
return (
<Alert variant="destructive">
<AlertTitle>{title}</AlertTitle>
<AlertDescription>{description}</AlertDescription>
</Alert>
);
}

export const VestingDetails = (props: VestingDetailsProps) => {
const { stakeholders, equityPlans } = props;

Expand Down
15 changes: 15 additions & 0 deletions src/components/securities/shared/EmptySelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";

interface EmptySelectProps {
title: string;
description: string;
}

export function EmptySelect({ title, description }: EmptySelectProps) {
return (
<Alert variant="destructive">
<AlertTitle>{title}</AlertTitle>
<AlertDescription>{description}</AlertDescription>
</Alert>
);
}
7 changes: 7 additions & 0 deletions src/components/securities/shares/data.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { SecuritiesStatusEnum } from "@prisma/client";
import { capitalize } from "lodash-es";

export const statusValues = Object.keys(SecuritiesStatusEnum).map((item) => ({
label: capitalize(item),
value: item,
}));
51 changes: 51 additions & 0 deletions src/components/securities/shares/share-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import {
StepperModal,
StepperModalContent,
type StepperModalProps,
StepperStep,
} from "@/components/ui/stepper";
import { AddShareFormProvider } from "@/providers/add-share-form-provider";
import { api } from "@/trpc/server";
import { ContributionDetails } from "./steps/contribution-details";
import { Documents } from "./steps/documents";
import { GeneralDetails } from "./steps/general-details";
import { RelevantDates } from "./steps/relevant-dates";

async function ContributionDetailsStep() {
const stakeholders = await api.stakeholder.getStakeholders.query();
return <ContributionDetails stakeholders={stakeholders} />;
}

async function GeneralDetailsStep() {
const shareClasses = await api.shareClass.get.query();
return <GeneralDetails shareClasses={shareClasses} />;
}

export const ShareModal = (props: Omit<StepperModalProps, "children">) => {
return (
<StepperModal {...props}>
<AddShareFormProvider>
<StepperStep title="General details">
<StepperModalContent>
<GeneralDetailsStep />
</StepperModalContent>
</StepperStep>
<StepperStep title="Contribution details">
<StepperModalContent>
<ContributionDetailsStep />
</StepperModalContent>
</StepperStep>
<StepperStep title="Relevant dates">
<StepperModalContent>
<RelevantDates />
</StepperModalContent>
</StepperStep>
<StepperStep title="Documents">
<StepperModalContent>
<Documents />
</StepperModalContent>
</StepperStep>
</AddShareFormProvider>
</StepperModal>
);
};
48 changes: 48 additions & 0 deletions src/components/securities/shares/share-table-toolbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useDataTable } from "@/components/ui/data-table/data-table";
import { ResetButton } from "@/components/ui/data-table/data-table-buttons";
import { DataTableFacetedFilter } from "@/components/ui/data-table/data-table-faceted-filter";
import { DataTableViewOptions } from "@/components/ui/data-table/data-table-view-options";
import { Input } from "@/components/ui/input";
import { statusValues } from "./data";

export function ShareTableToolbar() {
const { table } = useDataTable();
const isFiltered = table.getState().columnFilters.length > 0;

return (
<div className="flex w-full items-center justify-between">
<div className="flex flex-col gap-2 sm:flex-1 sm:flex-row sm:items-center sm:gap-0 sm:space-x-2">
<Input
placeholder="Search by stakeholder name..."
value={
(table.getColumn("stakeholderName")?.getFilterValue() as string) ??
""
}
onChange={(event) =>
table
.getColumn("stakeholderName")
?.setFilterValue(event.target.value)
}
className="h-8 w-64"
/>
<div className="space-x-2">
{table.getColumn("status") && (
<DataTableFacetedFilter
column={table.getColumn("status")}
title="Status"
options={statusValues}
/>
)}

{isFiltered && (
<ResetButton
className="p-1"
onClick={() => table.resetColumnFilters()}
/>
)}
</div>
</div>
<DataTableViewOptions />
</div>
);
}
Loading

0 comments on commit 69cf85d

Please sign in to comment.