Skip to content

Commit 69cf85d

Browse files
authoredMay 31, 2024··
Merge pull request #331 from captableinc/feat/create-share-page
feat: Allocate shares to stakeholders
2 parents b000aac + 21427cf commit 69cf85d

File tree

31 files changed

+2086
-44
lines changed

31 files changed

+2086
-44
lines changed
 

‎biome.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
".next",
88
"dist",
99
"public/pdf.worker.min.js",
10-
"./prisma/enums.ts"
10+
"./prisma/enums.ts",
11+
"./src/components/ui/simple-multi-select.tsx"
1112
]
1213
},
1314
"linter": {
@@ -25,7 +26,8 @@
2526
".next",
2627
"dist",
2728
"public/pdf.worker.min.js",
28-
"./prisma/enums.ts"
29+
"./prisma/enums.ts",
30+
"./src/components/ui/simple-multi-select.tsx"
2931
]
3032
},
3133
"formatter": {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/*
2+
Warnings:
3+
4+
- A unique constraint covering the columns `[companyId,certificateId]` on the table `Share` will be added. If there are existing duplicate values, this will fail.
5+
6+
*/
7+
-- CreateIndex
8+
CREATE UNIQUE INDEX "Share_companyId_certificateId_key" ON "Share"("companyId", "certificateId");

‎prisma/schema.prisma

+1
Original file line numberDiff line numberDiff line change
@@ -659,6 +659,7 @@ model Share {
659659
createdAt DateTime @default(now())
660660
updatedAt DateTime @updatedAt
661661
662+
@@unique([companyId, certificateId])
662663
@@index([companyId])
663664
@@index([shareClassId])
664665
@@index([stakeholderId])

‎src/app/(authenticated)/(dashboard)/[publicId]/securities/options/page.tsx

+1-9
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,7 @@ const OptionsPage = async () => {
2424
>
2525
<OptionModal
2626
title="Create an option"
27-
subtitle={
28-
<Tldr
29-
message="Please fill in the details to create an option. If you need help, click the link below."
30-
cta={{
31-
label: "Learn more",
32-
href: "https://captable.inc/help/stakeholder-options",
33-
}}
34-
/>
35-
}
27+
subtitle="Please fill in the details to create an option."
3628
trigger={
3729
<Button size="lg">
3830
<RiAddFill className="mr-2 h-5 w-5" />

‎src/app/(authenticated)/(dashboard)/[publicId]/securities/shares/page.tsx

+59-11
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,69 @@
11
import EmptyState from "@/components/common/empty-state";
2+
import Tldr from "@/components/common/tldr";
3+
import { ShareModal } from "@/components/securities/shares/share-modal";
4+
import ShareTable from "@/components/securities/shares/share-table";
25
import { Button } from "@/components/ui/button";
3-
import { RiPieChartFill } from "@remixicon/react";
4-
import { type Metadata } from "next";
6+
import { Card } from "@/components/ui/card";
7+
import { api } from "@/trpc/server";
8+
import { RiAddFill, RiPieChartFill } from "@remixicon/react";
9+
import type { Metadata } from "next";
510

611
export const metadata: Metadata = {
7-
title: "Cap table",
12+
title: "Captable | Shares",
813
};
914

10-
const SharesPage = () => {
15+
const SharesPage = async () => {
16+
const shares = await api.securities.getShares.query();
17+
18+
if (shares?.data?.length === 0) {
19+
return (
20+
<EmptyState
21+
icon={<RiPieChartFill />}
22+
title="You have not issued any shares"
23+
subtitle="Please click the button below to start issuing shares."
24+
>
25+
<ShareModal
26+
size="4xl"
27+
title="Create a share"
28+
subtitle="Please fill in the details to create and issue a share."
29+
trigger={
30+
<Button size="lg">
31+
<RiAddFill className="mr-2 h-5 w-5" />
32+
Create a share
33+
</Button>
34+
}
35+
/>
36+
</EmptyState>
37+
);
38+
}
39+
1140
return (
12-
<EmptyState
13-
icon={<RiPieChartFill />}
14-
title="Work in progress."
15-
subtitle="This page is not yet available."
16-
>
17-
<Button size="lg">Coming soon...</Button>
18-
</EmptyState>
41+
<div className="flex flex-col gap-y-3">
42+
<div className="flex items-center justify-between gap-y-3 ">
43+
<div className="gap-y-3">
44+
<h3 className="font-medium">Shares</h3>
45+
<p className="mt-1 text-sm text-muted-foreground">
46+
Issue shares to stakeholders
47+
</p>
48+
</div>
49+
<div>
50+
<ShareModal
51+
size="4xl"
52+
title="Create a share"
53+
subtitle="Please fill in the details to create and issue a share."
54+
trigger={
55+
<Button>
56+
<RiAddFill className="mr-2 h-5 w-5" />
57+
Create a share
58+
</Button>
59+
}
60+
/>
61+
</div>
62+
</div>
63+
<Card className="mx-auto mt-3 w-[28rem] sm:w-[38rem] md:w-full">
64+
<ShareTable shares={shares.data} />
65+
</Card>
66+
</div>
1967
);
2068
};
2169

‎src/app/(authenticated)/(dashboard)/[publicId]/settings/company/page.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { CompanyForm } from "@/components/onboarding/company-form";
22
import { api } from "@/trpc/server";
3-
import { type Metadata } from "next";
3+
import type { Metadata } from "next";
44

55
export const metadata: Metadata = {
66
title: "Company",
+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { cn } from "@/lib/utils";
2+
3+
export interface ISVGProps extends React.SVGProps<SVGSVGElement> {
4+
size?: number;
5+
className?: string;
6+
}
7+
8+
export const LoadingSpinner = ({
9+
size = 24,
10+
className,
11+
...props
12+
}: ISVGProps) => {
13+
return (
14+
<svg
15+
xmlns="http://www.w3.org/2000/svg"
16+
width={size}
17+
height={size}
18+
{...props}
19+
viewBox="0 0 24 24"
20+
fill="none"
21+
stroke="currentColor"
22+
strokeWidth="2"
23+
strokeLinecap="round"
24+
strokeLinejoin="round"
25+
className={cn("animate-spin", className)}
26+
>
27+
<title>Loading</title>
28+
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
29+
</svg>
30+
);
31+
};

‎src/components/onboarding/company-form.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ export const CompanyForm = ({ type, data }: CompanyFormProps) => {
8282
state: data?.company.state ?? "",
8383
streetAddress: data?.company.streetAddress ?? "",
8484
zipcode: data?.company.zipcode ?? "",
85+
country: data?.company.country ?? "",
8586
},
8687
},
8788
});

‎src/components/securities/options/steps/vesting-details.tsx

+1-15
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"use client";
22

3-
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
43
import { Button } from "@/components/ui/button";
54
import {
65
Form,
@@ -31,6 +30,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
3130
import { useForm } from "react-hook-form";
3231
import { NumericFormat } from "react-number-format";
3332
import { z } from "zod";
33+
import { EmptySelect } from "../../shared/EmptySelect";
3434

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

49-
interface EmptySelectProps {
50-
title: string;
51-
description: string;
52-
}
53-
54-
function EmptySelect({ title, description }: EmptySelectProps) {
55-
return (
56-
<Alert variant="destructive">
57-
<AlertTitle>{title}</AlertTitle>
58-
<AlertDescription>{description}</AlertDescription>
59-
</Alert>
60-
);
61-
}
62-
6349
export const VestingDetails = (props: VestingDetailsProps) => {
6450
const { stakeholders, equityPlans } = props;
6551

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
2+
3+
interface EmptySelectProps {
4+
title: string;
5+
description: string;
6+
}
7+
8+
export function EmptySelect({ title, description }: EmptySelectProps) {
9+
return (
10+
<Alert variant="destructive">
11+
<AlertTitle>{title}</AlertTitle>
12+
<AlertDescription>{description}</AlertDescription>
13+
</Alert>
14+
);
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { SecuritiesStatusEnum } from "@prisma/client";
2+
import { capitalize } from "lodash-es";
3+
4+
export const statusValues = Object.keys(SecuritiesStatusEnum).map((item) => ({
5+
label: capitalize(item),
6+
value: item,
7+
}));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import {
2+
StepperModal,
3+
StepperModalContent,
4+
type StepperModalProps,
5+
StepperStep,
6+
} from "@/components/ui/stepper";
7+
import { AddShareFormProvider } from "@/providers/add-share-form-provider";
8+
import { api } from "@/trpc/server";
9+
import { ContributionDetails } from "./steps/contribution-details";
10+
import { Documents } from "./steps/documents";
11+
import { GeneralDetails } from "./steps/general-details";
12+
import { RelevantDates } from "./steps/relevant-dates";
13+
14+
async function ContributionDetailsStep() {
15+
const stakeholders = await api.stakeholder.getStakeholders.query();
16+
return <ContributionDetails stakeholders={stakeholders} />;
17+
}
18+
19+
async function GeneralDetailsStep() {
20+
const shareClasses = await api.shareClass.get.query();
21+
return <GeneralDetails shareClasses={shareClasses} />;
22+
}
23+
24+
export const ShareModal = (props: Omit<StepperModalProps, "children">) => {
25+
return (
26+
<StepperModal {...props}>
27+
<AddShareFormProvider>
28+
<StepperStep title="General details">
29+
<StepperModalContent>
30+
<GeneralDetailsStep />
31+
</StepperModalContent>
32+
</StepperStep>
33+
<StepperStep title="Contribution details">
34+
<StepperModalContent>
35+
<ContributionDetailsStep />
36+
</StepperModalContent>
37+
</StepperStep>
38+
<StepperStep title="Relevant dates">
39+
<StepperModalContent>
40+
<RelevantDates />
41+
</StepperModalContent>
42+
</StepperStep>
43+
<StepperStep title="Documents">
44+
<StepperModalContent>
45+
<Documents />
46+
</StepperModalContent>
47+
</StepperStep>
48+
</AddShareFormProvider>
49+
</StepperModal>
50+
);
51+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { useDataTable } from "@/components/ui/data-table/data-table";
2+
import { ResetButton } from "@/components/ui/data-table/data-table-buttons";
3+
import { DataTableFacetedFilter } from "@/components/ui/data-table/data-table-faceted-filter";
4+
import { DataTableViewOptions } from "@/components/ui/data-table/data-table-view-options";
5+
import { Input } from "@/components/ui/input";
6+
import { statusValues } from "./data";
7+
8+
export function ShareTableToolbar() {
9+
const { table } = useDataTable();
10+
const isFiltered = table.getState().columnFilters.length > 0;
11+
12+
return (
13+
<div className="flex w-full items-center justify-between">
14+
<div className="flex flex-col gap-2 sm:flex-1 sm:flex-row sm:items-center sm:gap-0 sm:space-x-2">
15+
<Input
16+
placeholder="Search by stakeholder name..."
17+
value={
18+
(table.getColumn("stakeholderName")?.getFilterValue() as string) ??
19+
""
20+
}
21+
onChange={(event) =>
22+
table
23+
.getColumn("stakeholderName")
24+
?.setFilterValue(event.target.value)
25+
}
26+
className="h-8 w-64"
27+
/>
28+
<div className="space-x-2">
29+
{table.getColumn("status") && (
30+
<DataTableFacetedFilter
31+
column={table.getColumn("status")}
32+
title="Status"
33+
options={statusValues}
34+
/>
35+
)}
36+
37+
{isFiltered && (
38+
<ResetButton
39+
className="p-1"
40+
onClick={() => table.resetColumnFilters()}
41+
/>
42+
)}
43+
</div>
44+
</div>
45+
<DataTableViewOptions />
46+
</div>
47+
);
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,394 @@
1+
"use client";
2+
3+
import {
4+
type ColumnDef,
5+
type ColumnFiltersState,
6+
type SortingState,
7+
type VisibilityState,
8+
getCoreRowModel,
9+
getFacetedRowModel,
10+
getFacetedUniqueValues,
11+
getFilteredRowModel,
12+
getPaginationRowModel,
13+
getSortedRowModel,
14+
useReactTable,
15+
} from "@tanstack/react-table";
16+
import * as React from "react";
17+
18+
import { dayjsExt } from "@/common/dayjs";
19+
import { Checkbox } from "@/components/ui/checkbox";
20+
21+
import { Avatar, AvatarImage } from "@/components/ui/avatar";
22+
import { DataTable } from "@/components/ui/data-table/data-table";
23+
import { DataTableBody } from "@/components/ui/data-table/data-table-body";
24+
import { DataTableContent } from "@/components/ui/data-table/data-table-content";
25+
import { DataTableHeader } from "@/components/ui/data-table/data-table-header";
26+
import { DataTablePagination } from "@/components/ui/data-table/data-table-pagination";
27+
import type { RouterOutputs } from "@/trpc/shared";
28+
29+
import { Button } from "@/components/ui/button";
30+
import { SortButton } from "@/components/ui/data-table/data-table-buttons";
31+
import {
32+
DropdownMenuContent,
33+
DropdownMenuItem,
34+
DropdownMenuLabel,
35+
DropdownMenuSeparator,
36+
} from "@/components/ui/dropdown-menu";
37+
import { formatCurrency, formatNumber } from "@/lib/utils";
38+
import { getPresignedGetUrl } from "@/server/file-uploads";
39+
import { api } from "@/trpc/react";
40+
import {
41+
DropdownMenu,
42+
DropdownMenuTrigger,
43+
} from "@radix-ui/react-dropdown-menu";
44+
import { RiFileDownloadLine, RiMoreLine } from "@remixicon/react";
45+
import { useRouter } from "next/navigation";
46+
import { toast } from "sonner";
47+
import { ShareTableToolbar } from "./share-table-toolbar";
48+
49+
type Share = RouterOutputs["securities"]["getShares"]["data"];
50+
51+
type SharesType = {
52+
shares: Share;
53+
};
54+
55+
const humanizeShareStatus = (type: string) => {
56+
switch (type) {
57+
case "ACTIVE":
58+
return "Active";
59+
case "DRAFT":
60+
return "Draft";
61+
case "SIGNED":
62+
return "Signed";
63+
case "PENDING":
64+
return "Pending";
65+
default:
66+
return "";
67+
}
68+
};
69+
70+
const StatusColorProvider = (type: string) => {
71+
switch (type) {
72+
case "ACTIVE":
73+
return "bg-green-50 text-green-600 ring-green-600/20";
74+
case "DRAFT":
75+
return "bg-yellow-50 text-yellow-600 ring-yellow-600/20";
76+
case "SIGNED":
77+
return "bg-blue-50 text-blue-600 ring-blue-600/20";
78+
case "PENDING":
79+
return "bg-gray-50 text-gray-600 ring-gray-600/20";
80+
default:
81+
return "";
82+
}
83+
};
84+
85+
export const columns: ColumnDef<Share[number]>[] = [
86+
{
87+
id: "select",
88+
header: ({ table }) => (
89+
<Checkbox
90+
checked={
91+
table.getIsAllPageRowsSelected() ||
92+
(table.getIsSomePageRowsSelected() && "indeterminate")
93+
}
94+
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
95+
aria-label="Select all"
96+
/>
97+
),
98+
cell: ({ row }) => (
99+
<Checkbox
100+
checked={row.getIsSelected()}
101+
onCheckedChange={(value) => row.toggleSelected(!!value)}
102+
aria-label="Select row"
103+
/>
104+
),
105+
enableSorting: false,
106+
enableHiding: false,
107+
},
108+
{
109+
id: "stakeholderName",
110+
accessorKey: "stakeholder.name",
111+
header: ({ column }) => {
112+
return (
113+
<SortButton
114+
label="Stakeholder"
115+
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
116+
/>
117+
);
118+
},
119+
cell: ({ row }) => (
120+
<div className="flex">
121+
<Avatar className="rounded-full">
122+
<AvatarImage src={"/placeholders/user.svg"} />
123+
</Avatar>
124+
<div className="ml-2 pt-2">
125+
<p>{row?.original?.stakeholder?.name}</p>
126+
</div>
127+
</div>
128+
),
129+
},
130+
{
131+
id: "status",
132+
accessorKey: "status",
133+
header: ({ column }) => (
134+
<SortButton
135+
label="Status"
136+
onClick={() => column.toggleSorting(column?.getIsSorted() === "asc")}
137+
/>
138+
),
139+
cell: ({ row }) => {
140+
const status = row.original?.status;
141+
return (
142+
<span
143+
className={`inline-flex items-center rounded-md ${StatusColorProvider(
144+
status,
145+
)} px-2 py-1 text-xs text-center font-medium ring-1 ring-inset `}
146+
>
147+
{humanizeShareStatus(status)}
148+
</span>
149+
);
150+
},
151+
},
152+
{
153+
id: "shareClass",
154+
accessorKey: "shareClass.classType",
155+
156+
header: ({ column }) => (
157+
<SortButton
158+
label="Share class"
159+
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
160+
/>
161+
),
162+
cell: ({ row }) => (
163+
<div className="text-center">{row.original.shareClass.classType}</div>
164+
),
165+
},
166+
{
167+
id: "quantity",
168+
accessorKey: "quantity",
169+
header: ({ column }) => (
170+
<div className="flex justify-end">
171+
<SortButton
172+
label="Quantity"
173+
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
174+
/>
175+
</div>
176+
),
177+
cell: ({ row }) => {
178+
const quantity = row.original.quantity;
179+
return (
180+
<div className="text-center">
181+
{quantity ? formatNumber(quantity) : null}
182+
</div>
183+
);
184+
},
185+
},
186+
{
187+
id: "pricePerShare",
188+
accessorKey: "pricePerShare",
189+
header: ({ column }) => (
190+
<div className="flex justify-end">
191+
<SortButton
192+
label="Unit price"
193+
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
194+
/>
195+
</div>
196+
),
197+
cell: ({ row }) => {
198+
const price = row.original.pricePerShare;
199+
return (
200+
<div className="text-center">
201+
{price ? formatCurrency(price, "USD") : null}
202+
</div>
203+
);
204+
},
205+
},
206+
{
207+
id: "issueDate",
208+
accessorKey: "issueDate",
209+
header: ({ column }) => (
210+
<SortButton
211+
label="Issued"
212+
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
213+
/>
214+
),
215+
cell: ({ row }) => (
216+
<div className="text-center">
217+
{dayjsExt(row.original.issueDate).format("DD/MM/YYYY")}
218+
</div>
219+
),
220+
},
221+
{
222+
id: "boardApprovalDate",
223+
accessorKey: "boardApprovalDate",
224+
header: ({ column }) => (
225+
<div className="flex justify-end">
226+
<SortButton
227+
label="Board Approved"
228+
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
229+
/>
230+
</div>
231+
),
232+
cell: ({ row }) => (
233+
<div className="text-center">
234+
{dayjsExt(row.original.boardApprovalDate).format("DD/MM/YYYY")}
235+
</div>
236+
),
237+
},
238+
{
239+
id: "Documents",
240+
enableHiding: false,
241+
header: ({ column }) => {
242+
return (
243+
<SortButton
244+
label="Documents"
245+
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
246+
/>
247+
);
248+
},
249+
cell: ({ row }) => {
250+
const documents = row?.original?.documents;
251+
252+
const openFileOnTab = async (key: string) => {
253+
const fileUrl = await getPresignedGetUrl(key);
254+
window.open(fileUrl.url, "_blank");
255+
};
256+
257+
return (
258+
<DropdownMenu>
259+
<div className="items-end justify-end text-center">
260+
<DropdownMenuTrigger className="outline-none" asChild>
261+
<Button variant={"outline"} className="h-7 w-12 px-2">
262+
View
263+
</Button>
264+
</DropdownMenuTrigger>
265+
</div>
266+
<DropdownMenuContent align="end">
267+
<DropdownMenuLabel>Documents</DropdownMenuLabel>
268+
{documents?.map((doc) => (
269+
<DropdownMenuItem
270+
key={doc.id}
271+
className="hover:cursor-pointer"
272+
onClick={async () => {
273+
await openFileOnTab(doc.bucket.key);
274+
}}
275+
>
276+
<RiFileDownloadLine
277+
type={doc.bucket.mimeType}
278+
className="mx-3 cursor-pointer text-muted-foreground hover:text-primary/80"
279+
/>
280+
{doc.name.slice(0, 12)}
281+
<p className="mx-4 rounded-full bg-slate-100 text-xs text-slate-500">
282+
{doc?.uploader?.user?.name}
283+
</p>
284+
</DropdownMenuItem>
285+
))}
286+
</DropdownMenuContent>
287+
</DropdownMenu>
288+
);
289+
},
290+
},
291+
{
292+
id: "actions",
293+
enableHiding: false,
294+
cell: ({ row }) => {
295+
// eslint-disable-next-line react-hooks/rules-of-hooks
296+
const router = useRouter();
297+
// eslint-disable-next-line react-hooks/rules-of-hooks
298+
const share = row.original;
299+
300+
const deleteShareMutation = api.securities.deleteShare.useMutation({
301+
onSuccess: () => {
302+
toast.success("🎉 Successfully deleted the stakeholder");
303+
router.refresh();
304+
},
305+
onError: () => {
306+
toast.error("Failed deleting the share");
307+
},
308+
});
309+
310+
const updateAction = "Update Share";
311+
const deleteAction = "Delete Share";
312+
313+
const handleDeleteShare = async () => {
314+
await deleteShareMutation.mutateAsync({ shareId: share.id });
315+
};
316+
317+
return (
318+
<DropdownMenu>
319+
<div className="items-end justify-end text-right">
320+
<DropdownMenuTrigger
321+
className="border-0 border-white outline-none focus:outline-none"
322+
asChild
323+
>
324+
<Button variant="ghost" className="h-8 w-8 p-0">
325+
<>
326+
<span className="sr-only">Open menu</span>
327+
<RiMoreLine aria-hidden className="h-4 w-4" />
328+
</>
329+
</Button>
330+
</DropdownMenuTrigger>
331+
</div>
332+
<DropdownMenuContent align="end">
333+
<DropdownMenuLabel>Actions</DropdownMenuLabel>
334+
<DropdownMenuSeparator />
335+
<DropdownMenuItem>{updateAction}</DropdownMenuItem>
336+
<DropdownMenuItem
337+
onSelect={handleDeleteShare}
338+
className="text-red-500"
339+
>
340+
{deleteAction}
341+
</DropdownMenuItem>
342+
</DropdownMenuContent>
343+
</DropdownMenu>
344+
);
345+
},
346+
},
347+
];
348+
349+
const ShareTable = ({ shares }: SharesType) => {
350+
const [sorting, setSorting] = React.useState<SortingState>([]);
351+
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
352+
[],
353+
);
354+
const [columnVisibility, setColumnVisibility] =
355+
React.useState<VisibilityState>({});
356+
const [rowSelection, setRowSelection] = React.useState({});
357+
358+
const table = useReactTable({
359+
data: shares ?? [],
360+
columns: columns,
361+
enableRowSelection: true,
362+
onRowSelectionChange: setRowSelection,
363+
onSortingChange: setSorting,
364+
onColumnFiltersChange: setColumnFilters,
365+
onColumnVisibilityChange: setColumnVisibility,
366+
getCoreRowModel: getCoreRowModel(),
367+
getFilteredRowModel: getFilteredRowModel(),
368+
getPaginationRowModel: getPaginationRowModel(),
369+
getSortedRowModel: getSortedRowModel(),
370+
getFacetedRowModel: getFacetedRowModel(),
371+
getFacetedUniqueValues: getFacetedUniqueValues(),
372+
state: {
373+
sorting,
374+
columnFilters,
375+
columnVisibility,
376+
rowSelection,
377+
},
378+
});
379+
380+
return (
381+
<div className="w-full p-6">
382+
<DataTable table={table}>
383+
<ShareTableToolbar />
384+
<DataTableContent>
385+
<DataTableHeader />
386+
<DataTableBody />
387+
</DataTableContent>
388+
<DataTablePagination />
389+
</DataTable>
390+
</div>
391+
);
392+
};
393+
394+
export default ShareTable;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
"use client";
2+
3+
import { Button } from "@/components/ui/button";
4+
import {
5+
Form,
6+
FormControl,
7+
FormField,
8+
FormItem,
9+
FormLabel,
10+
FormMessage,
11+
} from "@/components/ui/form";
12+
import { Input } from "@/components/ui/input";
13+
import {
14+
Select,
15+
SelectContent,
16+
SelectItem,
17+
SelectTrigger,
18+
SelectValue,
19+
} from "@/components/ui/select";
20+
import {
21+
StepperModalFooter,
22+
StepperPrev,
23+
useStepper,
24+
} from "@/components/ui/stepper";
25+
import { useAddShareFormValues } from "@/providers/add-share-form-provider";
26+
import type { RouterOutputs } from "@/trpc/shared";
27+
import { zodResolver } from "@hookform/resolvers/zod";
28+
import { useForm } from "react-hook-form";
29+
import { NumericFormat } from "react-number-format";
30+
import { z } from "zod";
31+
import { EmptySelect } from "../../shared/EmptySelect";
32+
33+
interface ContributionDetailsProps {
34+
stakeholders: RouterOutputs["stakeholder"]["getStakeholders"];
35+
}
36+
37+
const formSchema = z.object({
38+
stakeholderId: z.string(),
39+
capitalContribution: z.coerce.number().min(0),
40+
ipContribution: z.coerce.number().min(0),
41+
debtCancelled: z.coerce.number().min(0),
42+
otherContributions: z.coerce.number().min(0),
43+
});
44+
45+
type TFormSchema = z.infer<typeof formSchema>;
46+
47+
export const ContributionDetails = ({
48+
stakeholders,
49+
}: ContributionDetailsProps) => {
50+
const form = useForm<TFormSchema>({ resolver: zodResolver(formSchema) });
51+
const { next } = useStepper();
52+
const { setValue } = useAddShareFormValues();
53+
54+
const handleSubmit = (data: TFormSchema) => {
55+
console.log({ data });
56+
setValue(data);
57+
next();
58+
};
59+
return (
60+
<Form {...form}>
61+
<form
62+
className="flex flex-col gap-y-4"
63+
onSubmit={form.handleSubmit(handleSubmit)}
64+
>
65+
<div className="space-y-4">
66+
<FormField
67+
control={form.control}
68+
name="stakeholderId"
69+
render={({ field }) => (
70+
<FormItem>
71+
<FormLabel>Stakeholder</FormLabel>
72+
{/* eslint-disable-next-line @typescript-eslint/no-unsafe-assignment */}
73+
<Select onValueChange={field.onChange} value={field.value}>
74+
<FormControl>
75+
<SelectTrigger className="w-full">
76+
<SelectValue placeholder="Select stakeholder" />
77+
</SelectTrigger>
78+
</FormControl>
79+
<SelectContent>
80+
{stakeholders.length ? (
81+
stakeholders.map((sh) => (
82+
<SelectItem key={sh.id} value={sh.id}>
83+
{sh.company.name} - {sh.name}
84+
</SelectItem>
85+
))
86+
) : (
87+
<EmptySelect
88+
title="Stakeholders not found"
89+
description="Please add a stakeholder first."
90+
/>
91+
)}
92+
</SelectContent>
93+
</Select>
94+
<FormMessage className="text-xs font-light" />
95+
</FormItem>
96+
)}
97+
/>
98+
<div className="flex items-center gap-4">
99+
<div className="flex-1">
100+
<FormField
101+
control={form.control}
102+
name="capitalContribution"
103+
render={({ field }) => {
104+
const { onChange, ...rest } = field;
105+
return (
106+
<FormItem>
107+
<FormLabel>Contributed capital amount</FormLabel>
108+
<FormControl>
109+
<NumericFormat
110+
thousandSeparator
111+
allowedDecimalSeparators={["%"]}
112+
decimalScale={2}
113+
prefix={"$ "}
114+
{...rest}
115+
customInput={Input}
116+
onValueChange={(values) => {
117+
const { floatValue } = values;
118+
onChange(floatValue);
119+
}}
120+
/>
121+
</FormControl>
122+
<FormMessage className="text-xs font-light" />
123+
</FormItem>
124+
);
125+
}}
126+
/>
127+
</div>
128+
<div className="flex-1">
129+
<FormField
130+
control={form.control}
131+
name="ipContribution"
132+
render={({ field }) => {
133+
const { onChange, ...rest } = field;
134+
return (
135+
<FormItem>
136+
<FormLabel>Value of intellectual property</FormLabel>
137+
<FormControl>
138+
<NumericFormat
139+
thousandSeparator
140+
allowedDecimalSeparators={["%"]}
141+
decimalScale={2}
142+
prefix={"$ "}
143+
{...rest}
144+
customInput={Input}
145+
onValueChange={(values) => {
146+
const { floatValue } = values;
147+
onChange(floatValue);
148+
}}
149+
/>
150+
</FormControl>
151+
<FormMessage className="text-xs font-light" />
152+
</FormItem>
153+
);
154+
}}
155+
/>
156+
</div>
157+
</div>
158+
<div className="flex items-center gap-4">
159+
<div className="flex-1">
160+
<FormField
161+
control={form.control}
162+
name="debtCancelled"
163+
render={({ field }) => {
164+
const { onChange, ...rest } = field;
165+
return (
166+
<FormItem>
167+
<FormLabel>Debt cancelled amount</FormLabel>
168+
<FormControl>
169+
<NumericFormat
170+
thousandSeparator
171+
allowedDecimalSeparators={["%"]}
172+
decimalScale={2}
173+
prefix={"$ "}
174+
{...rest}
175+
customInput={Input}
176+
onValueChange={(values) => {
177+
const { floatValue } = values;
178+
onChange(floatValue);
179+
}}
180+
/>
181+
</FormControl>
182+
<FormMessage className="text-xs font-light" />
183+
</FormItem>
184+
);
185+
}}
186+
/>
187+
</div>
188+
<div className="flex-1">
189+
<FormField
190+
control={form.control}
191+
name="otherContributions"
192+
render={({ field }) => {
193+
const { onChange, ...rest } = field;
194+
return (
195+
<FormItem>
196+
<FormLabel>Other contributed amount</FormLabel>
197+
<FormControl>
198+
<NumericFormat
199+
thousandSeparator
200+
allowedDecimalSeparators={["%"]}
201+
decimalScale={2}
202+
prefix={"$ "}
203+
{...rest}
204+
customInput={Input}
205+
onValueChange={(values) => {
206+
const { floatValue } = values;
207+
onChange(floatValue);
208+
}}
209+
/>
210+
</FormControl>
211+
<FormMessage className="text-xs font-light" />
212+
</FormItem>
213+
);
214+
}}
215+
/>
216+
</div>
217+
</div>
218+
</div>
219+
<StepperModalFooter>
220+
<StepperPrev>Back</StepperPrev>
221+
<Button type="submit">Save & Continue</Button>
222+
</StepperModalFooter>
223+
</form>
224+
</Form>
225+
);
226+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
"use client";
2+
3+
import { uploadFile } from "@/common/uploads";
4+
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
5+
import { Button } from "@/components/ui/button";
6+
import { DialogClose } from "@/components/ui/dialog";
7+
import {
8+
StepperModalFooter,
9+
StepperPrev,
10+
useStepper,
11+
} from "@/components/ui/stepper";
12+
import Uploader from "@/components/ui/uploader";
13+
import { invariant } from "@/lib/error";
14+
import { useAddShareFormValues } from "@/providers/add-share-form-provider";
15+
import { api } from "@/trpc/react";
16+
import { useSession } from "next-auth/react";
17+
import { useRouter } from "next/navigation";
18+
import { useState } from "react";
19+
import type { FileWithPath } from "react-dropzone";
20+
import { toast } from "sonner";
21+
22+
export const Documents = () => {
23+
const router = useRouter();
24+
const { data: session } = useSession();
25+
const { value } = useAddShareFormValues();
26+
const { reset } = useStepper();
27+
const [documentsList, setDocumentsList] = useState<FileWithPath[] | []>([]);
28+
const { mutateAsync: handleBucketUpload } = api.bucket.create.useMutation();
29+
const { mutateAsync: addShareMutation } =
30+
api.securities.addShares.useMutation({
31+
onSuccess: ({ success }) => {
32+
invariant(session, "session not found");
33+
if (success) {
34+
toast.success("Successfully issued the share.");
35+
router.refresh();
36+
reset();
37+
} else {
38+
toast.error("Failed issuing the share. Please try again.");
39+
}
40+
},
41+
});
42+
const handleComplete = async () => {
43+
invariant(session, "session not found");
44+
const uploadedDocuments: { name: string; bucketId: string }[] = [];
45+
for (const document of documentsList) {
46+
const { key, mimeType, name, size } = await uploadFile(document, {
47+
identifier: session.user.companyPublicId,
48+
keyPrefix: "sharesDocs",
49+
});
50+
const { id: bucketId, name: docName } = await handleBucketUpload({
51+
key,
52+
mimeType,
53+
name,
54+
size,
55+
});
56+
uploadedDocuments.push({ bucketId, name: docName });
57+
}
58+
await addShareMutation({ ...value, documents: uploadedDocuments });
59+
};
60+
return (
61+
<div className="flex flex-col gap-y-4">
62+
<div>
63+
<Uploader
64+
multiple={true}
65+
identifier={""}
66+
keyPrefix="equity-doc"
67+
shouldUpload={false}
68+
onSuccess={(bucketData) => {
69+
setDocumentsList(bucketData);
70+
}}
71+
accept={{
72+
"application/pdf": [".pdf"],
73+
}}
74+
/>
75+
{documentsList?.length ? (
76+
<Alert className="mt-5 bg-teal-100" variant="default">
77+
<AlertTitle>
78+
{documentsList.length > 1
79+
? `${documentsList.length} documents uploaded`
80+
: `${documentsList.length} document uploaded`}
81+
</AlertTitle>
82+
<AlertDescription>
83+
You can submit the form to proceed.
84+
</AlertDescription>
85+
</Alert>
86+
) : (
87+
<Alert variant="destructive" className="mt-5">
88+
<AlertTitle>0 document uploaded</AlertTitle>
89+
<AlertDescription>
90+
Please upload necessary documents to continue.
91+
</AlertDescription>
92+
</Alert>
93+
)}
94+
</div>
95+
<StepperModalFooter>
96+
<StepperPrev>Back</StepperPrev>
97+
<DialogClose asChild>
98+
<Button
99+
disabled={documentsList.length === 0}
100+
onClick={handleComplete}
101+
>
102+
Submit
103+
</Button>
104+
</DialogClose>
105+
</StepperModalFooter>
106+
</div>
107+
);
108+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
"use client";
2+
import { EmptySelect } from "@/components/securities/shared/EmptySelect";
3+
import { Button } from "@/components/ui/button";
4+
import {
5+
Form,
6+
FormControl,
7+
FormField,
8+
FormItem,
9+
FormLabel,
10+
FormMessage,
11+
} from "@/components/ui/form";
12+
import { Input } from "@/components/ui/input";
13+
import {
14+
Select,
15+
SelectContent,
16+
SelectItem,
17+
SelectTrigger,
18+
SelectValue,
19+
} from "@/components/ui/select";
20+
import {
21+
MultiSelector,
22+
MultiSelectorContent,
23+
MultiSelectorInput,
24+
MultiSelectorItem,
25+
MultiSelectorList,
26+
MultiSelectorTrigger,
27+
} from "@/components/ui/simple-multi-select";
28+
import {
29+
StepperModalFooter,
30+
StepperPrev,
31+
useStepper,
32+
} from "@/components/ui/stepper";
33+
import { VestingSchedule } from "@/lib/vesting";
34+
import {
35+
SecuritiesStatusEnum,
36+
ShareLegendsEnum,
37+
VestingScheduleEnum,
38+
} from "@/prisma/enums";
39+
import { useAddShareFormValues } from "@/providers/add-share-form-provider";
40+
import type { RouterOutputs } from "@/trpc/shared";
41+
import { zodResolver } from "@hookform/resolvers/zod";
42+
import { useForm } from "react-hook-form";
43+
import { NumericFormat } from "react-number-format";
44+
import { z } from "zod";
45+
46+
export const humanizeCompanyLegends = (type: string): string => {
47+
switch (type) {
48+
case ShareLegendsEnum.US_SECURITIES_ACT:
49+
return "US Securities Act";
50+
case ShareLegendsEnum.TRANSFER_RESTRICTIONS:
51+
return "Transfer Restrictions";
52+
case ShareLegendsEnum.SALE_AND_ROFR:
53+
return "Sale and ROFR";
54+
default:
55+
return "";
56+
}
57+
};
58+
59+
const formSchema = z.object({
60+
shareClassId: z.string(),
61+
certificateId: z.string(),
62+
status: z.nativeEnum(SecuritiesStatusEnum),
63+
quantity: z.coerce.number().min(0),
64+
vestingSchedule: z.nativeEnum(VestingScheduleEnum),
65+
companyLegends: z.nativeEnum(ShareLegendsEnum).array(),
66+
pricePerShare: z.coerce.number().min(0),
67+
});
68+
69+
type TFormSchema = z.infer<typeof formSchema>;
70+
71+
type ShareClasses = RouterOutputs["shareClass"]["get"];
72+
73+
interface GeneralDetailsProps {
74+
shareClasses: ShareClasses;
75+
}
76+
77+
export const GeneralDetails = ({ shareClasses }: GeneralDetailsProps) => {
78+
const form = useForm<TFormSchema>({
79+
resolver: zodResolver(formSchema),
80+
});
81+
const { next } = useStepper();
82+
const { setValue } = useAddShareFormValues();
83+
84+
const status = Object.values(SecuritiesStatusEnum);
85+
const vestingSchedule = Object.values(VestingScheduleEnum);
86+
const companyLegends = Object.values(ShareLegendsEnum);
87+
88+
const handleSubmit = (data: TFormSchema) => {
89+
setValue(data);
90+
next();
91+
};
92+
93+
return (
94+
<Form {...form}>
95+
<form
96+
onSubmit={form.handleSubmit(handleSubmit)}
97+
className="flex flex-col gap-y-4"
98+
>
99+
<div className="space-y-4">
100+
<div className="flex-1">
101+
<FormField
102+
control={form.control}
103+
name="certificateId"
104+
render={({ field }) => (
105+
<FormItem>
106+
<FormLabel>Certificate ID</FormLabel>
107+
<FormControl>
108+
<Input type="text" {...field} />
109+
</FormControl>
110+
<FormMessage className="text-xs font-light" />
111+
</FormItem>
112+
)}
113+
/>
114+
</div>
115+
<div className="flex items-center gap-4">
116+
<div className="flex-1">
117+
<FormField
118+
control={form.control}
119+
name="shareClassId"
120+
render={({ field }) => (
121+
<FormItem>
122+
<FormLabel>Share class</FormLabel>
123+
<Select onValueChange={field.onChange} value={field.value}>
124+
<FormControl>
125+
<SelectTrigger className="w-full">
126+
<SelectValue placeholder="Select a share class" />
127+
</SelectTrigger>
128+
</FormControl>
129+
<SelectContent>
130+
{shareClasses.length ? (
131+
shareClasses.map((sc) => (
132+
<SelectItem key={sc.id} value={sc.id}>
133+
{sc.name}
134+
</SelectItem>
135+
))
136+
) : (
137+
<EmptySelect
138+
title="Share class not found!"
139+
description="Please add required share class."
140+
/>
141+
)}
142+
</SelectContent>
143+
</Select>
144+
<FormMessage className="text-xs font-light" />
145+
</FormItem>
146+
)}
147+
/>
148+
</div>
149+
<div className="flex-1">
150+
<FormField
151+
control={form.control}
152+
name="status"
153+
render={({ field }) => (
154+
<FormItem>
155+
<FormLabel>Status</FormLabel>
156+
<Select
157+
onValueChange={field.onChange}
158+
value={field.value as string}
159+
>
160+
<FormControl>
161+
<SelectTrigger className="w-full">
162+
<SelectValue placeholder="Select status" />
163+
</SelectTrigger>
164+
</FormControl>
165+
<SelectContent>
166+
{status
167+
? status.map((s) => (
168+
<SelectItem key={s} value={s}>
169+
{s}
170+
</SelectItem>
171+
))
172+
: null}
173+
</SelectContent>
174+
</Select>
175+
<FormMessage className="text-xs font-light" />
176+
</FormItem>
177+
)}
178+
/>
179+
</div>
180+
</div>
181+
182+
<div className="flex items-center gap-4">
183+
<div className="flex-1">
184+
<FormField
185+
control={form.control}
186+
name="quantity"
187+
render={({ field }) => {
188+
const { onChange, ...rest } = field;
189+
return (
190+
<FormItem>
191+
<FormLabel>Quantity</FormLabel>
192+
<FormControl>
193+
<NumericFormat
194+
thousandSeparator
195+
allowedDecimalSeparators={["%"]}
196+
decimalScale={2}
197+
{...rest}
198+
customInput={Input}
199+
onValueChange={(values) => {
200+
const { floatValue } = values;
201+
onChange(floatValue);
202+
}}
203+
/>
204+
</FormControl>
205+
<FormMessage className="text-xs font-light" />
206+
</FormItem>
207+
);
208+
}}
209+
/>
210+
</div>
211+
<div className="flex-1">
212+
<FormField
213+
control={form.control}
214+
name="pricePerShare"
215+
render={({ field }) => {
216+
const { onChange, ...rest } = field;
217+
return (
218+
<FormItem>
219+
<FormLabel>Price per share</FormLabel>
220+
<FormControl>
221+
<NumericFormat
222+
thousandSeparator
223+
allowedDecimalSeparators={["%"]}
224+
decimalScale={2}
225+
prefix={"$ "}
226+
{...rest}
227+
customInput={Input}
228+
onValueChange={(values) => {
229+
const { floatValue } = values;
230+
onChange(floatValue);
231+
}}
232+
/>
233+
</FormControl>
234+
<FormMessage className="text-xs font-light" />
235+
</FormItem>
236+
);
237+
}}
238+
/>
239+
</div>
240+
</div>
241+
242+
<FormField
243+
control={form.control}
244+
name="vestingSchedule"
245+
render={({ field }) => (
246+
<FormItem>
247+
<FormLabel>Vesting schedule</FormLabel>
248+
<Select
249+
onValueChange={field.onChange}
250+
value={field.value as string}
251+
>
252+
<FormControl>
253+
<SelectTrigger>
254+
<SelectValue placeholder="Select vesting schedule" />
255+
</SelectTrigger>
256+
</FormControl>
257+
<SelectContent>
258+
{vestingSchedule?.length &&
259+
vestingSchedule.map((vs) => (
260+
<SelectItem key={vs} value={vs}>
261+
{VestingSchedule[vs]}
262+
</SelectItem>
263+
))}
264+
</SelectContent>
265+
</Select>
266+
<FormMessage className="text-xs font-light" />
267+
</FormItem>
268+
)}
269+
/>
270+
271+
<FormField
272+
control={form.control}
273+
name="companyLegends"
274+
render={({ field }) => (
275+
<FormItem>
276+
<FormLabel>Company legends</FormLabel>
277+
<MultiSelector
278+
onValuesChange={field.onChange}
279+
values={field.value}
280+
>
281+
<MultiSelectorTrigger>
282+
<MultiSelectorInput
283+
className="text-sm"
284+
placeholder="Select company legends"
285+
/>
286+
</MultiSelectorTrigger>
287+
<MultiSelectorContent>
288+
<MultiSelectorList>
289+
{companyLegends.map((cl) => (
290+
<MultiSelectorItem key={cl} value={cl}>
291+
{humanizeCompanyLegends(cl)}
292+
</MultiSelectorItem>
293+
))}
294+
</MultiSelectorList>
295+
</MultiSelectorContent>
296+
</MultiSelector>
297+
</FormItem>
298+
)}
299+
/>
300+
</div>
301+
<StepperModalFooter>
302+
<StepperPrev>Back</StepperPrev>
303+
<Button type="submit">Save & Continue</Button>
304+
</StepperModalFooter>
305+
</form>
306+
</Form>
307+
);
308+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
"use client";
2+
3+
import { Button } from "@/components/ui/button";
4+
import {
5+
Form,
6+
FormControl,
7+
FormField,
8+
FormItem,
9+
FormLabel,
10+
FormMessage,
11+
} from "@/components/ui/form";
12+
import { Input } from "@/components/ui/input";
13+
import {
14+
StepperModalFooter,
15+
StepperPrev,
16+
useStepper,
17+
} from "@/components/ui/stepper";
18+
import { useAddShareFormValues } from "@/providers/add-share-form-provider";
19+
import { zodResolver } from "@hookform/resolvers/zod";
20+
import { useForm } from "react-hook-form";
21+
import { z } from "zod";
22+
23+
const formSchema = z.object({
24+
boardApprovalDate: z.string().date(),
25+
rule144Date: z.string().date(),
26+
issueDate: z.string().date(),
27+
vestingStartDate: z.string().date(),
28+
});
29+
30+
type TFormSchema = z.infer<typeof formSchema>;
31+
32+
export const RelevantDates = () => {
33+
const form = useForm<TFormSchema>({ resolver: zodResolver(formSchema) });
34+
const { next } = useStepper();
35+
const { setValue } = useAddShareFormValues();
36+
37+
const handleSubmit = (data: TFormSchema) => {
38+
setValue(data);
39+
next();
40+
};
41+
return (
42+
<Form {...form}>
43+
<form
44+
className="flex flex-col gap-y-4"
45+
onSubmit={form.handleSubmit(handleSubmit)}
46+
>
47+
<div className="space-y-4">
48+
<div className="flex items-center gap-4">
49+
<div className="flex-1">
50+
<FormField
51+
control={form.control}
52+
name="issueDate"
53+
render={({ field }) => (
54+
<FormItem>
55+
<FormLabel>Issue date</FormLabel>
56+
<FormControl>
57+
<Input type="date" {...field} />
58+
</FormControl>
59+
<FormMessage className="text-xs font-light" />
60+
</FormItem>
61+
)}
62+
/>
63+
</div>
64+
<div className="flex-1">
65+
<FormField
66+
control={form.control}
67+
name="vestingStartDate"
68+
render={({ field }) => (
69+
<FormItem>
70+
<FormLabel>Vesting start date</FormLabel>
71+
<FormControl>
72+
<Input type="date" {...field} />
73+
</FormControl>
74+
<FormMessage className="text-xs font-light" />
75+
</FormItem>
76+
)}
77+
/>
78+
</div>
79+
</div>
80+
81+
<div className="flex items-center gap-4">
82+
<div className="flex-1">
83+
<FormField
84+
control={form.control}
85+
name="boardApprovalDate"
86+
render={({ field }) => (
87+
<FormItem>
88+
<FormLabel>Board approval date</FormLabel>
89+
<FormControl>
90+
<Input type="date" {...field} />
91+
</FormControl>
92+
<FormMessage className="text-xs font-light" />
93+
</FormItem>
94+
)}
95+
/>
96+
</div>
97+
<div className="flex-1">
98+
<FormField
99+
control={form.control}
100+
name="rule144Date"
101+
render={({ field }) => (
102+
<FormItem>
103+
<FormLabel>Rule 144 date</FormLabel>
104+
<FormControl>
105+
<Input type="date" {...field} />
106+
</FormControl>
107+
<FormMessage className="text-xs font-light" />
108+
</FormItem>
109+
)}
110+
/>
111+
</div>
112+
</div>
113+
</div>
114+
<StepperModalFooter>
115+
<StepperPrev>Back</StepperPrev>
116+
<Button type="submit">Save & Continue</Button>
117+
</StepperModalFooter>
118+
</form>
119+
</Form>
120+
);
121+
};
+315
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
"use client";
2+
3+
import { Badge } from "@/components/ui/badge";
4+
import {
5+
Command,
6+
CommandEmpty,
7+
CommandItem,
8+
CommandList,
9+
} from "@/components/ui/command";
10+
import { cn } from "@/lib/utils";
11+
import { RiCheckLine, RiCloseLine } from "@remixicon/react";
12+
import { Command as CommandPrimitive } from "cmdk";
13+
// import { X as RemoveIcon, Ri } from "lucide-react";
14+
import React, {
15+
KeyboardEvent,
16+
createContext,
17+
forwardRef,
18+
useCallback,
19+
useContext,
20+
useState,
21+
} from "react";
22+
23+
type MultiSelectorProps = {
24+
values: string[];
25+
onValuesChange: (value: string[]) => void;
26+
loop?: boolean;
27+
} & React.ComponentPropsWithoutRef<typeof CommandPrimitive>;
28+
29+
interface MultiSelectContextProps {
30+
value: string[];
31+
onValueChange: (value: any) => void;
32+
open: boolean;
33+
setOpen: (value: boolean) => void;
34+
inputValue: string;
35+
setInputValue: React.Dispatch<React.SetStateAction<string>>;
36+
activeIndex: number;
37+
setActiveIndex: React.Dispatch<React.SetStateAction<number>>;
38+
}
39+
40+
const MultiSelectContext = createContext<MultiSelectContextProps | null>(null);
41+
42+
const useMultiSelect = () => {
43+
const context = useContext(MultiSelectContext);
44+
if (!context) {
45+
throw new Error("useMultiSelect must be used within MultiSelectProvider");
46+
}
47+
return context;
48+
};
49+
50+
const MultiSelector = ({
51+
values: value,
52+
onValuesChange: onValueChange,
53+
loop = false,
54+
className,
55+
children,
56+
dir,
57+
...props
58+
}: MultiSelectorProps) => {
59+
const [inputValue, setInputValue] = useState("");
60+
const [open, setOpen] = useState<boolean>(false);
61+
const [activeIndex, setActiveIndex] = useState<number>(-1);
62+
63+
const onValueChangeHandler = useCallback(
64+
(val: string) => {
65+
if (value?.includes(val)) {
66+
onValueChange(value?.filter((item) => item !== val));
67+
} else {
68+
if (value?.length) {
69+
onValueChange([...value, val]);
70+
} else {
71+
onValueChange([val]);
72+
}
73+
}
74+
},
75+
[value],
76+
);
77+
78+
// TODO : change from else if use to switch case statement
79+
80+
const handleKeyDown = useCallback(
81+
(e: KeyboardEvent<HTMLDivElement>) => {
82+
const moveNext = () => {
83+
const nextIndex = activeIndex + 1;
84+
setActiveIndex(
85+
nextIndex > value.length - 1 ? (loop ? 0 : -1) : nextIndex,
86+
);
87+
};
88+
89+
const movePrev = () => {
90+
const prevIndex = activeIndex - 1;
91+
setActiveIndex(prevIndex < 0 ? value.length - 1 : prevIndex);
92+
};
93+
94+
if ((e.key === "Backspace" || e.key === "Delete") && value.length > 0) {
95+
if (inputValue.length === 0) {
96+
if (activeIndex !== -1 && activeIndex < value.length) {
97+
onValueChange(value.filter((item) => item !== value[activeIndex]));
98+
const newIndex = activeIndex - 1 < 0 ? 0 : activeIndex - 1;
99+
setActiveIndex(newIndex);
100+
} else {
101+
onValueChange(
102+
value.filter((item) => item !== value[value.length - 1]),
103+
);
104+
}
105+
}
106+
} else if (e.key === "Enter") {
107+
setOpen(true);
108+
} else if (e.key === "Escape") {
109+
if (activeIndex !== -1) {
110+
setActiveIndex(-1);
111+
} else {
112+
setOpen(false);
113+
}
114+
} else if (dir === "rtl") {
115+
if (e.key === "ArrowRight") {
116+
movePrev();
117+
} else if (e.key === "ArrowLeft" && (activeIndex !== -1 || loop)) {
118+
moveNext();
119+
}
120+
} else {
121+
if (e.key === "ArrowLeft") {
122+
movePrev();
123+
} else if (e.key === "ArrowRight" && (activeIndex !== -1 || loop)) {
124+
moveNext();
125+
}
126+
}
127+
},
128+
[value, inputValue, activeIndex, loop],
129+
);
130+
131+
return (
132+
<MultiSelectContext.Provider
133+
value={{
134+
value,
135+
onValueChange: onValueChangeHandler,
136+
open,
137+
setOpen,
138+
inputValue,
139+
setInputValue,
140+
activeIndex,
141+
setActiveIndex,
142+
}}
143+
>
144+
<Command
145+
onKeyDown={handleKeyDown}
146+
className={cn(
147+
"overflow-visible bg-transparent flex flex-col space-y-2",
148+
className,
149+
)}
150+
dir={dir}
151+
{...props}
152+
>
153+
{children}
154+
</Command>
155+
</MultiSelectContext.Provider>
156+
);
157+
};
158+
159+
const MultiSelectorTrigger = forwardRef<
160+
HTMLDivElement,
161+
React.HTMLAttributes<HTMLDivElement>
162+
>(({ className, children, ...props }, ref) => {
163+
const { value, onValueChange, activeIndex } = useMultiSelect();
164+
165+
const mousePreventDefault = useCallback((e: React.MouseEvent) => {
166+
e.preventDefault();
167+
e.stopPropagation();
168+
}, []);
169+
170+
return (
171+
<div
172+
ref={ref}
173+
className={cn(
174+
"flex flex-wrap gap-1 p-1 py-2 border border-muted rounded-lg bg-background",
175+
className,
176+
)}
177+
{...props}
178+
>
179+
{value?.map((item, index) => (
180+
<Badge
181+
key={item}
182+
className={cn(
183+
"px-1 rounded-xl flex items-center gap-1",
184+
activeIndex === index && "ring-2 ring-muted-foreground ",
185+
)}
186+
variant={"secondary"}
187+
>
188+
<span className="text-xs">{item}</span>
189+
<button
190+
aria-label={`Remove ${item} option`}
191+
aria-roledescription="button to remove option"
192+
type="button"
193+
onMouseDown={mousePreventDefault}
194+
onClick={() => onValueChange(item)}
195+
>
196+
<span className="sr-only">Remove {item} option</span>
197+
<RiCloseLine className="h-4 w-4 hover:stroke-destructive" />
198+
</button>
199+
</Badge>
200+
))}
201+
{children}
202+
</div>
203+
);
204+
});
205+
206+
MultiSelectorTrigger.displayName = "MultiSelectorTrigger";
207+
208+
const MultiSelectorInput = forwardRef<
209+
React.ElementRef<typeof CommandPrimitive.Input>,
210+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
211+
>(({ className, ...props }, ref) => {
212+
const { setOpen, inputValue, setInputValue, activeIndex, setActiveIndex } =
213+
useMultiSelect();
214+
return (
215+
<CommandPrimitive.Input
216+
{...props}
217+
ref={ref}
218+
value={inputValue}
219+
onValueChange={activeIndex === -1 ? setInputValue : undefined}
220+
onBlur={() => setOpen(false)}
221+
onFocus={() => setOpen(true)}
222+
onClick={() => setActiveIndex(-1)}
223+
className={cn(
224+
"ml-2 bg-transparent outline-none placeholder:text-muted-foreground flex-1",
225+
className,
226+
activeIndex !== -1 && "caret-transparent",
227+
)}
228+
/>
229+
);
230+
});
231+
232+
MultiSelectorInput.displayName = "MultiSelectorInput";
233+
234+
const MultiSelectorContent = forwardRef<
235+
HTMLDivElement,
236+
React.HTMLAttributes<HTMLDivElement>
237+
>(({ children }, ref) => {
238+
const { open } = useMultiSelect();
239+
return (
240+
<div ref={ref} className="relative">
241+
{open && children}
242+
</div>
243+
);
244+
});
245+
246+
MultiSelectorContent.displayName = "MultiSelectorContent";
247+
248+
const MultiSelectorList = forwardRef<
249+
React.ElementRef<typeof CommandPrimitive.List>,
250+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
251+
>(({ className, children }, ref) => {
252+
return (
253+
<CommandList
254+
ref={ref}
255+
className={cn(
256+
"p-2 flex flex-col gap-2 rounded-md scrollbar-thin scrollbar-track-transparent transition-colors scrollbar-thumb-muted-foreground dark:scrollbar-thumb-muted scrollbar-thumb-rounded-lg w-full absolute bg-background shadow-md z-10 border border-muted top-0",
257+
className,
258+
)}
259+
>
260+
{children}
261+
<CommandEmpty>
262+
<span className="text-muted-foreground">No results found</span>
263+
</CommandEmpty>
264+
</CommandList>
265+
);
266+
});
267+
268+
MultiSelectorList.displayName = "MultiSelectorList";
269+
270+
const MultiSelectorItem = forwardRef<
271+
React.ElementRef<typeof CommandPrimitive.Item>,
272+
{ value: string } & React.ComponentPropsWithoutRef<
273+
typeof CommandPrimitive.Item
274+
>
275+
>(({ className, value, children, ...props }, ref) => {
276+
const { value: Options, onValueChange, setInputValue } = useMultiSelect();
277+
278+
const mousePreventDefault = useCallback((e: React.MouseEvent) => {
279+
e.preventDefault();
280+
e.stopPropagation();
281+
}, []);
282+
283+
const isIncluded = Options?.includes(value);
284+
return (
285+
<CommandItem
286+
ref={ref}
287+
{...props}
288+
onSelect={() => {
289+
onValueChange(value);
290+
setInputValue("");
291+
}}
292+
className={cn(
293+
"rounded-md cursor-pointer px-2 py-1 transition-colors flex justify-between ",
294+
className,
295+
isIncluded && "opacity-50 cursor-default",
296+
props.disabled && "opacity-50 cursor-not-allowed",
297+
)}
298+
onMouseDown={mousePreventDefault}
299+
>
300+
{children}
301+
{isIncluded && <RiCheckLine className="h-4 w-4" />}
302+
</CommandItem>
303+
);
304+
});
305+
306+
MultiSelectorItem.displayName = "MultiSelectorItem";
307+
308+
export {
309+
MultiSelector,
310+
MultiSelectorContent,
311+
MultiSelectorInput,
312+
MultiSelectorItem,
313+
MultiSelectorList,
314+
MultiSelectorTrigger,
315+
};

‎src/lib/utils.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export function cn(...inputs: ClassValue[]) {
99
export function getFileSizeSuffix(bytes: number): string {
1010
const suffixes = ["", "K", "M", "G", "T"];
1111
const magnitude = Math.floor(Math.log2(bytes) / 10);
12-
const suffix = suffixes[magnitude] + "B";
12+
const suffix = `${suffixes[magnitude]}B`;
1313
return suffix;
1414
}
1515

@@ -71,3 +71,14 @@ export function compareFormDataWithInitial<T extends Record<string, string>>(
7171

7272
return isChanged;
7373
}
74+
75+
export function formatNumber(value: number): string {
76+
return new Intl.NumberFormat("en-US").format(value);
77+
}
78+
79+
export function formatCurrency(value: number, currency: "USD") {
80+
return new Intl.NumberFormat("en-US", {
81+
style: "currency",
82+
currency: currency,
83+
}).format(value);
84+
}
+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"use client";
2+
3+
import type { TypeZodAddShareMutationSchema } from "@/trpc/routers/securities-router/schema";
4+
import {
5+
type Dispatch,
6+
type ReactNode,
7+
createContext,
8+
useContext,
9+
useReducer,
10+
} from "react";
11+
12+
type TFormValue = TypeZodAddShareMutationSchema;
13+
14+
interface AddShareFormProviderProps {
15+
children: ReactNode;
16+
}
17+
18+
const AddShareFormProviderContext = createContext<{
19+
value: TFormValue;
20+
setValue: Dispatch<Partial<TFormValue>>;
21+
} | null>(null);
22+
23+
export function AddShareFormProvider({ children }: AddShareFormProviderProps) {
24+
const [value, setValue] = useReducer(
25+
(data: TFormValue, partialData: Partial<TFormValue>) => ({
26+
...data,
27+
...partialData,
28+
}),
29+
{} as TFormValue,
30+
);
31+
32+
return (
33+
<AddShareFormProviderContext.Provider value={{ value, setValue }}>
34+
{children}
35+
</AddShareFormProviderContext.Provider>
36+
);
37+
}
38+
39+
export const useAddShareFormValues = () => {
40+
const data = useContext(AddShareFormProviderContext);
41+
if (!data) {
42+
throw new Error("useAddShareFormValues shouldn't be null");
43+
}
44+
return data;
45+
};

‎src/server/audit/schema.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ export const AuditSchema = z.object({
3131
"option.created",
3232
"option.deleted",
3333

34+
"share.created",
35+
"share.updated",
36+
"share.deleted",
37+
3438
"safe.created",
3539
"safe.imported",
3640
"safe.sent",
@@ -49,7 +53,14 @@ export const AuditSchema = z.object({
4953

5054
target: z.array(
5155
z.object({
52-
type: z.enum(["user", "company", "document", "option", "documentShare"]),
56+
type: z.enum([
57+
"user",
58+
"company",
59+
"document",
60+
"option",
61+
"documentShare",
62+
"share",
63+
]),
5364
id: z.string().optional().nullable(),
5465
}),
5566
),

‎src/trpc/routers/company-router/router.ts

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export const companyRouter = createTRPCRouter({
3030
city: true,
3131
zipcode: true,
3232
streetAddress: true,
33+
country: true,
3334
logo: true,
3435
},
3536
},

‎src/trpc/routers/onboarding-router/schema.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,12 @@ export const ZodCompanyMutationSchema = z.object({
4747
zipcode: z.string().min(1, {
4848
message: "Zipcode is required",
4949
}),
50-
country: z.string().min(1, {
51-
message: "Country is required",
52-
}),
50+
country: z
51+
.string()
52+
.min(1, {
53+
message: "Country is required",
54+
})
55+
.default("US"),
5356
logo: z.string().min(1).optional(),
5457
}),
5558
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { generatePublicId } from "@/common/id";
2+
import { Audit } from "@/server/audit";
3+
import { checkMembership } from "@/server/auth";
4+
import { withAuth } from "@/trpc/api/trpc";
5+
import { ZodAddShareMutationSchema } from "../schema";
6+
7+
export const addShareProcedure = withAuth
8+
.input(ZodAddShareMutationSchema)
9+
.mutation(async ({ ctx, input }) => {
10+
console.log({ input }, "#############");
11+
12+
const { userAgent, requestIp } = ctx;
13+
14+
try {
15+
const user = ctx.session.user;
16+
const documents = input.documents;
17+
18+
await ctx.db.$transaction(async (tx) => {
19+
const { companyId } = await checkMembership({
20+
session: ctx.session,
21+
tx,
22+
});
23+
24+
const data = {
25+
companyId,
26+
stakeholderId: input.stakeholderId,
27+
shareClassId: input.shareClassId,
28+
status: input.status,
29+
certificateId: input.certificateId,
30+
quantity: input.quantity,
31+
pricePerShare: input.pricePerShare,
32+
capitalContribution: input.capitalContribution,
33+
ipContribution: input.ipContribution,
34+
debtCancelled: input.debtCancelled,
35+
otherContributions: input.otherContributions,
36+
vestingSchedule: input.vestingSchedule,
37+
companyLegends: input.companyLegends,
38+
issueDate: new Date(input.issueDate),
39+
rule144Date: new Date(input.rule144Date),
40+
vestingStartDate: new Date(input.vestingStartDate),
41+
boardApprovalDate: new Date(input.boardApprovalDate),
42+
};
43+
const share = await tx.share.create({ data });
44+
45+
const bulkDocuments = documents.map((doc) => ({
46+
companyId,
47+
uploaderId: user.memberId,
48+
publicId: generatePublicId(),
49+
name: doc.name,
50+
bucketId: doc.bucketId,
51+
shareId: share.id,
52+
}));
53+
54+
await tx.document.createMany({
55+
data: bulkDocuments,
56+
skipDuplicates: true,
57+
});
58+
59+
await Audit.create(
60+
{
61+
action: "share.created",
62+
companyId: user.companyId,
63+
actor: { type: "user", id: user.id },
64+
context: {
65+
userAgent,
66+
requestIp,
67+
},
68+
target: [{ type: "share", id: share.id }],
69+
summary: `${user.name} added share for stakeholder ${input.stakeholderId}`,
70+
},
71+
tx,
72+
);
73+
});
74+
75+
return {
76+
success: true,
77+
message: "🎉 Successfully added a share",
78+
};
79+
} catch (error) {
80+
console.error("Error adding shares: ", error);
81+
return {
82+
success: false,
83+
message: "Please use unique Certificate Id.",
84+
};
85+
}
86+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { Audit } from "@/server/audit";
2+
import { checkMembership } from "@/server/auth";
3+
import { withAuth, type withAuthTrpcContextType } from "@/trpc/api/trpc";
4+
import {
5+
type TypeZodDeleteShareMutationSchema,
6+
ZodDeleteShareMutationSchema,
7+
} from "../schema";
8+
9+
export const deleteShareProcedure = withAuth
10+
.input(ZodDeleteShareMutationSchema)
11+
.mutation(async (args) => {
12+
return await deleteShareHandler(args);
13+
});
14+
15+
interface deleteShareHandlerOptions {
16+
input: TypeZodDeleteShareMutationSchema;
17+
ctx: withAuthTrpcContextType;
18+
}
19+
20+
export async function deleteShareHandler({
21+
ctx: { db, session, requestIp, userAgent },
22+
input,
23+
}: deleteShareHandlerOptions) {
24+
const user = session.user;
25+
const { shareId } = input;
26+
try {
27+
await db.$transaction(async (tx) => {
28+
const { companyId } = await checkMembership({ session, tx });
29+
30+
const share = await tx.share.delete({
31+
where: {
32+
id: shareId,
33+
companyId,
34+
},
35+
select: {
36+
id: true,
37+
stakeholder: {
38+
select: {
39+
id: true,
40+
name: true,
41+
},
42+
},
43+
company: {
44+
select: {
45+
name: true,
46+
},
47+
},
48+
},
49+
});
50+
51+
await Audit.create(
52+
{
53+
action: "share.deleted",
54+
companyId: user.companyId,
55+
actor: { type: "user", id: session.user.id },
56+
context: {
57+
requestIp,
58+
userAgent,
59+
},
60+
target: [{ type: "share", id: share.id }],
61+
summary: `${user.name} deleted share of stakholder ${share.stakeholder.name}`,
62+
},
63+
tx,
64+
);
65+
});
66+
67+
return { success: true };
68+
} catch (err) {
69+
console.error(err);
70+
return {
71+
success: false,
72+
message: "Oops, something went wrong while deleting option.",
73+
};
74+
}
75+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { checkMembership } from "@/server/auth";
2+
import { withAuth } from "@/trpc/api/trpc";
3+
4+
export const getSharesProcedure = withAuth.query(
5+
async ({ ctx: { db, session } }) => {
6+
const data = await db.$transaction(async (tx) => {
7+
const { companyId } = await checkMembership({ session, tx });
8+
9+
const shares = await tx.share.findMany({
10+
where: {
11+
companyId,
12+
},
13+
select: {
14+
id: true,
15+
certificateId: true,
16+
quantity: true,
17+
pricePerShare: true,
18+
capitalContribution: true,
19+
ipContribution: true,
20+
debtCancelled: true,
21+
otherContributions: true,
22+
vestingSchedule: true,
23+
companyLegends: true,
24+
status: true,
25+
26+
issueDate: true,
27+
rule144Date: true,
28+
vestingStartDate: true,
29+
boardApprovalDate: true,
30+
stakeholder: {
31+
select: {
32+
name: true,
33+
},
34+
},
35+
shareClass: {
36+
select: {
37+
classType: true,
38+
},
39+
},
40+
documents: {
41+
select: {
42+
id: true,
43+
name: true,
44+
uploader: {
45+
select: {
46+
user: {
47+
select: {
48+
name: true,
49+
image: true,
50+
},
51+
},
52+
},
53+
},
54+
bucket: {
55+
select: {
56+
key: true,
57+
mimeType: true,
58+
size: true,
59+
},
60+
},
61+
},
62+
},
63+
},
64+
});
65+
66+
return shares;
67+
});
68+
69+
return { data };
70+
},
71+
);
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
import { createTRPCRouter } from "@/trpc/api/trpc";
22
import { addOptionProcedure } from "./procedures/add-option";
3+
import { addShareProcedure } from "./procedures/add-share";
34
import { deleteOptionProcedure } from "./procedures/delete-option";
5+
import { deleteShareProcedure } from "./procedures/delete-share";
46
import { getOptionsProcedure } from "./procedures/get-options";
7+
import { getSharesProcedure } from "./procedures/get-shares";
58

69
export const securitiesRouter = createTRPCRouter({
710
getOptions: getOptionsProcedure,
811
addOptions: addOptionProcedure,
912
deleteOption: deleteOptionProcedure,
13+
getShares: getSharesProcedure,
14+
addShares: addShareProcedure,
15+
deleteShare: deleteShareProcedure,
1016
});

‎src/trpc/routers/securities-router/schema.ts

+42
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import {
22
OptionStatusEnum,
33
OptionTypeEnum,
4+
ShareLegendsEnum,
45
VestingScheduleEnum,
56
} from "@/prisma/enums";
7+
import { SecuritiesStatusEnum } from "@prisma/client";
68
import { z } from "zod";
79

10+
// OPTIONS
811
export const ZodAddOptionMutationSchema = z.object({
912
id: z.string().optional(),
1013
grantId: z.string(),
@@ -40,3 +43,42 @@ export const ZodDeleteOptionMutationSchema = z.object({
4043
export type TypeZodDeleteOptionMutationSchema = z.infer<
4144
typeof ZodDeleteOptionMutationSchema
4245
>;
46+
47+
// SHARES
48+
export const ZodAddShareMutationSchema = z.object({
49+
id: z.string().optional().nullable(),
50+
stakeholderId: z.string(),
51+
shareClassId: z.string(),
52+
certificateId: z.string(),
53+
quantity: z.coerce.number().min(0),
54+
pricePerShare: z.coerce.number().min(0),
55+
capitalContribution: z.coerce.number().min(0),
56+
ipContribution: z.coerce.number().min(0),
57+
debtCancelled: z.coerce.number().min(0),
58+
otherContributions: z.coerce.number().min(0),
59+
status: z.nativeEnum(SecuritiesStatusEnum),
60+
vestingSchedule: z.nativeEnum(VestingScheduleEnum),
61+
companyLegends: z.nativeEnum(ShareLegendsEnum).array(),
62+
issueDate: z.string().date(),
63+
rule144Date: z.string().date(),
64+
vestingStartDate: z.string().date(),
65+
boardApprovalDate: z.string().date(),
66+
documents: z.array(
67+
z.object({
68+
bucketId: z.string(),
69+
name: z.string(),
70+
}),
71+
),
72+
});
73+
74+
export type TypeZodAddShareMutationSchema = z.infer<
75+
typeof ZodAddShareMutationSchema
76+
>;
77+
78+
export const ZodDeleteShareMutationSchema = z.object({
79+
shareId: z.string(),
80+
});
81+
82+
export type TypeZodDeleteShareMutationSchema = z.infer<
83+
typeof ZodDeleteShareMutationSchema
84+
>;

‎src/trpc/routers/share-class/router.ts

+23
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export const shareClassRouter = createTRPCRouter({
4848
};
4949

5050
await tx.shareClass.create({ data });
51+
5152
await Audit.create(
5253
{
5354
action: "shareClass.created",
@@ -137,4 +138,26 @@ export const shareClassRouter = createTRPCRouter({
137138
};
138139
}
139140
}),
141+
142+
get: withAuth.query(async ({ ctx: { db, session } }) => {
143+
const shareClass = await db.$transaction(async (tx) => {
144+
const { companyId } = await checkMembership({ session, tx });
145+
146+
return await tx.shareClass.findMany({
147+
where: {
148+
companyId,
149+
},
150+
select: {
151+
id: true,
152+
name: true,
153+
company: {
154+
select: {
155+
name: true,
156+
},
157+
},
158+
},
159+
});
160+
});
161+
return shareClass;
162+
}),
140163
});

‎src/trpc/routers/stakeholder-router/procedures/get-stakeholders.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,13 @@ export const getStakeholdersProcedure = withAuth.query(async ({ ctx }) => {
1111
where: {
1212
companyId,
1313
},
14-
14+
include: {
15+
company: {
16+
select: {
17+
name: true,
18+
},
19+
},
20+
},
1521
orderBy: {
1622
createdAt: "desc",
1723
},

0 commit comments

Comments
 (0)
Please sign in to comment.