Add status cycling and lead attribution tracking#6
Conversation
P0 — Write-only no more: - PATCH API endpoints for leads, opportunities, offers, assets - updateX() store functions for all four entities - StatusCycleButton client component: click any status badge to advance it through the pipeline (persisted to DB, page refreshes) - Opportunity table: status badges now clickable - Offers page: status badges now clickable - Assets page: status badges now clickable P1 — Revenue actualization: - buyUrl field exposed in Assets QuickCreate form + inline link on each card - Assets with a buyUrl show "Buy link active" badge; others show public /a/:id link - calUrl field added to Offer type, demo data, and Offers QuickCreate form - Offer landing pages (/o/:id) embed a Cal.com iframe when calUrl is set, falling back to lead form if no calUrl P1 — Content → lead attribution: - Public pages (/o/:id, /a/:id) read ?ref=content_01 from URL searchParams - LeadForm passes refContentId to /api/leads - addLead() stores refContentId on every lead - createLeadSchema validates the new field - Leads page shows "Via" column: resolves refContentId → content topic, or "Direct" if none set https://claude.ai/code/session_01J9wgN7CnCeMnJkAhTyNFMb
✅ Deploy Preview for personalcommandcenter ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Code Review
This pull request introduces PATCH endpoints for assets, leads, offers, and opportunities, alongside a new StatusCycleButton component for interactive status management. It also implements referral tracking via refContentId and adds support for calUrl in offers. The review feedback highlights critical security and reliability concerns: the new API endpoints are vulnerable to mass assignment due to unvalidated request bodies and have improper error serialization. Additionally, the StatusCycleButton lacks a check for successful network responses, which could lead to the UI becoming out of sync with the server state.
| const patch = await req.json(); | ||
| const updated = await updateAsset(id, patch); | ||
| if (!updated) return NextResponse.json({ error: "Not found" }, { status: 404 }); | ||
| return NextResponse.json(updated); | ||
| } catch (error) { | ||
| return NextResponse.json({ error }, { status: 400 }); |
There was a problem hiding this comment.
This endpoint is vulnerable to mass assignment and has an issue with error serialization.
- Mass Assignment: The
patchobject is passed directly fromreq.json()to the store update function without validation. This allows a client to potentially overwrite internal fields likeidorcreatedAtif they are included in the request body. - Error Serialization: In JavaScript,
JSON.stringify(new Error())typically returns{}. Returning the rawerrorobject in the response will likely result in an empty object being sent to the client. It is better to return the error message.
const body = await req.json();
const { status, buyUrl, price } = body;
const updated = await updateAsset(id, { status, buyUrl, price });
if (!updated) return NextResponse.json({ error: "Not found" }, { status: 404 });
return NextResponse.json(updated);
} catch (error) {
return NextResponse.json(
{ error: error instanceof Error ? error.message : "Unknown error" },
{ status: 400 }
);| const patch = await req.json(); | ||
| const updated = await updateLead(id, patch); | ||
| if (!updated) return NextResponse.json({ error: "Not found" }, { status: 404 }); | ||
| return NextResponse.json(updated); | ||
| } catch (error) { | ||
| return NextResponse.json({ error }, { status: 400 }); |
There was a problem hiding this comment.
This endpoint is vulnerable to mass assignment and has an issue with error serialization.
- Mass Assignment: The
patchobject is passed directly fromreq.json()to the store update function without validation. This allows a client to potentially overwrite internal fields likeidorcreatedAtif they are included in the request body. - Error Serialization: In JavaScript,
JSON.stringify(new Error())typically returns{}. Returning the rawerrorobject in the response will likely result in an empty object being sent to the client. It is better to return the error message.
const body = await req.json();
const { status, refContentId } = body;
const updated = await updateLead(id, { status, refContentId });
if (!updated) return NextResponse.json({ error: "Not found" }, { status: 404 });
return NextResponse.json(updated);
} catch (error) {
return NextResponse.json(
{ error: error instanceof Error ? error.message : "Unknown error" },
{ status: 400 }
);| const patch = await req.json(); | ||
| const updated = await updateOffer(id, patch); | ||
| if (!updated) return NextResponse.json({ error: "Not found" }, { status: 404 }); | ||
| return NextResponse.json(updated); | ||
| } catch (error) { | ||
| return NextResponse.json({ error }, { status: 400 }); |
There was a problem hiding this comment.
This endpoint is vulnerable to mass assignment and has an issue with error serialization.
- Mass Assignment: The
patchobject is passed directly fromreq.json()to the store update function without validation. This allows a client to potentially overwrite internal fields likeidorcreatedAtif they are included in the request body. - Error Serialization: In JavaScript,
JSON.stringify(new Error())typically returns{}. Returning the rawerrorobject in the response will likely result in an empty object being sent to the client. It is better to return the error message.
const body = await req.json();
const { status, ctaUrl, calUrl } = body;
const updated = await updateOffer(id, { status, ctaUrl, calUrl });
if (!updated) return NextResponse.json({ error: "Not found" }, { status: 404 });
return NextResponse.json(updated);
} catch (error) {
return NextResponse.json(
{ error: error instanceof Error ? error.message : "Unknown error" },
{ status: 400 }
);| const patch = await req.json(); | ||
| const updated = await updateOpportunity(id, patch); | ||
| if (!updated) return NextResponse.json({ error: "Not found" }, { status: 404 }); | ||
| return NextResponse.json(updated); | ||
| } catch (error) { | ||
| return NextResponse.json({ error }, { status: 400 }); |
There was a problem hiding this comment.
This endpoint is vulnerable to mass assignment and has an issue with error serialization.
- Mass Assignment: The
patchobject is passed directly fromreq.json()to the store update function without validation. This allows a client to potentially overwrite internal fields likeidorcreatedAtif they are included in the request body. - Error Serialization: In JavaScript,
JSON.stringify(new Error())typically returns{}. Returning the rawerrorobject in the response will likely result in an empty object being sent to the client. It is better to return the error message.
const body = await req.json();
const { status, nextAction, expectedRevenue } = body;
const updated = await updateOpportunity(id, { status, nextAction, expectedRevenue });
if (!updated) return NextResponse.json({ error: "Not found" }, { status: 404 });
return NextResponse.json(updated);
} catch (error) {
return NextResponse.json(
{ error: error instanceof Error ? error.message : "Unknown error" },
{ status: 400 }
);| await fetch(endpointMap[entityType], { | ||
| method: "PATCH", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify({ status: next }), | ||
| }); | ||
| setStatus(next); | ||
| router.refresh(); |
There was a problem hiding this comment.
The fetch call does not check if the response was successful (res.ok). If the API request fails (e.g., due to a 404 or 500 error), the component will still update its local state to the "next" status and trigger a router refresh, leading to a UI that is out of sync with the server data.
const res = await fetch(endpointMap[entityType], {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ status: next }),
});
if (!res.ok) throw new Error("Failed to update status");
setStatus(next);
router.refresh();
Summary
This PR introduces clickable status cycling for all entities (leads, opportunities, offers, assets) and adds content attribution tracking to leads. Users can now advance entity statuses by clicking a button, and leads capture which content item referred them.
Key Changes
New
StatusCycleButtoncomponent: A reusable client component that cycles through entity statuses with visual feedback. Includes:API endpoints for status updates: Added PATCH routes for:
/api/leads/[id]/api/opportunities/[id]/api/offers/[id]/api/assets/[id]Lead attribution tracking:
refContentIdfield to leads to track which content item referred themLeadFormto capture and passrefContentIdfrom URL search paramsdb.contentItemsfor displayOffer enhancements:
calUrlfield for Cal.com booking integrationcalUrlpresencerefquery parameter for content attributionUI improvements:
SectionCardcomponents with custom card layouts in assets and offers pagesStatusCycleButtoninstead of static badgeStore updates:
updateLead,updateOffer,updateOpportunity,updateAssetfunctionscalUrl,refContentId)Implementation Details
refContentIdto API, enabling attribution analyticshttps://claude.ai/code/session_01J9wgN7CnCeMnJkAhTyNFMb