diff --git a/apps/web/app/api/customers/[id]/activity/route.ts b/apps/web/app/api/customers/[id]/activity/route.ts index 95645768b93..0188fdb429c 100644 --- a/apps/web/app/api/customers/[id]/activity/route.ts +++ b/apps/web/app/api/customers/[id]/activity/route.ts @@ -30,13 +30,9 @@ export const GET = withWorkspace(async ({ workspace, params, session }) => { } let [events, link] = await Promise.all([ - getCustomerEvents( - { customerId: customer.id, clickId: customer.clickId }, - { - sortOrder: "desc", - interval: "1y", - }, - ), + getCustomerEvents({ + customerId: customer.id, + }), prisma.link.findUniqueOrThrow({ where: { diff --git a/apps/web/app/api/embed/referrals/earnings/route.ts b/apps/web/app/api/embed/referrals/earnings/route.ts index 683da06ffad..6fb19d93596 100644 --- a/apps/web/app/api/embed/referrals/earnings/route.ts +++ b/apps/web/app/api/embed/referrals/earnings/route.ts @@ -1,5 +1,5 @@ import { withReferralsEmbedToken } from "@/lib/embed/referrals/auth"; -import { SALES_PAGE_SIZE } from "@/lib/partners/constants"; +import { REFERRALS_EMBED_EARNINGS_LIMIT } from "@/lib/partners/constants"; import z from "@/lib/zod"; import { PartnerEarningsSchema } from "@/lib/zod/schemas/partner-profile"; import { prisma } from "@dub/prisma"; @@ -45,8 +45,8 @@ export const GET = withReferralsEmbedToken( }, }, }, - take: SALES_PAGE_SIZE, - skip: (page - 1) * SALES_PAGE_SIZE, + take: REFERRALS_EMBED_EARNINGS_LIMIT, + skip: (page - 1) * REFERRALS_EMBED_EARNINGS_LIMIT, orderBy: { createdAt: "desc", }, diff --git a/apps/web/app/api/partner-profile/programs/[programId]/customers/[customerId]/activity/route.ts b/apps/web/app/api/partner-profile/programs/[programId]/customers/[customerId]/activity/route.ts deleted file mode 100644 index 16790085889..00000000000 --- a/apps/web/app/api/partner-profile/programs/[programId]/customers/[customerId]/activity/route.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { getCustomerEvents } from "@/lib/analytics/get-customer-events"; -import { DubApiError } from "@/lib/api/errors"; -import { decodeLinkIfCaseSensitive } from "@/lib/api/links/case-sensitivity"; -import { getProgramEnrollmentOrThrow } from "@/lib/api/programs/get-program-enrollment-or-throw"; -import { withPartnerProfile } from "@/lib/auth/partner"; -import { customerActivityResponseSchema } from "@/lib/zod/schemas/customer-activity"; -import { prisma } from "@dub/prisma"; -import { NextResponse } from "next/server"; - -// GET /api/partner-profile/programs/:programId/customers/:customerId/activity – Get a customer's activity by ID -export const GET = withPartnerProfile(async ({ partner, params }) => { - const { customerId, programId } = params; - - const { program } = await getProgramEnrollmentOrThrow({ - partnerId: partner.id, - programId: programId, - }); - - if (program.slug === "framer") { - throw new DubApiError({ - code: "forbidden", - message: "Framer program does not support customer activity", - }); - } - - const customer = await prisma.customer.findUnique({ - where: { - id: customerId, - }, - include: { - link: { - include: { - programEnrollment: { - include: { - partner: true, - program: true, - }, - }, - }, - }, - }, - }); - - if ( - !customer || - ![ - customer?.link?.programEnrollment?.programId, - customer?.link?.programEnrollment?.program.slug, - ].includes(program.id) - ) { - throw new DubApiError({ - code: "not_found", - message: - "Customer not found. Make sure you're using the correct customer ID (e.g. `cus_3TagGjzRzmsFJdH8od2BNCsc`).", - }); - } - - if (!customer.linkId) { - return NextResponse.json( - customerActivityResponseSchema.parse({ - customer, - events: [], - ltv: 0, - timeToLead: null, - timeToSale: null, - link: null, - }), - ); - } - - let [events, link] = await Promise.all([ - getCustomerEvents( - { customerId: customer.id, clickId: customer.clickId }, - { - sortOrder: "desc", - interval: "1y", - }, - ), - - prisma.link.findUniqueOrThrow({ - where: { - id: customer.linkId!, - }, - select: { - id: true, - domain: true, - key: true, - shortLink: true, - folderId: true, - }, - }), - ]); - - link = decodeLinkIfCaseSensitive(link); - - // Find the LTV of the customer - // TODO: Calculate this from all events, not limited - const ltv = events.reduce((acc, event) => { - if (event.event === "sale" && event.saleAmount) { - acc += Number(event.saleAmount); - } - - return acc; - }, 0); - - // Find the time to lead of the customer - const timeToLead = - customer.clickedAt && customer.createdAt - ? customer.createdAt.getTime() - customer.clickedAt.getTime() - : null; - - // Find the time to first sale of the customer - // TODO: Calculate this from all events, not limited - const firstSale = events.filter(({ event }) => event === "sale").pop(); - - const timeToSale = - firstSale && customer.createdAt - ? new Date(firstSale.timestamp).getTime() - customer.createdAt.getTime() - : null; - - return NextResponse.json( - customerActivityResponseSchema.parse({ - ltv, - timeToLead, - timeToSale, - events, - link, - }), - ); -}); diff --git a/apps/web/app/api/partner-profile/programs/[programId]/customers/[customerId]/route.ts b/apps/web/app/api/partner-profile/programs/[programId]/customers/[customerId]/route.ts index 39b2e6ba44d..0dfd98e3a41 100644 --- a/apps/web/app/api/partner-profile/programs/[programId]/customers/[customerId]/route.ts +++ b/apps/web/app/api/partner-profile/programs/[programId]/customers/[customerId]/route.ts @@ -1,3 +1,4 @@ +import { getCustomerEvents } from "@/lib/analytics/get-customer-events"; import { transformCustomer } from "@/lib/api/customers/transform-customer"; import { DubApiError } from "@/lib/api/errors"; import { getProgramEnrollmentOrThrow } from "@/lib/api/programs/get-program-enrollment-or-throw"; @@ -11,67 +12,78 @@ import { NextResponse } from "next/server"; export const GET = withPartnerProfile(async ({ partner, params }) => { const { customerId, programId } = params; - const { program } = await getProgramEnrollmentOrThrow({ + const { program, links } = await getProgramEnrollmentOrThrow({ partnerId: partner.id, programId: programId, }); - if (program.slug === "framer") { - throw new DubApiError({ - code: "forbidden", - message: "Framer program does not support customer profile", - }); - } - const customer = await prisma.customer.findUnique({ where: { id: customerId, }, - include: { - link: { - include: { - programEnrollment: { - include: { - partner: true, - program: true, - }, - }, - }, - }, - }, }); - if ( - !customer || - ![ - customer?.link?.programEnrollment?.programId, - customer?.link?.programEnrollment?.program.slug, - ].includes(program.id) - ) { + if (!customer || customer?.projectId !== program.workspaceId) { throw new DubApiError({ code: "not_found", - message: - "Customer not found. Make sure you're using the correct customer ID (e.g. `cus_3TagGjzRzmsFJdH8od2BNCsc`).", + message: "Customer is not part of this program.", }); } - customer.avatar = null; - customer.email; + const events = await getCustomerEvents({ + customerId: customer.id, + linkIds: links.map((link) => link.id), + }); + + if (events.length === 0) { + throw new DubApiError({ + code: "not_found", + message: "Customer is not attributed to any links by this partner.", + }); + } + + // get the first partner link that this customer interacted with + const firstLinkId = events[events.length - 1].link_id; + const link = links.find((link) => link.id === firstLinkId); + + // Find the LTV of the customer + // TODO: Calculate this from all events, not limited + const ltv = events.reduce((acc, event) => { + if (event.event === "sale" && event.saleAmount) { + acc += Number(event.saleAmount); + } + + return acc; + }, 0); + + // Find the time to lead of the customer + const timeToLead = + customer.clickedAt && customer.createdAt + ? customer.createdAt.getTime() - customer.clickedAt.getTime() + : null; + + // Find the time to first sale of the customer + // TODO: Calculate this from all events, not limited + const firstSale = events.filter(({ event }) => event === "sale").pop(); + + const timeToSale = + firstSale && customer.createdAt + ? new Date(firstSale.timestamp).getTime() - customer.createdAt.getTime() + : null; return NextResponse.json( - PartnerProfileCustomerSchema.parse( - transformCustomer({ + PartnerProfileCustomerSchema.parse({ + ...transformCustomer({ ...customer, email: customer.email || customer.name || generateRandomName(), - link: customer.link - ? { - ...customer.link, - programEnrollment: customer.link.programEnrollment - ? { ...customer.link.programEnrollment, program: undefined } - : null, - } - : null, }), - ), + activity: { + ltv, + timeToLead, + timeToSale, + events, + link, + }, + }), ); }); diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/customers/[customerId]/page-client.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/customers/[customerId]/page-client.tsx index 6ce72b7a451..c32bef2d761 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/customers/[customerId]/page-client.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/customers/[customerId]/page-client.tsx @@ -1,5 +1,6 @@ "use client"; +import { CUSTOMER_PAGE_EVENTS_LIMIT } from "@/lib/partners/constants"; import useCustomer from "@/lib/swr/use-customer"; import useWorkspace from "@/lib/swr/use-workspace"; import { @@ -165,7 +166,7 @@ const SalesTable = memo(({ customerId }: { customerId: string }) => { const { id: workspaceId, slug } = useWorkspace(); const { data: salesData, isLoading: isSalesLoading } = useSWR( - `/api/events?event=sales&interval=all&limit=8&customerId=${customerId}&workspaceId=${workspaceId}`, + `/api/events?event=sales&interval=all&limit=${CUSTOMER_PAGE_EVENTS_LIMIT}&customerId=${customerId}&workspaceId=${workspaceId}`, fetcher, { keepPreviousData: true, @@ -175,7 +176,9 @@ const SalesTable = memo(({ customerId }: { customerId: string }) => { const { data: totalSales, isLoading: isTotalSalesLoading } = useSWR<{ sales: number; }>( - `/api/analytics?event=sales&interval=all&groupBy=count&customerId=${customerId}&workspaceId=${workspaceId}`, + // Only fetch total sales count if the sales data is equal to the limit + salesData?.length === CUSTOMER_PAGE_EVENTS_LIMIT && + `/api/analytics?event=sales&interval=all&groupBy=count&customerId=${customerId}&workspaceId=${workspaceId}`, fetcher, { keepPreviousData: true, @@ -185,9 +188,11 @@ const SalesTable = memo(({ customerId }: { customerId: string }) => { return ( ); }); @@ -199,22 +204,28 @@ const PartnerEarningsTable = memo( const { data: commissions, isLoading: isComissionsLoading } = useSWR< CommissionResponse[] >( - `/api/programs/${programId}/commissions?customerId=${customerId}&workspaceId=${workspaceId}&pageSize=8`, + `/api/programs/${programId}/commissions?customerId=${customerId}&workspaceId=${workspaceId}&pageSize=${CUSTOMER_PAGE_EVENTS_LIMIT}`, fetcher, ); const { data: totalCommissions, isLoading: isTotalCommissionsLoading } = useSWR<{ all: { count: number } }>( - `/api/programs/${programId}/commissions/count?customerId=${customerId}&workspaceId=${workspaceId}`, + // Only fetch total earnings count if the earnings data is equal to the limit + commissions?.length === CUSTOMER_PAGE_EVENTS_LIMIT && + `/api/programs/${programId}/commissions/count?customerId=${customerId}&workspaceId=${workspaceId}`, fetcher, ); return ( ); }, diff --git a/apps/web/app/app.dub.co/embed/referrals/earnings.tsx b/apps/web/app/app.dub.co/embed/referrals/earnings.tsx index 37e898edf7d..feeb622819e 100644 --- a/apps/web/app/app.dub.co/embed/referrals/earnings.tsx +++ b/apps/web/app/app.dub.co/embed/referrals/earnings.tsx @@ -1,4 +1,4 @@ -import { SALES_PAGE_SIZE } from "@/lib/partners/constants"; +import { REFERRALS_EMBED_EARNINGS_LIMIT } from "@/lib/partners/constants"; import { PartnerEarningsResponse } from "@/lib/types"; import { CommissionStatusBadges } from "@/ui/partners/commission-status-badges"; import { Gift, StatusBadge, Table, usePagination, useTable } from "@dub/ui"; @@ -13,7 +13,9 @@ import { motion } from "framer-motion"; import useSWR from "swr"; export function ReferralsEmbedEarnings({ salesCount }: { salesCount: number }) { - const { pagination, setPagination } = usePagination(SALES_PAGE_SIZE); + const { pagination, setPagination } = usePagination( + REFERRALS_EMBED_EARNINGS_LIMIT, + ); const { data: earnings, isLoading } = useSWR( `/api/embed/referrals/earnings?page=${pagination.pageIndex}`, fetcher, diff --git a/apps/web/app/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/customers/[customerId]/page-client.tsx b/apps/web/app/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/customers/[customerId]/page-client.tsx index bf61f6c2ad8..a4a3298efdd 100644 --- a/apps/web/app/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/customers/[customerId]/page-client.tsx +++ b/apps/web/app/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/customers/[customerId]/page-client.tsx @@ -1,10 +1,10 @@ "use client"; +import { CUSTOMER_PAGE_EVENTS_LIMIT } from "@/lib/partners/constants"; import useProgramEnrollment from "@/lib/swr/use-program-enrollment"; import { - CustomerActivityResponse, - CustomerEnriched, PartnerEarningsResponse, + PartnerProfileCustomerProps, } from "@/lib/types"; import { CustomerActivityList } from "@/ui/customers/customer-activity-list"; import { CustomerDetailsColumn } from "@/ui/customers/customer-details-column"; @@ -24,23 +24,12 @@ export function ProgramCustomerPageClient() { customerId: string; }>(); - const { - data: customer, - isLoading, - error, - } = useSWR( + const { data: customer, isLoading } = useSWR( `/api/partner-profile/programs/${programSlug}/customers/${customerId}`, fetcher, ); - const { data: customerActivity, isLoading: isCustomerActivityLoading } = - useSWR( - customer && - `/api/partner-profile/programs/${programSlug}/customers/${customer.id}/activity`, - fetcher, - ); - - if (!customer && !isLoading && !error) notFound(); + if (!customer && !isLoading) notFound(); return (
@@ -101,8 +90,8 @@ export function ProgramCustomerPageClient() { Activity
@@ -111,8 +100,8 @@ export function ProgramCustomerPageClient() {
@@ -126,7 +115,7 @@ const EarningsTable = memo(({ customerId }: { customerId: string }) => { const { data: earningsData, isLoading: isEarningsLoading } = useSWR< PartnerEarningsResponse[] >( - `/api/partner-profile/programs/${programSlug}/earnings?interval=all&pageSize=8&customerId=${customerId}`, + `/api/partner-profile/programs/${programSlug}/earnings?interval=all&pageSize=${CUSTOMER_PAGE_EVENTS_LIMIT}&customerId=${customerId}`, fetcher, { keepPreviousData: true, @@ -136,7 +125,9 @@ const EarningsTable = memo(({ customerId }: { customerId: string }) => { const { data: totalEarnings, isLoading: isTotalEarningsLoading } = useSWR<{ count: number; }>( - `/api/partner-profile/programs/${programSlug}/earnings/count?interval=all&customerId=${customerId}`, + // Only fetch total earnings count if the earnings data is equal to the limit + earningsData?.length === CUSTOMER_PAGE_EVENTS_LIMIT && + `/api/partner-profile/programs/${programSlug}/earnings/count?interval=all&customerId=${customerId}`, fetcher, { keepPreviousData: true, @@ -146,9 +137,13 @@ const EarningsTable = memo(({ customerId }: { customerId: string }) => { return ( ); }); diff --git a/apps/web/app/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/customers/[customerId]/page.tsx b/apps/web/app/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/customers/[customerId]/page.tsx index 67a7a9e209d..4982498f9dc 100644 --- a/apps/web/app/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/customers/[customerId]/page.tsx +++ b/apps/web/app/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/customers/[customerId]/page.tsx @@ -1,16 +1,8 @@ import { PageContent } from "@/ui/layout/page-content"; import { MaxWidthWrapper } from "@dub/ui"; -import { redirect } from "next/navigation"; import { ProgramCustomerPageClient } from "./page-client"; -export default function ProgramCustomer({ - params, -}: { - params: { programSlug: string; customerId: string }; -}) { - if (params.programSlug === "framer") { - redirect("/programs/framer"); - } +export default function ProgramCustomer() { return ( diff --git a/apps/web/app/partners.dub.co/(dashboard)/settings/payouts/payout-details-sheet.tsx b/apps/web/app/partners.dub.co/(dashboard)/settings/payouts/payout-details-sheet.tsx index 2cb91a0e2be..6c41e006453 100644 --- a/apps/web/app/partners.dub.co/(dashboard)/settings/payouts/payout-details-sheet.tsx +++ b/apps/web/app/partners.dub.co/(dashboard)/settings/payouts/payout-details-sheet.tsx @@ -1,4 +1,4 @@ -import { SHEET_MAX_ITEMS } from "@/lib/partners/constants"; +import { PAYOUTS_SHEET_ITEMS_LIMIT } from "@/lib/partners/constants"; import usePartnerProfile from "@/lib/swr/use-partner-profile"; import { PartnerEarningsResponse, PartnerPayoutResponse } from "@/lib/types"; import { CommissionTypeIcon } from "@/ui/partners/comission-type-icon"; @@ -43,7 +43,7 @@ function PayoutDetailsSheetContent({ payout }: PayoutDetailsSheetProps) { error, } = useSWR( partner - ? `/api/partner-profile/programs/${payout.program.id}/earnings?payoutId=${payout.id}&interval=all&pageSize=${SHEET_MAX_ITEMS}` + ? `/api/partner-profile/programs/${payout.program.id}/earnings?payoutId=${payout.id}&interval=all&pageSize=${PAYOUTS_SHEET_ITEMS_LIMIT}` : undefined, fetcher, ); diff --git a/apps/web/lib/analytics/get-customer-events.ts b/apps/web/lib/analytics/get-customer-events.ts index 1365d9604a8..3ac23ddbb0f 100644 --- a/apps/web/lib/analytics/get-customer-events.ts +++ b/apps/web/lib/analytics/get-customer-events.ts @@ -10,25 +10,14 @@ import { } from "../zod/schemas/clicks"; import { leadEventResponseSchema } from "../zod/schemas/leads"; import { saleEventResponseSchema } from "../zod/schemas/sales"; -import { EventsFilters } from "./types"; -import { getStartEndDates } from "./utils/get-start-end-dates"; - -export const getCustomerEvents = async ( - { customerId, clickId }: { customerId: string; clickId?: string | null }, - params: Pick< - EventsFilters, - "sortOrder" | "start" | "end" | "dataAvailableFrom" | "interval" - >, -) => { - let { sortOrder, start, end, dataAvailableFrom, interval } = params; - - const { startDate, endDate } = getStartEndDates({ - interval, - start, - end, - dataAvailableFrom, - }); +export const getCustomerEvents = async ({ + customerId, + linkIds, +}: { + customerId: string; + linkIds?: string[]; +}) => { const pipe = tb.buildPipe({ pipe: "v2_customer_events", parameters: z.any(), // TODO @@ -36,12 +25,8 @@ export const getCustomerEvents = async ( }); const response = await pipe({ - ...params, customerId, - ...(clickId ? { clickId } : {}), - order: sortOrder, - start: startDate.toISOString().replace("T", " ").replace("Z", ""), - end: endDate.toISOString().replace("T", " ").replace("Z", ""), + ...(linkIds ? { linkIds } : {}), }); const linksMap = await getLinksMap(response.data.map((d) => d.link_id)); diff --git a/apps/web/lib/partners/constants.ts b/apps/web/lib/partners/constants.ts index b89af852606..7953d03b5ed 100644 --- a/apps/web/lib/partners/constants.ts +++ b/apps/web/lib/partners/constants.ts @@ -1,5 +1,6 @@ -export const SHEET_MAX_ITEMS = 10; -export const SALES_PAGE_SIZE = 8; +export const PAYOUTS_SHEET_ITEMS_LIMIT = 10; +export const REFERRALS_EMBED_EARNINGS_LIMIT = 8; +export const CUSTOMER_PAGE_EVENTS_LIMIT = 8; export const PAYOUT_FEES = { business: { diff --git a/apps/web/lib/types.ts b/apps/web/lib/types.ts index 6a09e025261..2998c899971 100644 --- a/apps/web/lib/types.ts +++ b/apps/web/lib/types.ts @@ -2,6 +2,7 @@ import z from "@/lib/zod"; import { metaTagsSchema } from "@/lib/zod/schemas/metatags"; import { PartnerEarningsSchema, + PartnerProfileCustomerSchema, PartnerProfileLinkSchema, } from "@/lib/zod/schemas/partner-profile"; import { DirectorySyncProviders } from "@boxyhq/saml-jackson"; @@ -388,6 +389,10 @@ export type PartnerProps = z.infer; export type ProgramPartnerLinkProps = z.infer; +export type PartnerProfileCustomerProps = z.infer< + typeof PartnerProfileCustomerSchema +>; + export type PartnerProfileLinkProps = z.infer; export type EnrolledPartnerProps = z.infer< diff --git a/apps/web/lib/zod/schemas/partner-profile.ts b/apps/web/lib/zod/schemas/partner-profile.ts index 391552322b1..f073bc17661 100644 --- a/apps/web/lib/zod/schemas/partner-profile.ts +++ b/apps/web/lib/zod/schemas/partner-profile.ts @@ -8,6 +8,7 @@ import { getCommissionsCountQuerySchema, getCommissionsQuerySchema, } from "./commissions"; +import { customerActivityResponseSchema } from "./customer-activity"; import { CustomerEnrichedSchema } from "./customers"; import { LinkSchema } from "./links"; @@ -80,11 +81,11 @@ export const PartnerProfileLinkSchema = LinkSchema.pick({ export const PartnerProfileCustomerSchema = CustomerEnrichedSchema.pick({ id: true, - createdAt: true, country: true, - link: true, + createdAt: true, }).extend({ email: z .string() .transform((email) => email.replace(/(?<=^.).+(?=.@)/, "****")), + activity: customerActivityResponseSchema, }); diff --git a/apps/web/ui/customers/customer-details-column.tsx b/apps/web/ui/customers/customer-details-column.tsx index 441eee885dc..d44409c55f5 100644 --- a/apps/web/ui/customers/customer-details-column.tsx +++ b/apps/web/ui/customers/customer-details-column.tsx @@ -18,7 +18,10 @@ export function CustomerDetailsColumn({ customerActivity, isCustomerActivityLoading, }: { - customer?: CustomerProps; + customer?: Omit & { + name?: string; + externalId?: string; + }; customerActivity?: CustomerActivityResponse; isCustomerActivityLoading: boolean; }) { @@ -129,7 +132,7 @@ export function CustomerDetailsColumn({ )} - {customer && (customer?.externalId ?? null) !== null && ( + {customer?.externalId && (
External ID { diff --git a/apps/web/ui/customers/customer-partner-earnings-table.tsx b/apps/web/ui/customers/customer-partner-earnings-table.tsx index 81da8bb979f..161a7fa129d 100644 --- a/apps/web/ui/customers/customer-partner-earnings-table.tsx +++ b/apps/web/ui/customers/customer-partner-earnings-table.tsx @@ -117,15 +117,20 @@ export function CustomerPartnerEarningsTable({ ))} - {commissions?.length && totalCommissions && viewAllHref && ( -
- {commissions.length} of{" "} - - {totalCommissions} results - + {viewAllHref && ( +
+ {commissions.length} of + {totalCommissions ? ( + + {totalCommissions} + + ) : ( +
+ )} + results
)} diff --git a/apps/web/ui/customers/customer-sales-table.tsx b/apps/web/ui/customers/customer-sales-table.tsx index 0a7d017124f..9497efeb7ca 100644 --- a/apps/web/ui/customers/customer-sales-table.tsx +++ b/apps/web/ui/customers/customer-sales-table.tsx @@ -136,15 +136,20 @@ export function CustomerSalesTable({ ))} - {sales?.length && totalSales && viewAllHref && ( -
- {sales.length} of{" "} - - {totalSales} results - + {viewAllHref && ( +
+ {sales.length} of + {totalSales ? ( + + {totalSales} + + ) : ( +
+ )} + results
)} diff --git a/apps/web/ui/partners/partner-details-sheet.tsx b/apps/web/ui/partners/partner-details-sheet.tsx index 75a36c21e1a..181888cd591 100644 --- a/apps/web/ui/partners/partner-details-sheet.tsx +++ b/apps/web/ui/partners/partner-details-sheet.tsx @@ -1,4 +1,4 @@ -import { SHEET_MAX_ITEMS } from "@/lib/partners/constants"; +import { PAYOUTS_SHEET_ITEMS_LIMIT } from "@/lib/partners/constants"; import usePayouts from "@/lib/swr/use-payouts"; import useProgram from "@/lib/swr/use-program"; import useWorkspace from "@/lib/swr/use-workspace"; @@ -221,7 +221,7 @@ function PartnerPayouts({ partner }: { partner: EnrolledPartnerProps }) { error: payoutsError, loading, } = usePayouts({ - query: { partnerId: partner.id, pageSize: SHEET_MAX_ITEMS }, + query: { partnerId: partner.id, pageSize: PAYOUTS_SHEET_ITEMS_LIMIT }, }); const table = useTable({ diff --git a/apps/web/ui/partners/payout-details-sheet.tsx b/apps/web/ui/partners/payout-details-sheet.tsx index 523394e29ab..945c4fc4df0 100644 --- a/apps/web/ui/partners/payout-details-sheet.tsx +++ b/apps/web/ui/partners/payout-details-sheet.tsx @@ -1,4 +1,4 @@ -import { SHEET_MAX_ITEMS } from "@/lib/partners/constants"; +import { PAYOUTS_SHEET_ITEMS_LIMIT } from "@/lib/partners/constants"; import useWorkspace from "@/lib/swr/use-workspace"; import { CommissionResponse, PayoutResponse } from "@/lib/types"; import { X } from "@/ui/shared/icons"; @@ -49,7 +49,7 @@ function PayoutDetailsSheetContent({ isLoading, error, } = useSWR( - `/api/programs/${programId}/commissions?workspaceId=${workspaceId}&payoutId=${payout.id}&interval=all&pageSize=${SHEET_MAX_ITEMS}`, + `/api/programs/${programId}/commissions?workspaceId=${workspaceId}&payoutId=${payout.id}&interval=all&pageSize=${PAYOUTS_SHEET_ITEMS_LIMIT}`, fetcher, ); diff --git a/packages/tinybird/pipes/v2_customer_events.pipe b/packages/tinybird/pipes/v2_customer_events.pipe index 143873515f5..f23af53c4a9 100644 --- a/packages/tinybird/pipes/v2_customer_events.pipe +++ b/packages/tinybird/pipes/v2_customer_events.pipe @@ -4,7 +4,7 @@ DESCRIPTION > TAGS "Dub Endpoints" -NODE click_events +NODE lead_events SQL > % @@ -30,23 +30,28 @@ SQL > ip, CONCAT(country, '-', region) as region_processed, splitByString('?', referer_url)[1] as referer_url_processed, - 'click' as event - FROM dub_click_events_id + 'lead' as event, + event_id, + event_name, + metadata + FROM dub_lead_events_mv WHERE - click_id + customer_id = {{ String( - clickId, - '8bBSF1CbVlXRCMJY', - description="The unique ID for a given click event", + customerId, + 'cus_1JRJNSVARH220RCNJ2K5SAX9Q', + description="The unique ID for a given customer", required=True, ) }} + {% if defined(linkIds) %} AND link_id IN ({{ Array(linkIds, 'link_id') }}) {% end %} + ORDER BY timestamp {% if order == 'asc' %} ASC {% else %} DESC {% end %} LIMIT {{ Int32(limit, 100) }} -NODE lead_events +NODE click_events SQL > % @@ -72,23 +77,11 @@ SQL > ip, CONCAT(country, '-', region) as region_processed, splitByString('?', referer_url)[1] as referer_url_processed, - 'lead' as event, - event_id, - event_name - FROM dub_lead_events_mv + 'click' as event + FROM dub_click_events_id WHERE - customer_id - = {{ - String( - customerId, - 'cm1a18x7w0001aqhx744lrtxp', - description="The unique ID for a given customer", - required=True, - ) - }} - AND timestamp >= {{ DateTime(start, '2025-01-01 00:00:00') }} - AND timestamp < {{ DateTime(end, '2025-06-30 00:00:00') }} - ORDER BY timestamp {% if order == 'asc' %} ASC {% else %} DESC {% end %} + click_id IN (SELECT DISTINCT click_id FROM lead_events) + {% if defined(linkIds) %} AND link_id IN ({{ Array(linkIds, 'link_id') }}) {% end %} LIMIT {{ Int32(limit, 100) }} @@ -122,6 +115,7 @@ SQL > 'sale' as event, event_id, event_name, + metadata, amount as saleAmount, invoice_id, payment_processor @@ -131,13 +125,12 @@ SQL > = {{ String( customerId, - 'cm1a18x7w0001aqhx744lrtxp', + 'cus_1JRJNSVARH220RCNJ2K5SAX9Q', description="The unique ID for a given customer", required=True, ) }} - AND timestamp >= {{ DateTime(start, '2025-01-01 00:00:00') }} - AND timestamp < {{ DateTime(end, '2025-06-30 00:00:00') }} + {% if defined(linkIds) %} AND link_id IN ({{ Array(linkIds, 'link_id') }}) {% end %} LIMIT {{ Int32(limit, 100) }} @@ -146,26 +139,26 @@ NODE endpoint SQL > % - SELECT * - FROM - ( - {% if defined(clickId) %} - SELECT - *, - NULL AS event_id, - NULL AS event_name, - NULL AS saleAmount, - NULL AS invoice_id, - NULL AS payment_processor - FROM click_events - UNION ALL - {% end %} - SELECT *, NULL AS saleAmount, NULL AS invoice_id, NULL AS payment_processor - FROM lead_events - UNION ALL - SELECT * - FROM sale_events - ) - ORDER BY timestamp {% if order == 'asc' %} ASC {% else %} DESC {% end %} + SELECT * + FROM + ( + SELECT *, NULL AS saleAmount, NULL AS invoice_id, NULL AS payment_processor + FROM lead_events + UNION ALL + SELECT + *, + NULL AS event_id, + NULL AS event_name, + NULL AS metadata, + NULL AS saleAmount, + NULL AS invoice_id, + NULL AS payment_processor + FROM click_events + UNION ALL + SELECT * + FROM sale_events + ) + ORDER BY + timestamp DESC, CASE event WHEN 'click' THEN 1 WHEN 'lead' THEN 2 WHEN 'sale' THEN 3 END DESC diff --git a/packages/ui/src/date-picker/date-range-picker.tsx b/packages/ui/src/date-picker/date-range-picker.tsx index 49ec1287fc3..776cc81ae14 100644 --- a/packages/ui/src/date-picker/date-range-picker.tsx +++ b/packages/ui/src/date-picker/date-range-picker.tsx @@ -1,8 +1,12 @@ import { cn } from "@dub/utils"; import { enUS } from "date-fns/locale"; -import { useEffect, useMemo, useState } from "react"; +import { PropsWithChildren, useEffect, useMemo, useRef, useState } from "react"; import { SelectRangeEventHandler } from "react-day-picker"; -import { useKeyboardShortcut, useMediaQuery } from "../hooks"; +import { + useKeyboardShortcut, + useMediaQuery, + useScrollProgress, +} from "../hooks"; import { Popover } from "../popover"; import { Calendar as CalendarPrimitive } from "./calendar"; import { Presets } from "./presets"; @@ -132,24 +136,20 @@ const DateRangePickerInner = ({ popoverContentClassName="rounded-xl" content={
-
+
{presets && presets.length > 0 && ( -
-
- + +
+
+ +
-
+ )}
; } + +function PresetScrollContainer({ children }: PropsWithChildren) { + const ref = useRef(null); + const { scrollProgress, updateScrollProgress } = useScrollProgress(ref); + return ( +
+
+ {children} +
+ {/* Bottom scroll fade */} +
+
+ ); +}