Harden Linear XLSX end to end

This commit is contained in:
Julian Appel 2026-09-30 21:02:20 +02:00
parent 2581beee3c
commit 4ff01b2ccd
26 changed files with 522 additions and 246 deletions

View file

@ -0,0 +1,25 @@
import type { ExternalProjectObjectDto } from "../types";
export type RevitObjectSort = "family" | "room" | "worksharing";
export function filterAndSortRevitObjects(
objects: readonly ExternalProjectObjectDto[],
input: { search: string; boardFilter: string; assignmentFilter: string; sort: RevitObjectSort }
) {
const needle = input.search.trim().toLocaleLowerCase("de-DE");
return objects.filter((object) => {
if (input.boardFilter !== "all" && (input.boardFilter === "none" ? object.distributionBoardId !== null : object.distributionBoardId !== input.boardFilter)) return false;
if (input.assignmentFilter === "unassigned" && object.circuitDeviceRowId !== null) return false;
if (input.assignmentFilter === "linked" && object.circuitDeviceRowId === null) return false;
return !needle || [object.worksharingId, object.familyAndType, object.selectionMarker, object.sourceRoomNumber, object.sourceRoomName, object.planningValues.displayName ?? ""].some((value) => value.toLocaleLowerCase("de-DE").includes(needle));
}).sort((leftObject, rightObject) => {
const left = input.sort === "family" ? leftObject.familyAndType : input.sort === "room" ? `${leftObject.sourceRoomNumber}\u0000${leftObject.sourceRoomName}` : leftObject.worksharingId;
const right = input.sort === "family" ? rightObject.familyAndType : input.sort === "room" ? `${rightObject.sourceRoomNumber}\u0000${rightObject.sourceRoomName}` : rightObject.worksharingId;
return left.localeCompare(right, "de", { numeric: true }) || leftObject.worksharingId.localeCompare(rightObject.worksharingId);
});
}
export function createVirtualRevitObjectWindow<T>(rows: readonly T[], scrollTop: number, rowHeight: number, viewportHeight: number, overscan = 5) {
const first = Math.max(0, Math.floor(scrollTop / rowHeight) - overscan);
return { first, rows: rows.slice(first, first + Math.ceil(viewportHeight / rowHeight) + overscan * 2) };
}

View file

@ -3,6 +3,7 @@
import { useEffect, useMemo, useState } from "react";
import type { ExternalProjectObjectsDto, ExternalReturnScopeDto } from "../types";
import { createExternalReturnWorkbook, deleteMissingExternalProjectObjects, getExternalProjectObjects, updateExternalProjectObjects } from "../utils/api";
import { createVirtualRevitObjectWindow, filterAndSortRevitObjects, type RevitObjectSort } from "./revit-object-table-model";
const rowHeight = 44;
const viewportHeight = 520;
@ -12,7 +13,7 @@ export function RevitObjectTable({ projectId, onRevision }: { projectId: string;
const [search, setSearch] = useState("");
const [boardFilter, setBoardFilter] = useState("all");
const [assignmentFilter, setAssignmentFilter] = useState("all");
const [sort, setSort] = useState<"family" | "room" | "worksharing">("family");
const [sort, setSort] = useState<RevitObjectSort>("family");
const [selected, setSelected] = useState<Set<string>>(new Set());
const [scrollTop, setScrollTop] = useState(0);
const [bulkBoard, setBulkBoard] = useState("");
@ -33,20 +34,9 @@ export function RevitObjectTable({ projectId, onRevision }: { projectId: string;
const rows = useMemo(() => {
if (!data) return [];
const needle = search.trim().toLocaleLowerCase("de-DE");
return data.objects.filter((object) => {
if (boardFilter !== "all" && (boardFilter === "none" ? object.distributionBoardId !== null : object.distributionBoardId !== boardFilter)) return false;
if (assignmentFilter === "unassigned" && object.circuitDeviceRowId !== null) return false;
if (assignmentFilter === "linked" && object.circuitDeviceRowId === null) return false;
return !needle || [object.worksharingId, object.familyAndType, object.selectionMarker, object.sourceRoomNumber, object.sourceRoomName, object.planningValues.displayName ?? ""].some((value) => value.toLocaleLowerCase("de-DE").includes(needle));
}).sort((a, b) => {
const left = sort === "family" ? a.familyAndType : sort === "room" ? `${a.sourceRoomNumber}\u0000${a.sourceRoomName}` : a.worksharingId;
const right = sort === "family" ? b.familyAndType : sort === "room" ? `${b.sourceRoomNumber}\u0000${b.sourceRoomName}` : b.worksharingId;
return left.localeCompare(right, "de", { numeric: true }) || a.worksharingId.localeCompare(b.worksharingId);
});
return filterAndSortRevitObjects(data.objects, { search, boardFilter, assignmentFilter, sort });
}, [data, search, boardFilter, assignmentFilter, sort]);
const first = Math.max(0, Math.floor(scrollTop / rowHeight) - 5);
const visible = rows.slice(first, first + Math.ceil(viewportHeight / rowHeight) + 10);
const { first, rows: visible } = createVirtualRevitObjectWindow(rows, scrollTop, rowHeight, viewportHeight);
const selectable = rows.filter((object) => object.circuitDeviceRowId === null);
const returnSelectable = rows.filter((object) => object.presenceStatus === "present");
const selectedHasLinkedObject = data?.objects.some((object) => selected.has(object.id) && object.circuitDeviceRowId !== null) ?? false;