Skip to content

Commit 08026ee

Browse files
authored
Merge branch 'main' into mobile-text-area-focus
2 parents a2fc959 + ec93355 commit 08026ee

File tree

3 files changed

+153
-36
lines changed

3 files changed

+153
-36
lines changed

apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/create-commission-sheet.tsx

Lines changed: 87 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { createManualCommissionAction } from "@/lib/actions/partners/create-manual-commission";
22
import { handleMoneyKeyDown } from "@/lib/form-utils";
33
import { mutatePrefix } from "@/lib/swr/mutate";
4+
import useRewards from "@/lib/swr/use-rewards";
45
import useWorkspace from "@/lib/swr/use-workspace";
56
import { CustomerActivityResponse } from "@/lib/types";
67
import { createCommissionSchema } from "@/lib/zod/schemas/commissions";
@@ -54,6 +55,7 @@ function CreateCommissionSheetContent({
5455
const { id: workspaceId, defaultProgramId, slug } = useWorkspace();
5556
const [hasInvoiceId, setHasInvoiceId] = useState(false);
5657
const [hasProductId, setHasProductId] = useState(false);
58+
const [hasDate, setHasDate] = useState(false);
5759

5860
const [hasCustomLeadEventDate, setHasCustomLeadEventDate] = useState(false);
5961
const [hasCustomLeadEventName, setHasCustomLeadEventName] = useState(false);
@@ -107,6 +109,33 @@ function CreateCommissionSheetContent({
107109
"description",
108110
]);
109111

112+
const { rewards } = useRewards();
113+
const hasLeadRewards = rewards?.some((reward) => reward.event === "lead");
114+
115+
const commissionTypeOptions = [
116+
{
117+
value: "custom",
118+
label: "One-time",
119+
description:
120+
"Pay a one-time commission to a partner (e.g. bonuses, reimbursements, etc.)",
121+
},
122+
...(hasLeadRewards
123+
? [
124+
{
125+
value: "lead",
126+
label: "Lead",
127+
description: "Reward a partner for a qualified signup/referral.",
128+
},
129+
]
130+
: []),
131+
{
132+
value: "sale",
133+
label: "Recurring sale",
134+
description:
135+
"Reward a partner for a recurring subscription from a referred customer.",
136+
},
137+
];
138+
110139
// Fetch customer activity data when customer is selected and we're using existing events
111140
const { data: customerActivity, isLoading: isCustomerActivityLoading } =
112141
useSWR<CustomerActivityResponse>(
@@ -128,6 +157,12 @@ function CreateCommissionSheetContent({
128157
}
129158
}, [hasCustomLeadEventDate, setValue]);
130159

160+
useEffect(() => {
161+
if (!hasDate) {
162+
setValue("date", null);
163+
}
164+
}, [hasDate, setValue]);
165+
131166
useEffect(() => {
132167
if (commissionType === "custom") {
133168
setOpenAccordions(["partner-and-type", "commission"]);
@@ -342,18 +377,21 @@ function CreateCommissionSheetContent({
342377
</label>
343378
<ToggleGroup
344379
className="mt-2 flex w-full items-center gap-1 rounded-md border border-neutral-200 bg-neutral-50 p-1"
345-
optionClassName="h-8 flex items-center justify-center rounded-md flex-1 text-sm"
380+
optionClassName="h-8 flex items-center justify-center rounded-md flex-1 text-sm normal-case"
346381
indicatorClassName="bg-white"
347-
options={[
348-
{ value: "custom", label: "One-time" },
349-
{ value: "lead", label: "Lead" },
350-
{ value: "sale", label: "Sale" },
351-
]}
382+
options={commissionTypeOptions}
352383
selected={commissionType}
353384
selectAction={(id: CommissionType) =>
354385
setCommissionType(id)
355386
}
356387
/>
388+
<p className="mt-2 text-xs text-neutral-500">
389+
{
390+
commissionTypeOptions.find(
391+
(option) => option.value === commissionType,
392+
)?.description
393+
}
394+
</p>
357395
</div>
358396

359397
{commissionType !== "custom" && (
@@ -406,19 +444,6 @@ function CreateCommissionSheetContent({
406444
</ProgramSheetAccordionTrigger>
407445
<ProgramSheetAccordionContent>
408446
<div className="grid grid-cols-1 gap-6">
409-
<div>
410-
<SmartDateTimePicker
411-
value={date}
412-
onChange={(date) => {
413-
setValue("date", date, {
414-
shouldDirty: true,
415-
});
416-
}}
417-
label="Date"
418-
placeholder='E.g. "2024-03-01", "Last Thursday", "2 hours ago"'
419-
/>
420-
</div>
421-
422447
<div>
423448
<label
424449
htmlFor="amount"
@@ -503,6 +528,36 @@ function CreateCommissionSheetContent({
503528
/>
504529
</div>
505530
</div>
531+
532+
<div>
533+
<div className="flex items-center gap-4">
534+
<Switch
535+
fn={setHasDate}
536+
checked={hasDate}
537+
trackDimensions="w-8 h-4"
538+
thumbDimensions="w-3 h-3"
539+
thumbTranslate="translate-x-4"
540+
/>
541+
<h3 className="text-sm font-medium text-neutral-700">
542+
Add a custom date
543+
</h3>
544+
</div>
545+
546+
{hasDate && (
547+
<div className="mt-4">
548+
<SmartDateTimePicker
549+
value={date}
550+
onChange={(date) => {
551+
setValue("date", date, {
552+
shouldDirty: true,
553+
});
554+
}}
555+
label="Custom date"
556+
placeholder='E.g. "2024-03-01", "Last Thursday", "2 hours ago"'
557+
/>
558+
</div>
559+
)}
560+
</div>
506561
</div>
507562
</ProgramSheetAccordionContent>
508563
</ProgramSheetAccordionItem>
@@ -683,19 +738,6 @@ function CreateCommissionSheetContent({
683738
</>
684739
) : commissionType === "sale" ? (
685740
<div className="grid grid-cols-1 gap-6">
686-
<div>
687-
<SmartDateTimePicker
688-
value={saleEventDate}
689-
onChange={(date) => {
690-
setValue("saleEventDate", date, {
691-
shouldDirty: true,
692-
});
693-
}}
694-
label="Sale date"
695-
placeholder='E.g. "2024-03-01", "Last Thursday", "2 hours ago"'
696-
/>
697-
</div>
698-
699741
<div>
700742
<label
701743
htmlFor="saleAmount"
@@ -746,6 +788,19 @@ function CreateCommissionSheetContent({
746788
</div>
747789
</div>
748790

791+
<div>
792+
<SmartDateTimePicker
793+
value={saleEventDate}
794+
onChange={(date) => {
795+
setValue("saleEventDate", date, {
796+
shouldDirty: true,
797+
});
798+
}}
799+
label="Sale date"
800+
placeholder='E.g. "2024-03-01", "Last Thursday", "2 hours ago"'
801+
/>
802+
</div>
803+
749804
<div>
750805
<div className="flex items-center gap-4">
751806
<Switch

apps/web/lib/api/links/bulk-create-links.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,11 @@ export async function bulkCreateLinks({
125125
);
126126
}
127127

128-
createdLinksData.forEach((link, idx) => {
129-
const originalLink = links[idx];
128+
createdLinksData.forEach((link) => {
129+
const originalIndex = shortLinkToIndexMap.get(link.shortLink);
130+
if (originalIndex === undefined) return;
131+
132+
const originalLink = links[originalIndex];
130133
if (!originalLink) return;
131134

132135
const { tagId, tagIds, tagNames } = originalLink;
@@ -167,8 +170,11 @@ export async function bulkCreateLinks({
167170
if (hasWebhooks) {
168171
const linkWebhooksToCreate: { linkId: string; webhookId: string }[] = [];
169172

170-
createdLinksData.forEach((link, idx) => {
171-
const originalLink = links[idx];
173+
createdLinksData.forEach((link) => {
174+
const originalIndex = shortLinkToIndexMap.get(link.shortLink);
175+
if (originalIndex === undefined) return;
176+
177+
const originalLink = links[originalIndex];
172178
if (!originalLink?.webhookIds?.length) return;
173179

174180
originalLink.webhookIds.forEach((webhookId) => {

apps/web/tests/links/bulk-create-link.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import { IntegrationHarness } from "../utils/integration";
77
import { E2E_LINK, E2E_TAG, E2E_TAG_2 } from "../utils/resource";
88
import { LinkSchema, expectedLink } from "../utils/schema";
99

10+
type LinkWithTags = Link & {
11+
tags?: { id: string; name: string; color: string }[];
12+
};
13+
1014
const { domain } = E2E_LINK;
1115

1216
const setupBulkTest = async (ctx: any) => {
@@ -199,3 +203,55 @@ test("POST /links/bulk with multiple tags (by name)", async (ctx) => {
199203
expectedTags: [E2E_TAG_2, E2E_TAG],
200204
});
201205
});
206+
207+
test("POST /links/bulk assigns correct tags to each link (no tag mixing)", async (ctx) => {
208+
const testContext = await setupBulkTest(ctx);
209+
const { h } = testContext;
210+
211+
const bulkLinks = [
212+
{
213+
url: `https://example.com/${randomId()}`,
214+
domain,
215+
tagNames: [E2E_TAG.name],
216+
},
217+
{
218+
url: `https://example.com/${randomId()}`,
219+
domain,
220+
tagNames: [E2E_TAG_2.name],
221+
},
222+
{
223+
url: `https://example.com/${randomId()}`,
224+
domain,
225+
tagNames: [E2E_TAG.name, E2E_TAG_2.name],
226+
},
227+
];
228+
229+
const { status, data: links } = await testContext.http.post<LinkWithTags[]>({
230+
path: "/links/bulk",
231+
body: bulkLinks,
232+
});
233+
234+
onTestFinished(async () => {
235+
await Promise.all([
236+
h.deleteLink(links[0].id),
237+
h.deleteLink(links[1].id),
238+
h.deleteLink(links[2].id),
239+
]);
240+
});
241+
242+
expect(status).toEqual(200);
243+
expect(links).toHaveLength(3);
244+
245+
const link1 = links.find((l) => l.url === bulkLinks[0].url);
246+
expect(link1?.tags).toHaveLength(1);
247+
expect(link1?.tags?.map((t) => t.id)).toContain(E2E_TAG.id);
248+
249+
const link2 = links.find((l) => l.url === bulkLinks[1].url);
250+
expect(link2?.tags).toHaveLength(1);
251+
expect(link2?.tags?.map((t) => t.id)).toContain(E2E_TAG_2.id);
252+
253+
const link3 = links.find((l) => l.url === bulkLinks[2].url);
254+
expect(link3?.tags).toHaveLength(2);
255+
expect(link3?.tags?.map((t) => t.id)).toContain(E2E_TAG.id);
256+
expect(link3?.tags?.map((t) => t.id)).toContain(E2E_TAG_2.id);
257+
});

0 commit comments

Comments
 (0)