13 Commits
v0.1.6 ... main

16 changed files with 740 additions and 463 deletions

View File

@@ -1,5 +1,9 @@
# GovOPlaN Organizations # GovOPlaN Organizations
<!-- govoplan-repository-type:start -->
**Repository type:** module (platform).
<!-- govoplan-repository-type:end -->
`govoplan-organizations` is the canonical organizational model module for `govoplan-organizations` is the canonical organizational model module for
GovOPlaN. GovOPlaN.

View File

@@ -36,6 +36,36 @@ delegation enforcement, and explain responses.
This separation keeps the institution model stable even when authorization This separation keeps the institution model stable even when authorization
policy changes. policy changes.
## Access Projection Migration
`govoplan-access` now prefers `organizations.directory` for organization units
and function definitions when the Organizations module is installed. It uses
`govoplan-idm` for identity-to-function assignment facts and Access-owned
external function-role mappings to turn accepted function facts into rights.
The legacy Access tables `access_organization_units`, `access_functions`, and
`access_function_assignments` remain readable as compatibility/projection
tables during rollout. They are no longer the target model for new module
integrations.
Rollout plan:
- keep direct Access function/organization reads as a fallback when
Organizations or IDM are not installed;
- backfill Organizations units/functions from the Access projection for
existing installations that enable the Organizations module later;
- move new identity-to-function assignment workflows to IDM rather than adding
assignment ownership to Organizations;
- use Access external function-role mappings for authorization and check that
the referenced Organizations function is still active before granting
derived permissions;
- retire Access-owned organization/function tables only after a release-level
migration/backfill and rollback plan exists.
The close-out condition is that Access role resolution works with canonical
Organizations plus IDM installed and still works through projection fallback
for transition deployments.
## UI Boundary ## UI Boundary
The organization workspace at `/organizations` is the primary module UI for The organization workspace at `/organizations` is the primary module UI for

View File

