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:
Julian Appel 2026-09-26 21:16:48 +02:00
commit 2e223be8b4
11 changed files with 1457 additions and 156 deletions

View 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"]
);
});
});