forked from jappel/leistungsbilanz-ts
Compact circuit editor header, pin grid headers, scope sidebar to project
The circuit editor page now fills the viewport so only the grid scrolls and its sticky column headers stay visible. The large page title is replaced by a compact title bar with back link, board number/name, the feeding reference, the toolbar and a condensed power summary. Inside a project the sidebar shows only the active project with its sections, an expandable board list ordered by logical feed (current board highlighted), Revit and the print view. The feed ordering is shared with the print view through orderDistributionBoardsByFeed. Refs #26 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
898be43ebc
commit
07bc52a134
7 changed files with 444 additions and 156 deletions
|
|
@ -3,11 +3,12 @@
|
|||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { listProjects } from "../utils/api";
|
||||
import type { ProjectDto } from "../types";
|
||||
import { listCircuitLists, listDistributionBoards, listProjects } from "../utils/api";
|
||||
import type { CircuitListDto, DistributionBoardDto, ProjectDto } from "../types";
|
||||
import { ThemeToggle } from "./ThemeToggle";
|
||||
import { useAuth } from "./AuthProvider";
|
||||
import { getConfigEntry } from "../utils/config-access";
|
||||
import { orderDistributionBoardsByFeed } from "../utils/distribution-board-hierarchy";
|
||||
|
||||
// Anchors on the project overview page (/projects/[id]).
|
||||
const PROJECT_SECTIONS = [
|
||||
|
|
@ -20,10 +21,22 @@ const PROJECT_SECTIONS = [
|
|||
const PROJECT_ROUTES = [
|
||||
{ segment: "struktur", label: "Gebäudestruktur", icon: "▥" },
|
||||
{ segment: "grobbilanz", label: "Grobe Leistungsbilanz", icon: "Σ" },
|
||||
{ segment: "revit", label: "Revit", icon: "⇄" },
|
||||
] as const;
|
||||
|
||||
// Matches the project overview and all of its sub-routes (group 2 = rest).
|
||||
const PROJECT_PATH_PATTERN = /^\/projects\/([^/]+)(\/.*)?$/;
|
||||
const CIRCUIT_LIST_PATH_PATTERN = /^\/circuit-lists\/([^/]+)(\/|$)/;
|
||||
const BOARDS_EXPANDED_STORAGE_KEY = "leistungsbilanz:sidebar-boards-expanded";
|
||||
// Dispatched by the project page whenever its board list changes.
|
||||
const DISTRIBUTION_BOARDS_CHANGED_EVENT =
|
||||
"leistungsbilanz:distribution-boards-changed";
|
||||
|
||||
interface SidebarBoardLink {
|
||||
board: DistributionBoardDto;
|
||||
depth: number;
|
||||
circuitListId: string | null;
|
||||
}
|
||||
|
||||
interface SidebarProps {
|
||||
isCollapsed: boolean;
|
||||
|
|
@ -43,8 +56,65 @@ export function Sidebar({ isCollapsed, onToggleCollapsed }: SidebarProps) {
|
|||
? activeProjectId
|
||||
: null;
|
||||
|
||||
const activeCircuitListId =
|
||||
projectSubPath.match(CIRCUIT_LIST_PATH_PATTERN)?.[1] ?? null;
|
||||
|
||||
const [projects, setProjects] = useState<ProjectDto[]>([]);
|
||||
const [activeSection, setActiveSection] = useState<string | null>(null);
|
||||
const [boardLinks, setBoardLinks] = useState<SidebarBoardLink[]>([]);
|
||||
const [isBoardListExpanded, setIsBoardListExpanded] = useState(true);
|
||||
const activeProject = projects.find((project) => project.id === activeProjectId);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
setIsBoardListExpanded(
|
||||
window.localStorage.getItem(BOARDS_EXPANDED_STORAGE_KEY) !== "0"
|
||||
);
|
||||
} catch {
|
||||
// Storage may be unavailable; keep the list expanded.
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeProjectId) {
|
||||
setBoardLinks([]);
|
||||
return;
|
||||
}
|
||||
let isCurrent = true;
|
||||
function refreshBoards(projectId: string) {
|
||||
Promise.all([listDistributionBoards(projectId), listCircuitLists(projectId)])
|
||||
.then(([boards, circuitLists]) => {
|
||||
if (isCurrent) {
|
||||
setBoardLinks(buildBoardLinks(boards, circuitLists));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (isCurrent) {
|
||||
setBoardLinks([]);
|
||||
}
|
||||
});
|
||||
}
|
||||
const projectId = activeProjectId;
|
||||
const handleBoardsChanged = () => refreshBoards(projectId);
|
||||
refreshBoards(projectId);
|
||||
window.addEventListener(DISTRIBUTION_BOARDS_CHANGED_EVENT, handleBoardsChanged);
|
||||
return () => {
|
||||
isCurrent = false;
|
||||
window.removeEventListener(DISTRIBUTION_BOARDS_CHANGED_EVENT, handleBoardsChanged);
|
||||
};
|
||||
}, [activeProjectId]);
|
||||
|
||||
function toggleBoardList() {
|
||||
setIsBoardListExpanded((current) => {
|
||||
const next = !current;
|
||||
try {
|
||||
window.localStorage.setItem(BOARDS_EXPANDED_STORAGE_KEY, next ? "1" : "0");
|
||||
} catch {
|
||||
// Ignore unavailable storage; the toggle still works for this page.
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
function refreshProjects() {
|
||||
|
|
@ -117,52 +187,93 @@ export function Sidebar({ isCollapsed, onToggleCollapsed }: SidebarProps) {
|
|||
<span className="sidebar-label">Alle Projekte</span>
|
||||
</Link>
|
||||
|
||||
<div className="sidebar-section-label">Projekte</div>
|
||||
{projects.map((project) => (
|
||||
<Link
|
||||
key={project.id}
|
||||
href={`/projects/${project.id}`}
|
||||
className={
|
||||
project.id === activeProjectId ? "sidebar-link active" : "sidebar-link"
|
||||
}
|
||||
title={project.name}
|
||||
>
|
||||
<span className="sidebar-icon">▣</span>
|
||||
<span className="sidebar-label">{project.name}</span>
|
||||
</Link>
|
||||
))}
|
||||
{!projects.length ? (
|
||||
<div className="sidebar-section-label" style={{ opacity: 0.6 }}>
|
||||
Noch keine Projekte
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{activeProjectId && (
|
||||
{activeProjectId ? (
|
||||
<>
|
||||
<div className="sidebar-section-label">In diesem Projekt</div>
|
||||
{PROJECT_SECTIONS.map(({ id, label, icon }) =>
|
||||
overviewProjectId ? (
|
||||
<a
|
||||
key={id}
|
||||
href={`#${id}`}
|
||||
className={activeSection === id ? "sidebar-link active" : "sidebar-link"}
|
||||
title={label}
|
||||
>
|
||||
<span className="sidebar-icon">{icon}</span>
|
||||
<span className="sidebar-label">{label}</span>
|
||||
</a>
|
||||
) : (
|
||||
<Link
|
||||
key={id}
|
||||
href={`/projects/${activeProjectId}#${id}`}
|
||||
className="sidebar-link"
|
||||
title={label}
|
||||
>
|
||||
<span className="sidebar-icon">{icon}</span>
|
||||
<span className="sidebar-label">{label}</span>
|
||||
</Link>
|
||||
)
|
||||
)}
|
||||
<div className="sidebar-section-label">Aktives Projekt</div>
|
||||
<Link
|
||||
href={`/projects/${activeProjectId}`}
|
||||
className={overviewProjectId ? "sidebar-link active" : "sidebar-link"}
|
||||
title={activeProject?.name ?? "Projekt"}
|
||||
>
|
||||
<span className="sidebar-icon">▣</span>
|
||||
<span className="sidebar-label sidebar-project-name">
|
||||
{activeProject?.name ?? "Projekt"}
|
||||
</span>
|
||||
</Link>
|
||||
{PROJECT_SECTIONS.map(({ id, label, icon }) => (
|
||||
<div key={id}>
|
||||
<div className="sidebar-link-row">
|
||||
{overviewProjectId ? (
|
||||
<a
|
||||
href={`#${id}`}
|
||||
className={activeSection === id ? "sidebar-link active" : "sidebar-link"}
|
||||
title={label}
|
||||
>
|
||||
<span className="sidebar-icon">{icon}</span>
|
||||
<span className="sidebar-label">{label}</span>
|
||||
</a>
|
||||
) : (
|
||||
<Link
|
||||
href={`/projects/${activeProjectId}#${id}`}
|
||||
className="sidebar-link"
|
||||
title={label}
|
||||
>
|
||||
<span className="sidebar-icon">{icon}</span>
|
||||
<span className="sidebar-label">{label}</span>
|
||||
</Link>
|
||||
)}
|
||||
{id === "verteilungen" && boardLinks.length > 0 && !isCollapsed ? (
|
||||
<button
|
||||
aria-controls="sidebar-board-list"
|
||||
aria-expanded={isBoardListExpanded}
|
||||
aria-label={
|
||||
isBoardListExpanded ? "Verteilerliste einklappen" : "Verteilerliste ausklappen"
|
||||
}
|
||||
className="sidebar-subtoggle"
|
||||
onClick={toggleBoardList}
|
||||
title={
|
||||
isBoardListExpanded ? "Verteilerliste einklappen" : "Verteilerliste ausklappen"
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
{isBoardListExpanded ? "▾" : "▸"}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
{id === "verteilungen" && isBoardListExpanded && !isCollapsed ? (
|
||||
<div className="sidebar-board-list" id="sidebar-board-list">
|
||||
{boardLinks.map(({ board, depth, circuitListId }) => {
|
||||
const label = board.boardNumber
|
||||
? `${board.boardNumber} ${board.name}`
|
||||
: board.name;
|
||||
const isActive =
|
||||
circuitListId !== null && circuitListId === activeCircuitListId;
|
||||
return circuitListId ? (
|
||||
<Link
|
||||
key={board.id}
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
className={isActive ? "sidebar-board-link active" : "sidebar-board-link"}
|
||||
href={`/projects/${activeProjectId}/circuit-lists/${circuitListId}/tree-edit`}
|
||||
style={{ paddingLeft: `${0.9 + depth * 0.8}rem` }}
|
||||
title={label}
|
||||
>
|
||||
{label}
|
||||
</Link>
|
||||
) : (
|
||||
<span
|
||||
key={board.id}
|
||||
className="sidebar-board-link disabled"
|
||||
style={{ paddingLeft: `${0.9 + depth * 0.8}rem` }}
|
||||
title={`${label} (keine Stromkreisliste)`}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
{PROJECT_ROUTES.map(({ segment, label, icon }) => {
|
||||
const href = `/projects/${activeProjectId}/${segment}`;
|
||||
const isActive =
|
||||
|
|
@ -180,6 +291,36 @@ export function Sidebar({ isCollapsed, onToggleCollapsed }: SidebarProps) {
|
|||
</Link>
|
||||
);
|
||||
})}
|
||||
<a
|
||||
href={`/projects/${activeProjectId}/print`}
|
||||
className="sidebar-link"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
title="Seitenansicht (neuer Tab)"
|
||||
>
|
||||
<span className="sidebar-icon">⎙</span>
|
||||
<span className="sidebar-label">Seitenansicht</span>
|
||||
</a>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="sidebar-section-label">Projekte</div>
|
||||
{projects.map((project) => (
|
||||
<Link
|
||||
key={project.id}
|
||||
href={`/projects/${project.id}`}
|
||||
className="sidebar-link"
|
||||
title={project.name}
|
||||
>
|
||||
<span className="sidebar-icon">▣</span>
|
||||
<span className="sidebar-label">{project.name}</span>
|
||||
</Link>
|
||||
))}
|
||||
{!projects.length ? (
|
||||
<div className="sidebar-section-label" style={{ opacity: 0.6 }}>
|
||||
Noch keine Projekte
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</nav>
|
||||
|
|
@ -215,3 +356,17 @@ export function Sidebar({ isCollapsed, onToggleCollapsed }: SidebarProps) {
|
|||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
function buildBoardLinks(
|
||||
boards: DistributionBoardDto[],
|
||||
circuitLists: CircuitListDto[]
|
||||
): SidebarBoardLink[] {
|
||||
const circuitListIdByBoardId = new Map(
|
||||
circuitLists.map((circuitList) => [circuitList.distributionBoardId, circuitList.id])
|
||||
);
|
||||
return orderDistributionBoardsByFeed(boards).map(({ board, depth }) => ({
|
||||
board,
|
||||
depth,
|
||||
circuitListId: circuitListIdByBoardId.get(board.id) ?? null,
|
||||
}));
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue