finished with customer page tanstack
This commit is contained in:
@@ -34,6 +34,7 @@ export class CusService {
|
|||||||
expand,
|
expand,
|
||||||
withSubs = false,
|
withSubs = false,
|
||||||
allowNotFound = false,
|
allowNotFound = false,
|
||||||
|
withEvents = false,
|
||||||
}: {
|
}: {
|
||||||
db: DrizzleCli;
|
db: DrizzleCli;
|
||||||
idOrInternalId: string;
|
idOrInternalId: string;
|
||||||
@@ -45,6 +46,7 @@ export class CusService {
|
|||||||
expand?: (CusExpand | EntityExpand)[];
|
expand?: (CusExpand | EntityExpand)[];
|
||||||
withSubs?: boolean;
|
withSubs?: boolean;
|
||||||
allowNotFound?: boolean;
|
allowNotFound?: boolean;
|
||||||
|
withEvents?: boolean;
|
||||||
}): Promise<FullCustomer> {
|
}): Promise<FullCustomer> {
|
||||||
const includeInvoices = expand?.includes(CusExpand.Invoices) || false;
|
const includeInvoices = expand?.includes(CusExpand.Invoices) || false;
|
||||||
const withTrialsUsed = expand?.includes(CusExpand.TrialsUsed) || false;
|
const withTrialsUsed = expand?.includes(CusExpand.TrialsUsed) || false;
|
||||||
@@ -70,6 +72,7 @@ export class CusService {
|
|||||||
withEntities,
|
withEntities,
|
||||||
withTrialsUsed,
|
withTrialsUsed,
|
||||||
withSubs,
|
withSubs,
|
||||||
|
withEvents,
|
||||||
entityId
|
entityId
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -202,6 +202,7 @@ export const getFullCusQuery = (
|
|||||||
withEntities: boolean,
|
withEntities: boolean,
|
||||||
withTrialsUsed: boolean,
|
withTrialsUsed: boolean,
|
||||||
withSubs: boolean,
|
withSubs: boolean,
|
||||||
|
withEvents: boolean,
|
||||||
entityId?: string
|
entityId?: string
|
||||||
) => {
|
) => {
|
||||||
const sqlChunks: SQL[] = [];
|
const sqlChunks: SQL[] = [];
|
||||||
@@ -255,6 +256,32 @@ export const getFullCusQuery = (
|
|||||||
sqlChunks.push(buildInvoicesCTE(!!entityId));
|
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
|
// Build final SELECT
|
||||||
const selectFieldsChunks: SQL[] = [];
|
const selectFieldsChunks: SQL[] = [];
|
||||||
selectFieldsChunks.push(sql`
|
selectFieldsChunks.push(sql`
|
||||||
@@ -294,6 +321,11 @@ export const getFullCusQuery = (
|
|||||||
(SELECT invoices FROM customer_invoices) AS invoices`);
|
(SELECT invoices FROM customer_invoices) AS invoices`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (withEvents) {
|
||||||
|
selectFieldsChunks.push(sql`,
|
||||||
|
(SELECT events FROM customer_events) AS events`);
|
||||||
|
}
|
||||||
|
|
||||||
sqlChunks.push(sql`
|
sqlChunks.push(sql`
|
||||||
SELECT ${sql.join(selectFieldsChunks, sql``)}
|
SELECT ${sql.join(selectFieldsChunks, sql``)}
|
||||||
FROM customer_record cr
|
FROM customer_record cr
|
||||||
@@ -312,6 +344,7 @@ export const getPaginatedFullCusQuery = ({
|
|||||||
withSubs,
|
withSubs,
|
||||||
limit = 10,
|
limit = 10,
|
||||||
offset = 0,
|
offset = 0,
|
||||||
|
withEvents = false,
|
||||||
entityId,
|
entityId,
|
||||||
internalCustomerIds,
|
internalCustomerIds,
|
||||||
}: {
|
}: {
|
||||||
@@ -324,6 +357,7 @@ export const getPaginatedFullCusQuery = ({
|
|||||||
withSubs: boolean;
|
withSubs: boolean;
|
||||||
limit: number;
|
limit: number;
|
||||||
offset: number;
|
offset: number;
|
||||||
|
withEvents?: boolean;
|
||||||
entityId?: string;
|
entityId?: string;
|
||||||
internalCustomerIds?: string[];
|
internalCustomerIds?: string[];
|
||||||
}) => {
|
}) => {
|
||||||
@@ -342,6 +376,14 @@ export const getPaginatedFullCusQuery = ({
|
|||||||
FROM customers c
|
FROM customers c
|
||||||
WHERE c.org_id = ${orgId}
|
WHERE c.org_id = ${orgId}
|
||||||
AND c.env = ${env}
|
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
|
ORDER BY c.created_at DESC
|
||||||
LIMIT ${limit} OFFSET ${offset}
|
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 customer_prices cpr ON cpr.customer_product_id = cp.id
|
||||||
LEFT JOIN prices p ON cpr.price_id = p.id
|
LEFT JOIN prices p ON cpr.price_id = p.id
|
||||||
LEFT JOIN customer_entitlements ce ON ce.customer_product_id = cp.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) ${
|
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``
|
|
||||||
}
|
|
||||||
${withStatusFilter()}
|
${withStatusFilter()}
|
||||||
GROUP BY cp.id, prod.*
|
GROUP BY cp.id, prod.*
|
||||||
),
|
),
|
||||||
@@ -500,12 +535,14 @@ export const getPaginatedFullCusQuery = ({
|
|||||||
${withEntities ? sql`, COALESCE(ce.entities, '[]'::json) AS entities` : sql``}
|
${withEntities ? sql`, COALESCE(ce.entities, '[]'::json) AS entities` : sql``}
|
||||||
${includeInvoices ? sql`, COALESCE(ci.invoices, '[]'::json) AS invoices` : sql``}
|
${includeInvoices ? sql`, COALESCE(ci.invoices, '[]'::json) AS invoices` : sql``}
|
||||||
${withTrialsUsed ? sql`, COALESCE(ctu.trials_used, '[]'::json) AS trials_used` : 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
|
FROM customer_records cr
|
||||||
LEFT JOIN customer_products_aggregated cpa ON cpa.internal_customer_id = cr.internal_id
|
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``}
|
${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``}
|
${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``}
|
${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``}
|
${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
|
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({
|
res.status(200).json({
|
||||||
customer: fullCus,
|
customer: fullCus,
|
||||||
// products: getLatestProducts(products),
|
// 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) => {
|
// cusRouter.get("/:customer_id/stripe", async (req: any, res: any) => {
|
||||||
// try {
|
// try {
|
||||||
// const { db, org, features, env } = req;
|
// 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) => {
|
// cusRouter.get("/:customer_id/events", async (req: any, res: any) => {
|
||||||
try {
|
// try {
|
||||||
const { db, org, features, env } = req;
|
// const { db, org, features, env } = req;
|
||||||
const { customer_id } = req.params;
|
// const { customer_id } = req.params;
|
||||||
const orgId = req.orgId;
|
// const orgId = req.orgId;
|
||||||
const limit = req.query.limit || 10;
|
// const limit = req.query.limit || 10;
|
||||||
const period = req.query.period || "all";
|
// const period = req.query.period || "all";
|
||||||
|
|
||||||
const events = await EventService.getByCustomerId({
|
// console.log("Fetching events for customer:", customer_id);
|
||||||
db,
|
|
||||||
internalCustomerId: customer_id,
|
|
||||||
env,
|
|
||||||
orgId: orgId,
|
|
||||||
limit,
|
|
||||||
});
|
|
||||||
|
|
||||||
res.status(200).json({ events });
|
// const events = await EventService.getByCustomerId({
|
||||||
} catch (error) {
|
// db,
|
||||||
handleFrontendReqError({ req, error, res, action: "get customer events" });
|
// 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) => {
|
cusRouter.get("/:customer_id/data", async (req: any, res: any) => {
|
||||||
try {
|
try {
|
||||||
@@ -485,8 +429,6 @@ cusRouter.post("/all/full_customers", async (req: any, res: any) =>
|
|||||||
pageSize: page_size,
|
pageSize: page_size,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("First customer", customers?.[0]);
|
|
||||||
|
|
||||||
const fullCustomers = await CusBatchService.getByInternalIds({
|
const fullCustomers = await CusBatchService.getByInternalIds({
|
||||||
db,
|
db,
|
||||||
org,
|
org,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { FullCusProduct } from "../cusProductModels/cusProductModels.js";
|
import { FullCusProduct } from "../cusProductModels/cusProductModels.js";
|
||||||
|
import { Event } from "../eventModels/eventTable.js";
|
||||||
import { Subscription } from "../subModels/subModels.js";
|
import { Subscription } from "../subModels/subModels.js";
|
||||||
import { Customer } from "./cusModels.js";
|
import { Customer } from "./cusModels.js";
|
||||||
import { Entity } from "./entityModels/entityModels.js";
|
import { Entity } from "./entityModels/entityModels.js";
|
||||||
@@ -15,4 +16,5 @@ export type FullCustomer = Customer & {
|
|||||||
}[];
|
}[];
|
||||||
invoices?: Invoice[];
|
invoices?: Invoice[];
|
||||||
subscriptions?: Subscription[];
|
subscriptions?: Subscription[];
|
||||||
|
events?: Event[];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,24 +12,18 @@ import {
|
|||||||
TooltipContent,
|
TooltipContent,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate, useParams } from "react-router";
|
||||||
import { AppEnv } from "@autumn/shared";
|
|
||||||
import { useCustomerContext } from "./CustomerContext";
|
import { useCustomerContext } from "./CustomerContext";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { CusProductEntityItem } from "./components/CusProductEntityItem";
|
import { useCusQuery } from "./hooks/useCusQuery";
|
||||||
|
import { useCusEventsQuery } from "./hooks/useCusEventsQuery";
|
||||||
|
|
||||||
export const CustomerEventsList = ({
|
export const CustomerEventsList = () => {
|
||||||
events,
|
|
||||||
customer,
|
|
||||||
env,
|
|
||||||
}: {
|
|
||||||
events: any;
|
|
||||||
customer: any;
|
|
||||||
env: AppEnv;
|
|
||||||
}) => {
|
|
||||||
const [selectedEvent, setSelectedEvent] = useState<any>(null);
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [selectedEvent, setSelectedEvent] = useState<any>(null);
|
||||||
|
const { customer_id } = useParams();
|
||||||
const { showEntityView } = useCustomerContext();
|
const { showEntityView } = useCustomerContext();
|
||||||
|
const { events, isLoading, error } = useCusEventsQuery();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -45,18 +39,14 @@ export const CustomerEventsList = ({
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</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>
|
<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-full h-full items-center col-span-8 justify-end">
|
||||||
<div className="flex w-fit h-full items-center gap-4">
|
<div className="flex w-fit h-full items-center gap-4">
|
||||||
<Button
|
<Button
|
||||||
variant="analyse"
|
variant="analyse"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
navigateTo(
|
navigateTo(`/analytics?customer_id=${customer_id}`, navigate)
|
||||||
`/analytics?customer_id=${customer.id}`,
|
|
||||||
navigate,
|
|
||||||
env
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Analyse Events
|
Analyse Events
|
||||||
@@ -65,7 +55,13 @@ export const CustomerEventsList = ({
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<div className="flex pl-10 items-center h-10">
|
||||||
<p className="text-t3 text-sm">
|
<p className="text-t3 text-sm">
|
||||||
No events received for this customer
|
No events received for this customer
|
||||||
@@ -86,38 +82,42 @@ export const CustomerEventsList = ({
|
|||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{events.map((event: any) => (
|
{events &&
|
||||||
<Row
|
events.map((event: any) => (
|
||||||
key={event.id}
|
<Row
|
||||||
className={cn("grid-cols-12 pr-0", showEntityView && "grid-cols-15")}
|
key={event.id}
|
||||||
onClick={() => setSelectedEvent(event)}
|
className={cn(
|
||||||
>
|
"grid-cols-12 pr-0",
|
||||||
<Item className="col-span-3 font-mono">{event.event_name}</Item>
|
showEntityView && "grid-cols-15"
|
||||||
|
)}
|
||||||
|
onClick={() => setSelectedEvent(event)}
|
||||||
|
>
|
||||||
|
<Item className="col-span-3 font-mono">{event.event_name}</Item>
|
||||||
|
|
||||||
<Item className="col-span-3 relative">
|
<Item className="col-span-3 relative">
|
||||||
<span className="font-mono truncate">
|
<span className="font-mono truncate">
|
||||||
{event.value || event.properties.value || 1}
|
{event.value || event.properties.value || 1}
|
||||||
</span>
|
</span>
|
||||||
</Item>
|
</Item>
|
||||||
<Item className="col-span-3 font-mono">
|
<Item className="col-span-3 font-mono">
|
||||||
<span className="text-t3">POST </span>
|
<span className="text-t3">POST </span>
|
||||||
<span className="text-lime-600">200</span>
|
<span className="text-lime-600">200</span>
|
||||||
</Item>
|
</Item>
|
||||||
{showEntityView && <Item className="col-span-3"></Item>}
|
{showEntityView && <Item className="col-span-3"></Item>}
|
||||||
<Item className="col-span-2 text-t3 text-xs">
|
<Item className="col-span-2 text-t3 text-xs">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
{formatUnixToDateTime(event.timestamp).date}{" "}
|
{formatUnixToDateTime(event.timestamp).date}{" "}
|
||||||
{formatUnixToDateTime(event.timestamp).time}{" "}
|
{formatUnixToDateTime(event.timestamp).time}{" "}
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
{formatUnixToDateTimeWithMs(event.timestamp)}
|
{formatUnixToDateTimeWithMs(event.timestamp)}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Item>
|
</Item>
|
||||||
<Item className="col-span-1" />
|
<Item className="col-span-1" />
|
||||||
</Row>
|
</Row>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<p className="text-t3 text-xs w-full text-center mt-2">
|
<p className="text-t3 text-xs w-full text-center mt-2">
|
||||||
Showing last 10 events
|
Showing last 10 events
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
import React from "react";
|
||||||
import SmallSpinner from "@/components/general/SmallSpinner";
|
import SmallSpinner from "@/components/general/SmallSpinner";
|
||||||
|
import AddCouponDialogContent from "./components/add-coupon/AddCouponDialogContent";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
@@ -14,23 +17,11 @@ import { Customer } from "@autumn/shared";
|
|||||||
import { useCustomerContext } from "./CustomerContext";
|
import { useCustomerContext } from "./CustomerContext";
|
||||||
import { CusService } from "@/services/customers/CusService";
|
import { CusService } from "@/services/customers/CusService";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
|
|
||||||
import { navigateTo } from "@/utils/genUtils";
|
import { navigateTo } from "@/utils/genUtils";
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { Dialog, DialogTrigger } from "@/components/ui/dialog";
|
import { Dialog, DialogTrigger } from "@/components/ui/dialog";
|
||||||
import AddCouponDialogContent from "./add-coupon/AddCouponDialogContent";
|
import UpdateCustomerDialog from "./components/UpdateCustomerDialog";
|
||||||
import { cn } from "@/lib/utils";
|
import { Delete, Settings } from "lucide-react";
|
||||||
import UpdateCustomerDialog from "./UpdateCustomerDialog";
|
|
||||||
import {
|
|
||||||
Delete,
|
|
||||||
Pen,
|
|
||||||
Pencil,
|
|
||||||
Settings,
|
|
||||||
Settings2,
|
|
||||||
Ticket,
|
|
||||||
Trash,
|
|
||||||
} from "lucide-react";
|
|
||||||
|
|
||||||
export const CustomerToolbar = ({
|
export const CustomerToolbar = ({
|
||||||
className,
|
className,
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import { CustomerProductList } from "./customer-product-list/CustomerProductList
|
|||||||
import { useFeaturesQuery } from "@/hooks/queries/useFeaturesQuery";
|
import { useFeaturesQuery } from "@/hooks/queries/useFeaturesQuery";
|
||||||
import { CustomerEntitlementsList } from "./entitlements/CustomerEntitlementsList";
|
import { CustomerEntitlementsList } from "./entitlements/CustomerEntitlementsList";
|
||||||
import { useCusReferralQuery } from "./hooks/useCusReferralQuery";
|
import { useCusReferralQuery } from "./hooks/useCusReferralQuery";
|
||||||
|
import { InvoicesTable } from "./InvoicesTable";
|
||||||
|
import { CustomerEventsList } from "./CustomerEventsList";
|
||||||
|
|
||||||
export default function CustomerView() {
|
export default function CustomerView() {
|
||||||
// const { customer_id } = useParams();
|
// const { customer_id } = useParams();
|
||||||
@@ -96,7 +98,7 @@ export default function CustomerView() {
|
|||||||
// rewards: rewardsData?.rewards,
|
// 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 ">
|
<div className="flex flex-col gap-4 w-full ">
|
||||||
<CustomerPageHeader />
|
<CustomerPageHeader />
|
||||||
<div className="flex w-full !pb-[50px]">
|
<div className="flex w-full !pb-[50px]">
|
||||||
@@ -108,12 +110,8 @@ export default function CustomerView() {
|
|||||||
<CustomerEntitlementsList />
|
<CustomerEntitlementsList />
|
||||||
<div className="flex flex-col gap-2"></div>
|
<div className="flex flex-col gap-2"></div>
|
||||||
|
|
||||||
{/* <InvoicesTable />
|
<InvoicesTable />
|
||||||
<CustomerEventsList
|
<CustomerEventsList />
|
||||||
customer={customer}
|
|
||||||
events={events}
|
|
||||||
env={env}
|
|
||||||
/> */}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,11 +8,15 @@ import { Row, Item } from "@/components/general/TableGrid";
|
|||||||
import { AdminHover } from "@/components/general/AdminHover";
|
import { AdminHover } from "@/components/general/AdminHover";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { CusProductEntityItem } from "./components/CusProductEntityItem";
|
import { CusProductEntityItem } from "./components/CusProductEntityItem";
|
||||||
|
import { useCusQuery } from "./hooks/useCusQuery";
|
||||||
|
|
||||||
export const InvoicesTable = () => {
|
export const InvoicesTable = () => {
|
||||||
const { env, invoices, products, entityId, entities, showEntityView } =
|
// const { env, invoices, products, entityId, entities, showEntityView } =
|
||||||
useCustomerContext();
|
// useCustomerContext();
|
||||||
const axiosInstance = useAxiosInstance({ env });
|
const { entityId, showEntityView } = useCustomerContext();
|
||||||
|
const { customer, products, entities } = useCusQuery();
|
||||||
|
const axiosInstance = useAxiosInstance();
|
||||||
|
const invoices = customer.invoices;
|
||||||
|
|
||||||
const entity = entities.find(
|
const entity = entities.find(
|
||||||
(e: any) => e.id === entityId || e.internal_id === entityId
|
(e: any) => e.id === entityId || e.internal_id === entityId
|
||||||
@@ -100,7 +104,7 @@ export const InvoicesTable = () => {
|
|||||||
>
|
>
|
||||||
{invoice.product_ids
|
{invoice.product_ids
|
||||||
.map((p: string) => {
|
.map((p: string) => {
|
||||||
return products.find((product: Product) => product.id === p)
|
return products.find((product: any) => product.id === p)
|
||||||
?.name;
|
?.name;
|
||||||
})
|
})
|
||||||
.join(", ")}
|
.join(", ")}
|
||||||
|
|||||||
@@ -16,18 +16,21 @@ import { useNavigate } from "react-router";
|
|||||||
import { getRedirectUrl, navigateTo } from "@/utils/genUtils";
|
import { getRedirectUrl, navigateTo } from "@/utils/genUtils";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { OrgService } from "@/services/OrgService";
|
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 SmallSpinner from "@/components/general/SmallSpinner";
|
||||||
import { Blend, Search } from "lucide-react";
|
import { Blend, Search } from "lucide-react";
|
||||||
import { useOrg } from "@/hooks/common/useOrg";
|
import { useOrg } from "@/hooks/common/useOrg";
|
||||||
import { useCustomer } from "autumn-js/react";
|
import { useCustomer } from "autumn-js/react";
|
||||||
|
import { useCusQuery } from "../hooks/useCusQuery";
|
||||||
|
|
||||||
function AddProduct({
|
function AddProduct({
|
||||||
setMultiAttachOpen,
|
setMultiAttachOpen,
|
||||||
}: {
|
}: {
|
||||||
setMultiAttachOpen: (open: boolean) => void;
|
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 axiosInstance = useAxiosInstance({ env });
|
||||||
const { customer: autumnCustomer } = useCustomer();
|
const { customer: autumnCustomer } = useCustomer();
|
||||||
|
|
||||||
@@ -36,7 +39,7 @@ function AddProduct({
|
|||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const { org } = useOrg();
|
const { org } = useOrg();
|
||||||
|
|
||||||
const filteredProducts = products.filter((product: Product) => {
|
const filteredProducts = products.filter((product: ProductV2) => {
|
||||||
if (product.is_add_on && !searchQuery) return true;
|
if (product.is_add_on && !searchQuery) return true;
|
||||||
|
|
||||||
const entity = entities.find((e: Entity) => e.id === entityId);
|
const entity = entities.find((e: Entity) => e.id === entityId);
|
||||||
@@ -114,7 +117,7 @@ function AddProduct({
|
|||||||
No new products found
|
No new products found
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
filteredProducts.map((product: Product) => (
|
filteredProducts.map((product: ProductV2) => (
|
||||||
<DropdownProductItem
|
<DropdownProductItem
|
||||||
key={product.id}
|
key={product.id}
|
||||||
product={product}
|
product={product}
|
||||||
@@ -146,7 +149,7 @@ const DropdownProductItem = ({
|
|||||||
product,
|
product,
|
||||||
handleAddProduct,
|
handleAddProduct,
|
||||||
}: {
|
}: {
|
||||||
product: Product;
|
product: ProductV2;
|
||||||
handleAddProduct: any;
|
handleAddProduct: any;
|
||||||
}) => {
|
}) => {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { DialogFooter } from "@/components/ui/dialog";
|
import { DialogFooter } from "@/components/ui/dialog";
|
||||||
import { getBackendErr, navigateTo } from "@/utils/genUtils";
|
import { getBackendErr, navigateTo } from "@/utils/genUtils";
|
||||||
import { Reward, CreateCustomer, Customer } from "@autumn/shared";
|
import { CreateCustomer, Customer } from "@autumn/shared";
|
||||||
import { useEffect, useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { DialogTitle } from "@/components/ui/dialog";
|
import { DialogTitle } from "@/components/ui/dialog";
|
||||||
import { DialogContent } 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 { useAxiosInstance } from "@/services/useAxiosInstance";
|
||||||
import { useEnv } from "@/utils/envUtils";
|
import { useEnv } from "@/utils/envUtils";
|
||||||
import { CustomerConfig } from "./CustomerConfig";
|
import { CustomerConfig } from "./CustomerConfig";
|
||||||
import { CusService } from "@/services/customers/CusService";
|
import { CusService } from "@/services/customers/CusService";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import { useCusQuery } from "./hooks/useCusQuery";
|
import { useCusQuery } from "../hooks/useCusQuery";
|
||||||
|
|
||||||
const UpdateCustomerDialog = ({
|
const UpdateCustomerDialog = ({
|
||||||
selectedCustomer,
|
selectedCustomer,
|
||||||
@@ -7,7 +7,6 @@ import { Select, SelectItem } from "@/components/ui/select";
|
|||||||
import { DialogContent, DialogTitle } from "@/components/ui/dialog";
|
import { DialogContent, DialogTitle } from "@/components/ui/dialog";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useCustomerContext } from "../CustomerContext";
|
|
||||||
import { getBackendErr } from "@/utils/genUtils";
|
import { getBackendErr } from "@/utils/genUtils";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { CusService } from "@/services/customers/CusService";
|
import { CusService } from "@/services/customers/CusService";
|
||||||
@@ -15,8 +14,8 @@ import { useAxiosInstance } from "@/services/useAxiosInstance";
|
|||||||
import { getOriginalCouponId } from "@/utils/product/couponUtils";
|
import { getOriginalCouponId } from "@/utils/product/couponUtils";
|
||||||
import { WarningBox } from "@/components/general/modal-components/WarningBox";
|
import { WarningBox } from "@/components/general/modal-components/WarningBox";
|
||||||
import { useRewardsQuery } from "@/hooks/queries/useRewardsQuery";
|
import { useRewardsQuery } from "@/hooks/queries/useRewardsQuery";
|
||||||
import { useCusQuery } from "../hooks/useCusQuery";
|
import { useCusQuery } from "../../hooks/useCusQuery";
|
||||||
import { useCusReferralQuery } from "../hooks/useCusReferralQuery";
|
import { useCusReferralQuery } from "../../hooks/useCusReferralQuery";
|
||||||
|
|
||||||
const AddCouponDialogContent = ({
|
const AddCouponDialogContent = ({
|
||||||
setOpen,
|
setOpen,
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
|
import UpdateCustomerDialog from "../UpdateCustomerDialog";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Accordion } from "@/components/ui/accordion";
|
import { Accordion } from "@/components/ui/accordion";
|
||||||
import { Dialog } from "@/components/ui/dialog";
|
import { Dialog } from "@/components/ui/dialog";
|
||||||
import { CustomerRewards } from "./customer-rewards";
|
import { CustomerRewards } from "./customer-rewards";
|
||||||
import { useCustomerContext } from "../../CustomerContext";
|
|
||||||
import UpdateCustomerDialog from "../../UpdateCustomerDialog";
|
|
||||||
import { CustomerToolbar } from "../../CustomerToolbar";
|
import { CustomerToolbar } from "../../CustomerToolbar";
|
||||||
import { CustomerDetails } from "./CustomerDetails";
|
import { CustomerDetails } from "./CustomerDetails";
|
||||||
import { CustomerEntities } from "./CustomerEntities";
|
import { CustomerEntities } from "./CustomerEntities";
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import AddCouponDialogContent from "../../components/add-coupon/AddCouponDialogContent";
|
||||||
import { SideAccordion } from "@/components/general/SideAccordion";
|
import { SideAccordion } from "@/components/general/SideAccordion";
|
||||||
import { getRedirectUrl } from "@/utils/genUtils";
|
import { getRedirectUrl } from "@/utils/genUtils";
|
||||||
import { Dialog } from "@/components/ui/dialog";
|
import { Dialog } from "@/components/ui/dialog";
|
||||||
@@ -15,11 +16,9 @@ import { ArrowUpRightFromSquare } from "lucide-react";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
import { useCusQuery } from "../../hooks/useCusQuery";
|
|
||||||
import { useEnv } from "@/utils/envUtils";
|
import { useEnv } from "@/utils/envUtils";
|
||||||
import { useCusReferralQuery } from "../../hooks/useCusReferralQuery";
|
import { useCusReferralQuery } from "../../hooks/useCusReferralQuery";
|
||||||
import { useRewardsQuery } from "@/hooks/queries/useRewardsQuery";
|
import { useRewardsQuery } from "@/hooks/queries/useRewardsQuery";
|
||||||
import AddCouponDialogContent from "../../add-coupon/AddCouponDialogContent";
|
|
||||||
|
|
||||||
export const CustomerRewards = () => {
|
export const CustomerRewards = () => {
|
||||||
// const { discount, env } = useCustomerContext();
|
// const { discount, env } = useCustomerContext();
|
||||||
|
|||||||
@@ -119,13 +119,13 @@ export const CustomerProductList = () => {
|
|||||||
>
|
>
|
||||||
Show Expired
|
Show Expired
|
||||||
</Button>
|
</Button>
|
||||||
{/* <CreateEntitlement buttonType={"feature"} /> */}
|
|
||||||
<div className="flex items-center gap-0">
|
<div className="flex items-center gap-0">
|
||||||
{/* <MultiAttachDialog
|
<MultiAttachDialog
|
||||||
open={multiAttachOpen}
|
open={multiAttachOpen}
|
||||||
setOpen={setMultiAttachOpen}
|
setOpen={setMultiAttachOpen}
|
||||||
/>
|
/>
|
||||||
<AddProduct setMultiAttachOpen={setMultiAttachOpen} /> */}
|
<AddProduct setMultiAttachOpen={setMultiAttachOpen} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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 { products, isLoading: productsLoading } = useProductsQuery();
|
||||||
const { features, isLoading: featuresLoading } = useFeaturesQuery();
|
const { features, isLoading: featuresLoading } = useFeaturesQuery();
|
||||||
|
|
||||||
const customer = cachedCustomer || data?.customer;
|
const customer = data?.customer || cachedCustomer;
|
||||||
const cusWithCacheLoading = cachedCustomer ? false : customerLoading;
|
const cusWithCacheLoading = cachedCustomer ? false : customerLoading;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { FullProduct } from "@autumn/shared";
|
import { FullProduct, ProductV2 } from "@autumn/shared";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useAxiosInstance } from "@/services/useAxiosInstance";
|
import { useAxiosInstance } from "@/services/useAxiosInstance";
|
||||||
@@ -31,6 +31,8 @@ import { formatAmount } from "@/utils/product/productItemUtils";
|
|||||||
import { formatUnixToDate } from "@/utils/formatUtils/formatDateUtils";
|
import { formatUnixToDate } from "@/utils/formatUtils/formatDateUtils";
|
||||||
import { AddRewardButton, MultiAttachRewards } from "./MultiAttachRewards";
|
import { AddRewardButton, MultiAttachRewards } from "./MultiAttachRewards";
|
||||||
import { useAxiosSWR } from "@/services/useAxiosSwr";
|
import { useAxiosSWR } from "@/services/useAxiosSwr";
|
||||||
|
import { useCusQuery } from "../../hooks/useCusQuery";
|
||||||
|
import { useOrg } from "@/hooks/common/useOrg";
|
||||||
|
|
||||||
export const MultiAttachDialog = ({
|
export const MultiAttachDialog = ({
|
||||||
open,
|
open,
|
||||||
@@ -39,7 +41,9 @@ export const MultiAttachDialog = ({
|
|||||||
open: boolean;
|
open: boolean;
|
||||||
setOpen: (open: boolean) => void;
|
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();
|
const axiosInstance = useAxiosInstance();
|
||||||
|
|
||||||
@@ -160,7 +164,7 @@ export const MultiAttachDialog = ({
|
|||||||
window.open(getStripeInvoiceLink(data.invoice), "_blank");
|
window.open(getStripeInvoiceLink(data.invoice), "_blank");
|
||||||
}
|
}
|
||||||
|
|
||||||
await cusMutate();
|
await refetch();
|
||||||
toast.success("Products attached successfully");
|
toast.success("Products attached successfully");
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -204,12 +208,14 @@ export const MultiAttachDialog = ({
|
|||||||
<SelectContent className="max-h-[300px] overflow-y-auto">
|
<SelectContent className="max-h-[300px] overflow-y-auto">
|
||||||
{products
|
{products
|
||||||
.filter(
|
.filter(
|
||||||
(p: FullProduct) =>
|
(p: ProductV2) =>
|
||||||
!productOptions
|
!productOptions
|
||||||
.map((o, i) => (i !== index ? o.product : null))
|
.map((o, i) =>
|
||||||
|
i !== index ? o.product_id : null
|
||||||
|
)
|
||||||
.includes(p.id)
|
.includes(p.id)
|
||||||
)
|
)
|
||||||
.map((product: FullProduct) => (
|
.map((product: ProductV2) => (
|
||||||
<SelectItem key={product.id} value={product.id}>
|
<SelectItem key={product.id} value={product.id}>
|
||||||
{product.name}
|
{product.name}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { FullCustomer } from "@autumn/shared";
|
import { FullCustomer } from "@autumn/shared";
|
||||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||||
|
import { useEffect } from "react";
|
||||||
import { useCustomersQueryStates } from "./useCustomersQueryStates";
|
import { useCustomersQueryStates } from "./useCustomersQueryStates";
|
||||||
import { useAxiosInstance } from "@/services/useAxiosInstance";
|
import { useAxiosInstance } from "@/services/useAxiosInstance";
|
||||||
|
|
||||||
@@ -7,19 +8,12 @@ export const useFullCusSearchQuery = () => {
|
|||||||
const { queryStates } = useCustomersQueryStates();
|
const { queryStates } = useCustomersQueryStates();
|
||||||
const axiosInstance = useAxiosInstance();
|
const axiosInstance = useAxiosInstance();
|
||||||
|
|
||||||
const { data: fullCustomersData } = useQuery<{
|
const { refetch } = useQuery<{
|
||||||
fullCustomers: FullCustomer[];
|
fullCustomers: FullCustomer[];
|
||||||
}>({
|
}>({
|
||||||
queryKey: [
|
queryKey: ["full_customers"],
|
||||||
"full_customers",
|
// Pass AbortSignal so previous requests are canceled when a new refetch starts
|
||||||
queryStates.page,
|
queryFn: async ({ signal }) => {
|
||||||
queryStates.status,
|
|
||||||
queryStates.version,
|
|
||||||
queryStates.none,
|
|
||||||
queryStates.q,
|
|
||||||
],
|
|
||||||
queryFn: async () => {
|
|
||||||
console.log("Fetching full customers: ", queryStates.q);
|
|
||||||
const { data } = await axiosInstance.post(
|
const { data } = await axiosInstance.post(
|
||||||
`/customers/all/full_customers`,
|
`/customers/all/full_customers`,
|
||||||
{
|
{
|
||||||
@@ -31,12 +25,31 @@ export const useFullCusSearchQuery = () => {
|
|||||||
version: queryStates.version,
|
version: queryStates.version,
|
||||||
none: queryStates.none,
|
none: queryStates.none,
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
|
{ signal }
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log("data", data);
|
console.log(`Fetched ${data?.fullCustomers.length} full customers`);
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
placeholderData: keepPreviousData,
|
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 { AppEnv, UpdateProductSchema } from "@autumn/shared";
|
||||||
import { ProductService } from "@/services/products/ProductService";
|
import { ProductService } from "@/services/products/ProductService";
|
||||||
import { getBackendErr } from "@/utils/genUtils";
|
import { getBackendErr } from "@/utils/genUtils";
|
||||||
import { UpdateProductButton } from "@/views/products/product/components/UpdateProductButton";
|
|
||||||
import { useProductChangedAlert } from "./hooks/useProductChangedAlert";
|
import { useProductChangedAlert } from "./hooks/useProductChangedAlert";
|
||||||
import { useProductData } from "./hooks/useProductData";
|
import { useProductData } from "./hooks/useProductData";
|
||||||
|
import { UpdateProductButton } from "./components/UpdateProductButton";
|
||||||
|
|
||||||
function ProductView({ env }: { env: AppEnv }) {
|
function ProductView({ env }: { env: AppEnv }) {
|
||||||
const axiosInstance = useAxiosInstance();
|
const axiosInstance = useAxiosInstance();
|
||||||
|
|
||||||
const { product_id } = useParams();
|
const { product_id } = useParams();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const version = searchParams.get("version");
|
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