diff --git a/biome.json b/biome.json
index edab1ee11..292fd169c 100644
--- a/biome.json
+++ b/biome.json
@@ -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": {
@@ -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": {
diff --git a/prisma/migrations/20240531045514_uniq_certificate_on_share/migration.sql b/prisma/migrations/20240531045514_uniq_certificate_on_share/migration.sql
new file mode 100644
index 000000000..39746aa40
--- /dev/null
+++ b/prisma/migrations/20240531045514_uniq_certificate_on_share/migration.sql
@@ -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");
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 011a8c46a..6c9cb627e 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -659,6 +659,7 @@ model Share {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
+ @@unique([companyId, certificateId])
@@index([companyId])
@@index([shareClassId])
@@index([stakeholderId])
diff --git a/src/app/(authenticated)/(dashboard)/[publicId]/securities/options/page.tsx b/src/app/(authenticated)/(dashboard)/[publicId]/securities/options/page.tsx
index b86612e22..976ff3e92 100644
--- a/src/app/(authenticated)/(dashboard)/[publicId]/securities/options/page.tsx
+++ b/src/app/(authenticated)/(dashboard)/[publicId]/securities/options/page.tsx
@@ -24,15 +24,7 @@ const OptionsPage = async () => {
>
- }
+ subtitle="Please fill in the details to create an option."
trigger={
+ }
+ />
+
+ );
+ }
+
return (
- }
- title="Work in progress."
- subtitle="This page is not yet available."
- >
-
-
+
+
+
+
Shares
+
+ Issue shares to stakeholders
+
+
+
+
+
+ Create a share
+
+ }
+ />
+
+
+
+
+
+
);
};
diff --git a/src/app/(authenticated)/(dashboard)/[publicId]/settings/company/page.tsx b/src/app/(authenticated)/(dashboard)/[publicId]/settings/company/page.tsx
index a11f4dee3..28cd82951 100644
--- a/src/app/(authenticated)/(dashboard)/[publicId]/settings/company/page.tsx
+++ b/src/app/(authenticated)/(dashboard)/[publicId]/settings/company/page.tsx
@@ -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",
diff --git a/src/components/common/LoadingSpinner.tsx b/src/components/common/LoadingSpinner.tsx
new file mode 100644
index 000000000..e28313e08
--- /dev/null
+++ b/src/components/common/LoadingSpinner.tsx
@@ -0,0 +1,31 @@
+import { cn } from "@/lib/utils";
+
+export interface ISVGProps extends React.SVGProps {
+ size?: number;
+ className?: string;
+}
+
+export const LoadingSpinner = ({
+ size = 24,
+ className,
+ ...props
+}: ISVGProps) => {
+ return (
+
+ );
+};
diff --git a/src/components/onboarding/company-form.tsx b/src/components/onboarding/company-form.tsx
index 3731f29fe..a1932bf1c 100644
--- a/src/components/onboarding/company-form.tsx
+++ b/src/components/onboarding/company-form.tsx
@@ -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 ?? "",
},
},
});
diff --git a/src/components/securities/options/steps/vesting-details.tsx b/src/components/securities/options/steps/vesting-details.tsx
index 76dfe4dd3..e77e270e5 100644
--- a/src/components/securities/options/steps/vesting-details.tsx
+++ b/src/components/securities/options/steps/vesting-details.tsx
@@ -1,6 +1,5 @@
"use client";
-import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import {
Form,
@@ -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(),
@@ -46,20 +46,6 @@ interface VestingDetailsProps {
equityPlans: RouterOutputs["equityPlan"]["getPlans"];
}
-interface EmptySelectProps {
- title: string;
- description: string;
-}
-
-function EmptySelect({ title, description }: EmptySelectProps) {
- return (
-
- {title}
- {description}
-
- );
-}
-
export const VestingDetails = (props: VestingDetailsProps) => {
const { stakeholders, equityPlans } = props;
diff --git a/src/components/securities/shared/EmptySelect.tsx b/src/components/securities/shared/EmptySelect.tsx
new file mode 100644
index 000000000..23c87c452
--- /dev/null
+++ b/src/components/securities/shared/EmptySelect.tsx
@@ -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 (
+
+ {title}
+ {description}
+
+ );
+}
diff --git a/src/components/securities/shares/data.tsx b/src/components/securities/shares/data.tsx
new file mode 100644
index 000000000..31b00dba6
--- /dev/null
+++ b/src/components/securities/shares/data.tsx
@@ -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,
+}));
diff --git a/src/components/securities/shares/share-modal.tsx b/src/components/securities/shares/share-modal.tsx
new file mode 100644
index 000000000..cfebd90d0
--- /dev/null
+++ b/src/components/securities/shares/share-modal.tsx
@@ -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 ;
+}
+
+async function GeneralDetailsStep() {
+ const shareClasses = await api.shareClass.get.query();
+ return ;
+}
+
+export const ShareModal = (props: Omit) => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/components/securities/shares/share-table-toolbar.tsx b/src/components/securities/shares/share-table-toolbar.tsx
new file mode 100644
index 000000000..7836bd58c
--- /dev/null
+++ b/src/components/securities/shares/share-table-toolbar.tsx
@@ -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 (
+
+
+
+ table
+ .getColumn("stakeholderName")
+ ?.setFilterValue(event.target.value)
+ }
+ className="h-8 w-64"
+ />
+
+ {table.getColumn("status") && (
+
+ )}
+
+ {isFiltered && (
+ table.resetColumnFilters()}
+ />
+ )}
+
+
+
+
+ );
+}
diff --git a/src/components/securities/shares/share-table.tsx b/src/components/securities/shares/share-table.tsx
new file mode 100644
index 000000000..a55def1ef
--- /dev/null
+++ b/src/components/securities/shares/share-table.tsx
@@ -0,0 +1,394 @@
+"use client";
+
+import {
+ type ColumnDef,
+ type ColumnFiltersState,
+ type SortingState,
+ type VisibilityState,
+ getCoreRowModel,
+ getFacetedRowModel,
+ getFacetedUniqueValues,
+ getFilteredRowModel,
+ getPaginationRowModel,
+ getSortedRowModel,
+ useReactTable,
+} from "@tanstack/react-table";
+import * as React from "react";
+
+import { dayjsExt } from "@/common/dayjs";
+import { Checkbox } from "@/components/ui/checkbox";
+
+import { Avatar, AvatarImage } from "@/components/ui/avatar";
+import { DataTable } from "@/components/ui/data-table/data-table";
+import { DataTableBody } from "@/components/ui/data-table/data-table-body";
+import { DataTableContent } from "@/components/ui/data-table/data-table-content";
+import { DataTableHeader } from "@/components/ui/data-table/data-table-header";
+import { DataTablePagination } from "@/components/ui/data-table/data-table-pagination";
+import type { RouterOutputs } from "@/trpc/shared";
+
+import { Button } from "@/components/ui/button";
+import { SortButton } from "@/components/ui/data-table/data-table-buttons";
+import {
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+} from "@/components/ui/dropdown-menu";
+import { formatCurrency, formatNumber } from "@/lib/utils";
+import { getPresignedGetUrl } from "@/server/file-uploads";
+import { api } from "@/trpc/react";
+import {
+ DropdownMenu,
+ DropdownMenuTrigger,
+} from "@radix-ui/react-dropdown-menu";
+import { RiFileDownloadLine, RiMoreLine } from "@remixicon/react";
+import { useRouter } from "next/navigation";
+import { toast } from "sonner";
+import { ShareTableToolbar } from "./share-table-toolbar";
+
+type Share = RouterOutputs["securities"]["getShares"]["data"];
+
+type SharesType = {
+ shares: Share;
+};
+
+const humanizeShareStatus = (type: string) => {
+ switch (type) {
+ case "ACTIVE":
+ return "Active";
+ case "DRAFT":
+ return "Draft";
+ case "SIGNED":
+ return "Signed";
+ case "PENDING":
+ return "Pending";
+ default:
+ return "";
+ }
+};
+
+const StatusColorProvider = (type: string) => {
+ switch (type) {
+ case "ACTIVE":
+ return "bg-green-50 text-green-600 ring-green-600/20";
+ case "DRAFT":
+ return "bg-yellow-50 text-yellow-600 ring-yellow-600/20";
+ case "SIGNED":
+ return "bg-blue-50 text-blue-600 ring-blue-600/20";
+ case "PENDING":
+ return "bg-gray-50 text-gray-600 ring-gray-600/20";
+ default:
+ return "";
+ }
+};
+
+export const columns: ColumnDef[] = [
+ {
+ id: "select",
+ header: ({ table }) => (
+ table.toggleAllPageRowsSelected(!!value)}
+ aria-label="Select all"
+ />
+ ),
+ cell: ({ row }) => (
+ row.toggleSelected(!!value)}
+ aria-label="Select row"
+ />
+ ),
+ enableSorting: false,
+ enableHiding: false,
+ },
+ {
+ id: "stakeholderName",
+ accessorKey: "stakeholder.name",
+ header: ({ column }) => {
+ return (
+ column.toggleSorting(column.getIsSorted() === "asc")}
+ />
+ );
+ },
+ cell: ({ row }) => (
+
+
+
+
+
+
{row?.original?.stakeholder?.name}
+
+
+ ),
+ },
+ {
+ id: "status",
+ accessorKey: "status",
+ header: ({ column }) => (
+ column.toggleSorting(column?.getIsSorted() === "asc")}
+ />
+ ),
+ cell: ({ row }) => {
+ const status = row.original?.status;
+ return (
+
+ {humanizeShareStatus(status)}
+
+ );
+ },
+ },
+ {
+ id: "shareClass",
+ accessorKey: "shareClass.classType",
+
+ header: ({ column }) => (
+ column.toggleSorting(column.getIsSorted() === "asc")}
+ />
+ ),
+ cell: ({ row }) => (
+ {row.original.shareClass.classType}
+ ),
+ },
+ {
+ id: "quantity",
+ accessorKey: "quantity",
+ header: ({ column }) => (
+
+ column.toggleSorting(column.getIsSorted() === "asc")}
+ />
+
+ ),
+ cell: ({ row }) => {
+ const quantity = row.original.quantity;
+ return (
+
+ {quantity ? formatNumber(quantity) : null}
+
+ );
+ },
+ },
+ {
+ id: "pricePerShare",
+ accessorKey: "pricePerShare",
+ header: ({ column }) => (
+
+ column.toggleSorting(column.getIsSorted() === "asc")}
+ />
+
+ ),
+ cell: ({ row }) => {
+ const price = row.original.pricePerShare;
+ return (
+
+ {price ? formatCurrency(price, "USD") : null}
+
+ );
+ },
+ },
+ {
+ id: "issueDate",
+ accessorKey: "issueDate",
+ header: ({ column }) => (
+ column.toggleSorting(column.getIsSorted() === "asc")}
+ />
+ ),
+ cell: ({ row }) => (
+
+ {dayjsExt(row.original.issueDate).format("DD/MM/YYYY")}
+
+ ),
+ },
+ {
+ id: "boardApprovalDate",
+ accessorKey: "boardApprovalDate",
+ header: ({ column }) => (
+
+ column.toggleSorting(column.getIsSorted() === "asc")}
+ />
+
+ ),
+ cell: ({ row }) => (
+
+ {dayjsExt(row.original.boardApprovalDate).format("DD/MM/YYYY")}
+
+ ),
+ },
+ {
+ id: "Documents",
+ enableHiding: false,
+ header: ({ column }) => {
+ return (
+ column.toggleSorting(column.getIsSorted() === "asc")}
+ />
+ );
+ },
+ cell: ({ row }) => {
+ const documents = row?.original?.documents;
+
+ const openFileOnTab = async (key: string) => {
+ const fileUrl = await getPresignedGetUrl(key);
+ window.open(fileUrl.url, "_blank");
+ };
+
+ return (
+
+
+
+
+
+
+
+ Documents
+ {documents?.map((doc) => (
+ {
+ await openFileOnTab(doc.bucket.key);
+ }}
+ >
+
+ {doc.name.slice(0, 12)}
+
+ {doc?.uploader?.user?.name}
+
+
+ ))}
+
+
+ );
+ },
+ },
+ {
+ id: "actions",
+ enableHiding: false,
+ cell: ({ row }) => {
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const router = useRouter();
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const share = row.original;
+
+ const deleteShareMutation = api.securities.deleteShare.useMutation({
+ onSuccess: () => {
+ toast.success("🎉 Successfully deleted the stakeholder");
+ router.refresh();
+ },
+ onError: () => {
+ toast.error("Failed deleting the share");
+ },
+ });
+
+ const updateAction = "Update Share";
+ const deleteAction = "Delete Share";
+
+ const handleDeleteShare = async () => {
+ await deleteShareMutation.mutateAsync({ shareId: share.id });
+ };
+
+ return (
+
+
+
+
+
+
+
+ Actions
+
+ {updateAction}
+
+ {deleteAction}
+
+
+
+ );
+ },
+ },
+];
+
+const ShareTable = ({ shares }: SharesType) => {
+ const [sorting, setSorting] = React.useState([]);
+ const [columnFilters, setColumnFilters] = React.useState(
+ [],
+ );
+ const [columnVisibility, setColumnVisibility] =
+ React.useState({});
+ const [rowSelection, setRowSelection] = React.useState({});
+
+ const table = useReactTable({
+ data: shares ?? [],
+ columns: columns,
+ enableRowSelection: true,
+ onRowSelectionChange: setRowSelection,
+ onSortingChange: setSorting,
+ onColumnFiltersChange: setColumnFilters,
+ onColumnVisibilityChange: setColumnVisibility,
+ getCoreRowModel: getCoreRowModel(),
+ getFilteredRowModel: getFilteredRowModel(),
+ getPaginationRowModel: getPaginationRowModel(),
+ getSortedRowModel: getSortedRowModel(),
+ getFacetedRowModel: getFacetedRowModel(),
+ getFacetedUniqueValues: getFacetedUniqueValues(),
+ state: {
+ sorting,
+ columnFilters,
+ columnVisibility,
+ rowSelection,
+ },
+ });
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default ShareTable;
diff --git a/src/components/securities/shares/steps/contribution-details.tsx b/src/components/securities/shares/steps/contribution-details.tsx
new file mode 100644
index 000000000..4177447e2
--- /dev/null
+++ b/src/components/securities/shares/steps/contribution-details.tsx
@@ -0,0 +1,226 @@
+"use client";
+
+import { Button } from "@/components/ui/button";
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import {
+ StepperModalFooter,
+ StepperPrev,
+ useStepper,
+} from "@/components/ui/stepper";
+import { useAddShareFormValues } from "@/providers/add-share-form-provider";
+import type { RouterOutputs } from "@/trpc/shared";
+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";
+
+interface ContributionDetailsProps {
+ stakeholders: RouterOutputs["stakeholder"]["getStakeholders"];
+}
+
+const formSchema = z.object({
+ stakeholderId: z.string(),
+ capitalContribution: z.coerce.number().min(0),
+ ipContribution: z.coerce.number().min(0),
+ debtCancelled: z.coerce.number().min(0),
+ otherContributions: z.coerce.number().min(0),
+});
+
+type TFormSchema = z.infer;
+
+export const ContributionDetails = ({
+ stakeholders,
+}: ContributionDetailsProps) => {
+ const form = useForm({ resolver: zodResolver(formSchema) });
+ const { next } = useStepper();
+ const { setValue } = useAddShareFormValues();
+
+ const handleSubmit = (data: TFormSchema) => {
+ console.log({ data });
+ setValue(data);
+ next();
+ };
+ return (
+
+
+ );
+};
diff --git a/src/components/securities/shares/steps/documents.tsx b/src/components/securities/shares/steps/documents.tsx
new file mode 100644
index 000000000..706f8e404
--- /dev/null
+++ b/src/components/securities/shares/steps/documents.tsx
@@ -0,0 +1,108 @@
+"use client";
+
+import { uploadFile } from "@/common/uploads";
+import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
+import { Button } from "@/components/ui/button";
+import { DialogClose } from "@/components/ui/dialog";
+import {
+ StepperModalFooter,
+ StepperPrev,
+ useStepper,
+} from "@/components/ui/stepper";
+import Uploader from "@/components/ui/uploader";
+import { invariant } from "@/lib/error";
+import { useAddShareFormValues } from "@/providers/add-share-form-provider";
+import { api } from "@/trpc/react";
+import { useSession } from "next-auth/react";
+import { useRouter } from "next/navigation";
+import { useState } from "react";
+import type { FileWithPath } from "react-dropzone";
+import { toast } from "sonner";
+
+export const Documents = () => {
+ const router = useRouter();
+ const { data: session } = useSession();
+ const { value } = useAddShareFormValues();
+ const { reset } = useStepper();
+ const [documentsList, setDocumentsList] = useState([]);
+ const { mutateAsync: handleBucketUpload } = api.bucket.create.useMutation();
+ const { mutateAsync: addShareMutation } =
+ api.securities.addShares.useMutation({
+ onSuccess: ({ success }) => {
+ invariant(session, "session not found");
+ if (success) {
+ toast.success("Successfully issued the share.");
+ router.refresh();
+ reset();
+ } else {
+ toast.error("Failed issuing the share. Please try again.");
+ }
+ },
+ });
+ const handleComplete = async () => {
+ invariant(session, "session not found");
+ const uploadedDocuments: { name: string; bucketId: string }[] = [];
+ for (const document of documentsList) {
+ const { key, mimeType, name, size } = await uploadFile(document, {
+ identifier: session.user.companyPublicId,
+ keyPrefix: "sharesDocs",
+ });
+ const { id: bucketId, name: docName } = await handleBucketUpload({
+ key,
+ mimeType,
+ name,
+ size,
+ });
+ uploadedDocuments.push({ bucketId, name: docName });
+ }
+ await addShareMutation({ ...value, documents: uploadedDocuments });
+ };
+ return (
+
+
+
{
+ setDocumentsList(bucketData);
+ }}
+ accept={{
+ "application/pdf": [".pdf"],
+ }}
+ />
+ {documentsList?.length ? (
+
+
+ {documentsList.length > 1
+ ? `${documentsList.length} documents uploaded`
+ : `${documentsList.length} document uploaded`}
+
+
+ You can submit the form to proceed.
+
+
+ ) : (
+
+ 0 document uploaded
+
+ Please upload necessary documents to continue.
+
+
+ )}
+
+
+ Back
+
+
+
+
+
+ );
+};
diff --git a/src/components/securities/shares/steps/general-details.tsx b/src/components/securities/shares/steps/general-details.tsx
new file mode 100644
index 000000000..d65caeeda
--- /dev/null
+++ b/src/components/securities/shares/steps/general-details.tsx
@@ -0,0 +1,308 @@
+"use client";
+import { EmptySelect } from "@/components/securities/shared/EmptySelect";
+import { Button } from "@/components/ui/button";
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import {
+ MultiSelector,
+ MultiSelectorContent,
+ MultiSelectorInput,
+ MultiSelectorItem,
+ MultiSelectorList,
+ MultiSelectorTrigger,
+} from "@/components/ui/simple-multi-select";
+import {
+ StepperModalFooter,
+ StepperPrev,
+ useStepper,
+} from "@/components/ui/stepper";
+import { VestingSchedule } from "@/lib/vesting";
+import {
+ SecuritiesStatusEnum,
+ ShareLegendsEnum,
+ VestingScheduleEnum,
+} from "@/prisma/enums";
+import { useAddShareFormValues } from "@/providers/add-share-form-provider";
+import type { RouterOutputs } from "@/trpc/shared";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useForm } from "react-hook-form";
+import { NumericFormat } from "react-number-format";
+import { z } from "zod";
+
+export const humanizeCompanyLegends = (type: string): string => {
+ switch (type) {
+ case ShareLegendsEnum.US_SECURITIES_ACT:
+ return "US Securities Act";
+ case ShareLegendsEnum.TRANSFER_RESTRICTIONS:
+ return "Transfer Restrictions";
+ case ShareLegendsEnum.SALE_AND_ROFR:
+ return "Sale and ROFR";
+ default:
+ return "";
+ }
+};
+
+const formSchema = z.object({
+ shareClassId: z.string(),
+ certificateId: z.string(),
+ status: z.nativeEnum(SecuritiesStatusEnum),
+ quantity: z.coerce.number().min(0),
+ vestingSchedule: z.nativeEnum(VestingScheduleEnum),
+ companyLegends: z.nativeEnum(ShareLegendsEnum).array(),
+ pricePerShare: z.coerce.number().min(0),
+});
+
+type TFormSchema = z.infer;
+
+type ShareClasses = RouterOutputs["shareClass"]["get"];
+
+interface GeneralDetailsProps {
+ shareClasses: ShareClasses;
+}
+
+export const GeneralDetails = ({ shareClasses }: GeneralDetailsProps) => {
+ const form = useForm({
+ resolver: zodResolver(formSchema),
+ });
+ const { next } = useStepper();
+ const { setValue } = useAddShareFormValues();
+
+ const status = Object.values(SecuritiesStatusEnum);
+ const vestingSchedule = Object.values(VestingScheduleEnum);
+ const companyLegends = Object.values(ShareLegendsEnum);
+
+ const handleSubmit = (data: TFormSchema) => {
+ setValue(data);
+ next();
+ };
+
+ return (
+
+
+ );
+};
diff --git a/src/components/securities/shares/steps/relevant-dates.tsx b/src/components/securities/shares/steps/relevant-dates.tsx
new file mode 100644
index 000000000..e1553f3d4
--- /dev/null
+++ b/src/components/securities/shares/steps/relevant-dates.tsx
@@ -0,0 +1,121 @@
+"use client";
+
+import { Button } from "@/components/ui/button";
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
+import {
+ StepperModalFooter,
+ StepperPrev,
+ useStepper,
+} from "@/components/ui/stepper";
+import { useAddShareFormValues } from "@/providers/add-share-form-provider";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useForm } from "react-hook-form";
+import { z } from "zod";
+
+const formSchema = z.object({
+ boardApprovalDate: z.string().date(),
+ rule144Date: z.string().date(),
+ issueDate: z.string().date(),
+ vestingStartDate: z.string().date(),
+});
+
+type TFormSchema = z.infer;
+
+export const RelevantDates = () => {
+ const form = useForm({ resolver: zodResolver(formSchema) });
+ const { next } = useStepper();
+ const { setValue } = useAddShareFormValues();
+
+ const handleSubmit = (data: TFormSchema) => {
+ setValue(data);
+ next();
+ };
+ return (
+
+
+ );
+};
diff --git a/src/components/ui/simple-multi-select.tsx b/src/components/ui/simple-multi-select.tsx
new file mode 100644
index 000000000..4af827845
--- /dev/null
+++ b/src/components/ui/simple-multi-select.tsx
@@ -0,0 +1,315 @@
+"use client";
+
+import { Badge } from "@/components/ui/badge";
+import {
+ Command,
+ CommandEmpty,
+ CommandItem,
+ CommandList,
+} from "@/components/ui/command";
+import { cn } from "@/lib/utils";
+import { RiCheckLine, RiCloseLine } from "@remixicon/react";
+import { Command as CommandPrimitive } from "cmdk";
+// import { X as RemoveIcon, Ri } from "lucide-react";
+import React, {
+ KeyboardEvent,
+ createContext,
+ forwardRef,
+ useCallback,
+ useContext,
+ useState,
+} from "react";
+
+type MultiSelectorProps = {
+ values: string[];
+ onValuesChange: (value: string[]) => void;
+ loop?: boolean;
+} & React.ComponentPropsWithoutRef;
+
+interface MultiSelectContextProps {
+ value: string[];
+ onValueChange: (value: any) => void;
+ open: boolean;
+ setOpen: (value: boolean) => void;
+ inputValue: string;
+ setInputValue: React.Dispatch>;
+ activeIndex: number;
+ setActiveIndex: React.Dispatch>;
+}
+
+const MultiSelectContext = createContext(null);
+
+const useMultiSelect = () => {
+ const context = useContext(MultiSelectContext);
+ if (!context) {
+ throw new Error("useMultiSelect must be used within MultiSelectProvider");
+ }
+ return context;
+};
+
+const MultiSelector = ({
+ values: value,
+ onValuesChange: onValueChange,
+ loop = false,
+ className,
+ children,
+ dir,
+ ...props
+}: MultiSelectorProps) => {
+ const [inputValue, setInputValue] = useState("");
+ const [open, setOpen] = useState(false);
+ const [activeIndex, setActiveIndex] = useState(-1);
+
+ const onValueChangeHandler = useCallback(
+ (val: string) => {
+ if (value?.includes(val)) {
+ onValueChange(value?.filter((item) => item !== val));
+ } else {
+ if (value?.length) {
+ onValueChange([...value, val]);
+ } else {
+ onValueChange([val]);
+ }
+ }
+ },
+ [value],
+ );
+
+ // TODO : change from else if use to switch case statement
+
+ const handleKeyDown = useCallback(
+ (e: KeyboardEvent) => {
+ const moveNext = () => {
+ const nextIndex = activeIndex + 1;
+ setActiveIndex(
+ nextIndex > value.length - 1 ? (loop ? 0 : -1) : nextIndex,
+ );
+ };
+
+ const movePrev = () => {
+ const prevIndex = activeIndex - 1;
+ setActiveIndex(prevIndex < 0 ? value.length - 1 : prevIndex);
+ };
+
+ if ((e.key === "Backspace" || e.key === "Delete") && value.length > 0) {
+ if (inputValue.length === 0) {
+ if (activeIndex !== -1 && activeIndex < value.length) {
+ onValueChange(value.filter((item) => item !== value[activeIndex]));
+ const newIndex = activeIndex - 1 < 0 ? 0 : activeIndex - 1;
+ setActiveIndex(newIndex);
+ } else {
+ onValueChange(
+ value.filter((item) => item !== value[value.length - 1]),
+ );
+ }
+ }
+ } else if (e.key === "Enter") {
+ setOpen(true);
+ } else if (e.key === "Escape") {
+ if (activeIndex !== -1) {
+ setActiveIndex(-1);
+ } else {
+ setOpen(false);
+ }
+ } else if (dir === "rtl") {
+ if (e.key === "ArrowRight") {
+ movePrev();
+ } else if (e.key === "ArrowLeft" && (activeIndex !== -1 || loop)) {
+ moveNext();
+ }
+ } else {
+ if (e.key === "ArrowLeft") {
+ movePrev();
+ } else if (e.key === "ArrowRight" && (activeIndex !== -1 || loop)) {
+ moveNext();
+ }
+ }
+ },
+ [value, inputValue, activeIndex, loop],
+ );
+
+ return (
+
+
+ {children}
+
+
+ );
+};
+
+const MultiSelectorTrigger = forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, children, ...props }, ref) => {
+ const { value, onValueChange, activeIndex } = useMultiSelect();
+
+ const mousePreventDefault = useCallback((e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ }, []);
+
+ return (
+
+ {value?.map((item, index) => (
+
+ {item}
+
+
+ ))}
+ {children}
+
+ );
+});
+
+MultiSelectorTrigger.displayName = "MultiSelectorTrigger";
+
+const MultiSelectorInput = forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => {
+ const { setOpen, inputValue, setInputValue, activeIndex, setActiveIndex } =
+ useMultiSelect();
+ return (
+ setOpen(false)}
+ onFocus={() => setOpen(true)}
+ onClick={() => setActiveIndex(-1)}
+ className={cn(
+ "ml-2 bg-transparent outline-none placeholder:text-muted-foreground flex-1",
+ className,
+ activeIndex !== -1 && "caret-transparent",
+ )}
+ />
+ );
+});
+
+MultiSelectorInput.displayName = "MultiSelectorInput";
+
+const MultiSelectorContent = forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ children }, ref) => {
+ const { open } = useMultiSelect();
+ return (
+
+ {open && children}
+
+ );
+});
+
+MultiSelectorContent.displayName = "MultiSelectorContent";
+
+const MultiSelectorList = forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children }, ref) => {
+ return (
+
+ {children}
+
+ No results found
+
+
+ );
+});
+
+MultiSelectorList.displayName = "MultiSelectorList";
+
+const MultiSelectorItem = forwardRef<
+ React.ElementRef,
+ { value: string } & React.ComponentPropsWithoutRef<
+ typeof CommandPrimitive.Item
+ >
+>(({ className, value, children, ...props }, ref) => {
+ const { value: Options, onValueChange, setInputValue } = useMultiSelect();
+
+ const mousePreventDefault = useCallback((e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ }, []);
+
+ const isIncluded = Options?.includes(value);
+ return (
+ {
+ onValueChange(value);
+ setInputValue("");
+ }}
+ className={cn(
+ "rounded-md cursor-pointer px-2 py-1 transition-colors flex justify-between ",
+ className,
+ isIncluded && "opacity-50 cursor-default",
+ props.disabled && "opacity-50 cursor-not-allowed",
+ )}
+ onMouseDown={mousePreventDefault}
+ >
+ {children}
+ {isIncluded && }
+
+ );
+});
+
+MultiSelectorItem.displayName = "MultiSelectorItem";
+
+export {
+ MultiSelector,
+ MultiSelectorContent,
+ MultiSelectorInput,
+ MultiSelectorItem,
+ MultiSelectorList,
+ MultiSelectorTrigger,
+};
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index d2a5a5e86..d7e74c439 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -9,7 +9,7 @@ export function cn(...inputs: ClassValue[]) {
export function getFileSizeSuffix(bytes: number): string {
const suffixes = ["", "K", "M", "G", "T"];
const magnitude = Math.floor(Math.log2(bytes) / 10);
- const suffix = suffixes[magnitude] + "B";
+ const suffix = `${suffixes[magnitude]}B`;
return suffix;
}
@@ -71,3 +71,14 @@ export function compareFormDataWithInitial>(
return isChanged;
}
+
+export function formatNumber(value: number): string {
+ return new Intl.NumberFormat("en-US").format(value);
+}
+
+export function formatCurrency(value: number, currency: "USD") {
+ return new Intl.NumberFormat("en-US", {
+ style: "currency",
+ currency: currency,
+ }).format(value);
+}
diff --git a/src/providers/add-share-form-provider.tsx b/src/providers/add-share-form-provider.tsx
new file mode 100644
index 000000000..ad15b585d
--- /dev/null
+++ b/src/providers/add-share-form-provider.tsx
@@ -0,0 +1,45 @@
+"use client";
+
+import type { TypeZodAddShareMutationSchema } from "@/trpc/routers/securities-router/schema";
+import {
+ type Dispatch,
+ type ReactNode,
+ createContext,
+ useContext,
+ useReducer,
+} from "react";
+
+type TFormValue = TypeZodAddShareMutationSchema;
+
+interface AddShareFormProviderProps {
+ children: ReactNode;
+}
+
+const AddShareFormProviderContext = createContext<{
+ value: TFormValue;
+ setValue: Dispatch>;
+} | null>(null);
+
+export function AddShareFormProvider({ children }: AddShareFormProviderProps) {
+ const [value, setValue] = useReducer(
+ (data: TFormValue, partialData: Partial) => ({
+ ...data,
+ ...partialData,
+ }),
+ {} as TFormValue,
+ );
+
+ return (
+
+ {children}
+
+ );
+}
+
+export const useAddShareFormValues = () => {
+ const data = useContext(AddShareFormProviderContext);
+ if (!data) {
+ throw new Error("useAddShareFormValues shouldn't be null");
+ }
+ return data;
+};
diff --git a/src/server/audit/schema.ts b/src/server/audit/schema.ts
index 7b0e9ac20..43768caad 100644
--- a/src/server/audit/schema.ts
+++ b/src/server/audit/schema.ts
@@ -31,6 +31,10 @@ export const AuditSchema = z.object({
"option.created",
"option.deleted",
+ "share.created",
+ "share.updated",
+ "share.deleted",
+
"safe.created",
"safe.imported",
"safe.sent",
@@ -49,7 +53,14 @@ export const AuditSchema = z.object({
target: z.array(
z.object({
- type: z.enum(["user", "company", "document", "option", "documentShare"]),
+ type: z.enum([
+ "user",
+ "company",
+ "document",
+ "option",
+ "documentShare",
+ "share",
+ ]),
id: z.string().optional().nullable(),
}),
),
diff --git a/src/trpc/routers/company-router/router.ts b/src/trpc/routers/company-router/router.ts
index abcb847b1..10654dedc 100644
--- a/src/trpc/routers/company-router/router.ts
+++ b/src/trpc/routers/company-router/router.ts
@@ -30,6 +30,7 @@ export const companyRouter = createTRPCRouter({
city: true,
zipcode: true,
streetAddress: true,
+ country: true,
logo: true,
},
},
diff --git a/src/trpc/routers/onboarding-router/schema.ts b/src/trpc/routers/onboarding-router/schema.ts
index 5198c6d6d..e2def1311 100644
--- a/src/trpc/routers/onboarding-router/schema.ts
+++ b/src/trpc/routers/onboarding-router/schema.ts
@@ -47,9 +47,12 @@ export const ZodCompanyMutationSchema = z.object({
zipcode: z.string().min(1, {
message: "Zipcode is required",
}),
- country: z.string().min(1, {
- message: "Country is required",
- }),
+ country: z
+ .string()
+ .min(1, {
+ message: "Country is required",
+ })
+ .default("US"),
logo: z.string().min(1).optional(),
}),
});
diff --git a/src/trpc/routers/securities-router/procedures/add-share.ts b/src/trpc/routers/securities-router/procedures/add-share.ts
new file mode 100644
index 000000000..644528250
--- /dev/null
+++ b/src/trpc/routers/securities-router/procedures/add-share.ts
@@ -0,0 +1,86 @@
+import { generatePublicId } from "@/common/id";
+import { Audit } from "@/server/audit";
+import { checkMembership } from "@/server/auth";
+import { withAuth } from "@/trpc/api/trpc";
+import { ZodAddShareMutationSchema } from "../schema";
+
+export const addShareProcedure = withAuth
+ .input(ZodAddShareMutationSchema)
+ .mutation(async ({ ctx, input }) => {
+ console.log({ input }, "#############");
+
+ const { userAgent, requestIp } = ctx;
+
+ try {
+ const user = ctx.session.user;
+ const documents = input.documents;
+
+ await ctx.db.$transaction(async (tx) => {
+ const { companyId } = await checkMembership({
+ session: ctx.session,
+ tx,
+ });
+
+ const data = {
+ companyId,
+ stakeholderId: input.stakeholderId,
+ shareClassId: input.shareClassId,
+ status: input.status,
+ certificateId: input.certificateId,
+ quantity: input.quantity,
+ pricePerShare: input.pricePerShare,
+ capitalContribution: input.capitalContribution,
+ ipContribution: input.ipContribution,
+ debtCancelled: input.debtCancelled,
+ otherContributions: input.otherContributions,
+ vestingSchedule: input.vestingSchedule,
+ companyLegends: input.companyLegends,
+ issueDate: new Date(input.issueDate),
+ rule144Date: new Date(input.rule144Date),
+ vestingStartDate: new Date(input.vestingStartDate),
+ boardApprovalDate: new Date(input.boardApprovalDate),
+ };
+ const share = await tx.share.create({ data });
+
+ const bulkDocuments = documents.map((doc) => ({
+ companyId,
+ uploaderId: user.memberId,
+ publicId: generatePublicId(),
+ name: doc.name,
+ bucketId: doc.bucketId,
+ shareId: share.id,
+ }));
+
+ await tx.document.createMany({
+ data: bulkDocuments,
+ skipDuplicates: true,
+ });
+
+ await Audit.create(
+ {
+ action: "share.created",
+ companyId: user.companyId,
+ actor: { type: "user", id: user.id },
+ context: {
+ userAgent,
+ requestIp,
+ },
+ target: [{ type: "share", id: share.id }],
+ summary: `${user.name} added share for stakeholder ${input.stakeholderId}`,
+ },
+ tx,
+ );
+ });
+
+ return {
+ success: true,
+ message: "🎉 Successfully added a share",
+ };
+ } catch (error) {
+ console.error("Error adding shares: ", error);
+ return {
+ success: false,
+ message: "Please use unique Certificate Id.",
+ };
+ }
+ });
diff --git a/src/trpc/routers/securities-router/procedures/delete-share.ts b/src/trpc/routers/securities-router/procedures/delete-share.ts
new file mode 100644
index 000000000..e1f2154a7
--- /dev/null
+++ b/src/trpc/routers/securities-router/procedures/delete-share.ts
@@ -0,0 +1,75 @@
+import { Audit } from "@/server/audit";
+import { checkMembership } from "@/server/auth";
+import { withAuth, type withAuthTrpcContextType } from "@/trpc/api/trpc";
+import {
+ type TypeZodDeleteShareMutationSchema,
+ ZodDeleteShareMutationSchema,
+} from "../schema";
+
+export const deleteShareProcedure = withAuth
+ .input(ZodDeleteShareMutationSchema)
+ .mutation(async (args) => {
+ return await deleteShareHandler(args);
+ });
+
+interface deleteShareHandlerOptions {
+ input: TypeZodDeleteShareMutationSchema;
+ ctx: withAuthTrpcContextType;
+}
+
+export async function deleteShareHandler({
+ ctx: { db, session, requestIp, userAgent },
+ input,
+}: deleteShareHandlerOptions) {
+ const user = session.user;
+ const { shareId } = input;
+ try {
+ await db.$transaction(async (tx) => {
+ const { companyId } = await checkMembership({ session, tx });
+
+ const share = await tx.share.delete({
+ where: {
+ id: shareId,
+ companyId,
+ },
+ select: {
+ id: true,
+ stakeholder: {
+ select: {
+ id: true,
+ name: true,
+ },
+ },
+ company: {
+ select: {
+ name: true,
+ },
+ },
+ },
+ });
+
+ await Audit.create(
+ {
+ action: "share.deleted",
+ companyId: user.companyId,
+ actor: { type: "user", id: session.user.id },
+ context: {
+ requestIp,
+ userAgent,
+ },
+ target: [{ type: "share", id: share.id }],
+ summary: `${user.name} deleted share of stakholder ${share.stakeholder.name}`,
+ },
+ tx,
+ );
+ });
+
+ return { success: true };
+ } catch (err) {
+ console.error(err);
+ return {
+ success: false,
+ message: "Oops, something went wrong while deleting option.",
+ };
+ }
+}
diff --git a/src/trpc/routers/securities-router/procedures/get-shares.ts b/src/trpc/routers/securities-router/procedures/get-shares.ts
new file mode 100644
index 000000000..ead84143e
--- /dev/null
+++ b/src/trpc/routers/securities-router/procedures/get-shares.ts
@@ -0,0 +1,71 @@
+import { checkMembership } from "@/server/auth";
+import { withAuth } from "@/trpc/api/trpc";
+
+export const getSharesProcedure = withAuth.query(
+ async ({ ctx: { db, session } }) => {
+ const data = await db.$transaction(async (tx) => {
+ const { companyId } = await checkMembership({ session, tx });
+
+ const shares = await tx.share.findMany({
+ where: {
+ companyId,
+ },
+ select: {
+ id: true,
+ certificateId: true,
+ quantity: true,
+ pricePerShare: true,
+ capitalContribution: true,
+ ipContribution: true,
+ debtCancelled: true,
+ otherContributions: true,
+ vestingSchedule: true,
+ companyLegends: true,
+ status: true,
+
+ issueDate: true,
+ rule144Date: true,
+ vestingStartDate: true,
+ boardApprovalDate: true,
+ stakeholder: {
+ select: {
+ name: true,
+ },
+ },
+ shareClass: {
+ select: {
+ classType: true,
+ },
+ },
+ documents: {
+ select: {
+ id: true,
+ name: true,
+ uploader: {
+ select: {
+ user: {
+ select: {
+ name: true,
+ image: true,
+ },
+ },
+ },
+ },
+ bucket: {
+ select: {
+ key: true,
+ mimeType: true,
+ size: true,
+ },
+ },
+ },
+ },
+ },
+ });
+
+ return shares;
+ });
+
+ return { data };
+ },
+);
diff --git a/src/trpc/routers/securities-router/router.ts b/src/trpc/routers/securities-router/router.ts
index c352cc7a2..cf6d1ab1b 100644
--- a/src/trpc/routers/securities-router/router.ts
+++ b/src/trpc/routers/securities-router/router.ts
@@ -1,10 +1,16 @@
import { createTRPCRouter } from "@/trpc/api/trpc";
import { addOptionProcedure } from "./procedures/add-option";
+import { addShareProcedure } from "./procedures/add-share";
import { deleteOptionProcedure } from "./procedures/delete-option";
+import { deleteShareProcedure } from "./procedures/delete-share";
import { getOptionsProcedure } from "./procedures/get-options";
+import { getSharesProcedure } from "./procedures/get-shares";
export const securitiesRouter = createTRPCRouter({
getOptions: getOptionsProcedure,
addOptions: addOptionProcedure,
deleteOption: deleteOptionProcedure,
+ getShares: getSharesProcedure,
+ addShares: addShareProcedure,
+ deleteShare: deleteShareProcedure,
});
diff --git a/src/trpc/routers/securities-router/schema.ts b/src/trpc/routers/securities-router/schema.ts
index 140fa6b19..03a8554af 100644
--- a/src/trpc/routers/securities-router/schema.ts
+++ b/src/trpc/routers/securities-router/schema.ts
@@ -1,10 +1,13 @@
import {
OptionStatusEnum,
OptionTypeEnum,
+ ShareLegendsEnum,
VestingScheduleEnum,
} from "@/prisma/enums";
+import { SecuritiesStatusEnum } from "@prisma/client";
import { z } from "zod";
+// OPTIONS
export const ZodAddOptionMutationSchema = z.object({
id: z.string().optional(),
grantId: z.string(),
@@ -40,3 +43,42 @@ export const ZodDeleteOptionMutationSchema = z.object({
export type TypeZodDeleteOptionMutationSchema = z.infer<
typeof ZodDeleteOptionMutationSchema
>;
+
+// SHARES
+export const ZodAddShareMutationSchema = z.object({
+ id: z.string().optional().nullable(),
+ stakeholderId: z.string(),
+ shareClassId: z.string(),
+ certificateId: z.string(),
+ quantity: z.coerce.number().min(0),
+ pricePerShare: z.coerce.number().min(0),
+ capitalContribution: z.coerce.number().min(0),
+ ipContribution: z.coerce.number().min(0),
+ debtCancelled: z.coerce.number().min(0),
+ otherContributions: z.coerce.number().min(0),
+ status: z.nativeEnum(SecuritiesStatusEnum),
+ vestingSchedule: z.nativeEnum(VestingScheduleEnum),
+ companyLegends: z.nativeEnum(ShareLegendsEnum).array(),
+ issueDate: z.string().date(),
+ rule144Date: z.string().date(),
+ vestingStartDate: z.string().date(),
+ boardApprovalDate: z.string().date(),
+ documents: z.array(
+ z.object({
+ bucketId: z.string(),
+ name: z.string(),
+ }),
+ ),
+});
+
+export type TypeZodAddShareMutationSchema = z.infer<
+ typeof ZodAddShareMutationSchema
+>;
+
+export const ZodDeleteShareMutationSchema = z.object({
+ shareId: z.string(),
+});
+
+export type TypeZodDeleteShareMutationSchema = z.infer<
+ typeof ZodDeleteShareMutationSchema
+>;
diff --git a/src/trpc/routers/share-class/router.ts b/src/trpc/routers/share-class/router.ts
index 4e0690c1a..080895550 100644
--- a/src/trpc/routers/share-class/router.ts
+++ b/src/trpc/routers/share-class/router.ts
@@ -48,6 +48,7 @@ export const shareClassRouter = createTRPCRouter({
};
await tx.shareClass.create({ data });
+
await Audit.create(
{
action: "shareClass.created",
@@ -137,4 +138,26 @@ export const shareClassRouter = createTRPCRouter({
};
}
}),
+
+ get: withAuth.query(async ({ ctx: { db, session } }) => {
+ const shareClass = await db.$transaction(async (tx) => {
+ const { companyId } = await checkMembership({ session, tx });
+
+ return await tx.shareClass.findMany({
+ where: {
+ companyId,
+ },
+ select: {
+ id: true,
+ name: true,
+ company: {
+ select: {
+ name: true,
+ },
+ },
+ },
+ });
+ });
+ return shareClass;
+ }),
});
diff --git a/src/trpc/routers/stakeholder-router/procedures/get-stakeholders.ts b/src/trpc/routers/stakeholder-router/procedures/get-stakeholders.ts
index c1768827a..247a260c8 100644
--- a/src/trpc/routers/stakeholder-router/procedures/get-stakeholders.ts
+++ b/src/trpc/routers/stakeholder-router/procedures/get-stakeholders.ts
@@ -11,7 +11,13 @@ export const getStakeholdersProcedure = withAuth.query(async ({ ctx }) => {
where: {
companyId,
},
-
+ include: {
+ company: {
+ select: {
+ name: true,
+ },
+ },
+ },
orderBy: {
createdAt: "desc",
},