from typing import List, Optional from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from sqlalchemy.orm import Session from .. import models from ..database import get_db router = APIRouter(tags=["formboard"]) # ── Helpers ─────────────────────────────────────────────────────────────────── def _get_or_create(diagram_id: int, db: Session) -> models.FormboardLayout: layout = db.query(models.FormboardLayout).filter_by(diagram_id=diagram_id).first() if not layout: layout = models.FormboardLayout(diagram_id=diagram_id) db.add(layout) db.commit() db.refresh(layout) return layout def _nd(n: models.FormboardNode) -> dict: return { "id": n.id, "layout_id": n.layout_id, "node_type": n.node_type, "device_id": n.device_id, "x": n.x, "y": n.y, "label": n.label, "notes": n.notes, } def _sd(s: models.FormboardSegment) -> dict: return { "id": s.id, "layout_id": s.layout_id, "from_node_id": s.from_node_id, "to_node_id": s.to_node_id, "waypoints": s.waypoints or [], "wire_ids": s.wire_ids or [], "fitting_type": s.fitting_type, "fitting_color": s.fitting_color, "label": s.label, "length_mm": s.length_mm, } # ── Endpoints ───────────────────────────────────────────────────────────────── @router.get("/{diagram_id}") def get_layout(diagram_id: int, db: Session = Depends(get_db)): layout = _get_or_create(diagram_id, db) return { "id": layout.id, "diagram_id": layout.diagram_id, "nodes": [_nd(n) for n in layout.nodes], "segments": [_sd(s) for s in layout.segments], } @router.post("/{diagram_id}/sync") def sync_from_diagram(diagram_id: int, db: Session = Depends(get_db)): """Import all devices from the main diagram as connector nodes. Existing nodes (matched by device_id) are label-synced but not moved.""" diagram = db.query(models.Diagram).filter_by(id=diagram_id).first() if not diagram: raise HTTPException(404, "Diagram not found") layout = _get_or_create(diagram_id, db) existing_dev_ids = {n.device_id for n in layout.nodes if n.device_id is not None} added = 0 x = 80 for dev in diagram.devices: if dev.id in existing_dev_ids: # Keep label in sync if the node label is still the default node = next((n for n in layout.nodes if n.device_id == dev.id), None) if node: new_label = dev.reference or dev.label or f"Dev {dev.id}" if not node.label or node.label == f"Dev {dev.id}": node.label = new_label continue node = models.FormboardNode( layout_id=layout.id, node_type="connector", device_id=dev.id, x=x, y=180, label=dev.reference or dev.label or f"Dev {dev.id}", ) db.add(node) x += 200 added += 1 db.commit() db.refresh(layout) return { "added": added, "nodes": [_nd(n) for n in layout.nodes], "segments": [_sd(s) for s in layout.segments], } # ── Nodes ───────────────────────────────────────────────────────────────────── class NodeCreate(BaseModel): node_type: str = "branch" x: float = 100 y: float = 100 label: str = "" notes: str = "" device_id: Optional[int] = None class NodeUpdate(BaseModel): x: Optional[float] = None y: Optional[float] = None label: Optional[str] = None notes: Optional[str] = None @router.post("/{diagram_id}/nodes", status_code=201) def create_node(diagram_id: int, body: NodeCreate, db: Session = Depends(get_db)): layout = _get_or_create(diagram_id, db) node = models.FormboardNode( layout_id=layout.id, node_type=body.node_type, device_id=body.device_id, x=body.x, y=body.y, label=body.label, notes=body.notes, ) db.add(node) db.commit() db.refresh(node) return _nd(node) @router.patch("/nodes/{node_id}") def update_node(node_id: int, body: NodeUpdate, db: Session = Depends(get_db)): node = db.query(models.FormboardNode).filter_by(id=node_id).first() if not node: raise HTTPException(404) if body.x is not None: node.x = body.x if body.y is not None: node.y = body.y if body.label is not None: node.label = body.label if body.notes is not None: node.notes = body.notes db.commit() db.refresh(node) return _nd(node) @router.delete("/nodes/{node_id}", status_code=204) def delete_node(node_id: int, db: Session = Depends(get_db)): node = db.query(models.FormboardNode).filter_by(id=node_id).first() if not node: raise HTTPException(404) # Cascade-delete connected segments db.query(models.FormboardSegment).filter( (models.FormboardSegment.from_node_id == node_id) | (models.FormboardSegment.to_node_id == node_id) ).delete(synchronize_session=False) db.delete(node) db.commit() # ── Segments ────────────────────────────────────────────────────────────────── class SegCreate(BaseModel): from_node_id: int to_node_id: int wire_ids: List[int] = [] fitting_type: str = "open" fitting_color: str = "#888888" label: str = "" length_mm: Optional[float] = None class SegUpdate(BaseModel): waypoints: Optional[List] = None wire_ids: Optional[List[int]] = None fitting_type: Optional[str] = None fitting_color: Optional[str] = None label: Optional[str] = None length_mm: Optional[float] = None @router.post("/{diagram_id}/segments", status_code=201) def create_segment(diagram_id: int, body: SegCreate, db: Session = Depends(get_db)): layout = _get_or_create(diagram_id, db) seg = models.FormboardSegment( layout_id=layout.id, from_node_id=body.from_node_id, to_node_id=body.to_node_id, wire_ids=body.wire_ids, fitting_type=body.fitting_type, fitting_color=body.fitting_color, label=body.label, length_mm=body.length_mm, ) db.add(seg) db.commit() db.refresh(seg) return _sd(seg) @router.patch("/segments/{seg_id}") def update_segment(seg_id: int, body: SegUpdate, db: Session = Depends(get_db)): seg = db.query(models.FormboardSegment).filter_by(id=seg_id).first() if not seg: raise HTTPException(404) if body.waypoints is not None: seg.waypoints = body.waypoints if body.wire_ids is not None: seg.wire_ids = body.wire_ids if body.fitting_type is not None: seg.fitting_type = body.fitting_type if body.fitting_color is not None: seg.fitting_color = body.fitting_color if body.label is not None: seg.label = body.label if body.length_mm is not None: seg.length_mm = body.length_mm db.commit() db.refresh(seg) return _sd(seg) @router.delete("/segments/{seg_id}", status_code=204) def delete_segment(seg_id: int, db: Session = Depends(get_db)): seg = db.query(models.FormboardSegment).filter_by(id=seg_id).first() if not seg: raise HTTPException(404) db.delete(seg) db.commit()