mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-18 13:15:39 +00:00
feat: renamed old frontend and fixed some minor bugs
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||
|
||||
import { BillingTabGroup } from "./components";
|
||||
|
||||
export const BillingPage = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="h-full bg-bunker-800">
|
||||
<Helmet>
|
||||
<title>{t("common.head-title", { title: t("billing.title") })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
</Helmet>
|
||||
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
|
||||
<div className="w-full max-w-7xl px-6">
|
||||
<div className="my-6">
|
||||
<p className="text-3xl font-semibold text-gray-200">{t("billing.title")}</p>
|
||||
<div />
|
||||
</div>
|
||||
<OrgPermissionCan
|
||||
passThrough={false}
|
||||
I={OrgPermissionActions.Read}
|
||||
a={OrgPermissionSubjects.Billing}
|
||||
>
|
||||
<BillingTabGroup />
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { CurrentPlanSection } from "./CurrentPlanSection";
|
||||
import { PreviewSection } from "./PreviewSection";
|
||||
|
||||
export const BillingCloudTab = () => {
|
||||
return (
|
||||
<div>
|
||||
<PreviewSection />
|
||||
<CurrentPlanSection />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
import { faCircleCheck, faCircleXmark, faFileInvoice } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import {
|
||||
EmptyState,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import { useGetOrgPlanTable } from "@app/hooks/api";
|
||||
|
||||
export const CurrentPlanSection = () => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { data, isPending } = useGetOrgPlanTable(currentOrg?.id ?? "");
|
||||
|
||||
const displayCell = (value: null | number | string | boolean) => {
|
||||
if (value === null) return "-";
|
||||
|
||||
if (typeof value === "boolean") {
|
||||
if (value) return <FontAwesomeIcon icon={faCircleCheck} color="#2ecc71" />;
|
||||
|
||||
return <FontAwesomeIcon icon={faCircleXmark} color="#e74c3c" />;
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<h2 className="mb-8 flex-1 text-xl font-semibold text-white">Current usage</h2>
|
||||
<TableContainer className="mt-4">
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="w-1/3">Feature</Th>
|
||||
<Th className="w-1/3">Allowed</Th>
|
||||
<Th className="w-1/3">Used</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{!isPending &&
|
||||
data &&
|
||||
data?.rows?.length > 0 &&
|
||||
data.rows.map(({ name, allowed, used }) => {
|
||||
return (
|
||||
<Tr key={`current-plan-row-${name}`} className="h-12">
|
||||
<Td>{name}</Td>
|
||||
<Td>{displayCell(allowed)}</Td>
|
||||
<Td>{used}</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
{isPending && <TableSkeleton columns={5} innerKey="invoices" />}
|
||||
{!isPending && data && data?.rows?.length === 0 && (
|
||||
<Tr>
|
||||
<Td colSpan={3}>
|
||||
<EmptyState title="No plan details found" icon={faFileInvoice} />
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import { Fragment } from "react";
|
||||
import { Tab } from "@headlessui/react";
|
||||
|
||||
import { Modal, ModalContent } from "@app/components/v2";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
import { ManagePlansTable } from "./ManagePlansTable";
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["managePlan"]>;
|
||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["managePlan"]>, state?: boolean) => void;
|
||||
};
|
||||
|
||||
export const ManagePlansModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.managePlan?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("managePlan", isOpen);
|
||||
}}
|
||||
>
|
||||
<ModalContent className="max-w-screen-lg" title="Infisical Cloud Plans">
|
||||
<Tab.Group>
|
||||
<Tab.List className="max-w-screen-lg border-b-2 border-mineshaft-600">
|
||||
<Tab as={Fragment}>
|
||||
{({ selected }) => (
|
||||
<button
|
||||
type="button"
|
||||
className={`p-4 ${
|
||||
selected ? "border-b-2 border-white text-white" : "text-mineshaft-400"
|
||||
} w-30 font-semibold outline-none`}
|
||||
>
|
||||
Bill monthly
|
||||
</button>
|
||||
)}
|
||||
</Tab>
|
||||
<Tab as={Fragment}>
|
||||
{({ selected }) => (
|
||||
<button
|
||||
type="button"
|
||||
className={`p-4 ${
|
||||
selected ? "border-b-2 border-white text-white" : "text-mineshaft-400"
|
||||
} w-30 font-semibold outline-none`}
|
||||
>
|
||||
Bill yearly
|
||||
</button>
|
||||
)}
|
||||
</Tab>
|
||||
</Tab.List>
|
||||
<Tab.Panels className="mt-4">
|
||||
<Tab.Panel>
|
||||
<ManagePlansTable billingCycle="monthly" />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<ManagePlansTable billingCycle="yearly" />
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
import { faCircleCheck, faCircleXmark, faFileInvoice } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import {
|
||||
Button,
|
||||
EmptyState,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization, useSubscription } from "@app/context";
|
||||
import { useCreateCustomerPortalSession, useGetOrgPlansTable } from "@app/hooks/api";
|
||||
|
||||
type Props = {
|
||||
billingCycle: "monthly" | "yearly";
|
||||
};
|
||||
|
||||
export const ManagePlansTable = ({ billingCycle }: Props) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { subscription } = useSubscription();
|
||||
const { data: tableData, isPending: isTableDataLoading } = useGetOrgPlansTable({
|
||||
organizationId: currentOrg?.id ?? "",
|
||||
billingCycle
|
||||
});
|
||||
const createCustomerPortalSession = useCreateCustomerPortalSession();
|
||||
|
||||
const displayCell = (value: null | number | string | boolean) => {
|
||||
if (value === null) return "Unlimited";
|
||||
|
||||
if (typeof value === "boolean") {
|
||||
if (value) return <FontAwesomeIcon icon={faCircleCheck} color="#2ecc71" />;
|
||||
|
||||
return <FontAwesomeIcon icon={faCircleXmark} color="#e74c3c" />;
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
return (
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
{subscription && !isTableDataLoading && tableData && (
|
||||
<Tr>
|
||||
<Th className="">Feature / Limit</Th>
|
||||
{tableData.head.map(({ name, priceLine }) => {
|
||||
return (
|
||||
<Th
|
||||
key={`plans-feature-head-${billingCycle}-${name}`}
|
||||
className="flex-1 text-center"
|
||||
>
|
||||
<p>{name}</p>
|
||||
<p>{priceLine}</p>
|
||||
</Th>
|
||||
);
|
||||
})}
|
||||
</Tr>
|
||||
)}
|
||||
</THead>
|
||||
<TBody>
|
||||
{subscription &&
|
||||
!isTableDataLoading &&
|
||||
tableData &&
|
||||
tableData.rows.map(({ name, starter, pro, enterprise }) => {
|
||||
return (
|
||||
<Tr className="h-12" key={`plans-feature-row-${billingCycle}-${name}`}>
|
||||
<Td>{displayCell(name)}</Td>
|
||||
<Td className="text-center">{displayCell(starter)}</Td>
|
||||
<Td className="text-center">{displayCell(pro)}</Td>
|
||||
<Td className="text-center">{displayCell(enterprise)}</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
{isTableDataLoading && <TableSkeleton columns={5} innerKey="cloud-products" />}
|
||||
{!isTableDataLoading && tableData?.rows.length === 0 && (
|
||||
<Tr>
|
||||
<Td colSpan={5}>
|
||||
<EmptyState title="No cloud product details found" icon={faFileInvoice} />
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
{subscription && !isTableDataLoading && tableData && (
|
||||
<Tr className="h-12">
|
||||
<Td />
|
||||
{tableData.head.map(({ slug, tier }) => {
|
||||
const isCurrentPlan = slug === subscription.slug;
|
||||
let subscriptionText = "Upgrade";
|
||||
|
||||
if (subscription.tier > tier) {
|
||||
subscriptionText = "Downgrade";
|
||||
}
|
||||
|
||||
if (tier === 3) {
|
||||
subscriptionText = "Contact sales";
|
||||
}
|
||||
|
||||
return isCurrentPlan ? (
|
||||
<Td>
|
||||
<Button colorSchema="secondary" className="w-full" isDisabled>
|
||||
Current
|
||||
</Button>
|
||||
</Td>
|
||||
) : (
|
||||
<Td>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
if (!currentOrg?.id) return;
|
||||
|
||||
if (tier !== 3) {
|
||||
const { url } = await createCustomerPortalSession.mutateAsync(
|
||||
currentOrg.id
|
||||
);
|
||||
window.location.href = url;
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.href = "https://infisical.com/scheduledemo";
|
||||
}}
|
||||
color="mineshaft"
|
||||
className="w-full"
|
||||
>
|
||||
{subscriptionText}
|
||||
</Button>
|
||||
</Td>
|
||||
);
|
||||
})}
|
||||
</Tr>
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
};
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button } from "@app/components/v2";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
useOrganization,
|
||||
useSubscription
|
||||
} from "@app/context";
|
||||
import {
|
||||
useCreateCustomerPortalSession,
|
||||
useGetOrgPlanBillingInfo,
|
||||
useGetOrgTrialUrl
|
||||
} from "@app/hooks/api";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { ManagePlansModal } from "./ManagePlansModal";
|
||||
|
||||
export const PreviewSection = () => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { subscription } = useSubscription();
|
||||
const { data, isPending } = useGetOrgPlanBillingInfo(currentOrg?.id ?? "");
|
||||
const getOrgTrialUrl = useGetOrgTrialUrl();
|
||||
const createCustomerPortalSession = useCreateCustomerPortalSession();
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["managePlan"] as const);
|
||||
|
||||
const formatAmount = (amount: number) => {
|
||||
const formattedTotal = (Math.floor(amount) / 100).toLocaleString("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD"
|
||||
});
|
||||
|
||||
return formattedTotal;
|
||||
};
|
||||
|
||||
const formatDate = (date: number) => {
|
||||
const createdDate = new Date(date * 1000);
|
||||
const day: number = createdDate.getDate();
|
||||
const month: number = createdDate.getMonth() + 1;
|
||||
const year: number = createdDate.getFullYear();
|
||||
const formattedDate: string = `${day}/${month}/${year}`;
|
||||
|
||||
return formattedDate;
|
||||
};
|
||||
|
||||
function formatPlanSlug(slug: string) {
|
||||
return slug.replace(/(\b[a-z])/g, (match) => match.toUpperCase()).replace(/-/g, " ");
|
||||
}
|
||||
|
||||
const handleUpgradeBtnClick = async () => {
|
||||
try {
|
||||
if (!subscription || !currentOrg) return;
|
||||
|
||||
if (!subscription.has_used_trial) {
|
||||
// direct user to start pro trial
|
||||
const url = await getOrgTrialUrl.mutateAsync({
|
||||
orgId: currentOrg.id,
|
||||
success_url: window.location.href
|
||||
});
|
||||
|
||||
window.location.href = url;
|
||||
} else {
|
||||
// open compare plans modal
|
||||
handlePopUpOpen("managePlan");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{subscription &&
|
||||
subscription?.slug !== "enterprise" &&
|
||||
subscription?.slug !== "pro" &&
|
||||
subscription?.slug !== "pro-annual" && (
|
||||
<div className="flex flex-row space-x-6">
|
||||
<div className="mb-6 flex flex-1 items-center rounded-lg border border-primary/40 bg-primary/10 p-4">
|
||||
<div className="flex-1">
|
||||
<h2 className="text-xl font-medium text-mineshaft-100">
|
||||
Unleash the full power of{" "}
|
||||
<span className="bg-gradient-to-r from-primary-500 to-yellow bg-clip-text font-semibold text-transparent">
|
||||
Infisical
|
||||
</span>
|
||||
</h2>
|
||||
<p className="mt-4 text-gray-400">
|
||||
Get unlimited members, projects, RBAC, smart alerts, and so much more.
|
||||
</p>
|
||||
</div>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Billing}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
onClick={() => handleUpgradeBtnClick()}
|
||||
color="mineshaft"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
{!subscription.has_used_trial ? "Start Pro Free Trial" : "Upgrade Plan"}
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<div className="mb-6 flex w-full max-w-[12rem] flex-col items-center rounded-lg border border-mineshaft-600 bg-mineshaft-800 p-4">
|
||||
<div className="mb-4 flex w-full justify-center font-semibold text-mineshaft-200">
|
||||
Want to learn more?{" "}
|
||||
</div>
|
||||
<div className="flex w-full justify-center">
|
||||
<a
|
||||
href="https://infisical.com/schedule-demo"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span className="cursor-pointer rounded-full border border-mineshaft-500 bg-mineshaft-600 px-4 py-2 duration-200 hover:border-primary/40 hover:bg-primary/10">
|
||||
Book a demo{" "}
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.06rem] ml-1 text-xs"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!isPending && subscription && data && (
|
||||
<div className="mb-6 flex">
|
||||
<div className="mr-4 flex-1 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<p className="mb-2 text-gray-400">Current plan</p>
|
||||
<p className="mb-8 text-2xl font-semibold text-mineshaft-50">
|
||||
{`${formatPlanSlug(subscription.slug)} ${
|
||||
subscription.status === "trialing" ? "(Trial)" : ""
|
||||
}`}
|
||||
</p>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Billing}>
|
||||
{(isAllowed) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
if (!currentOrg?.id) return;
|
||||
const { url } = await createCustomerPortalSession.mutateAsync(currentOrg.id);
|
||||
window.location.href = url;
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
className="text-primary"
|
||||
>
|
||||
Manage plan →
|
||||
</button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<div className="mr-4 flex-1 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<p className="mb-2 text-gray-400">Price</p>
|
||||
<p className="mb-8 text-2xl font-semibold text-mineshaft-50">
|
||||
{subscription.status === "trialing"
|
||||
? "$0.00 / month"
|
||||
: `${formatAmount(data.amount)} / ${data.interval}`}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex-1 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<p className="mb-2 text-gray-400">Subscription renews on</p>
|
||||
<p className="mb-8 text-2xl font-semibold text-mineshaft-50">
|
||||
{formatDate(data.currentPeriodEnd)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<ManagePlansModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { BillingCloudTab } from "./BillingCloudTab";
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { CompanyNameSection } from "./CompanyNameSection";
|
||||
import { InvoiceEmailSection } from "./InvoiceEmailSection";
|
||||
import { PmtMethodsSection } from "./PmtMethodsSection";
|
||||
import { TaxIDSection } from "./TaxIDSection";
|
||||
|
||||
export const BillingDetailsTab = () => {
|
||||
return (
|
||||
<>
|
||||
<CompanyNameSection />
|
||||
<InvoiceEmailSection />
|
||||
<PmtMethodsSection />
|
||||
<TaxIDSection />
|
||||
</>
|
||||
);
|
||||
};
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button, FormControl, Input } from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||
import { useGetOrgBillingDetails, useUpdateOrgBillingDetails } from "@app/hooks/api";
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
name: z.string()
|
||||
})
|
||||
.required();
|
||||
|
||||
export const CompanyNameSection = () => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { reset, control, handleSubmit } = useForm({
|
||||
defaultValues: {
|
||||
name: ""
|
||||
},
|
||||
resolver: zodResolver(schema)
|
||||
});
|
||||
const { data } = useGetOrgBillingDetails(currentOrg?.id ?? "");
|
||||
const { mutateAsync, isPending } = useUpdateOrgBillingDetails();
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
reset({
|
||||
name: data?.name ?? ""
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const onFormSubmit = async ({ name }: { name: string }) => {
|
||||
try {
|
||||
if (!currentOrg?.id) return;
|
||||
if (name === "") return;
|
||||
await mutateAsync({
|
||||
name,
|
||||
organizationId: currentOrg.id
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully updated business name",
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to update business name",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onFormSubmit)}
|
||||
className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
|
||||
>
|
||||
<h2 className="mb-8 flex-1 text-xl font-semibold text-mineshaft-100">Business name</h2>
|
||||
<div className="max-w-md">
|
||||
<Controller
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input placeholder="Acme Corp" {...field} className="bg-mineshaft-800" />
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="name"
|
||||
/>
|
||||
</div>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Billing}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
type="submit"
|
||||
colorSchema="secondary"
|
||||
isLoading={isPending}
|
||||
isDisabled={isPending || !isAllowed}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button, FormControl, Input } from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||
import { useGetOrgBillingDetails, useUpdateOrgBillingDetails } from "@app/hooks/api";
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
email: z.string()
|
||||
})
|
||||
.required();
|
||||
|
||||
export const InvoiceEmailSection = () => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { reset, control, handleSubmit } = useForm({
|
||||
defaultValues: {
|
||||
email: ""
|
||||
},
|
||||
resolver: zodResolver(schema)
|
||||
});
|
||||
const { data } = useGetOrgBillingDetails(currentOrg?.id ?? "");
|
||||
const { mutateAsync, isPending } = useUpdateOrgBillingDetails();
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
reset({
|
||||
email: data?.email ?? ""
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const onFormSubmit = async ({ email }: { email: string }) => {
|
||||
try {
|
||||
if (!currentOrg?.id) return;
|
||||
if (email === "") return;
|
||||
|
||||
await mutateAsync({
|
||||
email,
|
||||
organizationId: currentOrg.id
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully updated invoice email recipient",
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to update invoice email recipient",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onFormSubmit)}
|
||||
className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
|
||||
>
|
||||
<h2 className="mb-8 flex-1 text-xl font-semibold text-white">Invoice email recipient</h2>
|
||||
<div className="max-w-md">
|
||||
<Controller
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input placeholder="[email protected]" {...field} className="bg-mineshaft-800" />
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="email"
|
||||
/>
|
||||
</div>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Billing}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
type="submit"
|
||||
colorSchema="secondary"
|
||||
isLoading={isPending}
|
||||
isDisabled={isPending || !isAllowed}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button } from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||
import { useAddOrgPmtMethod } from "@app/hooks/api";
|
||||
|
||||
import { PmtMethodsTable } from "./PmtMethodsTable";
|
||||
|
||||
export const PmtMethodsSection = () => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { mutateAsync, isPending } = useAddOrgPmtMethod();
|
||||
|
||||
const handleAddPmtMethodBtnClick = async () => {
|
||||
if (!currentOrg?.id) return;
|
||||
const url = await mutateAsync({
|
||||
organizationId: currentOrg.id,
|
||||
success_url: window.location.href,
|
||||
cancel_url: window.location.href
|
||||
});
|
||||
|
||||
window.location.href = url;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-8 flex items-center">
|
||||
<h2 className="flex-1 text-xl font-semibold text-white">Payment methods</h2>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Billing}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
onClick={handleAddPmtMethodBtnClick}
|
||||
colorSchema="secondary"
|
||||
isLoading={isPending}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Add method
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<PmtMethodsTable />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
import { faCreditCard, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
DeleteActionModal,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useDeleteOrgPmtMethod, useGetOrgPmtMethods } from "@app/hooks/api";
|
||||
|
||||
export const PmtMethodsTable = () => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { data, isPending } = useGetOrgPmtMethods(currentOrg?.id ?? "");
|
||||
const deleteOrgPmtMethod = useDeleteOrgPmtMethod();
|
||||
const { handlePopUpOpen, handlePopUpClose, handlePopUpToggle, popUp } = usePopUp([
|
||||
"removeCard"
|
||||
] as const);
|
||||
|
||||
const pmtMethodToRemove = popUp.removeCard.data as { id: string; last4: string } | undefined;
|
||||
|
||||
const handleDeletePmtMethodBtnClick = async () => {
|
||||
if (!currentOrg?.id || !pmtMethodToRemove) return;
|
||||
try {
|
||||
await deleteOrgPmtMethod.mutateAsync({
|
||||
organizationId: currentOrg.id,
|
||||
pmtMethodId: pmtMethodToRemove.id
|
||||
});
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully removed payment method"
|
||||
});
|
||||
handlePopUpClose("removeCard");
|
||||
} catch (error: any) {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: error.message ?? "Error removing payment method"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableContainer className="mt-4">
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="flex-1">Brand</Th>
|
||||
<Th className="flex-1">Type</Th>
|
||||
<Th className="flex-1">Last 4 Digits</Th>
|
||||
<Th className="flex-1">Expiration</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{!isPending &&
|
||||
data &&
|
||||
data?.length > 0 &&
|
||||
data.map(({ _id: id, brand, exp_month, exp_year, funding, last4 }) => (
|
||||
<Tr key={`pmt-method-${id}`} className="h-10">
|
||||
<Td>{brand.charAt(0).toUpperCase() + brand.slice(1)}</Td>
|
||||
<Td>{funding.charAt(0).toUpperCase() + funding.slice(1)}</Td>
|
||||
<Td>{last4}</Td>
|
||||
<Td>{`${exp_month}/${exp_year}`}</Td>
|
||||
<Td>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Delete}
|
||||
a={OrgPermissionSubjects.Billing}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
onClick={() => handlePopUpOpen("removeCard", { id, last4 })}
|
||||
size="lg"
|
||||
isDisabled={!isAllowed}
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
{isPending && <TableSkeleton columns={5} innerKey="pmt-methods" />}
|
||||
{!isPending && data && data?.length === 0 && (
|
||||
<Tr>
|
||||
<Td colSpan={5}>
|
||||
<EmptyState title="No payment methods on file" icon={faCreditCard} />
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.removeCard.isOpen}
|
||||
deleteKey="confirm"
|
||||
onChange={(isOpen) => handlePopUpToggle("removeCard", isOpen)}
|
||||
title={`Remove payment method ending in *${pmtMethodToRemove?.last4}?`}
|
||||
onDeleteApproved={handleDeletePmtMethodBtnClick}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import { useAddOrgTaxId } from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const taxIDTypes = [
|
||||
{ label: "Australia ABN", value: "au_abn" },
|
||||
{ label: "Australia ARN", value: "au_arn" },
|
||||
{ label: "Bulgaria UIC", value: "bg_uic" },
|
||||
{ label: "Brazil CNPJ", value: "br_cnpj" },
|
||||
{ label: "Brazil CPF", value: "br_cpf" },
|
||||
{ label: "Canada BN", value: "ca_bn" },
|
||||
{ label: "Canada GST/HST", value: "ca_gst_hst" },
|
||||
{ label: "Canada PST BC", value: "ca_pst_bc" },
|
||||
{ label: "Canada PST MB", value: "ca_pst_mb" },
|
||||
{ label: "Canada PST SK", value: "ca_pst_sk" },
|
||||
{ label: "Canada QST", value: "ca_qst" },
|
||||
{ label: "Switzerland VAT", value: "ch_vat" },
|
||||
{ label: "Chile TIN", value: "cl_tin" },
|
||||
{ label: "Egypt TIN", value: "eg_tin" },
|
||||
{ label: "Spain CIF", value: "es_cif" },
|
||||
{ label: "EU OSS VAT", value: "eu_oss_vat" },
|
||||
{ label: "EU VAT", value: "eu_vat" },
|
||||
{ label: "GB VAT", value: "gb_vat" },
|
||||
{ label: "Georgia VAT", value: "ge_vat" },
|
||||
{ label: "Hong Kong BR", value: "hk_br" },
|
||||
{ label: "Hungary TIN", value: "hu_tin" },
|
||||
{ label: "Indonesia NPWP", value: "id_npwp" },
|
||||
{ label: "Israel VAT", value: "il_vat" },
|
||||
{ label: "India GST", value: "in_gst" },
|
||||
{ label: "Iceland VAT", value: "is_vat" },
|
||||
{ label: "Japan CN", value: "jp_cn" },
|
||||
{ label: "Japan RN", value: "jp_rn" },
|
||||
{ label: "Japan TRN", value: "jp_trn" },
|
||||
{ label: "Kenya PIN", value: "ke_pin" },
|
||||
{ label: "South Korea BRN", value: "kr_brn" },
|
||||
{ label: "Liechtenstein UID", value: "li_uid" },
|
||||
{ label: "Mexico RFC", value: "mx_rfc" },
|
||||
{ label: "Malaysia FRP", value: "my_frp" },
|
||||
{ label: "Malaysia ITN", value: "my_itn" },
|
||||
{ label: "Malaysia SST", value: "my_sst" },
|
||||
{ label: "Norway VAT", value: "no_vat" },
|
||||
{ label: "New Zealand GST", value: "nz_gst" },
|
||||
{ label: "Philippines TIN", value: "ph_tin" },
|
||||
{ label: "Russia INN", value: "ru_inn" },
|
||||
{ label: "Russia KPP", value: "ru_kpp" },
|
||||
{ label: "Saudi Arabia VAT", value: "sa_vat" },
|
||||
{ label: "Singapore GST", value: "sg_gst" },
|
||||
{ label: "Singapore UEN", value: "sg_uen" },
|
||||
{ label: "Slovenia TIN", value: "si_tin" },
|
||||
{ label: "Thailand VAT", value: "th_vat" },
|
||||
{ label: "Turkey TIN", value: "tr_tin" },
|
||||
{ label: "Taiwan VAT", value: "tw_vat" },
|
||||
{ label: "Ukraine VAT", value: "ua_vat" },
|
||||
{ label: "US EIN", value: "us_ein" },
|
||||
{ label: "South Africa VAT", value: "za_vat" }
|
||||
];
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
type: z.string(),
|
||||
value: z.string()
|
||||
})
|
||||
.required();
|
||||
|
||||
export type AddTaxIDFormData = z.infer<typeof schema>;
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["addTaxID"]>;
|
||||
handlePopUpClose: (popUpName: keyof UsePopUpState<["addTaxID"]>) => void;
|
||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["addTaxID"]>, state?: boolean) => void;
|
||||
};
|
||||
|
||||
export const TaxIDModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const addOrgTaxId = useAddOrgTaxId();
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<AddTaxIDFormData>({
|
||||
resolver: zodResolver(schema)
|
||||
});
|
||||
|
||||
const onTaxIDModalSubmit = async ({ type, value }: AddTaxIDFormData) => {
|
||||
try {
|
||||
if (!currentOrg?.id) return;
|
||||
await addOrgTaxId.mutateAsync({
|
||||
organizationId: currentOrg.id,
|
||||
type,
|
||||
value
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully added Tax ID",
|
||||
type: "success"
|
||||
});
|
||||
handlePopUpClose("addTaxID");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to add Tax ID",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.addTaxID?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("addTaxID", isOpen);
|
||||
reset();
|
||||
}}
|
||||
>
|
||||
<ModalContent title="Add Tax ID">
|
||||
<form onSubmit={handleSubmit(onTaxIDModalSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="type"
|
||||
defaultValue="eu_vat"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl label="Type" errorText={error?.message} isError={Boolean(error)}>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{taxIDTypes.map(({ label, value }) => (
|
||||
<SelectItem value={String(value || "")} key={label}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="value"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Value" isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input {...field} placeholder="DE000000000" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-8 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button } from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { TaxIDModal } from "./TaxIDModal";
|
||||
import { TaxIDTable } from "./TaxIDTable";
|
||||
|
||||
export const TaxIDSection = () => {
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"addTaxID"
|
||||
] as const);
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-8 flex items-center">
|
||||
<h2 className="flex-1 text-xl font-semibold text-white">Tax ID</h2>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Billing}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
onClick={() => handlePopUpOpen("addTaxID")}
|
||||
colorSchema="secondary"
|
||||
isDisabled={!isAllowed}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
Add method
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<TaxIDTable />
|
||||
<TaxIDModal
|
||||
popUp={popUp}
|
||||
handlePopUpClose={handlePopUpClose}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
import { faFileInvoice, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||
import { useDeleteOrgTaxId, useGetOrgTaxIds } from "@app/hooks/api";
|
||||
|
||||
const taxIDTypeLabelMap: { [key: string]: string } = {
|
||||
au_abn: "Australia ABN",
|
||||
au_arn: "Australia ARN",
|
||||
bg_uic: "Bulgaria UIC",
|
||||
br_cnpj: "Brazil CNPJ",
|
||||
br_cpf: "Brazil CPF",
|
||||
ca_bn: "Canada BN",
|
||||
ca_gst_hst: "Canada GST/HST",
|
||||
ca_pst_bc: "Canada PST BC",
|
||||
ca_pst_mb: "Canada PST MB",
|
||||
ca_pst_sk: "Canada PST SK",
|
||||
ca_qst: "Canada QST",
|
||||
ch_vat: "Switzerland VAT",
|
||||
cl_tin: "Chile TIN",
|
||||
eg_tin: "Egypt TIN",
|
||||
es_cif: "Spain CIF",
|
||||
eu_oss_vat: "EU OSS VAT",
|
||||
eu_vat: "EU VAT",
|
||||
gb_vat: "GB VAT",
|
||||
ge_vat: "Georgia VAT",
|
||||
hk_br: "Hong Kong BR",
|
||||
hu_tin: "Hungary TIN",
|
||||
id_npwp: "Indonesia NPWP",
|
||||
il_vat: "Israel VAT",
|
||||
in_gst: "India GST",
|
||||
is_vat: "Iceland VAT",
|
||||
jp_cn: "Japan CN",
|
||||
jp_rn: "Japan RN",
|
||||
jp_trn: "Japan TRN",
|
||||
ke_pin: "Kenya PIN",
|
||||
kr_brn: "South Korea BRN",
|
||||
li_uid: "Liechtenstein UID",
|
||||
mx_rfc: "Mexico RFC",
|
||||
my_frp: "Malaysia FRP",
|
||||
my_itn: "Malaysia ITN",
|
||||
my_sst: "Malaysia SST",
|
||||
no_vat: "Norway VAT",
|
||||
nz_gst: "New Zealand GST",
|
||||
ph_tin: "Philippines TIN",
|
||||
ru_inn: "Russia INN",
|
||||
ru_kpp: "Russia KPP",
|
||||
sa_vat: "Saudi Arabia VAT",
|
||||
sg_gst: "Singapore GST",
|
||||
sg_uen: "Singapore UEN",
|
||||
si_tin: "Slovenia TIN",
|
||||
th_vat: "Thailand VAT",
|
||||
tr_tin: "Turkey TIN",
|
||||
tw_vat: "Taiwan VAT",
|
||||
ua_vat: "Ukraine VAT",
|
||||
us_ein: "US EIN",
|
||||
za_vat: "South Africa VAT"
|
||||
};
|
||||
|
||||
export const TaxIDTable = () => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { data, isPending } = useGetOrgTaxIds(currentOrg?.id ?? "");
|
||||
const deleteOrgTaxId = useDeleteOrgTaxId();
|
||||
|
||||
const handleDeleteTaxIdBtnClick = async (taxId: string) => {
|
||||
if (!currentOrg?.id) return;
|
||||
await deleteOrgTaxId.mutateAsync({
|
||||
organizationId: currentOrg.id,
|
||||
taxId
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<TableContainer className="mt-4">
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="flex-1">Type</Th>
|
||||
<Th className="flex-1">Value</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{!isPending &&
|
||||
data &&
|
||||
data?.length > 0 &&
|
||||
data.map(({ id, type, value }) => (
|
||||
<Tr key={`tax-id-${id}`} className="h-10">
|
||||
<Td>{taxIDTypeLabelMap[type]}</Td>
|
||||
<Td>{value}</Td>
|
||||
<Td>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Delete}
|
||||
a={OrgPermissionSubjects.Billing}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
onClick={async () => {
|
||||
await handleDeleteTaxIdBtnClick(id);
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
{isPending && <TableSkeleton columns={3} innerKey="tax-ids" />}
|
||||
{!isPending && data && data?.length === 0 && (
|
||||
<Tr>
|
||||
<Td colSpan={5}>
|
||||
<EmptyState title="No Tax IDs on file" icon={faFileInvoice} />
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { BillingDetailsTab } from "./BillingDetailsTab";
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { InvoicesTable } from "./InvoicesTable";
|
||||
|
||||
export const BillingReceiptsTab = () => {
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<h2 className="flex-1 text-xl font-semibold text-white">Invoices</h2>
|
||||
<InvoicesTable />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
import { faDownload, faFileInvoice } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import {
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import { useGetOrgInvoices } from "@app/hooks/api";
|
||||
|
||||
export const InvoicesTable = () => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { data, isPending } = useGetOrgInvoices(currentOrg?.id ?? "");
|
||||
return (
|
||||
<TableContainer className="mt-8">
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="flex-1">Invoice #</Th>
|
||||
<Th className="flex-1">Date</Th>
|
||||
<Th className="flex-1">Status</Th>
|
||||
<Th className="flex-1">Amount</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{!isPending &&
|
||||
data &&
|
||||
data?.length > 0 &&
|
||||
data.map(({ id, created, paid, number, total, invoice_pdf }) => {
|
||||
const formattedTotal = (Math.floor(total) / 100).toLocaleString("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD"
|
||||
});
|
||||
const createdDate = new Date(created * 1000);
|
||||
const day: number = createdDate.getDate();
|
||||
const month: number = createdDate.getMonth() + 1;
|
||||
const year: number = createdDate.getFullYear();
|
||||
const formattedDate: string = `${day}/${month}/${year}`;
|
||||
|
||||
return (
|
||||
<Tr key={`invoice-${id}`} className="h-10">
|
||||
<Td>{number}</Td>
|
||||
<Td>{formattedDate}</Td>
|
||||
<Td>{paid ? "Paid" : "Not Paid"}</Td>
|
||||
<Td>{formattedTotal}</Td>
|
||||
<Td>
|
||||
<IconButton
|
||||
onClick={async () => window.open(invoice_pdf)}
|
||||
size="lg"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
>
|
||||
<FontAwesomeIcon icon={faDownload} />
|
||||
</IconButton>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
{isPending && <TableSkeleton columns={5} innerKey="invoices" />}
|
||||
{!isPending && data && data?.length === 0 && (
|
||||
<Tr>
|
||||
<Td colSpan={5}>
|
||||
<EmptyState title="No invoices on file" icon={faFileInvoice} />
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { BillingReceiptsTab } from "./BillingReceiptsTab";
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import { LicensesSection } from "./LicensesSection";
|
||||
|
||||
export const BillingSelfHostedTab = () => {
|
||||
return (
|
||||
<div>
|
||||
<LicensesSection />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import { faFileContract } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import {
|
||||
EmptyState,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import { useGetOrgLicenses } from "@app/hooks/api";
|
||||
|
||||
export const LicensesSection = () => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { data, isPending } = useGetOrgLicenses(currentOrg?.id ?? "");
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<h2 className="mb-8 flex-1 text-xl font-semibold text-white">Enterprise licenses</h2>
|
||||
<TableContainer className="mt-4">
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>License Key</Th>
|
||||
<Th>Status</Th>
|
||||
<Th>Issued Date</Th>
|
||||
<Th>Expiry Date</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{!isPending &&
|
||||
data &&
|
||||
data?.length > 0 &&
|
||||
data.map(({ id, licenseKey, isActivated, createdAt, expiresAt }) => {
|
||||
const formattedCreatedAt = new Date(createdAt).toISOString().split("T")[0];
|
||||
const formattedExpiresAt = new Date(expiresAt).toISOString().split("T")[0];
|
||||
return (
|
||||
<Tr key={`license-${id}`} className="h-10">
|
||||
<Td>{licenseKey}</Td>
|
||||
<Td>{isActivated ? "Active" : "Inactive"}</Td>
|
||||
<Td>{formattedCreatedAt}</Td>
|
||||
<Td>{formattedExpiresAt}</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
{isPending && <TableSkeleton columns={4} innerKey="licenses" />}
|
||||
{!isPending && data && data?.length === 0 && (
|
||||
<Tr>
|
||||
<Td colSpan={4}>
|
||||
<EmptyState title="No enterprise licenses on file" icon={faFileContract} />
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./BillingSelfHostedTab";
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||
import { withPermission } from "@app/hoc";
|
||||
|
||||
import { BillingCloudTab } from "../BillingCloudTab";
|
||||
import { BillingDetailsTab } from "../BillingDetailsTab";
|
||||
import { BillingReceiptsTab } from "../BillingReceiptsTab";
|
||||
import { BillingSelfHostedTab } from "../BillingSelfHostedTab";
|
||||
|
||||
const tabs = [
|
||||
{ name: "Infisical Cloud", key: "tab-infisical-cloud" },
|
||||
{ name: "Infisical Self-Hosted", key: "tab-infisical-self-hosted" },
|
||||
{ name: "Receipts", key: "tab-receipts" },
|
||||
{ name: "Billing details", key: "tab-billing-details" }
|
||||
];
|
||||
|
||||
export const BillingTabGroup = withPermission(
|
||||
() => {
|
||||
return (
|
||||
<Tabs defaultValue={tabs[0].key}>
|
||||
<TabList>
|
||||
{tabs.map((tab) => (
|
||||
<Tab value={tab.key}>{tab.name}</Tab>
|
||||
))}
|
||||
</TabList>
|
||||
<TabPanel value={tabs[0].key}>
|
||||
<BillingCloudTab />
|
||||
</TabPanel>
|
||||
<TabPanel value={tabs[1].key}>
|
||||
<BillingSelfHostedTab />
|
||||
</TabPanel>
|
||||
<TabPanel value={tabs[2].key}>
|
||||
<BillingReceiptsTab />
|
||||
</TabPanel>
|
||||
<TabPanel value={tabs[3].key}>
|
||||
<BillingDetailsTab />
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
);
|
||||
},
|
||||
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.Billing }
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
export { BillingTabGroup } from "./BillingTabGroup";
|
||||
@@ -0,0 +1 @@
|
||||
export { BillingTabGroup } from "./BillingTabGroup";
|
||||
@@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { BillingPage } from "./BillingPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/organization/_layout/billing"
|
||||
)({
|
||||
component: BillingPage
|
||||
});
|
||||
Reference in New Issue
Block a user