project db addition and live tracker update

This commit is contained in:
Jun-te Kim 2026-05-27 16:27:48 +00:00
parent 5ea7e00fbe
commit 7086a63c1e
8 changed files with 10120 additions and 41 deletions

View file

@ -0,0 +1,10 @@
CREATE TABLE "hubspot_project_data" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"project_id" text NOT NULL,
"name" text,
"created_at" timestamp (6) with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp (6) with time zone DEFAULT now() NOT NULL,
CONSTRAINT "hubspot_project_data_project_id_unique" UNIQUE("project_id")
);
--> statement-breakpoint
ALTER TABLE "hubspot_deal_data" ADD COLUMN "project_id" text;

File diff suppressed because it is too large Load diff

View file

@ -1478,6 +1478,13 @@
"when": 1779889030729,
"tag": "0210_absent_dark_phoenix",
"breakpoints": true
},
{
"idx": 211,
"version": "7",
"when": 1779898075572,
"tag": "0211_lovely_sue_storm",
"breakpoints": true
}
]
}

View file

@ -8,6 +8,7 @@ export const hubspotDealData = pgTable("hubspot_deal_data", {
dealname: text("dealname"),
dealstage: text("dealstage"),
companyId: text("company_id"),
projectId: text("project_id"),
projectCode: text("project_code"),
landlordPropertyId: text("landlord_property_id"),

View file

@ -0,0 +1,21 @@
import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
import { InferModel } from "drizzle-orm";
export const hubspotProjectData = pgTable("hubspot_project_data", {
id: uuid("id").defaultRandom().primaryKey(),
projectId: text("project_id").notNull().unique(),
name: text("name"),
createdAt: timestamp("created_at", { precision: 6, withTimezone: true })
.defaultNow()
.notNull(),
updatedAt: timestamp("updated_at", { precision: 6, withTimezone: true })
.defaultNow()
.$onUpdate(() => new Date())
.notNull(),
});
export type HubspotProjectData = InferModel<typeof hubspotProjectData, "select">;
export type NewHubspotProjectData = InferModel<typeof hubspotProjectData, "insert">;

View file

@ -8,7 +8,7 @@ import SurveyedResultsPieChart from "./SurveyedResultsPieChart";
import DampMouldRiskPanel from "./DampMouldRiskPanel";
import CompletionTrendsChart from "./CompletionTrendsChart";
import SurveyIssuesPanel from "./SurveyIssuesPanel";
import BatchFilter from "./BatchFilter";
import GroupFilter, { type GroupOption } from "./GroupFilter";
import { STAGE_COLORS, STAGE_ORDER } from "./types";
import type {
ProjectData,
@ -313,10 +313,10 @@ interface AnalyticsViewProps {
) => void;
majorConditionDeals: ClassifiedDeal[];
totalDeals: number;
availableBatches: string[];
batchFilter: string[];
onBatchFilterChange: (next: string[]) => void;
batchFilterActive: boolean;
availableGroups: GroupOption[];
groupFilter: string[];
onGroupFilterChange: (next: string[]) => void;
groupFilterActive: boolean;
}
export default function AnalyticsView({
@ -327,18 +327,18 @@ export default function AnalyticsView({
onOpenTable,
majorConditionDeals,
totalDeals,
availableBatches,
batchFilter,
onBatchFilterChange,
batchFilterActive,
availableGroups,
groupFilter,
onGroupFilterChange,
groupFilterActive,
}: AnalyticsViewProps) {
const showBatchFilter = availableBatches.length > 0;
const showGroupFilter = availableGroups.length > 0;
return (
<div className="space-y-6">
{/* Row 1: project selector + (optional) batch filter + properties count */}
{/* Row 1: project selector + (optional) group filter + properties count */}
<div
className={`grid grid-cols-1 gap-4 ${
showBatchFilter ? "sm:grid-cols-3" : "sm:grid-cols-2"
showGroupFilter ? "sm:grid-cols-3" : "sm:grid-cols-2"
}`}
>
{/* Project selector */}
@ -369,19 +369,19 @@ export default function AnalyticsView({
</div>
</Card>
{/* Batch filter — only when current project has batched deals */}
{showBatchFilter && (
<BatchFilter
options={availableBatches}
selected={batchFilter}
onChange={onBatchFilterChange}
{/* Group filter — only when current project has more than one group */}
{showGroupFilter && (
<GroupFilter
options={availableGroups}
selected={groupFilter}
onChange={onGroupFilterChange}
/>
)}
{/* Properties in project (label swaps when batch filter is active) */}
{/* Properties in project (label swaps when group filter is active) */}
<StatCard
icon={Home}
title={batchFilterActive ? "Properties in Group" : "Properties in Project"}
title={groupFilterActive ? "Properties in Group" : "Properties in Project"}
value={currentProject.allDeals.length}
onClick={() =>
onOpenTable(

View file

@ -11,22 +11,26 @@ import {
} from "@/app/shadcn_components/ui/dropdown-menu";
import { Card } from "@/app/shadcn_components/ui/card";
export const UNBATCHED_KEY = "__UNBATCHED__" as const;
export type GroupOption = {
value: string;
label: string;
muted?: boolean;
};
interface BatchFilterProps {
options: string[]; // batch codes present in the current project; may include UNBATCHED_KEY
selected: string[]; // empty array = no filter applied (show everything)
interface GroupFilterProps {
options: GroupOption[];
selected: string[];
onChange: (next: string[]) => void;
variant?: "card" | "inline";
}
function BatchDropdown({
function GroupDropdown({
options,
selected,
onChange,
triggerClassName,
align = "start",
}: BatchFilterProps & {
}: GroupFilterProps & {
triggerClassName: string;
align?: "start" | "end";
}) {
@ -38,12 +42,16 @@ function BatchDropdown({
}
};
const label =
selected.length === 0
const selectedLabels = selected
.map((v) => options.find((o) => o.value === v)?.label)
.filter((l): l is string => l !== undefined);
const triggerLabel =
selectedLabels.length === 0
? "All groups"
: selected
.map((s) => (s === UNBATCHED_KEY ? "(Ungrouped)" : s))
.join(", ");
: selectedLabels.length === 1
? selectedLabels[0]
: `${selectedLabels[0]} +${selectedLabels.length - 1}`;
return (
<DropdownMenu>
@ -51,7 +59,7 @@ function BatchDropdown({
<button type="button" className={triggerClassName}>
<span className="flex items-center gap-2 min-w-0">
<Layers className="h-3.5 w-3.5 shrink-0 text-brandblue" />
<span className="truncate">{label}</span>
<span className="truncate">{triggerLabel}</span>
</span>
<ChevronDown className="h-3.5 w-3.5 shrink-0 text-gray-400" />
</button>
@ -76,16 +84,16 @@ function BatchDropdown({
<div className="max-h-72 overflow-y-auto">
{options.map((opt) => (
<DropdownMenuCheckboxItem
key={opt}
checked={selected.includes(opt)}
onCheckedChange={(val) => toggle(opt, !!val)}
key={opt.value}
checked={selected.includes(opt.value)}
onCheckedChange={(val) => toggle(opt.value, !!val)}
onSelect={(e) => e.preventDefault()}
className="text-sm"
>
{opt === UNBATCHED_KEY ? (
<span className="italic text-gray-500">(Ungrouped)</span>
{opt.muted ? (
<span className="italic text-gray-500">{opt.label}</span>
) : (
opt
opt.label
)}
</DropdownMenuCheckboxItem>
))}
@ -95,12 +103,12 @@ function BatchDropdown({
);
}
export default function BatchFilter(props: BatchFilterProps) {
export default function GroupFilter(props: GroupFilterProps) {
const { variant = "card" } = props;
if (variant === "inline") {
return (
<BatchDropdown
<GroupDropdown
{...props}
triggerClassName="h-9 px-3 pr-2 border border-brandblue/20 rounded-lg bg-white text-gray-800 font-medium text-sm focus:ring-2 focus:ring-brandblue focus:border-brandblue focus:outline-none transition-all flex items-center gap-2 min-w-[160px] max-w-[280px]"
/>
@ -113,7 +121,7 @@ export default function BatchFilter(props: BatchFilterProps) {
<p className="text-xs uppercase tracking-wide text-gray-600 mb-3 font-semibold">
Filter by Group
</p>
<BatchDropdown
<GroupDropdown
{...props}
triggerClassName="w-full px-4 py-2.5 pr-3 border border-brandblue/20 rounded-lg bg-white text-gray-800 font-medium text-sm focus:ring-2 focus:ring-brandblue focus:border-brandblue focus:outline-none transition-all flex items-center justify-between gap-2"
/>