finished with customer page tanstack
This commit is contained in:
@@ -34,6 +34,7 @@ export class CusService {
|
||||
expand,
|
||||
withSubs = false,
|
||||
allowNotFound = false,
|
||||
withEvents = false,
|
||||
}: {
|
||||
db: DrizzleCli;
|
||||
idOrInternalId: string;
|
||||
@@ -45,6 +46,7 @@ export class CusService {
|
||||
expand?: (CusExpand | EntityExpand)[];
|
||||
withSubs?: boolean;
|
||||
allowNotFound?: boolean;
|
||||
withEvents?: boolean;
|
||||
}): Promise<FullCustomer> {
|
||||
const includeInvoices = expand?.includes(CusExpand.Invoices) || false;
|
||||
const withTrialsUsed = expand?.includes(CusExpand.TrialsUsed) || false;
|
||||
@@ -70,6 +72,7 @@ export class CusService {
|
||||
withEntities,
|
||||
withTrialsUsed,
|
||||
withSubs,
|
||||
withEvents,
|
||||
entityId
|
||||
);
|
||||
|
||||
|
||||
@@ -202,6 +202,7 @@ export const getFullCusQuery = (
|
||||
withEntities: boolean,
|
||||
withTrialsUsed: boolean,
|
||||
withSubs: boolean,
|
||||
withEvents: boolean,
|
||||
entityId?: string
|
||||
) => {
|
||||
const sqlChunks: SQL[] = [];
|
||||
@@ -255,6 +256,32 @@ export const getFullCusQuery = (
|
||||
sqlChunks.push(buildInvoicesCTE(!!entityId));
|
||||
}
|
||||
|
||||
// Conditionally add events CTE
|
||||
if (withEvents) {
|
||||
sqlChunks.push(sql`, `);
|
||||
sqlChunks.push(sql`
|
||||
customer_events AS (
|
||||
SELECT
|
||||
COALESCE(
|
||||
json_agg(
|
||||
json_build_object(
|
||||
'id', e.id,
|
||||
'event_name', e.event_name,
|
||||
'value', e.value,
|
||||
'timestamp', e.timestamp,
|
||||
'properties', e.properties
|
||||
)
|
||||
ORDER BY e.timestamp DESC, e.id DESC
|
||||
) FILTER (WHERE e.id IS NOT NULL),
|
||||
'[]'::json
|
||||
) AS events
|
||||
FROM events e
|
||||
WHERE e.internal_customer_id = (SELECT internal_id FROM customer_record)
|
||||
AND e.set_usage = false
|
||||
)
|
||||
`);
|
||||
}
|
||||
|
||||
// Build final SELECT
|
||||
const selectFieldsChunks: SQL[] = [];
|
||||
selectFieldsChunks.push(sql`
|
||||
@@ -294,6 +321,11 @@ export const getFullCusQuery = (
|
||||
(SELECT invoices FROM customer_invoices) AS invoices`);
|
||||
}
|
||||
|
||||
if (withEvents) {
|
||||
selectFieldsChunks.push(sql`,
|
||||
(SELECT events FROM customer_events) AS events`);
|
||||
}
|
||||
|
||||
sqlChunks.push(sql`
|
||||
SELECT ${sql.join(selectFieldsChunks, sql``)}
|
||||
FROM customer_record cr
|
||||
@@ -312,6 +344,7 @@ export const getPaginatedFullCusQuery = ({
|
||||
withSubs,
|
||||
limit = 10,
|
||||
offset = 0,
|
||||
withEvents = false,
|
||||
entityId,
|
||||
internalCustomerIds,
|
||||
}: {
|
||||
@@ -324,6 +357,7 @@ export const getPaginatedFullCusQuery = ({
|
||||
withSubs: boolean;
|
||||
limit: number;
|
||||
offset: number;
|
||||
withEvents?: boolean;
|
||||
entityId?: string;
|
||||
internalCustomerIds?: string[];
|
||||
}) => {
|
||||
@@ -342,6 +376,14 @@ export const getPaginatedFullCusQuery = ({
|
||||
FROM customers c
|
||||
WHERE c.org_id = ${orgId}
|
||||
AND c.env = ${env}
|
||||
${
|
||||
internalCustomerIds
|
||||
? sql`AND c.internal_id IN (${sql.join(
|
||||
internalCustomerIds.map((id) => sql`${id}`),
|
||||
sql`, `
|
||||
)})`
|
||||
: sql``
|
||||
}
|
||||
ORDER BY c.created_at DESC
|
||||
LIMIT ${limit} OFFSET ${offset}
|
||||
),
|
||||
@@ -405,14 +447,7 @@ export const getPaginatedFullCusQuery = ({
|
||||
LEFT JOIN customer_prices cpr ON cpr.customer_product_id = cp.id
|
||||
LEFT JOIN prices p ON cpr.price_id = p.id
|
||||
LEFT JOIN customer_entitlements ce ON ce.customer_product_id = cp.id
|
||||
WHERE cp.internal_customer_id IN (SELECT internal_id FROM customer_records) ${
|
||||
internalCustomerIds
|
||||
? sql`AND cp.internal_customer_id IN (${sql.join(
|
||||
internalCustomerIds.map((id) => sql`${id}`),
|
||||
sql`, `
|
||||
)})`
|
||||
: sql``
|
||||
}
|
||||
WHERE cp.internal_customer_id IN (SELECT internal_id FROM customer_records)
|
||||
${withStatusFilter()}
|
||||
GROUP BY cp.id, prod.*
|
||||
),
|
||||
@@ -500,12 +535,14 @@ export const getPaginatedFullCusQuery = ({
|
||||
${withEntities ? sql`, COALESCE(ce.entities, '[]'::json) AS entities` : sql``}
|
||||
${includeInvoices ? sql`, COALESCE(ci.invoices, '[]'::json) AS invoices` : sql``}
|
||||
${withTrialsUsed ? sql`, COALESCE(ctu.trials_used, '[]'::json) AS trials_used` : sql``}
|
||||
${withEvents ? sql`, COALESCE(cev.events, '[]'::json) AS events` : sql``}
|
||||
FROM customer_records cr
|
||||
LEFT JOIN customer_products_aggregated cpa ON cpa.internal_customer_id = cr.internal_id
|
||||
${withSubs ? sql`LEFT JOIN customer_subscriptions cs ON cs.internal_customer_id = cr.internal_id` : sql``}
|
||||
${withEntities ? sql`LEFT JOIN customer_entities ce ON ce.internal_customer_id = cr.internal_id` : sql``}
|
||||
${includeInvoices ? sql`LEFT JOIN customer_invoices ci ON ci.internal_customer_id = cr.internal_id` : sql``}
|
||||
${withTrialsUsed ? sql`LEFT JOIN customer_trials_used ctu ON ctu.internal_customer_id = cr.internal_id` : sql``}
|
||||
${withEvents ? sql`LEFT JOIN customer_events cev ON cev.internal_customer_id = cr.internal_id` : sql``}
|
||||
ORDER BY cr.created_at DESC
|
||||
`;
|
||||
};
|
||||
|
||||
@@ -76,100 +76,6 @@ cusRouter.get("/:customer_id", async (req: any, res: any) => {
|
||||
],
|
||||
});
|
||||
|
||||
// const [coupons, products, customer] = await Promise.all([
|
||||
// RewardService.list({
|
||||
// db,
|
||||
// orgId: orgId,
|
||||
// env,
|
||||
// }),
|
||||
|
||||
// ProductService.listFull({ db, orgId, env, returnAll: true }),
|
||||
|
||||
// ]);
|
||||
|
||||
// let invoices = customer.invoices;
|
||||
// let entities = customer.entities;
|
||||
// const events = await EventService.getByCustomerId({
|
||||
// db,
|
||||
// internalCustomerId: customer.internal_id,
|
||||
// env,
|
||||
// orgId: orgId,
|
||||
// limit: 10,
|
||||
// });
|
||||
|
||||
// let fullCustomer = customer as any;
|
||||
// let cusProducts = fullCustomer.customer_products;
|
||||
// fullCustomer.products = fullCustomer.customer_products;
|
||||
// fullCustomer.entitlements = cusProducts.flatMap(
|
||||
// (product: FullCusProduct) => product.customer_entitlements
|
||||
// );
|
||||
// fullCustomer.prices = cusProducts.flatMap(
|
||||
// (product: FullCusProduct) => product.customer_prices
|
||||
// );
|
||||
|
||||
// for (const product of fullCustomer.products) {
|
||||
// product.entitlements = product.customer_entitlements.map(
|
||||
// (cusEnt: FullCustomerEntitlement) => {
|
||||
// return cusEnt.entitlement;
|
||||
// }
|
||||
// );
|
||||
// product.prices = product.customer_prices.map(
|
||||
// (cusPrice: FullCustomerPrice) => {
|
||||
// return cusPrice.price;
|
||||
// }
|
||||
// );
|
||||
// }
|
||||
|
||||
// let discount = null;
|
||||
// if (org.stripe_config && customer.processor?.id) {
|
||||
// try {
|
||||
// const stripeCli = createStripeCli({ org, env });
|
||||
// const stripeCus: any = await stripeCli.customers.retrieve(
|
||||
// customer.processor.id
|
||||
// );
|
||||
|
||||
// if (stripeCus.discount) {
|
||||
// discount = stripeCus.discount;
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.log("error", error);
|
||||
// }
|
||||
// }
|
||||
|
||||
// for (const invoice of invoices || []) {
|
||||
// invoice.product_ids = invoice.product_ids.sort();
|
||||
// invoice.internal_product_ids = invoice.internal_product_ids.sort();
|
||||
// }
|
||||
|
||||
// fullCustomer.entitlements = fullCustomer.entitlements.sort(
|
||||
// (a: any, b: any) => {
|
||||
// const productA = fullCustomer.products.find(
|
||||
// (p: any) => p.id === a.customer_product_id
|
||||
// );
|
||||
// const productB = fullCustomer.products.find(
|
||||
// (p: any) => p.id === b.customer_product_id
|
||||
// );
|
||||
|
||||
// return (
|
||||
// new Date(b.created_at).getTime() - new Date(a.created_at).getTime() ||
|
||||
// b.id.localeCompare(a.id)
|
||||
// );
|
||||
// }
|
||||
// );
|
||||
|
||||
// for (const cusEnt of fullCustomer.entitlements) {
|
||||
// // let entitlement = cusEnt.entitlement;
|
||||
|
||||
// // Show used, limit, etc.
|
||||
// let { balance, unused } = getCusEntMasterBalance({
|
||||
// cusEnt,
|
||||
// entities,
|
||||
// });
|
||||
|
||||
// cusEnt.balance = balance;
|
||||
// cusEnt.unused = unused;
|
||||
// }
|
||||
|
||||
res.status(200).json({
|
||||
customer: fullCus,
|
||||
// products: getLatestProducts(products),
|
||||
@@ -187,6 +93,40 @@ cusRouter.get("/:customer_id", async (req: any, res: any) => {
|
||||
}
|
||||
});
|
||||
|
||||
cusRouter.get("/:customer_id/events", async (req: any, res: any) => {
|
||||
try {
|
||||
const { db, org, features, env } = req;
|
||||
const { customer_id } = req.params;
|
||||
const orgId = req.orgId;
|
||||
|
||||
const customer = await CusService.get({
|
||||
db,
|
||||
orgId,
|
||||
env,
|
||||
idOrInternalId: customer_id,
|
||||
});
|
||||
|
||||
if (!customer) {
|
||||
throw new RecaseError({
|
||||
message: "Customer not found",
|
||||
code: ErrCode.CustomerNotFound,
|
||||
statusCode: StatusCodes.NOT_FOUND,
|
||||
});
|
||||
}
|
||||
|
||||
const events = await EventService.getByCustomerId({
|
||||
db,
|
||||
internalCustomerId: customer.internal_id,
|
||||
env,
|
||||
orgId: orgId,
|
||||
});
|
||||
|
||||
res.status(200).json({ events });
|
||||
} catch (error) {
|
||||
handleFrontendReqError({ req, error, res, action: "get customer events" });
|
||||
}
|
||||
});
|
||||
|
||||
// cusRouter.get("/:customer_id/stripe", async (req: any, res: any) => {
|
||||
// try {
|
||||
// const { db, org, features, env } = req;
|
||||
@@ -220,27 +160,31 @@ cusRouter.get("/:customer_id", async (req: any, res: any) => {
|
||||
// }
|
||||
// });
|
||||
|
||||
cusRouter.get("/:customer_id/events", async (req: any, res: any) => {
|
||||
try {
|
||||
const { db, org, features, env } = req;
|
||||
const { customer_id } = req.params;
|
||||
const orgId = req.orgId;
|
||||
const limit = req.query.limit || 10;
|
||||
const period = req.query.period || "all";
|
||||
// cusRouter.get("/:customer_id/events", async (req: any, res: any) => {
|
||||
// try {
|
||||
// const { db, org, features, env } = req;
|
||||
// const { customer_id } = req.params;
|
||||
// const orgId = req.orgId;
|
||||
// const limit = req.query.limit || 10;
|
||||
// const period = req.query.period || "all";
|
||||
|
||||
const events = await EventService.getByCustomerId({
|
||||
db,
|
||||
internalCustomerId: customer_id,
|
||||
env,
|
||||
orgId: orgId,
|
||||
limit,
|
||||
});
|
||||
// console.log("Fetching events for customer:", customer_id);
|
||||
|
||||
res.status(200).json({ events });
|
||||
} catch (error) {
|
||||
handleFrontendReqError({ req, error, res, action: "get customer events" });
|
||||
}
|
||||
});
|
||||
// const events = await EventService.getByCustomerId({
|
||||
// db,
|
||||
// internalCustomerId: customer_id,
|
||||
// env,
|
||||
// orgId: orgId,
|
||||
// limit,
|
||||
// });
|
||||
|
||||
// console.log("Events:", events);
|
||||
|
||||
// res.status(200).json({ events });
|
||||
// } catch (error) {
|
||||
// handleFrontendReqError({ req, error, res, action: "get customer events" });
|
||||
// }
|
||||
// });
|
||||
|
||||
cusRouter.get("/:customer_id/data", async (req: any, res: any) => {
|
||||
try {
|
||||
@@ -485,8 +429,6 @@ cusRouter.post("/all/full_customers", async (req: any, res: any) =>
|
||||
pageSize: page_size,
|
||||
});
|
||||
|
||||
console.log("First customer", customers?.[0]);
|
||||
|
||||
const fullCustomers = await CusBatchService.getByInternalIds({
|
||||
db,
|
||||
org,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { FullCusProduct } from "../cusProductModels/cusProductModels.js";
|
||||
import { Event } from "../eventModels/eventTable.js";
|
||||
import { Subscription } from "../subModels/subModels.js";
|
||||
import { Customer } from "./cusModels.js";
|
||||
import { Entity } from "./entityModels/entityModels.js";
|
||||
@@ -15,4 +16,5 @@ export type FullCustomer = Customer & {
|
||||
}[];
|
||||
invoices?: Invoice[];
|
||||
subscriptions?: Subscription[];
|
||||
events?: Event[];
|
||||
};
|
||||
|
||||
@@ -12,24 +12,18 @@ import {
|
||||
TooltipContent,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useNavigate } from "react-router";
|
||||
import { AppEnv } from "@autumn/shared";
|
||||
import { useNavigate, useParams } from "react-router";
|
||||
import { useCustomerContext } from "./CustomerContext";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CusProductEntityItem } from "./components/CusProductEntityItem";
|
||||
import { useCusQuery } from "./hooks/useCusQuery";
|
||||
import { useCusEventsQuery } from "./hooks/useCusEventsQuery";
|
||||
|
||||
export const CustomerEventsList = ({
|
||||
events,
|
||||
customer,
|
||||
env,
|
||||
}: {
|
||||
events: any;
|
||||
customer: any;
|
||||
env: AppEnv;
|
||||
}) => {
|
||||
const [selectedEvent, setSelectedEvent] = useState<any>(null);
|
||||
export const CustomerEventsList = () => {
|
||||
const navigate = useNavigate();
|
||||
const [selectedEvent, setSelectedEvent] = useState<any>(null);
|
||||
const { customer_id } = useParams();
|
||||
const { showEntityView } = useCustomerContext();
|
||||
const { events, isLoading, error } = useCusEventsQuery();
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -45,18 +39,14 @@ export const CustomerEventsList = ({
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<div className="flex items-center grid grid-cols-10 gap-8 justify-between border-y bg-stone-100 pl-10 pr-7 h-10">
|
||||
<div className="items-center grid grid-cols-10 gap-8 justify-between border-y bg-stone-100 pl-10 pr-7 h-10">
|
||||
<h2 className="text-sm text-t2 font-medium col-span-2 flex">Events</h2>
|
||||
<div className="flex w-full h-full items-center col-span-8 justify-end">
|
||||
<div className="flex w-fit h-full items-center gap-4">
|
||||
<Button
|
||||
variant="analyse"
|
||||
onClick={() =>
|
||||
navigateTo(
|
||||
`/analytics?customer_id=${customer.id}`,
|
||||
navigate,
|
||||
env
|
||||
)
|
||||
navigateTo(`/analytics?customer_id=${customer_id}`, navigate)
|
||||
}
|
||||
>
|
||||
Analyse Events
|
||||
@@ -65,7 +55,13 @@ export const CustomerEventsList = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{events.length === 0 ? (
|
||||
{isLoading ? (
|
||||
<div className="flex pl-10 items-center h-10">
|
||||
<p className="text-t3 text-sm shimmer">
|
||||
Loading events for this customer...
|
||||
</p>
|
||||
</div>
|
||||
) : events && events.length === 0 ? (
|
||||
<div className="flex pl-10 items-center h-10">
|
||||
<p className="text-t3 text-sm">
|
||||
No events received for this customer
|
||||
@@ -86,10 +82,14 @@ export const CustomerEventsList = ({
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{events.map((event: any) => (
|
||||
{events &&
|
||||
events.map((event: any) => (
|
||||
<Row
|
||||
key={event.id}
|
||||
className={cn("grid-cols-12 pr-0", showEntityView && "grid-cols-15")}
|
||||
className={cn(
|
||||
"grid-cols-12 pr-0",
|
||||
showEntityView && "grid-cols-15"
|
||||
)}
|
||||
onClick={() => setSelectedEvent(event)}
|
||||
>
|
||||
<Item className="col-span-3 font-mono">{event.event_name}</Item>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import React from "react";
|
||||
import SmallSpinner from "@/components/general/SmallSpinner";
|
||||
import AddCouponDialogContent from "./components/add-coupon/AddCouponDialogContent";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
@@ -14,23 +17,11 @@ import { Customer } from "@autumn/shared";
|
||||
import { useCustomerContext } from "./CustomerContext";
|
||||
import { CusService } from "@/services/customers/CusService";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
import { navigateTo } from "@/utils/genUtils";
|
||||
|
||||
import React from "react";
|
||||
import { Dialog, DialogTrigger } from "@/components/ui/dialog";
|
||||
import AddCouponDialogContent from "./add-coupon/AddCouponDialogContent";
|
||||
import { cn } from "@/lib/utils";
|
||||
import UpdateCustomerDialog from "./UpdateCustomerDialog";
|
||||
import {
|
||||
Delete,
|
||||
Pen,
|
||||
Pencil,
|
||||
Settings,
|
||||
Settings2,
|
||||
Ticket,
|
||||
Trash,
|
||||
} from "lucide-react";
|
||||
import UpdateCustomerDialog from "./components/UpdateCustomerDialog";
|
||||
import { Delete, Settings } from "lucide-react";
|
||||
|
||||
export const CustomerToolbar = ({
|
||||
className,
|
||||
|
||||
@@ -13,6 +13,8 @@ import { CustomerProductList } from "./customer-product-list/CustomerProductList
|
||||
import { useFeaturesQuery } from "@/hooks/queries/useFeaturesQuery";
|
||||
import { CustomerEntitlementsList } from "./entitlements/CustomerEntitlementsList";
|
||||
import { useCusReferralQuery } from "./hooks/useCusReferralQuery";
|
||||
import { InvoicesTable } from "./InvoicesTable";
|
||||
import { CustomerEventsList } from "./CustomerEventsList";
|
||||
|
||||
export default function CustomerView() {
|
||||
// const { customer_id } = useParams();
|
||||
@@ -96,7 +98,7 @@ export default function CustomerView() {
|
||||
// rewards: rewardsData?.rewards,
|
||||
}}
|
||||
>
|
||||
<div className="flex w-full overflow-auto h-full ">
|
||||
<div className="flex w-full overflow-y-scroll h-full">
|
||||
<div className="flex flex-col gap-4 w-full ">
|
||||
<CustomerPageHeader />
|
||||
<div className="flex w-full !pb-[50px]">
|
||||
@@ -108,12 +110,8 @@ export default function CustomerView() {
|
||||
<CustomerEntitlementsList />
|
||||
<div className="flex flex-col gap-2"></div>
|
||||
|
||||
{/* <InvoicesTable />
|
||||
<CustomerEventsList
|
||||
customer={customer}
|
||||
events={events}
|
||||
env={env}
|
||||
/> */}
|
||||
<InvoicesTable />
|
||||
<CustomerEventsList />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,11 +8,15 @@ import { Row, Item } from "@/components/general/TableGrid";
|
||||
import { AdminHover } from "@/components/general/AdminHover";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CusProductEntityItem } from "./components/CusProductEntityItem";
|
||||
import { useCusQuery } from "./hooks/useCusQuery";
|
||||
|
||||
export const InvoicesTable = () => {
|
||||
const { env, invoices, products, entityId, entities, showEntityView } =
|
||||
useCustomerContext();
|
||||
const axiosInstance = useAxiosInstance({ env });
|
||||
// const { env, invoices, products, entityId, entities, showEntityView } =
|
||||
// useCustomerContext();
|
||||
const { entityId, showEntityView } = useCustomerContext();
|
||||
const { customer, products, entities } = useCusQuery();
|
||||
const axiosInstance = useAxiosInstance();
|
||||
const invoices = customer.invoices;
|
||||
|
||||
const entity = entities.find(
|
||||
(e: any) => e.id === entityId || e.internal_id === entityId
|
||||
@@ -100,7 +104,7 @@ export const InvoicesTable = () => {
|
||||
>
|
||||
{invoice.product_ids
|
||||
.map((p: string) => {
|
||||
return products.find((product: Product) => product.id === p)
|
||||
return products.find((product: any) => product.id === p)
|
||||
?.name;
|
||||
})
|
||||
.join(", ")}
|
||||
|
||||
@@ -16,18 +16,21 @@ import { useNavigate } from "react-router";
|
||||
import { getRedirectUrl, navigateTo } from "@/utils/genUtils";
|
||||
import { toast } from "sonner";
|
||||
import { OrgService } from "@/services/OrgService";
|
||||
import { CusProductStatus, Entity, Product } from "@autumn/shared";
|
||||
import { CusProductStatus, Entity, Product, ProductV2 } from "@autumn/shared";
|
||||
import SmallSpinner from "@/components/general/SmallSpinner";
|
||||
import { Blend, Search } from "lucide-react";
|
||||
import { useOrg } from "@/hooks/common/useOrg";
|
||||
import { useCustomer } from "autumn-js/react";
|
||||
import { useCusQuery } from "../hooks/useCusQuery";
|
||||
|
||||
function AddProduct({
|
||||
setMultiAttachOpen,
|
||||
}: {
|
||||
setMultiAttachOpen: (open: boolean) => void;
|
||||
}) {
|
||||
const { products, customer, env, entityId, entities } = useCustomerContext();
|
||||
const { env, entityId } = useCustomerContext();
|
||||
const { products, customer, entities } = useCusQuery();
|
||||
|
||||
const axiosInstance = useAxiosInstance({ env });
|
||||
const { customer: autumnCustomer } = useCustomer();
|
||||
|
||||
@@ -36,7 +39,7 @@ function AddProduct({
|
||||
const [open, setOpen] = useState(false);
|
||||
const { org } = useOrg();
|
||||
|
||||
const filteredProducts = products.filter((product: Product) => {
|
||||
const filteredProducts = products.filter((product: ProductV2) => {
|
||||
if (product.is_add_on && !searchQuery) return true;
|
||||
|
||||
const entity = entities.find((e: Entity) => e.id === entityId);
|
||||
@@ -114,7 +117,7 @@ function AddProduct({
|
||||
No new products found
|
||||
</div>
|
||||
) : (
|
||||
filteredProducts.map((product: Product) => (
|
||||
filteredProducts.map((product: ProductV2) => (
|
||||
<DropdownProductItem
|
||||
key={product.id}
|
||||
product={product}
|
||||
@@ -146,7 +149,7 @@ const DropdownProductItem = ({
|
||||
product,
|
||||
handleAddProduct,
|
||||
}: {
|
||||
product: Product;
|
||||
product: ProductV2;
|
||||
handleAddProduct: any;
|
||||
}) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DialogFooter } from "@/components/ui/dialog";
|
||||
import { getBackendErr, navigateTo } from "@/utils/genUtils";
|
||||
import { Reward, CreateCustomer, Customer } from "@autumn/shared";
|
||||
import { useEffect, useState } from "react";
|
||||
import { CreateCustomer, Customer } from "@autumn/shared";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { DialogTitle } from "@/components/ui/dialog";
|
||||
import { DialogContent } from "@/components/ui/dialog";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useCustomerContext } from "./CustomerContext";
|
||||
import { useAxiosInstance } from "@/services/useAxiosInstance";
|
||||
import { useEnv } from "@/utils/envUtils";
|
||||
import { CustomerConfig } from "./CustomerConfig";
|
||||
import { CusService } from "@/services/customers/CusService";
|
||||
import { useNavigate } from "react-router";
|
||||
import { useCusQuery } from "./hooks/useCusQuery";
|
||||
import { useCusQuery } from "../hooks/useCusQuery";
|
||||
|
||||
const UpdateCustomerDialog = ({
|
||||
selectedCustomer,
|
||||
@@ -7,7 +7,6 @@ import { Select, SelectItem } from "@/components/ui/select";
|
||||
import { DialogContent, DialogTitle } from "@/components/ui/dialog";
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useCustomerContext } from "../CustomerContext";
|
||||
import { getBackendErr } from "@/utils/genUtils";
|
||||
import { toast } from "sonner";
|
||||
import { CusService } from "@/services/customers/CusService";
|
||||
@@ -15,8 +14,8 @@ import { useAxiosInstance } from "@/services/useAxiosInstance";
|
||||
import { getOriginalCouponId } from "@/utils/product/couponUtils";
|
||||
import { WarningBox } from "@/components/general/modal-components/WarningBox";
|
||||
import { useRewardsQuery } from "@/hooks/queries/useRewardsQuery";
|
||||
import { useCusQuery } from "../hooks/useCusQuery";
|
||||
import { useCusReferralQuery } from "../hooks/useCusReferralQuery";
|
||||
import { useCusQuery } from "../../hooks/useCusQuery";
|
||||
import { useCusReferralQuery } from "../../hooks/useCusReferralQuery";
|
||||
|
||||
const AddCouponDialogContent = ({
|
||||
setOpen,
|
||||
@@ -1,9 +1,8 @@
|
||||
import UpdateCustomerDialog from "../UpdateCustomerDialog";
|
||||
import { useState } from "react";
|
||||
import { Accordion } from "@/components/ui/accordion";
|
||||
import { Dialog } from "@/components/ui/dialog";
|
||||
import { CustomerRewards } from "./customer-rewards";
|
||||
import { useCustomerContext } from "../../CustomerContext";
|
||||
import UpdateCustomerDialog from "../../UpdateCustomerDialog";
|
||||
import { CustomerToolbar } from "../../CustomerToolbar";
|
||||
import { CustomerDetails } from "./CustomerDetails";
|
||||
import { CustomerEntities } from "./CustomerEntities";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import AddCouponDialogContent from "../../components/add-coupon/AddCouponDialogContent";
|
||||
import { SideAccordion } from "@/components/general/SideAccordion";
|
||||
import { getRedirectUrl } from "@/utils/genUtils";
|
||||
import { Dialog } from "@/components/ui/dialog";
|
||||
@@ -15,11 +16,9 @@ import { ArrowUpRightFromSquare } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Link } from "react-router";
|
||||
import { useCusQuery } from "../../hooks/useCusQuery";
|
||||
import { useEnv } from "@/utils/envUtils";
|
||||
import { useCusReferralQuery } from "../../hooks/useCusReferralQuery";
|
||||
import { useRewardsQuery } from "@/hooks/queries/useRewardsQuery";
|
||||
import AddCouponDialogContent from "../../add-coupon/AddCouponDialogContent";
|
||||
|
||||
export const CustomerRewards = () => {
|
||||
// const { discount, env } = useCustomerContext();
|
||||
|
||||
@@ -119,13 +119,13 @@ export const CustomerProductList = () => {
|
||||
>
|
||||
Show Expired
|
||||
</Button>
|
||||
{/* <CreateEntitlement buttonType={"feature"} /> */}
|
||||
|
||||
<div className="flex items-center gap-0">
|
||||
{/* <MultiAttachDialog
|
||||
<MultiAttachDialog
|
||||
open={multiAttachOpen}
|
||||
setOpen={setMultiAttachOpen}
|
||||
/>
|
||||
<AddProduct setMultiAttachOpen={setMultiAttachOpen} /> */}
|
||||
<AddProduct setMultiAttachOpen={setMultiAttachOpen} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useAxiosInstance } from "@/services/useAxiosInstance";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useParams } from "react-router";
|
||||
|
||||
export const useCusEventsQuery = () => {
|
||||
const axiosInstance = useAxiosInstance();
|
||||
const { customer_id } = useParams();
|
||||
|
||||
const fetcher = async () => {
|
||||
console.log("Fetching events for customer:", customer_id);
|
||||
const { data } = await axiosInstance.get(
|
||||
`/customers/${customer_id}/events`
|
||||
);
|
||||
console.log("Events:", data);
|
||||
return data;
|
||||
};
|
||||
|
||||
const { data, isLoading, error } = useQuery({
|
||||
queryKey: ["customer_events", customer_id],
|
||||
queryFn: fetcher,
|
||||
});
|
||||
|
||||
return { events: data?.events, isLoading, error };
|
||||
};
|
||||
@@ -32,7 +32,7 @@ export const useCusQuery = () => {
|
||||
const { products, isLoading: productsLoading } = useProductsQuery();
|
||||
const { features, isLoading: featuresLoading } = useFeaturesQuery();
|
||||
|
||||
const customer = cachedCustomer || data?.customer;
|
||||
const customer = data?.customer || cachedCustomer;
|
||||
const cusWithCacheLoading = cachedCustomer ? false : customerLoading;
|
||||
|
||||
return {
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { useEffect, useState } from "react";
|
||||
import { FullProduct } from "@autumn/shared";
|
||||
import { FullProduct, ProductV2 } from "@autumn/shared";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { toast } from "sonner";
|
||||
import { useAxiosInstance } from "@/services/useAxiosInstance";
|
||||
@@ -31,6 +31,8 @@ import { formatAmount } from "@/utils/product/productItemUtils";
|
||||
import { formatUnixToDate } from "@/utils/formatUtils/formatDateUtils";
|
||||
import { AddRewardButton, MultiAttachRewards } from "./MultiAttachRewards";
|
||||
import { useAxiosSWR } from "@/services/useAxiosSwr";
|
||||
import { useCusQuery } from "../../hooks/useCusQuery";
|
||||
import { useOrg } from "@/hooks/common/useOrg";
|
||||
|
||||
export const MultiAttachDialog = ({
|
||||
open,
|
||||
@@ -39,7 +41,9 @@ export const MultiAttachDialog = ({
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
}) => {
|
||||
const { customer, cusMutate, products, org } = useCustomerContext();
|
||||
// const { customer, cusMutate, products, org } = useCustomerContext();
|
||||
const { org } = useOrg();
|
||||
const { customer, products, refetch } = useCusQuery();
|
||||
|
||||
const axiosInstance = useAxiosInstance();
|
||||
|
||||
@@ -160,7 +164,7 @@ export const MultiAttachDialog = ({
|
||||
window.open(getStripeInvoiceLink(data.invoice), "_blank");
|
||||
}
|
||||
|
||||
await cusMutate();
|
||||
await refetch();
|
||||
toast.success("Products attached successfully");
|
||||
setOpen(false);
|
||||
} catch (error) {
|
||||
@@ -204,12 +208,14 @@ export const MultiAttachDialog = ({
|
||||
<SelectContent className="max-h-[300px] overflow-y-auto">
|
||||
{products
|
||||
.filter(
|
||||
(p: FullProduct) =>
|
||||
(p: ProductV2) =>
|
||||
!productOptions
|
||||
.map((o, i) => (i !== index ? o.product : null))
|
||||
.map((o, i) =>
|
||||
i !== index ? o.product_id : null
|
||||
)
|
||||
.includes(p.id)
|
||||
)
|
||||
.map((product: FullProduct) => (
|
||||
.map((product: ProductV2) => (
|
||||
<SelectItem key={product.id} value={product.id}>
|
||||
{product.name}
|
||||
</SelectItem>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FullCustomer } from "@autumn/shared";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import { useEffect } from "react";
|
||||
import { useCustomersQueryStates } from "./useCustomersQueryStates";
|
||||
import { useAxiosInstance } from "@/services/useAxiosInstance";
|
||||
|
||||
@@ -7,19 +8,12 @@ export const useFullCusSearchQuery = () => {
|
||||
const { queryStates } = useCustomersQueryStates();
|
||||
const axiosInstance = useAxiosInstance();
|
||||
|
||||
const { data: fullCustomersData } = useQuery<{
|
||||
const { refetch } = useQuery<{
|
||||
fullCustomers: FullCustomer[];
|
||||
}>({
|
||||
queryKey: [
|
||||
"full_customers",
|
||||
queryStates.page,
|
||||
queryStates.status,
|
||||
queryStates.version,
|
||||
queryStates.none,
|
||||
queryStates.q,
|
||||
],
|
||||
queryFn: async () => {
|
||||
console.log("Fetching full customers: ", queryStates.q);
|
||||
queryKey: ["full_customers"],
|
||||
// Pass AbortSignal so previous requests are canceled when a new refetch starts
|
||||
queryFn: async ({ signal }) => {
|
||||
const { data } = await axiosInstance.post(
|
||||
`/customers/all/full_customers`,
|
||||
{
|
||||
@@ -31,12 +25,31 @@ export const useFullCusSearchQuery = () => {
|
||||
version: queryStates.version,
|
||||
none: queryStates.none,
|
||||
},
|
||||
}
|
||||
},
|
||||
{ signal }
|
||||
);
|
||||
|
||||
console.log("data", data);
|
||||
console.log(`Fetched ${data?.fullCustomers.length} full customers`);
|
||||
return data;
|
||||
},
|
||||
placeholderData: keepPreviousData,
|
||||
enabled: false,
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
staleTime: Infinity,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// One controlled refetch per dependency change
|
||||
refetch();
|
||||
}, [
|
||||
// Trigger on all state changes that affect the payload
|
||||
queryStates.page,
|
||||
queryStates.status,
|
||||
queryStates.version,
|
||||
queryStates.none,
|
||||
queryStates.q,
|
||||
refetch,
|
||||
]);
|
||||
};
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
import SmallSpinner from "@/components/general/SmallSpinner";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
import { ProductV2 } from "@autumn/shared";
|
||||
|
||||
import { ProductService } from "@/services/products/ProductService";
|
||||
import { useNavigate } from "react-router";
|
||||
import { useAxiosInstance } from "@/services/useAxiosInstance";
|
||||
import { useProductContext } from "./ProductContext";
|
||||
import { getBackendErr, navigateTo } from "@/utils/genUtils";
|
||||
import { Delete, Settings } from "lucide-react";
|
||||
|
||||
export const EditProductToolbar = ({
|
||||
className,
|
||||
product,
|
||||
}: {
|
||||
className?: string;
|
||||
product: ProductV2;
|
||||
}) => {
|
||||
const { mutate, env, numVersions, version } = useProductContext();
|
||||
const axiosInstance = useAxiosInstance({ env });
|
||||
|
||||
const [deleteLoading, setDeleteLoading] = useState(false);
|
||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
if (version && version < numVersions) {
|
||||
toast.error("Can't delete earlier version of a product");
|
||||
return;
|
||||
}
|
||||
|
||||
setDeleteLoading(true);
|
||||
await ProductService.deleteProduct(axiosInstance, product.id);
|
||||
|
||||
if (numVersions > 1) {
|
||||
navigateTo(
|
||||
`/products/${product.id}?version=${numVersions - 1}`,
|
||||
navigate,
|
||||
env
|
||||
);
|
||||
toast.success(
|
||||
`${product.name} (version ${numVersions}) deleted successfully`
|
||||
);
|
||||
} else {
|
||||
navigateTo(`/products`, navigate, env);
|
||||
toast.success(`${product.name} deleted successfully`);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(getBackendErr(error, "Failed to delete product"));
|
||||
} finally {
|
||||
setDeleteLoading(false);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<DropdownMenu open={deleteOpen} onOpenChange={setDeleteOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
isIcon
|
||||
variant="ghost"
|
||||
dim={6}
|
||||
className={cn("rounded-full", className)}
|
||||
>
|
||||
<Settings size={14} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="text-t2" align="end">
|
||||
<DropdownMenuItem
|
||||
// className="flex items-center bg-red-500 text-white"
|
||||
className="flex items-center text-red-500 hover:!bg-red-500 hover:!text-white text-xs"
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
await handleDelete();
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between w-full gap-2">
|
||||
{numVersions > 1 ? "Delete version" : "Delete"}
|
||||
{deleteLoading ? <SmallSpinner /> : <Delete size={12} />}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
@@ -16,13 +16,12 @@ import { ManageProduct } from "./ManageProduct";
|
||||
import { AppEnv, UpdateProductSchema } from "@autumn/shared";
|
||||
import { ProductService } from "@/services/products/ProductService";
|
||||
import { getBackendErr } from "@/utils/genUtils";
|
||||
import { UpdateProductButton } from "@/views/products/product/components/UpdateProductButton";
|
||||
import { useProductChangedAlert } from "./hooks/useProductChangedAlert";
|
||||
import { useProductData } from "./hooks/useProductData";
|
||||
import { UpdateProductButton } from "./components/UpdateProductButton";
|
||||
|
||||
function ProductView({ env }: { env: AppEnv }) {
|
||||
const axiosInstance = useAxiosInstance();
|
||||
|
||||
const { product_id } = useParams();
|
||||
const [searchParams] = useSearchParams();
|
||||
const version = searchParams.get("version");
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
import { ToggleDisplayButton } from "@/components/general/ToggleDisplayButton";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Feature } from "@autumn/shared";
|
||||
import { EllipsisVertical, MinusIcon, PlusIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function MoreMenuButton({
|
||||
fields,
|
||||
setFields,
|
||||
showPerEntity,
|
||||
setShowPerEntity,
|
||||
selectedFeature,
|
||||
}: {
|
||||
fields: any;
|
||||
setFields: (fields: any) => void;
|
||||
showPerEntity: boolean;
|
||||
setShowPerEntity: (showPerEntity: boolean) => void;
|
||||
selectedFeature: Feature | null;
|
||||
}) {
|
||||
const [showPopover, setShowPopover] = useState(false);
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-t3 text-xs bg-transparent border-none shadow-none justify-start"
|
||||
onClick={() => setShowPopover(!showPopover)}
|
||||
// disabled={!selectedFeature}
|
||||
>
|
||||
<EllipsisVertical size={14} className="mr-1" />
|
||||
More
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-48 p-2 flex flex-col text-xs" align="end">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="text-xs text-t3 shadow-none border-none"
|
||||
onClick={() => {
|
||||
setFields({
|
||||
...fields,
|
||||
carry_from_previous: !fields.carry_from_previous,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
className="border-t3 mr-1"
|
||||
checked={fields.carry_from_previous}
|
||||
onCheckedChange={(checked) =>
|
||||
setFields({
|
||||
...fields,
|
||||
carry_from_previous: Boolean(checked),
|
||||
})
|
||||
}
|
||||
/>
|
||||
Keep usage on upgrade
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
className="h-7 shadow-none text-t3 text-xs justify-start border-none"
|
||||
variant="outline"
|
||||
startIcon={
|
||||
showPerEntity ? (
|
||||
<MinusIcon size={14} className="ml-0.5 mr-1" />
|
||||
) : (
|
||||
<PlusIcon size={14} className="ml-0.5 mr-1" />
|
||||
)
|
||||
}
|
||||
onClick={() => {
|
||||
setShowPerEntity(!showPerEntity);
|
||||
// hide the popover
|
||||
setShowPopover(false);
|
||||
}}
|
||||
>
|
||||
{showPerEntity ? "Remove Per Entity" : "Add Per Entity"}
|
||||
</Button>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
|
||||
import { EllipsisVertical, MinusIcon, PlusIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useProductItemContext } from "./ProductItemContext";
|
||||
import { ProductItem, UsageModel } from "@autumn/shared";
|
||||
|
||||
export default function MoreMenuButton({
|
||||
show,
|
||||
setShow,
|
||||
}: {
|
||||
show: any;
|
||||
setShow: (show: any) => void;
|
||||
}) {
|
||||
const [showPopover, setShowPopover] = useState(false);
|
||||
const {
|
||||
item,
|
||||
setItem,
|
||||
}: { item: ProductItem; setItem: (item: ProductItem) => void } =
|
||||
useProductItemContext();
|
||||
|
||||
return (
|
||||
<Popover open={showPopover} onOpenChange={setShowPopover}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-t3 text-xs bg-transparent border-none shadow-none justify-start"
|
||||
onClick={() => setShowPopover(!showPopover)}
|
||||
>
|
||||
<EllipsisVertical size={14} className="" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-fit min-w-48 p-0 py-1 flex flex-col text-xs"
|
||||
align="end"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="text-xs text-t2 shadow-none border-none w-full justify-start"
|
||||
onClick={() => {
|
||||
setItem({
|
||||
...item,
|
||||
reset_usage_when_enabled: !item.reset_usage_when_enabled,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
className="border-t3 mr-1"
|
||||
checked={item.reset_usage_when_enabled || false}
|
||||
/>
|
||||
Reset usage when product is enabled
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
export const MoreMenuPriceButton = () => {
|
||||
const [showPopover, setShowPopover] = useState(false);
|
||||
const { item, setItem } = useProductItemContext();
|
||||
|
||||
return (
|
||||
<Popover open={showPopover} onOpenChange={setShowPopover}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-t3 text-xs bg-transparent border-none shadow-none justify-start"
|
||||
onClick={() => setShowPopover(!showPopover)}
|
||||
>
|
||||
<EllipsisVertical size={14} className="" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-48 p-0 flex flex-col text-xs" align="end">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="text-xs text-t2 shadow-none border-none w-full justify-start"
|
||||
onClick={() => {
|
||||
setItem({
|
||||
...item,
|
||||
usage_model:
|
||||
item.usage_model == UsageModel.Prepaid
|
||||
? UsageModel.PayPerUse
|
||||
: UsageModel.Prepaid,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
className="border-t3 mr-1"
|
||||
checked={item.usage_model == UsageModel.Prepaid}
|
||||
/>
|
||||
Usage is Prepaid
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user