forked from jappel/leistungsbilanz-ts
Integrate project overview filters and icon actions
Document filter combinations, reset and browser persistence. Keep the search hint applicable before board information fields are introduced. Validation: 20 focused tests, 431 total tests, API/web builds, scripts and Next typechecks, and diff checks passed. GUI retest skipped as authorized based on the colleague's prior testing.
This commit is contained in:
commit
2e223be8b4
11 changed files with 1457 additions and 156 deletions
271
tests/overview-filters.test.ts
Normal file
271
tests/overview-filters.test.ts
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import {
|
||||
buildCostGroupFilterOptions,
|
||||
buildFloorFilterOptions,
|
||||
buildSupplyTypeFilterOptions,
|
||||
emptyDistributionBoardFilters,
|
||||
emptyProjectDeviceFilters,
|
||||
filterDistributionBoards,
|
||||
filterProjectDevices,
|
||||
isDistributionBoardFilterActive,
|
||||
isProjectDeviceFilterActive,
|
||||
retainAvailableFilterValues,
|
||||
toggleFilterValue,
|
||||
withoutCostGroupFilterId,
|
||||
withoutFloorFilterId,
|
||||
withoutSupplyTypeFilterId,
|
||||
} from "../src/frontend/utils/overview-filters.js";
|
||||
import type {
|
||||
DistributionBoardDto,
|
||||
FloorDto,
|
||||
ProjectDeviceDto,
|
||||
} from "../src/frontend/types.js";
|
||||
|
||||
type TestBoard = Pick<
|
||||
DistributionBoardDto,
|
||||
"id" | "name" | "floorId" | "supplyType"
|
||||
>;
|
||||
|
||||
function board(
|
||||
id: string,
|
||||
name: string,
|
||||
floorId: string | null,
|
||||
supplyType: DistributionBoardDto["supplyType"]
|
||||
): TestBoard {
|
||||
return { id, name, floorId, supplyType };
|
||||
}
|
||||
|
||||
function device(
|
||||
id: string,
|
||||
overrides: Partial<ProjectDeviceDto>
|
||||
): ProjectDeviceDto {
|
||||
return {
|
||||
id,
|
||||
projectId: "project-1",
|
||||
name: `device-${id}`,
|
||||
displayName: `Gerät ${id}`,
|
||||
phaseType: "single_phase",
|
||||
connectionKind: null,
|
||||
costGroup: null,
|
||||
category: "single_phase",
|
||||
quantity: 1,
|
||||
powerPerUnit: 1,
|
||||
simultaneityFactor: 1,
|
||||
totalPower: 1,
|
||||
cosPhi: null,
|
||||
remark: null,
|
||||
voltageV: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
const floors: FloorDto[] = [
|
||||
{ id: "floor-og", projectId: "project-1", name: "1. OG", sortOrder: 2 },
|
||||
{ id: "floor-eg", projectId: "project-1", name: "EG", sortOrder: 1 },
|
||||
];
|
||||
const floorNameById = new Map(floors.map((floor) => [floor.id, floor.name]));
|
||||
|
||||
const boards = [
|
||||
board("b1", "UV-EG-01", "floor-eg", "AV"),
|
||||
board("b2", "UV-OG-01", "floor-og", "SV"),
|
||||
board("b3", "HV", null, "AV"),
|
||||
board("b4", "SiBe-Zentrale", null, null),
|
||||
];
|
||||
|
||||
describe("overview filters: distribution boards", () => {
|
||||
it("returns every board without an active filter", () => {
|
||||
assert.equal(isDistributionBoardFilterActive(emptyDistributionBoardFilters), false);
|
||||
assert.deepEqual(
|
||||
filterDistributionBoards(boards, emptyDistributionBoardFilters, floorNameById),
|
||||
boards
|
||||
);
|
||||
});
|
||||
|
||||
it("also searches board number and location when the boards carry them", () => {
|
||||
const withInfo = [
|
||||
{ ...boards[0]!, boardNumber: "UV 0.1", location: "Raum 003 Technik" },
|
||||
{ ...boards[1]!, boardNumber: null, location: null },
|
||||
];
|
||||
assert.deepEqual(
|
||||
filterDistributionBoards(
|
||||
withInfo,
|
||||
{ ...emptyDistributionBoardFilters, query: "uv 0.1" },
|
||||
floorNameById
|
||||
).map((entry) => entry.id),
|
||||
[withInfo[0]!.id]
|
||||
);
|
||||
assert.deepEqual(
|
||||
filterDistributionBoards(
|
||||
withInfo,
|
||||
{ ...emptyDistributionBoardFilters, query: "technik" },
|
||||
floorNameById
|
||||
).map((entry) => entry.id),
|
||||
[withInfo[0]!.id]
|
||||
);
|
||||
});
|
||||
|
||||
it("searches board and floor names case-insensitively and ignores blank queries", () => {
|
||||
const byFloorName = filterDistributionBoards(
|
||||
boards,
|
||||
{ ...emptyDistributionBoardFilters, query: " og " },
|
||||
floorNameById
|
||||
);
|
||||
assert.deepEqual(byFloorName.map((entry) => entry.id), ["b2"]);
|
||||
const byBoardName = filterDistributionBoards(
|
||||
boards,
|
||||
{ ...emptyDistributionBoardFilters, query: "sibe" },
|
||||
floorNameById
|
||||
);
|
||||
assert.deepEqual(byBoardName.map((entry) => entry.id), ["b4"]);
|
||||
assert.equal(
|
||||
isDistributionBoardFilterActive({ ...emptyDistributionBoardFilters, query: " " }),
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it("filters floors with the dedicated option for boards without floor", () => {
|
||||
const result = filterDistributionBoards(
|
||||
boards,
|
||||
{ ...emptyDistributionBoardFilters, floorIds: ["floor-eg", withoutFloorFilterId] },
|
||||
floorNameById
|
||||
);
|
||||
assert.deepEqual(result.map((entry) => entry.id), ["b1", "b3", "b4"]);
|
||||
});
|
||||
|
||||
it("combines supply type with the other criteria", () => {
|
||||
const filters = {
|
||||
query: "",
|
||||
floorIds: [withoutFloorFilterId],
|
||||
supplyTypes: ["AV"],
|
||||
};
|
||||
assert.equal(isDistributionBoardFilterActive(filters), true);
|
||||
assert.deepEqual(
|
||||
filterDistributionBoards(boards, filters, floorNameById).map((entry) => entry.id),
|
||||
["b3"]
|
||||
);
|
||||
assert.deepEqual(
|
||||
filterDistributionBoards(
|
||||
boards,
|
||||
{ ...emptyDistributionBoardFilters, supplyTypes: [withoutSupplyTypeFilterId] },
|
||||
floorNameById
|
||||
).map((entry) => entry.id),
|
||||
["b4"]
|
||||
);
|
||||
});
|
||||
|
||||
it("offers floors in configured order followed by 'Ohne Etage'", () => {
|
||||
assert.deepEqual(buildFloorFilterOptions(floors), [
|
||||
{ id: "floor-eg", label: "EG" },
|
||||
{ id: "floor-og", label: "1. OG" },
|
||||
{ id: withoutFloorFilterId, label: "Ohne Etage" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("offers enabled and used supply types in catalog order", () => {
|
||||
const options = buildSupplyTypeFilterOptions(
|
||||
["AV", "SV", "EV", "USV"],
|
||||
["USV", "AV"],
|
||||
boards,
|
||||
{ AV: "AV – Allgemein", SV: "SV – Sicherheit" }
|
||||
);
|
||||
assert.deepEqual(options, [
|
||||
{ id: "AV", label: "AV – Allgemein" },
|
||||
{ id: "SV", label: "SV – Sicherheit" },
|
||||
{ id: "USV", label: "USV" },
|
||||
{ id: withoutSupplyTypeFilterId, label: "Nicht festgelegt" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("overview filters: project devices", () => {
|
||||
const devices = [
|
||||
device("d1", {
|
||||
displayName: "Steckdose Büro",
|
||||
costGroup: "444",
|
||||
connectionKind: "Steckdose",
|
||||
}),
|
||||
device("d2", {
|
||||
displayName: "Durchlauferhitzer",
|
||||
phaseType: "three_phase",
|
||||
category: "three_phase",
|
||||
costGroup: " 445 ",
|
||||
}),
|
||||
device("d3", {
|
||||
displayName: "Downlight",
|
||||
category: "lighting",
|
||||
costGroup: " ",
|
||||
}),
|
||||
device("d4", { displayName: "Kaffeemaschine", costGroup: null }),
|
||||
];
|
||||
|
||||
it("searches names, connection kind, cost group and the German category label", () => {
|
||||
assert.deepEqual(
|
||||
filterProjectDevices(devices, { ...emptyProjectDeviceFilters, query: "STECK" }).map(
|
||||
(entry) => entry.id
|
||||
),
|
||||
["d1"]
|
||||
);
|
||||
assert.deepEqual(
|
||||
filterProjectDevices(
|
||||
devices,
|
||||
{ ...emptyProjectDeviceFilters, query: "beleucht" },
|
||||
{ lighting: "Beleuchtung" }
|
||||
).map((entry) => entry.id),
|
||||
["d3"]
|
||||
);
|
||||
});
|
||||
|
||||
it("filters category, phase type and cost group together", () => {
|
||||
const filters = {
|
||||
query: "",
|
||||
categories: ["single_phase", "three_phase"],
|
||||
phaseTypes: ["three_phase"],
|
||||
costGroups: ["445"],
|
||||
};
|
||||
assert.equal(isProjectDeviceFilterActive(filters), true);
|
||||
assert.deepEqual(
|
||||
filterProjectDevices(devices, filters).map((entry) => entry.id),
|
||||
["d2"]
|
||||
);
|
||||
});
|
||||
|
||||
it("treats blank cost groups as 'Ohne Kostengruppe'", () => {
|
||||
assert.deepEqual(
|
||||
filterProjectDevices(devices, {
|
||||
...emptyProjectDeviceFilters,
|
||||
costGroups: [withoutCostGroupFilterId],
|
||||
}).map((entry) => entry.id),
|
||||
["d3", "d4"]
|
||||
);
|
||||
assert.deepEqual(buildCostGroupFilterOptions(devices), [
|
||||
{ id: "444", label: "444" },
|
||||
{ id: "445", label: "445" },
|
||||
{ id: withoutCostGroupFilterId, label: "Ohne Kostengruppe" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns an empty list when the filters hide every device", () => {
|
||||
assert.deepEqual(
|
||||
filterProjectDevices(devices, { ...emptyProjectDeviceFilters, query: "gibt es nicht" }),
|
||||
[]
|
||||
);
|
||||
assert.equal(isProjectDeviceFilterActive(emptyProjectDeviceFilters), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("overview filters: selection helpers", () => {
|
||||
it("toggles values without mutating the selection", () => {
|
||||
const selected = ["a"];
|
||||
assert.deepEqual(toggleFilterValue(selected, "b"), ["a", "b"]);
|
||||
assert.deepEqual(toggleFilterValue(selected, "a"), []);
|
||||
assert.deepEqual(selected, ["a"]);
|
||||
});
|
||||
|
||||
it("drops selected values that are no longer offered", () => {
|
||||
assert.deepEqual(
|
||||
retainAvailableFilterValues(["floor-eg", "deleted-floor"], buildFloorFilterOptions(floors)),
|
||||
["floor-eg"]
|
||||
);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue