Skip to content

Commit 525390a

Browse files
authored
Merge pull request #3442 from dubinc/reward-activity-log
Reward activity log
2 parents 12b763a + 94da85c commit 525390a

30 files changed

+965
-143
lines changed

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: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import * as z from "zod/v4";
1010

1111
// GET /api/activity-logs – get activity logs for a resource
1212
export const GET = withWorkspace(async ({ workspace, searchParams }) => {
13-
const { resourceType, resourceId, action } =
13+
const { resourceType, resourceId, parentResourceId, action } =
1414
getActivityLogsQuerySchema.parse(searchParams);
1515

1616
const programId = getDefaultProgramIdOrThrow(workspace);
@@ -20,6 +20,7 @@ export const GET = withWorkspace(async ({ workspace, searchParams }) => {
2020
programId,
2121
resourceType,
2222
resourceId,
23+
parentResourceId,
2324
action,
2425
},
2526
orderBy: {

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

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
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 {
@@ -11,7 +12,7 @@ import {
1112
} from "@/ui/partners/rewards/add-edit-reward-sheet";
1213
import { EventType } from "@dub/prisma/client";
1314
import { Button, useRouterStuff } from "@dub/ui";
14-
import { cn } from "@dub/utils";
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,21 @@ const RewardItem = ({
139140
reward: reward || undefined,
140141
});
141142

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

145154
return (
146155
<>
147156
{RewardSheet}
157+
{rewardHistorySheet}
148158
<As
149159
href={
150160
reward
@@ -163,14 +173,45 @@ const RewardItem = ({
163173
<Icon className="size-4 text-neutral-600" />
164174
</div>
165175
<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">
176+
<div className="flex w-full items-center gap-2">
167177
{reward ? (
168-
<span className="text-sm font-normal">
169-
<ProgramRewardDescription
170-
reward={reward}
171-
amountClassName="text-blue-600"
172-
/>
173-
</span>
178+
<div className="flex min-w-0 flex-1 flex-col gap-1">
179+
<div className="text-sm font-normal">
180+
<ProgramRewardDescription
181+
reward={reward}
182+
amountClassName="text-blue-600"
183+
/>
184+
</div>
185+
186+
<div className="flex items-center gap-1 text-xs font-medium text-neutral-500">
187+
<span>
188+
{`Updated ${formatDate(reward.updatedAt, {
189+
month: "short",
190+
day: "numeric",
191+
year: "numeric",
192+
})}`}
193+
</span>
194+
195+
{hasActivityLogs && (
196+
<>
197+
<span
198+
className="ml-1 size-1 shrink-0 rounded-full bg-neutral-400"
199+
aria-hidden
200+
/>
201+
<Button
202+
variant="outline"
203+
text="View history"
204+
className="h-4 w-fit px-1 py-0.5 text-xs font-medium text-neutral-500"
205+
onClick={(e) => {
206+
e.preventDefault();
207+
e.stopPropagation();
208+
setHistoryOpen(true);
209+
}}
210+
/>
211+
</>
212+
)}
213+
</div>
214+
</div>
174215
) : (
175216
<div className="flex flex-col">
176217
<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.

apps/web/lib/api/activity-log/track-activity-log.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,23 @@ import { prisma } from "@dub/prisma";
88
import { Prisma } from "@dub/prisma/client";
99
import { prettyPrint } from "@dub/utils";
1010

11+
const ACTIONS_WITHOUT_CHANGE_SET: ActivityLogAction[] = [
12+
"referral.created",
13+
"reward.created",
14+
"reward.deleted",
15+
];
16+
1117
export interface TrackActivityLogInput
1218
extends Pick<
1319
Prisma.ActivityLogUncheckedCreateInput,
14-
"workspaceId" | "programId" | "resourceId" | "userId" | "description"
20+
| "workspaceId"
21+
| "programId"
22+
| "resourceId"
23+
| "userId"
24+
| "description"
25+
| "parentResourceType"
26+
| "parentResourceId"
27+
| "batchId"
1528
> {
1629
resourceType: ActivityLogResourceType;
1730
action: ActivityLogAction;
@@ -25,7 +38,7 @@ export const trackActivityLog = async (
2538

2639
inputs = inputs.filter(
2740
(i) =>
28-
i.action === "referral.created" ||
41+
ACTIONS_WITHOUT_CHANGE_SET.includes(i.action) ||
2942
(i.changeSet && Object.keys(i.changeSet).length > 0),
3043
);
3144

0 commit comments

Comments
 (0)