From a155ac4f2f0e20f51e7ffe019f4f8a21cbcf7a81 Mon Sep 17 00:00:00 2001 From: Arturas Mickiewicz Date: Wed, 17 Jun 2026 13:30:12 +0200 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=90=9B=20fix:=20align=20POST=20/oppor?= =?UTF-8?q?tunity=20body=20with=20OpportunityFormDataWithAgentSubmitter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NewOpportunity sent `agentId` (camelCase), which the backend ignored, so the opportunity was never linked to the agent. Rename to `agent_id` per the SDK contract. The create endpoint does not accept a contact object, so contact is now persisted via a follow-up PATCH /opportunity/:id; that PATCH is best-effort and does not fail the create. Bump need4deed-sdk to 0.0.110. Co-Authored-By: Claude Opus 4.8 --- package.json | 2 +- .../NewOpportunity/NewOpportunity.tsx | 33 +++++++++++++++---- yarn.lock | 8 ++--- 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 617389d0..cf3edbb7 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "date-fns": "^4.1.0", "email-validator": "^2.0.4", "i18next": "^25.3.2", - "need4deed-sdk": "0.0.107", + "need4deed-sdk": "0.0.110", "next": "15.3.8", "react": "^19.0.0", "react-day-picker": "^9.13.0", diff --git a/src/components/Dashboard/NewOpportunity/NewOpportunity.tsx b/src/components/Dashboard/NewOpportunity/NewOpportunity.tsx index 812c5e5d..b135ec69 100644 --- a/src/components/Dashboard/NewOpportunity/NewOpportunity.tsx +++ b/src/components/Dashboard/NewOpportunity/NewOpportunity.tsx @@ -4,6 +4,7 @@ import { FormInput } from "@/components/core/common"; import { apiPathOpportunity, DashboardRoutes } from "@/config/constants"; import { useMutationQuery } from "@/hooks"; import { useGetCurrentAgent } from "@/hooks/useGetCurrentAgent"; +import axios from "axios"; import { ApiOpportunityGet } from "need4deed-sdk"; import i18next from "i18next"; import { useRouter } from "next/navigation"; @@ -24,14 +25,19 @@ import { Wrapper, } from "./styled"; -type CreateOpportunityBody = { +type CreateOpportunityResponse = { message: string; data: ApiOpportunityGet }; + +// The create endpoint (`OpportunityFormDataWithAgentSubmitter`) only accepts the +// opportunity itself + `agent_id`; contact details are persisted via a follow-up +// PATCH (`ApiOpportunityPatch.contact`) once we have the new opportunity id. +type CreateOpportunityVariables = { title: string; + agent_id?: number; contact: { name: string; phone: string; email: string; }; - agentId?: number; }; export function NewOpportunity() { @@ -58,9 +64,24 @@ export function NewOpportunity() { mutate: createOpportunity, isPending, error, - } = useMutationQuery({ - apiPath: `${apiPathOpportunity}/`, - method: "post", + } = useMutationQuery({ + mutationFn: async ({ title: oppTitle, agent_id, contact }) => { + const { data: created } = await axios.post(`${apiPathOpportunity}/`, { + title: oppTitle, + agent_id, + }); + const id = created?.data?.id; + if (id) { + // Contact is best-effort: the opportunity already exists and contact is + // editable on its profile, so a failed PATCH must not fail the create. + try { + await axios.patch(`${apiPathOpportunity}/${id}`, { contact }); + } catch { + // swallow — agent can set contact from the opportunity profile + } + } + return created; + }, onSuccessCallback: (response) => { const id = response?.data?.id; if (id) { @@ -85,12 +106,12 @@ export function NewOpportunity() { if (!validate()) return; createOpportunity({ title: title.trim(), + agent_id: agent?.id, contact: { name: contactName.trim(), phone: contactPhone.trim(), email: contactEmail.trim(), }, - agentId: agent?.id, }); }; diff --git a/yarn.lock b/yarn.lock index 221f917b..9f04dc4e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2412,10 +2412,10 @@ natural-compare@^1.4.0: resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== -need4deed-sdk@0.0.107: - version "0.0.107" - resolved "https://registry.yarnpkg.com/need4deed-sdk/-/need4deed-sdk-0.0.107.tgz#3b9e0b42970a9ef6a21045cf522bbca1cdeee902" - integrity sha512-UrHmAnlCg2jfBdM6oVYMESfg5H/U9dAAvIN14zWJBHP82NNtScFEqvojdZD9GlCgUxtDqW1chcWsRY/Zzm7Jvg== +need4deed-sdk@0.0.110: + version "0.0.110" + resolved "https://registry.yarnpkg.com/need4deed-sdk/-/need4deed-sdk-0.0.110.tgz#4428170ca81fddd35ec80f7a72825c3e6b29d8e7" + integrity sha512-kHa6B13x3CcHMhexam+9CBfRX9AV9/8K1RrUTDSAVdRcJ0PecNFs3JSscwBkJ7UWqjSKV89xaqVkN6N3qGQnAA== next@15.3.8: version "15.3.8" From 1536222c57d75993e827f1fd920cffbe028fcc77 Mon Sep 17 00:00:00 2001 From: Arturas Mickiewicz Date: Wed, 17 Jun 2026 13:36:26 +0200 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=90=9B=20fix:=20guard=20opportunity?= =?UTF-8?q?=20creation=20on=20agent=20being=20loaded?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The submit button was only disabled while the mutation was pending, so an agent could submit before GET /agent/me resolved (or if it errored), sending agent_id: undefined and creating an unlinked opportunity. Disable submit until the agent is loaded, and bail in handleSubmit as defense-in-depth. Co-Authored-By: Claude Opus 4.8 --- src/components/Dashboard/NewOpportunity/NewOpportunity.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/Dashboard/NewOpportunity/NewOpportunity.tsx b/src/components/Dashboard/NewOpportunity/NewOpportunity.tsx index b135ec69..c8a51f9e 100644 --- a/src/components/Dashboard/NewOpportunity/NewOpportunity.tsx +++ b/src/components/Dashboard/NewOpportunity/NewOpportunity.tsx @@ -104,9 +104,11 @@ export function NewOpportunity() { const handleSubmit = () => { if (!validate()) return; + // Don't create an unlinked opportunity: the agent must be loaded first. + if (!agent?.id) return; createOpportunity({ title: title.trim(), - agent_id: agent?.id, + agent_id: agent.id, contact: { name: contactName.trim(), phone: contactPhone.trim(), @@ -195,7 +197,7 @@ export function NewOpportunity() { backgroundcolor="var(--color-aubergine)" textColor="var(--color-white)" onClick={handleSubmit} - disabled={isPending} + disabled={isPending || agentLoading || !agent} />