added migration scripts and ability to add a deal in hubspot@

This commit is contained in:
Jun-te Kim 2025-10-22 15:44:00 +00:00
parent 3d088caec4
commit 5772f64f7a
11 changed files with 4079 additions and 33 deletions

1
generate_migration.sh Normal file
View file

@ -0,0 +1 @@
npx drizzle-kit generate

89
package-lock.json generated
View file

@ -12,6 +12,7 @@
"@headlessui/react": "^2.2.7",
"@heroicons/react": "^2.0.18",
"@hookform/resolvers": "^3.9.1",
"@hubspot/api-client": "^13.4.0",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.1.15",
@ -2614,6 +2615,24 @@
"react-hook-form": "^7.0.0"
}
},
"node_modules/@hubspot/api-client": {
"version": "13.4.0",
"resolved": "https://registry.npmjs.org/@hubspot/api-client/-/api-client-13.4.0.tgz",
"integrity": "sha512-B2Bu/F/nxzqvF0LlEIgA28G6ObANXkYosJSFLW3bdYXOOgH9kbVLJwPGze0H6L+dQJ+vmzZ1LeRpl5REXyHShA==",
"license": "ISC",
"dependencies": {
"@types/node": "*",
"@types/node-fetch": "^2.5.7",
"bottleneck": "^2.19.5",
"es6-promise": "^4.2.4",
"form-data": "^4.0.4",
"lodash.merge": "^4.6.2",
"node-fetch": "^2.6.0"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
@ -5874,6 +5893,16 @@
"integrity": "sha512-pg9d0yC4rVNWQzX8U7xb4olIOFuuVL9za3bzMT2pu2SU0SNEi66i2qrvhE2qt0HvkhuCaWJu7pLNOt/Pj8BIrw==",
"license": "MIT"
},
"node_modules/@types/node-fetch": {
"version": "2.6.13",
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz",
"integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==",
"license": "MIT",
"dependencies": {
"@types/node": "*",
"form-data": "^4.0.4"
}
},
"node_modules/@types/nodemailer": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.2.tgz",
@ -6805,7 +6834,6 @@
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true,
"license": "MIT"
},
"node_modules/at-least-node": {
@ -7098,6 +7126,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/bottleneck": {
"version": "2.19.5",
"resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz",
"integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==",
"license": "MIT"
},
"node_modules/bowser": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/bowser/-/bowser-2.12.0.tgz",
@ -7588,7 +7622,6 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
@ -8109,7 +8142,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.4.0"
@ -8624,6 +8656,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/es6-promise": {
"version": "4.2.8",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==",
"license": "MIT"
},
"node_modules/esbuild": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz",
@ -9543,7 +9581,6 @@
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"dev": true,
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
@ -11181,7 +11218,6 @@
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
@ -11191,7 +11227,6 @@
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
@ -11481,6 +11516,26 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/node-releases": {
"version": "2.0.19",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
@ -14224,6 +14279,12 @@
"node": ">=16"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/tree-kill": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
@ -14761,12 +14822,28 @@
"node": ">=12.0.0"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/whatwg-fetch": {
"version": "3.6.20",
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz",
"integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==",
"license": "MIT"
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View file

@ -18,6 +18,7 @@
"@headlessui/react": "^2.2.7",
"@heroicons/react": "^2.0.18",
"@hookform/resolvers": "^3.9.1",
"@hubspot/api-client": "^13.4.0",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.1.15",

1
push_to_db.sh Normal file
View file

@ -0,0 +1 @@
npx drizzle-kit push

View file

