Add organization tenant settings

This commit is contained in:
2026-07-10 17:54:14 +02:00
parent ad2561b50f
commit becbc8dd04
11 changed files with 830 additions and 79 deletions

View File

@@ -15,6 +15,7 @@ from govoplan_organizations.backend.db.models import (
OrganizationFunctionType,
OrganizationRelation,
OrganizationRelationType,
OrganizationTenantSettings,
OrganizationStructure,
OrganizationUnit,
OrganizationUnitType,
@@ -33,6 +34,8 @@ from .schemas import (
OrganizationModelResponse,
OrganizationRelationItem,
OrganizationRelationTypeItem,
OrganizationSettingsItem,
OrganizationSettingsUpdateRequest,
OrganizationStructureItem,
OrganizationUnitItem,
OrganizationUnitTypeItem,
@@ -61,6 +64,8 @@ ORG_MODEL_WRITE_SCOPES = ("organizations:model:write",)
ORG_UNIT_WRITE_SCOPES = ("organizations:unit:write",)
ORG_FUNCTION_WRITE_SCOPES = ("organizations:function:write",)
ORG_ASSIGN_SCOPES = ("organizations:function:assign",)
ORG_SETTINGS_READ_SCOPES = ("organizations:settings:read", "organizations:model:read", "admin:settings:read")
ORG_SETTINGS_WRITE_SCOPES = ("organizations:settings:write", "admin:settings:write")
SLUG_RE = re.compile(r"[^a-z0-9]+")
ModelT = TypeVar("ModelT")
@@ -129,6 +134,21 @@ def _item_unit_type(item: OrganizationUnitType) -> OrganizationUnitTypeItem:
return OrganizationUnitTypeItem(**_row_fields(item))
def _item_settings(item: OrganizationTenantSettings) -> OrganizationSettingsItem:
return OrganizationSettingsItem(**_row_fields(item))
def _default_settings(tenant_id: str) -> OrganizationSettingsItem:
return OrganizationSettingsItem(
tenant_id=tenant_id,
allow_tenant_model_customization=True,
require_model_change_requests=False,
audit_detail_level="standard",
change_retention_days=None,
settings={},
)
def _item_structure(item: OrganizationStructure) -> OrganizationStructureItem:
return OrganizationStructureItem(**_row_fields(item))
@@ -187,6 +207,48 @@ def _apply_slugged_update(session: Session, item: object, payload: object, tenan
setattr(item, "settings", value)
@router.get("/settings", response_model=OrganizationSettingsItem)
def get_organization_settings(
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(require_any_scope(*ORG_SETTINGS_READ_SCOPES)),
) -> OrganizationSettingsItem:
tenant_id = _tenant_id(principal)
item = session.query(OrganizationTenantSettings).filter(OrganizationTenantSettings.tenant_id == tenant_id).one_or_none()
return _item_settings(item) if item is not None else _default_settings(tenant_id)
@router.patch("/settings", response_model=OrganizationSettingsItem)
def update_organization_settings(
payload: OrganizationSettingsUpdateRequest,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(require_any_scope(*ORG_SETTINGS_WRITE_SCOPES)),
) -> OrganizationSettingsItem:
tenant_id = _tenant_id(principal)
item = session.query(OrganizationTenantSettings).filter(OrganizationTenantSettings.tenant_id == tenant_id).one_or_none()
if item is None:
item = OrganizationTenantSettings(
tenant_id=tenant_id,
allow_tenant_model_customization=True,
require_model_change_requests=False,
audit_detail_level="standard",
change_retention_days=None,
settings={},
)
session.add(item)
fields = payload.model_fields_set
for field in ("allow_tenant_model_customization", "require_model_change_requests", "audit_detail_level", "change_retention_days"):
if field in fields:
value = getattr(payload, field)
if field != "change_retention_days" and value is None:
raise _invalid(f"{field} cannot be empty.")
setattr(item, field, value)
if "settings" in fields:
if payload.settings is None:
raise _invalid("Settings cannot be empty.")
item.settings = payload.settings
return _item_settings(_commit(session, item))
@router.get("/model", response_model=OrganizationModelResponse)
def get_organization_model(
session: Session = Depends(get_session),
@@ -356,6 +418,8 @@ def update_unit(
raise _invalid("An organization unit cannot be its own parent.")
if payload.parent_id is not None:
_get_tenant_row(session, OrganizationUnit, payload.parent_id, tenant_id, "Parent organization unit")
if _would_create_parent_cycle(session, tenant_id, item.id, payload.parent_id):
raise _invalid("This parent assignment would create an organization unit cycle.")
item.parent_id = payload.parent_id
return _item_unit(_commit(session, item))
@@ -718,3 +782,17 @@ def _would_create_cycle(
query = query.filter(OrganizationRelation.id != exclude_relation_id)
pending.extend(row[0] for row in query.all())
return False
def _would_create_parent_cycle(session: Session, tenant_id: str, unit_id: str, parent_id: str) -> bool:
current: str | None = parent_id
seen: set[str] = set()
while current is not None:
if current == unit_id or current in seen:
return True
seen.add(current)
current = session.query(OrganizationUnit.parent_id).filter(
OrganizationUnit.tenant_id == tenant_id,
OrganizationUnit.id == current,
).scalar()
return False

View File

@@ -7,6 +7,27 @@ from pydantic import BaseModel, Field
StructureKind = Literal["hierarchy", "network", "membership", "classification"]
AuditDetailLevel = Literal["summary", "standard", "full"]
class OrganizationSettingsItem(BaseModel):
id: str | None = None
tenant_id: str
allow_tenant_model_customization: bool
require_model_change_requests: bool
audit_detail_level: AuditDetailLevel
change_retention_days: int | None = None
settings: dict[str, Any]
created_at: datetime | None = None
updated_at: datetime | None = None
class OrganizationSettingsUpdateRequest(BaseModel):
allow_tenant_model_customization: bool | None = None
require_model_change_requests: bool | None = None
audit_detail_level: AuditDetailLevel | None = None
change_retention_days: int | None = Field(default=None, ge=0)
settings: dict[str, Any] | None = None
class OrganizationUnitTypeItem(BaseModel):

View File

@@ -4,7 +4,7 @@ import uuid
from datetime import datetime
from typing import Any
from sqlalchemy import Boolean, DateTime, ForeignKey, Index, JSON, String, Text, UniqueConstraint
from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Integer, JSON, String, Text, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column
from govoplan_core.db.base import Base, TimestampMixin
@@ -42,6 +42,19 @@ class OrganizationUnitType(Base, TimestampMixin):
settings: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
class OrganizationTenantSettings(Base, TimestampMixin):
__tablename__ = "organizations_tenant_settings"
__table_args__ = (UniqueConstraint("tenant_id", name="uq_organizations_tenant_settings_tenant"),)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
allow_tenant_model_customization: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
require_model_change_requests: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
audit_detail_level: Mapped[str] = mapped_column(String(30), default="standard", nullable=False)
change_retention_days: Mapped[int | None] = mapped_column(Integer, nullable=True)
settings: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
class OrganizationStructure(Base, TimestampMixin):
__tablename__ = "organizations_structures"
__table_args__ = (UniqueConstraint("tenant_id", "slug", name="uq_organizations_structures_tenant_slug"),)
@@ -172,6 +185,7 @@ __all__ = [
"OrganizationRelation",
"OrganizationRelationType",
"OrganizationStructure",
"OrganizationTenantSettings",
"OrganizationUnit",
"OrganizationUnitType",
"new_uuid",

View File

@@ -46,6 +46,8 @@ def _permission(scope: str, label: str, description: str) -> PermissionDefinitio
PERMISSIONS = (
_permission("organizations:model:read", "View organization model", "Read organization meta-model definitions such as unit types, structures, and relation types."),
_permission("organizations:model:write", "Manage organization model", "Create and edit organization meta-model definitions."),
_permission("organizations:settings:read", "View organization settings", "Read organization governance, audit, and retention settings."),
_permission("organizations:settings:write", "Manage organization settings", "Edit organization governance, audit, and retention settings."),
_permission("organizations:unit:read", "View organization units", "Read concrete organization units and relations."),
_permission("organizations:unit:write", "Manage organization units", "Create and edit concrete organization units and relations."),
_permission("organizations:function:read", "View organization functions", "Read function definitions and identity-held assignments."),
@@ -64,7 +66,7 @@ ROLE_TEMPLATES = (
slug="organization_viewer",
name="Organization viewer",
description="Read organization model, organization units, and function assignments.",
permissions=("organizations:model:read", "organizations:unit:read", "organizations:function:read"),
permissions=("organizations:model:read", "organizations:settings:read", "organizations:unit:read", "organizations:function:read"),
),
)
@@ -112,6 +114,7 @@ manifest = ModuleManifest(
uninstall_guard_providers=(
persistent_table_uninstall_guard(
organization_models.OrganizationUnitType,
organization_models.OrganizationTenantSettings,
organization_models.OrganizationStructure,
organization_models.OrganizationRelationType,
organization_models.OrganizationRelation,

View File

@@ -71,6 +71,24 @@ def _create_unit_types() -> None:
_create_index_if_missing(op.f("ix_organizations_unit_types_tenant_id"), "organizations_unit_types", ["tenant_id"])
def _create_tenant_settings() -> None:
if "organizations_tenant_settings" not in _tables():
op.create_table(
"organizations_tenant_settings",
sa.Column("id", sa.String(length=36), nullable=False),
sa.Column("tenant_id", sa.String(length=36), nullable=False),
sa.Column("allow_tenant_model_customization", sa.Boolean(), nullable=False),
sa.Column("require_model_change_requests", sa.Boolean(), nullable=False),
sa.Column("audit_detail_level", sa.String(length=30), nullable=False),
sa.Column("change_retention_days", sa.Integer(), nullable=True),
_json_column("settings"),
*_timestamp_columns(),
sa.PrimaryKeyConstraint("id", name=op.f("pk_organizations_tenant_settings")),
sa.UniqueConstraint("tenant_id", name="uq_organizations_tenant_settings_tenant"),
)
_create_index_if_missing(op.f("ix_organizations_tenant_settings_tenant_id"), "organizations_tenant_settings", ["tenant_id"])
def _create_structures() -> None:
if "organizations_structures" not in _tables():
op.create_table(
@@ -271,6 +289,7 @@ def _create_function_assignments() -> None:
def upgrade() -> None:
_create_unit_types()
_create_tenant_settings()
_create_structures()
_create_relation_types()
_create_units()
@@ -293,6 +312,7 @@ def downgrade() -> None:
"organizations_relation_types",
"organizations_structures",
"organizations_function_types",
"organizations_tenant_settings",
"organizations_unit_types",
):
if table_name in _tables():

View File

@@ -13,6 +13,27 @@ export type OrganizationUnitTypeItem = {
};
export type OrganizationStructureKind = "hierarchy" | "network" | "membership" | "classification";
export type OrganizationAuditDetailLevel = "summary" | "standard" | "full";
export type OrganizationSettingsItem = {
id?: string | null;
tenant_id: string;
allow_tenant_model_customization: boolean;
require_model_change_requests: boolean;
audit_detail_level: OrganizationAuditDetailLevel;
change_retention_days?: number | null;
settings: Record<string, unknown>;
created_at?: string | null;
updated_at?: string | null;
};
export type OrganizationSettingsUpdatePayload = {
allow_tenant_model_customization?: boolean;
require_model_change_requests?: boolean;
audit_detail_level?: OrganizationAuditDetailLevel;
change_retention_days?: number | null;
settings?: Record<string, unknown>;
};
export type OrganizationStructureItem = {
id: string;
@@ -205,6 +226,14 @@ export function getOrganizationModel(settings: ApiSettings): Promise<Organizatio
return apiFetch<OrganizationModel>(settings, "/api/v1/organizations/model");
}
export function getOrganizationSettings(settings: ApiSettings): Promise<OrganizationSettingsItem> {
return apiFetch<OrganizationSettingsItem>(settings, "/api/v1/organizations/settings");
}
export function patchOrganizationSettings(settings: ApiSettings, payload: OrganizationSettingsUpdatePayload): Promise<OrganizationSettingsItem> {
return patch(settings, "/api/v1/organizations/settings", payload);
}
export function createUnitType(settings: ApiSettings, payload: SluggedCreatePayload): Promise<OrganizationUnitTypeItem> {
return post(settings, "/api/v1/organizations/unit-types", payload);
}

View File

@@ -1,18 +1,156 @@
import type { ApiSettings, AuthInfo } from "@govoplan/core-webui";
import OrganizationsPage, { type OrganizationSection } from "./OrganizationsPage";
import { useEffect, useState } from "react";
import {
AdminPageLayout,
Button,
Card,
FormField,
ToggleSwitch,
adminErrorMessage,
hasAnyScope,
useUnsavedDraftGuard,
type ApiSettings,
type AuthInfo
} from "@govoplan/core-webui";
import {
getOrganizationSettings,
patchOrganizationSettings,
type OrganizationAuditDetailLevel,
type OrganizationSettingsItem
} from "../../api/organizations";
const MODEL_SECTIONS: OrganizationSection[] = ["model"];
const FALLBACK_SETTINGS: OrganizationSettingsItem = {
tenant_id: "",
allow_tenant_model_customization: true,
require_model_change_requests: false,
audit_detail_level: "standard",
change_retention_days: null,
settings: {}
};
const AUDIT_DETAIL_LEVELS: Array<{ value: OrganizationAuditDetailLevel; label: string }> = [
{ value: "summary", label: "i18n:govoplan-organizations.audit_summary.6a7d68a5" },
{ value: "standard", label: "i18n:govoplan-organizations.audit_standard.785d981f" },
{ value: "full", label: "i18n:govoplan-organizations.audit_full.0c83669c" }
];
export default function OrganizationsAdminPanel({ settings, auth }: { settings: ApiSettings; auth: AuthInfo }) {
const [draft, setDraft] = useState<OrganizationSettingsItem>(FALLBACK_SETTINGS);
const [savedDraft, setSavedDraft] = useState<OrganizationSettingsItem>(FALLBACK_SETTINGS);
const [loading, setLoading] = useState(true);
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const [success, setSuccess] = useState("");
const canWrite = hasAnyScope(auth, ["organizations:settings:write", "admin:settings:write"]);
const dirty = settingsKey(draft) !== settingsKey(savedDraft);
useUnsavedDraftGuard({
dirty,
onSave: save,
onDiscard: () => setDraft(savedDraft)
});
useEffect(() => {
void load();
}, [settings.accessToken, settings.apiBaseUrl]);
async function load() {
setLoading(true);
setError("");
try {
const next = await getOrganizationSettings(settings);
setDraft(next);
setSavedDraft(next);
} catch (caught) {
setError(adminErrorMessage(caught));
} finally {
setLoading(false);
}
}
async function save(): Promise<boolean> {
setBusy(true);
setError("");
setSuccess("");
try {
const saved = await patchOrganizationSettings(settings, {
allow_tenant_model_customization: draft.allow_tenant_model_customization,
require_model_change_requests: draft.require_model_change_requests,
audit_detail_level: draft.audit_detail_level,
change_retention_days: draft.change_retention_days ?? null,
settings: draft.settings
});
setDraft(saved);
setSavedDraft(saved);
setSuccess("i18n:govoplan-organizations.organization_settings_saved.bfbcfdfa");
return true;
} catch (caught) {
setError(adminErrorMessage(caught));
return false;
} finally {
setBusy(false);
}
}
return (
<OrganizationsPage
settings={settings}
auth={auth}
mode="admin"
initialSection="model"
availableSections={MODEL_SECTIONS}
title="i18n:govoplan-organizations.organization_model.4f924c0e"
description="i18n:govoplan-organizations.organization_model_admin_description.35dc9f10"
/>
<AdminPageLayout
title="i18n:govoplan-organizations.organization_settings.c9ab9829"
description="i18n:govoplan-organizations.organization_settings_description.35dc9f10"
loading={loading}
loadingLabel="i18n:govoplan-organizations.loading_organization_settings.c6008db8"
error={error}
success={success}
actions={<><Button onClick={() => void load()} disabled={loading || busy}>i18n:govoplan-organizations.reload.cce71553</Button><Button variant="primary" onClick={() => void save()} disabled={!canWrite || busy || !dirty}>{busy ? "i18n:govoplan-organizations.saving.56a2285c" : "i18n:govoplan-organizations.save_settings.913aba9f"}</Button></>}
>
<div className="organizations-settings-grid">
<Card title="i18n:govoplan-organizations.model_governance.6aa18fd0">
<div className="settings-list">
<ToggleSwitch
checked={draft.allow_tenant_model_customization}
disabled={!canWrite || busy}
onChange={(checked) => setDraft({ ...draft, allow_tenant_model_customization: checked })}
label="i18n:govoplan-organizations.allow_tenant_model_customization.2425d751"
/>
<ToggleSwitch
checked={draft.require_model_change_requests}
disabled={!canWrite || busy}
onChange={(checked) => setDraft({ ...draft, require_model_change_requests: checked })}
label="i18n:govoplan-organizations.require_model_change_requests.83454cad"
/>
</div>
<p className="muted small-note">i18n:govoplan-organizations.model_governance_help.0fa69021</p>
</Card>
<Card title="i18n:govoplan-organizations.audit_and_retention.3ba1d2fc">
<div className="organizations-form-grid">
<FormField label="i18n:govoplan-organizations.audit_detail_level.7397355d">
<select value={draft.audit_detail_level} disabled={!canWrite || busy} onChange={(event) => setDraft({ ...draft, audit_detail_level: event.target.value as OrganizationAuditDetailLevel })}>
{AUDIT_DETAIL_LEVELS.map((item) => <option key={item.value} value={item.value}>{item.label}</option>)}
</select>
</FormField>
<FormField label="i18n:govoplan-organizations.change_retention_days.71bbd140">
<input
type="number"
min={0}
value={draft.change_retention_days ?? ""}
disabled={!canWrite || busy}
placeholder="i18n:govoplan-organizations.unlimited.35569464"
onChange={(event) => setDraft({ ...draft, change_retention_days: event.target.value === "" ? null : Math.max(0, Number(event.target.value)) })}
/>
</FormField>
</div>
<p className="muted small-note">i18n:govoplan-organizations.audit_retention_help.42dec57d</p>
</Card>
</div>
</AdminPageLayout>
);
}
function settingsKey(settings: OrganizationSettingsItem): string {
return JSON.stringify({
allow_tenant_model_customization: settings.allow_tenant_model_customization,
require_model_change_requests: settings.require_model_change_requests,
audit_detail_level: settings.audit_detail_level,
change_retention_days: settings.change_retention_days ?? null,
settings: settings.settings
});
}

View File

@@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useState, type FormEvent } from "react";
import { RefreshCw, Save, XCircle } from "lucide-react";
import { Edit3, Plus, RefreshCw, Save, XCircle } from "lucide-react";
import {
ApiError,
Button,
@@ -226,6 +226,24 @@ function sluggedPayload(draft: SluggedDraft): SluggedCreatePayload {
};
}
function sluggedPatchPayload(draft: SluggedDraft): Partial<SluggedCreatePayload> {
return {
name: draft.name.trim(),
slug: textOrNull(draft.slug),
description: textOrNull(draft.description),
is_active: draft.is_active
};
}
function sluggedDraftFrom(item: { name: string; slug: string; description?: string | null; is_active: boolean }): SluggedDraft {
return {
name: item.name,
slug: item.slug,
description: item.description ?? "",
is_active: item.is_active
};
}
function isSluggedDirty(draft: SluggedDraft): boolean {
return draft.name.trim() !== "" || draft.slug.trim() !== "" || draft.description.trim() !== "" || !draft.is_active;
}
@@ -297,6 +315,17 @@ function ActiveToggleButton({ item, disabled, onToggle }: { item: ActiveItem; di
);
}
function RowActions({ item, disabled, onEdit, onToggle }: { item: ActiveItem; disabled: boolean; onEdit: () => void; onToggle: () => void }) {
return (
<div className="organizations-row-actions">
<Button type="button" variant="ghost" disabled={disabled} onClick={onEdit} title="i18n:govoplan-organizations.edit.7dce1220">
<Edit3 size={16} aria-hidden="true" /> i18n:govoplan-organizations.edit.7dce1220
</Button>
<ActiveToggleButton item={item} disabled={disabled} onToggle={onToggle} />
</div>
);
}
function SluggedFields({
draft,
onChange,
@@ -354,6 +383,15 @@ export default function OrganizationsPage({
const [functionTypeDraft, setFunctionTypeDraft] = useState<FunctionTypeDraft>(() => emptyFunctionTypeDraft());
const [functionDraft, setFunctionDraft] = useState<FunctionDraft>(() => emptyFunctionDraft());
const [assignmentDraft, setAssignmentDraft] = useState<AssignmentDraft>(() => emptyAssignmentDraft());
const [editingUnitTypeId, setEditingUnitTypeId] = useState<string | null>(null);
const [editingStructureId, setEditingStructureId] = useState<string | null>(null);
const [editingRelationTypeId, setEditingRelationTypeId] = useState<string | null>(null);
const [editingUnitId, setEditingUnitId] = useState<string | null>(null);
const [editingRelationId, setEditingRelationId] = useState<string | null>(null);
const [editingFunctionTypeId, setEditingFunctionTypeId] = useState<string | null>(null);
const [editingFunctionId, setEditingFunctionId] = useState<string | null>(null);
const [editingAssignmentId, setEditingAssignmentId] = useState<string | null>(null);
const [selectedUnitId, setSelectedUnitId] = useState("");
const canWriteModel = hasScope(auth, "organizations:model:write");
const canWriteUnits = hasScope(auth, "organizations:unit:write");
@@ -366,6 +404,15 @@ export default function OrganizationsPage({
const unitById = useMemo(() => mapById(model.units), [model.units]);
const functionTypeById = useMemo(() => mapById(model.function_types), [model.function_types]);
const functionById = useMemo(() => mapById(model.functions), [model.functions]);
const unitsByParentId = useMemo(() => {
const mapped = new Map<string, OrganizationUnitItem[]>();
for (const unit of model.units) {
const key = unit.parent_id || "";
mapped.set(key, [...(mapped.get(key) ?? []), unit]);
}
for (const units of mapped.values()) units.sort((left, right) => left.name.localeCompare(right.name));
return mapped;
}, [model.units]);
const hasDirtyDraft = isSluggedDirty(unitTypeDraft) ||
isSluggedDirty(structureDraft) ||
@@ -396,6 +443,10 @@ export default function OrganizationsPage({
if (!sectionSet.has(active)) setActive(firstAvailableSection);
}, [active, firstAvailableSection, sectionSet]);
useEffect(() => {
if (selectedUnitId && !unitById.has(selectedUnitId)) setSelectedUnitId("");
}, [selectedUnitId, unitById]);
const runAction = useCallback(async (action: () => Promise<unknown>, successMessage = ""): Promise<boolean> => {
setBusy(true);
setError("");
@@ -422,6 +473,14 @@ export default function OrganizationsPage({
setFunctionTypeDraft(emptyFunctionTypeDraft());
setFunctionDraft(emptyFunctionDraft());
setAssignmentDraft(emptyAssignmentDraft());
setEditingUnitTypeId(null);
setEditingStructureId(null);
setEditingRelationTypeId(null);
setEditingUnitId(null);
setEditingRelationId(null);
setEditingFunctionTypeId(null);
setEditingFunctionId(null);
setEditingAssignmentId(null);
}, []);
function rejectMissingName(): Promise<boolean> {
@@ -438,20 +497,33 @@ export default function OrganizationsPage({
event?.preventDefault();
if (!canWriteModel) return rejectMissingWritePermission();
if (!unitTypeDraft.name.trim()) return rejectMissingName();
const ok = await runAction(() => createUnitType(settings, sluggedPayload(unitTypeDraft)), "i18n:govoplan-organizations.unit_type_added.e017dc8c");
if (ok) setUnitTypeDraft(emptySluggedDraft());
const ok = await runAction(
() => editingUnitTypeId ? patchUnitType(settings, editingUnitTypeId, sluggedPatchPayload(unitTypeDraft)) : createUnitType(settings, sluggedPayload(unitTypeDraft)),
editingUnitTypeId ? "i18n:govoplan-organizations.unit_type_updated.6d8f2a16" : "i18n:govoplan-organizations.unit_type_added.e017dc8c"
);
if (ok) {
setUnitTypeDraft(emptySluggedDraft());
setEditingUnitTypeId(null);
}
return ok;
}, [canWriteModel, runAction, settings, unitTypeDraft]);
}, [canWriteModel, editingUnitTypeId, runAction, settings, unitTypeDraft]);
const submitStructure = useCallback(async (event?: FormEvent): Promise<boolean> => {
event?.preventDefault();
if (!canWriteModel) return rejectMissingWritePermission();
if (!structureDraft.name.trim()) return rejectMissingName();
const payload: StructureCreatePayload = { ...sluggedPayload(structureDraft), structure_kind: structureDraft.structure_kind };
const ok = await runAction(() => createStructure(settings, payload), "i18n:govoplan-organizations.structure_added.f6cf8e74");
if (ok) setStructureDraft(emptyStructureDraft());
const patchPayload: Partial<StructureCreatePayload> = { ...sluggedPatchPayload(structureDraft), structure_kind: structureDraft.structure_kind };
const ok = await runAction(
() => editingStructureId ? patchStructure(settings, editingStructureId, patchPayload) : createStructure(settings, payload),
editingStructureId ? "i18n:govoplan-organizations.structure_updated.2591e75f" : "i18n:govoplan-organizations.structure_added.f6cf8e74"
);
if (ok) {
setStructureDraft(emptyStructureDraft());
setEditingStructureId(null);
}
return ok;
}, [canWriteModel, runAction, settings, structureDraft]);
}, [canWriteModel, editingStructureId, runAction, settings, structureDraft]);
const submitRelationType = useCallback(async (event?: FormEvent): Promise<boolean> => {
event?.preventDefault();
@@ -465,10 +537,24 @@ export default function OrganizationsPage({
is_hierarchical: relationTypeDraft.is_hierarchical,
allow_cycles: relationTypeDraft.allow_cycles
};
const ok = await runAction(() => createRelationType(settings, payload), "i18n:govoplan-organizations.relation_type_added.6f1edff1");
if (ok) setRelationTypeDraft(emptyRelationTypeDraft());
const patchPayload: Partial<RelationTypeCreatePayload> = {
...sluggedPatchPayload(relationTypeDraft),
structure_id: textOrNull(relationTypeDraft.structure_id),
source_unit_type_id: textOrNull(relationTypeDraft.source_unit_type_id),
target_unit_type_id: textOrNull(relationTypeDraft.target_unit_type_id),
is_hierarchical: relationTypeDraft.is_hierarchical,
allow_cycles: relationTypeDraft.allow_cycles
};
const ok = await runAction(
() => editingRelationTypeId ? patchRelationType(settings, editingRelationTypeId, patchPayload) : createRelationType(settings, payload),
editingRelationTypeId ? "i18n:govoplan-organizations.relation_type_updated.5ac57ec7" : "i18n:govoplan-organizations.relation_type_added.6f1edff1"
);
if (ok) {
setRelationTypeDraft(emptyRelationTypeDraft());
setEditingRelationTypeId(null);
}
return ok;
}, [canWriteModel, relationTypeDraft, runAction, settings]);
}, [canWriteModel, editingRelationTypeId, relationTypeDraft, runAction, settings]);
const submitUnit = useCallback(async (event?: FormEvent): Promise<boolean> => {
event?.preventDefault();
@@ -479,10 +565,21 @@ export default function OrganizationsPage({
unit_type_id: textOrNull(unitDraft.unit_type_id),
parent_id: textOrNull(unitDraft.parent_id)
};
const ok = await runAction(() => createUnit(settings, payload), "i18n:govoplan-organizations.unit_added.760a8512");
if (ok) setUnitDraft(emptyUnitDraft());
const patchPayload: Partial<UnitCreatePayload> = {
...sluggedPatchPayload(unitDraft),
unit_type_id: textOrNull(unitDraft.unit_type_id),
parent_id: textOrNull(unitDraft.parent_id)
};
const ok = await runAction(
() => editingUnitId ? patchUnit(settings, editingUnitId, patchPayload) : createUnit(settings, payload),
editingUnitId ? "i18n:govoplan-organizations.unit_updated.15f02216" : "i18n:govoplan-organizations.unit_added.760a8512"
);
if (ok) {
setUnitDraft(emptyUnitDraft());
setEditingUnitId(null);
}
return ok;
}, [canWriteUnits, runAction, settings, unitDraft]);
}, [canWriteUnits, editingUnitId, runAction, settings, unitDraft]);
const submitRelation = useCallback(async (event?: FormEvent): Promise<boolean> => {
event?.preventDefault();
@@ -492,10 +589,17 @@ export default function OrganizationsPage({
return false;
}
const payload: RelationCreatePayload = { ...relationDraft, settings: {} };
const ok = await runAction(() => createRelation(settings, payload), "i18n:govoplan-organizations.relation_added.ca90461a");
if (ok) setRelationDraft(emptyRelationDraft());
const patchPayload: Partial<RelationCreatePayload> = { ...relationDraft };
const ok = await runAction(
() => editingRelationId ? patchRelation(settings, editingRelationId, patchPayload) : createRelation(settings, payload),
editingRelationId ? "i18n:govoplan-organizations.relation_updated.c6212776" : "i18n:govoplan-organizations.relation_added.ca90461a"
);
if (ok) {
setRelationDraft(emptyRelationDraft());
setEditingRelationId(null);
}
return ok;
}, [canWriteUnits, relationDraft, runAction, settings]);
}, [canWriteUnits, editingRelationId, relationDraft, runAction, settings]);
const submitFunctionType = useCallback(async (event?: FormEvent): Promise<boolean> => {
event?.preventDefault();
@@ -507,10 +611,22 @@ export default function OrganizationsPage({
delegable: functionTypeDraft.delegable,
act_in_place_allowed: functionTypeDraft.act_in_place_allowed
};
const ok = await runAction(() => createFunctionType(settings, payload), "i18n:govoplan-organizations.function_type_added.2cd9e899");
if (ok) setFunctionTypeDraft(emptyFunctionTypeDraft());
const patchPayload: Partial<FunctionTypeCreatePayload> = {
...sluggedPatchPayload(functionTypeDraft),
organization_unit_type_id: textOrNull(functionTypeDraft.organization_unit_type_id),
delegable: functionTypeDraft.delegable,
act_in_place_allowed: functionTypeDraft.act_in_place_allowed
};
const ok = await runAction(
() => editingFunctionTypeId ? patchFunctionType(settings, editingFunctionTypeId, patchPayload) : createFunctionType(settings, payload),
editingFunctionTypeId ? "i18n:govoplan-organizations.function_type_updated.1a4f29f6" : "i18n:govoplan-organizations.function_type_added.2cd9e899"
);
if (ok) {
setFunctionTypeDraft(emptyFunctionTypeDraft());
setEditingFunctionTypeId(null);
}
return ok;
}, [canWriteModel, functionTypeDraft, runAction, settings]);
}, [canWriteModel, editingFunctionTypeId, functionTypeDraft, runAction, settings]);
const submitFunction = useCallback(async (event?: FormEvent): Promise<boolean> => {
event?.preventDefault();
@@ -527,10 +643,23 @@ export default function OrganizationsPage({
delegable: functionDraft.delegable,
act_in_place_allowed: functionDraft.act_in_place_allowed
};
const ok = await runAction(() => createFunction(settings, payload), "i18n:govoplan-organizations.function_added.e2266702");
if (ok) setFunctionDraft(emptyFunctionDraft());
const patchPayload: Partial<FunctionCreatePayload> = {
...sluggedPatchPayload(functionDraft),
organization_unit_id: functionDraft.organization_unit_id,
function_type_id: textOrNull(functionDraft.function_type_id),
delegable: functionDraft.delegable,
act_in_place_allowed: functionDraft.act_in_place_allowed
};
const ok = await runAction(
() => editingFunctionId ? patchFunction(settings, editingFunctionId, patchPayload) : createFunction(settings, payload),
editingFunctionId ? "i18n:govoplan-organizations.function_updated.65016009" : "i18n:govoplan-organizations.function_added.e2266702"
);
if (ok) {
setFunctionDraft(emptyFunctionDraft());
setEditingFunctionId(null);
}
return ok;
}, [canWriteFunctions, functionDraft, runAction, settings]);
}, [canWriteFunctions, editingFunctionId, functionDraft, runAction, settings]);
const submitAssignment = useCallback(async (event?: FormEvent): Promise<boolean> => {
event?.preventDefault();
@@ -550,10 +679,16 @@ export default function OrganizationsPage({
is_active: assignmentDraft.is_active,
settings: {}
};
const ok = await runAction(() => createFunctionAssignment(settings, payload), "i18n:govoplan-organizations.assignment_added.94263d1b");
if (ok) setAssignmentDraft(emptyAssignmentDraft());
const ok = await runAction(
() => editingAssignmentId ? patchFunctionAssignment(settings, editingAssignmentId, payload) : createFunctionAssignment(settings, payload),
editingAssignmentId ? "i18n:govoplan-organizations.assignment_updated.680b6e33" : "i18n:govoplan-organizations.assignment_added.94263d1b"
);
if (ok) {
setAssignmentDraft(emptyAssignmentDraft());
setEditingAssignmentId(null);
}
return ok;
}, [assignmentDraft, canAssignFunctions, runAction, settings]);
}, [assignmentDraft, canAssignFunctions, editingAssignmentId, runAction, settings]);
const saveDrafts = useCallback(async (): Promise<boolean> => {
if (isSluggedDirty(unitTypeDraft) && !(await submitUnitType())) return false;
@@ -624,12 +759,103 @@ export default function OrganizationsPage({
void runAction(() => patchFunctionAssignment(settings, item.id, { is_active: !item.is_active }));
}, [runAction, settings]);
const editUnitType = useCallback((item: OrganizationUnitTypeItem) => {
setEditingUnitTypeId(item.id);
setUnitTypeDraft(sluggedDraftFrom(item));
setActive("model");
}, []);
const editStructure = useCallback((item: OrganizationStructureItem) => {
setEditingStructureId(item.id);
setStructureDraft({ ...sluggedDraftFrom(item), structure_kind: item.structure_kind as OrganizationStructureKind });
setActive("model");
}, []);
const editRelationType = useCallback((item: OrganizationRelationTypeItem) => {
setEditingRelationTypeId(item.id);
setRelationTypeDraft({
...sluggedDraftFrom(item),
structure_id: item.structure_id ?? "",
source_unit_type_id: item.source_unit_type_id ?? "",
target_unit_type_id: item.target_unit_type_id ?? "",
is_hierarchical: item.is_hierarchical,
allow_cycles: item.allow_cycles
});
setActive("model");
}, []);
const editUnit = useCallback((item: OrganizationUnitItem) => {
setEditingUnitId(item.id);
setSelectedUnitId(item.id);
setUnitDraft({ ...sluggedDraftFrom(item), unit_type_id: item.unit_type_id ?? "", parent_id: item.parent_id ?? "" });
setActive("units");
}, []);
const addSubunit = useCallback((item: OrganizationUnitItem) => {
setSelectedUnitId(item.id);
setEditingUnitId(null);
setUnitDraft({ ...emptyUnitDraft(), parent_id: item.id });
setActive("units");
}, []);
const editRelation = useCallback((item: OrganizationRelationItem) => {
setEditingRelationId(item.id);
setRelationDraft({
structure_id: item.structure_id,
relation_type_id: item.relation_type_id,
source_unit_id: item.source_unit_id,
target_unit_id: item.target_unit_id,
is_active: item.is_active
});
setActive("relations");
}, []);
const editFunctionType = useCallback((item: OrganizationFunctionTypeItem) => {
setEditingFunctionTypeId(item.id);
setFunctionTypeDraft({
...sluggedDraftFrom(item),
organization_unit_type_id: item.organization_unit_type_id ?? "",
delegable: item.delegable,
act_in_place_allowed: item.act_in_place_allowed
});
setActive("model");
}, []);
const editFunction = useCallback((item: OrganizationFunctionItem) => {
setEditingFunctionId(item.id);
setSelectedUnitId(item.organization_unit_id);
setFunctionDraft({
...sluggedDraftFrom(item),
organization_unit_id: item.organization_unit_id,
function_type_id: item.function_type_id ?? "",
delegable: item.delegable,
act_in_place_allowed: item.act_in_place_allowed
});
setActive("functions");
}, []);
const editAssignment = useCallback((item: OrganizationFunctionAssignmentItem) => {
setEditingAssignmentId(item.id);
setSelectedUnitId(item.organization_unit_id);
setAssignmentDraft({
identity_id: item.identity_id,
account_id: item.account_id ?? "",
function_id: item.function_id,
applies_to_subunits: item.applies_to_subunits,
source: item.source,
delegated_from_assignment_id: item.delegated_from_assignment_id ?? "",
acting_for_account_id: item.acting_for_account_id ?? "",
is_active: item.is_active
});
setActive("assignments");
}, []);
const unitTypeColumns: DataGridColumn<OrganizationUnitTypeItem>[] = [
{ id: "name", header: "i18n:govoplan-organizations.name.709a2322", minWidth: 180, sortable: true, filterable: true, value: (row) => row.name },
{ id: "slug", header: "i18n:govoplan-organizations.slug.094da9b9", width: 160, sortable: true, filterable: true, value: (row) => row.slug },
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
{ id: "description", header: "i18n:govoplan-organizations.description.55f8ebc8", minWidth: 220, value: (row) => row.description ?? "" },
{ id: "actions", header: "", width: 130, sticky: "end", render: (row) => <ActiveToggleButton item={row} disabled={!canWriteModel || busy} onToggle={() => toggleUnitType(row)} /> }
{ id: "actions", header: "", width: 220, sticky: "end", render: (row) => <RowActions item={row} disabled={!canWriteModel || busy} onEdit={() => editUnitType(row)} onToggle={() => toggleUnitType(row)} /> }
];
const structureColumns: DataGridColumn<OrganizationStructureItem>[] = [
@@ -637,7 +863,7 @@ export default function OrganizationsPage({
{ id: "kind", header: "i18n:govoplan-organizations.kind.794c9d9c", width: 140, sortable: true, filterable: true, value: (row) => row.structure_kind },
{ id: "slug", header: "i18n:govoplan-organizations.slug.094da9b9", width: 160, value: (row) => row.slug },
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
{ id: "actions", header: "", width: 130, sticky: "end", render: (row) => <ActiveToggleButton item={row} disabled={!canWriteModel || busy} onToggle={() => toggleStructure(row)} /> }
{ id: "actions", header: "", width: 220, sticky: "end", render: (row) => <RowActions item={row} disabled={!canWriteModel || busy} onEdit={() => editStructure(row)} onToggle={() => toggleStructure(row)} /> }
];
const relationTypeColumns: DataGridColumn<OrganizationRelationTypeItem>[] = [
@@ -647,7 +873,7 @@ export default function OrganizationsPage({
{ id: "target", header: "i18n:govoplan-organizations.target_unit.a1507d86", minWidth: 170, render: (row) => optionalLabel(unitTypeById.get(row.target_unit_type_id || "")?.name) },
{ id: "hierarchical", header: "i18n:govoplan-organizations.hierarchical.8964f313", width: 130, render: (row) => activeStatus(row.is_hierarchical) },
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
{ id: "actions", header: "", width: 130, sticky: "end", render: (row) => <ActiveToggleButton item={row} disabled={!canWriteModel || busy} onToggle={() => toggleRelationType(row)} /> }
{ id: "actions", header: "", width: 220, sticky: "end", render: (row) => <RowActions item={row} disabled={!canWriteModel || busy} onEdit={() => editRelationType(row)} onToggle={() => toggleRelationType(row)} /> }
];
const unitColumns: DataGridColumn<OrganizationUnitItem>[] = [
@@ -656,7 +882,7 @@ export default function OrganizationsPage({
{ id: "parent", header: "i18n:govoplan-organizations.parent_unit.1986c35e", minWidth: 180, render: (row) => optionalLabel(unitById.get(row.parent_id || "")?.name) },
{ id: "slug", header: "i18n:govoplan-organizations.slug.094da9b9", width: 160, value: (row) => row.slug },
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
{ id: "actions", header: "", width: 130, sticky: "end", render: (row) => <ActiveToggleButton item={row} disabled={!canWriteUnits || busy} onToggle={() => toggleUnit(row)} /> }
{ id: "actions", header: "", width: 220, sticky: "end", render: (row) => <RowActions item={row} disabled={!canWriteUnits || busy} onEdit={() => editUnit(row)} onToggle={() => toggleUnit(row)} /> }
];
const relationColumns: DataGridColumn<OrganizationRelationItem>[] = [
@@ -665,7 +891,7 @@ export default function OrganizationsPage({
{ id: "source", header: "i18n:govoplan-organizations.source_unit.2fbd8baa", minWidth: 190, render: (row) => optionalLabel(unitById.get(row.source_unit_id)?.name) },
{ id: "target", header: "i18n:govoplan-organizations.target_unit.a1507d86", minWidth: 190, render: (row) => optionalLabel(unitById.get(row.target_unit_id)?.name) },
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
{ id: "actions", header: "", width: 130, sticky: "end", render: (row) => <ActiveToggleButton item={row} disabled={!canWriteUnits || busy} onToggle={() => toggleRelation(row)} /> }
{ id: "actions", header: "", width: 220, sticky: "end", render: (row) => <RowActions item={row} disabled={!canWriteUnits || busy} onEdit={() => editRelation(row)} onToggle={() => toggleRelation(row)} /> }
];
const functionTypeColumns: DataGridColumn<OrganizationFunctionTypeItem>[] = [
@@ -674,7 +900,7 @@ export default function OrganizationsPage({
{ id: "delegable", header: "i18n:govoplan-organizations.delegable.b4f0137d", width: 120, render: (row) => activeStatus(row.delegable) },
{ id: "actInPlace", header: "i18n:govoplan-organizations.act_in_place.49b942bd", width: 140, render: (row) => activeStatus(row.act_in_place_allowed) },
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
{ id: "actions", header: "", width: 130, sticky: "end", render: (row) => <ActiveToggleButton item={row} disabled={!canWriteModel || busy} onToggle={() => toggleFunctionType(row)} /> }
{ id: "actions", header: "", width: 220, sticky: "end", render: (row) => <RowActions item={row} disabled={!canWriteModel || busy} onEdit={() => editFunctionType(row)} onToggle={() => toggleFunctionType(row)} /> }
];
const functionColumns: DataGridColumn<OrganizationFunctionItem>[] = [
@@ -683,7 +909,7 @@ export default function OrganizationsPage({
{ id: "type", header: "i18n:govoplan-organizations.function_type.501fe7c0", minWidth: 180, render: (row) => optionalLabel(functionTypeById.get(row.function_type_id || "")?.name) },
{ id: "delegable", header: "i18n:govoplan-organizations.delegable.b4f0137d", width: 120, render: (row) => activeStatus(row.delegable) },
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
{ id: "actions", header: "", width: 130, sticky: "end", render: (row) => <ActiveToggleButton item={row} disabled={!canWriteFunctions || busy} onToggle={() => toggleFunction(row)} /> }
{ id: "actions", header: "", width: 220, sticky: "end", render: (row) => <RowActions item={row} disabled={!canWriteFunctions || busy} onEdit={() => editFunction(row)} onToggle={() => toggleFunction(row)} /> }
];
const assignmentColumns: DataGridColumn<OrganizationFunctionAssignmentItem>[] = [
@@ -693,11 +919,15 @@ export default function OrganizationsPage({
{ id: "subunits", header: "i18n:govoplan-organizations.applies_to_subunits.7a15f741", width: 150, render: (row) => activeStatus(row.applies_to_subunits) },
{ id: "source", header: "i18n:govoplan-organizations.source.6da13add", width: 130, value: (row) => row.source },
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
{ id: "actions", header: "", width: 130, sticky: "end", render: (row) => <ActiveToggleButton item={row} disabled={!canAssignFunctions || busy} onToggle={() => toggleAssignment(row)} /> }
{ id: "actions", header: "", width: 220, sticky: "end", render: (row) => <RowActions item={row} disabled={!canAssignFunctions || busy} onEdit={() => editAssignment(row)} onToggle={() => toggleAssignment(row)} /> }
];
const canWriteActive = active === "model" ? canWriteModel : active === "units" || active === "relations" ? canWriteUnits : active === "functions" ? canWriteFunctions : canAssignFunctions;
function cancelEditButton(onCancel: () => void) {
return <Button type="button" variant="ghost" onClick={onCancel} disabled={busy}>i18n:govoplan-organizations.cancel_edit.309c2a6f</Button>;
}
const content = (
<div className={`${mode === "workspace" ? "content-pad " : ""}organizations-page ${mode === "admin" ? "organizations-admin-page" : ""}`.trim()}>
<div className="page-heading split organizations-heading">
@@ -754,7 +984,10 @@ export default function OrganizationsPage({
<Card title="i18n:govoplan-organizations.unit_types.c7afc174">
<form className="organizations-form-grid" onSubmit={(event) => void submitUnitType(event)}>
<SluggedFields draft={unitTypeDraft} onChange={setUnitTypeDraft} disabled={!canWriteModel || busy} />
<Button className="wide" type="submit" variant="primary" disabled={!canWriteModel || busy}>i18n:govoplan-organizations.add_unit_type.58f2c05a</Button>
<div className="organizations-form-actions wide">
<Button type="submit" variant="primary" disabled={!canWriteModel || busy}>{editingUnitTypeId ? "i18n:govoplan-organizations.update_unit_type.b96fc0ad" : "i18n:govoplan-organizations.add_unit_type.58f2c05a"}</Button>
{editingUnitTypeId && cancelEditButton(() => { setEditingUnitTypeId(null); setUnitTypeDraft(emptySluggedDraft()); })}
</div>
</form>
</Card>
<Card title="i18n:govoplan-organizations.unit_types.c7afc174">
@@ -770,7 +1003,10 @@ export default function OrganizationsPage({
{STRUCTURE_KINDS.map((item) => <option key={item.value} value={item.value}>{item.label}</option>)}
</select>
</FormField>
<Button className="wide" type="submit" variant="primary" disabled={!canWriteModel || busy}>i18n:govoplan-organizations.add_structure.b722042a</Button>
<div className="organizations-form-actions wide">
<Button type="submit" variant="primary" disabled={!canWriteModel || busy}>{editingStructureId ? "i18n:govoplan-organizations.update_structure.b2e25446" : "i18n:govoplan-organizations.add_structure.b722042a"}</Button>
{editingStructureId && cancelEditButton(() => { setEditingStructureId(null); setStructureDraft(emptyStructureDraft()); })}
</div>
</form>
</Card>
<Card title="i18n:govoplan-organizations.structures.f9b7f3b4">
@@ -803,7 +1039,10 @@ export default function OrganizationsPage({
<label><input type="checkbox" checked={relationTypeDraft.is_hierarchical} disabled={!canWriteModel || busy} onChange={(event) => setRelationTypeDraft({ ...relationTypeDraft, is_hierarchical: event.target.checked })} /> i18n:govoplan-organizations.hierarchical.8964f313</label>
<label><input type="checkbox" checked={relationTypeDraft.allow_cycles} disabled={!canWriteModel || busy} onChange={(event) => setRelationTypeDraft({ ...relationTypeDraft, allow_cycles: event.target.checked })} /> i18n:govoplan-organizations.allow_cycles.31327578</label>
</div>
<Button className="wide" type="submit" variant="primary" disabled={!canWriteModel || busy}>i18n:govoplan-organizations.add_relation_type.2ad19d03</Button>
<div className="organizations-form-actions wide">
<Button type="submit" variant="primary" disabled={!canWriteModel || busy}>{editingRelationTypeId ? "i18n:govoplan-organizations.update_relation_type.e6e7f30c" : "i18n:govoplan-organizations.add_relation_type.2ad19d03"}</Button>
{editingRelationTypeId && cancelEditButton(() => { setEditingRelationTypeId(null); setRelationTypeDraft(emptyRelationTypeDraft()); })}
</div>
</form>
</Card>
<Card title="i18n:govoplan-organizations.relation_types.e5890528">
@@ -824,7 +1063,10 @@ export default function OrganizationsPage({
<label><input type="checkbox" checked={functionTypeDraft.delegable} disabled={!canWriteModel || busy} onChange={(event) => setFunctionTypeDraft({ ...functionTypeDraft, delegable: event.target.checked })} /> i18n:govoplan-organizations.delegable.b4f0137d</label>
<label><input type="checkbox" checked={functionTypeDraft.act_in_place_allowed} disabled={!canWriteModel || busy} onChange={(event) => setFunctionTypeDraft({ ...functionTypeDraft, act_in_place_allowed: event.target.checked })} /> i18n:govoplan-organizations.act_in_place.49b942bd</label>
</div>
<Button className="wide" type="submit" variant="primary" disabled={!canWriteModel || busy}>i18n:govoplan-organizations.add_function_type.90d793f1</Button>
<div className="organizations-form-actions wide">
<Button type="submit" variant="primary" disabled={!canWriteModel || busy}>{editingFunctionTypeId ? "i18n:govoplan-organizations.update_function_type.0b7e07d3" : "i18n:govoplan-organizations.add_function_type.90d793f1"}</Button>
{editingFunctionTypeId && cancelEditButton(() => { setEditingFunctionTypeId(null); setFunctionTypeDraft(emptyFunctionTypeDraft()); })}
</div>
</form>
</Card>
<Card title="i18n:govoplan-organizations.function_types.172c01fe">
@@ -835,30 +1077,75 @@ export default function OrganizationsPage({
);
}
function renderUnitTreeNodes(parentId = "", depth = 0, seen: ReadonlySet<string> = new Set()): JSX.Element[] {
return (unitsByParentId.get(parentId) ?? []).flatMap((unit) => {
if (seen.has(unit.id)) return [];
const nextSeen = new Set(seen);
nextSeen.add(unit.id);
return [
<div className="organizations-tree-row" key={unit.id}>
<button
type="button"
className={`organizations-tree-node ${selectedUnitId === unit.id ? "active" : ""}`.trim()}
style={{ paddingLeft: `${10 + depth * 18}px` }}
onClick={() => setSelectedUnitId(unit.id)}
>
<strong>{unit.name}</strong>
<span>{unitTypeById.get(unit.unit_type_id || "")?.name ?? "i18n:govoplan-organizations.no_unit_type.73e799f5"}</span>
</button>
<Button type="button" variant="ghost" disabled={!canWriteUnits || busy} onClick={() => addSubunit(unit)} title="i18n:govoplan-organizations.add_subunit.8256a8f7">
<Plus size={16} aria-hidden="true" /> i18n:govoplan-organizations.add_subunit.8256a8f7
</Button>
</div>,
...renderUnitTreeNodes(unit.id, depth + 1, nextSeen)
];
});
}
function renderUnitsSection() {
return (
<div className="organizations-section-grid">
<Card title="i18n:govoplan-organizations.add_unit.8fa12fb1">
<form className="organizations-form-grid" onSubmit={(event) => void submitUnit(event)}>
<SluggedFields draft={unitDraft} onChange={setUnitDraft} disabled={!canWriteUnits || busy} />
<FormField label="i18n:govoplan-organizations.unit_type.9e62810d">
<select value={unitDraft.unit_type_id} disabled={!canWriteUnits || busy} onChange={(event) => setUnitDraft({ ...unitDraft, unit_type_id: event.target.value })}>
<option value="">i18n:govoplan-organizations.none.334c4a4c</option>
{model.unit_types.map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
</select>
</FormField>
<FormField label="i18n:govoplan-organizations.parent_unit.1986c35e">
<select value={unitDraft.parent_id} disabled={!canWriteUnits || busy} onChange={(event) => setUnitDraft({ ...unitDraft, parent_id: event.target.value })}>
<option value="">i18n:govoplan-organizations.none.334c4a4c</option>
{model.units.map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
</select>
</FormField>
<Button className="wide" type="submit" variant="primary" disabled={!canWriteUnits || busy}>i18n:govoplan-organizations.add_unit.8fa12fb1</Button>
</form>
</Card>
<Card title="i18n:govoplan-organizations.units.e14d0d92">
<DataGrid id="organizations-units" rows={model.units} columns={unitColumns} getRowKey={(row) => row.id} emptyText="i18n:govoplan-organizations.no_units_found.eea2dd0c" initialFit="container" />
<div className="organizations-editor-grid">
<Card title="i18n:govoplan-organizations.organization_tree.e5bfb195">
<div className="organizations-tree-toolbar">
<Button type="button" variant="secondary" disabled={!canWriteUnits || busy} onClick={() => { setEditingUnitId(null); setUnitDraft(emptyUnitDraft()); }} title="i18n:govoplan-organizations.add_root_unit.1ef8a9f5">
<Plus size={16} aria-hidden="true" /> i18n:govoplan-organizations.add_root_unit.1ef8a9f5
</Button>
</div>
<div className="organizations-tree-list">
{model.units.length ? renderUnitTreeNodes() : <p className="muted small-note">i18n:govoplan-organizations.no_units_found.eea2dd0c</p>}
</div>
</Card>
<div className="organizations-table-stack">
<Card title="i18n:govoplan-organizations.add_unit.8fa12fb1">
<form className="organizations-form-grid" onSubmit={(event) => void submitUnit(event)}>
<SluggedFields draft={unitDraft} onChange={setUnitDraft} disabled={!canWriteUnits || busy} />
<FormField label="i18n:govoplan-organizations.unit_type.9e62810d">
<select value={unitDraft.unit_type_id} disabled={!canWriteUnits || busy} onChange={(event) => setUnitDraft({ ...unitDraft, unit_type_id: event.target.value })}>
<option value="">i18n:govoplan-organizations.none.334c4a4c</option>
{model.unit_types.map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
</select>
</FormField>
<FormField label="i18n:govoplan-organizations.parent_unit.1986c35e">
<select value={unitDraft.parent_id} disabled={!canWriteUnits || busy} onChange={(event) => setUnitDraft({ ...unitDraft, parent_id: event.target.value })}>
<option value="">i18n:govoplan-organizations.none.334c4a4c</option>
{model.units.filter((item) => item.id !== editingUnitId).map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
</select>
</FormField>
{unitDraft.parent_id && (
<p className="organizations-field-note wide">
i18n:govoplan-organizations.selected_parent.0e013d44 <strong>{unitById.get(unitDraft.parent_id)?.name ?? unitDraft.parent_id}</strong>
</p>
)}
<div className="organizations-form-actions wide">
<Button type="submit" variant="primary" disabled={!canWriteUnits || busy}>{editingUnitId ? "i18n:govoplan-organizations.update_unit.67e2500f" : "i18n:govoplan-organizations.add_unit.8fa12fb1"}</Button>
{editingUnitId && cancelEditButton(() => { setEditingUnitId(null); setUnitDraft(emptyUnitDraft()); })}
</div>
</form>
</Card>
<Card title="i18n:govoplan-organizations.units.e14d0d92">
<DataGrid id="organizations-units" rows={model.units} columns={unitColumns} getRowKey={(row) => row.id} emptyText="i18n:govoplan-organizations.no_units_found.eea2dd0c" initialFit="container" />
</Card>
</div>
</div>
);
}
@@ -896,7 +1183,10 @@ export default function OrganizationsPage({
<input type="checkbox" checked={relationDraft.is_active} disabled={!canWriteUnits || busy} onChange={(event) => setRelationDraft({ ...relationDraft, is_active: event.target.checked })} />
<span>i18n:govoplan-organizations.active.a733b809</span>
</label>
<Button className="wide" type="submit" variant="primary" disabled={!canWriteUnits || busy}>i18n:govoplan-organizations.add_relation.6b6e67ea</Button>
<div className="organizations-form-actions wide">
<Button type="submit" variant="primary" disabled={!canWriteUnits || busy}>{editingRelationId ? "i18n:govoplan-organizations.update_relation.4c44ce83" : "i18n:govoplan-organizations.add_relation.6b6e67ea"}</Button>
{editingRelationId && cancelEditButton(() => { setEditingRelationId(null); setRelationDraft(emptyRelationDraft()); })}
</div>
</form>
</Card>
<Card title="i18n:govoplan-organizations.relations.1c796711">
@@ -928,7 +1218,10 @@ export default function OrganizationsPage({
<label><input type="checkbox" checked={functionDraft.delegable} disabled={!canWriteFunctions || busy} onChange={(event) => setFunctionDraft({ ...functionDraft, delegable: event.target.checked })} /> i18n:govoplan-organizations.delegable.b4f0137d</label>
<label><input type="checkbox" checked={functionDraft.act_in_place_allowed} disabled={!canWriteFunctions || busy} onChange={(event) => setFunctionDraft({ ...functionDraft, act_in_place_allowed: event.target.checked })} /> i18n:govoplan-organizations.act_in_place.49b942bd</label>
</div>
<Button className="wide" type="submit" variant="primary" disabled={!canWriteFunctions || busy}>i18n:govoplan-organizations.add_function.6abafee0</Button>
<div className="organizations-form-actions wide">
<Button type="submit" variant="primary" disabled={!canWriteFunctions || busy}>{editingFunctionId ? "i18n:govoplan-organizations.update_function.504f03e5" : "i18n:govoplan-organizations.add_function.6abafee0"}</Button>
{editingFunctionId && cancelEditButton(() => { setEditingFunctionId(null); setFunctionDraft(emptyFunctionDraft()); })}
</div>
</form>
</Card>
<Card title="i18n:govoplan-organizations.functions.805dc49b">
@@ -962,7 +1255,10 @@ export default function OrganizationsPage({
<label><input type="checkbox" checked={assignmentDraft.applies_to_subunits} disabled={!canAssignFunctions || busy} onChange={(event) => setAssignmentDraft({ ...assignmentDraft, applies_to_subunits: event.target.checked })} /> i18n:govoplan-organizations.applies_to_subunits.7a15f741</label>
<label><input type="checkbox" checked={assignmentDraft.is_active} disabled={!canAssignFunctions || busy} onChange={(event) => setAssignmentDraft({ ...assignmentDraft, is_active: event.target.checked })} /> i18n:govoplan-organizations.active.a733b809</label>
</div>
<Button className="wide" type="submit" variant="primary" disabled={!canAssignFunctions || busy}>i18n:govoplan-organizations.add_assignment.6682f5f5</Button>
<div className="organizations-form-actions wide">
<Button type="submit" variant="primary" disabled={!canAssignFunctions || busy}>{editingAssignmentId ? "i18n:govoplan-organizations.update_assignment.bf130723" : "i18n:govoplan-organizations.add_assignment.6682f5f5"}</Button>
{editingAssignmentId && cancelEditButton(() => { setEditingAssignmentId(null); setAssignmentDraft(emptyAssignmentDraft()); })}
</div>
</form>
</Card>
<Card title="i18n:govoplan-organizations.function_assignments.4c5c6dae">

View File

@@ -9,26 +9,41 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-organizations.add_function_type.90d793f1": "Add function type",
"i18n:govoplan-organizations.add_relation.6b6e67ea": "Add relation",
"i18n:govoplan-organizations.add_relation_type.2ad19d03": "Add relation type",
"i18n:govoplan-organizations.add_root_unit.1ef8a9f5": "Add root unit",
"i18n:govoplan-organizations.add_structure.b722042a": "Add structure",
"i18n:govoplan-organizations.add_subunit.8256a8f7": "Add subunit",
"i18n:govoplan-organizations.add_unit.8fa12fb1": "Add unit",
"i18n:govoplan-organizations.add_unit_type.58f2c05a": "Add unit type",
"i18n:govoplan-organizations.allow_cycles.31327578": "Allow cycles",
"i18n:govoplan-organizations.applies_to_subunits.7a15f741": "Applies to subunits",
"i18n:govoplan-organizations.assignment_added.94263d1b": "Function assignment added.",
"i18n:govoplan-organizations.assignment_updated.680b6e33": "Function assignment updated.",
"i18n:govoplan-organizations.assignments.278f513e": "Assignments",
"i18n:govoplan-organizations.audit_and_retention.3ba1d2fc": "Audit and retention",
"i18n:govoplan-organizations.audit_detail_level.7397355d": "Audit detail level",
"i18n:govoplan-organizations.audit_full.0c83669c": "Full",
"i18n:govoplan-organizations.audit_retention_help.42dec57d": "Controls how much organization-change detail is retained for review, troubleshooting, and evidence.",
"i18n:govoplan-organizations.audit_standard.785d981f": "Standard",
"i18n:govoplan-organizations.audit_summary.6a7d68a5": "Summary",
"i18n:govoplan-organizations.classification.3e9f6c3a": "Classification",
"i18n:govoplan-organizations.allow_tenant_model_customization.2425d751": "Tenant admins may customize the organization meta-model",
"i18n:govoplan-organizations.change_retention_days.71bbd140": "Retain organization change detail for days",
"i18n:govoplan-organizations.cancel_edit.309c2a6f": "Cancel edit",
"i18n:govoplan-organizations.concrete_model.4e9c531a": "Concrete model",
"i18n:govoplan-organizations.deactivate.46ab070d": "Deactivate",
"i18n:govoplan-organizations.delegable.b4f0137d": "Delegable",
"i18n:govoplan-organizations.description.55f8ebc8": "Description",
"i18n:govoplan-organizations.direct_assignment.2a068aac": "direct",
"i18n:govoplan-organizations.discard_organization_drafts.766d5f46": "Discard organization drafts",
"i18n:govoplan-organizations.edit.7dce1220": "Edit",
"i18n:govoplan-organizations.function.28822f3a": "Function",
"i18n:govoplan-organizations.function_added.e2266702": "Function added.",
"i18n:govoplan-organizations.function_assignments.4c5c6dae": "Function assignments",
"i18n:govoplan-organizations.function_type.501fe7c0": "Function type",
"i18n:govoplan-organizations.function_type_added.2cd9e899": "Function type added.",
"i18n:govoplan-organizations.function_type_updated.1a4f29f6": "Function type updated.",
"i18n:govoplan-organizations.function_types.172c01fe": "Function types",
"i18n:govoplan-organizations.function_updated.65016009": "Function updated.",
"i18n:govoplan-organizations.functions.805dc49b": "Functions",
"i18n:govoplan-organizations.hierarchical.8964f313": "Hierarchical",
"i18n:govoplan-organizations.hierarchy.74797f37": "Hierarchy",
@@ -37,9 +52,12 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-organizations.inactive.09af574c": "Inactive",
"i18n:govoplan-organizations.kind.794c9d9c": "Kind",
"i18n:govoplan-organizations.loading_organization_model.846aa317": "Loading organization model...",
"i18n:govoplan-organizations.loading_organization_settings.c6008db8": "Loading organization settings...",
"i18n:govoplan-organizations.membership.4531d86d": "Membership",
"i18n:govoplan-organizations.meta_model.7398487c": "Meta-model",
"i18n:govoplan-organizations.model.20f35e63": "Model",
"i18n:govoplan-organizations.model_governance.6aa18fd0": "Model governance",
"i18n:govoplan-organizations.model_governance_help.0fa69021": "These switches define whether the tenant can change its own organization model directly or must record governed change requests.",
"i18n:govoplan-organizations.name.709a2322": "Name",
"i18n:govoplan-organizations.name_is_required.27cd3782": "Name is required.",
"i18n:govoplan-organizations.network.a24d31c3": "Network",
@@ -49,23 +67,33 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-organizations.no_relation_types_found.6f90bb81": "No relation types found.",
"i18n:govoplan-organizations.no_relations_found.4e75b11d": "No relations found.",
"i18n:govoplan-organizations.no_structures_found.20b382d0": "No structures found.",
"i18n:govoplan-organizations.no_unit_type.73e799f5": "No unit type",
"i18n:govoplan-organizations.no_unit_types_found.c81fb2a7": "No unit types found.",
"i18n:govoplan-organizations.no_units_found.eea2dd0c": "No units found.",
"i18n:govoplan-organizations.none.334c4a4c": "None",
"i18n:govoplan-organizations.optional_account_id.5fcf495c": "Optional account ID",
"i18n:govoplan-organizations.organization_model.4f924c0e": "Organization model",
"i18n:govoplan-organizations.organization_model_admin_description.35dc9f10": "Configure tenant-local unit types, parallel structures, relation types, and function types used by the organization module.",
"i18n:govoplan-organizations.organization_settings.c9ab9829": "Organization settings",
"i18n:govoplan-organizations.organization_settings_description.35dc9f10": "Configure tenant-level governance, audit, and retention behavior for organization changes.",
"i18n:govoplan-organizations.organization_settings_saved.bfbcfdfa": "Organization settings saved.",
"i18n:govoplan-organizations.organization_tree.e5bfb195": "Organization tree",
"i18n:govoplan-organizations.organizations.220edf64": "Organizations",
"i18n:govoplan-organizations.organizations_intro.4e67c4bb": "Model organization types, concrete units, structures, functions, and identity-held function assignments.",
"i18n:govoplan-organizations.parent_unit.1986c35e": "Parent unit",
"i18n:govoplan-organizations.reactivate.58f2855d": "Reactivate",
"i18n:govoplan-organizations.relation_added.ca90461a": "Relation added.",
"i18n:govoplan-organizations.relation_updated.c6212776": "Relation updated.",
"i18n:govoplan-organizations.relation_type.d0aee2e7": "Relation type",
"i18n:govoplan-organizations.relation_type_added.6f1edff1": "Relation type added.",
"i18n:govoplan-organizations.relation_type_updated.5ac57ec7": "Relation type updated.",
"i18n:govoplan-organizations.relation_types.e5890528": "Relation types",
"i18n:govoplan-organizations.relations.1c796711": "Relations",
"i18n:govoplan-organizations.reload.cce71553": "Reload",
"i18n:govoplan-organizations.require_model_change_requests.83454cad": "Organization model changes require recorded change requests",
"i18n:govoplan-organizations.save_settings.913aba9f": "Save settings",
"i18n:govoplan-organizations.save_drafts.606f9401": "Save drafts",
"i18n:govoplan-organizations.saving.56a2285c": "Saving...",
"i18n:govoplan-organizations.select_function.4895a67d": "Select function",
"i18n:govoplan-organizations.select_function_type.2f426c43": "Select function type",
"i18n:govoplan-organizations.select_relation_type.73f92478": "Select relation type",
@@ -74,21 +102,34 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-organizations.select_target_unit.8d607541": "Select target unit",
"i18n:govoplan-organizations.select_unit.013bf13a": "Select unit",
"i18n:govoplan-organizations.select_unit_type.2c71d14f": "Select unit type",
"i18n:govoplan-organizations.selected_parent.0e013d44": "Selected parent:",
"i18n:govoplan-organizations.slug.094da9b9": "Slug",
"i18n:govoplan-organizations.source.6da13add": "Source",
"i18n:govoplan-organizations.source_unit.2fbd8baa": "Source unit",
"i18n:govoplan-organizations.status.bae7d5be": "Status",
"i18n:govoplan-organizations.structure.7732fb0b": "Structure",
"i18n:govoplan-organizations.structure_added.f6cf8e74": "Structure added.",
"i18n:govoplan-organizations.structure_updated.2591e75f": "Structure updated.",
"i18n:govoplan-organizations.structures.f9b7f3b4": "Structures",
"i18n:govoplan-organizations.target_unit.a1507d86": "Target unit",
"i18n:govoplan-organizations.type.599dcce2": "Type",
"i18n:govoplan-organizations.unit.8fe4d595": "Unit",
"i18n:govoplan-organizations.unit_added.760a8512": "Unit added.",
"i18n:govoplan-organizations.unit_updated.15f02216": "Unit updated.",
"i18n:govoplan-organizations.unit_type.9e62810d": "Unit type",
"i18n:govoplan-organizations.unit_type_added.e017dc8c": "Unit type added.",
"i18n:govoplan-organizations.unit_type_updated.6d8f2a16": "Unit type updated.",
"i18n:govoplan-organizations.unit_types.c7afc174": "Unit types",
"i18n:govoplan-organizations.units.e14d0d92": "Units",
"i18n:govoplan-organizations.unlimited.35569464": "Unlimited",
"i18n:govoplan-organizations.update_assignment.bf130723": "Update assignment",
"i18n:govoplan-organizations.update_function.504f03e5": "Update function",
"i18n:govoplan-organizations.update_function_type.0b7e07d3": "Update function type",
"i18n:govoplan-organizations.update_relation.4c44ce83": "Update relation",
"i18n:govoplan-organizations.update_relation_type.e6e7f30c": "Update relation type",
"i18n:govoplan-organizations.update_structure.b2e25446": "Update structure",
"i18n:govoplan-organizations.update_unit.67e2500f": "Update unit",
"i18n:govoplan-organizations.update_unit_type.b96fc0ad": "Update unit type",
"i18n:govoplan-organizations.write_permission_required.8b09fd67": "You do not have write permission for this organization section."
},
de: {
@@ -99,26 +140,41 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-organizations.add_function_type.90d793f1": "Funktionstyp hinzufügen",
"i18n:govoplan-organizations.add_relation.6b6e67ea": "Beziehung hinzufügen",
"i18n:govoplan-organizations.add_relation_type.2ad19d03": "Beziehungstyp hinzufügen",
"i18n:govoplan-organizations.add_root_unit.1ef8a9f5": "Wurzeleinheit hinzufügen",
"i18n:govoplan-organizations.add_structure.b722042a": "Struktur hinzufügen",
"i18n:govoplan-organizations.add_subunit.8256a8f7": "Untereinheit hinzufügen",
"i18n:govoplan-organizations.add_unit.8fa12fb1": "Einheit hinzufügen",
"i18n:govoplan-organizations.add_unit_type.58f2c05a": "Einheitstyp hinzufügen",
"i18n:govoplan-organizations.allow_cycles.31327578": "Zyklen erlauben",
"i18n:govoplan-organizations.applies_to_subunits.7a15f741": "Gilt für Untereinheiten",
"i18n:govoplan-organizations.assignment_added.94263d1b": "Funktionszuordnung hinzugefügt.",
"i18n:govoplan-organizations.assignment_updated.680b6e33": "Funktionszuordnung aktualisiert.",
"i18n:govoplan-organizations.assignments.278f513e": "Zuordnungen",
"i18n:govoplan-organizations.audit_and_retention.3ba1d2fc": "Audit und Aufbewahrung",
"i18n:govoplan-organizations.audit_detail_level.7397355d": "Audit-Detailgrad",
"i18n:govoplan-organizations.audit_full.0c83669c": "Vollständig",
"i18n:govoplan-organizations.audit_retention_help.42dec57d": "Steuert, wie viele Details zu Organisationsänderungen für Prüfung, Fehlersuche und Nachweise aufbewahrt werden.",
"i18n:govoplan-organizations.audit_standard.785d981f": "Standard",
"i18n:govoplan-organizations.audit_summary.6a7d68a5": "Zusammenfassung",
"i18n:govoplan-organizations.classification.3e9f6c3a": "Klassifikation",
"i18n:govoplan-organizations.allow_tenant_model_customization.2425d751": "Mandantenadmins dürfen das Organisations-Metamodell anpassen",
"i18n:govoplan-organizations.change_retention_days.71bbd140": "Änderungsdetails zur Organisation in Tagen aufbewahren",
"i18n:govoplan-organizations.cancel_edit.309c2a6f": "Bearbeitung abbrechen",
"i18n:govoplan-organizations.concrete_model.4e9c531a": "Konkretes Modell",
"i18n:govoplan-organizations.deactivate.46ab070d": "Deaktivieren",
"i18n:govoplan-organizations.delegable.b4f0137d": "Delegierbar",
"i18n:govoplan-organizations.description.55f8ebc8": "Beschreibung",
"i18n:govoplan-organizations.direct_assignment.2a068aac": "direkt",
"i18n:govoplan-organizations.discard_organization_drafts.766d5f46": "Organisationsentwürfe verwerfen",
"i18n:govoplan-organizations.edit.7dce1220": "Bearbeiten",
"i18n:govoplan-organizations.function.28822f3a": "Funktion",
"i18n:govoplan-organizations.function_added.e2266702": "Funktion hinzugefügt.",
"i18n:govoplan-organizations.function_assignments.4c5c6dae": "Funktionszuordnungen",
"i18n:govoplan-organizations.function_type.501fe7c0": "Funktionstyp",
"i18n:govoplan-organizations.function_type_added.2cd9e899": "Funktionstyp hinzugefügt.",
"i18n:govoplan-organizations.function_type_updated.1a4f29f6": "Funktionstyp aktualisiert.",
"i18n:govoplan-organizations.function_types.172c01fe": "Funktionstypen",
"i18n:govoplan-organizations.function_updated.65016009": "Funktion aktualisiert.",
"i18n:govoplan-organizations.functions.805dc49b": "Funktionen",
"i18n:govoplan-organizations.hierarchical.8964f313": "Hierarchisch",
"i18n:govoplan-organizations.hierarchy.74797f37": "Hierarchie",
@@ -127,9 +183,12 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-organizations.inactive.09af574c": "Inaktiv",
"i18n:govoplan-organizations.kind.794c9d9c": "Art",
"i18n:govoplan-organizations.loading_organization_model.846aa317": "Organisationsmodell wird geladen...",
"i18n:govoplan-organizations.loading_organization_settings.c6008db8": "Organisationseinstellungen werden geladen...",
"i18n:govoplan-organizations.membership.4531d86d": "Mitgliedschaft",
"i18n:govoplan-organizations.meta_model.7398487c": "Metamodell",
"i18n:govoplan-organizations.model.20f35e63": "Modell",
"i18n:govoplan-organizations.model_governance.6aa18fd0": "Modell-Governance",
"i18n:govoplan-organizations.model_governance_help.0fa69021": "Diese Schalter legen fest, ob der Mandant sein Organisationsmodell direkt ändern darf oder geregelte Änderungsanträge erfassen muss.",
"i18n:govoplan-organizations.name.709a2322": "Name",
"i18n:govoplan-organizations.name_is_required.27cd3782": "Name ist erforderlich.",
"i18n:govoplan-organizations.network.a24d31c3": "Netzwerk",
@@ -139,23 +198,33 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-organizations.no_relation_types_found.6f90bb81": "Keine Beziehungstypen gefunden.",
"i18n:govoplan-organizations.no_relations_found.4e75b11d": "Keine Beziehungen gefunden.",
"i18n:govoplan-organizations.no_structures_found.20b382d0": "Keine Strukturen gefunden.",
"i18n:govoplan-organizations.no_unit_type.73e799f5": "Kein Einheitstyp",
"i18n:govoplan-organizations.no_unit_types_found.c81fb2a7": "Keine Einheitstypen gefunden.",
"i18n:govoplan-organizations.no_units_found.eea2dd0c": "Keine Einheiten gefunden.",
"i18n:govoplan-organizations.none.334c4a4c": "Keine",
"i18n:govoplan-organizations.optional_account_id.5fcf495c": "Optionale Konto-ID",
"i18n:govoplan-organizations.organization_model.4f924c0e": "Organisationsmodell",
"i18n:govoplan-organizations.organization_model_admin_description.35dc9f10": "Konfiguriere mandantenbezogene Einheitstypen, parallele Strukturen, Beziehungstypen und Funktionstypen des Organisationsmoduls.",
"i18n:govoplan-organizations.organization_settings.c9ab9829": "Organisationseinstellungen",
"i18n:govoplan-organizations.organization_settings_description.35dc9f10": "Konfiguriere Governance, Audit und Aufbewahrung für Organisationsänderungen auf Mandantenebene.",
"i18n:govoplan-organizations.organization_settings_saved.bfbcfdfa": "Organisationseinstellungen gespeichert.",
"i18n:govoplan-organizations.organization_tree.e5bfb195": "Organisationsbaum",
"i18n:govoplan-organizations.organizations.220edf64": "Organisationen",
"i18n:govoplan-organizations.organizations_intro.4e67c4bb": "Modelliere Organisationstypen, konkrete Einheiten, Strukturen, Funktionen und identitätsbezogene Funktionszuordnungen.",
"i18n:govoplan-organizations.parent_unit.1986c35e": "Übergeordnete Einheit",
"i18n:govoplan-organizations.reactivate.58f2855d": "Reaktivieren",
"i18n:govoplan-organizations.relation_added.ca90461a": "Beziehung hinzugefügt.",
"i18n:govoplan-organizations.relation_updated.c6212776": "Beziehung aktualisiert.",
"i18n:govoplan-organizations.relation_type.d0aee2e7": "Beziehungstyp",
"i18n:govoplan-organizations.relation_type_added.6f1edff1": "Beziehungstyp hinzugefügt.",
"i18n:govoplan-organizations.relation_type_updated.5ac57ec7": "Beziehungstyp aktualisiert.",
"i18n:govoplan-organizations.relation_types.e5890528": "Beziehungstypen",
"i18n:govoplan-organizations.relations.1c796711": "Beziehungen",
"i18n:govoplan-organizations.reload.cce71553": "Neu laden",
"i18n:govoplan-organizations.require_model_change_requests.83454cad": "Änderungen am Organisationsmodell erfordern erfasste Änderungsanträge",
"i18n:govoplan-organizations.save_settings.913aba9f": "Einstellungen speichern",
"i18n:govoplan-organizations.save_drafts.606f9401": "Entwürfe speichern",
"i18n:govoplan-organizations.saving.56a2285c": "Speichern...",
"i18n:govoplan-organizations.select_function.4895a67d": "Funktion auswählen",
"i18n:govoplan-organizations.select_function_type.2f426c43": "Funktionstyp auswählen",
"i18n:govoplan-organizations.select_relation_type.73f92478": "Beziehungstyp auswählen",
@@ -164,21 +233,34 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-organizations.select_target_unit.8d607541": "Ziel-Einheit auswählen",
"i18n:govoplan-organizations.select_unit.013bf13a": "Einheit auswählen",
"i18n:govoplan-organizations.select_unit_type.2c71d14f": "Einheitstyp auswählen",
"i18n:govoplan-organizations.selected_parent.0e013d44": "Ausgewählte übergeordnete Einheit:",
"i18n:govoplan-organizations.slug.094da9b9": "Slug",
"i18n:govoplan-organizations.source.6da13add": "Quelle",
"i18n:govoplan-organizations.source_unit.2fbd8baa": "Quell-Einheit",
"i18n:govoplan-organizations.status.bae7d5be": "Status",
"i18n:govoplan-organizations.structure.7732fb0b": "Struktur",
"i18n:govoplan-organizations.structure_added.f6cf8e74": "Struktur hinzugefügt.",
"i18n:govoplan-organizations.structure_updated.2591e75f": "Struktur aktualisiert.",
"i18n:govoplan-organizations.structures.f9b7f3b4": "Strukturen",
"i18n:govoplan-organizations.target_unit.a1507d86": "Ziel-Einheit",
"i18n:govoplan-organizations.type.599dcce2": "Typ",
"i18n:govoplan-organizations.unit.8fe4d595": "Einheit",
"i18n:govoplan-organizations.unit_added.760a8512": "Einheit hinzugefügt.",
"i18n:govoplan-organizations.unit_updated.15f02216": "Einheit aktualisiert.",
"i18n:govoplan-organizations.unit_type.9e62810d": "Einheitstyp",
"i18n:govoplan-organizations.unit_type_added.e017dc8c": "Einheitstyp hinzugefügt.",
"i18n:govoplan-organizations.unit_type_updated.6d8f2a16": "Einheitstyp aktualisiert.",
"i18n:govoplan-organizations.unit_types.c7afc174": "Einheitstypen",
"i18n:govoplan-organizations.units.e14d0d92": "Einheiten",
"i18n:govoplan-organizations.unlimited.35569464": "Unbegrenzt",
"i18n:govoplan-organizations.update_assignment.bf130723": "Zuordnung aktualisieren",
"i18n:govoplan-organizations.update_function.504f03e5": "Funktion aktualisieren",
"i18n:govoplan-organizations.update_function_type.0b7e07d3": "Funktionstyp aktualisieren",
"i18n:govoplan-organizations.update_relation.4c44ce83": "Beziehung aktualisieren",
"i18n:govoplan-organizations.update_relation_type.e6e7f30c": "Beziehungstyp aktualisieren",
"i18n:govoplan-organizations.update_structure.b2e25446": "Struktur aktualisieren",
"i18n:govoplan-organizations.update_unit.67e2500f": "Einheit aktualisieren",
"i18n:govoplan-organizations.update_unit_type.b96fc0ad": "Einheitstyp aktualisieren",
"i18n:govoplan-organizations.write_permission_required.8b09fd67": "Du hast keine Schreibberechtigung für diesen Organisationsbereich."
}
};

View File

@@ -21,11 +21,11 @@ const translations = {
const organizationAdminSections: AdminSectionsUiCapability = {
sections: [
{
id: "tenant-organization-model",
label: "i18n:govoplan-organizations.organization_model.4f924c0e",
id: "tenant-organization-settings",
label: "i18n:govoplan-organizations.organization_settings.c9ab9829",
group: "TENANT",
order: 85,
anyOf: ["organizations:model:read", "admin:settings:read"],
anyOf: ["organizations:settings:read", "admin:settings:read"],
render: ({ settings, auth }) => createElement(OrganizationsAdminPanel, { settings, auth })
}
]

View File

@@ -14,6 +14,11 @@
max-width: 100%;
}
.organizations-settings-grid {
display: grid;
gap: 18px;
}
.organizations-heading {
margin-bottom: 4px;
}
@@ -33,6 +38,13 @@
align-items: start;
}
.organizations-editor-grid {
display: grid;
grid-template-columns: minmax(260px, 340px) minmax(0, 1fr);
gap: 18px;
align-items: start;
}
.organizations-form-panel,
.organizations-table-panel {
min-width: 0;
@@ -53,6 +65,18 @@
grid-column: 1 / -1;
}
.organizations-form-actions,
.organizations-row-actions {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.organizations-row-actions {
justify-content: flex-end;
}
.organizations-check-list {
display: grid;
gap: 10px;
@@ -73,6 +97,51 @@
gap: 18px;
}
.organizations-tree-toolbar {
display: flex;
justify-content: flex-start;
margin-bottom: 12px;
}
.organizations-tree-list {
display: grid;
gap: 4px;
max-height: 640px;
overflow: auto;
}
.organizations-tree-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 6px;
}
.organizations-tree-node {
display: grid;
gap: 2px;
width: 100%;
min-width: 0;
padding: 8px 10px;
border: 1px solid transparent;
border-radius: 7px;
background: transparent;
color: var(--text);
text-align: left;
cursor: pointer;
}
.organizations-tree-node:hover,
.organizations-tree-node.active {
border-color: var(--line);
background: var(--surface-muted, #f6f7f9);
}
.organizations-tree-node span {
color: var(--muted);
font-size: 12px;
}
.organizations-field-note {
margin: 8px 0 0;
color: var(--muted);
@@ -90,7 +159,8 @@
}
@media (max-width: 1100px) {
.organizations-section-grid {
.organizations-section-grid,
.organizations-editor-grid {
grid-template-columns: 1fr;
}
}