diff --git a/referral-builder/.gitignore b/referral-builder/.gitignore new file mode 100644 index 0000000..a725057 --- /dev/null +++ b/referral-builder/.gitignore @@ -0,0 +1,40 @@ +# Dependencies +node_modules/ + +# Environment variables +.env +.env*.local + +# OS files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Build outputs +dist/ +build/ +.next/ +out/ + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# HubSpot +.hubspot/ + +# Vercel +.vercel/ diff --git a/referral-builder/CHANGES.md b/referral-builder/CHANGES.md new file mode 100644 index 0000000..ec84e73 --- /dev/null +++ b/referral-builder/CHANGES.md @@ -0,0 +1,267 @@ +# Referral Builder Fixes - Implementation Summary + +## Overview +This document summarizes the changes made to fix the HubSpot UI extension and Vercel API for referral creation. + +--- + +## Changes Made + +### 1. New API Endpoint: `/api/deals/[dealId]/context` +**File:** `vercel-api/src/app/api/deals/[dealId]/context/route.ts` (NEW) + +**Purpose:** Fetches the deal's associated company ID reliably before referral creation. + +**Returns:** +```json +{ + "dealId": "12345", + "dealName": "Example Deal", + "dealStage": "qualifiedtobuy", + "companyId": "67890", + "hasCompany": true +} +``` + +**Why this is needed:** +- The UI needs to know the company associated with a deal before creating a referral +- Without this endpoint, users would get "dealId and companyId are required" errors +- Enables the UI to show a warning if a deal has no associated company + +--- + +### 2. ReferralBuilderCard.tsx - Deal Context Loading +**File:** `hubspot-card/src/app/cards/ReferralBuilderCard.tsx` + +**Changes:** +1. **Added state for deal context:** + - `dealCompanyId` - stores the company ID from the deal's associations + - `dealContextLoaded` - tracks whether context has been loaded + +2. **Added `loadDealContext()` function:** + - Fetches `/api/deals/[dealId]/context` on card load + - Sets `dealCompanyId` from the response + - Marks context as loaded even on error to prevent blocking + +3. **Updated `useEffect` hook:** + - Now loads deal context along with referrals and property definitions + - `await Promise.all([loadDealContext(), loadReferrals(), loadPropertyDefinitions()])` + +4. **Updated UI to show context status:** + - Displays "Associated Company ID: [id]" when available + - Shows warning message if deal has no associated company + - Warning: "This deal has no associated company. You must select a company to create a referral." + +--- + +### 3. ReferralBuilderCard.tsx - Referral Status and Client Interest Dropdowns +**File:** `hubspot-card/src/app/cards/ReferralBuilderCard.tsx` + +**Changes:** +1. **Added state for new referral form fields:** + - `newReferralStatus` - selected referral status for new referrals + - `newClientInterest` - selected client interest for new referrals + +2. **Added two new Select dropdowns in "Add referral" form:** + ```tsx + setNewClientInterest(val)} + /> + ``` + +3. **Updated `createReferral()` function:** + - Now uses `dealCompanyId` as fallback if no company is manually selected + - Includes `outreachStatus` and `clientInterest` in the API payload + - Clears these fields after successful creation + +4. **Updated `canCreate` logic:** + - Now requires `dealContextLoaded` to be true before allowing submit + - Allows submit if either `dealCompanyId` or `selectedCompanyId` is available + - `Boolean(dealId && dealContextLoaded && (dealCompanyId || selectedCompanyId))` + +--- + +### 4. API Already Supports Status and Interest Fields +**File:** `vercel-api/src/app/api/referrals/route.ts` + +**Existing Implementation (No Changes Needed):** +- API already accepts `outreachStatus` and `clientInterest` parameters +- Maps them to HubSpot properties: + - `outreachStatus` → `HS_REFERRAL_PROPS.OUTREACH` (referral_status) + - `clientInterest` → `HS_REFERRAL_PROPS.INTEREST` (client_interest) +- Saves these properties both during creation and updates + +--- + +### 5. Program and Session Dropdowns Already Show Names +**Files:** +- `vercel-api/src/app/api/companies/[companyId]/programs/route.ts` +- `vercel-api/src/app/api/programs/[programId]/sessions/route.ts` + +**Existing Implementation (No Changes Needed):** +- Both endpoints already return `{ id, name }` objects +- Program endpoint: `name: p?.properties?.name || 'Program ${p.id}'` +- Session endpoint: `name: s?.properties?.name || 'Session ${s.id}'` +- UI already uses `name` as the label and `id` as the value + +**Note:** If dropdowns still show IDs, it means the HubSpot records don't have the `name` property populated. + +--- + +## Configuration Reference + +### HubSpot Property Names (from config.ts) +- `referral_status` - Outreach status (Draft, Ready to Send, Sent, etc.) +- `client_interest` - Client interest level (Active/considering, Shortlist, etc.) +- `referral_note_to_company` - Note to company +- `referral_key` - Unique key: `[dealId]-[companyId]` +- `referral_name` - Display name: `[Company Name] – Deal [dealId]` + +### Property Options (sourced from `/api/referrals/properties`) +**Referral Status Options:** +- Draft +- Ready to Send +- Sent +- Resend +- Don't send (already sent) + +**Client Interest Options:** +- Active / considering +- Shortlist +- Neutral +- Unlikely +- Declined +- Selected + +--- + +## How the Flow Works Now + +### 1. Card Load +1. User opens a Deal record in HubSpot +2. ReferralBuilderCard.tsx loads and extracts `dealId` from context +3. Card fetches three things in parallel: + - Deal context (including `companyId`) + - Existing referrals for the deal + - Property definitions (dropdown options) +4. Card displays deal info and any existing referrals + +### 2. Creating a Referral +1. If deal has a company: + - `dealCompanyId` is automatically set + - User can proceed to select program/session + - OR user can search and select a different company + +2. If deal has NO company: + - Warning message is shown + - User MUST search and select a company + - Submit button remains disabled until company is selected + +3. User fills in optional fields: + - Program (cascading load of sessions) + - Session + - Note + - **NEW:** Referral status + - **NEW:** Client interest + +4. User clicks "Create referral": + - Payload includes `dealId`, `companyId`, and all optional fields + - API creates/updates referral record + - API sets properties: referral_status, client_interest, note + - API creates associations: Deal, Company, Program (if selected), Session (if selected) + - Success message shown + - Form is cleared + - Referrals list is refreshed + +### 3. Program/Session Dropdowns +- API returns `{ id, name }` for each program/session +- UI displays `name` as the label +- UI submits `id` as the value +- If name is missing in HubSpot, falls back to "Program [id]" or "Session [id]" + +--- + +## Files Modified + +1. **NEW:** `vercel-api/src/app/api/deals/[dealId]/context/route.ts` +2. **MODIFIED:** `hubspot-card/src/app/cards/ReferralBuilderCard.tsx` +3. **CREATED:** `referral-builder/TEST_CHECKLIST.md` +4. **CREATED:** `referral-builder/CHANGES.md` + +--- + +## Testing Instructions + +See `TEST_CHECKLIST.md` for comprehensive testing steps. + +**Quick Smoke Test:** +1. Open a deal with an associated company +2. Verify card shows "Associated Company ID: [number]" +3. Select a program and session (verify names are shown) +4. Set referral status and client interest +5. Click "Create referral" +6. Verify success message and referral appears in list +7. Open the referral record in HubSpot and verify all properties are set + +--- + +## Troubleshooting + +### "dealId and companyId are required" error +- **Solution:** This should now be fixed! The card fetches `companyId` automatically from `/api/deals/[dealId]/context` +- **If still occurs:** Check that the new endpoint is deployed and accessible + +### Program/Session dropdowns showing IDs instead of names +- **Check:** Are the HubSpot Program and Session records missing the `name` property? +- **Solution:** Populate the `name` property on those records in HubSpot +- **API already correct:** The API is already trying to fetch the `name` property + +### Referral status/interest dropdowns not showing options +- **Check:** Is `/api/referrals/properties` endpoint working? +- **Fallback:** The UI has default options hardcoded if the API fails + +### Submit button is disabled +- **Check 1:** Has the deal context finished loading? (dealContextLoaded = true) +- **Check 2:** Does the deal have an associated company OR has user selected one? +- **Check 3:** Are there any error messages displayed? + +--- + +## Summary of Fixes + +✅ **Task 1: Fix create referral submit error** +- Created `/api/deals/[dealId]/context` endpoint to fetch companyId +- Updated UI to fetch and use dealCompanyId on load +- Submit now blocked until context is loaded and companyId is available +- Warning message shown if deal has no company + +✅ **Task 2: Add referral_status and client_interest dropdowns** +- Added two Select dropdowns to the "Add referral" form +- Values are included in the payload to `/api/referrals` POST endpoint +- API already saves these properties to the referral record (no changes needed) + +✅ **Task 3: Program/session dropdown labels** +- API already returns `{ id, name }` objects (no changes needed) +- UI already uses `name` as label and `id` as value (no changes needed) +- If IDs are still showing, the HubSpot records need `name` property populated + +--- + +## Next Steps + +1. Deploy the updated code to Vercel +2. Update `API_BASE` in `ReferralBuilderCard.tsx` to point to the Vercel domain +3. Test using the checklist in `TEST_CHECKLIST.md` +4. If program/session names are still showing IDs, populate the `name` property on those HubSpot records diff --git a/referral-builder/QUICKSTART.md b/referral-builder/QUICKSTART.md new file mode 100644 index 0000000..fd98bfa --- /dev/null +++ b/referral-builder/QUICKSTART.md @@ -0,0 +1,106 @@ +# Quick Start Guide + +Get the Referral Builder running in 15 minutes. + +## Prerequisites Checklist + +- [ ] HubSpot developer account +- [ ] HubSpot private app access token with required scopes +- [ ] Custom objects created (Program, Session, Referral) +- [ ] Node.js 18+ installed +- [ ] HubSpot CLI: `npm install -g @hubspot/cli` +- [ ] Vercel account +- [ ] Git installed + +## 5-Step Setup + +### 1️⃣ Deploy Vercel API (5 min) + +```bash +cd referral-builder/vercel-api +npm install +cp .env.example .env +# Edit .env and add your HUBSPOT_ACCESS_TOKEN +npm run dev # Test locally (optional) +vercel # Deploy to production +``` + +📝 **Save your Vercel URL**: `https://your-project.vercel.app` + +### 2️⃣ Configure Environment Variables in Vercel (2 min) + +Go to Vercel Dashboard → Settings → Environment Variables + +Add: +``` +HUBSPOT_ACCESS_TOKEN=pat-na1-xxxxx +HS_PROGRAM_OBJECT_TYPE=p_program +HS_SESSION_OBJECT_TYPE=p_session +HS_REFERRAL_OBJECT_TYPE=p_referral +``` + +### 3️⃣ Update HubSpot Card Config (3 min) + +**A. Edit `hubspot-card/src/app/app-hsmeta.json`:** +```json +{ + "permittedUrls": { + "fetch": [ + "https://api.hubapi.com", + "https://your-project.vercel.app" ← YOUR VERCEL URL + ] + } +} +``` + +**B. Edit `hubspot-card/src/app/cards/ReferralBuilderCard.tsx` line 13:** +```typescript +const API_BASE = "https://your-project.vercel.app"; // ← YOUR VERCEL URL +``` + +### 4️⃣ Deploy HubSpot Card (3 min) + +```bash +cd ../hubspot-card +hs auth # Authenticate with HubSpot +hs project upload +``` + +### 5️⃣ Add Card to Deal Layout (2 min) + +1. HubSpot → **Settings** → **Objects** → **Deals** → **Record customization** +2. Edit your layout +3. Right sidebar → **Add card** → Find **"Referral Builder"** +4. Save & Publish + +## ✅ Test It + +1. Open any Deal in HubSpot +2. You should see the **Referral Builder** card in the sidebar +3. Try: + - Search for a company + - Select company → see programs + - Select program → see sessions + - Create a referral + - Update outreach status and client interest + +## 🚨 Common Issues + +| Problem | Solution | +|---------|----------| +| Card not visible | Add card to Deal layout in HubSpot settings | +| API errors | Check Vercel domain is correct in both config files | +| No companies found | Verify HUBSPOT_ACCESS_TOKEN has company read scope | +| No programs/sessions | Check associations in HubSpot custom objects | + +## 📖 Need More Help? + +See the full [README.md](./README.md) for: +- Detailed setup instructions +- API endpoint documentation +- Troubleshooting guide +- Architecture overview + +--- + +**That's it! You're ready to build referrals.** 🎉 diff --git a/referral-builder/README.md b/referral-builder/README.md new file mode 100644 index 0000000..a4f03b2 --- /dev/null +++ b/referral-builder/README.md @@ -0,0 +1,651 @@ +# Camp Referral Builder + +A complete HubSpot integration for managing camp referrals with a Deal sidebar card and external API backend. + +## 📋 Overview + +This project provides a **Referral Builder** for HubSpot that: + +- Displays a custom card on Deal records +- Allows searching for Companies (camps) +- Lists Programs associated with Companies +- Lists Sessions associated with Programs +- Creates Referral records linking Deal → Company → Program → Session +- Updates referral properties (outreach status, client interest, notes) + +### ✨ 2025.02 Schema Update + +This version has been updated to use the **2025.02 HubSpot schema** with the following improvements: + +- **Correct property names**: Updated from `referral_outreach_status` → `referral_status` and `referral_client_interest` → `client_interest` +- **Upsert logic**: Create/Update operations now search for existing referrals by key to prevent duplicates +- **Dynamic associations**: Association type IDs are fetched dynamically instead of hardcoded +- **Auto-managed fields**: Automatically sets `referral_name`, `copied_from_deal_key`, and `copied_from_year` +- **Dynamic enum loading**: Property dropdown options are loaded from HubSpot API +- **Better error handling**: Improved error messages and form clearing after successful create + +## 🏗️ Architecture + +The project consists of two main parts: + +### 1. **HubSpot Card** (`hubspot-card/`) +- Uses HubSpot Developer Projects with `platformVersion: 2025.2` +- React-based UI card that appears in the Deal sidebar +- Deployed via `hs project upload` command + +### 2. **Vercel API** (`vercel-api/`) +- Next.js API routes deployed on Vercel +- Handles all HubSpot API interactions +- Manages custom object operations (Company, Program, Session, Referral) + +``` +┌─────────────────┐ +│ Deal Record │ +│ (HubSpot UI) │ +└────────┬────────┘ + │ + │ displays + ▼ +┌─────────────────┐ ┌──────────────────┐ +│ Referral Card │───────▶│ Vercel API │ +│ (React UI) │ fetch │ (Next.js) │ +└─────────────────┘ └────────┬─────────┘ + │ + │ HubSpot API + ▼ + ┌─────────────────┐ + │ HubSpot CRM │ + │ (Companies, │ + │ Programs, │ + │ Sessions, │ + │ Referrals) │ + └─────────────────┘ +``` + +## 🚀 Getting Started + +### Prerequisites + +1. **HubSpot Account** with: + - Developer account access + - Custom objects created: Program, Session, Referral + - Private app access token with scopes: + - `crm.objects.deals.read` + - `crm.objects.deals.write` + - `crm.objects.companies.read` + - `crm.objects.custom.read` + - `crm.objects.custom.write` + +2. **Development Tools**: + - Node.js 18+ installed + - HubSpot CLI installed: `npm install -g @hubspot/cli` + - Vercel account + - Git + +3. **HubSpot Custom Objects**: + You need to create these custom objects in HubSpot: + - **Program** (associated with Company) + - **Session** (associated with Program) + - **Referral** (associated with Deal, Company, Program, Session) + + Key properties for **Referral** object (2025.02 schema): + - **Editable in UI**: + - `referral_key` (text) - Unique identifier + - `referral_status` (dropdown) - Draft, Ready to Send, Sent, Resend, Don't send (already sent) + - `client_interest` (dropdown) - Active / considering, Shortlist, Neutral, Unlikely, Declined, Selected + - `referral_note_to_company` (text area) - Notes + - `previously_sent_to_camp` (dropdown) - Yes (true), No (false) + - **Auto-managed**: + - `referral_name` (text) - Display name (auto-set on create) + - `copied_from_deal_key` (text) - Source deal key + - `copied_from_year` (number) - Source year + - `email_send_count` (number) - Email tracking + - `email_last_sent_datetime` (datetime) - Last email sent + +--- + +## 📦 Installation + +### Step 1: Set Up HubSpot Custom Objects + +1. Go to **Settings** → **Data Management** → **Objects** +2. Create custom objects: + +#### A. Program Object +- **Name**: Program +- **Associations**: Company (many-to-one) +- **Properties**: name (text) + +#### B. Session Object +- **Name**: Session +- **Associations**: Program (many-to-one) +- **Properties**: + - `name` (text) + - `start_date` (date) + - `end_date` (date) + - `price` (number) + - `weeks` (number) + +#### C. Referral Object +- **Name**: Referral +- **Associations**: + - Deal (many-to-one) + - Company (many-to-one) + - Program (many-to-one, optional) + - Session (many-to-one, optional) +- **Properties** (2025.02 schema): + - **Editable**: + - `referral_key` (text, unique) + - `referral_status` (dropdown): Draft, Ready to Send, Sent, Resend, Don't send (already sent) + - `client_interest` (dropdown): Active / considering, Shortlist, Neutral, Unlikely, Declined, Selected + - `referral_note_to_company` (text area) + - `previously_sent_to_camp` (dropdown): Yes (true), No (false) + - **Auto-managed**: + - `referral_name` (text) - Auto-set on create + - `copied_from_deal_key` (text) + - `copied_from_year` (number) + - `email_send_count` (number) + - `email_last_sent_datetime` (datetime) + +### Step 2: Deploy Vercel API + +1. **Navigate to the Vercel API directory**: + ```bash + cd referral-builder/vercel-api + ``` + +2. **Install dependencies**: + ```bash + npm install + ``` + +3. **Set up environment variables**: + ```bash + cp .env.example .env + ``` + +4. **Edit `.env`** and add your HubSpot access token: + ```env + HUBSPOT_ACCESS_TOKEN=pat-na1-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + + # Adjust these if your custom object names differ + HS_PROGRAM_OBJECT_TYPE=p_program + HS_SESSION_OBJECT_TYPE=p_session + HS_REFERRAL_OBJECT_TYPE=p_referral + + # Adjust these if your property names differ (2025.02 schema defaults) + # Editable properties + HS_REFERRAL_KEY_PROP=referral_key + HS_REFERRAL_OUTREACH_PROP=referral_status + HS_REFERRAL_INTEREST_PROP=client_interest + HS_REFERRAL_NOTE_PROP=referral_note_to_company + HS_REFERRAL_PREVIOUSLY_SENT_PROP=previously_sent_to_camp + + # Auto-managed properties + HS_REFERRAL_NAME_PROP=referral_name + HS_REFERRAL_COPIED_DEAL_KEY_PROP=copied_from_deal_key + HS_REFERRAL_COPIED_YEAR_PROP=copied_from_year + + # Optional: Deal properties for metadata copying + # HS_DEAL_KEY_PROP=deal_key_property_name + # HS_DEAL_YEAR_PROP=deal_year_property_name + ``` + +5. **Test locally** (optional): + ```bash + npm run dev + ``` + Visit http://localhost:3000/api/health to verify it works. + +6. **Deploy to Vercel**: + + **Option A: Deploy via Vercel CLI** + ```bash + npm install -g vercel + vercel + ``` + Follow the prompts to link to your Vercel account. + + **Option B: Deploy via Vercel Dashboard** + - Push code to GitHub + - Go to https://vercel.com/new + - Import your repository + - Add environment variables in Vercel dashboard + +7. **Configure Vercel Environment Variables**: + In Vercel dashboard → Settings → Environment Variables, add: + - `HUBSPOT_ACCESS_TOKEN` + - `HS_PROGRAM_OBJECT_TYPE` + - `HS_SESSION_OBJECT_TYPE` + - `HS_REFERRAL_OBJECT_TYPE` + - (and any other variables from `.env.example`) + +8. **Note your Vercel domain**: + After deployment, you'll get a URL like `https://your-project.vercel.app` + +### Step 3: Configure HubSpot Card + +1. **Navigate to the HubSpot card directory**: + ```bash + cd ../hubspot-card + ``` + +2. **Update the Vercel domain in TWO places**: + + **A. In `src/app/app-hsmeta.json`**: + ```json + { + "permittedUrls": { + "fetch": [ + "https://api.hubapi.com", + "https://your-project.vercel.app" ← CHANGE THIS + ] + } + } + ``` + + **B. In `src/app/cards/ReferralBuilderCard.tsx`** (line 13): + ```typescript + const API_BASE = "https://your-project.vercel.app"; // ← CHANGE THIS + ``` + +3. **Authenticate HubSpot CLI**: + ```bash + hs auth + ``` + Follow the prompts to authenticate with your HubSpot account. + +4. **Deploy to HubSpot**: + ```bash + hs project upload + ``` + + This will upload your card to HubSpot. + +### Step 4: Add Card to Deal Layout + +The card won't automatically appear on Deals. You need to add it: + +1. Go to **Settings** → **Objects** → **Deals** → **Record customization** +2. Edit the layout you use +3. In the right sidebar, click **"Add card"** +4. Find **"Referral Builder"** under your app +5. Click **Save** and **Publish** + +### Step 5: Test the Integration + +1. Open any Deal record in HubSpot +2. The **Referral Builder** card should appear in the right sidebar +3. Test the workflow: + - Search for a company + - Select a company → programs should load + - Select a program → sessions should load + - Create a referral + - Update referral properties + - Reload to see persisted data + +--- + +## 🔧 Configuration + +### Custom Object Names + +If your custom objects use different names (e.g., `p_camp_program` instead of `p_program`): + +1. Update `.env` in `vercel-api/`: + ```env + HS_PROGRAM_OBJECT_TYPE=p_camp_program + HS_SESSION_OBJECT_TYPE=p_camp_session + HS_REFERRAL_OBJECT_TYPE=p_camp_referral + ``` + +2. Redeploy to Vercel + +### Property Names + +If your Referral object uses different property names: + +1. Update `.env` in `vercel-api/`: + ```env + HS_REFERRAL_KEY_PROP=custom_referral_key + HS_REFERRAL_OUTREACH_PROP=custom_outreach_status + # etc. + ``` + +2. Update the card UI in `ReferralBuilderCard.tsx` where it calls `updateReferral()` to use matching property names + +3. Redeploy both Vercel API and HubSpot card + +--- + +## 📁 Project Structure + +``` +referral-builder/ +├── hubspot-card/ # HubSpot UI Extension +│ ├── hsproject.json # Project config (platformVersion: 2025.2) +│ ├── .gitignore +│ └── src/ +│ └── app/ +│ ├── app-hsmeta.json # App metadata +│ └── cards/ +│ ├── card-hsmeta.json # Card metadata +│ ├── ReferralBuilderCard.tsx # Card UI component +│ └── package.json +│ +└── vercel-api/ # Next.js API Backend + ├── package.json + ├── tsconfig.json + ├── next.config.js + ├── vercel.json + ├── .env.example + ├── .gitignore + └── src/ + ├── lib/ + │ ├── hubspot.ts # HubSpot API client + │ ├── config.ts # Configuration constants + │ ├── associations.ts # Association helper + │ └── objects.ts # Object helper + └── app/ + └── api/ + ├── health/ + │ └── route.ts # Health check endpoint + ├── companies/ + │ ├── search/ + │ │ └── route.ts # Search companies + │ └── [companyId]/ + │ └── programs/ + │ └── route.ts # Get programs for company + ├── programs/ + │ └── [programId]/ + │ └── sessions/ + │ └── route.ts # Get sessions for program + ├── deals/ + │ └── [dealId]/ + │ └── referrals/ + │ └── route.ts # Get referrals for deal + └── referrals/ + ├── route.ts # Create/upsert referral + ├── properties/ + │ └── route.ts # Get property definitions + └── [referralId]/ + └── route.ts # Update referral +``` + +--- + +## 🔌 API Endpoints + +All endpoints are prefixed with your Vercel domain (e.g., `https://your-project.vercel.app`). + +### `GET /api/health` +Health check endpoint. + +**Response:** +```json +{ + "ok": true, + "ts": "2025-01-11T12:00:00.000Z" +} +``` + +### `GET /api/companies/search?q={query}` +Search for companies by name. + +**Parameters:** +- `q` (required): Search query +- `limit` (optional): Max results (default: 20) + +**Response:** +```json +{ + "results": [ + { + "id": "12345", + "name": "Camp Adventure" + } + ] +} +``` + +### `GET /api/companies/{companyId}/programs` +Get programs associated with a company. + +**Response:** +```json +{ + "results": [ + { + "id": "67890", + "name": "Summer Adventure Program" + } + ] +} +``` + +### `GET /api/programs/{programId}/sessions` +Get sessions associated with a program. + +**Response:** +```json +{ + "results": [ + { + "id": "11111", + "name": "Session 1", + "startDate": "2025-06-01", + "endDate": "2025-06-15", + "price": "1200", + "weeks": "2" + } + ] +} +``` + +### `GET /api/deals/{dealId}/referrals` +Get all referrals associated with a deal. + +**Response:** +```json +{ + "results": [ + { + "id": "22222", + "referralKey": "12345-67890", + "outreachStatus": "Draft", + "clientInterest": "Active / considering", + "note": "Great fit for the family", + "company": { + "id": "67890", + "name": "Camp Adventure" + }, + "program": { + "id": "11111", + "name": "Summer Adventure Program" + }, + "session": { + "id": "33333", + "name": "Session 1", + "startDate": "2025-06-01", + "endDate": "2025-06-15", + "price": "1200" + } + } + ] +} +``` + +### `GET /api/referrals/properties` +Get property definitions for referral enums (used for dropdown options). + +**Response:** +```json +{ + "properties": { + "referral_status": { + "name": "referral_status", + "label": "Outreach Status", + "options": [ + { "label": "Draft", "value": "Draft" }, + { "label": "Ready to Send", "value": "Ready to Send" } + ] + }, + "client_interest": { + "name": "client_interest", + "label": "Client Interest", + "options": [ + { "label": "Active / considering", "value": "Active / considering" } + ] + } + } +} +``` + +### `POST /api/referrals` +Create a new referral (or update if already exists). + +**Request body:** +```json +{ + "dealId": "12345", + "companyId": "67890", + "programId": "11111", + "sessionId": "33333", + "note": "Great fit for the family", + "outreachStatus": "Draft", + "clientInterest": "Active / considering" +} +``` + +**Response:** +```json +{ + "ok": true, + "referralId": "22222", + "created": true, + "updated": false +} +``` + +### `PATCH /api/referrals/{referralId}` +Update referral properties. + +**Request body:** +```json +{ + "properties": { + "referral_status": "Sent", + "client_interest": "Shortlist", + "referral_note_to_company": "Updated note" + } +} +``` + +**Response:** +```json +{ + "ok": true +} +``` + +--- + +## 🐛 Troubleshooting + +### Card not appearing on Deals +- Verify you added the card to the Deal layout (Settings → Objects → Deals → Record customization) +- Check that `objectTypes: ["deals"]` is set in `card-hsmeta.json` +- Re-run `hs project upload` + +### API requests failing +- Verify your Vercel domain is correctly set in both `app-hsmeta.json` and `ReferralBuilderCard.tsx` +- Check Vercel logs for errors +- Verify `HUBSPOT_ACCESS_TOKEN` is set in Vercel environment variables +- Test the health endpoint: `https://your-project.vercel.app/api/health` + +### Custom objects not found +- Verify object type IDs in `.env` match your HubSpot setup +- Check object API names in HubSpot Settings → Objects +- Use `p_{object_name}` format or full `objectTypeId` + +### Permission errors +- Verify your HubSpot access token has all required scopes +- Check token hasn't expired +- Ensure token has access to custom objects + +### Associations not working +- Verify associations are set up between objects in HubSpot +- Check that Companies have associated Programs +- Check that Programs have associated Sessions +- Ensure association labels are set correctly + +--- + +## 🔄 Updating the Application + +### Update HubSpot Card UI + +1. Make changes to `ReferralBuilderCard.tsx` +2. Run: + ```bash + cd hubspot-card + hs project upload + ``` +3. Refresh the Deal page in HubSpot + +### Update Vercel API + +1. Make changes to API routes or helpers +2. Commit and push to GitHub (if using automatic deployments) +3. Or run: + ```bash + cd vercel-api + vercel --prod + ``` +4. Changes take effect immediately (no HubSpot refresh needed) + +--- + +## 📚 Additional Resources + +- [HubSpot Developer Projects Documentation](https://developers.hubspot.com/docs/platform/developer-projects) +- [HubSpot UI Extensions Documentation](https://developers.hubspot.com/docs/platform/ui-extensions-overview) +- [HubSpot CRM API Documentation](https://developers.hubspot.com/docs/api/crm/understanding-the-crm) +- [Vercel Documentation](https://vercel.com/docs) +- [Next.js API Routes](https://nextjs.org/docs/api-routes/introduction) + +--- + +## 🤝 Support + +If you encounter issues: + +1. Check the Troubleshooting section above +2. Review Vercel deployment logs +3. Check HubSpot developer console for errors +4. Verify all environment variables are set correctly + +--- + +## 📝 License + +This project is provided as-is for internal use. + +--- + +## ✅ Next Steps / Future Enhancements + +Consider implementing: + +1. **Session Multi-Select**: Allow selecting multiple session options per referral +2. **Copy Prior Year Referrals**: Clone referrals from previous Deals for the same household +3. **Auto-Update Deal**: When client interest becomes "Selected", update Deal amount and close date +4. **Enhanced Error Handling**: More robust error messages and loading states +5. **Bulk Operations**: Create multiple referrals at once +6. **Filtering & Sorting**: Filter referrals by status, sort by date/interest +7. **Email Integration**: Send referral emails directly from the card +8. **Analytics Dashboard**: Track referral conversion rates + +--- + +**Built with ❤️ using HubSpot 2025.2 Platform and Next.js** diff --git a/referral-builder/TEST_CHECKLIST.md b/referral-builder/TEST_CHECKLIST.md new file mode 100644 index 0000000..d433123 --- /dev/null +++ b/referral-builder/TEST_CHECKLIST.md @@ -0,0 +1,233 @@ +# HubSpot Referral Builder - Test Checklist + +## Overview +This checklist verifies that the referral creation flow works correctly with all the recent fixes. + +## Test Environment Setup +1. Ensure the Vercel API is deployed and `API_BASE` in `ReferralBuilderCard.tsx` points to the correct domain +2. Ensure HubSpot access token is configured in Vercel environment variables +3. Have a test Deal record with an associated Company in HubSpot + +--- + +## Test 1: Deal Context Loading +**Purpose:** Verify that the card correctly loads dealId and companyId from the deal's context. + +### Steps: +1. Navigate to a Deal record in HubSpot +2. Open the Referral Builder card in the sidebar + +### Expected Results: +- [ ] Card displays "Deal ID: [number]" +- [ ] Card displays "Associated Company ID: [number]" (if deal has a company) +- [ ] If deal has NO associated company, card shows warning: "Warning: This deal has no associated company. You must select a company to create a referral." +- [ ] No error messages appear during loading +- [ ] Existing referrals section loads (may be empty) + +--- + +## Test 2: Company Search and Selection +**Purpose:** Verify company search functionality works (for deals without an associated company). + +### Steps: +1. In the "Add referral" section, type a company name in "Search company" field +2. Click "Search" button +3. Select a company from the dropdown + +### Expected Results: +- [ ] Company dropdown populates with search results +- [ ] Company names are displayed (not just IDs) +- [ ] Selecting a company triggers the program dropdown to load + +--- + +## Test 3: Program Dropdown with Friendly Names +**Purpose:** Verify that program dropdown shows human-friendly names instead of IDs. + +### Steps: +1. After selecting a company (or if deal already has one), observe the "Program" dropdown +2. Click the Program dropdown to view options + +### Expected Results: +- [ ] Program dropdown shows program names (e.g., "Summer Camp 2025") +- [ ] NOT showing "Program 12345678" unless the record truly has no name +- [ ] Dropdown is populated with programs associated with the selected company + +--- + +## Test 4: Session Dropdown with Friendly Names +**Purpose:** Verify that session dropdown shows human-friendly names and details. + +### Steps: +1. After selecting a program, observe the "Session" dropdown +2. Click the Session dropdown to view options + +### Expected Results: +- [ ] Session dropdown shows session names with details (e.g., "Session 1 (2025-06-01) $5000") +- [ ] NOT showing "Session 98765432" unless the record truly has no name +- [ ] Dropdown is populated with sessions associated with the selected program + +--- + +## Test 5: Referral Status Dropdown +**Purpose:** Verify that referral_status dropdown is present and functional in the create form. + +### Steps: +1. In the "Add referral" section, locate the "Referral status (optional)" dropdown +2. Click to view available options + +### Expected Results: +- [ ] Dropdown is present between "Note to company" and "Client interest" +- [ ] Options include: "Draft", "Ready to Send", "Sent", "Resend", "Don't send (already sent)" +- [ ] Values can be selected + +--- + +## Test 6: Client Interest Dropdown +**Purpose:** Verify that client_interest dropdown is present and functional in the create form. + +### Steps: +1. In the "Add referral" section, locate the "Client interest (optional)" dropdown +2. Click to view available options + +### Expected Results: +- [ ] Dropdown is present after "Referral status" +- [ ] Options include: "Active / considering", "Shortlist", "Neutral", "Unlikely", "Declined", "Selected" +- [ ] Values can be selected + +--- + +## Test 7: Create Referral with All Fields +**Purpose:** Verify that a referral can be created with all fields populated. + +### Steps: +1. Fill in all fields in the "Add referral" form: + - Select or search for a company + - Select a program + - Select a session + - Enter a note in "Note to company" + - Select a value for "Referral status" + - Select a value for "Client interest" +2. Click "Create referral" button + +### Expected Results: +- [ ] Success message appears: "Referral created (ID [number])" +- [ ] Form fields are cleared after creation +- [ ] Referral appears in "Existing referrals" section +- [ ] Selected program and session show names (not IDs) in the existing referrals list + +--- + +## Test 8: Verify Referral Record in HubSpot +**Purpose:** Verify that the created referral has all properties set correctly in HubSpot. + +### Steps: +1. After creating a referral, navigate to the referral record in HubSpot +2. Check the properties on the referral record + +### Expected Results: +- [ ] `referral_key` property is set to "[dealId]-[companyId]" +- [ ] `referral_name` property is set to "[Company Name] – Deal [dealId]" +- [ ] `referral_status` property is set to the selected value (if provided) +- [ ] `client_interest` property is set to the selected value (if provided) +- [ ] `referral_note_to_company` property contains the entered note (if provided) +- [ ] `copied_from_deal_key` and `copied_from_year` are set +- [ ] Associations exist: Referral → Deal, Referral → Company +- [ ] If program selected: Association exists: Referral → Program +- [ ] If session selected: Association exists: Referral → Session + +--- + +## Test 9: Submit Blocked Without Company +**Purpose:** Verify that submit is disabled when no company is available. + +### Steps: +1. Navigate to a Deal record that has NO associated company +2. Do NOT select a company from the search dropdown +3. Attempt to click "Create referral" button + +### Expected Results: +- [ ] "Create referral" button is disabled +- [ ] Warning message is displayed: "Warning: This deal has no associated company..." + +--- + +## Test 10: Update Existing Referral +**Purpose:** Verify that existing referrals can be updated with new status and interest values. + +### Steps: +1. In the "Existing referrals" section, locate a referral +2. Change the "Outreach" dropdown value +3. Change the "Client interest" dropdown value +4. Modify the note +5. Click "Save" button + +### Expected Results: +- [ ] Success message appears: "Referral updated" +- [ ] Changes are persisted (refresh the card and verify values remain) +- [ ] Updated values appear in HubSpot referral record + +--- + +## Test 11: Reload Button +**Purpose:** Verify that the reload button refreshes the referrals list. + +### Steps: +1. In the "Existing referrals" section, click "Reload" button + +### Expected Results: +- [ ] Loading indicator appears briefly +- [ ] Referrals list refreshes with latest data from HubSpot +- [ ] No errors occur + +--- + +## Test 12: API Error Handling +**Purpose:** Verify that API errors are displayed to the user. + +### Steps: +1. Temporarily modify `API_BASE` to an invalid URL or stop the Vercel API +2. Try to create a referral + +### Expected Results: +- [ ] Error message is displayed in red text +- [ ] Error message is descriptive (e.g., "Failed to create/update referral") +- [ ] Application does not crash +- [ ] User can retry after fixing the issue + +--- + +## Additional Notes + +### Common Issues and Solutions: + +1. **Program/Session showing IDs instead of names:** + - Verify that the HubSpot Program and Session records have a `name` property populated + - Check the API responses in browser DevTools Network tab to confirm names are returned + +2. **"dealId and companyId are required" error:** + - Verify that the deal has an associated company OR a company is selected from search + - Check the new `/api/deals/[dealId]/context` endpoint is working + +3. **Referral status/interest dropdowns not showing:** + - Verify that `/api/referrals/properties` endpoint is returning options + - Check that the UI correctly maps the options to the Select components + +### API Endpoints Reference: +- `GET /api/deals/[dealId]/context` - Get deal's associated companyId ✅ NEW +- `GET /api/deals/[dealId]/referrals` - List referrals for a deal +- `POST /api/referrals` - Create/update referral (now accepts outreachStatus, clientInterest) +- `PATCH /api/referrals/[referralId]` - Update referral properties +- `GET /api/referrals/properties` - Get property definitions and options +- `GET /api/companies/search?q=[query]` - Search companies +- `GET /api/companies/[companyId]/programs` - List programs (returns id + name) +- `GET /api/programs/[programId]/sessions` - List sessions (returns id + name + details) + +--- + +## Summary + +**Total Tests:** 12 +**Critical Tests:** 1, 7, 8 (deal context, create flow, HubSpot verification) + +All tests should pass for the referral creation flow to be considered fully functional. diff --git a/referral-builder/hubspot-card/.gitignore b/referral-builder/hubspot-card/.gitignore new file mode 100644 index 0000000..26f3f0a --- /dev/null +++ b/referral-builder/hubspot-card/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +.env +.DS_Store +dist/ +*.log +.hubspot/ diff --git a/referral-builder/hubspot-card/hsproject.json b/referral-builder/hubspot-card/hsproject.json new file mode 100644 index 0000000..98479b8 --- /dev/null +++ b/referral-builder/hubspot-card/hsproject.json @@ -0,0 +1,5 @@ +{ + "name": "Camp Referral Builder", + "srcDir": "src", + "platformVersion": "2025.2" +} diff --git a/referral-builder/hubspot-card/src/app/app-hsmeta.json b/referral-builder/hubspot-card/src/app/app-hsmeta.json new file mode 100644 index 0000000..dd93be5 --- /dev/null +++ b/referral-builder/hubspot-card/src/app/app-hsmeta.json @@ -0,0 +1,35 @@ +{ + "uid": "camp_referral_builder_app", + "type": "app", + "config": { + "description": "Referral Builder (Deal → Company → Program → Session → Referral)", + "name": "Camp Referral Builder", + "distribution": "private", + "auth": { + "type": "static", + "requiredScopes": [ + "crm.objects.deals.read", + "crm.objects.deals.write", + "crm.objects.companies.read", + "crm.objects.custom.read", + "crm.objects.custom.write" + ], + "optionalScopes": [], + "conditionallyRequiredScopes": [] + }, + "permittedUrls": { + "fetch": [ + "https://api.hubapi.com", + "https://YOUR_VERCEL_DOMAIN" + ], + "iframe": [], + "img": [] + }, + "support": { + "supportEmail": "support@example.com", + "documentationUrl": "https://example.com/docs", + "supportUrl": "https://example.com/support", + "supportPhone": "+18005555555" + } + } +} diff --git a/referral-builder/hubspot-card/src/app/cards/ReferralBuilderCard.tsx b/referral-builder/hubspot-card/src/app/cards/ReferralBuilderCard.tsx new file mode 100644 index 0000000..2e8098b --- /dev/null +++ b/referral-builder/hubspot-card/src/app/cards/ReferralBuilderCard.tsx @@ -0,0 +1,504 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { + hubspot, + Box, + Button, + Divider, + Flex, + Heading, + Input, + Select, + Text, + TextArea, +} from "@hubspot/ui-extensions"; + +const API_BASE = "https://YOUR_VERCEL_DOMAIN"; // <-- CHANGE THIS (no trailing slash) + +type Option = { label: string; value: string }; + +type ReferralRow = { + id: string; + referralKey?: string; + outreachStatus?: string; + clientInterest?: string; + note?: string; + company?: { id?: string; name?: string }; + program?: { id?: string; name?: string }; + session?: { id?: string; name?: string; startDate?: string; endDate?: string; price?: string }; +}; + +// Default options (will be replaced by API-loaded options if available) +const DEFAULT_OUTREACH_OPTIONS: Option[] = [ + { label: "Draft", value: "Draft" }, + { label: "Ready to Send", value: "Ready to Send" }, + { label: "Sent", value: "Sent" }, + { label: "Resend", value: "Resend" }, + { label: "Don't send (already sent)", value: "Don't send (already sent)" }, +]; + +const DEFAULT_INTEREST_OPTIONS: Option[] = [ + { label: "Active / considering", value: "Active / considering" }, + { label: "Shortlist", value: "Shortlist" }, + { label: "Neutral", value: "Neutral" }, + { label: "Unlikely", value: "Unlikely" }, + { label: "Declined", value: "Declined" }, + { label: "Selected", value: "Selected" }, +]; + +const DEFAULT_PREVIOUSLY_SENT_OPTIONS: Option[] = [ + { label: "Yes (true)", value: "Yes (true)" }, + { label: "No (false)", value: "No (false)" }, +]; + +hubspot.extend(({ context, actions }) => ( + +)); + +function ReferralBuilderCard({ context, actions }: any) { + const dealId = context?.crm?.objectId ? String(context.crm.objectId) : null; + + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + + const [referrals, setReferrals] = useState([]); + + // Deal context - companyId is fetched from the deal's associations + const [dealCompanyId, setDealCompanyId] = useState(null); + const [dealContextLoaded, setDealContextLoaded] = useState(false); + + const [companyQuery, setCompanyQuery] = useState(""); + const [companyOptions, setCompanyOptions] = useState([]); + const [selectedCompanyId, setSelectedCompanyId] = useState(""); + + const [programOptions, setProgramOptions] = useState([]); + const [selectedProgramId, setSelectedProgramId] = useState(""); + + const [sessionOptions, setSessionOptions] = useState([]); + const [selectedSessionId, setSelectedSessionId] = useState(""); + + const [note, setNote] = useState(""); + + // New referral form fields for referral_status and client_interest + const [newReferralStatus, setNewReferralStatus] = useState(""); + const [newClientInterest, setNewClientInterest] = useState(""); + + const [outreachOptions, setOutreachOptions] = useState(DEFAULT_OUTREACH_OPTIONS); + const [interestOptions, setInterestOptions] = useState(DEFAULT_INTEREST_OPTIONS); + const [previouslySentOptions, setPreviouslySentOptions] = useState(DEFAULT_PREVIOUSLY_SENT_OPTIONS); + + const canCreate = useMemo(() => { + // Can create if we have dealId, dealCompanyId (or selected company), and context is loaded + return Boolean(dealId && dealContextLoaded && (dealCompanyId || selectedCompanyId)); + }, [dealId, dealContextLoaded, dealCompanyId, selectedCompanyId]); + + async function apiRequest(path: string, init?: { method?: string; body?: any }) { + const url = `${API_BASE}${path}`; + const headers: Record = {}; + + if (init?.body) { + headers["Content-Type"] = "application/json"; + } + + const res = await hubspot.fetch(url, { + method: init?.method || "GET", + headers, + body: init?.body ? JSON.stringify(init.body) : undefined, + }); + + let data: any = null; + try { + data = await res.json(); + } catch (e) { + // ignore + } + + if (!res.ok) { + const msg = data?.error || data?.message || `Request failed (${res.status})`; + throw new Error(msg); + } + return data; + } + + async function loadDealContext() { + if (!dealId) return; + try { + const data = await apiRequest(`/api/deals/${dealId}/context`); + setDealCompanyId(data?.companyId || null); + setDealContextLoaded(true); + } catch (e) { + console.error("Failed to load deal context:", e); + setDealContextLoaded(true); // Mark as loaded even on error to prevent blocking UI + } + } + + async function loadPropertyDefinitions() { + try { + const data = await apiRequest(`/api/referrals/properties`); + const props = data?.properties || {}; + + // Update options if available from API + if (props.referral_status?.options?.length) { + setOutreachOptions(props.referral_status.options); + } + if (props.client_interest?.options?.length) { + setInterestOptions(props.client_interest.options); + } + if (props.previously_sent_to_camp?.options?.length) { + setPreviouslySentOptions(props.previously_sent_to_camp.options); + } + } catch (e) { + console.error("Failed to load property definitions:", e); + // Continue with default options + } + } + + async function loadReferrals() { + if (!dealId) return; + const data = await apiRequest(`/api/deals/${dealId}/referrals`); + setReferrals(data?.results || []); + } + + async function searchCompanies() { + if (!companyQuery.trim()) { + setCompanyOptions([]); + return; + } + const data = await apiRequest(`/api/companies/search?q=${encodeURIComponent(companyQuery.trim())}`); + const opts: Option[] = (data?.results || []).map((c: any) => ({ + label: c.name || `Company ${c.id}`, + value: String(c.id), + })); + setCompanyOptions(opts); + } + + async function loadPrograms(companyId: string) { + setProgramOptions([]); + setSelectedProgramId(""); + setSessionOptions([]); + setSelectedSessionId(""); + + if (!companyId) return; + const data = await apiRequest(`/api/companies/${companyId}/programs`); + const opts: Option[] = (data?.results || []).map((p: any) => ({ + label: p.name || `Program ${p.id}`, + value: String(p.id), + })); + setProgramOptions(opts); + } + + async function loadSessions(programId: string) { + setSessionOptions([]); + setSelectedSessionId(""); + + if (!programId) return; + const data = await apiRequest(`/api/programs/${programId}/sessions`); + + const opts: Option[] = (data?.results || []).map((s: any) => { + const labelParts = [s.name || `Session ${s.id}`]; + if (s.startDate) labelParts.push(`(${s.startDate})`); + if (s.price) labelParts.push(`$${s.price}`); + return { label: labelParts.join(" "), value: String(s.id) }; + }); + + setSessionOptions(opts); + } + + async function createReferral() { + if (!dealId) return; + + // Use selectedCompanyId if available, otherwise use dealCompanyId + const companyId = selectedCompanyId || dealCompanyId; + if (!companyId) return; + + const payload = { + dealId, + companyId, + programId: selectedProgramId || undefined, + sessionId: selectedSessionId || undefined, + note: note || undefined, + outreachStatus: newReferralStatus || undefined, + clientInterest: newClientInterest || undefined, + }; + + const data = await apiRequest(`/api/referrals`, { method: "POST", body: payload }); + + const message = data?.created + ? `Referral created (ID ${data?.referralId || "unknown"})` + : `Referral updated (ID ${data?.referralId || "unknown"})`; + + actions?.addAlert?.({ + type: "success", + message, + }); + + // Clear form + setNote(""); + setNewReferralStatus(""); + setNewClientInterest(""); + setSelectedCompanyId(""); + setSelectedProgramId(""); + setSelectedSessionId(""); + setCompanyQuery(""); + setCompanyOptions([]); + setProgramOptions([]); + setSessionOptions([]); + + // Refresh referrals list + await loadReferrals(); + } + + async function updateReferral(referralId: string, properties: Record) { + await apiRequest(`/api/referrals/${referralId}`, { + method: "PATCH", + body: { properties }, + }); + actions?.addAlert?.({ type: "success", message: "Referral updated" }); + await loadReferrals(); + } + + useEffect(() => { + (async () => { + setError(null); + if (!dealId) return; + try { + setBusy(true); + await Promise.all([loadDealContext(), loadReferrals(), loadPropertyDefinitions()]); + } catch (e: any) { + setError(e?.message || "Failed to load data"); + } finally { + setBusy(false); + } + })(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dealId]); + + if (!dealId) { + return ( + + This card is meant to run on a Deal record. + + ); + } + + return ( + + Referral Builder + Deal ID: {dealId} + {dealContextLoaded && dealCompanyId && ( + Associated Company ID: {dealCompanyId} + )} + {dealContextLoaded && !dealCompanyId && ( + + Warning: This deal has no associated company. You must select a company to create a referral. + + )} + + {error ? {error} : null} + + + + Existing referrals + + + {busy ? Loading… : null} + + {referrals.length === 0 ? ( + No referrals yet. + ) : ( + + {referrals.map((r) => ( + + + {r.company?.name || "Company"} + {" — "} + {r.program?.name || "Program"} + {" — "} + {r.session?.name || "Session"} + + + + { + setReferrals((prev) => + prev.map((x) => + x.id === r.id ? { ...x, clientInterest: val } : x + ) + ); + }} + /> + + +