- }
- text={isMobile ? undefined : "View Events"}
- />
-
+
diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/customers/page.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/customers/page.tsx
index 5caa0ee5b14..42dfbf98be0 100644
--- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/customers/page.tsx
+++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/customers/page.tsx
@@ -14,7 +14,7 @@ import {
} from "@dub/ui";
import { COUNTRIES, cn, currencyFormatter, formatDate } from "@dub/utils";
import Link from "next/link";
-import { useParams } from "next/navigation";
+import { useParams, useRouter } from "next/navigation";
export default function ProgramPartnerCustomersPage() {
const { partnerId } = useParams() as { partnerId: string };
@@ -36,6 +36,7 @@ export default function ProgramPartnerCustomersPage() {
}
function PartnerCustomers({ partner }: { partner: EnrolledPartnerProps }) {
+ const router = useRouter();
const { slug } = useWorkspace();
const {
@@ -111,8 +112,17 @@ function PartnerCustomers({ partner }: { partner: EnrolledPartnerProps }) {
),
},
],
- onRowClick: (row) =>
+ onRowClick: (row, e) => {
+ const url = `/${slug}/program/customers/${row.original.id}`;
+ if (e.metaKey || e.ctrlKey) window.open(url, "_blank");
+ else router.push(url);
+ },
+ onRowAuxClick: (row) =>
window.open(`/${slug}/program/customers/${row.original.id}`, "_blank"),
+ rowProps: (row) => ({
+ onPointerEnter: () =>
+ router.prefetch(`/${slug}/program/customers/${row.original.id}`),
+ }),
resourceName: (p) => `customer${p ? "s" : ""}`,
sortBy: "createdAt",
sortOrder: "desc",
diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/payouts/page.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/payouts/page.tsx
index cad14c3c545..1c9b75d8f4c 100644
--- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/payouts/page.tsx
+++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/payouts/page.tsx
@@ -16,7 +16,7 @@ import {
import { cn, currencyFormatter, formatPeriod } from "@dub/utils";
import { PayoutPaidCell } from "app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-paid-cell";
import Link from "next/link";
-import { useParams } from "next/navigation";
+import { useParams, useRouter } from "next/navigation";
export default function ProgramPartnerPayoutsPage() {
const { partnerId } = useParams() as { partnerId: string };
@@ -38,6 +38,7 @@ export default function ProgramPartnerPayoutsPage() {
}
function PartnerPayouts({ partner }: { partner: EnrolledPartnerProps }) {
+ const router = useRouter();
const { slug } = useWorkspace();
const {
@@ -101,12 +102,17 @@ function PartnerPayouts({ partner }: { partner: EnrolledPartnerProps }) {
},
},
],
- onRowClick: (row) => {
- window.open(
- `/${slug}/program/payouts?partnerId=${partner.id}&payoutId=${row.original.id}&sortBy=initiatedAt`,
- "_blank",
- );
+ onRowClick: (row, e) => {
+ const url = `/${slug}/program/payouts/${row.original.id}`;
+ if (e.metaKey || e.ctrlKey) window.open(url, "_blank");
+ else router.push(url);
},
+ onRowAuxClick: (row) =>
+ window.open(`/${slug}/program/payouts/${row.original.id}`, "_blank"),
+ rowProps: (row) => ({
+ onPointerEnter: () =>
+ router.prefetch(`/${slug}/program/payouts/${row.original.id}`),
+ }),
resourceName: (p) => `payout${p ? "s" : ""}`,
thClassName: () => "border-l-0",
tdClassName: () => "border-l-0",
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 08aef1c3851..469491037d1 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
@@ -8,6 +8,7 @@ import usePartnersCount from "@/lib/swr/use-partners-count";
import useProgram from "@/lib/swr/use-program";
import useWorkspace from "@/lib/swr/use-workspace";
import { EnrolledPartnerProps } from "@/lib/types";
+import { ACTIVE_ENROLLMENT_STATUSES } from "@/lib/zod/schemas/partners";
import { useArchivePartnerModal } from "@/ui/modals/archive-partner-modal";
import { useBanPartnerModal } from "@/ui/modals/ban-partner-modal";
import { useBulkArchivePartnersModal } from "@/ui/modals/bulk-archive-partners-modal";
@@ -27,6 +28,7 @@ import { ProgramEnrollmentStatus } from "@dub/prisma/client";
import {
AnimatedSizeContainer,
Button,
+ DynamicTooltipWrapper,
EditColumnsButton,
Filter,
Icon,
@@ -122,7 +124,7 @@ export function PartnersTable() {
const status = (
searchParams.get("status") || searchParams.get("search")
? undefined
- : "approved"
+ : "approved_invited"
) as ProgramEnrollmentStatus;
const sortBy =
@@ -529,7 +531,7 @@ export function PartnersTable() {
}}
/>
- {(status === "approved" ||
+ {(!searchParams.get("status") ||
searchParams.get("status") === "approved") && (
!ACTIVE_ENROLLMENT_STATUSES.includes(partner.status),
+ )
+ ? `You cannot perform this action because one or more partners are not in ${ACTIVE_ENROLLMENT_STATUSES.join(", ")} statuses.`
+ : undefined;
+
return (
@@ -939,11 +950,13 @@ function MenuItem({
label,
onSelect,
variant = "default",
+ disabledTooltip,
}: {
icon: Icon;
label: string;
onSelect: () => void;
variant?: "default" | "danger";
+ disabledTooltip?: string | boolean;
}) {
const variantStyles = {
default: {
@@ -959,16 +972,22 @@ function MenuItem({
const { text, icon } = variantStyles[variant];
return (
-
-
- {label}
-
+
+
+ {label}
+
+
);
}
diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/[payoutId]/page-client.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/[payoutId]/page-client.tsx
index 2acabd3145f..a0ac81e61f9 100644
--- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/[payoutId]/page-client.tsx
+++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/[payoutId]/page-client.tsx
@@ -168,12 +168,13 @@ function PayoutDetailsContent({
Partner: (
- {payout.partner.name}
+ {payout.partner.name}
),
diff --git a/apps/web/lib/actions/partners/bulk-archive-partners.ts b/apps/web/lib/actions/partners/bulk-archive-partners.ts
index 40618da1a37..fffa0db594a 100644
--- a/apps/web/lib/actions/partners/bulk-archive-partners.ts
+++ b/apps/web/lib/actions/partners/bulk-archive-partners.ts
@@ -2,7 +2,10 @@
import { recordAuditLog } from "@/lib/api/audit-logs/record-audit-log";
import { getDefaultProgramIdOrThrow } from "@/lib/api/programs/get-default-program-id-or-throw";
-import { bulkArchivePartnersSchema } from "@/lib/zod/schemas/partners";
+import {
+ ACTIVE_ENROLLMENT_STATUSES,
+ bulkArchivePartnersSchema,
+} from "@/lib/zod/schemas/partners";
import { prisma } from "@dub/prisma";
import { waitUntil } from "@vercel/functions";
import { authActionClient } from "../safe-action";
@@ -28,7 +31,7 @@ export const bulkArchivePartnersAction = authActionClient
},
programId,
status: {
- not: "archived",
+ in: ACTIVE_ENROLLMENT_STATUSES,
},
},
select: {
diff --git a/apps/web/lib/actions/partners/bulk-ban-partners.ts b/apps/web/lib/actions/partners/bulk-ban-partners.ts
index 0ee0d972d35..aa4ca613a93 100644
--- a/apps/web/lib/actions/partners/bulk-ban-partners.ts
+++ b/apps/web/lib/actions/partners/bulk-ban-partners.ts
@@ -4,7 +4,10 @@ import { recordAuditLog } from "@/lib/api/audit-logs/record-audit-log";
import { resolveFraudGroups } from "@/lib/api/fraud/resolve-fraud-groups";
import { getDefaultProgramIdOrThrow } from "@/lib/api/programs/get-default-program-id-or-throw";
import { enqueueBatchJobs } from "@/lib/cron/enqueue-batch-jobs";
-import { bulkBanPartnersSchema } from "@/lib/zod/schemas/partners";
+import {
+ ACTIVE_ENROLLMENT_STATUSES,
+ bulkBanPartnersSchema,
+} from "@/lib/zod/schemas/partners";
import { prisma } from "@dub/prisma";
import { ProgramEnrollmentStatus } from "@dub/prisma/client";
import { APP_DOMAIN_WITH_NGROK } from "@dub/utils";
@@ -32,7 +35,7 @@ export const bulkBanPartnersAction = authActionClient
},
programId,
status: {
- not: "banned",
+ in: ACTIVE_ENROLLMENT_STATUSES,
},
},
select: {
diff --git a/apps/web/lib/api/fraud/constants.ts b/apps/web/lib/api/fraud/constants.ts
index bd8239ce178..1a54b779da9 100644
--- a/apps/web/lib/api/fraud/constants.ts
+++ b/apps/web/lib/api/fraud/constants.ts
@@ -51,7 +51,7 @@ export const FRAUD_RULES: FraudRuleInfo[] = [
type: "partnerDuplicatePayoutMethod",
name: "Duplicate payout method",
description:
- "This partner is using a payout method that is already linked to another partner account, which may indicate account duplication or fraudulent behavior.",
+ "This payout method is already linked to another partner. May indicate duplicate accounts or fraud, and is flagged to prevent abuse of restrictions, caps, or bonuses.",
scope: "partner",
severity: "high",
configurable: true,
diff --git a/apps/web/lib/api/partners/get-partners-count.ts b/apps/web/lib/api/partners/get-partners-count.ts
index 6d7e2c7489d..2dcec84e6dd 100644
--- a/apps/web/lib/api/partners/get-partners-count.ts
+++ b/apps/web/lib/api/partners/get-partners-count.ts
@@ -41,7 +41,12 @@ export async function getPartnersCount(
...(groupId && {
groupId,
}),
- status,
+ status:
+ status === "approved_invited"
+ ? {
+ in: ["approved", "invited"],
+ }
+ : status,
...enrollmentMetricWhere,
},
},
@@ -108,7 +113,12 @@ export async function getPartnersCount(
}),
...commonWhere,
},
- status,
+ status:
+ status === "approved_invited"
+ ? {
+ in: ["approved", "invited"],
+ }
+ : status,
...enrollmentMetricWhere,
},
_count: true,
diff --git a/apps/web/lib/api/partners/program-enrollment-query.ts b/apps/web/lib/api/partners/program-enrollment-query.ts
index ef78eb51b2c..38e057e9449 100644
--- a/apps/web/lib/api/partners/program-enrollment-query.ts
+++ b/apps/web/lib/api/partners/program-enrollment-query.ts
@@ -153,7 +153,12 @@ export function buildProgramEnrollmentWhereForList(
in: partnerIds,
},
}),
- status,
+ status:
+ status === "approved_invited"
+ ? {
+ in: ["approved", "invited"],
+ }
+ : status,
groupId,
...(country || search || email
? {
diff --git a/apps/web/lib/zod/schemas/partners.ts b/apps/web/lib/zod/schemas/partners.ts
index ef4530f7ad0..7aadd18b77b 100644
--- a/apps/web/lib/zod/schemas/partners.ts
+++ b/apps/web/lib/zod/schemas/partners.ts
@@ -191,6 +191,10 @@ export const getPartnersQuerySchema = z
.extend(getPaginationQuerySchema({ pageSize: PARTNERS_MAX_PAGE_SIZE }));
export const getPartnersQuerySchemaExtended = getPartnersQuerySchema.extend({
+ status: z
+ .enum(ProgramEnrollmentStatus)
+ .or(z.enum(["approved_invited"]))
+ .optional(),
partnerIds: z
.union([z.string(), z.array(z.string())])
.transform((v) => (Array.isArray(v) ? v : v.split(",")))
diff --git a/apps/web/ui/analytics/toggle.tsx b/apps/web/ui/analytics/toggle.tsx
index 08e56c39322..579fbb619b5 100644
--- a/apps/web/ui/analytics/toggle.tsx
+++ b/apps/web/ui/analytics/toggle.tsx
@@ -225,17 +225,17 @@ export function AnalyticsToggle({
))}
- {isMobile ? dateRangePicker : filterSelect}
+ {filterSelect}
- {isMobile ? filterSelect : dateRangePicker}
+ {dateRangePicker}
{!dashboardProps && (
{page === "analytics" && (
diff --git a/apps/web/ui/customers/customers-table/customers-table.tsx b/apps/web/ui/customers/customers-table/customers-table.tsx
index 34437b72cc2..b91c5613f25 100644
--- a/apps/web/ui/customers/customers-table/customers-table.tsx
+++ b/apps/web/ui/customers/customers-table/customers-table.tsx
@@ -404,6 +404,7 @@ export function CustomersTable({
sortBy={sortBy}
sortOrder={sortOrder}
enabled={canManageCustomers}
+ isProgramPage={isProgramPage}
/>
{!canManageCustomers || customers?.length !== 0 ? (
diff --git a/apps/web/ui/customers/customers-table/use-customer-filters.tsx b/apps/web/ui/customers/customers-table/use-customer-filters.tsx
index 99636e72d01..e6d374cb2c6 100644
--- a/apps/web/ui/customers/customers-table/use-customer-filters.tsx
+++ b/apps/web/ui/customers/customers-table/use-customer-filters.tsx
@@ -16,10 +16,13 @@ import { useDebounce } from "use-debounce";
export function useCustomerFilters(
extraSearchParams: Record,
- { enabled = true }: { enabled?: boolean } = {},
+ {
+ enabled = true,
+ isProgramPage = false,
+ }: { enabled?: boolean; isProgramPage?: boolean } = {},
) {
const { searchParamsObj, queryParams } = useRouterStuff();
- const { id: workspaceId, slug } = useWorkspace();
+ const { id: workspaceId, slug, defaultProgramId } = useWorkspace();
const [selectedFilter, setSelectedFilter] = useState(null);
const [search, setSearch] = useState("");
@@ -38,6 +41,10 @@ export function useCustomerFilters(
>({
query: {
groupBy: "country",
+ ...(isProgramPage &&
+ defaultProgramId && {
+ programId: defaultProgramId,
+ }),
},
enabled,
});
@@ -53,6 +60,10 @@ export function useCustomerFilters(
>({
query: {
groupBy: "linkId",
+ ...(isProgramPage &&
+ defaultProgramId && {
+ programId: defaultProgramId,
+ }),
},
enabled,
});