@@ -1,6 +1,6 @@
{ {
"name": "@govoplan/organizations-webui", "name": "@govoplan/organizations-webui",
"version": "0.1.6", "version": "0.1.8",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "webui/src/index.ts", "main": "webui/src/index.ts",
@@ -19,7 +19,7 @@
"LICENSE" "LICENSE"
], ],
"peerDependencies": { "peerDependencies": {
"@govoplan/core-webui": "^0.1.6", "@govoplan/core-webui": "^0.1.9",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
"lucide-react": "^1.23.0", "lucide-react": "^1.23.0",
"react": "^19.0.0", "react": "^19.0.0",

View File

@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "govoplan-organizations" name = "govoplan-organizations"
version = "0.1.6" version = "0.1.8"
description = "GovOPlaN organizational model module." description = "GovOPlaN organizational model module."
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"
authors = [{ name = "GovOPlaN" }] authors = [{ name = "GovOPlaN" }]
dependencies = [ dependencies = [
"govoplan-core>=0.1.6", "govoplan-core>=0.1.8",
"govoplan-tenancy>=0.1.6", "govoplan-tenancy>=0.1.8",
] ]
[tool.setuptools.packages.find] [tool.setuptools.packages.find]

View File

@@ -1,3 +1,3 @@
"""GovOPlaN organizations module.""" """GovOPlaN organizations module."""
__version__ = "0.1.6" __version__ = "0.1.8"

View File

@@ -16,6 +16,7 @@ from govoplan_core.core.modules import (
RoleTemplate, RoleTemplate,
) )
from govoplan_core.core.organizations import CAPABILITY_ORGANIZATION_DIRECTORY from govoplan_core.core.organizations import CAPABILITY_ORGANIZATION_DIRECTORY
from govoplan_core.core.views import ViewSurface
from govoplan_core.db.base import Base from govoplan_core.db.base import Base
from govoplan_organizations.backend.db import models as organization_models # noqa: F401 - populate metadata from govoplan_organizations.backend.db import models as organization_models # noqa: F401 - populate metadata
@@ -86,7 +87,7 @@ def _organization_directory(context: ModuleContext) -> object:
manifest = ModuleManifest( manifest = ModuleManifest(
id="organizations", id="organizations",
name="Organizations", name="Organizations",
version="0.1.6", version="0.1.8",
required_capabilities=(CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR), required_capabilities=(CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR),
optional_dependencies=("tenancy", "access", "audit", "policy"), optional_dependencies=("tenancy", "access", "audit", "policy"),
permissions=PERMISSIONS, permissions=PERMISSIONS,
@@ -98,6 +99,15 @@ manifest = ModuleManifest(
package_name="@govoplan/organizations-webui", package_name="@govoplan/organizations-webui",
routes=(FrontendRoute(path="/organizations", component="OrganizationsPage", required_any=ORGANIZATIONS_READ_SCOPES, order=70),), routes=(FrontendRoute(path="/organizations", component="OrganizationsPage", required_any=ORGANIZATIONS_READ_SCOPES, order=70),),
nav_items=(NavItem(path="/organizations", label="Organizations", icon="users", required_any=ORGANIZATIONS_READ_SCOPES, order=70),), nav_items=(NavItem(path="/organizations", label="Organizations", icon="users", required_any=ORGANIZATIONS_READ_SCOPES, order=70),),
view_surfaces=(
ViewSurface(
id="organizations.admin.tenant",
module_id="organizations",
kind="section",
label="Organizations administration",
order=85,
),
),
), ),
migration_spec=MigrationSpec( migration_spec=MigrationSpec(
module_id="organizations", module_id="organizations",

View File

@@ -0,0 +1 @@
"""Organizations Alembic revisions."""

View File

@@ -0,0 +1,187 @@
"""v0.1.7 organizations baseline
Revision ID: 6d7e8f9a0b1c
Revises: None
Create Date: 2026-07-11 00:00:00.000000
"""
from __future__ import annotations
from alembic import op
import sqlalchemy as sa
revision = '6d7e8f9a0b1c'
down_revision = None
branch_labels = None
depends_on = '4f2a9c8e7b6d'
def upgrade() -> None:
op.create_table('organizations_structures',
sa.Column('id', sa.String(length=36), nullable=False),
sa.Column('tenant_id', sa.String(length=36), nullable=False),
sa.Column('slug', sa.String(length=100), nullable=False),
sa.Column('name', sa.String(length=255), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('structure_kind', sa.String(length=30), nullable=False),
sa.Column('is_active', sa.Boolean(), nullable=False),
sa.Column('settings', sa.JSON(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
sa.PrimaryKeyConstraint('id', name=op.f('pk_organizations_structures')),
sa.UniqueConstraint('tenant_id', 'slug', name='uq_organizations_structures_tenant_slug')
)
op.create_index(op.f('ix_organizations_structures_tenant_id'), 'organizations_structures', ['tenant_id'], unique=False)
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),
sa.Column('settings', sa.JSON(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
sa.PrimaryKeyConstraint('id', name=op.f('pk_organizations_tenant_settings')),
sa.UniqueConstraint('tenant_id', name='uq_organizations_tenant_settings_tenant')
)
op.create_index(op.f('ix_organizations_tenant_settings_tenant_id'), 'organizations_tenant_settings', ['tenant_id'], unique=False)
op.create_table('organizations_unit_types',
sa.Column('id', sa.String(length=36), nullable=False),
sa.Column('tenant_id', sa.String(length=36), nullable=False),
sa.Column('slug', sa.String(length=100), nullable=False),
sa.Column('name', sa.String(length=255), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=False),
sa.Column('settings', sa.JSON(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
sa.PrimaryKeyConstraint('id', name=op.f('pk_organizations_unit_types')),
sa.UniqueConstraint('tenant_id', 'slug', name='uq_organizations_unit_types_tenant_slug')
)
op.create_index(op.f('ix_organizations_unit_types_tenant_id'), 'organizations_unit_types', ['tenant_id'], unique=False)
op.create_table('organizations_function_types',
sa.Column('id', sa.String(length=36), nullable=False),
sa.Column('tenant_id', sa.String(length=36), nullable=False),
sa.Column('slug', sa.String(length=100), nullable=False),
sa.Column('name', sa.String(length=255), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('organization_unit_type_id', sa.String(length=36), nullable=True),
sa.Column('delegable', sa.Boolean(), nullable=False),
sa.Column('act_in_place_allowed', sa.Boolean(), nullable=False),
sa.Column('is_active', sa.Boolean(), nullable=False),
sa.Column('settings', sa.JSON(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(['organization_unit_type_id'], ['organizations_unit_types.id'], name=op.f('fk_organizations_function_types_organization_unit_type_id_organizations_unit_types'), ondelete='SET NULL'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_organizations_function_types')),
sa.UniqueConstraint('tenant_id', 'slug', name='uq_organizations_function_types_tenant_slug')
)
op.create_index(op.f('ix_organizations_function_types_organization_unit_type_id'), 'organizations_function_types', ['organization_unit_type_id'], unique=False)
op.create_index(op.f('ix_organizations_function_types_tenant_id'), 'organizations_function_types', ['tenant_id'], unique=False)
op.create_table('organizations_relation_types',
sa.Column('id', sa.String(length=36), nullable=False),
sa.Column('tenant_id', sa.String(length=36), nullable=False),
sa.Column('structure_id', sa.String(length=36), nullable=True),
sa.Column('slug', sa.String(length=100), nullable=False),
sa.Column('name', sa.String(length=255), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('source_unit_type_id', sa.String(length=36), nullable=True),
sa.Column('target_unit_type_id', sa.String(length=36), nullable=True),
sa.Column('is_hierarchical', sa.Boolean(), nullable=False),
sa.Column('allow_cycles', sa.Boolean(), nullable=False),
sa.Column('is_active', sa.Boolean(), nullable=False),
sa.Column('settings', sa.JSON(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(['source_unit_type_id'], ['organizations_unit_types.id'], name=op.f('fk_organizations_relation_types_source_unit_type_id_organizations_unit_types'), ondelete='SET NULL'),
sa.ForeignKeyConstraint(['structure_id'], ['organizations_structures.id'], name=op.f('fk_organizations_relation_types_structure_id_organizations_structures'), ondelete='SET NULL'),
sa.ForeignKeyConstraint(['target_unit_type_id'], ['organizations_unit_types.id'], name=op.f('fk_organizations_relation_types_target_unit_type_id_organizations_unit_types'), ondelete='SET NULL'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_organizations_relation_types')),
sa.UniqueConstraint('tenant_id', 'slug', name='uq_organizations_relation_types_tenant_slug')
)
op.create_index(op.f('ix_organizations_relation_types_source_unit_type_id'), 'organizations_relation_types', ['source_unit_type_id'], unique=False)
op.create_index('ix_organizations_relation_types_structure', 'organizations_relation_types', ['tenant_id', 'structure_id'], unique=False)
op.create_index(op.f('ix_organizations_relation_types_structure_id'), 'organizations_relation_types', ['structure_id'], unique=False)
op.create_index(op.f('ix_organizations_relation_types_target_unit_type_id'), 'organizations_relation_types', ['target_unit_type_id'], unique=False)
op.create_index(op.f('ix_organizations_relation_types_tenant_id'), 'organizations_relation_types', ['tenant_id'], unique=False)
op.create_table('organizations_units',
sa.Column('id', sa.String(length=36), nullable=False),
sa.Column('tenant_id', sa.String(length=36), nullable=False),
sa.Column('unit_type_id', sa.String(length=36), nullable=True),
sa.Column('parent_id', sa.String(length=36), nullable=True),
sa.Column('slug', sa.String(length=100), nullable=False),
sa.Column('name', sa.String(length=255), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=False),
sa.Column('settings', sa.JSON(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(['parent_id'], ['organizations_units.id'], name=op.f('fk_organizations_units_parent_id_organizations_units'), ondelete='SET NULL'),
sa.ForeignKeyConstraint(['unit_type_id'], ['organizations_unit_types.id'], name=op.f('fk_organizations_units_unit_type_id_organizations_unit_types'), ondelete='SET NULL'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_organizations_units')),
sa.UniqueConstraint('tenant_id', 'slug', name='uq_organizations_units_tenant_slug')
)
op.create_index(op.f('ix_organizations_units_parent_id'), 'organizations_units', ['parent_id'], unique=False)
op.create_index(op.f('ix_organizations_units_tenant_id'), 'organizations_units', ['tenant_id'], unique=False)
op.create_index(op.f('ix_organizations_units_unit_type_id'), 'organizations_units', ['unit_type_id'], unique=False)
op.create_table('organizations_functions',
sa.Column('id', sa.String(length=36), nullable=False),
sa.Column('tenant_id', sa.String(length=36), nullable=False),
sa.Column('function_type_id', sa.String(length=36), nullable=True),
sa.Column('organization_unit_id', sa.String(length=36), nullable=False),
sa.Column('slug', sa.String(length=100), nullable=False),
sa.Column('name', sa.String(length=255), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('delegable', sa.Boolean(), nullable=False),
sa.Column('act_in_place_allowed', sa.Boolean(), nullable=False),
sa.Column('is_active', sa.Boolean(), nullable=False),
sa.Column('settings', sa.JSON(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(['function_type_id'], ['organizations_function_types.id'], name=op.f('fk_organizations_functions_function_type_id_organizations_function_types'), ondelete='SET NULL'),
sa.ForeignKeyConstraint(['organization_unit_id'], ['organizations_units.id'], name=op.f('fk_organizations_functions_organization_unit_id_organizations_units'), ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_organizations_functions')),
sa.UniqueConstraint('tenant_id', 'organization_unit_id', 'slug', name='uq_organizations_functions_tenant_unit_slug')
)
op.create_index(op.f('ix_organizations_functions_function_type_id'), 'organizations_functions', ['function_type_id'], unique=False)
op.create_index(op.f('ix_organizations_functions_organization_unit_id'), 'organizations_functions', ['organization_unit_id'], unique=False)
op.create_index(op.f('ix_organizations_functions_tenant_id'), 'organizations_functions', ['tenant_id'], unique=False)
op.create_table('organizations_relations',
sa.Column('id', sa.String(length=36), nullable=False),
sa.Column('tenant_id', sa.String(length=36), nullable=False),
sa.Column('structure_id', sa.String(length=36), nullable=False),
sa.Column('relation_type_id', sa.String(length=36), nullable=False),
sa.Column('source_unit_id', sa.String(length=36), nullable=False),
sa.Column('target_unit_id', sa.String(length=36), nullable=False),
sa.Column('valid_from', sa.DateTime(timezone=True), nullable=True),
sa.Column('valid_until', sa.DateTime(timezone=True), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=False),
sa.Column('settings', sa.JSON(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(['relation_type_id'], ['organizations_relation_types.id'], name=op.f('fk_organizations_relations_relation_type_id_organizations_relation_types'), ondelete='CASCADE'),
sa.ForeignKeyConstraint(['source_unit_id'], ['organizations_units.id'], name=op.f('fk_organizations_relations_source_unit_id_organizations_units'), ondelete='CASCADE'),
sa.ForeignKeyConstraint(['structure_id'], ['organizations_structures.id'], name=op.f('fk_organizations_relations_structure_id_organizations_structures'), ondelete='CASCADE'),
sa.ForeignKeyConstraint(['target_unit_id'], ['organizations_units.id'], name=op.f('fk_organizations_relations_target_unit_id_organizations_units'), ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_organizations_relations')),
sa.UniqueConstraint('tenant_id', 'structure_id', 'relation_type_id', 'source_unit_id', 'target_unit_id', name='uq_organizations_relations_edge')
)
op.create_index(op.f('ix_organizations_relations_relation_type_id'), 'organizations_relations', ['relation_type_id'], unique=False)
op.create_index(op.f('ix_organizations_relations_source_unit_id'), 'organizations_relations', ['source_unit_id'], unique=False)
op.create_index(op.f('ix_organizations_relations_structure_id'), 'organizations_relations', ['structure_id'], unique=False)
op.create_index('ix_organizations_relations_structure_source', 'organizations_relations', ['tenant_id', 'structure_id', 'source_unit_id'], unique=False)
op.create_index('ix_organizations_relations_structure_target', 'organizations_relations', ['tenant_id', 'structure_id', 'target_unit_id'], unique=False)
op.create_index(op.f('ix_organizations_relations_target_unit_id'), 'organizations_relations', ['target_unit_id'], unique=False)
op.create_index(op.f('ix_organizations_relations_tenant_id'), 'organizations_relations', ['tenant_id'], unique=False)
def downgrade() -> None:
op.drop_table('organizations_relations')
op.drop_table('organizations_functions')
op.drop_table('organizations_units')
op.drop_table('organizations_relation_types')
op.drop_table('organizations_function_types')
op.drop_table('organizations_unit_types')
op.drop_table('organizations_tenant_settings')
op.drop_table('organizations_structures')

View File

@@ -1,11 +1,14 @@
{ {
"name": "@govoplan/organizations-webui", "name": "@govoplan/organizations-webui",
"version": "0.1.6", "version": "0.1.8",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"module": "src/index.ts", "module": "src/index.ts",
"types": "src/index.ts", "types": "src/index.ts",
"scripts": {
"test:organizations-tree": "node scripts/test-organizations-tree-structure.mjs"
},
"exports": { "exports": {
".": { ".": {
"types": "./src/index.ts", "types": "./src/index.ts",
@@ -14,7 +17,7 @@
"./styles/organizations.css": "./src/styles/organizations.css" "./styles/organizations.css": "./src/styles/organizations.css"
}, },
"peerDependencies": { "peerDependencies": {
"@govoplan/core-webui": "^0.1.6", "@govoplan/core-webui": "^0.1.9",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
"lucide-react": "^1.23.0", "lucide-react": "^1.23.0",
"react": "^19.0.0", "react": "^19.0.0",

View File

@@ -0,0 +1,24 @@
import { readFileSync } from "node:fs";
function assert(condition, message) {
if (!condition) throw new Error(message);
}
const source = readFileSync(
new URL("../src/features/organizations/OrganizationsPage.tsx", import.meta.url),
"utf8"
);
const styles = readFileSync(
new URL("../src/styles/organizations.css", import.meta.url),
"utf8"
);
assert(source.includes("ExplorerTree,"), "Organizations imports the central ExplorerTree");
assert(source.includes("<ExplorerTree"), "the organization hierarchy uses ExplorerTree");
assert(source.includes("collapsible={false}"), "the always-expanded hierarchy uses the non-collapsible contract");
assert(source.includes("renderNodeActions="), "per-unit controls use the sibling node-action slot");
assert(source.includes("<IconButton"), "per-unit controls use the central icon-only action primitive");
assert(!source.includes("renderUnitTreeNodes"), "the custom recursive renderer was removed");
assert(!source.includes("organizations-tree-row"), "the custom tree row was removed");
assert(!source.includes("organizations-tree-node"), "the custom tree node was removed");
assert(!styles.includes("organizations-tree-"), "Organizations no longer owns shared tree styling");

View File

@@ -121,7 +121,7 @@ export default function OrganizationsAdminPanel({ settings, auth }: { settings:
</Card> </Card>
<Card title="i18n:govoplan-organizations.audit_and_retention.3ba1d2fc"> <Card title="i18n:govoplan-organizations.audit_and_retention.3ba1d2fc">
<div className="organizations-form-grid"> <div className="admin-form-grid two-columns">
<FormField label="i18n:govoplan-organizations.audit_detail_level.7397355d"> <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 })}> <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>)} {AUDIT_DETAIL_LEVELS.map((item) => <option key={item.value} value={item.value}>{item.label}</option>)}

File diff suppressed because it is too large Load Diff

View File

@@ -24,6 +24,7 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-organizations.allow_tenant_model_customization.2425d751": "Tenant admins may customize the organization meta-model", "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.change_retention_days.71bbd140": "Retain organization change detail for days",
"i18n:govoplan-organizations.change_request_id.a6e7fd6b": "Change request ID", "i18n:govoplan-organizations.change_request_id.a6e7fd6b": "Change request ID",
"i18n:govoplan-organizations.change_request_id_help.0c119a5d": "Only required when organization model governance requires an approved recorded change request.",
"i18n:govoplan-organizations.cancel_edit.309c2a6f": "Cancel edit", "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",
@@ -66,9 +67,9 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-organizations.none.334c4a4c": "None", "i18n:govoplan-organizations.none.334c4a4c": "None",
"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.c9ab9829": "Organizations",
"i18n:govoplan-organizations.organization_settings_description.35dc9f10": "Configure tenant-level governance, audit, and retention behavior for organization changes.", "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_settings_saved.bfbcfdfa": "Organizations saved.",
"i18n:govoplan-organizations.organization_tree.e5bfb195": "Organization tree", "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, and functions.", "i18n:govoplan-organizations.organizations_intro.4e67c4bb": "Model organization types, concrete units, structures, and functions.",
@@ -146,6 +147,7 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-organizations.allow_tenant_model_customization.2425d751": "Mandantenadmins dürfen das Organisations-Metamodell anpassen", "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.change_retention_days.71bbd140": "Änderungsdetails zur Organisation in Tagen aufbewahren",
"i18n:govoplan-organizations.change_request_id.a6e7fd6b": "Änderungsantrags-ID", "i18n:govoplan-organizations.change_request_id.a6e7fd6b": "Änderungsantrags-ID",
"i18n:govoplan-organizations.change_request_id_help.0c119a5d": "Nur erforderlich, wenn die Organisationsmodell-Governance einen genehmigten erfassten Änderungsantrag verlangt.",
"i18n:govoplan-organizations.cancel_edit.309c2a6f": "Bearbeitung abbrechen", "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",
@@ -188,9 +190,9 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-organizations.none.334c4a4c": "Keine", "i18n:govoplan-organizations.none.334c4a4c": "Keine",
"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.c9ab9829": "Organisationen",
"i18n:govoplan-organizations.organization_settings_description.35dc9f10": "Konfiguriere Governance, Audit und Aufbewahrung für Organisationsänderungen auf Mandantenebene.", "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_settings_saved.bfbcfdfa": "Organisationen gespeichert.",
"i18n:govoplan-organizations.organization_tree.e5bfb195": "Organisationsbaum", "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 und Funktionen.", "i18n:govoplan-organizations.organizations_intro.4e67c4bb": "Modelliere Organisationstypen, konkrete Einheiten, Strukturen und Funktionen.",

View File

@@ -23,7 +23,8 @@ const organizationAdminSections: AdminSectionsUiCapability = {
sections: [ sections: [
{ {
id: "tenant-organization-settings", id: "tenant-organization-settings",
label: "i18n:govoplan-organizations.organization_settings.c9ab9829", surfaceId: "organizations.admin.tenant",
label: "i18n:govoplan-organizations.organizations.220edf64",
group: "TENANT", group: "TENANT",
order: 85, order: 85,
anyOf: ["organizations:settings:read", "admin:settings:read"], anyOf: ["organizations:settings:read", "admin:settings:read"],
@@ -45,11 +46,20 @@ export const organizationsModule: PlatformWebModule = {
dependencies: ["access"], dependencies: ["access"],
optionalDependencies: ["admin"], optionalDependencies: ["admin"],
translations, translations,
viewSurfaces: [
{
id: "organizations.admin.tenant",
moduleId: "organizations",
kind: "section",
label: "Organizations administration",
order: 85
}
],
navItems: [ navItems: [
{ {
to: "/organizations", to: "/organizations",
label: "i18n:govoplan-organizations.organizations.220edf64", label: "i18n:govoplan-organizations.organizations.220edf64",
iconName: "users", iconName: "building-2",
anyOf: organizationReadScopes, anyOf: organizationReadScopes,
order: 70 order: 70
} }

View File

@@ -1,13 +1,12 @@
.organizations-workspace { .organizations-workspace {
grid-template-columns: 230px minmax(0, 1fr); grid-template-columns: 230px minmax(0, 1fr);
background: var(--bg, #f8f7f4); background: var(--bg);
} }
.organizations-page { .organizations-page {
display: grid; display: grid;
gap: 18px; gap: 18px;
width: 100%; width: 100%;
max-width: 1480px;
} }
.organizations-admin-page { .organizations-admin-page {
@@ -26,137 +25,29 @@
.organizations-toolbar { .organizations-toolbar {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: flex-end;
gap: 12px; gap: 12px;
flex-wrap: wrap; flex-wrap: wrap;
} }
.organizations-change-request {
display: inline-grid;
gap: 4px;
min-width: 220px;
color: var(--muted);
font-size: 12px;
font-weight: 600;
}
.organizations-change-request input {
min-height: 36px;
}
.organizations-section-grid {
display: grid;
grid-template-columns: minmax(280px, 360px) minmax(0, 1fr);
gap: 18px;
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;
}
.organizations-form-panel .card-body,
.organizations-table-panel .card-body {
min-width: 0;
}
.organizations-form-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
.organizations-form-grid .wide {
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-contributed-action {
display: inline-flex;
}
.organizations-check-list { .organizations-check-list {
display: grid; display: grid;
gap: 10px; gap: 10px;
grid-column: 1 / -1; grid-column: 1 / -1;
} }
.organizations-check-list label,
.organizations-inline-check {
display: inline-flex;
align-items: center;
gap: 8px;
color: var(--text);
font-weight: 600;
}
.organizations-table-stack { .organizations-table-stack {
display: grid; display: grid;
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%; 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-table-stack > .card {
.organizations-tree-node.active { width: 100%;
border-color: var(--line);
background: var(--surface-muted, #f6f7f9);
} }
.organizations-tree-node span { .organizations-dialog-change-request {
color: var(--muted); padding-top: 2px;
font-size: 12px;
} }
.organizations-field-note { .organizations-field-note {
@@ -181,19 +72,8 @@
color: var(--muted); color: var(--muted);
} }
@media (max-width: 1100px) {
.organizations-section-grid,
.organizations-editor-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 900px) { @media (max-width: 900px) {
.organizations-workspace { .organizations-workspace {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.organizations-form-grid {
grid-template-columns: 1fr;
}
} }