Route boards to circuit-first editor
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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) => {
|
||||||
<tr key={board.id}>
|
const circuitList = circuitListByBoardId.get(board.id);
|
||||||
<td>{board.name}</td>
|
return (
|
||||||
<td className="text-end">
|
<tr key={board.id}>
|
||||||
<Link
|
<td>{board.name}</td>
|
||||||
className="btn btn-sm btn-outline-primary"
|
<td className="text-end">
|
||||||
href={`/projects/${projectId}/circuit-lists?boardId=${board.id}`}
|
<div className="d-inline-flex gap-2">
|
||||||
>
|
{circuitList ? (
|
||||||
Stromkreisliste öffnen
|
<Link
|
||||||
</Link>
|
className="btn btn-sm btn-primary"
|
||||||
</td>
|
href={`/projects/${projectId}/circuit-lists/${circuitList.id}/tree-edit`}
|
||||||
</tr>
|
>
|
||||||
))}
|
Stromkreisliste öffnen
|
||||||
|
</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>
|
||||||
|
</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">
|
||||||
|
|||||||
Reference in New Issue
Block a user