@ -0,0 +1,81 @@
// app/api/book-survey/route.ts
import { NextResponse } from "next/server";
import { db } from "@/app/db/db";
import { propertyStatusTracker } from "@/app/db/schema/crm/property_status_tracker";
import { eq, and } from "drizzle-orm";
export async function POST(req: Request) {
try {
const { dealName, pipelineId, dealStageId, propertyId, portfolioId } =
await req.json();
// 1⃣ Create HubSpot deal
const hsRes = await fetch("https://api.hubapi.com/crm/v3/objects/deals", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.HUBSPOT_API_KEY}`,
},
body: JSON.stringify({
properties: {
dealname: dealName,
pipeline: pipelineId,
dealstage: dealStageId,
},
}),
});
if (!hsRes.ok) {
const err = await hsRes.text();
throw new Error(`HubSpot error: ${err}`);
}
const hsData = await hsRes.json();
const hubspotDealId = hsData.id;
// 2⃣ Check if record exists for property + portfolio
const existing = await db
.select()
.from(propertyStatusTracker)
.where(
and(
eq(propertyStatusTracker.propertyId, propertyId),
eq(propertyStatusTracker.portfolioId, portfolioId)
)
);
if (existing.length > 0) {
// 3⃣ Update existing record
await db
.update(propertyStatusTracker)
.set({
hubspotDealId,
updatedAt: new Date(),
})
.where(
and(
eq(propertyStatusTracker.propertyId, propertyId),
eq(propertyStatusTracker.portfolioId, portfolioId)
)
);
} else {
// 4⃣ Create new record
await db.insert(propertyStatusTracker).values({
hubspotDealId: hubspotDealId,
propertyId: propertyId,
portfolioId: portfolioId,
});
}
return NextResponse.json({
message: existing.length > 0 ? "Updated existing tracker" : "Created new tracker",
dealId: hubspotDealId,
});
} catch (error: any) {
console.error("❌ Error creating or updating HubSpot deal:", error);
return NextResponse.json(
{ error: error.message || "Internal Server Error" },
{ status: 500 }
);
}
}

View file

@ -0,0 +1 @@
ALTER TABLE "property_status_tracker" DROP COLUMN "hubspot_pipeline_id";

File diff suppressed because it is too large Load diff

View file

@ -841,6 +841,13 @@
"when": 1760711090309,
"tag": "0119_marvelous_blur",
"breakpoints": true
},
{
"idx": 120,
"version": "7",
"when": 1761146299937,
"tag": "0120_flashy_puck",
"breakpoints": true
}
]
}

View file

@ -9,14 +9,14 @@ export const propertyStatusTracker = pgTable("property_status_tracker", {
hubspotDealId: text("hubspot_deal_id").notNull(),
// foreign keys
propertyId: bigint("property_id", { mode: "bigint" })
propertyId: bigint("property_id", { mode: "number" })
.notNull()
.references(() => property.id, { onDelete: "cascade" }),
portfolioId: bigint("portfolio_id", { mode: "bigint" })
portfolioId: bigint("portfolio_id", { mode: "number" })
.notNull()
.references(() => portfolio.id, { onDelete: "cascade" }),
hubspotPipelineId: text("hubspot_pipeline_id").notNull(),
createdAt: timestamp("created_at", { precision: 6, withTimezone: true })
.defaultNow()

View file

@ -11,32 +11,71 @@ import { Button } from "@/app/shadcn_components/ui/button";
import { Input } from "@/app/shadcn_components/ui/input";
import { Label } from "@/app/shadcn_components/ui/label";
import { useState, useEffect } from "react";
import { useMutation } from "@tanstack/react-query";
interface BookSurveyModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
propertyId: bigint;
portfolioId: bigint;
address: string;
}
export default function BookSurveyModal({ open, onOpenChange }: BookSurveyModalProps) {
export default function BookSurveyModal({
open,
onOpenChange,
propertyId,
portfolioId,
address,
}: BookSurveyModalProps) {
const [formData, setFormData] = useState({
deal_name: "",
dealname: "",
});
// 🧠 Simple mutation to call your HubSpot API
const bookSurveyMutation = useMutation({
mutationFn: async (data: typeof formData) => {
const res = await fetch("/api/book-survey", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
dealName: address,
pipelineId: "2400089278",
dealStageId: "3288115388",
propertyId: propertyId.toString(),
portfolioId: portfolioId.toString(),
}),
});
if (!res.ok) throw new Error("Failed to create HubSpot deal");
return res.json();
},
onSuccess: (data) => {
console.log("✅ Deal created successfully:", data);
console.log("HUBSPOT DEAL ID MADE", data.dealId);
onOpenChange(false);
},
onError: (error) => {
console.error("❌ Deal creation failed:", error);
},
});
// ✏️ Handle input
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
};
// 🚀 Submit
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log("Form submitted:", formData);
onOpenChange(false); // ✅ close on submit
bookSurveyMutation.mutate(formData);
};
// ✅ Reset form whenever modal closes
// 🔁 Reset when modal closes
useEffect(() => {
if (!open) {
setFormData({ deal_name: ""});
setFormData({ dealname: ""});
}
}, [open]);
@ -44,25 +83,37 @@ export default function BookSurveyModal({ open, onOpenChange }: BookSurveyModalP
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Book a Survey</DialogTitle>
<DialogTitle>Create HubSpot Deal (Test)</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
{/* Deal Name */}
<div>
<Label htmlFor="name">Name</Label>
<Label htmlFor="dealname">Deal Name</Label>
<Input
id="name"
name="name"
value={formData.deal_name}
id="dealname"
name="dealname"
value={formData.dealname}
onChange={handleChange}
placeholder="Enter your name"
placeholder="Enter deal name"
required
/>
</div>
{/* Error Message */}
{bookSurveyMutation.isError && (
<p className="text-sm text-red-500">
{(bookSurveyMutation.error as Error).message}
</p>
)}
<DialogFooter>
<Button type="submit" className="w-full">
Submit
<Button
type="submit"
className="w-full"
disabled={bookSurveyMutation.isPending}
>
{bookSurveyMutation.isPending ? "Creating..." : "Submit"}
</Button>
</DialogFooter>
</form>
@ -70,12 +121,3 @@ export default function BookSurveyModal({ open, onOpenChange }: BookSurveyModalP
</Dialog>
);
}
// TODO:
// 1) When I press the submit button
// * Call to Next JS backend to upload a deal in hubspot CRM
// * update information in Db
// 2) Show khalim weird dialog button when closing dialog
// 3) Make it functional per individual deal instead of just one
// Push to production after MR with Khalim

View file

@ -265,7 +265,13 @@ export const columns: ColumnDef<PropertyWithRelations>[] = [
</DropdownMenu>
{/* ✅ Render modal outside dropdown context */}
{openModal && (
<BookSurveyModal open={openModal} onOpenChange={setOpenModal} />
<BookSurveyModal
open={openModal}
onOpenChange={setOpenModal}
propertyId={propertyId}
portfolioId={portfolioId}
address={"ask khalim"}
/>
)}
</div>
);