Add organization tenant settings
This commit is contained in:
@@ -15,6 +15,7 @@ from govoplan_organizations.backend.db.models import (
|
||||
OrganizationFunctionType,
|
||||
OrganizationRelation,
|
||||
OrganizationRelationType,
|
||||
OrganizationTenantSettings,
|
||||
OrganizationStructure,
|
||||
OrganizationUnit,
|
||||
OrganizationUnitType,
|
||||
@@ -33,6 +34,8 @@ from .schemas import (
|
||||
OrganizationModelResponse,
|
||||
OrganizationRelationItem,
|
||||
OrganizationRelationTypeItem,
|
||||
OrganizationSettingsItem,
|
||||
OrganizationSettingsUpdateRequest,
|
||||
OrganizationStructureItem,
|
||||
OrganizationUnitItem,
|
||||
OrganizationUnitTypeItem,
|
||||
@@ -61,6 +64,8 @@ ORG_MODEL_WRITE_SCOPES = ("organizations:model:write",)
|
||||
ORG_UNIT_WRITE_SCOPES = ("organizations:unit:write",)
|
||||
ORG_FUNCTION_WRITE_SCOPES = ("organizations:function:write",)
|
||||
ORG_ASSIGN_SCOPES = ("organizations:function:assign",)
|
||||
ORG_SETTINGS_READ_SCOPES = ("organizations:settings:read", "organizations:model:read", "admin:settings:read")
|
||||
ORG_SETTINGS_WRITE_SCOPES = ("organizations:settings:write", "admin:settings:write")
|
||||
SLUG_RE = re.compile(r"[^a-z0-9]+")
|
||||
|
||||
ModelT = TypeVar("ModelT")
|
||||
@@ -129,6 +134,21 @@ def _item_unit_type(item: OrganizationUnitType) -> OrganizationUnitTypeItem:
|
||||
return OrganizationUnitTypeItem(**_row_fields(item))
|
||||
|
||||
|
||||
def _item_settings(item: OrganizationTenantSettings) -> OrganizationSettingsItem:
|
||||
return OrganizationSettingsItem(**_row_fields(item))
|
||||
|
||||
|
||||
def _default_settings(tenant_id: str) -> OrganizationSettingsItem:
|
||||
return OrganizationSettingsItem(
|
||||
tenant_id=tenant_id,
|
||||
allow_tenant_model_customization=True,
|
||||
require_model_change_requests=False,
|
||||
audit_detail_level="standard",
|
||||
change_retention_days=None,
|
||||
settings={},
|
||||
)
|
||||
|
||||
|
||||
def _item_structure(item: OrganizationStructure) -> OrganizationStructureItem:
|
||||
return OrganizationStructureItem(**_row_fields(item))
|
||||
|
||||
@@ -187,6 +207,48 @@ def _apply_slugged_update(session: Session, item: object, payload: object, tenan
|
||||
setattr(item, "settings", value)
|
||||
|
||||
|
||||
@router.get("/settings", response_model=OrganizationSettingsItem)
|
||||
def get_organization_settings(
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(require_any_scope(*ORG_SETTINGS_READ_SCOPES)),
|
||||
) -> OrganizationSettingsItem:
|
||||
tenant_id = _tenant_id(principal)
|
||||
item = session.query(OrganizationTenantSettings).filter(OrganizationTenantSettings.tenant_id == tenant_id).one_or_none()
|
||||
return _item_settings(item) if item is not None else _default_settings(tenant_id)
|
||||
|
||||
|
||||
@router.patch("/settings", response_model=OrganizationSettingsItem)
|
||||
def update_organization_settings(
|
||||
payload: OrganizationSettingsUpdateRequest,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(require_any_scope(*ORG_SETTINGS_WRITE_SCOPES)),
|
||||
) -> OrganizationSettingsItem:
|
||||
tenant_id = _tenant_id(principal)
|
||||
item = session.query(OrganizationTenantSettings).filter(OrganizationTenantSettings.tenant_id == tenant_id).one_or_none()
|
||||
if item is None:
|
||||
item = OrganizationTenantSettings(
|
||||
tenant_id=tenant_id,
|
||||
allow_tenant_model_customization=True,
|
||||
require_model_change_requests=False,
|
||||
audit_detail_level="standard",
|
||||
change_retention_days=None,
|
||||
settings={},
|
||||
)
|
||||
session.add(item)
|
||||
fields = payload.model_fields_set
|
||||
for field in ("allow_tenant_model_customization", "require_model_change_requests", "audit_detail_level", "change_retention_days"):
|
||||
if field in fields:
|
||||
value = getattr(payload, field)
|
||||
if field != "change_retention_days" and value is None:
|
||||
raise _invalid(f"{field} cannot be empty.")
|
||||
setattr(item, field, value)
|
||||
if "settings" in fields:
|
||||
if payload.settings is None:
|
||||
raise _invalid("Settings cannot be empty.")
|
||||
item.settings = payload.settings
|
||||
return _item_settings(_commit(session, item))
|
||||
|
||||
|
||||
@router.get("/model", response_model=OrganizationModelResponse)
|
||||
def get_organization_model(
|
||||
session: Session = Depends(get_session),
|
||||
@@ -356,6 +418,8 @@ def update_unit(
|
||||
raise _invalid("An organization unit cannot be its own parent.")
|
||||
if payload.parent_id is not None:
|
||||
_get_tenant_row(session, OrganizationUnit, payload.parent_id, tenant_id, "Parent organization unit")
|
||||
if _would_create_parent_cycle(session, tenant_id, item.id, payload.parent_id):
|
||||
raise _invalid("This parent assignment would create an organization unit cycle.")
|
||||
item.parent_id = payload.parent_id
|
||||
return _item_unit(_commit(session, item))
|
||||
|
||||
@@ -718,3 +782,17 @@ def _would_create_cycle(
|
||||
query = query.filter(OrganizationRelation.id != exclude_relation_id)
|
||||
pending.extend(row[0] for row in query.all())
|
||||
return False
|
||||
|
||||
|
||||
def _would_create_parent_cycle(session: Session, tenant_id: str, unit_id: str, parent_id: str) -> bool:
|
||||
current: str | None = parent_id
|
||||
seen: set[str] = set()
|
||||
while current is not None:
|
||||
if current == unit_id or current in seen:
|
||||
return True
|
||||
seen.add(current)
|
||||
current = session.query(OrganizationUnit.parent_id).filter(
|
||||
OrganizationUnit.tenant_id == tenant_id,
|
||||
OrganizationUnit.id == current,
|
||||
).scalar()
|
||||
return False
|
||||
|
||||
@@ -7,6 +7,27 @@ from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
StructureKind = Literal["hierarchy", "network", "membership", "classification"]
|
||||
AuditDetailLevel = Literal["summary", "standard", "full"]
|
||||
|
||||
|
||||
class OrganizationSettingsItem(BaseModel):
|
||||
id: str | None = None
|
||||
tenant_id: str
|
||||
allow_tenant_model_customization: bool
|
||||
require_model_change_requests: bool
|
||||
audit_detail_level: AuditDetailLevel
|
||||
change_retention_days: int | None = None
|
||||
settings: dict[str, Any]
|
||||
created_at: datetime | None = None
|
||||
updated_at: datetime | None = None
|
||||
|
||||
|
||||
class OrganizationSettingsUpdateRequest(BaseModel):
|
||||
allow_tenant_model_customization: bool | None = None
|
||||
require_model_change_requests: bool | None = None
|
||||
audit_detail_level: AuditDetailLevel | None = None
|
||||
change_retention_days: int | None = Field(default=None, ge=0)
|
||||
settings: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class OrganizationUnitTypeItem(BaseModel):
|
||||
|
||||
@@ -4,7 +4,7 @@ import uuid
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import Boolean, DateTime, ForeignKey, Index, JSON, String, Text, UniqueConstraint
|
||||
from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Integer, JSON, String, Text, UniqueConstraint
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from govoplan_core.db.base import Base, TimestampMixin
|
||||
@@ -42,6 +42,19 @@ class OrganizationUnitType(Base, TimestampMixin):
|
||||
settings: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
|
||||
|
||||
|
||||
class OrganizationTenantSettings(Base, TimestampMixin):
|
||||
__tablename__ = "organizations_tenant_settings"
|
||||
__table_args__ = (UniqueConstraint("tenant_id", name="uq_organizations_tenant_settings_tenant"),)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
||||
tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
|
||||
allow_tenant_model_customization: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
require_model_change_requests: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||
audit_detail_level: Mapped[str] = mapped_column(String(30), default="standard", nullable=False)
|
||||
change_retention_days: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
settings: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
|
||||
|
||||
|
||||
class OrganizationStructure(Base, TimestampMixin):
|
||||
__tablename__ = "organizations_structures"
|
||||
__table_args__ = (UniqueConstraint("tenant_id", "slug", name="uq_organizations_structures_tenant_slug"),)
|
||||
@@ -172,6 +185,7 @@ __all__ = [
|
||||
"OrganizationRelation",
|
||||
"OrganizationRelationType",
|
||||
"OrganizationStructure",
|
||||
"OrganizationTenantSettings",
|
||||
"OrganizationUnit",
|
||||
"OrganizationUnitType",
|
||||
"new_uuid",
|
||||
|
||||
@@ -46,6 +46,8 @@ def _permission(scope: str, label: str, description: str) -> PermissionDefinitio
|
||||
PERMISSIONS = (
|
||||
_permission("organizations:model:read", "View organization model", "Read organization meta-model definitions such as unit types, structures, and relation types."),
|
||||
_permission("organizations:model:write", "Manage organization model", "Create and edit organization meta-model definitions."),
|
||||
_permission("organizations:settings:read", "View organization settings", "Read organization governance, audit, and retention settings."),
|
||||
_permission("organizations:settings:write", "Manage organization settings", "Edit organization governance, audit, and retention settings."),
|
||||
_permission("organizations:unit:read", "View organization units", "Read concrete organization units and relations."),
|
||||
_permission("organizations:unit:write", "Manage organization units", "Create and edit concrete organization units and relations."),
|
||||
_permission("organizations:function:read", "View organization functions", "Read function definitions and identity-held assignments."),
|
||||
@@ -64,7 +66,7 @@ ROLE_TEMPLATES = (
|
||||
slug="organization_viewer",
|
||||
name="Organization viewer",
|
||||
description="Read organization model, organization units, and function assignments.",
|
||||
permissions=("organizations:model:read", "organizations:unit:read", "organizations:function:read"),
|
||||
permissions=("organizations:model:read", "organizations:settings:read", "organizations:unit:read", "organizations:function:read"),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -112,6 +114,7 @@ manifest = ModuleManifest(
|
||||
uninstall_guard_providers=(
|
||||
persistent_table_uninstall_guard(
|
||||
organization_models.OrganizationUnitType,
|
||||
organization_models.OrganizationTenantSettings,
|
||||
organization_models.OrganizationStructure,
|
||||
organization_models.OrganizationRelationType,
|
||||
organization_models.OrganizationRelation,
|
||||
|
||||
@@ -71,6 +71,24 @@ def _create_unit_types() -> None:
|
||||
_create_index_if_missing(op.f("ix_organizations_unit_types_tenant_id"), "organizations_unit_types", ["tenant_id"])
|
||||
|
||||
|
||||
def _create_tenant_settings() -> None:
|
||||
if "organizations_tenant_settings" not in _tables():
|
||||
op.create_table(
|
||||
"organizations_tenant_settings",
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("tenant_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("allow_tenant_model_customization", sa.Boolean(), nullable=False),
|
||||
sa.Column("require_model_change_requests", sa.Boolean(), nullable=False),
|
||||
sa.Column("audit_detail_level", sa.String(length=30), nullable=False),
|
||||
sa.Column("change_retention_days", sa.Integer(), nullable=True),
|
||||
_json_column("settings"),
|
||||
*_timestamp_columns(),
|
||||
sa.PrimaryKeyConstraint("id", name=op.f("pk_organizations_tenant_settings")),
|
||||
sa.UniqueConstraint("tenant_id", name="uq_organizations_tenant_settings_tenant"),
|
||||
)
|
||||
_create_index_if_missing(op.f("ix_organizations_tenant_settings_tenant_id"), "organizations_tenant_settings", ["tenant_id"])
|
||||
|
||||
|
||||
def _create_structures() -> None:
|
||||
if "organizations_structures" not in _tables():
|
||||
op.create_table(
|
||||
@@ -271,6 +289,7 @@ def _create_function_assignments() -> None:
|
||||
|
||||
def upgrade() -> None:
|
||||
_create_unit_types()
|
||||
_create_tenant_settings()
|
||||
_create_structures()
|
||||
_create_relation_types()
|
||||
_create_units()
|
||||
@@ -293,6 +312,7 @@ def downgrade() -> None:
|
||||
"organizations_relation_types",
|
||||
"organizations_structures",
|
||||
"organizations_function_types",
|
||||
"organizations_tenant_settings",
|
||||
"organizations_unit_types",
|
||||
):
|
||||
if table_name in _tables():
|
||||
|
||||
@@ -13,6 +13,27 @@ export type OrganizationUnitTypeItem = {
|
||||
};
|
||||
|
||||
export type OrganizationStructureKind = "hierarchy" | "network" | "membership" | "classification";
|
||||
export type OrganizationAuditDetailLevel = "summary" | "standard" | "full";
|
||||
|
||||
export type OrganizationSettingsItem = {
|
||||
id?: string | null;
|
||||
tenant_id: string;
|
||||
allow_tenant_model_customization: boolean;
|
||||
require_model_change_requests: boolean;
|
||||
audit_detail_level: OrganizationAuditDetailLevel;
|
||||
change_retention_days?: number | null;
|
||||
settings: Record<string, unknown>;
|
||||
created_at?: string | null;
|
||||
updated_at?: string | null;
|
||||
};
|
||||
|
||||
export type OrganizationSettingsUpdatePayload = {
|
||||
allow_tenant_model_customization?: boolean;
|
||||
require_model_change_requests?: boolean;
|
||||
audit_detail_level?: OrganizationAuditDetailLevel;
|
||||
change_retention_days?: number | null;
|
||||
settings?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type OrganizationStructureItem = {
|
||||
id: string;
|
||||
@@ -205,6 +226,14 @@ export function getOrganizationModel(settings: ApiSettings): Promise<Organizatio
|
||||
return apiFetch<OrganizationModel>(settings, "/api/v1/organizations/model");
|
||||
}
|
||||
|
||||
export function getOrganizationSettings(settings: ApiSettings): Promise<OrganizationSettingsItem> {
|
||||
return apiFetch<OrganizationSettingsItem>(settings, "/api/v1/organizations/settings");
|
||||
}
|
||||
|
||||
export function patchOrganizationSettings(settings: ApiSettings, payload: OrganizationSettingsUpdatePayload): Promise<OrganizationSettingsItem> {
|
||||
return patch(settings, "/api/v1/organizations/settings", payload);
|
||||
}
|
||||
|
||||
export function createUnitType(settings: ApiSettings, payload: SluggedCreatePayload): Promise<OrganizationUnitTypeItem> {
|
||||
return post(settings, "/api/v1/organizations/unit-types", payload);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,156 @@
|
||||
import type { ApiSettings, AuthInfo } from "@govoplan/core-webui";
|
||||
import OrganizationsPage, { type OrganizationSection } from "./OrganizationsPage";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
AdminPageLayout,
|
||||
Button,
|
||||
Card,
|
||||
FormField,
|
||||
ToggleSwitch,
|
||||
adminErrorMessage,
|
||||
hasAnyScope,
|
||||
useUnsavedDraftGuard,
|
||||
type ApiSettings,
|
||||
type AuthInfo
|
||||
} from "@govoplan/core-webui";
|
||||
import {
|
||||
getOrganizationSettings,
|
||||
patchOrganizationSettings,
|
||||
type OrganizationAuditDetailLevel,
|
||||
type OrganizationSettingsItem
|
||||
} from "../../api/organizations";
|
||||
|
||||
const MODEL_SECTIONS: OrganizationSection[] = ["model"];
|
||||
const FALLBACK_SETTINGS: OrganizationSettingsItem = {
|
||||
tenant_id: "",
|
||||
allow_tenant_model_customization: true,
|
||||
require_model_change_requests: false,
|
||||
audit_detail_level: "standard",
|
||||
change_retention_days: null,
|
||||
settings: {}
|
||||
};
|
||||
|
||||
const AUDIT_DETAIL_LEVELS: Array<{ value: OrganizationAuditDetailLevel; label: string }> = [
|
||||
{ value: "summary", label: "i18n:govoplan-organizations.audit_summary.6a7d68a5" },
|
||||
{ value: "standard", label: "i18n:govoplan-organizations.audit_standard.785d981f" },
|
||||
{ value: "full", label: "i18n:govoplan-organizations.audit_full.0c83669c" }
|
||||
];
|
||||
|
||||
export default function OrganizationsAdminPanel({ settings, auth }: { settings: ApiSettings; auth: AuthInfo }) {
|
||||
const [draft, setDraft] = useState<OrganizationSettingsItem>(FALLBACK_SETTINGS);
|
||||
const [savedDraft, setSavedDraft] = useState<OrganizationSettingsItem>(FALLBACK_SETTINGS);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [success, setSuccess] = useState("");
|
||||
const canWrite = hasAnyScope(auth, ["organizations:settings:write", "admin:settings:write"]);
|
||||
const dirty = settingsKey(draft) !== settingsKey(savedDraft);
|
||||
|
||||
useUnsavedDraftGuard({
|
||||
dirty,
|
||||
onSave: save,
|
||||
onDiscard: () => setDraft(savedDraft)
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
}, [settings.accessToken, settings.apiBaseUrl]);
|
||||
|
||||
async function load() {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
try {
|
||||
const next = await getOrganizationSettings(settings);
|
||||
setDraft(next);
|
||||
setSavedDraft(next);
|
||||
} catch (caught) {
|
||||
setError(adminErrorMessage(caught));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function save(): Promise<boolean> {
|
||||
setBusy(true);
|
||||
setError("");
|
||||
setSuccess("");
|
||||
try {
|
||||
const saved = await patchOrganizationSettings(settings, {
|
||||
allow_tenant_model_customization: draft.allow_tenant_model_customization,
|
||||
require_model_change_requests: draft.require_model_change_requests,
|
||||
audit_detail_level: draft.audit_detail_level,
|
||||
change_retention_days: draft.change_retention_days ?? null,
|
||||
settings: draft.settings
|
||||
});
|
||||
setDraft(saved);
|
||||
setSavedDraft(saved);
|
||||
setSuccess("i18n:govoplan-organizations.organization_settings_saved.bfbcfdfa");
|
||||
return true;
|
||||
} catch (caught) {
|
||||
setError(adminErrorMessage(caught));
|
||||
return false;
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<OrganizationsPage
|
||||
settings={settings}
|
||||
auth={auth}
|
||||
mode="admin"
|
||||
initialSection="model"
|
||||
availableSections={MODEL_SECTIONS}
|
||||
title="i18n:govoplan-organizations.organization_model.4f924c0e"
|
||||
description="i18n:govoplan-organizations.organization_model_admin_description.35dc9f10"
|
||||
<AdminPageLayout
|
||||
title="i18n:govoplan-organizations.organization_settings.c9ab9829"
|
||||
description="i18n:govoplan-organizations.organization_settings_description.35dc9f10"
|
||||
loading={loading}
|
||||
loadingLabel="i18n:govoplan-organizations.loading_organization_settings.c6008db8"
|
||||
error={error}
|
||||
success={success}
|
||||
actions={<><Button onClick={() => void load()} disabled={loading || busy}>i18n:govoplan-organizations.reload.cce71553</Button><Button variant="primary" onClick={() => void save()} disabled={!canWrite || busy || !dirty}>{busy ? "i18n:govoplan-organizations.saving.56a2285c" : "i18n:govoplan-organizations.save_settings.913aba9f"}</Button></>}
|
||||
>
|
||||
<div className="organizations-settings-grid">
|
||||
<Card title="i18n:govoplan-organizations.model_governance.6aa18fd0">
|
||||
<div className="settings-list">
|
||||
<ToggleSwitch
|
||||
checked={draft.allow_tenant_model_customization}
|
||||
disabled={!canWrite || busy}
|
||||
onChange={(checked) => setDraft({ ...draft, allow_tenant_model_customization: checked })}
|
||||
label="i18n:govoplan-organizations.allow_tenant_model_customization.2425d751"
|
||||
/>
|
||||
<ToggleSwitch
|
||||
checked={draft.require_model_change_requests}
|
||||
disabled={!canWrite || busy}
|
||||
onChange={(checked) => setDraft({ ...draft, require_model_change_requests: checked })}
|
||||
label="i18n:govoplan-organizations.require_model_change_requests.83454cad"
|
||||
/>
|
||||
</div>
|
||||
<p className="muted small-note">i18n:govoplan-organizations.model_governance_help.0fa69021</p>
|
||||
</Card>
|
||||
|
||||
<Card title="i18n:govoplan-organizations.audit_and_retention.3ba1d2fc">
|
||||
<div className="organizations-form-grid">
|
||||
<FormField label="i18n:govoplan-organizations.audit_detail_level.7397355d">
|
||||
<select value={draft.audit_detail_level} disabled={!canWrite || busy} onChange={(event) => setDraft({ ...draft, audit_detail_level: event.target.value as OrganizationAuditDetailLevel })}>
|
||||
{AUDIT_DETAIL_LEVELS.map((item) => <option key={item.value} value={item.value}>{item.label}</option>)}
|
||||
</select>
|
||||
</FormField>
|
||||
<FormField label="i18n:govoplan-organizations.change_retention_days.71bbd140">
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
value={draft.change_retention_days ?? ""}
|
||||
disabled={!canWrite || busy}
|
||||
placeholder="i18n:govoplan-organizations.unlimited.35569464"
|
||||
onChange={(event) => setDraft({ ...draft, change_retention_days: event.target.value === "" ? null : Math.max(0, Number(event.target.value)) })}
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
<p className="muted small-note">i18n:govoplan-organizations.audit_retention_help.42dec57d</p>
|
||||
</Card>
|
||||
</div>
|
||||
</AdminPageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
function settingsKey(settings: OrganizationSettingsItem): string {
|
||||
return JSON.stringify({
|
||||
allow_tenant_model_customization: settings.allow_tenant_model_customization,
|
||||
require_model_change_requests: settings.require_model_change_requests,
|
||||
audit_detail_level: settings.audit_detail_level,
|
||||
change_retention_days: settings.change_retention_days ?? null,
|
||||
settings: settings.settings
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect, useMemo, useState, type FormEvent } from "react";
|
||||
import { RefreshCw, Save, XCircle } from "lucide-react";
|
||||
import { Edit3, Plus, RefreshCw, Save, XCircle } from "lucide-react";
|
||||
import {
|
||||
ApiError,
|
||||
Button,
|
||||
@@ -226,6 +226,24 @@ function sluggedPayload(draft: SluggedDraft): SluggedCreatePayload {
|
||||
};
|
||||
}
|
||||
|
||||
function sluggedPatchPayload(draft: SluggedDraft): Partial<SluggedCreatePayload> {
|
||||
return {
|
||||
name: draft.name.trim(),
|
||||
slug: textOrNull(draft.slug),
|
||||
description: textOrNull(draft.description),
|
||||
is_active: draft.is_active
|
||||
};
|
||||
}
|
||||
|
||||
function sluggedDraftFrom(item: { name: string; slug: string; description?: string | null; is_active: boolean }): SluggedDraft {
|
||||
return {
|
||||
name: item.name,
|
||||
slug: item.slug,
|
||||
description: item.description ?? "",
|
||||
is_active: item.is_active
|
||||
};
|
||||
}
|
||||
|
||||
function isSluggedDirty(draft: SluggedDraft): boolean {
|
||||
return draft.name.trim() !== "" || draft.slug.trim() !== "" || draft.description.trim() !== "" || !draft.is_active;
|
||||
}
|
||||
@@ -297,6 +315,17 @@ function ActiveToggleButton({ item, disabled, onToggle }: { item: ActiveItem; di
|
||||
);
|
||||
}
|
||||
|
||||
function RowActions({ item, disabled, onEdit, onToggle }: { item: ActiveItem; disabled: boolean; onEdit: () => void; onToggle: () => void }) {
|
||||
return (
|
||||
<div className="organizations-row-actions">
|
||||
<Button type="button" variant="ghost" disabled={disabled} onClick={onEdit} title="i18n:govoplan-organizations.edit.7dce1220">
|
||||
<Edit3 size={16} aria-hidden="true" /> i18n:govoplan-organizations.edit.7dce1220
|
||||
</Button>
|
||||
<ActiveToggleButton item={item} disabled={disabled} onToggle={onToggle} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SluggedFields({
|
||||
draft,
|
||||
onChange,
|
||||
@@ -354,6 +383,15 @@ export default function OrganizationsPage({
|
||||
const [functionTypeDraft, setFunctionTypeDraft] = useState<FunctionTypeDraft>(() => emptyFunctionTypeDraft());
|
||||
const [functionDraft, setFunctionDraft] = useState<FunctionDraft>(() => emptyFunctionDraft());
|
||||
const [assignmentDraft, setAssignmentDraft] = useState<AssignmentDraft>(() => emptyAssignmentDraft());
|
||||
const [editingUnitTypeId, setEditingUnitTypeId] = useState<string | null>(null);
|
||||
const [editingStructureId, setEditingStructureId] = useState<string | null>(null);
|
||||
const [editingRelationTypeId, setEditingRelationTypeId] = useState<string | null>(null);
|
||||
const [editingUnitId, setEditingUnitId] = useState<string | null>(null);
|
||||
const [editingRelationId, setEditingRelationId] = useState<string | null>(null);
|
||||
const [editingFunctionTypeId, setEditingFunctionTypeId] = useState<string | null>(null);
|
||||
const [editingFunctionId, setEditingFunctionId] = useState<string | null>(null);
|
||||
const [editingAssignmentId, setEditingAssignmentId] = useState<string | null>(null);
|
||||
const [selectedUnitId, setSelectedUnitId] = useState("");
|
||||
|
||||
const canWriteModel = hasScope(auth, "organizations:model:write");
|
||||
const canWriteUnits = hasScope(auth, "organizations:unit:write");
|
||||
@@ -366,6 +404,15 @@ export default function OrganizationsPage({
|
||||
const unitById = useMemo(() => mapById(model.units), [model.units]);
|
||||
const functionTypeById = useMemo(() => mapById(model.function_types), [model.function_types]);
|
||||
const functionById = useMemo(() => mapById(model.functions), [model.functions]);
|
||||
const unitsByParentId = useMemo(() => {
|
||||
const mapped = new Map<string, OrganizationUnitItem[]>();
|
||||
for (const unit of model.units) {
|
||||
const key = unit.parent_id || "";
|
||||
mapped.set(key, [...(mapped.get(key) ?? []), unit]);
|
||||
}
|
||||
for (const units of mapped.values()) units.sort((left, right) => left.name.localeCompare(right.name));
|
||||
return mapped;
|
||||
}, [model.units]);
|
||||
|
||||
const hasDirtyDraft = isSluggedDirty(unitTypeDraft) ||
|
||||
isSluggedDirty(structureDraft) ||
|
||||
@@ -396,6 +443,10 @@ export default function OrganizationsPage({
|
||||
if (!sectionSet.has(active)) setActive(firstAvailableSection);
|
||||
}, [active, firstAvailableSection, sectionSet]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedUnitId && !unitById.has(selectedUnitId)) setSelectedUnitId("");
|
||||
}, [selectedUnitId, unitById]);
|
||||
|
||||
const runAction = useCallback(async (action: () => Promise<unknown>, successMessage = ""): Promise<boolean> => {
|
||||
setBusy(true);
|
||||
setError("");
|
||||
@@ -422,6 +473,14 @@ export default function OrganizationsPage({
|
||||
setFunctionTypeDraft(emptyFunctionTypeDraft());
|
||||
setFunctionDraft(emptyFunctionDraft());
|
||||
setAssignmentDraft(emptyAssignmentDraft());
|
||||
setEditingUnitTypeId(null);
|
||||
setEditingStructureId(null);
|
||||
setEditingRelationTypeId(null);
|
||||
setEditingUnitId(null);
|
||||
setEditingRelationId(null);
|
||||
setEditingFunctionTypeId(null);
|
||||
setEditingFunctionId(null);
|
||||
setEditingAssignmentId(null);
|
||||
}, []);
|
||||
|
||||
function rejectMissingName(): Promise<boolean> {
|
||||
@@ -438,20 +497,33 @@ export default function OrganizationsPage({
|
||||
event?.preventDefault();
|
||||
if (!canWriteModel) return rejectMissingWritePermission();
|
||||
if (!unitTypeDraft.name.trim()) return rejectMissingName();
|
||||
const ok = await runAction(() => createUnitType(settings, sluggedPayload(unitTypeDraft)), "i18n:govoplan-organizations.unit_type_added.e017dc8c");
|
||||
if (ok) setUnitTypeDraft(emptySluggedDraft());
|
||||
const ok = await runAction(
|
||||
() => editingUnitTypeId ? patchUnitType(settings, editingUnitTypeId, sluggedPatchPayload(unitTypeDraft)) : createUnitType(settings, sluggedPayload(unitTypeDraft)),
|
||||
editingUnitTypeId ? "i18n:govoplan-organizations.unit_type_updated.6d8f2a16" : "i18n:govoplan-organizations.unit_type_added.e017dc8c"
|
||||
);
|
||||
if (ok) {
|
||||
setUnitTypeDraft(emptySluggedDraft());
|
||||
setEditingUnitTypeId(null);
|
||||
}
|
||||
return ok;
|
||||
}, [canWriteModel, runAction, settings, unitTypeDraft]);
|
||||
}, [canWriteModel, editingUnitTypeId, runAction, settings, unitTypeDraft]);
|
||||
|
||||
const submitStructure = useCallback(async (event?: FormEvent): Promise<boolean> => {
|
||||
event?.preventDefault();
|
||||
if (!canWriteModel) return rejectMissingWritePermission();
|
||||
if (!structureDraft.name.trim()) return rejectMissingName();
|
||||
const payload: StructureCreatePayload = { ...sluggedPayload(structureDraft), structure_kind: structureDraft.structure_kind };
|
||||
const ok = await runAction(() => createStructure(settings, payload), "i18n:govoplan-organizations.structure_added.f6cf8e74");
|
||||
if (ok) setStructureDraft(emptyStructureDraft());
|
||||
const patchPayload: Partial<StructureCreatePayload> = { ...sluggedPatchPayload(structureDraft), structure_kind: structureDraft.structure_kind };
|
||||
const ok = await runAction(
|
||||
() => editingStructureId ? patchStructure(settings, editingStructureId, patchPayload) : createStructure(settings, payload),
|
||||
editingStructureId ? "i18n:govoplan-organizations.structure_updated.2591e75f" : "i18n:govoplan-organizations.structure_added.f6cf8e74"
|
||||
);
|
||||
if (ok) {
|
||||
setStructureDraft(emptyStructureDraft());
|
||||
setEditingStructureId(null);
|
||||
}
|
||||
return ok;
|
||||
}, [canWriteModel, runAction, settings, structureDraft]);
|
||||
}, [canWriteModel, editingStructureId, runAction, settings, structureDraft]);
|
||||
|
||||
const submitRelationType = useCallback(async (event?: FormEvent): Promise<boolean> => {
|
||||
event?.preventDefault();
|
||||
@@ -465,10 +537,24 @@ export default function OrganizationsPage({
|
||||
is_hierarchical: relationTypeDraft.is_hierarchical,
|
||||
allow_cycles: relationTypeDraft.allow_cycles
|
||||
};
|
||||
const ok = await runAction(() => createRelationType(settings, payload), "i18n:govoplan-organizations.relation_type_added.6f1edff1");
|
||||
if (ok) setRelationTypeDraft(emptyRelationTypeDraft());
|
||||
const patchPayload: Partial<RelationTypeCreatePayload> = {
|
||||
...sluggedPatchPayload(relationTypeDraft),
|
||||
structure_id: textOrNull(relationTypeDraft.structure_id),
|
||||
source_unit_type_id: textOrNull(relationTypeDraft.source_unit_type_id),
|
||||
target_unit_type_id: textOrNull(relationTypeDraft.target_unit_type_id),
|
||||
is_hierarchical: relationTypeDraft.is_hierarchical,
|
||||
allow_cycles: relationTypeDraft.allow_cycles
|
||||
};
|
||||
const ok = await runAction(
|
||||
() => editingRelationTypeId ? patchRelationType(settings, editingRelationTypeId, patchPayload) : createRelationType(settings, payload),
|
||||
editingRelationTypeId ? "i18n:govoplan-organizations.relation_type_updated.5ac57ec7" : "i18n:govoplan-organizations.relation_type_added.6f1edff1"
|
||||
);
|
||||
if (ok) {
|
||||
setRelationTypeDraft(emptyRelationTypeDraft());
|
||||
setEditingRelationTypeId(null);
|
||||
}
|
||||
return ok;
|
||||
}, [canWriteModel, relationTypeDraft, runAction, settings]);
|
||||
}, [canWriteModel, editingRelationTypeId, relationTypeDraft, runAction, settings]);
|
||||
|
||||
const submitUnit = useCallback(async (event?: FormEvent): Promise<boolean> => {
|
||||
event?.preventDefault();
|
||||
@@ -479,10 +565,21 @@ export default function OrganizationsPage({
|
||||
unit_type_id: textOrNull(unitDraft.unit_type_id),
|
||||
parent_id: textOrNull(unitDraft.parent_id)
|
||||
};
|
||||
const ok = await runAction(() => createUnit(settings, payload), "i18n:govoplan-organizations.unit_added.760a8512");
|
||||
if (ok) setUnitDraft(emptyUnitDraft());
|
||||
const patchPayload: Partial<UnitCreatePayload> = {
|
||||
...sluggedPatchPayload(unitDraft),
|
||||
unit_type_id: textOrNull(unitDraft.unit_type_id),
|
||||
parent_id: textOrNull(unitDraft.parent_id)
|
||||
};
|
||||
const ok = await runAction(
|
||||
() => editingUnitId ? patchUnit(settings, editingUnitId, patchPayload) : createUnit(settings, payload),
|
||||
editingUnitId ? "i18n:govoplan-organizations.unit_updated.15f02216" : "i18n:govoplan-organizations.unit_added.760a8512"
|
||||
);
|
||||
if (ok) {
|
||||
setUnitDraft(emptyUnitDraft());
|
||||
setEditingUnitId(null);
|
||||
}
|
||||
return ok;
|
||||
}, [canWriteUnits, runAction, settings, unitDraft]);
|
||||
}, [canWriteUnits, editingUnitId, runAction, settings, unitDraft]);
|
||||
|
||||
const submitRelation = useCallback(async (event?: FormEvent): Promise<boolean> => {
|
||||
event?.preventDefault();
|
||||
@@ -492,10 +589,17 @@ export default function OrganizationsPage({
|
||||
return false;
|
||||
}
|
||||
const payload: RelationCreatePayload = { ...relationDraft, settings: {} };
|
||||
const ok = await runAction(() => createRelation(settings, payload), "i18n:govoplan-organizations.relation_added.ca90461a");
|
||||
if (ok) setRelationDraft(emptyRelationDraft());
|
||||
const patchPayload: Partial<RelationCreatePayload> = { ...relationDraft };
|
||||
const ok = await runAction(
|
||||
() => editingRelationId ? patchRelation(settings, editingRelationId, patchPayload) : createRelation(settings, payload),
|
||||
editingRelationId ? "i18n:govoplan-organizations.relation_updated.c6212776" : "i18n:govoplan-organizations.relation_added.ca90461a"
|
||||
);
|
||||
if (ok) {
|
||||
setRelationDraft(emptyRelationDraft());
|
||||
setEditingRelationId(null);
|
||||
}
|
||||
return ok;
|
||||
}, [canWriteUnits, relationDraft, runAction, settings]);
|
||||
}, [canWriteUnits, editingRelationId, relationDraft, runAction, settings]);
|
||||
|
||||
const submitFunctionType = useCallback(async (event?: FormEvent): Promise<boolean> => {
|
||||
event?.preventDefault();
|
||||
@@ -507,10 +611,22 @@ export default function OrganizationsPage({
|
||||
delegable: functionTypeDraft.delegable,
|
||||
act_in_place_allowed: functionTypeDraft.act_in_place_allowed
|
||||
};
|
||||
const ok = await runAction(() => createFunctionType(settings, payload), "i18n:govoplan-organizations.function_type_added.2cd9e899");
|
||||
if (ok) setFunctionTypeDraft(emptyFunctionTypeDraft());
|
||||
const patchPayload: Partial<FunctionTypeCreatePayload> = {
|
||||
...sluggedPatchPayload(functionTypeDraft),
|
||||
organization_unit_type_id: textOrNull(functionTypeDraft.organization_unit_type_id),
|
||||
delegable: functionTypeDraft.delegable,
|
||||
act_in_place_allowed: functionTypeDraft.act_in_place_allowed
|
||||
};
|
||||
const ok = await runAction(
|
||||
() => editingFunctionTypeId ? patchFunctionType(settings, editingFunctionTypeId, patchPayload) : createFunctionType(settings, payload),
|
||||
editingFunctionTypeId ? "i18n:govoplan-organizations.function_type_updated.1a4f29f6" : "i18n:govoplan-organizations.function_type_added.2cd9e899"
|
||||
);
|
||||
if (ok) {
|
||||
setFunctionTypeDraft(emptyFunctionTypeDraft());
|
||||
setEditingFunctionTypeId(null);
|
||||
}
|
||||
return ok;
|
||||
}, [canWriteModel, functionTypeDraft, runAction, settings]);
|
||||
}, [canWriteModel, editingFunctionTypeId, functionTypeDraft, runAction, settings]);
|
||||
|
||||
const submitFunction = useCallback(async (event?: FormEvent): Promise<boolean> => {
|
||||
event?.preventDefault();
|
||||
@@ -527,10 +643,23 @@ export default function OrganizationsPage({
|
||||
delegable: functionDraft.delegable,
|
||||
act_in_place_allowed: functionDraft.act_in_place_allowed
|
||||
};
|
||||
const ok = await runAction(() => createFunction(settings, payload), "i18n:govoplan-organizations.function_added.e2266702");
|
||||
if (ok) setFunctionDraft(emptyFunctionDraft());
|
||||
const patchPayload: Partial<FunctionCreatePayload> = {
|
||||
...sluggedPatchPayload(functionDraft),
|
||||
organization_unit_id: functionDraft.organization_unit_id,
|
||||
function_type_id: textOrNull(functionDraft.function_type_id),
|
||||
delegable: functionDraft.delegable,
|
||||
act_in_place_allowed: functionDraft.act_in_place_allowed
|
||||
};
|
||||
const ok = await runAction(
|
||||
() => editingFunctionId ? patchFunction(settings, editingFunctionId, patchPayload) : createFunction(settings, payload),
|
||||
editingFunctionId ? "i18n:govoplan-organizations.function_updated.65016009" : "i18n:govoplan-organizations.function_added.e2266702"
|
||||
);
|
||||
if (ok) {
|
||||
setFunctionDraft(emptyFunctionDraft());
|
||||
setEditingFunctionId(null);
|
||||
}
|
||||
return ok;
|
||||
}, [canWriteFunctions, functionDraft, runAction, settings]);
|
||||
}, [canWriteFunctions, editingFunctionId, functionDraft, runAction, settings]);
|
||||
|
||||
const submitAssignment = useCallback(async (event?: FormEvent): Promise<boolean> => {
|
||||
event?.preventDefault();
|
||||
@@ -550,10 +679,16 @@ export default function OrganizationsPage({
|
||||
is_active: assignmentDraft.is_active,
|
||||
settings: {}
|
||||
};
|
||||
const ok = await runAction(() => createFunctionAssignment(settings, payload), "i18n:govoplan-organizations.assignment_added.94263d1b");
|
||||
if (ok) setAssignmentDraft(emptyAssignmentDraft());
|
||||
const ok = await runAction(
|
||||
() => editingAssignmentId ? patchFunctionAssignment(settings, editingAssignmentId, payload) : createFunctionAssignment(settings, payload),
|
||||
editingAssignmentId ? "i18n:govoplan-organizations.assignment_updated.680b6e33" : "i18n:govoplan-organizations.assignment_added.94263d1b"
|
||||
);
|
||||
if (ok) {
|
||||
setAssignmentDraft(emptyAssignmentDraft());
|
||||
setEditingAssignmentId(null);
|
||||
}
|
||||
return ok;
|
||||
}, [assignmentDraft, canAssignFunctions, runAction, settings]);
|
||||
}, [assignmentDraft, canAssignFunctions, editingAssignmentId, runAction, settings]);
|
||||
|
||||
const saveDrafts = useCallback(async (): Promise<boolean> => {
|
||||
if (isSluggedDirty(unitTypeDraft) && !(await submitUnitType())) return false;
|
||||
@@ -624,12 +759,103 @@ export default function OrganizationsPage({
|
||||
void runAction(() => patchFunctionAssignment(settings, item.id, { is_active: !item.is_active }));
|
||||
}, [runAction, settings]);
|
||||
|
||||
const editUnitType = useCallback((item: OrganizationUnitTypeItem) => {
|
||||
setEditingUnitTypeId(item.id);
|
||||
setUnitTypeDraft(sluggedDraftFrom(item));
|
||||
setActive("model");
|
||||
}, []);
|
||||
|
||||
const editStructure = useCallback((item: OrganizationStructureItem) => {
|
||||
setEditingStructureId(item.id);
|
||||
setStructureDraft({ ...sluggedDraftFrom(item), structure_kind: item.structure_kind as OrganizationStructureKind });
|
||||
setActive("model");
|
||||
}, []);
|
||||
|
||||
const editRelationType = useCallback((item: OrganizationRelationTypeItem) => {
|
||||
setEditingRelationTypeId(item.id);
|
||||
setRelationTypeDraft({
|
||||
...sluggedDraftFrom(item),
|
||||
structure_id: item.structure_id ?? "",
|
||||
source_unit_type_id: item.source_unit_type_id ?? "",
|
||||
target_unit_type_id: item.target_unit_type_id ?? "",
|
||||
is_hierarchical: item.is_hierarchical,
|
||||
allow_cycles: item.allow_cycles
|
||||
});
|
||||
setActive("model");
|
||||
}, []);
|
||||
|
||||
const editUnit = useCallback((item: OrganizationUnitItem) => {
|
||||
setEditingUnitId(item.id);
|
||||
setSelectedUnitId(item.id);
|
||||
setUnitDraft({ ...sluggedDraftFrom(item), unit_type_id: item.unit_type_id ?? "", parent_id: item.parent_id ?? "" });
|
||||
setActive("units");
|
||||
}, []);
|
||||
|
||||
const addSubunit = useCallback((item: OrganizationUnitItem) => {
|
||||
setSelectedUnitId(item.id);
|
||||
setEditingUnitId(null);
|
||||
setUnitDraft({ ...emptyUnitDraft(), parent_id: item.id });
|
||||
setActive("units");
|
||||
}, []);
|
||||
|
||||
const editRelation = useCallback((item: OrganizationRelationItem) => {
|
||||
setEditingRelationId(item.id);
|
||||
setRelationDraft({
|
||||
structure_id: item.structure_id,
|
||||
relation_type_id: item.relation_type_id,
|
||||
source_unit_id: item.source_unit_id,
|
||||
target_unit_id: item.target_unit_id,
|
||||
is_active: item.is_active
|
||||
});
|
||||
setActive("relations");
|
||||
}, []);
|
||||
|
||||
const editFunctionType = useCallback((item: OrganizationFunctionTypeItem) => {
|
||||
setEditingFunctionTypeId(item.id);
|
||||
setFunctionTypeDraft({
|
||||
...sluggedDraftFrom(item),
|
||||
organization_unit_type_id: item.organization_unit_type_id ?? "",
|
||||
delegable: item.delegable,
|
||||
act_in_place_allowed: item.act_in_place_allowed
|
||||
});
|
||||
setActive("model");
|
||||
}, []);
|
||||
|
||||
const editFunction = useCallback((item: OrganizationFunctionItem) => {
|
||||
setEditingFunctionId(item.id);
|
||||
setSelectedUnitId(item.organization_unit_id);
|
||||
setFunctionDraft({
|
||||
...sluggedDraftFrom(item),
|
||||
organization_unit_id: item.organization_unit_id,
|
||||
function_type_id: item.function_type_id ?? "",
|
||||
delegable: item.delegable,
|
||||
act_in_place_allowed: item.act_in_place_allowed
|
||||
});
|
||||
setActive("functions");
|
||||
}, []);
|
||||
|
||||
const editAssignment = useCallback((item: OrganizationFunctionAssignmentItem) => {
|
||||
setEditingAssignmentId(item.id);
|
||||
setSelectedUnitId(item.organization_unit_id);
|
||||
setAssignmentDraft({
|
||||
identity_id: item.identity_id,
|
||||
account_id: item.account_id ?? "",
|
||||
function_id: item.function_id,
|
||||
applies_to_subunits: item.applies_to_subunits,
|
||||
source: item.source,
|
||||
delegated_from_assignment_id: item.delegated_from_assignment_id ?? "",
|
||||
acting_for_account_id: item.acting_for_account_id ?? "",
|
||||
is_active: item.is_active
|
||||
});
|
||||
setActive("assignments");
|
||||
}, []);
|
||||
|
||||
const unitTypeColumns: DataGridColumn<OrganizationUnitTypeItem>[] = [
|
||||
{ id: "name", header: "i18n:govoplan-organizations.name.709a2322", minWidth: 180, sortable: true, filterable: true, value: (row) => row.name },
|
||||
{ id: "slug", header: "i18n:govoplan-organizations.slug.094da9b9", width: 160, sortable: true, filterable: true, value: (row) => row.slug },
|
||||
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
|
||||
{ id: "description", header: "i18n:govoplan-organizations.description.55f8ebc8", minWidth: 220, value: (row) => row.description ?? "" },
|
||||
{ id: "actions", header: "", width: 130, sticky: "end", render: (row) => <ActiveToggleButton item={row} disabled={!canWriteModel || busy} onToggle={() => toggleUnitType(row)} /> }
|
||||
{ id: "actions", header: "", width: 220, sticky: "end", render: (row) => <RowActions item={row} disabled={!canWriteModel || busy} onEdit={() => editUnitType(row)} onToggle={() => toggleUnitType(row)} /> }
|
||||
];
|
||||
|
||||
const structureColumns: DataGridColumn<OrganizationStructureItem>[] = [
|
||||
@@ -637,7 +863,7 @@ export default function OrganizationsPage({
|
||||
{ id: "kind", header: "i18n:govoplan-organizations.kind.794c9d9c", width: 140, sortable: true, filterable: true, value: (row) => row.structure_kind },
|
||||
{ id: "slug", header: "i18n:govoplan-organizations.slug.094da9b9", width: 160, value: (row) => row.slug },
|
||||
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
|
||||
{ id: "actions", header: "", width: 130, sticky: "end", render: (row) => <ActiveToggleButton item={row} disabled={!canWriteModel || busy} onToggle={() => toggleStructure(row)} /> }
|
||||
{ id: "actions", header: "", width: 220, sticky: "end", render: (row) => <RowActions item={row} disabled={!canWriteModel || busy} onEdit={() => editStructure(row)} onToggle={() => toggleStructure(row)} /> }
|
||||
];
|
||||
|
||||
const relationTypeColumns: DataGridColumn<OrganizationRelationTypeItem>[] = [
|
||||
@@ -647,7 +873,7 @@ export default function OrganizationsPage({
|
||||
{ id: "target", header: "i18n:govoplan-organizations.target_unit.a1507d86", minWidth: 170, render: (row) => optionalLabel(unitTypeById.get(row.target_unit_type_id || "")?.name) },
|
||||
{ id: "hierarchical", header: "i18n:govoplan-organizations.hierarchical.8964f313", width: 130, render: (row) => activeStatus(row.is_hierarchical) },
|
||||
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
|
||||
{ id: "actions", header: "", width: 130, sticky: "end", render: (row) => <ActiveToggleButton item={row} disabled={!canWriteModel || busy} onToggle={() => toggleRelationType(row)} /> }
|
||||
{ id: "actions", header: "", width: 220, sticky: "end", render: (row) => <RowActions item={row} disabled={!canWriteModel || busy} onEdit={() => editRelationType(row)} onToggle={() => toggleRelationType(row)} /> }
|
||||
];
|
||||
|
||||
const unitColumns: DataGridColumn<OrganizationUnitItem>[] = [
|
||||
@@ -656,7 +882,7 @@ export default function OrganizationsPage({
|
||||
{ id: "parent", header: "i18n:govoplan-organizations.parent_unit.1986c35e", minWidth: 180, render: (row) => optionalLabel(unitById.get(row.parent_id || "")?.name) },
|
||||
{ id: "slug", header: "i18n:govoplan-organizations.slug.094da9b9", width: 160, value: (row) => row.slug },
|
||||
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
|
||||
{ id: "actions", header: "", width: 130, sticky: "end", render: (row) => <ActiveToggleButton item={row} disabled={!canWriteUnits || busy} onToggle={() => toggleUnit(row)} /> }
|
||||
{ id: "actions", header: "", width: 220, sticky: "end", render: (row) => <RowActions item={row} disabled={!canWriteUnits || busy} onEdit={() => editUnit(row)} onToggle={() => toggleUnit(row)} /> }
|
||||
];
|
||||
|
||||
const relationColumns: DataGridColumn<OrganizationRelationItem>[] = [
|
||||
@@ -665,7 +891,7 @@ export default function OrganizationsPage({
|
||||
{ id: "source", header: "i18n:govoplan-organizations.source_unit.2fbd8baa", minWidth: 190, render: (row) => optionalLabel(unitById.get(row.source_unit_id)?.name) },
|
||||
{ id: "target", header: "i18n:govoplan-organizations.target_unit.a1507d86", minWidth: 190, render: (row) => optionalLabel(unitById.get(row.target_unit_id)?.name) },
|
||||
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
|
||||
{ id: "actions", header: "", width: 130, sticky: "end", render: (row) => <ActiveToggleButton item={row} disabled={!canWriteUnits || busy} onToggle={() => toggleRelation(row)} /> }
|
||||
{ id: "actions", header: "", width: 220, sticky: "end", render: (row) => <RowActions item={row} disabled={!canWriteUnits || busy} onEdit={() => editRelation(row)} onToggle={() => toggleRelation(row)} /> }
|
||||
];
|
||||
|
||||
const functionTypeColumns: DataGridColumn<OrganizationFunctionTypeItem>[] = [
|
||||
@@ -674,7 +900,7 @@ export default function OrganizationsPage({
|
||||
{ id: "delegable", header: "i18n:govoplan-organizations.delegable.b4f0137d", width: 120, render: (row) => activeStatus(row.delegable) },
|
||||
{ id: "actInPlace", header: "i18n:govoplan-organizations.act_in_place.49b942bd", width: 140, render: (row) => activeStatus(row.act_in_place_allowed) },
|
||||
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
|
||||
{ id: "actions", header: "", width: 130, sticky: "end", render: (row) => <ActiveToggleButton item={row} disabled={!canWriteModel || busy} onToggle={() => toggleFunctionType(row)} /> }
|
||||
{ id: "actions", header: "", width: 220, sticky: "end", render: (row) => <RowActions item={row} disabled={!canWriteModel || busy} onEdit={() => editFunctionType(row)} onToggle={() => toggleFunctionType(row)} /> }
|
||||
];
|
||||
|
||||
const functionColumns: DataGridColumn<OrganizationFunctionItem>[] = [
|
||||
@@ -683,7 +909,7 @@ export default function OrganizationsPage({
|
||||
{ id: "type", header: "i18n:govoplan-organizations.function_type.501fe7c0", minWidth: 180, render: (row) => optionalLabel(functionTypeById.get(row.function_type_id || "")?.name) },
|
||||
{ id: "delegable", header: "i18n:govoplan-organizations.delegable.b4f0137d", width: 120, render: (row) => activeStatus(row.delegable) },
|
||||
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
|
||||
{ id: "actions", header: "", width: 130, sticky: "end", render: (row) => <ActiveToggleButton item={row} disabled={!canWriteFunctions || busy} onToggle={() => toggleFunction(row)} /> }
|
||||
{ id: "actions", header: "", width: 220, sticky: "end", render: (row) => <RowActions item={row} disabled={!canWriteFunctions || busy} onEdit={() => editFunction(row)} onToggle={() => toggleFunction(row)} /> }
|
||||
];
|
||||
|
||||
const assignmentColumns: DataGridColumn<OrganizationFunctionAssignmentItem>[] = [
|
||||
@@ -693,11 +919,15 @@ export default function OrganizationsPage({
|
||||
{ id: "subunits", header: "i18n:govoplan-organizations.applies_to_subunits.7a15f741", width: 150, render: (row) => activeStatus(row.applies_to_subunits) },
|
||||
{ id: "source", header: "i18n:govoplan-organizations.source.6da13add", width: 130, value: (row) => row.source },
|
||||
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
|
||||
{ id: "actions", header: "", width: 130, sticky: "end", render: (row) => <ActiveToggleButton item={row} disabled={!canAssignFunctions || busy} onToggle={() => toggleAssignment(row)} /> }
|
||||
{ id: "actions", header: "", width: 220, sticky: "end", render: (row) => <RowActions item={row} disabled={!canAssignFunctions || busy} onEdit={() => editAssignment(row)} onToggle={() => toggleAssignment(row)} /> }
|
||||
];
|
||||
|
||||
const canWriteActive = active === "model" ? canWriteModel : active === "units" || active === "relations" ? canWriteUnits : active === "functions" ? canWriteFunctions : canAssignFunctions;
|
||||
|
||||
function cancelEditButton(onCancel: () => void) {
|
||||
return <Button type="button" variant="ghost" onClick={onCancel} disabled={busy}>i18n:govoplan-organizations.cancel_edit.309c2a6f</Button>;
|
||||
}
|
||||
|
||||
const content = (
|
||||
<div className={`${mode === "workspace" ? "content-pad " : ""}organizations-page ${mode === "admin" ? "organizations-admin-page" : ""}`.trim()}>
|
||||
<div className="page-heading split organizations-heading">
|
||||
@@ -754,7 +984,10 @@ export default function OrganizationsPage({
|
||||
<Card title="i18n:govoplan-organizations.unit_types.c7afc174">
|
||||
<form className="organizations-form-grid" onSubmit={(event) => void submitUnitType(event)}>
|
||||
<SluggedFields draft={unitTypeDraft} onChange={setUnitTypeDraft} disabled={!canWriteModel || busy} />
|
||||
<Button className="wide" type="submit" variant="primary" disabled={!canWriteModel || busy}>i18n:govoplan-organizations.add_unit_type.58f2c05a</Button>
|
||||
<div className="organizations-form-actions wide">
|
||||
<Button type="submit" variant="primary" disabled={!canWriteModel || busy}>{editingUnitTypeId ? "i18n:govoplan-organizations.update_unit_type.b96fc0ad" : "i18n:govoplan-organizations.add_unit_type.58f2c05a"}</Button>
|
||||
{editingUnitTypeId && cancelEditButton(() => { setEditingUnitTypeId(null); setUnitTypeDraft(emptySluggedDraft()); })}
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
<Card title="i18n:govoplan-organizations.unit_types.c7afc174">
|
||||
@@ -770,7 +1003,10 @@ export default function OrganizationsPage({
|
||||
{STRUCTURE_KINDS.map((item) => <option key={item.value} value={item.value}>{item.label}</option>)}
|
||||
</select>
|
||||
</FormField>
|
||||
<Button className="wide" type="submit" variant="primary" disabled={!canWriteModel || busy}>i18n:govoplan-organizations.add_structure.b722042a</Button>
|
||||
<div className="organizations-form-actions wide">
|
||||
<Button type="submit" variant="primary" disabled={!canWriteModel || busy}>{editingStructureId ? "i18n:govoplan-organizations.update_structure.b2e25446" : "i18n:govoplan-organizations.add_structure.b722042a"}</Button>
|
||||
{editingStructureId && cancelEditButton(() => { setEditingStructureId(null); setStructureDraft(emptyStructureDraft()); })}
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
<Card title="i18n:govoplan-organizations.structures.f9b7f3b4">
|
||||
@@ -803,7 +1039,10 @@ export default function OrganizationsPage({
|
||||
<label><input type="checkbox" checked={relationTypeDraft.is_hierarchical} disabled={!canWriteModel || busy} onChange={(event) => setRelationTypeDraft({ ...relationTypeDraft, is_hierarchical: event.target.checked })} /> i18n:govoplan-organizations.hierarchical.8964f313</label>
|
||||
<label><input type="checkbox" checked={relationTypeDraft.allow_cycles} disabled={!canWriteModel || busy} onChange={(event) => setRelationTypeDraft({ ...relationTypeDraft, allow_cycles: event.target.checked })} /> i18n:govoplan-organizations.allow_cycles.31327578</label>
|
||||
</div>
|
||||
<Button className="wide" type="submit" variant="primary" disabled={!canWriteModel || busy}>i18n:govoplan-organizations.add_relation_type.2ad19d03</Button>
|
||||
<div className="organizations-form-actions wide">
|
||||
<Button type="submit" variant="primary" disabled={!canWriteModel || busy}>{editingRelationTypeId ? "i18n:govoplan-organizations.update_relation_type.e6e7f30c" : "i18n:govoplan-organizations.add_relation_type.2ad19d03"}</Button>
|
||||
{editingRelationTypeId && cancelEditButton(() => { setEditingRelationTypeId(null); setRelationTypeDraft(emptyRelationTypeDraft()); })}
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
<Card title="i18n:govoplan-organizations.relation_types.e5890528">
|
||||
@@ -824,7 +1063,10 @@ export default function OrganizationsPage({
|
||||
<label><input type="checkbox" checked={functionTypeDraft.delegable} disabled={!canWriteModel || busy} onChange={(event) => setFunctionTypeDraft({ ...functionTypeDraft, delegable: event.target.checked })} /> i18n:govoplan-organizations.delegable.b4f0137d</label>
|
||||
<label><input type="checkbox" checked={functionTypeDraft.act_in_place_allowed} disabled={!canWriteModel || busy} onChange={(event) => setFunctionTypeDraft({ ...functionTypeDraft, act_in_place_allowed: event.target.checked })} /> i18n:govoplan-organizations.act_in_place.49b942bd</label>
|
||||
</div>
|
||||
<Button className="wide" type="submit" variant="primary" disabled={!canWriteModel || busy}>i18n:govoplan-organizations.add_function_type.90d793f1</Button>
|
||||
<div className="organizations-form-actions wide">
|
||||
<Button type="submit" variant="primary" disabled={!canWriteModel || busy}>{editingFunctionTypeId ? "i18n:govoplan-organizations.update_function_type.0b7e07d3" : "i18n:govoplan-organizations.add_function_type.90d793f1"}</Button>
|
||||
{editingFunctionTypeId && cancelEditButton(() => { setEditingFunctionTypeId(null); setFunctionTypeDraft(emptyFunctionTypeDraft()); })}
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
<Card title="i18n:govoplan-organizations.function_types.172c01fe">
|
||||
@@ -835,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() {
|
||||
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">
|
||||
<form className="organizations-form-grid" onSubmit={(event) => void submitUnit(event)}>
|
||||
<SluggedFields draft={unitDraft} onChange={setUnitDraft} disabled={!canWriteUnits || busy} />
|
||||
@@ -850,16 +1128,25 @@ export default function OrganizationsPage({
|
||||
<FormField label="i18n:govoplan-organizations.parent_unit.1986c35e">
|
||||
<select value={unitDraft.parent_id} disabled={!canWriteUnits || busy} onChange={(event) => setUnitDraft({ ...unitDraft, parent_id: event.target.value })}>
|
||||
<option value="">i18n:govoplan-organizations.none.334c4a4c</option>
|
||||
{model.units.map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
|
||||
{model.units.filter((item) => item.id !== editingUnitId).map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
|
||||
</select>
|
||||
</FormField>
|
||||
<Button className="wide" type="submit" variant="primary" disabled={!canWriteUnits || busy}>i18n:govoplan-organizations.add_unit.8fa12fb1</Button>
|
||||
{unitDraft.parent_id && (
|
||||
<p className="organizations-field-note wide">
|
||||
i18n:govoplan-organizations.selected_parent.0e013d44 <strong>{unitById.get(unitDraft.parent_id)?.name ?? unitDraft.parent_id}</strong>
|
||||
</p>
|
||||
)}
|
||||
<div className="organizations-form-actions wide">
|
||||
<Button type="submit" variant="primary" disabled={!canWriteUnits || busy}>{editingUnitId ? "i18n:govoplan-organizations.update_unit.67e2500f" : "i18n:govoplan-organizations.add_unit.8fa12fb1"}</Button>
|
||||
{editingUnitId && cancelEditButton(() => { setEditingUnitId(null); setUnitDraft(emptyUnitDraft()); })}
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
<Card title="i18n:govoplan-organizations.units.e14d0d92">
|
||||
<DataGrid id="organizations-units" rows={model.units} columns={unitColumns} getRowKey={(row) => row.id} emptyText="i18n:govoplan-organizations.no_units_found.eea2dd0c" initialFit="container" />
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -896,7 +1183,10 @@ export default function OrganizationsPage({
|
||||
<input type="checkbox" checked={relationDraft.is_active} disabled={!canWriteUnits || busy} onChange={(event) => setRelationDraft({ ...relationDraft, is_active: event.target.checked })} />
|
||||
<span>i18n:govoplan-organizations.active.a733b809</span>
|
||||
</label>
|
||||
<Button className="wide" type="submit" variant="primary" disabled={!canWriteUnits || busy}>i18n:govoplan-organizations.add_relation.6b6e67ea</Button>
|
||||
<div className="organizations-form-actions wide">
|
||||
<Button type="submit" variant="primary" disabled={!canWriteUnits || busy}>{editingRelationId ? "i18n:govoplan-organizations.update_relation.4c44ce83" : "i18n:govoplan-organizations.add_relation.6b6e67ea"}</Button>
|
||||
{editingRelationId && cancelEditButton(() => { setEditingRelationId(null); setRelationDraft(emptyRelationDraft()); })}
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
<Card title="i18n:govoplan-organizations.relations.1c796711">
|
||||
@@ -928,7 +1218,10 @@ export default function OrganizationsPage({
|
||||
<label><input type="checkbox" checked={functionDraft.delegable} disabled={!canWriteFunctions || busy} onChange={(event) => setFunctionDraft({ ...functionDraft, delegable: event.target.checked })} /> i18n:govoplan-organizations.delegable.b4f0137d</label>
|
||||
<label><input type="checkbox" checked={functionDraft.act_in_place_allowed} disabled={!canWriteFunctions || busy} onChange={(event) => setFunctionDraft({ ...functionDraft, act_in_place_allowed: event.target.checked })} /> i18n:govoplan-organizations.act_in_place.49b942bd</label>
|
||||
</div>
|
||||
<Button className="wide" type="submit" variant="primary" disabled={!canWriteFunctions || busy}>i18n:govoplan-organizations.add_function.6abafee0</Button>
|
||||
<div className="organizations-form-actions wide">
|
||||
<Button type="submit" variant="primary" disabled={!canWriteFunctions || busy}>{editingFunctionId ? "i18n:govoplan-organizations.update_function.504f03e5" : "i18n:govoplan-organizations.add_function.6abafee0"}</Button>
|
||||
{editingFunctionId && cancelEditButton(() => { setEditingFunctionId(null); setFunctionDraft(emptyFunctionDraft()); })}
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
<Card title="i18n:govoplan-organizations.functions.805dc49b">
|
||||
@@ -962,7 +1255,10 @@ export default function OrganizationsPage({
|
||||
<label><input type="checkbox" checked={assignmentDraft.applies_to_subunits} disabled={!canAssignFunctions || busy} onChange={(event) => setAssignmentDraft({ ...assignmentDraft, applies_to_subunits: event.target.checked })} /> i18n:govoplan-organizations.applies_to_subunits.7a15f741</label>
|
||||
<label><input type="checkbox" checked={assignmentDraft.is_active} disabled={!canAssignFunctions || busy} onChange={(event) => setAssignmentDraft({ ...assignmentDraft, is_active: event.target.checked })} /> i18n:govoplan-organizations.active.a733b809</label>
|
||||
</div>
|
||||
<Button className="wide" type="submit" variant="primary" disabled={!canAssignFunctions || busy}>i18n:govoplan-organizations.add_assignment.6682f5f5</Button>
|
||||
<div className="organizations-form-actions wide">
|
||||
<Button type="submit" variant="primary" disabled={!canAssignFunctions || busy}>{editingAssignmentId ? "i18n:govoplan-organizations.update_assignment.bf130723" : "i18n:govoplan-organizations.add_assignment.6682f5f5"}</Button>
|
||||
{editingAssignmentId && cancelEditButton(() => { setEditingAssignmentId(null); setAssignmentDraft(emptyAssignmentDraft()); })}
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
<Card title="i18n:govoplan-organizations.function_assignments.4c5c6dae">
|
||||
|
||||
@@ -9,26 +9,41 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-organizations.add_function_type.90d793f1": "Add function type",
|
||||
"i18n:govoplan-organizations.add_relation.6b6e67ea": "Add relation",
|
||||
"i18n:govoplan-organizations.add_relation_type.2ad19d03": "Add relation type",
|
||||
"i18n:govoplan-organizations.add_root_unit.1ef8a9f5": "Add root unit",
|
||||
"i18n:govoplan-organizations.add_structure.b722042a": "Add structure",
|
||||
"i18n:govoplan-organizations.add_subunit.8256a8f7": "Add subunit",
|
||||
"i18n:govoplan-organizations.add_unit.8fa12fb1": "Add unit",
|
||||
"i18n:govoplan-organizations.add_unit_type.58f2c05a": "Add unit type",
|
||||
"i18n:govoplan-organizations.allow_cycles.31327578": "Allow cycles",
|
||||
"i18n:govoplan-organizations.applies_to_subunits.7a15f741": "Applies to subunits",
|
||||
"i18n:govoplan-organizations.assignment_added.94263d1b": "Function assignment added.",
|
||||
"i18n:govoplan-organizations.assignment_updated.680b6e33": "Function assignment updated.",
|
||||
"i18n:govoplan-organizations.assignments.278f513e": "Assignments",
|
||||
"i18n:govoplan-organizations.audit_and_retention.3ba1d2fc": "Audit and retention",
|
||||
"i18n:govoplan-organizations.audit_detail_level.7397355d": "Audit detail level",
|
||||
"i18n:govoplan-organizations.audit_full.0c83669c": "Full",
|
||||
"i18n:govoplan-organizations.audit_retention_help.42dec57d": "Controls how much organization-change detail is retained for review, troubleshooting, and evidence.",
|
||||
"i18n:govoplan-organizations.audit_standard.785d981f": "Standard",
|
||||
"i18n:govoplan-organizations.audit_summary.6a7d68a5": "Summary",
|
||||
"i18n:govoplan-organizations.classification.3e9f6c3a": "Classification",
|
||||
"i18n:govoplan-organizations.allow_tenant_model_customization.2425d751": "Tenant admins may customize the organization meta-model",
|
||||
"i18n:govoplan-organizations.change_retention_days.71bbd140": "Retain organization change detail for days",
|
||||
"i18n:govoplan-organizations.cancel_edit.309c2a6f": "Cancel edit",
|
||||
"i18n:govoplan-organizations.concrete_model.4e9c531a": "Concrete model",
|
||||
"i18n:govoplan-organizations.deactivate.46ab070d": "Deactivate",
|
||||
"i18n:govoplan-organizations.delegable.b4f0137d": "Delegable",
|
||||
"i18n:govoplan-organizations.description.55f8ebc8": "Description",
|
||||
"i18n:govoplan-organizations.direct_assignment.2a068aac": "direct",
|
||||
"i18n:govoplan-organizations.discard_organization_drafts.766d5f46": "Discard organization drafts",
|
||||
"i18n:govoplan-organizations.edit.7dce1220": "Edit",
|
||||
"i18n:govoplan-organizations.function.28822f3a": "Function",
|
||||
"i18n:govoplan-organizations.function_added.e2266702": "Function added.",
|
||||
"i18n:govoplan-organizations.function_assignments.4c5c6dae": "Function assignments",
|
||||
"i18n:govoplan-organizations.function_type.501fe7c0": "Function type",
|
||||
"i18n:govoplan-organizations.function_type_added.2cd9e899": "Function type added.",
|
||||
"i18n:govoplan-organizations.function_type_updated.1a4f29f6": "Function type updated.",
|
||||
"i18n:govoplan-organizations.function_types.172c01fe": "Function types",
|
||||
"i18n:govoplan-organizations.function_updated.65016009": "Function updated.",
|
||||
"i18n:govoplan-organizations.functions.805dc49b": "Functions",
|
||||
"i18n:govoplan-organizations.hierarchical.8964f313": "Hierarchical",
|
||||
"i18n:govoplan-organizations.hierarchy.74797f37": "Hierarchy",
|
||||
@@ -37,9 +52,12 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-organizations.inactive.09af574c": "Inactive",
|
||||
"i18n:govoplan-organizations.kind.794c9d9c": "Kind",
|
||||
"i18n:govoplan-organizations.loading_organization_model.846aa317": "Loading organization model...",
|
||||
"i18n:govoplan-organizations.loading_organization_settings.c6008db8": "Loading organization settings...",
|
||||
"i18n:govoplan-organizations.membership.4531d86d": "Membership",
|
||||
"i18n:govoplan-organizations.meta_model.7398487c": "Meta-model",
|
||||
"i18n:govoplan-organizations.model.20f35e63": "Model",
|
||||
"i18n:govoplan-organizations.model_governance.6aa18fd0": "Model governance",
|
||||
"i18n:govoplan-organizations.model_governance_help.0fa69021": "These switches define whether the tenant can change its own organization model directly or must record governed change requests.",
|
||||
"i18n:govoplan-organizations.name.709a2322": "Name",
|
||||
"i18n:govoplan-organizations.name_is_required.27cd3782": "Name is required.",
|
||||
"i18n:govoplan-organizations.network.a24d31c3": "Network",
|
||||
@@ -49,23 +67,33 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-organizations.no_relation_types_found.6f90bb81": "No relation types found.",
|
||||
"i18n:govoplan-organizations.no_relations_found.4e75b11d": "No relations found.",
|
||||
"i18n:govoplan-organizations.no_structures_found.20b382d0": "No structures found.",
|
||||
"i18n:govoplan-organizations.no_unit_type.73e799f5": "No unit type",
|
||||
"i18n:govoplan-organizations.no_unit_types_found.c81fb2a7": "No unit types found.",
|
||||
"i18n:govoplan-organizations.no_units_found.eea2dd0c": "No units found.",
|
||||
"i18n:govoplan-organizations.none.334c4a4c": "None",
|
||||
"i18n:govoplan-organizations.optional_account_id.5fcf495c": "Optional account ID",
|
||||
"i18n:govoplan-organizations.organization_model.4f924c0e": "Organization model",
|
||||
"i18n:govoplan-organizations.organization_model_admin_description.35dc9f10": "Configure tenant-local unit types, parallel structures, relation types, and function types used by the organization module.",
|
||||
"i18n:govoplan-organizations.organization_settings.c9ab9829": "Organization settings",
|
||||
"i18n:govoplan-organizations.organization_settings_description.35dc9f10": "Configure tenant-level governance, audit, and retention behavior for organization changes.",
|
||||
"i18n:govoplan-organizations.organization_settings_saved.bfbcfdfa": "Organization settings saved.",
|
||||
"i18n:govoplan-organizations.organization_tree.e5bfb195": "Organization tree",
|
||||
"i18n:govoplan-organizations.organizations.220edf64": "Organizations",
|
||||
"i18n:govoplan-organizations.organizations_intro.4e67c4bb": "Model organization types, concrete units, structures, functions, and identity-held function assignments.",
|
||||
"i18n:govoplan-organizations.parent_unit.1986c35e": "Parent unit",
|
||||
"i18n:govoplan-organizations.reactivate.58f2855d": "Reactivate",
|
||||
"i18n:govoplan-organizations.relation_added.ca90461a": "Relation added.",
|
||||
"i18n:govoplan-organizations.relation_updated.c6212776": "Relation updated.",
|
||||
"i18n:govoplan-organizations.relation_type.d0aee2e7": "Relation type",
|
||||
"i18n:govoplan-organizations.relation_type_added.6f1edff1": "Relation type added.",
|
||||
"i18n:govoplan-organizations.relation_type_updated.5ac57ec7": "Relation type updated.",
|
||||
"i18n:govoplan-organizations.relation_types.e5890528": "Relation types",
|
||||
"i18n:govoplan-organizations.relations.1c796711": "Relations",
|
||||
"i18n:govoplan-organizations.reload.cce71553": "Reload",
|
||||
"i18n:govoplan-organizations.require_model_change_requests.83454cad": "Organization model changes require recorded change requests",
|
||||
"i18n:govoplan-organizations.save_settings.913aba9f": "Save settings",
|
||||
"i18n:govoplan-organizations.save_drafts.606f9401": "Save drafts",
|
||||
"i18n:govoplan-organizations.saving.56a2285c": "Saving...",
|
||||
"i18n:govoplan-organizations.select_function.4895a67d": "Select function",
|
||||
"i18n:govoplan-organizations.select_function_type.2f426c43": "Select function type",
|
||||
"i18n:govoplan-organizations.select_relation_type.73f92478": "Select relation type",
|
||||
@@ -74,21 +102,34 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-organizations.select_target_unit.8d607541": "Select target unit",
|
||||
"i18n:govoplan-organizations.select_unit.013bf13a": "Select unit",
|
||||
"i18n:govoplan-organizations.select_unit_type.2c71d14f": "Select unit type",
|
||||
"i18n:govoplan-organizations.selected_parent.0e013d44": "Selected parent:",
|
||||
"i18n:govoplan-organizations.slug.094da9b9": "Slug",
|
||||
"i18n:govoplan-organizations.source.6da13add": "Source",
|
||||
"i18n:govoplan-organizations.source_unit.2fbd8baa": "Source unit",
|
||||
"i18n:govoplan-organizations.status.bae7d5be": "Status",
|
||||
"i18n:govoplan-organizations.structure.7732fb0b": "Structure",
|
||||
"i18n:govoplan-organizations.structure_added.f6cf8e74": "Structure added.",
|
||||
"i18n:govoplan-organizations.structure_updated.2591e75f": "Structure updated.",
|
||||
"i18n:govoplan-organizations.structures.f9b7f3b4": "Structures",
|
||||
"i18n:govoplan-organizations.target_unit.a1507d86": "Target unit",
|
||||
"i18n:govoplan-organizations.type.599dcce2": "Type",
|
||||
"i18n:govoplan-organizations.unit.8fe4d595": "Unit",
|
||||
"i18n:govoplan-organizations.unit_added.760a8512": "Unit added.",
|
||||
"i18n:govoplan-organizations.unit_updated.15f02216": "Unit updated.",
|
||||
"i18n:govoplan-organizations.unit_type.9e62810d": "Unit type",
|
||||
"i18n:govoplan-organizations.unit_type_added.e017dc8c": "Unit type added.",
|
||||
"i18n:govoplan-organizations.unit_type_updated.6d8f2a16": "Unit type updated.",
|
||||
"i18n:govoplan-organizations.unit_types.c7afc174": "Unit types",
|
||||
"i18n:govoplan-organizations.units.e14d0d92": "Units",
|
||||
"i18n:govoplan-organizations.unlimited.35569464": "Unlimited",
|
||||
"i18n:govoplan-organizations.update_assignment.bf130723": "Update assignment",
|
||||
"i18n:govoplan-organizations.update_function.504f03e5": "Update function",
|
||||
"i18n:govoplan-organizations.update_function_type.0b7e07d3": "Update function type",
|
||||
"i18n:govoplan-organizations.update_relation.4c44ce83": "Update relation",
|
||||
"i18n:govoplan-organizations.update_relation_type.e6e7f30c": "Update relation type",
|
||||
"i18n:govoplan-organizations.update_structure.b2e25446": "Update structure",
|
||||
"i18n:govoplan-organizations.update_unit.67e2500f": "Update unit",
|
||||
"i18n:govoplan-organizations.update_unit_type.b96fc0ad": "Update unit type",
|
||||
"i18n:govoplan-organizations.write_permission_required.8b09fd67": "You do not have write permission for this organization section."
|
||||
},
|
||||
de: {
|
||||
@@ -99,26 +140,41 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-organizations.add_function_type.90d793f1": "Funktionstyp hinzufügen",
|
||||
"i18n:govoplan-organizations.add_relation.6b6e67ea": "Beziehung hinzufügen",
|
||||
"i18n:govoplan-organizations.add_relation_type.2ad19d03": "Beziehungstyp hinzufügen",
|
||||
"i18n:govoplan-organizations.add_root_unit.1ef8a9f5": "Wurzeleinheit hinzufügen",
|
||||
"i18n:govoplan-organizations.add_structure.b722042a": "Struktur hinzufügen",
|
||||
"i18n:govoplan-organizations.add_subunit.8256a8f7": "Untereinheit hinzufügen",
|
||||
"i18n:govoplan-organizations.add_unit.8fa12fb1": "Einheit hinzufügen",
|
||||
"i18n:govoplan-organizations.add_unit_type.58f2c05a": "Einheitstyp hinzufügen",
|
||||
"i18n:govoplan-organizations.allow_cycles.31327578": "Zyklen erlauben",
|
||||
"i18n:govoplan-organizations.applies_to_subunits.7a15f741": "Gilt für Untereinheiten",
|
||||
"i18n:govoplan-organizations.assignment_added.94263d1b": "Funktionszuordnung hinzugefügt.",
|
||||
"i18n:govoplan-organizations.assignment_updated.680b6e33": "Funktionszuordnung aktualisiert.",
|
||||
"i18n:govoplan-organizations.assignments.278f513e": "Zuordnungen",
|
||||
"i18n:govoplan-organizations.audit_and_retention.3ba1d2fc": "Audit und Aufbewahrung",
|
||||
"i18n:govoplan-organizations.audit_detail_level.7397355d": "Audit-Detailgrad",
|
||||
"i18n:govoplan-organizations.audit_full.0c83669c": "Vollständig",
|
||||
"i18n:govoplan-organizations.audit_retention_help.42dec57d": "Steuert, wie viele Details zu Organisationsänderungen für Prüfung, Fehlersuche und Nachweise aufbewahrt werden.",
|
||||
"i18n:govoplan-organizations.audit_standard.785d981f": "Standard",
|
||||
"i18n:govoplan-organizations.audit_summary.6a7d68a5": "Zusammenfassung",
|
||||
"i18n:govoplan-organizations.classification.3e9f6c3a": "Klassifikation",
|
||||
"i18n:govoplan-organizations.allow_tenant_model_customization.2425d751": "Mandantenadmins dürfen das Organisations-Metamodell anpassen",
|
||||
"i18n:govoplan-organizations.change_retention_days.71bbd140": "Änderungsdetails zur Organisation in Tagen aufbewahren",
|
||||
"i18n:govoplan-organizations.cancel_edit.309c2a6f": "Bearbeitung abbrechen",
|
||||
"i18n:govoplan-organizations.concrete_model.4e9c531a": "Konkretes Modell",
|
||||
"i18n:govoplan-organizations.deactivate.46ab070d": "Deaktivieren",
|
||||
"i18n:govoplan-organizations.delegable.b4f0137d": "Delegierbar",
|
||||
"i18n:govoplan-organizations.description.55f8ebc8": "Beschreibung",
|
||||
"i18n:govoplan-organizations.direct_assignment.2a068aac": "direkt",
|
||||
"i18n:govoplan-organizations.discard_organization_drafts.766d5f46": "Organisationsentwürfe verwerfen",
|
||||
"i18n:govoplan-organizations.edit.7dce1220": "Bearbeiten",
|
||||
"i18n:govoplan-organizations.function.28822f3a": "Funktion",
|
||||
"i18n:govoplan-organizations.function_added.e2266702": "Funktion hinzugefügt.",
|
||||
"i18n:govoplan-organizations.function_assignments.4c5c6dae": "Funktionszuordnungen",
|
||||
"i18n:govoplan-organizations.function_type.501fe7c0": "Funktionstyp",
|
||||
"i18n:govoplan-organizations.function_type_added.2cd9e899": "Funktionstyp hinzugefügt.",
|
||||
"i18n:govoplan-organizations.function_type_updated.1a4f29f6": "Funktionstyp aktualisiert.",
|
||||
"i18n:govoplan-organizations.function_types.172c01fe": "Funktionstypen",
|
||||
"i18n:govoplan-organizations.function_updated.65016009": "Funktion aktualisiert.",
|
||||
"i18n:govoplan-organizations.functions.805dc49b": "Funktionen",
|
||||
"i18n:govoplan-organizations.hierarchical.8964f313": "Hierarchisch",
|
||||
"i18n:govoplan-organizations.hierarchy.74797f37": "Hierarchie",
|
||||
@@ -127,9 +183,12 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-organizations.inactive.09af574c": "Inaktiv",
|
||||
"i18n:govoplan-organizations.kind.794c9d9c": "Art",
|
||||
"i18n:govoplan-organizations.loading_organization_model.846aa317": "Organisationsmodell wird geladen...",
|
||||
"i18n:govoplan-organizations.loading_organization_settings.c6008db8": "Organisationseinstellungen werden geladen...",
|
||||
"i18n:govoplan-organizations.membership.4531d86d": "Mitgliedschaft",
|
||||
"i18n:govoplan-organizations.meta_model.7398487c": "Metamodell",
|
||||
"i18n:govoplan-organizations.model.20f35e63": "Modell",
|
||||
"i18n:govoplan-organizations.model_governance.6aa18fd0": "Modell-Governance",
|
||||
"i18n:govoplan-organizations.model_governance_help.0fa69021": "Diese Schalter legen fest, ob der Mandant sein Organisationsmodell direkt ändern darf oder geregelte Änderungsanträge erfassen muss.",
|
||||
"i18n:govoplan-organizations.name.709a2322": "Name",
|
||||
"i18n:govoplan-organizations.name_is_required.27cd3782": "Name ist erforderlich.",
|
||||
"i18n:govoplan-organizations.network.a24d31c3": "Netzwerk",
|
||||
@@ -139,23 +198,33 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-organizations.no_relation_types_found.6f90bb81": "Keine Beziehungstypen gefunden.",
|
||||
"i18n:govoplan-organizations.no_relations_found.4e75b11d": "Keine Beziehungen gefunden.",
|
||||
"i18n:govoplan-organizations.no_structures_found.20b382d0": "Keine Strukturen gefunden.",
|
||||
"i18n:govoplan-organizations.no_unit_type.73e799f5": "Kein Einheitstyp",
|
||||
"i18n:govoplan-organizations.no_unit_types_found.c81fb2a7": "Keine Einheitstypen gefunden.",
|
||||
"i18n:govoplan-organizations.no_units_found.eea2dd0c": "Keine Einheiten gefunden.",
|
||||
"i18n:govoplan-organizations.none.334c4a4c": "Keine",
|
||||
"i18n:govoplan-organizations.optional_account_id.5fcf495c": "Optionale Konto-ID",
|
||||
"i18n:govoplan-organizations.organization_model.4f924c0e": "Organisationsmodell",
|
||||
"i18n:govoplan-organizations.organization_model_admin_description.35dc9f10": "Konfiguriere mandantenbezogene Einheitstypen, parallele Strukturen, Beziehungstypen und Funktionstypen des Organisationsmoduls.",
|
||||
"i18n:govoplan-organizations.organization_settings.c9ab9829": "Organisationseinstellungen",
|
||||
"i18n:govoplan-organizations.organization_settings_description.35dc9f10": "Konfiguriere Governance, Audit und Aufbewahrung für Organisationsänderungen auf Mandantenebene.",
|
||||
"i18n:govoplan-organizations.organization_settings_saved.bfbcfdfa": "Organisationseinstellungen gespeichert.",
|
||||
"i18n:govoplan-organizations.organization_tree.e5bfb195": "Organisationsbaum",
|
||||
"i18n:govoplan-organizations.organizations.220edf64": "Organisationen",
|
||||
"i18n:govoplan-organizations.organizations_intro.4e67c4bb": "Modelliere Organisationstypen, konkrete Einheiten, Strukturen, Funktionen und identitätsbezogene Funktionszuordnungen.",
|
||||
"i18n:govoplan-organizations.parent_unit.1986c35e": "Übergeordnete Einheit",
|
||||
"i18n:govoplan-organizations.reactivate.58f2855d": "Reaktivieren",
|
||||
"i18n:govoplan-organizations.relation_added.ca90461a": "Beziehung hinzugefügt.",
|
||||
"i18n:govoplan-organizations.relation_updated.c6212776": "Beziehung aktualisiert.",
|
||||
"i18n:govoplan-organizations.relation_type.d0aee2e7": "Beziehungstyp",
|
||||
"i18n:govoplan-organizations.relation_type_added.6f1edff1": "Beziehungstyp hinzugefügt.",
|
||||
"i18n:govoplan-organizations.relation_type_updated.5ac57ec7": "Beziehungstyp aktualisiert.",
|
||||
"i18n:govoplan-organizations.relation_types.e5890528": "Beziehungstypen",
|
||||
"i18n:govoplan-organizations.relations.1c796711": "Beziehungen",
|
||||
"i18n:govoplan-organizations.reload.cce71553": "Neu laden",
|
||||
"i18n:govoplan-organizations.require_model_change_requests.83454cad": "Änderungen am Organisationsmodell erfordern erfasste Änderungsanträge",
|
||||
"i18n:govoplan-organizations.save_settings.913aba9f": "Einstellungen speichern",
|
||||
"i18n:govoplan-organizations.save_drafts.606f9401": "Entwürfe speichern",
|
||||
"i18n:govoplan-organizations.saving.56a2285c": "Speichern...",
|
||||
"i18n:govoplan-organizations.select_function.4895a67d": "Funktion auswählen",
|
||||
"i18n:govoplan-organizations.select_function_type.2f426c43": "Funktionstyp auswählen",
|
||||
"i18n:govoplan-organizations.select_relation_type.73f92478": "Beziehungstyp auswählen",
|
||||
@@ -164,21 +233,34 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-organizations.select_target_unit.8d607541": "Ziel-Einheit auswählen",
|
||||
"i18n:govoplan-organizations.select_unit.013bf13a": "Einheit auswählen",
|
||||
"i18n:govoplan-organizations.select_unit_type.2c71d14f": "Einheitstyp auswählen",
|
||||
"i18n:govoplan-organizations.selected_parent.0e013d44": "Ausgewählte übergeordnete Einheit:",
|
||||
"i18n:govoplan-organizations.slug.094da9b9": "Slug",
|
||||
"i18n:govoplan-organizations.source.6da13add": "Quelle",
|
||||
"i18n:govoplan-organizations.source_unit.2fbd8baa": "Quell-Einheit",
|
||||
"i18n:govoplan-organizations.status.bae7d5be": "Status",
|
||||
"i18n:govoplan-organizations.structure.7732fb0b": "Struktur",
|
||||
"i18n:govoplan-organizations.structure_added.f6cf8e74": "Struktur hinzugefügt.",
|
||||
"i18n:govoplan-organizations.structure_updated.2591e75f": "Struktur aktualisiert.",
|
||||
"i18n:govoplan-organizations.structures.f9b7f3b4": "Strukturen",
|
||||
"i18n:govoplan-organizations.target_unit.a1507d86": "Ziel-Einheit",
|
||||
"i18n:govoplan-organizations.type.599dcce2": "Typ",
|
||||
"i18n:govoplan-organizations.unit.8fe4d595": "Einheit",
|
||||
"i18n:govoplan-organizations.unit_added.760a8512": "Einheit hinzugefügt.",
|
||||
"i18n:govoplan-organizations.unit_updated.15f02216": "Einheit aktualisiert.",
|
||||
"i18n:govoplan-organizations.unit_type.9e62810d": "Einheitstyp",
|
||||
"i18n:govoplan-organizations.unit_type_added.e017dc8c": "Einheitstyp hinzugefügt.",
|
||||
"i18n:govoplan-organizations.unit_type_updated.6d8f2a16": "Einheitstyp aktualisiert.",
|
||||
"i18n:govoplan-organizations.unit_types.c7afc174": "Einheitstypen",
|
||||
"i18n:govoplan-organizations.units.e14d0d92": "Einheiten",
|
||||
"i18n:govoplan-organizations.unlimited.35569464": "Unbegrenzt",
|
||||
"i18n:govoplan-organizations.update_assignment.bf130723": "Zuordnung aktualisieren",
|
||||
"i18n:govoplan-organizations.update_function.504f03e5": "Funktion aktualisieren",
|
||||
"i18n:govoplan-organizations.update_function_type.0b7e07d3": "Funktionstyp aktualisieren",
|
||||
"i18n:govoplan-organizations.update_relation.4c44ce83": "Beziehung aktualisieren",
|
||||
"i18n:govoplan-organizations.update_relation_type.e6e7f30c": "Beziehungstyp aktualisieren",
|
||||
"i18n:govoplan-organizations.update_structure.b2e25446": "Struktur aktualisieren",
|
||||
"i18n:govoplan-organizations.update_unit.67e2500f": "Einheit aktualisieren",
|
||||
"i18n:govoplan-organizations.update_unit_type.b96fc0ad": "Einheitstyp aktualisieren",
|
||||
"i18n:govoplan-organizations.write_permission_required.8b09fd67": "Du hast keine Schreibberechtigung für diesen Organisationsbereich."
|
||||
}
|
||||
};
|
||||
|
||||
@@ -21,11 +21,11 @@ const translations = {
|
||||
const organizationAdminSections: AdminSectionsUiCapability = {
|
||||
sections: [
|
||||
{
|
||||
id: "tenant-organization-model",
|
||||
label: "i18n:govoplan-organizations.organization_model.4f924c0e",
|
||||
id: "tenant-organization-settings",
|
||||
label: "i18n:govoplan-organizations.organization_settings.c9ab9829",
|
||||
group: "TENANT",
|
||||
order: 85,
|
||||
anyOf: ["organizations:model:read", "admin:settings:read"],
|
||||
anyOf: ["organizations:settings:read", "admin:settings:read"],
|
||||
render: ({ settings, auth }) => createElement(OrganizationsAdminPanel, { settings, auth })
|
||||
}
|
||||
]
|
||||
|
||||
@@ -14,6 +14,11 @@
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.organizations-settings-grid {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.organizations-heading {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
@@ -33,6 +38,13 @@
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.organizations-editor-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(260px, 340px) minmax(0, 1fr);
|
||||
gap: 18px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.organizations-form-panel,
|
||||
.organizations-table-panel {
|
||||
min-width: 0;
|
||||
@@ -53,6 +65,18 @@
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.organizations-form-actions,
|
||||
.organizations-row-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.organizations-row-actions {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.organizations-check-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
@@ -73,6 +97,51 @@
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.organizations-tree-toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.organizations-tree-list {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
max-height: 640px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.organizations-tree-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.organizations-tree-node {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 7px;
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.organizations-tree-node:hover,
|
||||
.organizations-tree-node.active {
|
||||
border-color: var(--line);
|
||||
background: var(--surface-muted, #f6f7f9);
|
||||
}
|
||||
|
||||
.organizations-tree-node span {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.organizations-field-note {
|
||||
margin: 8px 0 0;
|
||||
color: var(--muted);
|
||||
@@ -90,7 +159,8 @@
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.organizations-section-grid {
|
||||
.organizations-section-grid,
|
||||
.organizations-editor-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user