mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-30 12:55:02 +00:00
added files for reports
This commit is contained in:
parent
8cdb1ad8bc
commit
8325cb6206
13 changed files with 513 additions and 5 deletions
2
.db-env
Normal file
2
.db-env
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
PGADMIN_DEFAULT_EMAIL=junte@domna.homes
|
||||
PGADMIN_DEFAULT_PASSWORD=makingwarmhomes
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
version: '3.8'
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
frontend:
|
||||
|
|
@ -14,6 +14,17 @@ services:
|
|||
networks:
|
||||
- frontend-net
|
||||
|
||||
pgadmin:
|
||||
image: dpage/pgadmin4
|
||||
hostname: pgadmin
|
||||
ports:
|
||||
- 5556:80
|
||||
env_file:
|
||||
- ../.db-env
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- frontend-net
|
||||
|
||||
networks:
|
||||
frontend-net:
|
||||
driver: bridge
|
||||
|
|
|
|||
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -30,7 +30,7 @@
|
|||
"@remixicon/react": "^4.2.0",
|
||||
"@tanstack/react-query": "^4.29.12",
|
||||
"@tanstack/react-table": "^8.9.3",
|
||||
"@tremor/react": "^3.16.0",
|
||||
"@tremor/react": "^3.18.7",
|
||||
"@types/node": "20.2.3",
|
||||
"@types/react": "18.3.1",
|
||||
"@types/react-dom": "18.3.1",
|
||||
|
|
@ -64,7 +64,7 @@
|
|||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"@testing-library/cypress": "^10.0.3",
|
||||
"@types/nodemailer": "^7.0.2",
|
||||
"@types/pg": "^8.10.2",
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
"@remixicon/react": "^4.2.0",
|
||||
"@tanstack/react-query": "^4.29.12",
|
||||
"@tanstack/react-table": "^8.9.3",
|
||||
"@tremor/react": "^3.16.0",
|
||||
"@tremor/react": "^3.18.7",
|
||||
"@types/node": "20.2.3",
|
||||
"@types/react": "18.3.1",
|
||||
"@types/react-dom": "18.3.1",
|
||||
|
|
@ -70,7 +70,7 @@
|
|||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"@testing-library/cypress": "^10.0.3",
|
||||
"@types/nodemailer": "^7.0.2",
|
||||
"@types/pg": "^8.10.2",
|
||||
|
|
|
|||
18
src/app/db/schema/crm/hubspot_company_table.ts
Normal file
18
src/app/db/schema/crm/hubspot_company_table.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { pgTable, uuid, text, timestamp } from "drizzle-orm/pg-core";
|
||||
|
||||
export const hubspotCompanyData = pgTable("hubspot_company_data", {
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
|
||||
companyId: text("company_id").notNull(),
|
||||
companyName: text("company_name"),
|
||||
groupId: text("group_id"),
|
||||
|
||||
createdAt: timestamp("created_at", { precision: 6, withTimezone: true })
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
|
||||
updatedAt: timestamp("updated_at", { precision: 6, withTimezone: true })
|
||||
.defaultNow()
|
||||
.$onUpdate(() => new Date())
|
||||
.notNull(),
|
||||
});
|
||||
27
src/app/db/schema/crm/hubspot_deal_table.ts
Normal file
27
src/app/db/schema/crm/hubspot_deal_table.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { pgTable, uuid, text, timestamp } from "drizzle-orm/pg-core";
|
||||
import { InferModel } from "drizzle-orm";
|
||||
|
||||
export const hubspotDealData = pgTable("hubspot_deal_data", {
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
|
||||
dealId: text("deal_id").notNull(),
|
||||
dealname: text("dealname"),
|
||||
dealstage: text("dealstage"),
|
||||
companyId: text("company_id"),
|
||||
projectCode: text("project_code"),
|
||||
|
||||
landlordPropertyId: text("landlord_property_id"),
|
||||
uprn: text("uprn"),
|
||||
outcome: text("outcome"),
|
||||
outcomeNotes: text("outcome_notes"),
|
||||
|
||||
createdAt: timestamp("created_at", { precision: 6, withTimezone: true })
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
|
||||
updatedAt: timestamp("updated_at", { precision: 6, withTimezone: true })
|
||||
.defaultNow()
|
||||
.$onUpdate(() => new Date())
|
||||
.notNull(),
|
||||
});
|
||||
|
||||
122
src/app/portfolio/[slug]/(portfolio)/reports/DealStageChart.tsx
Normal file
122
src/app/portfolio/[slug]/(portfolio)/reports/DealStageChart.tsx
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useMemo } from "react";
|
||||
import { BarList, Card, Title } from "@tremor/react";
|
||||
import TableViewer from "./TableViewer";
|
||||
|
||||
const STAGE_ORDER = [
|
||||
"Initial Planning", // 0
|
||||
"Booking Team to contact Tenant", // 1
|
||||
"Survey in Progress", // 2
|
||||
"Not viable", // 3
|
||||
"Needs HA Support", // 4
|
||||
"Coordination + Design", // 5
|
||||
"Ready to be installed" //7
|
||||
];
|
||||
|
||||
const STAGE_LABELS: Record<string, string> = {
|
||||
"1617223910": STAGE_ORDER[0],
|
||||
"3583836399": STAGE_ORDER[0],
|
||||
"3589581001": STAGE_ORDER[1],
|
||||
"3569878239": STAGE_ORDER[1],
|
||||
"1617223911": STAGE_ORDER[1],
|
||||
"1984184569": STAGE_ORDER[1],
|
||||
"3569572028": STAGE_ORDER[1],
|
||||
"3570936026": STAGE_ORDER[1],
|
||||
"2663668937": STAGE_ORDER[1],
|
||||
"1984401629": STAGE_ORDER[1],
|
||||
"1617223912": STAGE_ORDER[2],
|
||||
"1617223913": STAGE_ORDER[2],
|
||||
"2558220518": STAGE_ORDER[1],
|
||||
"3474594026": STAGE_ORDER[1],
|
||||
"3206388924": STAGE_ORDER[2],
|
||||
"1617223915": STAGE_ORDER[2],
|
||||
"1617223917": STAGE_ORDER[2],
|
||||
"1887735998": STAGE_ORDER[3],
|
||||
"3061261536": STAGE_ORDER[4],
|
||||
"2571417798": "[Ops] Surveyed under 2019 - Needs Re-survey",
|
||||
"1617223914": STAGE_ORDER[5],
|
||||
"1887736000": "[Deprecated, please don't use] Files Missing From Assessor",
|
||||
"1617223916": "[Ops] Properties to Review Manually",
|
||||
"2628341989": STAGE_ORDER[2],
|
||||
"3441170637": STAGE_ORDER[2], // check if assessor or coordination
|
||||
"2628233422": STAGE_ORDER[5],
|
||||
"1887735999": STAGE_ORDER[4],
|
||||
"1960060104": "[Ops] HA Informed",
|
||||
"1960060105": "[Ops] HA Works Scheduled",
|
||||
"1960060106": "[Ops] HA Works Complete",
|
||||
"1668803772": "[Ops] ERF Delivered to HA",
|
||||
"1668803773": "[Ops] ERF Signed",
|
||||
"2769407183": "[Ops] PV - Needs Heating Upgrade (Pre EPR D)",
|
||||
"2769407184": "[Ops] Talk to client, Needs Heating Upgrade (Pre EPR C)",
|
||||
"2702650617": STAGE_ORDER[5],
|
||||
"2473886962": STAGE_ORDER[5],
|
||||
"3016601828": STAGE_ORDER[4],
|
||||
"3389868276": "[Engagement Team] Blocked - Needs Completion of Pilot",
|
||||
"3389880508": "[Engagement Team] Blocked - Installer Negotiation",
|
||||
"3399016689": "[Engagement Team] Eligible but blocked - part of incomplete flat",
|
||||
"1668803774": STAGE_ORDER[6], // Ready for Invoicing
|
||||
"3440363736": STAGE_ORDER[6], // [Finance] Needs Invoicing - Files Sent
|
||||
"1618526429": "[Ops] Invoiced - Send Files to Installer",
|
||||
"3080225005": "[Ops] Files Sent to Installer",
|
||||
"1961258215": "[Ops] Installer Cancelled - Finalized",
|
||||
"1961258214": "[Ops] Installer Cancelled - In Progress",
|
||||
"1961258213": "[Ops] Install Scheduled",
|
||||
"1617223918": "[Ops] Install Complete",
|
||||
"1961258216": "[Compliance] Lodgement Complete",
|
||||
"1961258217": "[Compliance] Documentation Sent to HA",
|
||||
"3027432668": "[Team ???] Submitted to "
|
||||
}
|
||||
|
||||
interface DealStageChartProps {
|
||||
deals: any[];
|
||||
onOpenTable?: (stageName: string, filteredDeals: any[]) => void;
|
||||
}
|
||||
|
||||
export function DealStageChart({ deals, onOpenTable }: DealStageChartProps) {
|
||||
const data = useMemo(() => {
|
||||
const counts: Record<string, number> = {};
|
||||
deals.forEach((d) => {
|
||||
const stageId = d.dealstage || "unknown";
|
||||
const stageName = STAGE_LABELS[stageId] || "Unknown Stage";
|
||||
counts[stageName] = (counts[stageName] || 0) + 1;
|
||||
});
|
||||
|
||||
const unsorted = Object.entries(counts).map(([name, value]) => ({
|
||||
name,
|
||||
value,
|
||||
}));
|
||||
|
||||
return unsorted.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]);
|
||||
|
||||
// handle click event
|
||||
const handleBarClick = (value: { name: string; value: number }) => {
|
||||
const filtered = deals.filter((d) => {
|
||||
const stageId = d.dealstage || "unknown";
|
||||
const stageName = STAGE_LABELS[stageId] || "Unknown Stage";
|
||||
return stageName === value.name;
|
||||
});
|
||||
|
||||
onOpenTable?.(value.name, filtered);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="max-w-lg mx-auto">
|
||||
<Title>Project Progress</Title>
|
||||
<BarList
|
||||
data={data}
|
||||
color="blue"
|
||||
sortOrder="none"
|
||||
onValueChange={handleBarClick}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
123
src/app/portfolio/[slug]/(portfolio)/reports/Report.tsx
Normal file
123
src/app/portfolio/[slug]/(portfolio)/reports/Report.tsx
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { DealStageChart } from "./DealStageChart";
|
||||
import SurveyedPieChart from "./SurveyedResultsPieChart";
|
||||
import TableViewer from "./TableViewer";
|
||||
|
||||
interface ReportsProps {
|
||||
deals: Record<string, any>[];
|
||||
}
|
||||
|
||||
export default function Reports({ deals }: ReportsProps) {
|
||||
const [openTable, setOpenTable] = useState<{
|
||||
stage: string;
|
||||
data: any[];
|
||||
} | null>(null);
|
||||
|
||||
const handleOpenTable = (stage: string, filteredDeals: any[]) => {
|
||||
setOpenTable({ stage, data: filteredDeals });
|
||||
};
|
||||
|
||||
if (!deals || deals.length === 0) {
|
||||
return (
|
||||
<div className="p-6 text-center text-gray-500 border rounded-xl shadow-sm">
|
||||
No deal data available.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Group deals by projectCode
|
||||
const groupedDeals = deals.reduce((acc, deal) => {
|
||||
const project = deal.projectCode || "Unknown Project";
|
||||
if (!acc[project]) acc[project] = [];
|
||||
acc[project].push(deal);
|
||||
return acc;
|
||||
}, {} as Record<string, any[]>);
|
||||
|
||||
const projectCodes = Object.keys(groupedDeals);
|
||||
const [currentProjectCode, setCurrentProjectCode] = useState(projectCodes[0]);
|
||||
const currentDeals = groupedDeals[currentProjectCode];
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
{/* 🔹 Centered Dropdown Selector for Projects */}
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<h2 className="text-xl font-semibold text-gray-800">
|
||||
Select Project
|
||||
</h2>
|
||||
|
||||
<div className="relative w-full max-w-xs">
|
||||
<select
|
||||
value={currentProjectCode}
|
||||
onChange={(e) => setCurrentProjectCode(e.target.value)}
|
||||
className="w-full appearance-none px-4 py-2 pr-10 border border-gray-300 rounded-lg bg-white text-center text-gray-800 shadow-sm focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
>
|
||||
{projectCodes.map((code) => (
|
||||
<option key={code} value={code}>
|
||||
{code}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
{/* Custom dropdown arrow */}
|
||||
<div className="pointer-events-none absolute inset-y-0 right-3 flex items-center text-gray-500">
|
||||
▼
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Charts */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="border rounded-xl p-4 shadow-sm bg-white">
|
||||
<DealStageChart deals={currentDeals} onOpenTable={handleOpenTable} />
|
||||
</div>
|
||||
<div className="border rounded-xl p-4 shadow-sm bg-white">
|
||||
<SurveyedPieChart deals={currentDeals} onOpenTable={handleOpenTable} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center text-gray-500 text-sm">
|
||||
Showing project <span className="font-medium">{currentProjectCode}</span>
|
||||
</div>
|
||||
|
||||
{/* 🔹 Modal Table */}
|
||||
{openTable && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
||||
<div className="bg-white rounded-xl shadow-2xl p-6 w-full max-w-5xl h-[90vh] flex flex-col">
|
||||
<h2 className="text-2xl font-semibold mb-4 text-center">
|
||||
{openTable.stage} — {openTable.data.length} Properties
|
||||
</h2>
|
||||
|
||||
<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",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex justify-center">
|
||||
<button
|
||||
onClick={() => setOpenTable(null)}
|
||||
className="px-6 py-2 bg-gray-200 hover:bg-gray-300 rounded-lg transition"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
"use client";
|
||||
|
||||
import { DonutChart, Card, Title } from "@tremor/react";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
interface SurveyedPieChartProps {
|
||||
deals: Record<string, any>[];
|
||||
onOpenTable?: (outcome: string, filteredDeals: Record<string, any>[]) => void;
|
||||
}
|
||||
|
||||
export default function SurveyedPieChart({
|
||||
deals,
|
||||
onOpenTable,
|
||||
}: SurveyedPieChartProps) {
|
||||
const [selected, setSelected] = useState<string | null>(null);
|
||||
|
||||
const surveyorOutcomes = [
|
||||
"Surveyed",
|
||||
"Surveyed - Pending Upload",
|
||||
"Tenant Refusal",
|
||||
"Other",
|
||||
"Not Viable",
|
||||
"Not Attempted",
|
||||
"No Answer",
|
||||
"Cancelled / No Show",
|
||||
"Rescheduled",
|
||||
];
|
||||
|
||||
const data = useMemo(() => {
|
||||
const outcomeCounts: Record<string, number> = {};
|
||||
|
||||
deals.forEach((deal) => {
|
||||
const outcome = deal.outcome;
|
||||
if (outcome && surveyorOutcomes.includes(outcome)) {
|
||||
outcomeCounts[outcome] = (outcomeCounts[outcome] || 0) + 1;
|
||||
}
|
||||
});
|
||||
|
||||
return Object.entries(outcomeCounts).map(([name, amount]) => ({
|
||||
name,
|
||||
amount,
|
||||
}));
|
||||
}, [deals]);
|
||||
|
||||
const handleClick = (value: { name: string; amount: number }) => {
|
||||
if (!value) return; // guard clause
|
||||
const filteredDeals = deals.filter((d) => d.outcome === value.name);
|
||||
setSelected(null); // remove highlight after click
|
||||
onOpenTable?.(value.name, filteredDeals);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="max-w-lg mx-auto">
|
||||
<div className="flex flex-col items-center space-y-6">
|
||||
<Title className="text-center text-lg font-semibold">
|
||||
Surveyed Outcome
|
||||
</Title>
|
||||
|
||||
<DonutChart
|
||||
data={data}
|
||||
category="amount"
|
||||
index="name"
|
||||
valueFormatter={(n) => `${n.toLocaleString()}`}
|
||||
colors={[
|
||||
"indigo",
|
||||
"cyan",
|
||||
"emerald",
|
||||
"amber",
|
||||
"rose",
|
||||
"violet",
|
||||
"gray",
|
||||
]}
|
||||
className="w-64 h-64 cursor-pointer"
|
||||
onValueChange={handleClick}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
79
src/app/portfolio/[slug]/(portfolio)/reports/TableViewer.tsx
Normal file
79
src/app/portfolio/[slug]/(portfolio)/reports/TableViewer.tsx
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useMemo } from "react";
|
||||
|
||||
interface TableViewerProps {
|
||||
data: Record<string, any>[];
|
||||
columns?: string[]; // optional: which columns to show
|
||||
columnLabels?: Record<string, string>; // 👈 map data keys to display names
|
||||
}
|
||||
|
||||
export default function TableViewer({ data, columns, columnLabels }: TableViewerProps) {
|
||||
const [searchTerms, setSearchTerms] = useState<Record<string, string>>({});
|
||||
|
||||
const visibleColumns = columns?.length ? columns : Object.keys(data?.[0] || {});
|
||||
|
||||
const filteredData = useMemo(() => {
|
||||
return data.filter((row) =>
|
||||
visibleColumns.every((col) => {
|
||||
const term = searchTerms[col]?.toLowerCase() || "";
|
||||
if (!term) return true;
|
||||
const value = String(row[col] ?? "").toLowerCase();
|
||||
return value.includes(term);
|
||||
})
|
||||
);
|
||||
}, [data, searchTerms, visibleColumns]);
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto border rounded-xl shadow-md p-4">
|
||||
<table className="min-w-full text-sm border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-gray-100">
|
||||
{visibleColumns.map((col) => (
|
||||
<th key={col} className="border p-2 text-left">
|
||||
<div className="flex flex-col">
|
||||
<span className="font-semibold capitalize">
|
||||
{columnLabels?.[col] || col}
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={`Search ${columnLabels?.[col] || col}`}
|
||||
className="mt-1 p-1 border rounded text-xs"
|
||||
onChange={(e) =>
|
||||
setSearchTerms((prev) => ({
|
||||
...prev,
|
||||
[col]: e.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredData.length === 0 ? (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={visibleColumns.length}
|
||||
className="text-center p-4 text-gray-400"
|
||||
>
|
||||
No results found
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
filteredData.map((row, i) => (
|
||||
<tr key={i} className="odd:bg-white even:bg-gray-50">
|
||||
{visibleColumns.map((col) => (
|
||||
<td key={col} className="border p-2">
|
||||
{String(row[col] ?? "")}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
34
src/app/portfolio/[slug]/(portfolio)/reports/page.tsx
Normal file
34
src/app/portfolio/[slug]/(portfolio)/reports/page.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { getServerSession } from "next-auth";
|
||||
import { AuthOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { redirect } from "next/navigation";
|
||||
import { surveyDB } from "../../../../db/surveyDB/connection";
|
||||
import { hubspotDealData } from "../../../../db/schema/crm/hubspot_deal_table";
|
||||
import { eq } from "drizzle-orm";
|
||||
import Reports from "./Report";
|
||||
|
||||
const Demo = async () => {
|
||||
const user = await getServerSession(AuthOptions);
|
||||
|
||||
if (!user?.user) {
|
||||
console.error("User not found");
|
||||
redirect("/");
|
||||
}
|
||||
|
||||
// Abri company id
|
||||
const companyId = "237615001799";
|
||||
|
||||
const deals = await surveyDB
|
||||
.select()
|
||||
.from(hubspotDealData)
|
||||
.where(eq(hubspotDealData.companyId, companyId));
|
||||
console.log(deals);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1> hello reports</h1>
|
||||
{/* <Reports deals={deals} /> */}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Demo;
|
||||
|
|
@ -2,6 +2,8 @@ import { Toolbar } from "@/app/components/building-passport/Toolbar";
|
|||
import { getPropertyMeta, getDocument } from "./utils";
|
||||
import BackToPortfolioButton from "@/app/components/building-passport/BackToPortfolioButton";
|
||||
import { ExclamationCircleIcon } from "@heroicons/react/24/outline";
|
||||
// import "@tremor/react/dist/esm/tremor.css";
|
||||
|
||||
|
||||
function EstimatedDataNotification() {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -4,3 +4,14 @@ import { twMerge } from "tailwind-merge"
|
|||
export function cn (...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
export const focusRing = [
|
||||
// base
|
||||
"outline outline-offset-2 outline-0 focus-visible:outline-2",
|
||||
// outline color
|
||||
"outline-blue-500 dark:outline-blue-500",
|
||||
]
|
||||
|
||||
export function cx(...args: ClassValue[]) {
|
||||
return twMerge(clsx(...args))
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue