Skip to content

Commit 149ee2b

Browse files
committed
Merge branch 'main' into partner-marketplace-requirements
2 parents cc840dc + 14565e3 commit 149ee2b

40 files changed

+1323
-162
lines changed

apps/web/app/(ee)/api/payouts/route.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ export const GET = withWorkspace(async ({ workspace, searchParams }) => {
8181
include: {
8282
programEnrollment: true,
8383
partner: true,
84+
user: true,
8485
},
8586
skip: (page - 1) * pageSize,
8687
take: pageSize,

apps/web/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/overview/page-client.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export function PageClient() {
5353
return true;
5454
}, [data]);
5555

56-
const reward: Omit<RewardProps, "id"> = {
56+
const reward: Omit<RewardProps, "id" | "updatedAt"> = {
5757
type: (data.type ?? "flat") as RewardStructure,
5858
amountInCents: data.amountInCents != null ? data.amountInCents * 100 : null,
5959
amountInPercentage: data.amountInPercentage,

apps/web/app/api/activity-logs/route.ts

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ import {
66
} from "@/lib/zod/schemas/activity-log";
77
import { prisma } from "@dub/prisma";
88
import { NextResponse } from "next/server";
9+
import { v4 as uuid } from "uuid";
910
import * as z from "zod/v4";
1011

1112
// GET /api/activity-logs – get activity logs for a resource
1213
export const GET = withWorkspace(async ({ workspace, searchParams }) => {
13-
const { resourceType, resourceId, action } =
14+
const { resourceType, resourceId, parentResourceId, action } =
1415
getActivityLogsQuerySchema.parse(searchParams);
1516

1617
const programId = getDefaultProgramIdOrThrow(workspace);
@@ -20,6 +21,7 @@ export const GET = withWorkspace(async ({ workspace, searchParams }) => {
2021
programId,
2122
resourceType,
2223
resourceId,
24+
parentResourceId,
2325
action,
2426
},
2527
orderBy: {
@@ -31,5 +33,47 @@ export const GET = withWorkspace(async ({ workspace, searchParams }) => {
3133
},
3234
});
3335

34-
return NextResponse.json(z.array(activityLogSchema).parse(activityLogs));
36+
const parsedActivityLogs = z.array(activityLogSchema).parse(activityLogs);
37+
38+
// polyfill first group change activity log based on program enrollment creation date
39+
if (resourceType === "partner" && resourceId) {
40+
const programEnrollment = await prisma.programEnrollment.findUnique({
41+
where: {
42+
partnerId_programId: {
43+
partnerId: resourceId,
44+
programId,
45+
},
46+
},
47+
select: {
48+
createdAt: true,
49+
groupId: true,
50+
partnerGroup: {
51+
select: {
52+
name: true,
53+
},
54+
},
55+
},
56+
});
57+
if (programEnrollment) {
58+
parsedActivityLogs.push({
59+
id: uuid(),
60+
action: "partner.groupChanged",
61+
description: null,
62+
createdAt: programEnrollment.createdAt,
63+
user: null,
64+
changeSet: {
65+
group: {
66+
old: null,
67+
new: parsedActivityLogs[parsedActivityLogs.length - 1]?.changeSet
68+
?.group.old ?? {
69+
id: programEnrollment.groupId,
70+
name: programEnrollment.partnerGroup?.name,
71+
},
72+
},
73+
},
74+
});
75+
}
76+
}
77+
78+
return NextResponse.json(parsedActivityLogs);
3579
});

apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/rewards/group-rewards.tsx

Lines changed: 64 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,16 @@
33
import useGroup from "@/lib/swr/use-group";
44
import type { GroupProps, RewardProps } from "@/lib/types";
55
import { DEFAULT_PARTNER_GROUP } from "@/lib/zod/schemas/groups";
6+
import { useRewardHistorySheet } from "@/ui/activity-logs/reward-history-sheet";
67
import { REWARD_EVENTS } from "@/ui/partners/constants";
78
import { ProgramRewardDescription } from "@/ui/partners/program-reward-description";
89
import {
910
RewardSheet,
1011
useRewardSheet,
1112
} from "@/ui/partners/rewards/add-edit-reward-sheet";
1213
import { EventType } from "@dub/prisma/client";
13-
import { Button, useRouterStuff } from "@dub/ui";
14-
import { cn } from "@dub/utils";
14+
import { Button, TimestampTooltip, useRouterStuff } from "@dub/ui";
15+
import { cn, formatDate } from "@dub/utils";
1516
import Link from "next/link";
1617
import { useParams } from "next/navigation";
1718
import { useEffect, useState } from "react";
@@ -139,12 +140,22 @@ const RewardItem = ({
139140
reward: reward || undefined,
140141
});
141142

143+
const {
144+
hasActivityLogs,
145+
finalActivityLogDate,
146+
rewardHistorySheet,
147+
setIsOpen: setHistoryOpen,
148+
} = useRewardHistorySheet({
149+
reward: reward ?? null,
150+
});
151+
142152
const Icon = REWARD_EVENTS[event].icon;
143153
const As = reward ? Link : "div";
144154

145155
return (
146156
<>
147157
{RewardSheet}
158+
{rewardHistorySheet}
148159
<As
149160
href={
150161
reward
@@ -163,14 +174,58 @@ const RewardItem = ({
163174
<Icon className="size-4 text-neutral-600" />
164175
</div>
165176
<div className="flex flex-1 flex-col justify-between gap-y-4 md:flex-row md:items-center">
166-
<div className="flex items-center gap-2">
177+
<div className="flex w-full items-center gap-2">
167178
{reward ? (
168-
<span className="text-sm font-normal">
169-
<ProgramRewardDescription
170-
reward={reward}
171-
amountClassName="text-blue-600"
172-
/>
173-
</span>
179+
<div className="flex min-w-0 flex-1 flex-col gap-1">
180+
<div className="text-sm font-normal">
181+
<ProgramRewardDescription
182+
reward={reward}
183+
amountClassName="text-blue-600"
184+
/>
185+
</div>
186+
187+
<div className="flex items-center gap-1 text-xs font-medium text-neutral-500">
188+
<span>Last updated </span>
189+
{!finalActivityLogDate ? (
190+
<div className="h-3 w-16 animate-pulse rounded bg-neutral-100" />
191+
) : (
192+
<TimestampTooltip
193+
timestamp={finalActivityLogDate}
194+
side="left"
195+
rows={["local", "utc", "unix"]}
196+
>
197+
<span>
198+
{formatDate(finalActivityLogDate, {
199+
month: "short",
200+
day: "numeric",
201+
year: "numeric",
202+
})}
203+
</span>
204+
</TimestampTooltip>
205+
)}
206+
207+
{!hasActivityLogs ? (
208+
<div className="ml-1 h-3 w-20 animate-pulse rounded bg-neutral-100" />
209+
) : (
210+
<>
211+
<span
212+
className="ml-1 size-1 shrink-0 rounded-full bg-neutral-400"
213+
aria-hidden
214+
/>
215+
<Button
216+
variant="outline"
217+
text="View history"
218+
className="h-4 w-fit px-1 py-0.5 text-xs font-medium text-neutral-500"
219+
onClick={(e) => {
220+
e.preventDefault();
221+
e.stopPropagation();
222+
setHistoryOpen(true);
223+
}}
224+
/>
225+
</>
226+
)}
227+
</div>
228+
</div>
174229
) : (
175230
<div className="flex flex-col">
176231
<span className="text-sm font-medium text-neutral-900">

apps/web/lib/actions/partners/create-reward.ts

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"use server";
22

3+
import { trackRewardActivityLog } from "@/lib/api/activity-log/track-reward-activity-log";
34
import { recordAuditLog } from "@/lib/api/audit-logs/record-audit-log";
45
import { createId } from "@/lib/api/create-id";
56
import { getGroupOrThrow } from "@/lib/api/groups/get-group-or-throw";
@@ -107,19 +108,32 @@ export const createRewardAction = authActionClient
107108
});
108109

109110
waitUntil(
110-
recordAuditLog({
111-
workspaceId: workspace.id,
112-
programId,
113-
action: "reward.created",
114-
description: `Reward ${reward.id} created`,
115-
actor: user,
116-
targets: [
117-
{
118-
type: "reward",
119-
id: reward.id,
120-
metadata: serializeReward(reward),
121-
},
122-
],
123-
}),
111+
Promise.allSettled([
112+
recordAuditLog({
113+
workspaceId: workspace.id,
114+
programId,
115+
action: "reward.created",
116+
description: `Reward ${reward.id} created`,
117+
actor: user,
118+
targets: [
119+
{
120+
type: "reward",
121+
id: reward.id,
122+
metadata: serializeReward(reward),
123+
},
124+
],
125+
}),
126+
127+
trackRewardActivityLog({
128+
workspaceId: workspace.id,
129+
programId,
130+
userId: user.id,
131+
resourceId: reward.id,
132+
parentResourceType: "group",
133+
parentResourceId: groupId,
134+
old: null,
135+
new: reward,
136+
}),
137+
]),
124138
);
125139
});

apps/web/lib/actions/partners/delete-reward.ts

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"use server";
22

3+
import { trackRewardActivityLog } from "@/lib/api/activity-log/track-reward-activity-log";
34
import { recordAuditLog } from "@/lib/api/audit-logs/record-audit-log";
45
import { getRewardOrThrow } from "@/lib/api/partners/get-reward-or-throw";
56
import { getDefaultProgramIdOrThrow } from "@/lib/api/programs/get-default-program-id-or-throw";
@@ -35,8 +36,8 @@ export const deleteRewardAction = authActionClient
3536

3637
const rewardIdColumn = REWARD_EVENT_COLUMN_MAPPING[reward.event];
3738

38-
await prisma.$transaction(async (tx) => {
39-
await tx.partnerGroup.update({
39+
const partnerGroup = await prisma.$transaction(async (tx) => {
40+
const group = await tx.partnerGroup.update({
4041
// @ts-ignore
4142
where: {
4243
[rewardIdColumn]: reward.id,
@@ -60,22 +61,37 @@ export const deleteRewardAction = authActionClient
6061
id: reward.id,
6162
},
6263
});
64+
65+
return group;
6366
});
6467

6568
waitUntil(
66-
recordAuditLog({
67-
workspaceId: workspace.id,
68-
programId,
69-
action: "reward.deleted",
70-
description: `Reward ${rewardId} deleted`,
71-
actor: user,
72-
targets: [
73-
{
74-
type: "reward",
75-
id: rewardId,
76-
metadata: reward,
77-
},
78-
],
79-
}),
69+
Promise.allSettled([
70+
recordAuditLog({
71+
workspaceId: workspace.id,
72+
programId,
73+
action: "reward.deleted",
74+
description: `Reward ${rewardId} deleted`,
75+
actor: user,
76+
targets: [
77+
{
78+
type: "reward",
79+
id: rewardId,
80+
metadata: reward,
81+
},
82+
],
83+
}),
84+
85+
trackRewardActivityLog({
86+
workspaceId: workspace.id,
87+
programId,
88+
userId: user.id,
89+
resourceId: reward.id,
90+
parentResourceType: "group",
91+
parentResourceId: partnerGroup?.id,
92+
old: reward,
93+
new: null,
94+
}),
95+
]),
8096
);
8197
});

apps/web/lib/actions/partners/update-reward.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"use server";
22

3+
import { trackRewardActivityLog } from "@/lib/api/activity-log/track-reward-activity-log";
34
import { recordAuditLog } from "@/lib/api/audit-logs/record-audit-log";
45
import { getRewardOrThrow } from "@/lib/api/partners/get-reward-or-throw";
56
import { serializeReward } from "@/lib/api/partners/serialize-reward";
@@ -96,6 +97,10 @@ export const updateRewardAction = authActionClient
9697
salePartnerGroup,
9798
].some((group) => group?.slug === "default");
9899

100+
// Determine the groupId from the partner group relation
101+
const partnerGroup =
102+
clickPartnerGroup || leadPartnerGroup || salePartnerGroup;
103+
99104
waitUntil(
100105
Promise.allSettled([
101106
recordAuditLog({
@@ -113,6 +118,17 @@ export const updateRewardAction = authActionClient
113118
],
114119
}),
115120

121+
trackRewardActivityLog({
122+
workspaceId: workspace.id,
123+
programId,
124+
userId: user.id,
125+
resourceId: rewardMetadata.id,
126+
parentResourceType: "group",
127+
parentResourceId: partnerGroup?.id,
128+
old: reward,
129+
new: updatedReward,
130+
}),
131+
116132
// we only cache default group pages for now so we need to invalidate them
117133
...(isDefaultGroup
118134
? [

apps/web/lib/api/activity-log/build-change-set.ts renamed to apps/web/lib/api/activity-log/build-program-enrollment-change-set.ts

File renamed without changes.

0 commit comments

Comments
 (0)