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, OrganizationFunctionType,
OrganizationRelation, OrganizationRelation,
OrganizationRelationType, OrganizationRelationType,
OrganizationTenantSettings,
OrganizationStructure, OrganizationStructure,
OrganizationUnit, OrganizationUnit,
OrganizationUnitType, OrganizationUnitType,
@@ -33,6 +34,8 @@ from .schemas import (
OrganizationModelResponse, OrganizationModelResponse,
OrganizationRelationItem, OrganizationRelationItem,
OrganizationRelationTypeItem, OrganizationRelationTypeItem,
OrganizationSettingsItem,
OrganizationSettingsUpdateRequest,
OrganizationStructureItem, OrganizationStructureItem,
OrganizationUnitItem, OrganizationUnitItem,
OrganizationUnitTypeItem, OrganizationUnitTypeItem,
@@ -61,6 +64,8 @@ ORG_MODEL_WRITE_SCOPES = ("organizations:model:write",)
ORG_UNIT_WRITE_SCOPES = ("organizations:unit:write",) ORG_UNIT_WRITE_SCOPES = ("organizations:unit:write",)
ORG_FUNCTION_WRITE_SCOPES = ("organizations:function:write",) ORG_FUNCTION_WRITE_SCOPES = ("organizations:function:write",)
ORG_ASSIGN_SCOPES = ("organizations:function:assign",) 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]+") SLUG_RE = re.compile(r"[^a-z0-9]+")
ModelT = TypeVar("ModelT") ModelT = TypeVar("ModelT")
@@ -129,6 +134,21 @@ def _item_unit_type(item: OrganizationUnitType) -> OrganizationUnitTypeItem:
return OrganizationUnitTypeItem(**_row_fields(item)) 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: def _item_structure(item: OrganizationStructure) -> OrganizationStructureItem:
return OrganizationStructureItem(**_row_fields(item)) return OrganizationStructureItem(**_row_fields(item))
@@ -187,6 +207,48 @@ def _apply_slugged_update(session: Session, item: object, payload: object, tenan
setattr(item, "settings", value) 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) @router.get("/model", response_model=OrganizationModelResponse)
def get_organization_model( def get_organization_model(
session: Session = Depends(get_session), session: Session = Depends(get_session),
@@ -356,6 +418,8 @@ def update_unit(
raise _invalid("An organization unit cannot be its own parent.") raise _invalid("An organization unit cannot be its own parent.")
if payload.parent_id is not None: if payload.parent_id is not None:
_get_tenant_row(session, OrganizationUnit, payload.parent_id, tenant_id, "Parent organization unit") _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 item.parent_id = payload.parent_id
return _item_unit(_commit(session, item)) return _item_unit(_commit(session, item))
@@ -718,3 +782,17 @@ def _would_create_cycle(
query = query.filter(OrganizationRelation.id != exclude_relation_id) query = query.filter(OrganizationRelation.id != exclude_relation_id)
pending.extend(row[0] for row in query.all()) pending.extend(row[0] for row in query.all())
return False 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"] 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): class OrganizationUnitTypeItem(BaseModel):

View File

@@ -4,7 +4,7 @@ import uuid
from datetime import datetime from datetime import datetime
from typing import Any 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 sqlalchemy.orm import Mapped, mapped_column
from govoplan_core.db.base import Base, TimestampMixin 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) 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): class OrganizationStructure(Base, TimestampMixin):
__tablename__ = "organizations_structures" __tablename__ = "organizations_structures"
__table_args__ = (UniqueConstraint("tenant_id", "slug", name="uq_organizations_structures_tenant_slug"),) __table_args__ = (UniqueConstraint("tenant_id", "slug", name="uq_organizations_structures_tenant_slug"),)
@@ -172,6 +185,7 @@ __all__ = [
"OrganizationRelation", "OrganizationRelation",
"OrganizationRelationType", "OrganizationRelationType",
"OrganizationStructure", "OrganizationStructure",
"OrganizationTenantSettings",
"OrganizationUnit", "OrganizationUnit",
"OrganizationUnitType", "OrganizationUnitType",
"new_uuid", "new_uuid",

View File

@@ -46,6 +46,8 @@ def _permission(scope: str, label: str, description: str) -> PermissionDefinitio
PERMISSIONS = ( PERMISSIONS = (
_permission("organizations:model:read", "View organization model", "Read organization meta-model definitions such as unit types, structures, and relation types."), _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: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: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: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."), _permission("organizations:function:read", "View organization functions", "Read function definitions and identity-held assignments."),
@@ -64,7 +66,7 @@ ROLE_TEMPLATES = (
slug="organization_viewer", slug="organization_viewer",
name="Organization viewer", name="Organization viewer",
description="Read organization model, organization units, and function assignments.", 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=( uninstall_guard_providers=(
persistent_table_uninstall_guard( persistent_table_uninstall_guard(
organization_models.OrganizationUnitType, organization_models.OrganizationUnitType,
organization_models.OrganizationTenantSettings,
organization_models.OrganizationStructure, organization_models.OrganizationStructure,
organization_models.OrganizationRelationType, organization_models.OrganizationRelationType,
organization_models.OrganizationRelation, 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"]) _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: def _create_structures() -> None:
if "organizations_structures" not in _tables(): if "organizations_structures" not in _tables():
op.create_table( op.create_table(
@@ -271,6 +289,7 @@ def _create_function_assignments() -> None:
def upgrade() -> None: def upgrade() -> None:
_create_unit_types() _create_unit_types()
_create_tenant_settings()
_create_structures() _create_structures()
_create_relation_types() _create_relation_types()
_create_units() _create_units()
@@ -293,6 +312,7 @@ def downgrade() -> None:
"organizations_relation_types", "organizations_relation_types",
"organizations_structures", "organizations_structures",
"organizations_function_types", "organizations_function_types",
"organizations_tenant_settings",
"organizations_unit_types", "organizations_unit_types",
): ):
if table_name in _tables(): if table_name in _tables():

View File

@@ -13,6 +13,27 @@ export type OrganizationUnitTypeItem = {
}; };
export type OrganizationStructureKind = "hierarchy" | "network" | "membership" | "classification"; 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 = { export type OrganizationStructureItem = {
id: string; id: string;
@@ -205,6 +226,14 @@ export function getOrganizationModel(settings: ApiSettings): Promise<Organizatio
return apiFetch<OrganizationModel>(settings, "/api/v1/organizations/model"); 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> { export function createUnitType(settings: ApiSettings, payload: SluggedCreatePayload): Promise<OrganizationUnitTypeItem> {
return post(settings, "/api/v1/organizations/unit-types", payload); return post(settings, "/api/v1/organizations/unit-types", payload);
} }

View File

@@ -1,18 +1,156 @@
import type { ApiSettings, AuthInfo } from "@govoplan/core-webui"; import { useEffect, useState } from "react";
import OrganizationsPage, { type OrganizationSection } from "./OrganizationsPage"; 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 }) { 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 ( return (
<OrganizationsPage <AdminPageLayout
settings={settings} title="i18n:govoplan-organizations.organization_settings.c9ab9829"
auth={auth} description="i18n:govoplan-organizations.organization_settings_description.35dc9f10"
mode="admin" loading={loading}
initialSection="model" loadingLabel="i18n:govoplan-organizations.loading_organization_settings.c6008db8"
availableSections={MODEL_SECTIONS} error={error}
title="i18n:govoplan-organizations.organization_model.4f924c0e" success={success}
description="i18n:govoplan-organizations.organization_model_admin_description.35dc9f10" 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 { 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 { import {
ApiError, ApiError,
Button, 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 { function isSluggedDirty(draft: SluggedDraft): boolean {
return draft.name.trim() !== "" || draft.slug.trim() !== "" || draft.description.trim() !== "" || !draft.is_active; 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({ function SluggedFields({
draft, draft,
onChange, onChange,
@@ -354,6 +383,15 @@ export default function OrganizationsPage({
const [functionTypeDraft, setFunctionTypeDraft] = useState<FunctionTypeDraft>(() => emptyFunctionTypeDraft()); const [functionTypeDraft, setFunctionTypeDraft] = useState<FunctionTypeDraft>(() => emptyFunctionTypeDraft());
const [functionDraft, setFunctionDraft] = useState<FunctionDraft>(() => emptyFunctionDraft()); const [functionDraft, setFunctionDraft] = useState<FunctionDraft>(() => emptyFunctionDraft());
const [assignmentDraft, setAssignmentDraft] = useState<AssignmentDraft>(() => emptyAssignmentDraft()); 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 canWriteModel = hasScope(auth, "organizations:model:write");
const canWriteUnits = hasScope(auth, "organizations:unit: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 unitById = useMemo(() => mapById(model.units), [model.units]);
const functionTypeById = useMemo(() => mapById(model.function_types), [model.function_types]); const functionTypeById = useMemo(() => mapById(model.function_types), [model.function_types]);
const functionById = useMemo(() => mapById(model.functions), [model.functions]); 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) || const hasDirtyDraft = isSluggedDirty(unitTypeDraft) ||
isSluggedDirty(structureDraft) || isSluggedDirty(structureDraft) ||
@@ -396,6 +443,10 @@ export default function OrganizationsPage({
if (!sectionSet.has(active)) setActive(firstAvailableSection); if (!sectionSet.has(active)) setActive(firstAvailableSection);
}, [active, firstAvailableSection, sectionSet]); }, [active, firstAvailableSection, sectionSet]);
useEffect(() => {
if (selectedUnitId && !unitById.has(selectedUnitId)) setSelectedUnitId("");
}, [selectedUnitId, unitById]);
const runAction = useCallback(async (action: () => Promise<unknown>, successMessage = ""): Promise<boolean> => { const runAction = useCallback(async (action: () => Promise<unknown>, successMessage = ""): Promise<boolean> => {
setBusy(true); setBusy(true);
setError(""); setError("");
@@ -422,6 +473,14 @@ export default function OrganizationsPage({
setFunctionTypeDraft(emptyFunctionTypeDraft()); setFunctionTypeDraft(emptyFunctionTypeDraft());
setFunctionDraft(emptyFunctionDraft()); setFunctionDraft(emptyFunctionDraft());
setAssignmentDraft(emptyAssignmentDraft()); setAssignmentDraft(emptyAssignmentDraft());
setEditingUnitTypeId(null);
setEditingStructureId(null);
setEditingRelationTypeId(null);
setEditingUnitId(null);
setEditingRelationId(null);
setEditingFunctionTypeId(null);
setEditingFunctionId(null);
setEditingAssignmentId(null);
}, []); }, []);
function rejectMissingName(): Promise<boolean> { function rejectMissingName(): Promise<boolean> {
@@ -438,20 +497,33 @@ export default function OrganizationsPage({
event?.preventDefault(); event?.preventDefault();
if (!canWriteModel) return rejectMissingWritePermission(); if (!canWriteModel) return rejectMissingWritePermission();
if (!unitTypeDraft.name.trim()) return rejectMissingName(); if (!unitTypeDraft.name.trim()) return rejectMissingName();
const ok = await runAction(() => createUnitType(settings, sluggedPayload(unitTypeDraft)), "i18n:govoplan-organizations.unit_type_added.e017dc8c"); const ok = await runAction(
if (ok) setUnitTypeDraft(emptySluggedDraft()); () => 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; return ok;
}, [canWriteModel, runAction, settings, unitTypeDraft]); }, [canWriteModel, editingUnitTypeId, runAction, settings, unitTypeDraft]);
const submitStructure = useCallback(async (event?: FormEvent): Promise<boolean> => { const submitStructure = useCallback(async (event?: FormEvent): Promise<boolean> => {
event?.preventDefault(); event?.preventDefault();
if (!canWriteModel) return rejectMissingWritePermission(); if (!canWriteModel) return rejectMissingWritePermission();
if (!structureDraft.name.trim()) return rejectMissingName(); if (!structureDraft.name.trim()) return rejectMissingName();
const payload: StructureCreatePayload = { ...sluggedPayload(structureDraft), structure_kind: structureDraft.structure_kind }; const payload: StructureCreatePayload = { ...sluggedPayload(structureDraft), structure_kind: structureDraft.structure_kind };
const ok = await runAction(() => createStructure(settings, payload), "i18n:govoplan-organizations.structure_added.f6cf8e74"); const patchPayload: Partial<StructureCreatePayload> = { ...sluggedPatchPayload(structureDraft), structure_kind: structureDraft.structure_kind };
if (ok) setStructureDraft(emptyStructureDraft()); 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; return ok;
}, [canWriteModel, runAction, settings, structureDraft]); }, [canWriteModel, editingStructureId, runAction, settings, structureDraft]);
const submitRelationType = useCallback(async (event?: FormEvent): Promise<boolean> => { const submitRelationType = useCallback(async (event?: FormEvent): Promise<boolean> => {
event?.preventDefault(); event?.preventDefault();
@@ -465,10 +537,24 @@ export default function OrganizationsPage({
is_hierarchical: relationTypeDraft.is_hierarchical, is_hierarchical: relationTypeDraft.is_hierarchical,
allow_cycles: relationTypeDraft.allow_cycles allow_cycles: relationTypeDraft.allow_cycles
}; };
const ok = await runAction(() => createRelationType(settings, payload), "i18n:govoplan-organizations.relation_type_added.6f1edff1"); const patchPayload: Partial<RelationTypeCreatePayload> = {
if (ok) setRelationTypeDraft(emptyRelationTypeDraft()); ...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; return ok;
}, [canWriteModel, relationTypeDraft, runAction, settings]); }, [canWriteModel, editingRelationTypeId, relationTypeDraft, runAction, settings]);
const submitUnit = useCallback(async (event?: FormEvent): Promise<boolean> => { const submitUnit = useCallback(async (event?: FormEvent): Promise<boolean> => {
event?.preventDefault(); event?.preventDefault();
@@ -479,10 +565,21 @@ export default function OrganizationsPage({
unit_type_id: textOrNull(unitDraft.unit_type_id), unit_type_id: textOrNull(unitDraft.unit_type_id),
parent_id: textOrNull(unitDraft.parent_id) parent_id: textOrNull(unitDraft.parent_id)
}; };
const ok = await runAction(() => createUnit(settings, payload), "i18n:govoplan-organizations.unit_added.760a8512"); const patchPayload: Partial<UnitCreatePayload> = {
if (ok) setUnitDraft(emptyUnitDraft()); ...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; return ok;
}, [canWriteUnits, runAction, settings, unitDraft]); }, [canWriteUnits, editingUnitId, runAction, settings, unitDraft]);
const submitRelation = useCallback(async (event?: FormEvent): Promise<boolean> => { const submitRelation = useCallback(async (event?: FormEvent): Promise<boolean> => {
event?.preventDefault(); event?.preventDefault();
@@ -492,10 +589,17 @@ export default function OrganizationsPage({
return false; return false;
} }
const payload: RelationCreatePayload = { ...relationDraft, settings: {} }; const payload: RelationCreatePayload = { ...relationDraft, settings: {} };
const ok = await runAction(() => createRelation(settings, payload), "i18n:govoplan-organizations.relation_added.ca90461a"); const patchPayload: Partial<RelationCreatePayload> = { ...relationDraft };
if (ok) setRelationDraft(emptyRelationDraft()); 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; return ok;
}, [canWriteUnits, relationDraft, runAction, settings]); }, [canWriteUnits, editingRelationId, relationDraft, runAction, settings]);
const submitFunctionType = useCallback(async (event?: FormEvent): Promise<boolean> => { const submitFunctionType = useCallback(async (event?: FormEvent): Promise<boolean> => {
event?.preventDefault(); event?.preventDefault();
@@ -507,10 +611,22 @@ export default function OrganizationsPage({
delegable: functionTypeDraft.delegable, delegable: functionTypeDraft.delegable,
act_in_place_allowed: functionTypeDraft.act_in_place_allowed act_in_place_allowed: functionTypeDraft.act_in_place_allowed
}; };
const ok = await runAction(() => createFunctionType(settings, payload), "i18n:govoplan-organizations.function_type_added.2cd9e899"); const patchPayload: Partial<FunctionTypeCreatePayload> = {
if (ok) setFunctionTypeDraft(emptyFunctionTypeDraft()); ...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; return ok;
}, [canWriteModel, functionTypeDraft, runAction, settings]); }, [canWriteModel, editingFunctionTypeId, functionTypeDraft, runAction, settings]);
const submitFunction = useCallback(async (event?: FormEvent): Promise<boolean> => { const submitFunction = useCallback(async (event?: FormEvent): Promise<boolean> => {
event?.preventDefault(); event?.preventDefault();
@@ -527,10 +643,23 @@ export default function OrganizationsPage({
delegable: functionDraft.delegable, delegable: functionDraft.delegable,
act_in_place_allowed: functionDraft.act_in_place_allowed act_in_place_allowed: functionDraft.act_in_place_allowed
}; };
const ok = await runAction(() => createFunction(settings, payload), "i18n:govoplan-organizations.function_added.e2266702"); const patchPayload: Partial<FunctionCreatePayload> = {
if (ok) setFunctionDraft(emptyFunctionDraft()); ...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; return ok;
}, [canWriteFunctions, functionDraft, runAction, settings]); }, [canWriteFunctions, editingFunctionId, functionDraft, runAction, settings]);
const submitAssignment = useCallback(async (event?: FormEvent): Promise<boolean> => { const submitAssignment = useCallback(async (event?: FormEvent): Promise<boolean> => {
event?.preventDefault(); event?.preventDefault();
@@ -550,10 +679,16 @@ export default function OrganizationsPage({
is_active: assignmentDraft.is_active, is_active: assignmentDraft.is_active,
settings: {} settings: {}
}; };
const ok = await runAction(() => createFunctionAssignment(settings, payload), "i18n:govoplan-organizations.assignment_added.94263d1b"); const ok = await runAction(
if (ok) setAssignmentDraft(emptyAssignmentDraft()); () => 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; return ok;
}, [assignmentDraft, canAssignFunctions, runAction, settings]); }, [assignmentDraft, canAssignFunctions, editingAssignmentId, runAction, settings]);
const saveDrafts = useCallback(async (): Promise<boolean> => { const saveDrafts = useCallback(async (): Promise<boolean> => {
if (isSluggedDirty(unitTypeDraft) && !(await submitUnitType())) return false; 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 })); void runAction(() => patchFunctionAssignment(settings, item.id, { is_active: !item.is_active }));
}, [runAction, settings]); }, [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>[] = [ const unitTypeColumns: DataGridColumn<OrganizationUnitTypeItem>[] = [
{ id: "name", header: "i18n:govoplan-organizations.name.709a2322", minWidth: 180, sortable: true, filterable: true, value: (row) => row.name }, { 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: "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: "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: "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>[] = [ 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: "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: "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: "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>[] = [ 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: "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: "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: "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>[] = [ 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: "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: "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: "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>[] = [ 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: "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: "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: "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>[] = [ 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: "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: "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: "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>[] = [ 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: "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: "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: "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>[] = [ 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: "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: "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: "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; 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 = ( const content = (
<div className={`${mode === "workspace" ? "content-pad " : ""}organizations-page ${mode === "admin" ? "organizations-admin-page" : ""}`.trim()}> <div className={`${mode === "workspace" ? "content-pad " : ""}organizations-page ${mode === "admin" ? "organizations-admin-page" : ""}`.trim()}>
<div className="page-heading split organizations-heading"> <div className="page-heading split organizations-heading">
@@ -754,7 +984,10 @@ export default function OrganizationsPage({
<Card title="i18n:govoplan-organizations.unit_types.c7afc174"> <Card title="i18n:govoplan-organizations.unit_types.c7afc174">
<form className="organizations-form-grid" onSubmit={(event) => void submitUnitType(event)}> <form className="organizations-form-grid" onSubmit={(event) => void submitUnitType(event)}>
<SluggedFields draft={unitTypeDraft} onChange={setUnitTypeDraft} disabled={!canWriteModel || busy} /> <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> </form>
</Card> </Card>
<Card title="i18n:govoplan-organizations.unit_types.c7afc174"> <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>)} {STRUCTURE_KINDS.map((item) => <option key={item.value} value={item.value}>{item.label}</option>)}
</select> </select>
</FormField> </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> </form>
</Card> </Card>
<Card title="i18n:govoplan-organizations.structures.f9b7f3b4"> <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.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> <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> </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> </form>
</Card> </Card>
<Card title="i18n:govoplan-organizations.relation_types.e5890528"> <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.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> <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> </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> </form>
</Card> </Card>
<Card title="i18n:govoplan-organizations.function_types.172c01fe"> <Card title="i18n:govoplan-organizations.function_types.172c01fe">
@@ -835,9 +1077,45 @@ 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() { function renderUnitsSection() {
return ( return (
<div className="organizations-section-grid"> <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"> <Card title="i18n:govoplan-organizations.add_unit.8fa12fb1">
<form className="organizations-form-grid" onSubmit={(event) => void submitUnit(event)}> <form className="organizations-form-grid" onSubmit={(event) => void submitUnit(event)}>
<SluggedFields draft={unitDraft} onChange={setUnitDraft} disabled={!canWriteUnits || busy} /> <SluggedFields draft={unitDraft} onChange={setUnitDraft} disabled={!canWriteUnits || busy} />
@@ -850,16 +1128,25 @@ export default function OrganizationsPage({
<FormField label="i18n:govoplan-organizations.parent_unit.1986c35e"> <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 })}> <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> <option value="">i18n:govoplan-organizations.none.334c4a4c</option>
{model.units.map((item) => <option key={item.id} value={item.id}>{item.name}</option>)} {model.units.filter((item) => item.id !== editingUnitId).map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
</select> </select>
</FormField> </FormField>
<Button className="wide" type="submit" variant="primary" disabled={!canWriteUnits || busy}>i18n:govoplan-organizations.add_unit.8fa12fb1</Button> {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> </form>
</Card> </Card>
<Card title="i18n:govoplan-organizations.units.e14d0d92"> <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" /> <DataGrid id="organizations-units" rows={model.units} columns={unitColumns} getRowKey={(row) => row.id} emptyText="i18n:govoplan-organizations.no_units_found.eea2dd0c" initialFit="container" />
</Card> </Card>
</div> </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 })} /> <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> <span>i18n:govoplan-organizations.active.a733b809</span>
</label> </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> </form>
</Card> </Card>
<Card title="i18n:govoplan-organizations.relations.1c796711"> <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.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> <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> </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> </form>
</Card> </Card>
<Card title="i18n:govoplan-organizations.functions.805dc49b"> <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.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> <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> </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> </form>
</Card> </Card>
<Card title="i18n:govoplan-organizations.function_assignments.4c5c6dae"> <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_function_type.90d793f1": "Add function type",
"i18n:govoplan-organizations.add_relation.6b6e67ea": "Add relation", "i18n:govoplan-organizations.add_relation.6b6e67ea": "Add relation",
"i18n:govoplan-organizations.add_relation_type.2ad19d03": "Add relation type", "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_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.8fa12fb1": "Add unit",
"i18n:govoplan-organizations.add_unit_type.58f2c05a": "Add unit type", "i18n:govoplan-organizations.add_unit_type.58f2c05a": "Add unit type",
"i18n:govoplan-organizations.allow_cycles.31327578": "Allow cycles", "i18n:govoplan-organizations.allow_cycles.31327578": "Allow cycles",
"i18n:govoplan-organizations.applies_to_subunits.7a15f741": "Applies to subunits", "i18n:govoplan-organizations.applies_to_subunits.7a15f741": "Applies to subunits",
"i18n:govoplan-organizations.assignment_added.94263d1b": "Function assignment added.", "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.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.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.concrete_model.4e9c531a": "Concrete model",
"i18n:govoplan-organizations.deactivate.46ab070d": "Deactivate", "i18n:govoplan-organizations.deactivate.46ab070d": "Deactivate",
"i18n:govoplan-organizations.delegable.b4f0137d": "Delegable", "i18n:govoplan-organizations.delegable.b4f0137d": "Delegable",
"i18n:govoplan-organizations.description.55f8ebc8": "Description", "i18n:govoplan-organizations.description.55f8ebc8": "Description",
"i18n:govoplan-organizations.direct_assignment.2a068aac": "direct", "i18n:govoplan-organizations.direct_assignment.2a068aac": "direct",
"i18n:govoplan-organizations.discard_organization_drafts.766d5f46": "Discard organization drafts", "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.28822f3a": "Function",
"i18n:govoplan-organizations.function_added.e2266702": "Function added.", "i18n:govoplan-organizations.function_added.e2266702": "Function added.",
"i18n:govoplan-organizations.function_assignments.4c5c6dae": "Function assignments", "i18n:govoplan-organizations.function_assignments.4c5c6dae": "Function assignments",
"i18n:govoplan-organizations.function_type.501fe7c0": "Function type", "i18n:govoplan-organizations.function_type.501fe7c0": "Function type",
"i18n:govoplan-organizations.function_type_added.2cd9e899": "Function type added.", "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_types.172c01fe": "Function types",
"i18n:govoplan-organizations.function_updated.65016009": "Function updated.",
"i18n:govoplan-organizations.functions.805dc49b": "Functions", "i18n:govoplan-organizations.functions.805dc49b": "Functions",
"i18n:govoplan-organizations.hierarchical.8964f313": "Hierarchical", "i18n:govoplan-organizations.hierarchical.8964f313": "Hierarchical",
"i18n:govoplan-organizations.hierarchy.74797f37": "Hierarchy", "i18n:govoplan-organizations.hierarchy.74797f37": "Hierarchy",
@@ -37,9 +52,12 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-organizations.inactive.09af574c": "Inactive", "i18n:govoplan-organizations.inactive.09af574c": "Inactive",
"i18n:govoplan-organizations.kind.794c9d9c": "Kind", "i18n:govoplan-organizations.kind.794c9d9c": "Kind",
"i18n:govoplan-organizations.loading_organization_model.846aa317": "Loading organization model...", "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.membership.4531d86d": "Membership",
"i18n:govoplan-organizations.meta_model.7398487c": "Meta-model", "i18n:govoplan-organizations.meta_model.7398487c": "Meta-model",
"i18n:govoplan-organizations.model.20f35e63": "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.709a2322": "Name",
"i18n:govoplan-organizations.name_is_required.27cd3782": "Name is required.", "i18n:govoplan-organizations.name_is_required.27cd3782": "Name is required.",
"i18n:govoplan-organizations.network.a24d31c3": "Network", "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_relation_types_found.6f90bb81": "No relation types found.",
"i18n:govoplan-organizations.no_relations_found.4e75b11d": "No relations 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_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_unit_types_found.c81fb2a7": "No unit types found.",
"i18n:govoplan-organizations.no_units_found.eea2dd0c": "No units found.", "i18n:govoplan-organizations.no_units_found.eea2dd0c": "No units found.",
"i18n:govoplan-organizations.none.334c4a4c": "None", "i18n:govoplan-organizations.none.334c4a4c": "None",
"i18n:govoplan-organizations.optional_account_id.5fcf495c": "Optional account ID", "i18n:govoplan-organizations.optional_account_id.5fcf495c": "Optional account ID",
"i18n:govoplan-organizations.organization_model.4f924c0e": "Organization model", "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_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.220edf64": "Organizations",
"i18n:govoplan-organizations.organizations_intro.4e67c4bb": "Model organization types, concrete units, structures, functions, and identity-held function assignments.", "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.parent_unit.1986c35e": "Parent unit",
"i18n:govoplan-organizations.reactivate.58f2855d": "Reactivate", "i18n:govoplan-organizations.reactivate.58f2855d": "Reactivate",
"i18n:govoplan-organizations.relation_added.ca90461a": "Relation added.", "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.d0aee2e7": "Relation type",
"i18n:govoplan-organizations.relation_type_added.6f1edff1": "Relation type added.", "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.relation_types.e5890528": "Relation types",
"i18n:govoplan-organizations.relations.1c796711": "Relations", "i18n:govoplan-organizations.relations.1c796711": "Relations",
"i18n:govoplan-organizations.reload.cce71553": "Reload", "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.save_drafts.606f9401": "Save drafts",
"i18n:govoplan-organizations.saving.56a2285c": "Saving...",
"i18n:govoplan-organizations.select_function.4895a67d": "Select function", "i18n:govoplan-organizations.select_function.4895a67d": "Select function",
"i18n:govoplan-organizations.select_function_type.2f426c43": "Select function type", "i18n:govoplan-organizations.select_function_type.2f426c43": "Select function type",
"i18n:govoplan-organizations.select_relation_type.73f92478": "Select relation 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_target_unit.8d607541": "Select target unit",
"i18n:govoplan-organizations.select_unit.013bf13a": "Select unit", "i18n:govoplan-organizations.select_unit.013bf13a": "Select unit",
"i18n:govoplan-organizations.select_unit_type.2c71d14f": "Select unit type", "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.slug.094da9b9": "Slug",
"i18n:govoplan-organizations.source.6da13add": "Source", "i18n:govoplan-organizations.source.6da13add": "Source",
"i18n:govoplan-organizations.source_unit.2fbd8baa": "Source unit", "i18n:govoplan-organizations.source_unit.2fbd8baa": "Source unit",
"i18n:govoplan-organizations.status.bae7d5be": "Status", "i18n:govoplan-organizations.status.bae7d5be": "Status",
"i18n:govoplan-organizations.structure.7732fb0b": "Structure", "i18n:govoplan-organizations.structure.7732fb0b": "Structure",
"i18n:govoplan-organizations.structure_added.f6cf8e74": "Structure added.", "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.structures.f9b7f3b4": "Structures",
"i18n:govoplan-organizations.target_unit.a1507d86": "Target unit", "i18n:govoplan-organizations.target_unit.a1507d86": "Target unit",
"i18n:govoplan-organizations.type.599dcce2": "Type", "i18n:govoplan-organizations.type.599dcce2": "Type",
"i18n:govoplan-organizations.unit.8fe4d595": "Unit", "i18n:govoplan-organizations.unit.8fe4d595": "Unit",
"i18n:govoplan-organizations.unit_added.760a8512": "Unit added.", "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.9e62810d": "Unit type",
"i18n:govoplan-organizations.unit_type_added.e017dc8c": "Unit type added.", "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.unit_types.c7afc174": "Unit types",
"i18n:govoplan-organizations.units.e14d0d92": "Units", "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." "i18n:govoplan-organizations.write_permission_required.8b09fd67": "You do not have write permission for this organization section."
}, },
de: { de: {
@@ -99,26 +140,41 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-organizations.add_function_type.90d793f1": "Funktionstyp hinzufügen", "i18n:govoplan-organizations.add_function_type.90d793f1": "Funktionstyp hinzufügen",
"i18n:govoplan-organizations.add_relation.6b6e67ea": "Beziehung 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_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_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.8fa12fb1": "Einheit hinzufügen",
"i18n:govoplan-organizations.add_unit_type.58f2c05a": "Einheitstyp hinzufügen", "i18n:govoplan-organizations.add_unit_type.58f2c05a": "Einheitstyp hinzufügen",
"i18n:govoplan-organizations.allow_cycles.31327578": "Zyklen erlauben", "i18n:govoplan-organizations.allow_cycles.31327578": "Zyklen erlauben",
"i18n:govoplan-organizations.applies_to_subunits.7a15f741": "Gilt für Untereinheiten", "i18n:govoplan-organizations.applies_to_subunits.7a15f741": "Gilt für Untereinheiten",
"i18n:govoplan-organizations.assignment_added.94263d1b": "Funktionszuordnung hinzugefügt.", "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.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.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.concrete_model.4e9c531a": "Konkretes Modell",
"i18n:govoplan-organizations.deactivate.46ab070d": "Deaktivieren", "i18n:govoplan-organizations.deactivate.46ab070d": "Deaktivieren",
"i18n:govoplan-organizations.delegable.b4f0137d": "Delegierbar", "i18n:govoplan-organizations.delegable.b4f0137d": "Delegierbar",
"i18n:govoplan-organizations.description.55f8ebc8": "Beschreibung", "i18n:govoplan-organizations.description.55f8ebc8": "Beschreibung",
"i18n:govoplan-organizations.direct_assignment.2a068aac": "direkt", "i18n:govoplan-organizations.direct_assignment.2a068aac": "direkt",
"i18n:govoplan-organizations.discard_organization_drafts.766d5f46": "Organisationsentwürfe verwerfen", "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.28822f3a": "Funktion",
"i18n:govoplan-organizations.function_added.e2266702": "Funktion hinzugefügt.", "i18n:govoplan-organizations.function_added.e2266702": "Funktion hinzugefügt.",
"i18n:govoplan-organizations.function_assignments.4c5c6dae": "Funktionszuordnungen", "i18n:govoplan-organizations.function_assignments.4c5c6dae": "Funktionszuordnungen",
"i18n:govoplan-organizations.function_type.501fe7c0": "Funktionstyp", "i18n:govoplan-organizations.function_type.501fe7c0": "Funktionstyp",
"i18n:govoplan-organizations.function_type_added.2cd9e899": "Funktionstyp hinzugefügt.", "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_types.172c01fe": "Funktionstypen",
"i18n:govoplan-organizations.function_updated.65016009": "Funktion aktualisiert.",
"i18n:govoplan-organizations.functions.805dc49b": "Funktionen", "i18n:govoplan-organizations.functions.805dc49b": "Funktionen",
"i18n:govoplan-organizations.hierarchical.8964f313": "Hierarchisch", "i18n:govoplan-organizations.hierarchical.8964f313": "Hierarchisch",
"i18n:govoplan-organizations.hierarchy.74797f37": "Hierarchie", "i18n:govoplan-organizations.hierarchy.74797f37": "Hierarchie",
@@ -127,9 +183,12 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-organizations.inactive.09af574c": "Inaktiv", "i18n:govoplan-organizations.inactive.09af574c": "Inaktiv",
"i18n:govoplan-organizations.kind.794c9d9c": "Art", "i18n:govoplan-organizations.kind.794c9d9c": "Art",
"i18n:govoplan-organizations.loading_organization_model.846aa317": "Organisationsmodell wird geladen...", "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.membership.4531d86d": "Mitgliedschaft",
"i18n:govoplan-organizations.meta_model.7398487c": "Metamodell", "i18n:govoplan-organizations.meta_model.7398487c": "Metamodell",
"i18n:govoplan-organizations.model.20f35e63": "Modell", "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.709a2322": "Name",
"i18n:govoplan-organizations.name_is_required.27cd3782": "Name ist erforderlich.", "i18n:govoplan-organizations.name_is_required.27cd3782": "Name ist erforderlich.",
"i18n:govoplan-organizations.network.a24d31c3": "Netzwerk", "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_relation_types_found.6f90bb81": "Keine Beziehungstypen gefunden.",
"i18n:govoplan-organizations.no_relations_found.4e75b11d": "Keine Beziehungen 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_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_unit_types_found.c81fb2a7": "Keine Einheitstypen gefunden.",
"i18n:govoplan-organizations.no_units_found.eea2dd0c": "Keine Einheiten gefunden.", "i18n:govoplan-organizations.no_units_found.eea2dd0c": "Keine Einheiten gefunden.",
"i18n:govoplan-organizations.none.334c4a4c": "Keine", "i18n:govoplan-organizations.none.334c4a4c": "Keine",
"i18n:govoplan-organizations.optional_account_id.5fcf495c": "Optionale Konto-ID", "i18n:govoplan-organizations.optional_account_id.5fcf495c": "Optionale Konto-ID",
"i18n:govoplan-organizations.organization_model.4f924c0e": "Organisationsmodell", "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_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.220edf64": "Organisationen",
"i18n:govoplan-organizations.organizations_intro.4e67c4bb": "Modelliere Organisationstypen, konkrete Einheiten, Strukturen, Funktionen und identitätsbezogene Funktionszuordnungen.", "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.parent_unit.1986c35e": "Übergeordnete Einheit",
"i18n:govoplan-organizations.reactivate.58f2855d": "Reaktivieren", "i18n:govoplan-organizations.reactivate.58f2855d": "Reaktivieren",
"i18n:govoplan-organizations.relation_added.ca90461a": "Beziehung hinzugefügt.", "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.d0aee2e7": "Beziehungstyp",
"i18n:govoplan-organizations.relation_type_added.6f1edff1": "Beziehungstyp hinzugefügt.", "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.relation_types.e5890528": "Beziehungstypen",
"i18n:govoplan-organizations.relations.1c796711": "Beziehungen", "i18n:govoplan-organizations.relations.1c796711": "Beziehungen",
"i18n:govoplan-organizations.reload.cce71553": "Neu laden", "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.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.4895a67d": "Funktion auswählen",
"i18n:govoplan-organizations.select_function_type.2f426c43": "Funktionstyp auswählen", "i18n:govoplan-organizations.select_function_type.2f426c43": "Funktionstyp auswählen",
"i18n:govoplan-organizations.select_relation_type.73f92478": "Beziehungstyp 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_target_unit.8d607541": "Ziel-Einheit auswählen",
"i18n:govoplan-organizations.select_unit.013bf13a": "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.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.slug.094da9b9": "Slug",
"i18n:govoplan-organizations.source.6da13add": "Quelle", "i18n:govoplan-organizations.source.6da13add": "Quelle",
"i18n:govoplan-organizations.source_unit.2fbd8baa": "Quell-Einheit", "i18n:govoplan-organizations.source_unit.2fbd8baa": "Quell-Einheit",
"i18n:govoplan-organizations.status.bae7d5be": "Status", "i18n:govoplan-organizations.status.bae7d5be": "Status",
"i18n:govoplan-organizations.structure.7732fb0b": "Struktur", "i18n:govoplan-organizations.structure.7732fb0b": "Struktur",
"i18n:govoplan-organizations.structure_added.f6cf8e74": "Struktur hinzugefügt.", "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.structures.f9b7f3b4": "Strukturen",
"i18n:govoplan-organizations.target_unit.a1507d86": "Ziel-Einheit", "i18n:govoplan-organizations.target_unit.a1507d86": "Ziel-Einheit",
"i18n:govoplan-organizations.type.599dcce2": "Typ", "i18n:govoplan-organizations.type.599dcce2": "Typ",
"i18n:govoplan-organizations.unit.8fe4d595": "Einheit", "i18n:govoplan-organizations.unit.8fe4d595": "Einheit",
"i18n:govoplan-organizations.unit_added.760a8512": "Einheit hinzugefügt.", "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.9e62810d": "Einheitstyp",
"i18n:govoplan-organizations.unit_type_added.e017dc8c": "Einheitstyp hinzugefügt.", "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.unit_types.c7afc174": "Einheitstypen",
"i18n:govoplan-organizations.units.e14d0d92": "Einheiten", "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." "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 = { const organizationAdminSections: AdminSectionsUiCapability = {
sections: [ sections: [
{ {
id: "tenant-organization-model", id: "tenant-organization-settings",
label: "i18n:govoplan-organizations.organization_model.4f924c0e", label: "i18n:govoplan-organizations.organization_settings.c9ab9829",
group: "TENANT", group: "TENANT",
order: 85, order: 85,
anyOf: ["organizations:model:read", "admin:settings:read"], anyOf: ["organizations:settings:read", "admin:settings:read"],
render: ({ settings, auth }) => createElement(OrganizationsAdminPanel, { settings, auth }) render: ({ settings, auth }) => createElement(OrganizationsAdminPanel, { settings, auth })
} }
] ]

View File

@@ -14,6 +14,11 @@
max-width: 100%; max-width: 100%;
} }
.organizations-settings-grid {
display: grid;
gap: 18px;
}
.organizations-heading { .organizations-heading {
margin-bottom: 4px; margin-bottom: 4px;
} }
@@ -33,6 +38,13 @@
align-items: start; 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-form-panel,
.organizations-table-panel { .organizations-table-panel {
min-width: 0; min-width: 0;
@@ -53,6 +65,18 @@
grid-column: 1 / -1; 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 { .organizations-check-list {
display: grid; display: grid;
gap: 10px; gap: 10px;
@@ -73,6 +97,51 @@
gap: 18px; 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 { .organizations-field-note {
margin: 8px 0 0; margin: 8px 0 0;
color: var(--muted); color: var(--muted);
@@ -90,7 +159,8 @@
} }
@media (max-width: 1100px) { @media (max-width: 1100px) {
.organizations-section-grid { .organizations-section-grid,
.organizations-editor-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
} }