Route boards to circuit-first editor

This commit is contained in:
2026-07-22 19:23:30 +02:00
parent 7580ad0ade
commit 689cfd3c65
3 changed files with 82 additions and 18 deletions
@@ -239,3 +239,29 @@ Acceptance criteria:
- the codebase can later add rule-based sizing without redesigning circuits/devices - the codebase can later add rule-based sizing without redesigning circuits/devices
- users can manually override calculated suggestions - users can manually override calculated suggestions
## Phase 9: Development Environment and Docker
Goal:
Make short development sessions quick to start and stop.
Tasks:
- add a Docker-based local development setup
- start frontend, API and SQLite persistence with one command
- keep the database in an explicit persistent volume
- run or document pending database migrations during startup
- add concise start, stop and reset instructions
- retain a non-Docker development path for direct debugging
Acceptance criteria:
- a new development session starts with one documented command
- stopping and restarting does not lose the local database
- frontend and API health can be checked without manual port setup
- the development workflow works consistently across short sessions
Note:
This phase may be moved earlier if repeated local startup overhead begins to slow down feature development.
@@ -11,18 +11,21 @@ export default function CircuitTreeEditPage() {
<main className="container py-4"> <main className="container py-4">
<div className="d-flex justify-content-between align-items-center mb-3"> <div className="d-flex justify-content-between align-items-center mb-3">
<div> <div>
<h1 className="h4 mb-1">Circuit Tree Editor</h1> <h1 className="h4 mb-1">Stromkreisliste</h1>
<p className="text-secondary mb-0">Basic editing for the circuit-first model.</p> <p className="text-secondary mb-0">Stromkreise und zugeordnete Geräte bearbeiten.</p>
</div> </div>
<div className="d-flex gap-2"> <div className="d-flex gap-2">
<Link <Link
href={`/projects/${params.projectId}/circuit-lists/${params.circuitListId}/tree`} href={`/projects/${params.projectId}/circuit-lists/${params.circuitListId}/tree`}
className="btn btn-outline-secondary btn-sm" className="btn btn-outline-secondary btn-sm"
> >
Read-only tree Strukturvorschau
</Link>
<Link href={`/projects/${params.projectId}`} className="btn btn-outline-secondary btn-sm">
Zum Projekt
</Link> </Link>
<Link href={`/projects/${params.projectId}/circuit-lists`} className="btn btn-outline-secondary btn-sm"> <Link href={`/projects/${params.projectId}/circuit-lists`} className="btn btn-outline-secondary btn-sm">
Legacy editor Legacy-Editor
</Link> </Link>
</div> </div>
</div> </div>
+40 -5
View File
@@ -11,6 +11,7 @@ import {
createProjectDevice, createProjectDevice,
createRoom, createRoom,
deleteProjectDevice, deleteProjectDevice,
listCircuitLists,
listDistributionBoards, listDistributionBoards,
listFloors, listFloors,
listGlobalDevices, listGlobalDevices,
@@ -21,6 +22,7 @@ import {
updateProjectSettings, updateProjectSettings,
} from "../../../frontend/utils/api"; } from "../../../frontend/utils/api";
import type { import type {
CircuitListDto,
CreateProjectDeviceInput, CreateProjectDeviceInput,
DistributionBoardDto, DistributionBoardDto,
FloorDto, FloorDto,
@@ -56,6 +58,7 @@ export default function ProjectDetailPage() {
const [projectId, setProjectId] = useState(""); const [projectId, setProjectId] = useState("");
const [project, setProject] = useState<ProjectDto | null>(null); const [project, setProject] = useState<ProjectDto | null>(null);
const [boards, setBoards] = useState<DistributionBoardDto[]>([]); const [boards, setBoards] = useState<DistributionBoardDto[]>([]);
const [circuitLists, setCircuitLists] = useState<CircuitListDto[]>([]);
const [floors, setFloors] = useState<FloorDto[]>([]); const [floors, setFloors] = useState<FloorDto[]>([]);
const [rooms, setRooms] = useState<RoomDto[]>([]); const [rooms, setRooms] = useState<RoomDto[]>([]);
const [projectDevices, setProjectDevices] = useState<ProjectDeviceDto[]>([]); const [projectDevices, setProjectDevices] = useState<ProjectDeviceDto[]>([]);
@@ -94,12 +97,21 @@ export default function ProjectDetailPage() {
Promise.all([ Promise.all([
listProjects(), listProjects(),
listDistributionBoards(projectId), listDistributionBoards(projectId),
listCircuitLists(projectId),
listFloors(projectId), listFloors(projectId),
listRooms(projectId), listRooms(projectId),
listProjectDevices(projectId), listProjectDevices(projectId),
listGlobalDevices(), listGlobalDevices(),
]) ])
.then(([projects, distributionBoards, loadedFloors, loadedRooms, loadedProjectDevices, loadedGlobalDevices]) => { .then(([
projects,
distributionBoards,
loadedCircuitLists,
loadedFloors,
loadedRooms,
loadedProjectDevices,
loadedGlobalDevices,
]) => {
const currentProject = projects.find((item) => item.id === projectId) ?? null; const currentProject = projects.find((item) => item.id === projectId) ?? null;
setProject(currentProject); setProject(currentProject);
if (currentProject) { if (currentProject) {
@@ -107,6 +119,7 @@ export default function ProjectDetailPage() {
setThreePhaseVoltageV(String(currentProject.threePhaseVoltageV)); setThreePhaseVoltageV(String(currentProject.threePhaseVoltageV));
} }
setBoards(distributionBoards); setBoards(distributionBoards);
setCircuitLists(loadedCircuitLists);
setFloors(loadedFloors); setFloors(loadedFloors);
setRooms(loadedRooms); setRooms(loadedRooms);
setProjectDevices(loadedProjectDevices); setProjectDevices(loadedProjectDevices);
@@ -123,6 +136,10 @@ export default function ProjectDetailPage() {
const roomCount = useMemo(() => rooms.length, [rooms.length]); const roomCount = useMemo(() => rooms.length, [rooms.length]);
const projectDeviceCount = useMemo(() => projectDevices.length, [projectDevices.length]); const projectDeviceCount = useMemo(() => projectDevices.length, [projectDevices.length]);
const floorById = useMemo(() => new Map(floors.map((item) => [item.id, item])), [floors]); const floorById = useMemo(() => new Map(floors.map((item) => [item.id, item])), [floors]);
const circuitListByBoardId = useMemo(
() => new Map(circuitLists.map((circuitList) => [circuitList.distributionBoardId, circuitList])),
[circuitLists]
);
async function handleCreateBoard(event: FormEvent<HTMLFormElement>) { async function handleCreateBoard(event: FormEvent<HTMLFormElement>) {
event.preventDefault(); event.preventDefault();
@@ -134,6 +151,7 @@ export default function ProjectDetailPage() {
try { try {
const created = await createDistributionBoard(projectId, boardName.trim()); const created = await createDistributionBoard(projectId, boardName.trim());
setBoards((current) => [...current, created]); setBoards((current) => [...current, created]);
setCircuitLists(await listCircuitLists(projectId));
setBoardName(""); setBoardName("");
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : "Verteilung konnte nicht erstellt werden."); setError(err instanceof Error ? err.message : "Verteilung konnte nicht erstellt werden.");
@@ -415,19 +433,36 @@ export default function ProjectDetailPage() {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{boards.map((board) => ( {boards.map((board) => {
const circuitList = circuitListByBoardId.get(board.id);
return (
<tr key={board.id}> <tr key={board.id}>
<td>{board.name}</td> <td>{board.name}</td>
<td className="text-end"> <td className="text-end">
<div className="d-inline-flex gap-2">
{circuitList ? (
<Link <Link
className="btn btn-sm btn-outline-primary" className="btn btn-sm btn-primary"
href={`/projects/${projectId}/circuit-lists?boardId=${board.id}`} href={`/projects/${projectId}/circuit-lists/${circuitList.id}/tree-edit`}
> >
Stromkreisliste öffnen Stromkreisliste öffnen
</Link> </Link>
) : (
<span className="text-secondary small align-self-center">
Keine Stromkreisliste vorhanden
</span>
)}
<Link
className="btn btn-sm btn-outline-secondary"
href={`/projects/${projectId}/circuit-lists?boardId=${board.id}`}
>
Legacy
</Link>
</div>
</td> </td>
</tr> </tr>
))} );
})}
{!boards.length ? ( {!boards.length ? (
<tr> <tr>
<td colSpan={2} className="text-center text-secondary py-4"> <td colSpan={2} className="text-center text-secondary py-4">