save current progress

This commit is contained in:
Jun-te Kim 2025-11-05 20:09:20 +00:00
parent 16c918a095
commit 8ecc159bf3
5 changed files with 130 additions and 77 deletions

View file

@ -15,6 +15,9 @@ export const hubspotDealData = pgTable("hubspot_deal_data", {
outcome: text("outcome"),
outcomeNotes: text("outcome_notes"),
majorConditionIssueDescription: text("major_condition_issue_description"),
majorConditionIssuePhotos: text("major_condition_issue_photos"),
createdAt: timestamp("created_at", { precision: 6, withTimezone: true })
.defaultNow()
.notNull(),

View file

@ -2,6 +2,8 @@
@tailwind components;
@tailwind utilities;
/* 🌞 Light Theme (raw HSL values) */
:root {
--background: 0 0% 100%;

View file

@ -5,6 +5,8 @@ import MicrosoftSignInButton from "./components/signin/MicrosoftSignInButton";
import EmailSignInButton from "./components/signin/EmailSignInButton";
import { redirect } from "next/navigation";
import Image from "next/image";
import "@tremor/react/dist/esm/tremor.css";
export default async function Home(props: {
searchParams: Promise<{ error?: string }>;

View file

@ -7,11 +7,11 @@ const STAGE_ORDER = [
"Initial planning",
"Booking team to contact tenant",
"Survey in progress",
"Not viable",
"Needs Heating Upgrade installed",
"Needs support",
"Coordination + design",
"Ready to be installed",
"Ready for install",
"Installed",
"Needs support from HA",
"Not viable for funding",
];
const stage = (label: string) => STAGE_ORDER.find((s) => s === label)!;
@ -26,8 +26,8 @@ const STAGE_LABELS: Record<string, string> = {
"1984184569": stage("Booking team to contact tenant"),
"3569572028": stage("Booking team to contact tenant"),
"3570936026": stage("Booking team to contact tenant"),
"2663668937": stage("Booking team to contact tenant"),
"1984401629": stage("Booking team to contact tenant"),
"2663668937": stage("Needs support from HA"),
"1984401629": stage("Survey in progress"),
"2558220518": stage("Booking team to contact tenant"),
"3474594026": stage("Booking team to contact tenant"),
@ -35,27 +35,25 @@ const STAGE_LABELS: Record<string, string> = {
"1617223913": stage("Survey in progress"),
"3206388924": stage("Survey in progress"),
"1617223915": stage("Survey in progress"),
"1617223917": stage("Survey in progress"),
"2571417798": stage("Survey in progress"),
"1887736000": stage("Survey in progress"),
"1617223916": stage("Survey in progress"),
"2628341989": stage("Survey in progress"),
"3441170637": stage("Survey in progress"),
"1617223917": stage("Not viable for funding"),
"2571417798": stage("Booking team to contact tenant"),
"1887735998": stage("Not viable"),
"1617223916": stage("Coordination + design"),
"2628341989": stage("Coordination + design"),
"3441170637": stage("Coordination + design"),
"3061261536": stage("Needs Heating Upgrade installed"),
"1887735999": stage("Needs Heating Upgrade installed"),
"3016601828": stage("Needs Heating Upgrade installed"),
"1887735998": stage("Not viable for funding"),
"3061261536": stage("Needs support from HA"),
"1887735999": stage("Needs support from HA"),
"3016601828": stage("Needs support from HA"),
"1617223914": stage("Coordination + design"),
"2628233422": stage("Coordination + design"),
"2702650617": stage("Coordination + design"),
"2473886962": stage("Coordination + design"),
"1617223914": stage("Needs support"),
"2628233422": stage("Needs support"),
"2702650617": stage("Needs support"),
"2473886962": stage("Needs support"),
"1668803774": stage("Coordination + design"),
"3440363736": stage("Coordination + design"),
"2769407183": stage("Needs Heating Upgrade installed"),
"1668803774": stage("Ready for install"),
"3440363736": stage("Ready for install"),
"2769407183": stage("Needs support from HA"),
};
interface DealStageChartProps {
@ -67,31 +65,18 @@ export function DealStageChart({ deals, onOpenTable }: DealStageChartProps) {
const data = useMemo(() => {
const counts: Record<string, number> = {};
// Count deals by stage
deals.forEach((d) => {
const stageId = d.dealstage || "unknown";
const stageName = STAGE_LABELS[stageId] || "Unknown Stage";
counts[stageName] = (counts[stageName] || 0) + 1;
});
// Ensure every stage in STAGE_ORDER has a default of 0
const complete = STAGE_ORDER.map((name) => ({
return STAGE_ORDER.map((name) => ({
name,
value: counts[name] || 0,
}));
// Sort according to STAGE_ORDER (just in case)
return complete.sort((a, b) => {
const aIndex = STAGE_ORDER.indexOf(a.name);
const bIndex = STAGE_ORDER.indexOf(b.name);
return (
(aIndex === -1 ? Number.MAX_SAFE_INTEGER : aIndex) -
(bIndex === -1 ? Number.MAX_SAFE_INTEGER : bIndex)
);
});
}, [deals]);
// ✅ Calculate total deals
const total = useMemo(() => deals.length, [deals]);
const handleBarClick = (value: { name: string; value: number }) => {
@ -103,31 +88,61 @@ export function DealStageChart({ deals, onOpenTable }: DealStageChartProps) {
onOpenTable?.(value.name, filtered);
};
// ✅ Split into normal and exception stages
const normalStages = data.filter(
(d) =>
!["Needs support from HA", "Not viable for funding"].includes(d.name) &&
d.name &&
d.name !== ""
);
const exceptionStages = data.filter((d) =>
["Needs support from HA", "Not viable for funding"].includes(d.name)
);
return (
<Card className="max-w-lg mx-auto bg-white rounded-2xl shadow-md hover:shadow-lg transition-all duration-200 p-6">
<div className="flex flex-col items-center mb-4">
<Title className="text-gray-800 text-lg font-semibold tracking-tight text-center">
Project Progress by Stage
</Title>
<p className="text-sm text-gray-500 text-center mt-1">
Click a bar to view related properties
</p>
<div className="flex flex-col gap-3"> {/* Reduced gap */}
{/* Main Progress Chart */}
<Card className="bg-white rounded-xl shadow-sm hover:shadow-md transition-all duration-200 p-4">
<div className="flex flex-col items-center mb-2">
<Title className="text-gray-800 text-base font-semibold text-center">
Project Progress by Stage
</Title>
<p className="text-xs text-gray-500 text-center mt-0.5">
Click a bar to view related properties
</p>
<p className="text-xs text-gray-700 font-medium mt-1">
Total: {total.toLocaleString()} properties
</p>
</div>
{/* ✅ Total count */}
<p className="text-sm text-gray-700 font-medium mt-2">
Total: {total.toLocaleString()} properties
</p>
</div>
<div className="w-full">
<BarList
data={data}
data={normalStages}
color="blue"
sortOrder="none"
className="cursor-pointer"
onValueChange={handleBarClick}
/>
</div>
</Card>
</Card>
{/* Exception Chart */}
<Card className="bg-white rounded-xl shadow-sm hover:shadow-md transition-all duration-200 p-4">
<div className="flex flex-col items-center mb-2">
<Title className="text-gray-800 text-base font-semibold text-center">
Needs HA Support & Not Viable
</Title>
<p className="text-xs text-gray-500 text-center mt-0.5">
Click to explore exceptions
</p>
</div>
<BarList
data={exceptionStages}
color="red"
sortOrder="none"
className="cursor-pointer"
onValueChange={handleBarClick}
/>
</Card>
</div>
);
}

View file

@ -10,13 +10,12 @@ import {
CardTitle,
CardContent,
} from "@/app/shadcn_components/ui/card";
import { Home, AlertTriangle, BarChart3 } from "lucide-react";
import { Home, AlertTriangle } from "lucide-react";
import { motion } from "framer-motion";
interface ReportsProps {
deals: Record<string, any>[];
}
const MAJOR_CONDITION_STAGE_ID = "3061261536";
export default function LiveTracker({ deals }: ReportsProps) {
@ -32,7 +31,10 @@ export default function LiveTracker({ deals }: ReportsProps) {
const [openTable, setOpenTable] = useState<{
stage: string;
data: any[];
columns: string[];
columnLabels: Record<string, string>;
} | null>(null);
const projectCodes = Object.keys(groupedDeals);
const [currentProjectCode, setCurrentProjectCode] = useState(projectCodes[0]);
const currentDeals = groupedDeals[currentProjectCode];
@ -43,8 +45,23 @@ export default function LiveTracker({ deals }: ReportsProps) {
const majorIssues = majorConditionDeals.length;
const majorPercent = ((majorIssues / totalProperties) * 100).toFixed(1);
const handleOpenTable = (stage: string, filteredDeals: any[]) => {
setOpenTable({ stage, data: filteredDeals });
const handleOpenTable = (
stage: string,
filteredDeals: any[],
columns?: string[],
columnLabels?: Record<string, string>
) => {
setOpenTable({
stage,
data: filteredDeals,
columns:
columns || ["dealname", "landlordPropertyId"],
columnLabels:
columnLabels || {
dealname: "Address Ref.",
landlordPropertyId: "Property Ref.",
},
});
};
if (!deals?.length) {
@ -66,18 +83,42 @@ export default function LiveTracker({ deals }: ReportsProps) {
icon={Home}
title="Total Properties"
value={totalProperties}
onClick={() => handleOpenTable("All Properties", deals)}
onClick={() =>
handleOpenTable(
"All Properties",
deals,
["dealname", "landlordPropertyId", "projectCode"],
{
dealname: "Address Ref.",
landlordPropertyId: "Property Ref.",
projectCode: "Project Code",
}
)
}
accent="brandblue"
/>
{/* Major Issues */}
<StatCard
icon={AlertTriangle}
title="Major Condition Issues"
value={`${majorIssues} `}
title="Awaab's Law Reporting"
value={`${majorIssues}`}
subtitle={`(${majorPercent}%)`}
onClick={() =>
handleOpenTable("Major Condition Issues", majorConditionDeals)
handleOpenTable(
"Awaab's Law Reporting",
majorConditionDeals,
[
"dealname",
"landlordPropertyId",
"majorConditionIssueDescription",
],
{
dealname: "Address Ref.",
landlordPropertyId: "Property Ref.",
majorConditionIssueDescription: "Surveyor's Notes"
}
)
}
accent="red"
/>
@ -155,18 +196,8 @@ export default function LiveTracker({ deals }: ReportsProps) {
<div className="flex-1 overflow-auto">
<TableViewer
data={openTable.data}
columns={[
"dealname",
"landlordPropertyId",
"outcome",
"outcomeNotes",
]}
columnLabels={{
dealname: "Address Ref.",
landlordPropertyId: "Property Ref.",
outcome: "Outcome",
outcomeNotes: "Notes from Surveyor",
}}
columns={openTable.columns}
columnLabels={openTable.columnLabels}
/>
</div>
@ -185,7 +216,7 @@ export default function LiveTracker({ deals }: ReportsProps) {
);
}
/** 🔸Small stat card to match DashboardSummary visuals */
/** 🔸Small stat card component */
function StatCard({
icon: Icon,
title,