Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
Trophy,
useKeyboardShortcut,
User,
useRouterStuff,
Users,
} from "@dub/ui";
import { cn, COUNTRIES, currencyFormatter, OG_AVATAR_URL } from "@dub/utils";
Expand All @@ -29,6 +30,24 @@ import { NetworkPartnerChangeHistory } from "./network-partner-change-history";

type NetworkPartnerSheetTabId = "about" | "programs" | "duplicates";

function isDirectDraftToApprovedNetworkApproval(
partner: AdminNetworkPartner,
): boolean {
if (partner.networkStatus !== "approved") {
return false;
}

const networkLogs = (partner.changeHistoryLog ?? []).filter(
(entry) => entry.field === "networkStatus",
);

return (
networkLogs.length === 1 &&
networkLogs[0].from === "draft" &&
networkLogs[0].to === "approved"
);
}

export function NetworkPartnerApplicationSheet({
isOpen,
setIsOpen,
Expand All @@ -44,11 +63,15 @@ export function NetworkPartnerApplicationSheet({
onNext?: () => void;
onReview: (
partner: AdminNetworkPartner,
status: "approved" | "rejected",
status: "approved" | "rejected" | "draft",
) => Promise<void>;
}) {
const [currentTabId, setCurrentTabId] =
useState<NetworkPartnerSheetTabId>("about");
const { queryParams } = useRouterStuff();

const showRevertToDraftInsteadOfReject =
isDirectDraftToApprovedNetworkApproval(partner);

const PartnerDetails = (
<div className="rounded-lg border border-neutral-200 bg-neutral-100 p-3">
Expand Down Expand Up @@ -93,6 +116,21 @@ export function NetworkPartnerApplicationSheet({
confirmShortcutOptions: { sheet: true, modal: true },
});

const {
setShowConfirmModal: setShowRevertToDraftConfirm,
confirmModal: revertToDraftModal,
} = useConfirmModal({
title: "Revert network profile to draft",
description: PartnerDetails,
confirmText: "Revert to draft",
confirmVariant: "danger",
onConfirm: async () => {
await onReview(partner, "draft");
},
confirmShortcut: "r",
confirmShortcutOptions: { sheet: true, modal: true },
});

useKeyboardShortcut(
"ArrowRight",
() => {
Expand Down Expand Up @@ -120,19 +158,28 @@ export function NetworkPartnerApplicationSheet({

useKeyboardShortcut("r", () => setShowRejectConfirm(true), {
sheet: true,
enabled: !["rejected", "trusted"].includes(partner.networkStatus),
enabled:
!showRevertToDraftInsteadOfReject &&
!["rejected", "trusted"].includes(partner.networkStatus),
});

useKeyboardShortcut("r", () => setShowRevertToDraftConfirm(true), {
sheet: true,
enabled: showRevertToDraftInsteadOfReject,
});

return (
<Sheet
open={isOpen}
onOpenChange={setIsOpen}
onClose={() => queryParams({ del: "partnerId", scroll: false })}
contentProps={{
className: "md:w-[max(min(calc(100vw-334px),1170px),540px)]",
}}
>
{approveModal}
{rejectModal}
{revertToDraftModal}

<div className="flex size-full flex-col">
<div className="flex h-16 shrink-0 items-center justify-between border-b border-neutral-200 px-6 py-4">
Expand Down Expand Up @@ -217,19 +264,30 @@ export function NetworkPartnerApplicationSheet({

<div className="shrink-0 border-t border-neutral-200 p-5">
<div className="flex justify-end gap-2">
<Button
type="button"
variant="secondary"
text="Reject"
className="w-fit shrink-0"
shortcut="R"
onClick={() => setShowRejectConfirm(true)}
disabledTooltip={
["rejected", "trusted"].includes(partner.networkStatus)
? `Cannot reject a ${partner.networkStatus} partner.`
: undefined
}
/>
{showRevertToDraftInsteadOfReject ? (
<Button
type="button"
variant="secondary"
text="Revert to draft"
className="w-fit shrink-0"
shortcut="R"
onClick={() => setShowRevertToDraftConfirm(true)}
/>
) : (
<Button
type="button"
variant="secondary"
text="Reject"
className="w-fit shrink-0"
shortcut="R"
onClick={() => setShowRejectConfirm(true)}
disabledTooltip={
["rejected", "trusted"].includes(partner.networkStatus)
? `Cannot reject a ${partner.networkStatus} partner.`
: undefined
}
/>
)}
<Button
type="button"
variant="primary"
Expand Down Expand Up @@ -279,7 +337,12 @@ function NetworkPartnerSheetTabs({
id: "programs",
label: "Programs",
icon: Trophy,
badge: programsCount > 99 ? "99+" : programsCount,
badge:
programsCount === 0
? undefined
: programsCount > 99
? "99+"
: programsCount,
},
...(duplicatesCount > 0
? [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ export default function NetworkApplicationsPage() {
useRouterStuff();

const [detailsSheetState, setDetailsSheetState] = useState<
{ open: false; partnerId: null } | { open: true; partnerId: string }
| { open: false; partnerId: string | null }
| { open: true; partnerId: string }
>({ open: false, partnerId: null });

const {
Expand Down Expand Up @@ -205,7 +206,7 @@ export default function NetworkApplicationsPage() {

const handleReviewPartner = async (
partner: AdminNetworkPartner,
status: "approved" | "rejected",
status: "approved" | "rejected" | "draft",
) => {
const currentIndex = partners.findIndex(({ id }) => id === partner.id);
const fallbackPartnerId =
Expand All @@ -232,7 +233,9 @@ export default function NetworkApplicationsPage() {
toast.success(
status === "approved"
? "Partner approved for the network."
: "Partner rejected from the network.",
: status === "draft"
? "Network profile reverted to draft."
: "Partner rejected from the network.",
);

await mutate();
Expand Down Expand Up @@ -399,15 +402,7 @@ export default function NetworkApplicationsPage() {
: undefined
}
setIsOpen={(open) => {
if (!open) {
setDetailsSheetState({ open: false, partnerId: null });
queryParams({ del: "partnerId", scroll: false });
} else if (detailsSheetState.partnerId) {
setDetailsSheetState({
open: true,
partnerId: detailsSheetState.partnerId,
});
}
setDetailsSheetState((state) => ({ ...state, open }) as any);
}}
onReview={handleReviewPartner}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@ import { NextResponse } from "next/server";
import * as z from "zod/v4";

const updateAdminNetworkStatusSchema = z.object({
status: z.enum(["approved", "rejected"]),
status: z.enum(["approved", "rejected", "draft"]),
});

// PATCH /api/admin/partners/[partnerId]/network-status
export const PATCH = withAdmin(
async ({ params, req }) => {
const { partnerId } = params;
const { status } = updateAdminNetworkStatusSchema.parse(await req.json());
const { status: updatedStatus } = updateAdminNetworkStatusSchema.parse(
await req.json(),
);

const existingPartner = await prisma.partner.findUnique({
where: {
Expand All @@ -35,39 +37,49 @@ export const PATCH = withAdmin(
return new Response("Partner not found.", { status: 404 });
}

if (existingPartner.networkStatus === status) {
if (existingPartner.networkStatus === updatedStatus) {
return new Response("Partner is already in this status.", {
status: 400,
});
}

if (existingPartner.networkStatus === "trusted" && status === "approved") {
if (
existingPartner.networkStatus === "trusted" &&
updatedStatus === "approved"
) {
return new Response("Trusted partners cannot be approved.", {
status: 400,
});
}

const partnerChangeHistoryLog = existingPartner.changeHistoryLog
let partnerChangeHistoryLog = existingPartner.changeHistoryLog
? partnerProfileChangeHistoryLogSchema.parse(
existingPartner.changeHistoryLog,
)
: [];

partnerChangeHistoryLog.push({
field: "networkStatus",
from: existingPartner.networkStatus,
to: status,
changedAt: new Date(),
});
if (updatedStatus === "draft") {
// if reverting back to draft, remove any pre-existing networkStatus change logs
partnerChangeHistoryLog = partnerChangeHistoryLog.filter(
(log) => log.field !== "networkStatus",
);
} else {
partnerChangeHistoryLog.push({
field: "networkStatus",
from: existingPartner.networkStatus,
to: updatedStatus,
changedAt: new Date(),
});
}

const updatedPartner = await prisma.partner.update({
where: {
id: partnerId,
},
data: {
networkStatus: status,
networkStatus: updatedStatus,
changeHistoryLog: partnerChangeHistoryLog,
reviewedAt: new Date(),
reviewedAt: updatedStatus === "draft" ? null : new Date(),
},
select: {
id: true,
Expand All @@ -77,18 +89,20 @@ export const PATCH = withAdmin(

if (
existingPartner.email &&
(status === "approved" || status === "rejected")
// only send notification emails if partner actually submitted their profile
existingPartner.networkStatus === "submitted" &&
(updatedStatus === "approved" || updatedStatus === "rejected")
) {
waitUntil(
sendEmail({
to: existingPartner.email,
subject:
status === "approved"
updatedStatus === "approved"
? "Your Dub Partner Network application was approved"
: "Dub Partner Network application update",
variant: "notifications",
react:
status === "approved"
updatedStatus === "approved"
? NetworkPartnerApplicationApproved({
name: existingPartner.name,
email: existingPartner.email,
Expand Down
4 changes: 1 addition & 3 deletions apps/web/app/(ee)/api/admin/partners/trusted/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ import * as z from "zod/v4";
export const GET = withAdmin(async () => {
const partners = await prisma.partner.findMany({
where: {
trustedAt: {
not: null,
},
networkStatus: "trusted",
},
orderBy: {
trustedAt: "desc",
Expand Down
4 changes: 0 additions & 4 deletions apps/web/app/(ee)/api/cron/cleanup/declined-invites/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,6 @@ export async function POST(req: Request) {
updatedAt: {
lt: subDays(new Date(), 90),
},
// only delete if there are no messages (e.g. prior network messages)
messages: {
none: {},
},
},
take: 250,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,6 @@ export async function POST(req: Request) {
commissions: {
none: {},
},
messages: {
none: {},
},
},
take: 250,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export function UnapprovedProgramPage({
const badge = PartnerStatusBadges[programEnrollment.status];

const { setShowConfirmModal, confirmModal } = useConfirmModal({
title: "Withdraw Application",
title: "Withdraw application",
description: `Are you sure you want to withdraw your application for ${programEnrollment.program.name}? This will delete your application completely and you'll have to re-apply if you want to join again.`,
confirmText: "Withdraw application",
onConfirm: async () => {
Expand Down Expand Up @@ -105,7 +105,7 @@ export function UnapprovedProgramPage({
<div className="mt-6">
<Button
variant="secondary"
text="Withdraw Application"
text="Withdraw application"
onClick={() => setShowConfirmModal(true)}
className="h-8 px-2.5"
/>
Expand Down
Loading
Loading