diff --git a/server/src/internal/customers/CusService.ts b/server/src/internal/customers/CusService.ts index cbbbf615f..d1d91d9f5 100644 --- a/server/src/internal/customers/CusService.ts +++ b/server/src/internal/customers/CusService.ts @@ -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 { 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 ); diff --git a/server/src/internal/customers/getFullCusQuery.ts b/server/src/internal/customers/getFullCusQuery.ts index 6a66fe6e4..ffc5a3bdc 100644 --- a/server/src/internal/customers/getFullCusQuery.ts +++ b/server/src/internal/customers/getFullCusQuery.ts @@ -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 `; }; diff --git a/server/src/internal/customers/internalCusRouter.ts b/server/src/internal/customers/internalCusRouter.ts index 6952f1939..3400b5b97 100644 --- a/server/src/internal/customers/internalCusRouter.ts +++ b/server/src/internal/customers/internalCusRouter.ts @@ -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, diff --git a/shared/models/cusModels/fullCusModel.ts b/shared/models/cusModels/fullCusModel.ts index b1aa1733d..794a80256 100644 --- a/shared/models/cusModels/fullCusModel.ts +++ b/shared/models/cusModels/fullCusModel.ts @@ -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[]; }; diff --git a/vite/src/views/customers/customer/CustomerEventsList.tsx b/vite/src/views/customers/customer/CustomerEventsList.tsx index c846906b1..e64b6cd81 100644 --- a/vite/src/views/customers/customer/CustomerEventsList.tsx +++ b/vite/src/views/customers/customer/CustomerEventsList.tsx @@ -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(null); +export const CustomerEventsList = () => { const navigate = useNavigate(); + const [selectedEvent, setSelectedEvent] = useState(null); + const { customer_id } = useParams(); const { showEntityView } = useCustomerContext(); + const { events, isLoading, error } = useCusEventsQuery(); return (
@@ -45,18 +39,14 @@ export const CustomerEventsList = ({ -
+

Events

- {events.length === 0 ? ( + {isLoading ? ( +
+

+ Loading events for this customer... +

+
+ ) : events && events.length === 0 ? (

No events received for this customer @@ -86,38 +82,42 @@ export const CustomerEventsList = ({ )} - {events.map((event: any) => ( - setSelectedEvent(event)} - > - {event.event_name} + {events && + events.map((event: any) => ( + setSelectedEvent(event)} + > + {event.event_name} - - - {event.value || event.properties.value || 1} - - - - POST - 200 - - {showEntityView && } - - - - {formatUnixToDateTime(event.timestamp).date}{" "} - {formatUnixToDateTime(event.timestamp).time}{" "} - - - {formatUnixToDateTimeWithMs(event.timestamp)} - - - - - - ))} + + + {event.value || event.properties.value || 1} + + + + POST + 200 + + {showEntityView && } + + + + {formatUnixToDateTime(event.timestamp).date}{" "} + {formatUnixToDateTime(event.timestamp).time}{" "} + + + {formatUnixToDateTimeWithMs(event.timestamp)} + + + + + + ))}

Showing last 10 events diff --git a/vite/src/views/customers/customer/CustomerToolbar.tsx b/vite/src/views/customers/customer/CustomerToolbar.tsx index 76a02b51f..d3dbaaf19 100644 --- a/vite/src/views/customers/customer/CustomerToolbar.tsx +++ b/vite/src/views/customers/customer/CustomerToolbar.tsx @@ -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, diff --git a/vite/src/views/customers/customer/CustomerView.tsx b/vite/src/views/customers/customer/CustomerView.tsx index 860bcf0a3..1869e8649 100644 --- a/vite/src/views/customers/customer/CustomerView.tsx +++ b/vite/src/views/customers/customer/CustomerView.tsx @@ -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, }} > -

+
@@ -108,12 +110,8 @@ export default function CustomerView() {
- {/* - */} + +
diff --git a/vite/src/views/customers/customer/InvoicesTable.tsx b/vite/src/views/customers/customer/InvoicesTable.tsx index 088363beb..c22c95bb3 100644 --- a/vite/src/views/customers/customer/InvoicesTable.tsx +++ b/vite/src/views/customers/customer/InvoicesTable.tsx @@ -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(", ")} diff --git a/vite/src/views/customers/customer/add-product/NewProductDropdown.tsx b/vite/src/views/customers/customer/add-product/NewProductDropdown.tsx index 476146351..cdd614858 100644 --- a/vite/src/views/customers/customer/add-product/NewProductDropdown.tsx +++ b/vite/src/views/customers/customer/add-product/NewProductDropdown.tsx @@ -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
) : ( - filteredProducts.map((product: Product) => ( + filteredProducts.map((product: ProductV2) => ( { const [isLoading, setIsLoading] = useState(false); diff --git a/vite/src/views/customers/customer/CustomerConfig.tsx b/vite/src/views/customers/customer/components/CustomerConfig.tsx similarity index 100% rename from vite/src/views/customers/customer/CustomerConfig.tsx rename to vite/src/views/customers/customer/components/CustomerConfig.tsx diff --git a/vite/src/views/customers/customer/components/entity-header.tsx b/vite/src/views/customers/customer/components/EntityHeader.tsx similarity index 100% rename from vite/src/views/customers/customer/components/entity-header.tsx rename to vite/src/views/customers/customer/components/EntityHeader.tsx diff --git a/vite/src/views/customers/customer/UpdateCustomerDialog.tsx b/vite/src/views/customers/customer/components/UpdateCustomerDialog.tsx similarity index 91% rename from vite/src/views/customers/customer/UpdateCustomerDialog.tsx rename to vite/src/views/customers/customer/components/UpdateCustomerDialog.tsx index cce2c662c..dc20e964c 100644 --- a/vite/src/views/customers/customer/UpdateCustomerDialog.tsx +++ b/vite/src/views/customers/customer/components/UpdateCustomerDialog.tsx @@ -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, diff --git a/vite/src/views/customers/customer/add-coupon/AddCouponDialogContent.tsx b/vite/src/views/customers/customer/components/add-coupon/AddCouponDialogContent.tsx similarity index 95% rename from vite/src/views/customers/customer/add-coupon/AddCouponDialogContent.tsx rename to vite/src/views/customers/customer/components/add-coupon/AddCouponDialogContent.tsx index fb035bfcd..82b0efda7 100644 --- a/vite/src/views/customers/customer/add-coupon/AddCouponDialogContent.tsx +++ b/vite/src/views/customers/customer/components/add-coupon/AddCouponDialogContent.tsx @@ -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, diff --git a/vite/src/views/customers/customer/components/customer-sidebar/CustomerSidebar.tsx b/vite/src/views/customers/customer/components/customer-sidebar/CustomerSidebar.tsx index ce7c6feb0..60e933971 100644 --- a/vite/src/views/customers/customer/components/customer-sidebar/CustomerSidebar.tsx +++ b/vite/src/views/customers/customer/components/customer-sidebar/CustomerSidebar.tsx @@ -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"; diff --git a/vite/src/views/customers/customer/components/customer-sidebar/customer-rewards.tsx b/vite/src/views/customers/customer/components/customer-sidebar/customer-rewards.tsx index a87ec401f..0f120e036 100644 --- a/vite/src/views/customers/customer/components/customer-sidebar/customer-rewards.tsx +++ b/vite/src/views/customers/customer/components/customer-sidebar/customer-rewards.tsx @@ -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(); diff --git a/vite/src/views/customers/customer/customer-product-list/CustomerProductList.tsx b/vite/src/views/customers/customer/customer-product-list/CustomerProductList.tsx index 5db5d9f8b..5ee0ee9a3 100644 --- a/vite/src/views/customers/customer/customer-product-list/CustomerProductList.tsx +++ b/vite/src/views/customers/customer/customer-product-list/CustomerProductList.tsx @@ -119,13 +119,13 @@ export const CustomerProductList = () => { > Show Expired - {/* */} +
- {/* - */} +
diff --git a/vite/src/views/customers/customer/hooks/useCusEventsQuery.tsx b/vite/src/views/customers/customer/hooks/useCusEventsQuery.tsx new file mode 100644 index 000000000..908df50d0 --- /dev/null +++ b/vite/src/views/customers/customer/hooks/useCusEventsQuery.tsx @@ -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 }; +}; diff --git a/vite/src/views/customers/customer/hooks/useCusQuery.tsx b/vite/src/views/customers/customer/hooks/useCusQuery.tsx index 29c85c6a4..004293dc3 100644 --- a/vite/src/views/customers/customer/hooks/useCusQuery.tsx +++ b/vite/src/views/customers/customer/hooks/useCusQuery.tsx @@ -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 { diff --git a/vite/src/views/customers/customer/product/multi-attach/MultiAttachDialog.tsx b/vite/src/views/customers/customer/product/multi-attach/MultiAttachDialog.tsx index d82ca9b4d..9ad4577f6 100644 --- a/vite/src/views/customers/customer/product/multi-attach/MultiAttachDialog.tsx +++ b/vite/src/views/customers/customer/product/multi-attach/MultiAttachDialog.tsx @@ -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 = ({ {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) => ( {product.name} diff --git a/vite/src/views/customers/hooks/useFullCusSearchQuery.tsx b/vite/src/views/customers/hooks/useFullCusSearchQuery.tsx index 2cceed262..0a1991d9e 100644 --- a/vite/src/views/customers/hooks/useFullCusSearchQuery.tsx +++ b/vite/src/views/customers/hooks/useFullCusSearchQuery.tsx @@ -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, + ]); }; diff --git a/vite/src/views/products/product/EditProductToolbar.tsx b/vite/src/views/products/product/EditProductToolbar.tsx deleted file mode 100644 index 8fc3280ff..000000000 --- a/vite/src/views/products/product/EditProductToolbar.tsx +++ /dev/null @@ -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 ( - - - - - - { - e.stopPropagation(); - e.preventDefault(); - await handleDelete(); - }} - > -
- {numVersions > 1 ? "Delete version" : "Delete"} - {deleteLoading ? : } -
-
-
-
- ); -}; diff --git a/vite/src/views/products/product/ProductView.tsx b/vite/src/views/products/product/ProductView.tsx index c9d69d60b..ccbbdf717 100644 --- a/vite/src/views/products/product/ProductView.tsx +++ b/vite/src/views/products/product/ProductView.tsx @@ -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"); diff --git a/vite/src/views/products/product/entitlements/MoreMenuButton.tsx b/vite/src/views/products/product/entitlements/MoreMenuButton.tsx deleted file mode 100644 index 8ec949d63..000000000 --- a/vite/src/views/products/product/entitlements/MoreMenuButton.tsx +++ /dev/null @@ -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 ( - - - - - -
- -
- -
-
- ); -} diff --git a/vite/src/views/products/product/product-item/MoreMenuButton.tsx b/vite/src/views/products/product/product-item/MoreMenuButton.tsx deleted file mode 100644 index 9059351b2..000000000 --- a/vite/src/views/products/product/product-item/MoreMenuButton.tsx +++ /dev/null @@ -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 ( - - - - - -
- -
-
-
- ); -} - -export const MoreMenuPriceButton = () => { - const [showPopover, setShowPopover] = useState(false); - const { item, setItem } = useProductItemContext(); - - return ( - - - - - -
- -
-
-
- ); -};