mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-08 11:37:25 +00:00
updated PIBI ui
This commit is contained in:
parent
2df592d1ac
commit
2fc87d4dbf
3 changed files with 702 additions and 669 deletions
|
|
@ -1,20 +1,24 @@
|
|||
/**
|
||||
* Live Tracking — PibiSection (replaces pibi-dates.cy.js + pibi-measures.cy.js)
|
||||
* Live Tracking — PibiSection (flat TanStack Table redesign)
|
||||
*
|
||||
* Tests the approver flow for the new per-measure PIBI request log:
|
||||
* 1. Empty state renders with a "Log first PIBI" prompt
|
||||
* 2. Approver can open the log form, pick measures + date, and submit
|
||||
* 3. Submitted batch appears as a group with orderedAt header
|
||||
* 4. Approver can mark a row complete / undo it
|
||||
* 5. Approver can delete a row
|
||||
* Tests the approver flow for the per-measure PIBI request log rendered
|
||||
* directly on the DealPage (pibi-surveys tab), not in a drawer.
|
||||
*
|
||||
* Requires LIVE_PORTFOLIO_SLUG env var; skipped otherwise.
|
||||
* All network calls are intercepted so no real DB / HubSpot round-trips occur.
|
||||
* 1. Empty state renders with a "Log first PIBI" prompt
|
||||
* 2. Flat table shows existing rows (no batch grouping)
|
||||
* 3. Every row has always-editable cells (measure select, date inputs)
|
||||
* 4. Save button is disabled when row is clean, enabled after editing
|
||||
* 5. Save calls PATCH for existing rows
|
||||
* 6. Delete calls DELETE
|
||||
* 7. "+ Add row" appends a blank row; save calls POST
|
||||
* 8. "Mark all complete" PATCHes all incomplete rows
|
||||
* 9. Scope badges appear for approved / proposed measures
|
||||
*
|
||||
* Requires LIVE_PORTFOLIO_SLUG; skipped otherwise.
|
||||
* All network calls are intercepted — no real DB / HubSpot round-trips.
|
||||
*/
|
||||
|
||||
const PORTFOLIO_SLUG = Cypress.env("LIVE_PORTFOLIO_SLUG");
|
||||
const TARGET_DEAL_NAME = Cypress.env("LIVE_PIBI_DEAL_NAME");
|
||||
|
||||
const PORTFOLIO_ID_GLOB = "*";
|
||||
|
||||
function stubGet(pibiRequests = []) {
|
||||
|
|
@ -25,15 +29,18 @@ function stubGet(pibiRequests = []) {
|
|||
).as("getPibiRequests");
|
||||
}
|
||||
|
||||
function stubMeasures(approvedMeasures = ["ASHP", "CWI"], instructedMeasures = []) {
|
||||
function stubMeasures(
|
||||
approvedMeasures = ["ASHP", "CWI"],
|
||||
instructedMeasures = [],
|
||||
) {
|
||||
cy.intercept(
|
||||
"GET",
|
||||
`/api/portfolio/${PORTFOLIO_ID_GLOB}/pibi-measures*`,
|
||||
{ body: { approvedMeasures, instructedMeasures } },
|
||||
{ body: { pibiMeasures: approvedMeasures, approvedMeasures, instructedMeasures } },
|
||||
).as("getPibiMeasures");
|
||||
}
|
||||
|
||||
function stubPost(response = { ok: true, insertedCount: 2, hubspotSync: "ok" }) {
|
||||
function stubPost(response = { ok: true, insertedCount: 1, hubspotSync: "ok" }) {
|
||||
cy.intercept(
|
||||
"POST",
|
||||
`/api/portfolio/${PORTFOLIO_ID_GLOB}/pibi-requests`,
|
||||
|
|
@ -57,19 +64,11 @@ function stubDelete(id, response = { ok: true, hubspotSync: "ok" }) {
|
|||
).as(`deletePibiRequest-${id}`);
|
||||
}
|
||||
|
||||
function openDrawerAtPibiSection() {
|
||||
function openDealPageAtPibiTab() {
|
||||
cy.visit(`/portfolio/${PORTFOLIO_SLUG}/your-projects/live`);
|
||||
cy.contains("button, [role=tab]", "Measures").click();
|
||||
|
||||
if (TARGET_DEAL_NAME) {
|
||||
cy.contains("[data-testid=measures-row]", TARGET_DEAL_NAME).click();
|
||||
} else {
|
||||
cy.get("[data-testid=measures-row]").first().click();
|
||||
}
|
||||
|
||||
cy.get("[data-testid=property-detail-drawer]").should("be.visible");
|
||||
cy.get("[data-testid=drawer-tab-pibi-surveys]").click();
|
||||
cy.get("[data-testid=drawer-tab-panel-pibi-surveys]").should("be.visible");
|
||||
cy.contains("button, [role=tab]", "Properties").click();
|
||||
cy.get("[data-testid=property-row-link]").first().click();
|
||||
cy.get("[data-testid=deal-page-tab-pibi-surveys]").click();
|
||||
}
|
||||
|
||||
describe("PibiSection", function () {
|
||||
|
|
@ -84,145 +83,204 @@ describe("PibiSection", function () {
|
|||
stubMeasures();
|
||||
});
|
||||
|
||||
// ── Empty state ──────────────────────────────────────────────────────────────
|
||||
// ── Cycle 1: Empty state ──────────────────────────────────────────────────
|
||||
|
||||
it("shows empty state with Log first PIBI prompt when no requests exist", () => {
|
||||
stubGet([]);
|
||||
openDrawerAtPibiSection();
|
||||
openDealPageAtPibiTab();
|
||||
cy.wait("@getPibiRequests");
|
||||
cy.get("[data-testid=pibi-empty-state]").should("be.visible");
|
||||
cy.get("[data-testid=pibi-empty-state]").should("contain.text", "No PIBIs logged yet");
|
||||
cy.get("[data-testid=pibi-empty-state]").should(
|
||||
"contain.text",
|
||||
"No PIBIs logged yet",
|
||||
);
|
||||
cy.get("[data-testid=pibi-empty-add-row]").should("be.visible");
|
||||
});
|
||||
|
||||
// ── Log form ─────────────────────────────────────────────────────────────────
|
||||
// ── Cycle 2: Flat table renders rows (no batch groups) ────────────────────
|
||||
|
||||
it("opens log form when approver clicks Log PIBI button", () => {
|
||||
stubGet([]);
|
||||
openDrawerAtPibiSection();
|
||||
cy.wait("@getPibiRequests");
|
||||
|
||||
cy.get("[data-testid=log-pibi-button]").click();
|
||||
cy.get("[data-testid=pibi-log-form]").should("be.visible");
|
||||
cy.get("[data-testid=pibi-order-date-input]").should("be.visible");
|
||||
});
|
||||
|
||||
it("submits selected measures and order date to POST endpoint", () => {
|
||||
stubGet([]);
|
||||
// After POST, return a batch so the list re-fetches populated
|
||||
const orderedAt = "2026-05-06T00:00:00.000Z";
|
||||
stubPost({ ok: true, insertedCount: 2, hubspotSync: "ok" });
|
||||
cy.intercept(
|
||||
"GET",
|
||||
`/api/portfolio/${PORTFOLIO_ID_GLOB}/pibi-requests*`,
|
||||
{ body: { pibiRequests: [
|
||||
{ id: "1", measureName: "ASHP", orderedAt, completedAt: null },
|
||||
{ id: "2", measureName: "CWI", orderedAt, completedAt: null },
|
||||
] } },
|
||||
).as("getPibiRequestsAfter");
|
||||
|
||||
openDrawerAtPibiSection();
|
||||
cy.wait("@getPibiRequests");
|
||||
|
||||
cy.get("[data-testid=log-pibi-button]").click();
|
||||
|
||||
cy.get("[data-testid=pibi-measure-checkbox-ASHP]").check();
|
||||
cy.get("[data-testid=pibi-measure-checkbox-CWI]").check();
|
||||
cy.get("[data-testid=pibi-order-date-input]").clear().type("2026-05-06");
|
||||
|
||||
cy.get("[data-testid=pibi-submit-button]").click();
|
||||
|
||||
cy.wait("@postPibiRequest").then((interception) => {
|
||||
expect(interception.request.body.measureNames).to.include.members(["ASHP", "CWI"]);
|
||||
expect(interception.request.body.orderedAt).to.include("2026-05-06");
|
||||
});
|
||||
});
|
||||
|
||||
// ── Batch display ─────────────────────────────────────────────────────────────
|
||||
|
||||
it("renders a batch group with orderedAt header and measure rows", () => {
|
||||
const orderedAt = "2026-05-01T00:00:00.000Z";
|
||||
it("renders a flat table of rows with no batch grouping", () => {
|
||||
stubGet([
|
||||
{ id: "1", measureName: "ASHP", orderedAt, completedAt: null },
|
||||
{ id: "2", measureName: "CWI", orderedAt, completedAt: null },
|
||||
{
|
||||
id: "1",
|
||||
measureName: "ASHP",
|
||||
orderedAt: "2026-05-01T00:00:00.000Z",
|
||||
completedAt: null,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
measureName: "CWI",
|
||||
orderedAt: "2026-04-01T00:00:00.000Z",
|
||||
completedAt: null,
|
||||
},
|
||||
]);
|
||||
openDrawerAtPibiSection();
|
||||
openDealPageAtPibiTab();
|
||||
cy.wait("@getPibiRequests");
|
||||
|
||||
cy.get("[data-testid=pibi-batch-group]").should("have.length", 1);
|
||||
cy.get("[data-testid=pibi-batch-group]").should("contain.text", "01 May 2026");
|
||||
cy.get("[data-testid=pibi-row-1]").should("contain.text", "ASHP");
|
||||
cy.get("[data-testid=pibi-row-2]").should("contain.text", "CWI");
|
||||
cy.get("[data-testid=pibi-row-1]").should("be.visible");
|
||||
cy.get("[data-testid=pibi-row-2]").should("be.visible");
|
||||
cy.get("[data-testid=pibi-batch-group]").should("not.exist");
|
||||
});
|
||||
|
||||
it("renders two separate batch groups for different orderedAt values", () => {
|
||||
// ── Cycle 3: Always-editable cells ────────────────────────────────────────
|
||||
|
||||
it("renders measure select and date inputs as always-editable cells", () => {
|
||||
stubGet([
|
||||
{ id: "1", measureName: "ASHP", orderedAt: "2026-04-01T00:00:00.000Z", completedAt: null },
|
||||
{ id: "2", measureName: "CWI", orderedAt: "2026-05-01T00:00:00.000Z", completedAt: null },
|
||||
{
|
||||
id: "5",
|
||||
measureName: "ASHP",
|
||||
orderedAt: "2026-05-01T00:00:00.000Z",
|
||||
completedAt: null,
|
||||
},
|
||||
]);
|
||||
openDrawerAtPibiSection();
|
||||
openDealPageAtPibiTab();
|
||||
cy.wait("@getPibiRequests");
|
||||
|
||||
cy.get("[data-testid=pibi-batch-group]").should("have.length", 2);
|
||||
cy.get("[data-testid=pibi-measure-select-5]").should("be.visible");
|
||||
cy.get("[data-testid=pibi-ordered-date-5]").should("be.visible");
|
||||
cy.get("[data-testid=pibi-completed-date-5]").should("be.visible");
|
||||
});
|
||||
|
||||
// ── Complete / undo ───────────────────────────────────────────────────────────
|
||||
// ── Cycle 4: Save disabled when clean, enabled after editing ──────────────
|
||||
|
||||
it("marks a row complete when approver clicks Complete", () => {
|
||||
const orderedAt = "2026-05-01T00:00:00.000Z";
|
||||
stubGet([{ id: "10", measureName: "ASHP", orderedAt, completedAt: null }]);
|
||||
it("shows Save disabled on load, enabled after editing a cell", () => {
|
||||
stubGet([
|
||||
{
|
||||
id: "6",
|
||||
measureName: "ASHP",
|
||||
orderedAt: "2026-05-01T00:00:00.000Z",
|
||||
completedAt: null,
|
||||
},
|
||||
]);
|
||||
openDealPageAtPibiTab();
|
||||
cy.wait("@getPibiRequests");
|
||||
|
||||
cy.get("[data-testid=pibi-save-6]").should("be.disabled");
|
||||
cy.get("[data-testid=pibi-ordered-date-6]").clear().type("2026-06-01");
|
||||
cy.get("[data-testid=pibi-save-6]").should("not.be.disabled");
|
||||
});
|
||||
|
||||
// ── Cycle 5: Save calls PATCH ─────────────────────────────────────────────
|
||||
|
||||
it("calls PATCH with updated fields when approver clicks Save", () => {
|
||||
stubGet([
|
||||
{
|
||||
id: "10",
|
||||
measureName: "ASHP",
|
||||
orderedAt: "2026-05-01T00:00:00.000Z",
|
||||
completedAt: null,
|
||||
},
|
||||
]);
|
||||
stubPatch("10");
|
||||
openDrawerAtPibiSection();
|
||||
openDealPageAtPibiTab();
|
||||
cy.wait("@getPibiRequests");
|
||||
|
||||
cy.get("[data-testid=pibi-complete-button-10]").click();
|
||||
cy.get("[data-testid=pibi-completed-date-10]").type("2026-05-15");
|
||||
cy.get("[data-testid=pibi-save-10]").click();
|
||||
|
||||
cy.wait("@patchPibiRequest-10").then((interception) => {
|
||||
expect(interception.request.body.completedAt).to.not.be.null;
|
||||
expect(interception.request.body.dealId).to.be.a("string");
|
||||
expect(interception.request.body.completedAt).to.include("2026-05-15");
|
||||
});
|
||||
});
|
||||
|
||||
it("undoes completion when approver clicks Undo on a completed row", () => {
|
||||
const orderedAt = "2026-05-01T00:00:00.000Z";
|
||||
const completedAt = "2026-05-06T10:00:00.000Z";
|
||||
stubGet([{ id: "11", measureName: "CWI", orderedAt, completedAt }]);
|
||||
stubPatch("11");
|
||||
openDrawerAtPibiSection();
|
||||
cy.wait("@getPibiRequests");
|
||||
// ── Cycle 6: Delete calls DELETE ──────────────────────────────────────────
|
||||
|
||||
cy.get("[data-testid=pibi-complete-button-11]").should("contain.text", "Undo");
|
||||
cy.get("[data-testid=pibi-complete-button-11]").click();
|
||||
cy.wait("@patchPibiRequest-11").then((interception) => {
|
||||
expect(interception.request.body.completedAt).to.be.null;
|
||||
});
|
||||
});
|
||||
|
||||
// ── Delete ────────────────────────────────────────────────────────────────────
|
||||
|
||||
it("deletes a row when approver clicks Delete", () => {
|
||||
const orderedAt = "2026-05-01T00:00:00.000Z";
|
||||
stubGet([{ id: "20", measureName: "EWI", orderedAt, completedAt: null }]);
|
||||
it("calls DELETE when approver clicks Delete on a row", () => {
|
||||
stubGet([
|
||||
{
|
||||
id: "20",
|
||||
measureName: "EWI",
|
||||
orderedAt: "2026-05-01T00:00:00.000Z",
|
||||
completedAt: null,
|
||||
},
|
||||
]);
|
||||
stubDelete("20");
|
||||
openDrawerAtPibiSection();
|
||||
openDealPageAtPibiTab();
|
||||
cy.wait("@getPibiRequests");
|
||||
|
||||
cy.get("[data-testid=pibi-delete-button-20]").click();
|
||||
cy.get("[data-testid=pibi-delete-20]").click();
|
||||
cy.wait("@deletePibiRequest-20");
|
||||
});
|
||||
|
||||
// ── Mark all complete ─────────────────────────────────────────────────────────
|
||||
// ── Cycle 7: Add row → POST ───────────────────────────────────────────────
|
||||
|
||||
it("marks all rows in a batch complete via the batch header button", () => {
|
||||
const orderedAt = "2026-05-01T00:00:00.000Z";
|
||||
it("appends a blank row on add-row click and POSTs on save", () => {
|
||||
stubGet([]);
|
||||
stubPost({ ok: true, insertedCount: 1, hubspotSync: "ok" });
|
||||
cy.intercept("GET", `/api/portfolio/${PORTFOLIO_ID_GLOB}/pibi-requests*`, {
|
||||
body: {
|
||||
pibiRequests: [
|
||||
{
|
||||
id: "99",
|
||||
measureName: "ASHP",
|
||||
orderedAt: new Date().toISOString(),
|
||||
completedAt: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
}).as("getPibiRequestsAfter");
|
||||
|
||||
openDealPageAtPibiTab();
|
||||
cy.wait("@getPibiRequests");
|
||||
|
||||
cy.get("[data-testid=pibi-empty-add-row]").click();
|
||||
cy.get("[data-testid=pibi-section]").find("tr[data-testid^=pibi-row-new]").should("have.length", 1);
|
||||
|
||||
cy.get("[data-testid=pibi-section]")
|
||||
.find("[data-testid^=pibi-save-new]")
|
||||
.click();
|
||||
|
||||
cy.wait("@postPibiRequest").then((interception) => {
|
||||
expect(interception.request.body.measureNames).to.be.an("array").with.length(1);
|
||||
expect(interception.request.body.orderedAt).to.be.a("string");
|
||||
});
|
||||
});
|
||||
|
||||
// ── Cycle 8: Mark all complete ────────────────────────────────────────────
|
||||
|
||||
it("PATCHes all incomplete rows when Mark all complete is clicked", () => {
|
||||
stubGet([
|
||||
{ id: "30", measureName: "ASHP", orderedAt, completedAt: null },
|
||||
{ id: "31", measureName: "CWI", orderedAt, completedAt: null },
|
||||
{
|
||||
id: "30",
|
||||
measureName: "ASHP",
|
||||
orderedAt: "2026-05-01T00:00:00.000Z",
|
||||
completedAt: null,
|
||||
},
|
||||
{
|
||||
id: "31",
|
||||
measureName: "CWI",
|
||||
orderedAt: "2026-05-01T00:00:00.000Z",
|
||||
completedAt: null,
|
||||
},
|
||||
]);
|
||||
stubPatch("30");
|
||||
stubPatch("31");
|
||||
openDrawerAtPibiSection();
|
||||
openDealPageAtPibiTab();
|
||||
cy.wait("@getPibiRequests");
|
||||
|
||||
cy.get("[data-testid=pibi-batch-complete-button]").click();
|
||||
cy.wait("@patchPibiRequest-30");
|
||||
cy.get("[data-testid=pibi-mark-all-complete]").click();
|
||||
cy.wait("@patchPibiRequest-30").then((i) => {
|
||||
expect(i.request.body.completedAt).to.be.a("string");
|
||||
});
|
||||
cy.wait("@patchPibiRequest-31");
|
||||
});
|
||||
|
||||
// ── Cycle 9: Scope badges ─────────────────────────────────────────────────
|
||||
|
||||
it("shows Approved badge for a measure in approvedMeasures", () => {
|
||||
stubGet([
|
||||
{
|
||||
id: "40",
|
||||
measureName: "ASHP",
|
||||
orderedAt: "2026-05-01T00:00:00.000Z",
|
||||
completedAt: null,
|
||||
},
|
||||
]);
|
||||
stubMeasures(["ASHP"], []);
|
||||
openDealPageAtPibiTab();
|
||||
cy.wait("@getPibiRequests");
|
||||
|
||||
cy.get("[data-testid=pibi-row-40]").should("contain.text", "Approved");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -28,14 +28,13 @@ import {
|
|||
MeasureApprovalEditor,
|
||||
InstructMeasureEditor,
|
||||
ApprovalLogSection,
|
||||
PibiDatesEditor,
|
||||
PibiMeasureSelector,
|
||||
SurveyRequestSection,
|
||||
RemovalRequestSection,
|
||||
SectionHeader,
|
||||
SECTION_TITLES,
|
||||
WRITE_ROLES,
|
||||
} from "../PropertyDetailDrawer";
|
||||
import { PibiSection } from "../PibiSection";
|
||||
|
||||
type Tab = "works" | "pibi-surveys" | "documents";
|
||||
const VALID_TABS: Tab[] = ["works", "pibi-surveys", "documents"];
|
||||
|
|
@ -84,8 +83,6 @@ export default function DealPage({
|
|||
const technicalApprovedMeasures = parseMeasures(
|
||||
deal.technicalApprovedMeasuresForInstall ?? null,
|
||||
);
|
||||
const pibiMeasures = parseMeasures(deal.measuresForPibiOrdered ?? null);
|
||||
|
||||
const isApprover = userCapability.includes("approver");
|
||||
const canWrite = WRITE_ROLES.includes(userRole);
|
||||
const stageColors = STAGE_COLORS[deal.displayStage] ?? STAGE_COLORS["Unknown Stage"];
|
||||
|
|
@ -308,43 +305,12 @@ export default function DealPage({
|
|||
>
|
||||
<div>
|
||||
<SectionHeader id="pibi" label={SECTION_TITLES.pibi} />
|
||||
<div className="space-y-3">
|
||||
<PibiDatesEditor
|
||||
dealId={deal.dealId}
|
||||
portfolioId={portfolioId}
|
||||
initialOrderDate={deal.pibiOrderDate}
|
||||
initialCompletedDate={deal.pibiCompletedDate}
|
||||
canEdit={canWrite}
|
||||
/>
|
||||
{isApprover ? (
|
||||
<PibiMeasureSelector
|
||||
dealId={deal.dealId}
|
||||
portfolioId={portfolioId}
|
||||
proposedMeasures={parseMeasures(deal.proposedMeasures ?? null)}
|
||||
canEdit
|
||||
/>
|
||||
) : (
|
||||
pibiMeasures.length > 0 && (
|
||||
<div className="divide-y divide-gray-50">
|
||||
<InfoRow
|
||||
label="Measures for PIBI"
|
||||
value={
|
||||
<span className="flex flex-wrap gap-1.5">
|
||||
{pibiMeasures.map((m) => (
|
||||
<span
|
||||
key={m}
|
||||
className="px-2 py-0.5 rounded-full text-[11px] bg-gray-50 border border-gray-200 text-gray-600"
|
||||
>
|
||||
{m}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<PibiSection
|
||||
dealId={deal.dealId}
|
||||
portfolioId={portfolioId}
|
||||
proposedMeasures={parseMeasures(deal.proposedMeasures ?? null)}
|
||||
canEdit={isApprover}
|
||||
/>
|
||||
</div>
|
||||
<div className="border-t border-gray-100 pt-4">
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-gray-400 mb-3">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue