From 6056214039c6659dcdcd7f47cb83ca931261d543 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Sat, 18 Apr 2026 10:46:21 +0000 Subject: [PATCH] recovering from merge missing files, adding Hubspot sync capabilities --- .../[portfolioId]/approvals/route.ts | 26 +++++ .../[portfolioId]/removal-requests/route.ts | 25 ++++- .../api/upload/contractor-install/route.ts | 10 ++ src/app/lib/hubspot/client.ts | 14 +++ src/app/lib/hubspot/dealSync.ts | 96 +++++++++++++++++++ .../(portfolio)/settings/user-access/page.tsx | 2 + .../your-projects/live/LiveTracker.tsx | 2 + 7 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 src/app/lib/hubspot/client.ts create mode 100644 src/app/lib/hubspot/dealSync.ts diff --git a/src/app/api/portfolio/[portfolioId]/approvals/route.ts b/src/app/api/portfolio/[portfolioId]/approvals/route.ts index c4b3933..9714052 100644 --- a/src/app/api/portfolio/[portfolioId]/approvals/route.ts +++ b/src/app/api/portfolio/[portfolioId]/approvals/route.ts @@ -10,6 +10,7 @@ import { and, eq, inArray, sql } from "drizzle-orm"; import { z } from "zod"; import { getServerSession } from "next-auth"; import { AuthOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { syncMeasureApprovalsToHubSpot } from "@/app/lib/hubspot/dealSync"; async function getRequestingUserId(email: string): Promise { const rows = await db @@ -204,6 +205,31 @@ export async function POST( }); } + const affectedDealIds = [...new Set(body.changes.map((c) => c.hubspotDealId))]; + for (const dealId of affectedDealIds) { + const approvalRows = await db + .select({ + measureName: dealMeasureApprovals.measureName, + approvedByEmail: user.email, + }) + .from(dealMeasureApprovals) + .leftJoin(user, eq(user.id, dealMeasureApprovals.approvedBy)) + .where( + and( + eq(dealMeasureApprovals.hubspotDealId, dealId), + eq(dealMeasureApprovals.isApproved, true), + ), + ); + + void syncMeasureApprovalsToHubSpot({ + hubspotDealId: dealId, + approvedMeasures: approvalRows.map((r) => ({ + measureName: r.measureName, + approvedByEmail: r.approvedByEmail ?? "unknown", + })), + }); + } + return NextResponse.json({ success: true }); } catch (err) { console.error("POST /approvals error:", err); diff --git a/src/app/api/portfolio/[portfolioId]/removal-requests/route.ts b/src/app/api/portfolio/[portfolioId]/removal-requests/route.ts index 5b7aa0a..5a933b8 100644 --- a/src/app/api/portfolio/[portfolioId]/removal-requests/route.ts +++ b/src/app/api/portfolio/[portfolioId]/removal-requests/route.ts @@ -7,6 +7,7 @@ import { and, eq, desc } from "drizzle-orm"; import { z } from "zod"; import { getServerSession } from "next-auth"; import { AuthOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { syncRemovalRequestToHubSpot } from "@/app/lib/hubspot/dealSync"; const WRITE_ROLES = ["creator", "admin", "write"] as const; @@ -204,6 +205,13 @@ export async function POST( }) .returning(); + void syncRemovalRequestToHubSpot({ + hubspotDealId, + status: "pending", + reason, + requestedByEmail: requestingUser.email, + }); + return NextResponse.json({ success: true, id: String(inserted.id) }); } catch (err) { console.error("[removal-requests POST]", err); @@ -258,8 +266,15 @@ export async function PATCH( try { const target = await db - .select({ id: propertyRemovalRequests.id, status: propertyRemovalRequests.status }) + .select({ + id: propertyRemovalRequests.id, + status: propertyRemovalRequests.status, + hubspotDealId: propertyRemovalRequests.hubspotDealId, + reason: propertyRemovalRequests.reason, + requestedByEmail: user.email, + }) .from(propertyRemovalRequests) + .innerJoin(user, eq(user.id, propertyRemovalRequests.requestedBy)) .where(eq(propertyRemovalRequests.id, BigInt(requestId))) .limit(1); @@ -283,6 +298,14 @@ export async function PATCH( }) .where(eq(propertyRemovalRequests.id, BigInt(requestId))); + void syncRemovalRequestToHubSpot({ + hubspotDealId: target[0].hubspotDealId, + status: action, + reason: target[0].reason, + requestedByEmail: target[0].requestedByEmail, + reviewedByEmail: requestingUser.email, + }); + return NextResponse.json({ success: true }); } catch (err) { console.error("[removal-requests PATCH]", err); diff --git a/src/app/api/upload/contractor-install/route.ts b/src/app/api/upload/contractor-install/route.ts index c3c3fa0..b8ce562 100644 --- a/src/app/api/upload/contractor-install/route.ts +++ b/src/app/api/upload/contractor-install/route.ts @@ -6,6 +6,7 @@ import { eq, inArray } from "drizzle-orm"; import { z } from "zod"; import { getServerSession } from "next-auth"; import { AuthOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { syncContractorDocUploadToHubSpot } from "@/app/lib/hubspot/dealSync"; // POST — record a contractor install document in uploaded_files (fileType optional — can be classified later) export async function POST(req: NextRequest) { @@ -56,6 +57,15 @@ export async function POST(req: NextRequest) { }) .returning({ id: uploadedFiles.id }); + if (body.hubspotDealId) { + void syncContractorDocUploadToHubSpot({ + hubspotDealId: body.hubspotDealId, + fileType: body.fileType ?? null, + measureName: body.measureName ?? null, + uploadedByEmail: session.user.email, + }); + } + return NextResponse.json({ id: inserted.id.toString() }, { status: 201 }); } catch (err) { console.error("POST /upload/contractor-install error:", err); diff --git a/src/app/lib/hubspot/client.ts b/src/app/lib/hubspot/client.ts new file mode 100644 index 0000000..80c2532 --- /dev/null +++ b/src/app/lib/hubspot/client.ts @@ -0,0 +1,14 @@ +import { Client } from "@hubspot/api-client"; + +let _client: Client | null = null; + +export function getHubSpotClient(): Client { + if (!_client) { + const accessToken = process.env.HUBSPOT_API_KEY; + if (!accessToken) { + throw new Error("HUBSPOT_API_KEY environment variable is not set"); + } + _client = new Client({ accessToken }); + } + return _client; +} diff --git a/src/app/lib/hubspot/dealSync.ts b/src/app/lib/hubspot/dealSync.ts new file mode 100644 index 0000000..32a3f4f --- /dev/null +++ b/src/app/lib/hubspot/dealSync.ts @@ -0,0 +1,96 @@ +import { getHubSpotClient } from "./client"; + +export async function syncRemovalRequestToHubSpot(params: { + hubspotDealId: string; + status: "pending" | "approved" | "declined"; + reason: string; + requestedByEmail: string; + reviewedByEmail?: string | null; +}): Promise { + try { + const client = getHubSpotClient(); + + const statusLabel = + params.status === "pending" + ? "Removal Request In Progress" + : params.status === "approved" + ? "Removed From Project" + : ""; + + let log = `Requested by: ${params.requestedByEmail}\nReason: ${params.reason}`; + if (params.reviewedByEmail) { + const action = params.status === "approved" ? "Approved" : "Declined"; + log += `\n${action} by: ${params.reviewedByEmail}`; + } + + await client.crm.deals.basicApi.update(params.hubspotDealId, { + properties: { + project_removal_status: statusLabel, + project_removal_request_log: log, + }, + }); + } catch (err) { + console.error("[HubSpot] syncRemovalRequestToHubSpot failed", { + dealId: params.hubspotDealId, + error: err, + }); + } +} + +export async function syncContractorDocUploadToHubSpot(params: { + hubspotDealId: string; + fileType: string | null; + measureName: string | null; + uploadedByEmail: string; +}): Promise { + try { + const client = getHubSpotClient(); + + const log = [ + `File type: ${params.fileType ?? "unclassified"}`, + `Measure: ${params.measureName ?? "N/A"}`, + `Uploaded by: ${params.uploadedByEmail}`, + ].join("\n"); + + await client.crm.deals.basicApi.update(params.hubspotDealId, { + properties: { + contractor_document_upload_log: log, + }, + }); + } catch (err) { + console.error("[HubSpot] syncContractorDocUploadToHubSpot failed", { + dealId: params.hubspotDealId, + error: err, + }); + } +} + +export async function syncMeasureApprovalsToHubSpot(params: { + hubspotDealId: string; + approvedMeasures: Array<{ measureName: string; approvedByEmail: string }>; +}): Promise { + try { + const client = getHubSpotClient(); + + const log = + params.approvedMeasures.length === 0 + ? "No measures currently approved" + : [ + "Approved measures:", + ...params.approvedMeasures.map( + (m) => `- ${m.measureName} (approved by ${m.approvedByEmail})`, + ), + ].join("\n"); + + await client.crm.deals.basicApi.update(params.hubspotDealId, { + properties: { + client_measures_approval_log: log, + }, + }); + } catch (err) { + console.error("[HubSpot] syncMeasureApprovalsToHubSpot failed", { + dealId: params.hubspotDealId, + error: err, + }); + } +} diff --git a/src/app/portfolio/[slug]/(portfolio)/settings/user-access/page.tsx b/src/app/portfolio/[slug]/(portfolio)/settings/user-access/page.tsx index 5d749ab..ae98d36 100644 --- a/src/app/portfolio/[slug]/(portfolio)/settings/user-access/page.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/settings/user-access/page.tsx @@ -1,4 +1,5 @@ import { UsersPermissionsCard } from "../UsersPermissionsCard"; +import { CapabilitiesCard } from "../CapabilitiesCard"; export default async function UserAccessPage(props: { params: Promise<{ slug: string }>; @@ -8,6 +9,7 @@ export default async function UserAccessPage(props: { return (
+
); } diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/LiveTracker.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/LiveTracker.tsx index f8e92ab..7311c30 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/LiveTracker.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/LiveTracker.tsx @@ -217,6 +217,8 @@ export default function LiveTracker({ data={currentProject?.allDeals ?? []} onOpenDrawer={handleOpenDrawer} docStatusMap={docStatusMap} + portfolioId={portfolioId} + userCapability={userCapability} />