Personen-Belegung pro Raum + Stempel-Aggregation

Architekten-Workflow: bei Schulen/Buero/Versammlungsstaetten muss die
Personenzahl pro Raum erfasst werden (SIA: m²/Person als Indikator).

Backend:
- UserString dossier_raum_personen (int)
- _attach_meta + _read_meta + state-emit
- _update_wall raum-Branch akzeptiert "personen" im Patch
- compute_sia_bilanz aggregiert personen-Summe ueber Scope
- Stempel: neue Show-Flag stempel_show_personen (default false) +
  Bilanz-Renderer-Zeile "N Personen"
- Stempel-Stil-Field showPersonen mit dabei

Frontend (RaumProperties):
- Personen-Input (number, min 0) zwischen Funktion + Layout-Builder
- Nur sichtbar bei normalen Raeumen (nicht GF/AGF)
- Live-Anzeige "m²/Person" Suffix wenn area + personen > 0
  → User sieht sofort ob die Belegung sinnvoll ist (SIA-Vergleich)

Frontend (StempelProperties):
- Neuer Show-Toggle "Personen" (default off)
- Live-Vorschau zeigt Personen-Summe wenn aktiv + > 0
- _OFF_BY_DEFAULT Set generalisiert Default-Handling (showCount, showPersonen)
This commit is contained in:
2026-05-27 01:08:58 +02:00
parent 1c3b0f3919
commit e2d66a5d64
2 changed files with 84 additions and 11 deletions
+37 -6
View File
@@ -2722,7 +2722,8 @@ def _attach_meta(obj_attrs, wall_id, type_, geschoss, dicke, uk_over, ok_over,
stempel_show_nf=None, stempel_show_vf=None, stempel_show_nf=None, stempel_show_vf=None,
stempel_show_ff=None, stempel_show_ngf=None, stempel_show_ff=None, stempel_show_ngf=None,
stempel_show_gf=None, stempel_show_agf=None, stempel_show_gf=None, stempel_show_agf=None,
stempel_show_count=None, stempel_show_seps=None, stempel_show_count=None, stempel_show_personen=None,
stempel_show_seps=None,
stempel_stil_id=None, stempel_stil_id=None,
wand_layered=None, wand_layers=None, wand_layer_idx=None, wand_layered=None, wand_layers=None, wand_layer_idx=None,
wand_chain_members=None, wand_chain_members=None,
@@ -2962,6 +2963,7 @@ def _attach_meta(obj_attrs, wall_id, type_, geschoss, dicke, uk_over, ok_over,
(_KEY_STEMPEL_SHOW_GF, stempel_show_gf), (_KEY_STEMPEL_SHOW_GF, stempel_show_gf),
(_KEY_STEMPEL_SHOW_AGF, stempel_show_agf), (_KEY_STEMPEL_SHOW_AGF, stempel_show_agf),
(_KEY_STEMPEL_SHOW_COUNT, stempel_show_count), (_KEY_STEMPEL_SHOW_COUNT, stempel_show_count),
(_KEY_STEMPEL_SHOW_PERS, stempel_show_personen),
(_KEY_STEMPEL_SHOW_SEPS, stempel_show_seps), (_KEY_STEMPEL_SHOW_SEPS, stempel_show_seps),
): ):
if _v is not None: if _v is not None:
@@ -3200,6 +3202,7 @@ def _read_meta(obj):
st_show_gf = _sh_on(_KEY_STEMPEL_SHOW_GF, True) st_show_gf = _sh_on(_KEY_STEMPEL_SHOW_GF, True)
st_show_agf = _sh_on(_KEY_STEMPEL_SHOW_AGF, True) st_show_agf = _sh_on(_KEY_STEMPEL_SHOW_AGF, True)
st_show_count = _sh_on(_KEY_STEMPEL_SHOW_COUNT, False) st_show_count = _sh_on(_KEY_STEMPEL_SHOW_COUNT, False)
st_show_pers = _sh_on(_KEY_STEMPEL_SHOW_PERS, False)
st_show_seps = _sh_on(_KEY_STEMPEL_SHOW_SEPS, True) st_show_seps = _sh_on(_KEY_STEMPEL_SHOW_SEPS, True)
st_stil_id = a.GetUserString(_KEY_STEMPEL_STIL_ID) or "" st_stil_id = a.GetUserString(_KEY_STEMPEL_STIL_ID) or ""
# Field-Layout — parsed JSON list of rows # Field-Layout — parsed JSON list of rows
@@ -3311,6 +3314,7 @@ def _read_meta(obj):
"raum_align": r_align, "raum_align": r_align,
"raum_sia": r_sia, "raum_sia": r_sia,
"raum_fuellung": r_fuell, "raum_fuellung": r_fuell,
"raum_personen": r_personen,
"raum_txt_font": r_font, "raum_txt_font": r_font,
"raum_txt_bold": r_bold, "raum_txt_bold": r_bold,
"raum_txt_italic": r_ital, "raum_txt_italic": r_ital,
@@ -3340,6 +3344,7 @@ def _read_meta(obj):
"stempel_show_gf": st_show_gf, "stempel_show_gf": st_show_gf,
"stempel_show_agf": st_show_agf, "stempel_show_agf": st_show_agf,
"stempel_show_count": st_show_count, "stempel_show_count": st_show_count,
"stempel_show_personen": st_show_pers,
"stempel_show_seps": st_show_seps, "stempel_show_seps": st_show_seps,
"stempel_stil_id": st_stil_id, "stempel_stil_id": st_stil_id,
"wand_layered": w_layered, "wand_layered": w_layered,
@@ -3978,6 +3983,7 @@ _KEY_STEMPEL_SHOW_NGF = "dossier_stempel_show_ngf" # default "1"
_KEY_STEMPEL_SHOW_GF = "dossier_stempel_show_gf" # default "1" _KEY_STEMPEL_SHOW_GF = "dossier_stempel_show_gf" # default "1"
_KEY_STEMPEL_SHOW_AGF = "dossier_stempel_show_agf" # default "1" _KEY_STEMPEL_SHOW_AGF = "dossier_stempel_show_agf" # default "1"
_KEY_STEMPEL_SHOW_COUNT = "dossier_stempel_show_count" # default "0" _KEY_STEMPEL_SHOW_COUNT = "dossier_stempel_show_count" # default "0"
_KEY_STEMPEL_SHOW_PERS = "dossier_stempel_show_pers" # default "0"
_KEY_STEMPEL_SHOW_SEPS = "dossier_stempel_show_seps" # default "1" _KEY_STEMPEL_SHOW_SEPS = "dossier_stempel_show_seps" # default "1"
_KEY_STEMPEL_STIL_ID = "dossier_stempel_stil_id" # aktiver Stil _KEY_STEMPEL_STIL_ID = "dossier_stempel_stil_id" # aktiver Stil
# Storage-Key fuer Stempel-Stile (Presets, per Doc) # Storage-Key fuer Stempel-Stile (Presets, per Doc)
@@ -3985,7 +3991,8 @@ _KEY_STEMPEL_STILE = "dossier_stempel_stile"
_STEMPEL_STIL_FIELDS = ( _STEMPEL_STIL_FIELDS = (
"header", "showScope", "header", "showScope",
"showHnf", "showNnf", "showNf", "showVf", "showFf", "showHnf", "showNnf", "showNf", "showVf", "showFf",
"showNgf", "showGf", "showAgf", "showCount", "showSeparators", "showNgf", "showGf", "showAgf", "showCount", "showPersonen",
"showSeparators",
"font", "bold", "italic", "txtH", "font", "bold", "italic", "txtH",
) )
@@ -5160,7 +5167,7 @@ def compute_sia_bilanz(doc, scope="total"):
laut SIA 416). laut SIA 416).
""" """
out = {"hnf": 0.0, "nnf": 0.0, "vf": 0.0, "ff": 0.0, out = {"hnf": 0.0, "nnf": 0.0, "vf": 0.0, "ff": 0.0,
"gf": 0.0, "agf": 0.0, "count": 0, "gf": 0.0, "agf": 0.0, "count": 0, "personen": 0,
"scope": scope, "geschossName": ""} "scope": scope, "geschossName": ""}
if doc is None: return out if doc is None: return out
target_gid = None target_gid = None
@@ -5181,6 +5188,8 @@ def compute_sia_bilanz(doc, scope="total"):
if sia not in ("hnf", "nnf", "vf", "ff", "gf", "agf"): continue if sia not in ("hnf", "nnf", "vf", "ff", "gf", "agf"): continue
out[sia] += float(area) out[sia] += float(area)
out["count"] += 1 out["count"] += 1
try: out["personen"] += int(m.get("raum_personen", 0) or 0)
except Exception: pass
except Exception: pass except Exception: pass
out["nf"] = out["hnf"] + out["nnf"] out["nf"] = out["hnf"] + out["nnf"]
out["ngf"] = out["nf"] + out["vf"] + out["ff"] out["ngf"] = out["nf"] + out["vf"] + out["ff"]
@@ -5188,7 +5197,8 @@ def compute_sia_bilanz(doc, scope="total"):
def _format_bilanz_lines(bilanz, rundung="0.1", visibility=None, def _format_bilanz_lines(bilanz, rundung="0.1", visibility=None,
show_separators=True, show_count=False): show_separators=True, show_count=False,
show_personen=False):
"""Baut die Stempel-Textzeilen aus einer Bilanz. Zeigt nur Kategorien """Baut die Stempel-Textzeilen aus einer Bilanz. Zeigt nur Kategorien
mit Flaeche > 0 UND deren visibility-Flag True ist. mit Flaeche > 0 UND deren visibility-Flag True ist.
@@ -5231,6 +5241,11 @@ def _format_bilanz_lines(bilanz, rundung="0.1", visibility=None,
if show_count and bilanz.get("count", 0) > 0: if show_count and bilanz.get("count", 0) > 0:
if show_separators and lines: lines.append(sep) if show_separators and lines: lines.append(sep)
lines.append("{} Räume".format(bilanz["count"])) lines.append("{} Räume".format(bilanz["count"]))
# Personen-Total optional (Summe ueber alle Raeume im Scope)
if show_personen and bilanz.get("personen", 0) > 0:
if not (show_count and show_separators) and show_separators and lines:
lines.append(sep)
lines.append("{} Personen".format(bilanz["personen"]))
return lines return lines
@@ -5238,6 +5253,7 @@ def _make_stempel_text(pos, scope, doc, text_height=0.20, z=0.0,
font=None, bold=False, italic=False, font=None, bold=False, italic=False,
header_text="Nutzflächen", show_scope=True, header_text="Nutzflächen", show_scope=True,
show_separators=True, show_count=False, show_separators=True, show_count=False,
show_personen=False,
visibility=None): visibility=None):
"""Baut die Stempel-TextEntity fuer einen Scope ("total" oder """Baut die Stempel-TextEntity fuer einen Scope ("total" oder
"geschoss:<id>"). header_text + show_* steuern Layout-Customisation. "geschoss:<id>"). header_text + show_* steuern Layout-Customisation.
@@ -5262,7 +5278,8 @@ def _make_stempel_text(pos, scope, doc, text_height=0.20, z=0.0,
header = " · ".join(header_parts) if header_parts else "" header = " · ".join(header_parts) if header_parts else ""
body = _format_bilanz_lines(bilanz, visibility=visibility, body = _format_bilanz_lines(bilanz, visibility=visibility,
show_separators=show_separators, show_separators=show_separators,
show_count=show_count) show_count=show_count,
show_personen=show_personen)
if not body: if not body:
body = ["(keine klassifizierten Räume)"] body = ["(keine klassifizierten Räume)"]
lines = [] lines = []
@@ -6771,6 +6788,7 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name
show_scope=bool(meta.get("stempel_show_scope", True)), show_scope=bool(meta.get("stempel_show_scope", True)),
show_separators=bool(meta.get("stempel_show_seps", True)), show_separators=bool(meta.get("stempel_show_seps", True)),
show_count=bool(meta.get("stempel_show_count", False)), show_count=bool(meta.get("stempel_show_count", False)),
show_personen=bool(meta.get("stempel_show_personen", False)),
visibility=visibility) visibility=visibility)
if new_te is None: return False if new_te is None: return False
# In-place replace # In-place replace
@@ -7144,6 +7162,7 @@ class ElementeBridge(panel_base.BaseBridge):
"layout": meta.get("raum_layout") or [], "layout": meta.get("raum_layout") or [],
"txtModus": meta.get("raum_txt_modus", "fix"), "txtModus": meta.get("raum_txt_modus", "fix"),
"stilId": meta.get("raum_stil_id", ""), "stilId": meta.get("raum_stil_id", ""),
"personen": int(meta.get("raum_personen", 0) or 0),
"area": area, "area": area,
"areaFmt": _format_area(area, rnd_eff), "areaFmt": _format_area(area, rnd_eff),
"umfang": perim, "umfang": perim,
@@ -7174,6 +7193,7 @@ class ElementeBridge(panel_base.BaseBridge):
"showGf": bool(meta.get("stempel_show_gf", True)), "showGf": bool(meta.get("stempel_show_gf", True)),
"showAgf": bool(meta.get("stempel_show_agf", True)), "showAgf": bool(meta.get("stempel_show_agf", True)),
"showCount": bool(meta.get("stempel_show_count", False)), "showCount": bool(meta.get("stempel_show_count", False)),
"showPersonen": bool(meta.get("stempel_show_personen", False)),
"showSeparators": bool(meta.get("stempel_show_seps", True)), "showSeparators": bool(meta.get("stempel_show_seps", True)),
"stilId": meta.get("stempel_stil_id", ""), "stilId": meta.get("stempel_stil_id", ""),
"bilanz": bilanz, "bilanz": bilanz,
@@ -11015,6 +11035,13 @@ class ElementeBridge(panel_base.BaseBridge):
old_meta.get("raum_show_area", True))) old_meta.get("raum_show_area", True)))
r_show_sia = bool(p.get("showSia", r_show_sia = bool(p.get("showSia",
old_meta.get("raum_show_sia", False))) old_meta.get("raum_show_sia", False)))
# Personen-Belegung (int, default 0)
try:
r_personen = int(p.get("personen",
old_meta.get("raum_personen", 0)))
except Exception:
r_personen = int(old_meta.get("raum_personen", 0) or 0)
if r_personen < 0: r_personen = 0
# Layout: Liste-of-Rows aus Frontend (JSON-serializable). Wenn # Layout: Liste-of-Rows aus Frontend (JSON-serializable). Wenn
# nicht im Patch → vom alten meta uebernehmen. # nicht im Patch → vom alten meta uebernehmen.
r_layout = p.get("layout", old_meta.get("raum_layout", [])) r_layout = p.get("layout", old_meta.get("raum_layout", []))
@@ -11072,7 +11099,8 @@ class ElementeBridge(panel_base.BaseBridge):
raum_show_area=r_show_are, raum_show_area=r_show_are,
raum_show_sia=r_show_sia, raum_show_sia=r_show_sia,
raum_layout=r_layout, raum_layout=r_layout,
raum_txt_modus=r_modus) raum_txt_modus=r_modus,
raum_personen=r_personen)
axis_obj.Attributes = attrs axis_obj.Attributes = attrs
axis_obj.CommitChanges() axis_obj.CommitChanges()
_save_last(raum_name_last=r_name, raum_rundung=r_rnd, _save_last(raum_name_last=r_name, raum_rundung=r_rnd,
@@ -11123,6 +11151,8 @@ class ElementeBridge(panel_base.BaseBridge):
old_meta.get("stempel_show_agf", True))) old_meta.get("stempel_show_agf", True)))
st_show_count = bool(p.get("showCount", st_show_count = bool(p.get("showCount",
old_meta.get("stempel_show_count", False))) old_meta.get("stempel_show_count", False)))
st_show_pers = bool(p.get("showPersonen",
old_meta.get("stempel_show_personen", False)))
st_show_seps = bool(p.get("showSeparators", st_show_seps = bool(p.get("showSeparators",
old_meta.get("stempel_show_seps", True))) old_meta.get("stempel_show_seps", True)))
attrs = axis_obj.Attributes attrs = axis_obj.Attributes
@@ -11142,6 +11172,7 @@ class ElementeBridge(panel_base.BaseBridge):
stempel_show_gf=st_show_gf, stempel_show_gf=st_show_gf,
stempel_show_agf=st_show_agf, stempel_show_agf=st_show_agf,
stempel_show_count=st_show_count, stempel_show_count=st_show_count,
stempel_show_personen=st_show_pers,
stempel_show_seps=st_show_seps) stempel_show_seps=st_show_seps)
axis_obj.Attributes = attrs axis_obj.Attributes = attrs
axis_obj.CommitChanges() axis_obj.CommitChanges()
+47 -5
View File
@@ -937,6 +937,7 @@ function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns, fo
const [name, setName] = useState(raum.name || 'Raum') const [name, setName] = useState(raum.name || 'Raum')
const [nummer, setNummer] = useState(raum.nummer || '') const [nummer, setNummer] = useState(raum.nummer || '')
const [funktion, setFunktion] = useState(raum.funktion || '') const [funktion, setFunktion] = useState(raum.funktion || '')
const [personenStr, setPersonenStr] = useState(String(raum.personen || 0))
// Texthoehe (raum.txtH) + Ausrichtung (raum.align) werden via Oberleiste // Texthoehe (raum.txtH) + Ausrichtung (raum.align) werden via Oberleiste
// gesetzt — kein Local-State noetig, Stil-Speichern liest direkt aus raum. // gesetzt — kein Local-State noetig, Stil-Speichern liest direkt aus raum.
const txtModus = raum.txtModus || 'fix' const txtModus = raum.txtModus || 'fix'
@@ -948,7 +949,8 @@ function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns, fo
setName(raum.name || 'Raum') setName(raum.name || 'Raum')
setNummer(raum.nummer || '') setNummer(raum.nummer || '')
setFunktion(raum.funktion || '') setFunktion(raum.funktion || '')
}, [raum.id, raum.name, raum.nummer, raum.funktion]) setPersonenStr(String(raum.personen || 0))
}, [raum.id, raum.name, raum.nummer, raum.funktion, raum.personen])
// Aktueller Wert von raum_fuellung: "" | "Solid" | "Hatch1" | … | "ByLayer" // Aktueller Wert von raum_fuellung: "" | "Solid" | "Hatch1" | … | "ByLayer"
const fuell = raum.fuellung || '' const fuell = raum.fuellung || ''
@@ -1133,6 +1135,33 @@ function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns, fo
</div> </div>
)} )}
{/* Personen-Belegung (SIA: Schulen, Bueros, Versammlungen) —
aggregiert im Bilanz-Stempel als "Personen"-Zeile wenn aktiv. */}
{!isFlaeche && (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 60 }}>Personen</span>
<input type="number" min="0" step="1" value={personenStr}
onChange={(e) => setPersonenStr(e.target.value)}
onBlur={() => {
const n = parseInt(personenStr, 10)
const v = Number.isFinite(n) && n >= 0 ? n : 0
if (v !== (raum.personen || 0)) onUpdate({ personen: v })
setPersonenStr(String(v))
}}
onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }}
placeholder="0"
title="Anzahl Personen — wird im Bilanz-Stempel summiert (m²/Person nach SIA)"
style={{ width: 64, fontSize: 11, textAlign: 'right',
fontFamily: 'DM Mono, monospace' }} />
{raum.area > 0 && raum.personen > 0 && (
<span style={{ fontSize: 9, color: 'var(--text-muted)',
fontFamily: 'DM Mono, monospace' }}>
{(raum.area / raum.personen).toFixed(1)} /Person
</span>
)}
</div>
)}
{/* Stempel-Layout — Drag-and-Drop. Jede Row ist eine Textzeile, {/* Stempel-Layout — Drag-and-Drop. Jede Row ist eine Textzeile,
Felder innerhalb einer Row landen in derselben Zeile. Drag Felder innerhalb einer Row landen in derselben Zeile. Drag
zwischen Rows um umzuordnen. Klick auf Field oben fügt es in zwischen Rows um umzuordnen. Klick auf Field oben fügt es in
@@ -1185,7 +1214,8 @@ function StempelProperties({ stempel, geschosse, stempelStile, onUpdate, onDelet
setHeaderDraft(stempel.header || 'Nutzflächen') setHeaderDraft(stempel.header || 'Nutzflächen')
}, [stempel.id, stempel.header]) }, [stempel.id, stempel.header])
// Show-Flags vom Backend (default true ausser showCount) // Show-Flags vom Backend default true ausser folgende:
const _OFF_BY_DEFAULT = new Set(['showCount', 'showPersonen'])
const _show = (k, def = true) => const _show = (k, def = true) =>
(stempel[k] !== undefined ? !!stempel[k] : def) (stempel[k] !== undefined ? !!stempel[k] : def)
@@ -1205,6 +1235,7 @@ function StempelProperties({ stempel, geschosse, stempelStile, onUpdate, onDelet
showGf: _show('showGf'), showGf: _show('showGf'),
showAgf: _show('showAgf'), showAgf: _show('showAgf'),
showCount: _show('showCount', false), showCount: _show('showCount', false),
showPersonen: _show('showPersonen', false),
showSeparators: _show('showSeparators'), showSeparators: _show('showSeparators'),
font: stempel.font || '', font: stempel.font || '',
bold: !!stempel.bold, bold: !!stempel.bold,
@@ -1239,9 +1270,9 @@ function StempelProperties({ stempel, geschosse, stempelStile, onUpdate, onDelet
const ShowTog = ({ field, label, hint }) => ( const ShowTog = ({ field, label, hint }) => (
<BarToggle label={label} <BarToggle label={label}
icon={_show(field, field === 'showCount' ? false : true) ? 'check_box' : 'check_box_outline_blank'} icon={_show(field, _OFF_BY_DEFAULT.has(field) ? false : true) ? 'check_box' : 'check_box_outline_blank'}
active={_show(field, field === 'showCount' ? false : true)} active={_show(field, _OFF_BY_DEFAULT.has(field) ? false : true)}
onClick={() => onUpdate({ [field]: !_show(field, field === 'showCount' ? false : true) })} onClick={() => onUpdate({ [field]: !_show(field, _OFF_BY_DEFAULT.has(field) ? false : true) })}
title={hint || label} /> title={hint || label} />
) )
@@ -1327,6 +1358,7 @@ function StempelProperties({ stempel, geschosse, stempelStile, onUpdate, onDelet
<ShowTog field="showGf" label="GF" /> <ShowTog field="showGf" label="GF" />
<ShowTog field="showAgf" label="AGF" /> <ShowTog field="showAgf" label="AGF" />
<ShowTog field="showCount" label="Anzahl" hint="Anzahl klassifizierter Räume" /> <ShowTog field="showCount" label="Anzahl" hint="Anzahl klassifizierter Räume" />
<ShowTog field="showPersonen" label="Personen" hint="Summe der Personen-Belegung (SIA)" />
<ShowTog field="showSeparators" label="Linien" hint="Trennlinien zwischen Sections" /> <ShowTog field="showSeparators" label="Linien" hint="Trennlinien zwischen Sections" />
</div> </div>
</div> </div>
@@ -1357,6 +1389,16 @@ function StempelProperties({ stempel, geschosse, stempelStile, onUpdate, onDelet
<span>{r.val.toFixed(1)} </span> <span>{r.val.toFixed(1)} </span>
</div> </div>
))} ))}
{_show('showPersonen', false) && (bilanz.personen || 0) > 0 && (
<div style={{ display: 'flex', justifyContent: 'space-between',
padding: '2px 0',
borderTop: '1px dashed var(--border-light)',
marginTop: 2 }}
title="Summe Personen über alle Räume im Scope">
<span>Personen</span>
<span>{bilanz.personen}</span>
</div>
)}
</div> </div>
<div style={{ <div style={{