diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/analytics/analytics-partners-table.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/analytics/analytics-partners-table.tsx
index 0a52f83562..1906888812 100644
--- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/analytics/analytics-partners-table.tsx
+++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/analytics/analytics-partners-table.tsx
@@ -60,6 +60,7 @@ export function AnalyticsPartnersTable() {
? new Date(p.payoutsEnabledAt)
: null,
}}
+ showRewardsTooltip={true}
/>
);
},
diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/applications/page-client.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/applications/page-client.tsx
index 11c2e7dcdb..0ba952110b 100644
--- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/applications/page-client.tsx
+++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/applications/page-client.tsx
@@ -170,7 +170,11 @@ export function ProgramPartnersApplicationsPageClient() {
minSize: 250,
cell: ({ row }) => {
return (
-
+
);
},
},
diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/applications/rejected/page-client.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/applications/rejected/page-client.tsx
index 21012e1906..a1c27b388a 100644
--- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/applications/rejected/page-client.tsx
+++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/applications/rejected/page-client.tsx
@@ -8,7 +8,6 @@ import useWorkspace from "@/lib/swr/use-workspace";
import { EnrolledPartnerProps } from "@/lib/types";
import { useConfirmModal } from "@/ui/modals/confirm-modal";
import { PartnerApplicationSheet } from "@/ui/partners/partner-application-sheet";
-import { PartnerRowItem } from "@/ui/partners/partner-row-item";
import { PartnerSocialColumn } from "@/ui/partners/partner-social-column";
import { AnimatedEmptyState } from "@/ui/shared/animated-empty-state";
import { SearchBoxPersisted } from "@/ui/shared/search-box";
@@ -30,6 +29,7 @@ import {
formatDate,
getDomainWithoutWWW,
} from "@dub/utils";
+import { OG_AVATAR_URL } from "@dub/utils/src/constants";
import { Row } from "@tanstack/react-table";
import { Command } from "cmdk";
import { useAction } from "next-safe-action/hooks";
@@ -124,7 +124,20 @@ export function ProgramPartnersRejectedApplicationsPageClient() {
minSize: 250,
cell: ({ row }) => {
return (
-
+
+
+

+
+
+ {row.original.name}
+
+
);
},
},
diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx
index 6ff820f1dd..08a3da7dc0 100644
--- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx
+++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx
@@ -158,7 +158,11 @@ export function PartnersTable() {
minSize: 250,
cell: ({ row }) => {
return (
-
+
);
},
},
diff --git a/apps/web/lib/api/groups/get-groups.ts b/apps/web/lib/api/groups/get-groups.ts
index 250ec08c0c..1a483e3811 100644
--- a/apps/web/lib/api/groups/get-groups.ts
+++ b/apps/web/lib/api/groups/get-groups.ts
@@ -31,6 +31,7 @@ export async function getGroups(filters: GroupFilters) {
includeExpandedFields,
} = filters;
+ // First get the basic group data with metrics
const groups = (await prisma.$queryRaw`
SELECT
pg.id,
@@ -94,6 +95,37 @@ export async function getGroups(filters: GroupFilters) {
LIMIT ${pageSize} OFFSET ${(page - 1) * pageSize}
`) satisfies Array;
+ // Get group IDs for fetching rewards
+ const groupIdsFromQuery = groups.map((group) => group.id);
+
+ // Fetch rewards for these groups
+ const groupsWithRewards = await prisma.partnerGroup.findMany({
+ where: {
+ id: {
+ in: groupIdsFromQuery,
+ },
+ },
+ include: {
+ clickReward: true,
+ leadReward: true,
+ saleReward: true,
+ discount: true,
+ },
+ });
+
+ // Create a map for quick lookup
+ const rewardsMap = new Map(
+ groupsWithRewards.map((group) => [
+ group.id,
+ {
+ clickReward: group.clickReward,
+ leadReward: group.leadReward,
+ saleReward: group.saleReward,
+ discount: group.discount,
+ },
+ ]),
+ );
+
return groups.map((group) => ({
...group,
partners: Number(group.partners),
@@ -104,5 +136,7 @@ export async function getGroups(filters: GroupFilters) {
totalConversions: Number(group.totalConversions),
totalCommissions: Number(group.totalCommissions),
netRevenue: Number(group.netRevenue),
+ // Add reward data
+ ...rewardsMap.get(group.id),
}));
}
diff --git a/apps/web/ui/partners/partner-rewards-tooltip.tsx b/apps/web/ui/partners/partner-rewards-tooltip.tsx
new file mode 100644
index 0000000000..fcd1861c97
--- /dev/null
+++ b/apps/web/ui/partners/partner-rewards-tooltip.tsx
@@ -0,0 +1,142 @@
+import { CursorRays, Gift, InvoiceDollar, UserPlus } from "@dub/ui/icons";
+import React from "react";
+
+interface PartnerRewardsTooltipProps {
+ group?: {
+ clickReward?: {
+ amount: number;
+ type: "percentage" | "flat";
+ maxDuration?: number | null;
+ } | null;
+ leadReward?: {
+ amount: number;
+ type: "percentage" | "flat";
+ } | null;
+ saleReward?: {
+ amount: number;
+ type: "percentage" | "flat";
+ maxDuration?: number | null;
+ } | null;
+ discount?: {
+ amount: number;
+ type: "percentage" | "flat";
+ maxDuration?: number | null;
+ } | null;
+ } | null;
+}
+
+export function PartnerRewardsTooltip({ group }: PartnerRewardsTooltipProps) {
+ if (!group) {
+ return null; // Don't show tooltip if no group found
+ }
+
+ const rewards: Array<{
+ icon: React.ComponentType<{ className?: string }>;
+ text: string;
+ }> = [];
+
+ // Add click reward if exists
+ if (group.clickReward) {
+ const amount =
+ group.clickReward.type === "percentage"
+ ? `${group.clickReward.amount}%`
+ : `$${(group.clickReward.amount / 100).toFixed(2)}`;
+ rewards.push({
+ icon: CursorRays,
+ text: `${amount} per click`,
+ });
+ }
+
+ // Add lead reward if exists
+ if (group.leadReward) {
+ const amount =
+ group.leadReward.type === "percentage"
+ ? `${group.leadReward.amount}%`
+ : `$${(group.leadReward.amount / 100).toFixed(2)}`;
+ rewards.push({
+ icon: UserPlus,
+ text: `${amount} per lead`,
+ });
+ }
+
+ // Add sale reward if exists
+ if (group.saleReward) {
+ const amount =
+ group.saleReward.type === "percentage"
+ ? `${group.saleReward.amount}%`
+ : `$${(group.saleReward.amount / 100).toFixed(2)}`;
+
+ let durationText = "";
+ const maxDuration = group.saleReward.maxDuration;
+ if (maxDuration === null) {
+ durationText = " for the customer's lifetime";
+ } else if (maxDuration === 0) {
+ durationText = " for the first sale";
+ } else if (maxDuration && maxDuration > 1) {
+ durationText =
+ maxDuration % 12 === 0
+ ? ` for ${maxDuration / 12} year${maxDuration / 12 > 1 ? "s" : ""}`
+ : ` for ${maxDuration} month${maxDuration > 1 ? "s" : ""}`;
+ }
+
+ let text = "";
+ if (maxDuration === 0) {
+ // For first sale only, use "earn" instead of "per sale"
+ text = `Earn ${amount}${durationText}`;
+ } else {
+ // For recurring sales, use "per sale" with prefix
+ const prefix = group.saleReward.type === "percentage" ? "Up to " : "";
+ text = `${prefix}${amount} per sale${durationText}`;
+ }
+
+ rewards.push({
+ icon: InvoiceDollar,
+ text,
+ });
+ }
+
+ // Add discount if exists
+ if (group.discount) {
+ const amount =
+ group.discount.type === "percentage"
+ ? `${group.discount.amount}%`
+ : `$${(group.discount.amount / 100).toFixed(2)}`;
+
+ let durationText = "";
+ const maxDuration = group.discount.maxDuration;
+ if (maxDuration === null) {
+ durationText = " for their lifetime";
+ } else if (maxDuration === 0) {
+ durationText = " for their first purchase";
+ } else if (maxDuration && maxDuration > 1) {
+ durationText =
+ maxDuration % 12 === 0
+ ? ` for ${maxDuration / 12} year${maxDuration / 12 > 1 ? "s" : ""}`
+ : ` for ${maxDuration} month${maxDuration > 1 ? "s" : ""}`;
+ }
+
+ rewards.push({
+ icon: Gift, // Using Gift icon for discount
+ text: `New users get ${amount} off${durationText}`,
+ });
+ }
+
+ // Always show rewards - partners will always have rewards through their group
+ return (
+
+ {rewards.map((reward, index) => {
+ const IconComponent = reward.icon;
+ return (
+
+
+
+
+
+ {reward.text}
+
+
+ );
+ })}
+
+ );
+}
diff --git a/apps/web/ui/partners/partner-row-item.tsx b/apps/web/ui/partners/partner-row-item.tsx
index 88bfdb03a2..2ceffbf65e 100644
--- a/apps/web/ui/partners/partner-row-item.tsx
+++ b/apps/web/ui/partners/partner-row-item.tsx
@@ -1,61 +1,77 @@
+import useGroups from "@/lib/swr/use-groups";
import { DynamicTooltipWrapper, GreekTemple } from "@dub/ui";
import { cn } from "@dub/utils";
import { OG_AVATAR_URL } from "@dub/utils/src/constants";
import { CircleMinus } from "lucide-react";
import Link from "next/link";
import { useParams } from "next/navigation";
+import { PartnerRewardsTooltip } from "./partner-rewards-tooltip";
export function PartnerRowItem({
partner,
showPermalink = true,
+ showRewardsTooltip = false,
}: {
partner: {
id: string;
name: string;
image?: string | null;
payoutsEnabledAt?: Date | null;
+ groupId?: string | null;
};
showPermalink?: boolean;
+ showRewardsTooltip?: boolean;
}) {
const { slug } = useParams();
const As = showPermalink ? Link : "div";
const showPayoutsEnabled = "payoutsEnabledAt" in partner;
+ // Get groups data to find partner's group and their rewards
+ const { groups } = useGroups({ enabled: showRewardsTooltip });
+ const partnerGroup = showRewardsTooltip
+ ? groups?.find((group) => group.id === partner.groupId)
+ : undefined;
+
return (
-
- Payouts{" "}
- {partner.payoutsEnabledAt ? "enabled" : "disabled"}
-
- {partner.payoutsEnabledAt ? (
-
- ) : (
-
- )}
+ content:
,
+ delayDuration: 150,
+ }
+ : showPayoutsEnabled
+ ? {
+ content: (
+
+
+ Payouts{" "}
+ {partner.payoutsEnabledAt ? "enabled" : "disabled"}
+
+ {partner.payoutsEnabledAt ? (
+
+ ) : (
+
+ )}
+
+
+
+ {partner.payoutsEnabledAt
+ ? "This partner has payouts enabled, which means they will be able to receive payouts from this program"
+ : "This partner does not have payouts enabled, which means they will not be able to receive any payouts from this program"}
-
- {partner.payoutsEnabledAt
- ? "This partner has payouts enabled, which means they will be able to receive payouts from this program"
- : "This partner does not have payouts enabled, which means they will not be able to receive any payouts from this program"}
-
-
- ),
- }
- : undefined
+ ),
+ }
+ : undefined
}
>
@@ -64,7 +80,7 @@ export function PartnerRowItem({
alt={partner.name}
className="size-5 shrink-0 rounded-full"
/>
- {showPayoutsEnabled && (
+ {showPayoutsEnabled && !showRewardsTooltip && (