Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 35aebe8759 | |||
| 28f799e426 | |||
| df91c70491 | |||
| 81e532fd54 | |||
| 025067eb87 | |||
| 6b0b2d2d0b | |||
| f09fdf2ef7 | |||
| c240778ad2 | |||
| 45cda1a33f | |||
| 39c081c4fb | |||
| 213157aca1 | |||
| 2810b1c500 | |||
| f1ae795917 |
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
"""GovOPlaN organizations module."""
|
"""GovOPlaN organizations module."""
|
||||||
|
|
||||||
__version__ = "0.1.6"
|
__version__ = "0.1.8"
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Organizations Alembic revisions."""
|
||||||
@@ -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')
|
||||||
@@ -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",
|
||||||
|
|||||||
24
webui/scripts/test-organizations-tree-structure.mjs
Normal file
24
webui/scripts/test-organizations-tree-structure.mjs
Normal 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");
|
||||||
@@ -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>)}
|
||||||
|
|||||||
@@ -1,24 +1,32 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState, type FormEvent } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState, type FormEvent } from "react";
|
||||||
import { Edit3, Plus, RefreshCw, Save, XCircle } from "lucide-react";
|
import { Edit3, Plus, RefreshCw } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
|
AdminIconButton,
|
||||||
ApiError,
|
ApiError,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
DataGrid,
|
DataGrid,
|
||||||
|
Dialog,
|
||||||
DismissibleAlert,
|
DismissibleAlert,
|
||||||
|
ExplorerTree,
|
||||||
FormField,
|
FormField,
|
||||||
|
IconButton,
|
||||||
LoadingFrame,
|
LoadingFrame,
|
||||||
ModuleSubnav,
|
ModuleSubnav,
|
||||||
PageTitle,
|
PageTitle,
|
||||||
StatusBadge,
|
StatusBadge,
|
||||||
|
TableActionGroup,
|
||||||
|
ToggleSwitch,
|
||||||
hasScope,
|
hasScope,
|
||||||
|
isViewSurfaceVisible,
|
||||||
|
useEffectiveView,
|
||||||
useUnsavedDraftGuard,
|
useUnsavedDraftGuard,
|
||||||
usePlatformUiCapabilities,
|
usePlatformUiCapabilities,
|
||||||
|
useViewSurfaces,
|
||||||
type ApiSettings,
|
type ApiSettings,
|
||||||
type AuthInfo,
|
type AuthInfo,
|
||||||
type DataGridColumn,
|
type DataGridColumn,
|
||||||
type ModuleSubnavGroup,
|
type ModuleSubnavGroup,
|
||||||
type OrganizationFunctionActionContribution,
|
|
||||||
type OrganizationFunctionActionsUiCapability
|
type OrganizationFunctionActionsUiCapability
|
||||||
} from "@govoplan/core-webui";
|
} from "@govoplan/core-webui";
|
||||||
import {
|
import {
|
||||||
@@ -57,6 +65,7 @@ import {
|
|||||||
|
|
||||||
export type OrganizationSection = "model" | "units" | "relations" | "functions";
|
export type OrganizationSection = "model" | "units" | "relations" | "functions";
|
||||||
type OrganizationsPageMode = "workspace" | "admin";
|
type OrganizationsPageMode = "workspace" | "admin";
|
||||||
|
type OrganizationEditor = "unitType" | "structure" | "relationType" | "unit" | "relation" | "functionType" | "function";
|
||||||
|
|
||||||
type OrganizationsPageProps = {
|
type OrganizationsPageProps = {
|
||||||
settings: ApiSettings;
|
settings: ApiSettings;
|
||||||
@@ -113,11 +122,26 @@ type FunctionDraft = SluggedDraft & {
|
|||||||
act_in_place_allowed: boolean;
|
act_in_place_allowed: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ActiveItem = {
|
type OrganizationsInitialQuery = {
|
||||||
id: string;
|
section: OrganizationSection | "";
|
||||||
is_active: boolean;
|
unitId: string;
|
||||||
|
functionId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function organizationSectionFromQuery(value: string | null): OrganizationSection | "" {
|
||||||
|
return value === "model" || value === "units" || value === "relations" || value === "functions" ? value : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function organizationsInitialQuery(): OrganizationsInitialQuery {
|
||||||
|
if (typeof window === "undefined") return { section: "", unitId: "", functionId: "" };
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
return {
|
||||||
|
section: organizationSectionFromQuery(params.get("section")),
|
||||||
|
unitId: params.get("unit_id") || "",
|
||||||
|
functionId: params.get("function_id") || ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const EMPTY_MODEL: OrganizationModel = {
|
const EMPTY_MODEL: OrganizationModel = {
|
||||||
unit_types: [],
|
unit_types: [],
|
||||||
structures: [],
|
structures: [],
|
||||||
@@ -275,27 +299,6 @@ function activeStatus(active: boolean): JSX.Element {
|
|||||||
return <StatusBadge status={active ? "success" : "inactive"} label={active ? "i18n:govoplan-organizations.active.a733b809" : "i18n:govoplan-organizations.inactive.09af574c"} />;
|
return <StatusBadge status={active ? "success" : "inactive"} label={active ? "i18n:govoplan-organizations.active.a733b809" : "i18n:govoplan-organizations.inactive.09af574c"} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ActiveToggleButton({ item, disabled, onToggle }: { item: ActiveItem; disabled: boolean; onToggle: () => void }) {
|
|
||||||
const label = item.is_active ? "i18n:govoplan-organizations.deactivate.46ab070d" : "i18n:govoplan-organizations.reactivate.58f2855d";
|
|
||||||
return (
|
|
||||||
<Button type="button" variant={item.is_active ? "secondary" : "primary"} disabled={disabled} onClick={onToggle} title={label}>
|
|
||||||
{label}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function RowActions({ item, disabled, onEdit, onToggle, extra }: { item: ActiveItem; disabled: boolean; onEdit: () => void; onToggle: () => void; extra?: JSX.Element[] }) {
|
|
||||||
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} />
|
|
||||||
{extra}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SluggedFields({
|
function SluggedFields({
|
||||||
draft,
|
draft,
|
||||||
onChange,
|
onChange,
|
||||||
@@ -318,10 +321,14 @@ function SluggedFields({
|
|||||||
<textarea rows={3} value={draft.description} disabled={disabled} onChange={(event) => onChange({ ...draft, description: event.target.value })} />
|
<textarea rows={3} value={draft.description} disabled={disabled} onChange={(event) => onChange({ ...draft, description: event.target.value })} />
|
||||||
</FormField>
|
</FormField>
|
||||||
</div>
|
</div>
|
||||||
<label className="organizations-inline-check wide">
|
<div className="wide">
|
||||||
<input type="checkbox" checked={draft.is_active} disabled={disabled} onChange={(event) => onChange({ ...draft, is_active: event.target.checked })} />
|
<ToggleSwitch
|
||||||
<span>i18n:govoplan-organizations.active.a733b809</span>
|
checked={draft.is_active}
|
||||||
</label>
|
disabled={disabled}
|
||||||
|
onChange={(checked) => onChange({ ...draft, is_active: checked })}
|
||||||
|
label="i18n:govoplan-organizations.active.a733b809"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -345,7 +352,10 @@ export default function OrganizationsPage({
|
|||||||
}: OrganizationsPageProps) {
|
}: OrganizationsPageProps) {
|
||||||
const sectionSet = useMemo(() => new Set(availableSections.length ? availableSections : ALL_SECTIONS), [availableSections]);
|
const sectionSet = useMemo(() => new Set(availableSections.length ? availableSections : ALL_SECTIONS), [availableSections]);
|
||||||
const firstAvailableSection = availableSections.find((section) => sectionSet.has(section)) ?? "model";
|
const firstAvailableSection = availableSections.find((section) => sectionSet.has(section)) ?? "model";
|
||||||
const [active, setActive] = useState<OrganizationSection>(sectionSet.has(initialSection) ? initialSection : firstAvailableSection);
|
const initialQuery = useMemo(() => organizationsInitialQuery(), []);
|
||||||
|
const requestedInitialSection = initialQuery.section && sectionSet.has(initialQuery.section) ? initialQuery.section : initialSection;
|
||||||
|
const [active, setActive] = useState<OrganizationSection>(sectionSet.has(requestedInitialSection) ? requestedInitialSection : firstAvailableSection);
|
||||||
|
const appliedInitialQueryRef = useRef(false);
|
||||||
const visibleSectionGroups = useMemo(() => sectionGroupsFor(sectionSet), [sectionSet]);
|
const visibleSectionGroups = useMemo(() => sectionGroupsFor(sectionSet), [sectionSet]);
|
||||||
const [model, setModel] = useState<OrganizationModel>(EMPTY_MODEL);
|
const [model, setModel] = useState<OrganizationModel>(EMPTY_MODEL);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -367,9 +377,12 @@ export default function OrganizationsPage({
|
|||||||
const [editingRelationId, setEditingRelationId] = useState<string | null>(null);
|
const [editingRelationId, setEditingRelationId] = useState<string | null>(null);
|
||||||
const [editingFunctionTypeId, setEditingFunctionTypeId] = useState<string | null>(null);
|
const [editingFunctionTypeId, setEditingFunctionTypeId] = useState<string | null>(null);
|
||||||
const [editingFunctionId, setEditingFunctionId] = useState<string | null>(null);
|
const [editingFunctionId, setEditingFunctionId] = useState<string | null>(null);
|
||||||
|
const [activeEditor, setActiveEditor] = useState<OrganizationEditor | null>(null);
|
||||||
const [selectedUnitId, setSelectedUnitId] = useState("");
|
const [selectedUnitId, setSelectedUnitId] = useState("");
|
||||||
const [changeRequestId, setChangeRequestId] = useState("");
|
const [changeRequestId, setChangeRequestId] = useState("");
|
||||||
const functionActionCapabilities = usePlatformUiCapabilities<OrganizationFunctionActionsUiCapability>("organizations.functionActions");
|
const functionActionCapabilities = usePlatformUiCapabilities<OrganizationFunctionActionsUiCapability>("organizations.functionActions");
|
||||||
|
const effectiveView = useEffectiveView();
|
||||||
|
const viewSurfaces = useViewSurfaces();
|
||||||
|
|
||||||
const canWriteModel = hasScope(auth, "organizations:model:write");
|
const canWriteModel = hasScope(auth, "organizations:model:write");
|
||||||
const canWriteUnits = hasScope(auth, "organizations:unit:write");
|
const canWriteUnits = hasScope(auth, "organizations:unit:write");
|
||||||
@@ -385,8 +398,11 @@ export default function OrganizationsPage({
|
|||||||
() => functionActionCapabilities
|
() => functionActionCapabilities
|
||||||
.flatMap((capability) => capability.actions)
|
.flatMap((capability) => capability.actions)
|
||||||
.filter((contribution) => contributionVisible(auth, contribution))
|
.filter((contribution) => contributionVisible(auth, contribution))
|
||||||
|
.filter((contribution) =>
|
||||||
|
isViewSurfaceVisible(effectiveView, contribution.surfaceId, viewSurfaces)
|
||||||
|
)
|
||||||
.sort((left, right) => (left.order ?? 100) - (right.order ?? 100)),
|
.sort((left, right) => (left.order ?? 100) - (right.order ?? 100)),
|
||||||
[auth, functionActionCapabilities]
|
[auth, effectiveView, functionActionCapabilities, viewSurfaces]
|
||||||
);
|
);
|
||||||
const unitsByParentId = useMemo(() => {
|
const unitsByParentId = useMemo(() => {
|
||||||
const mapped = new Map<string, OrganizationUnitItem[]>();
|
const mapped = new Map<string, OrganizationUnitItem[]>();
|
||||||
@@ -467,6 +483,8 @@ export default function OrganizationsPage({
|
|||||||
setEditingRelationId(null);
|
setEditingRelationId(null);
|
||||||
setEditingFunctionTypeId(null);
|
setEditingFunctionTypeId(null);
|
||||||
setEditingFunctionId(null);
|
setEditingFunctionId(null);
|
||||||
|
setActiveEditor(null);
|
||||||
|
setChangeRequestId("");
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
function rejectMissingName(): Promise<boolean> {
|
function rejectMissingName(): Promise<boolean> {
|
||||||
@@ -490,6 +508,8 @@ export default function OrganizationsPage({
|
|||||||
if (ok) {
|
if (ok) {
|
||||||
setUnitTypeDraft(emptySluggedDraft());
|
setUnitTypeDraft(emptySluggedDraft());
|
||||||
setEditingUnitTypeId(null);
|
setEditingUnitTypeId(null);
|
||||||
|
setActiveEditor(null);
|
||||||
|
setChangeRequestId("");
|
||||||
}
|
}
|
||||||
return ok;
|
return ok;
|
||||||
}, [canWriteModel, editingUnitTypeId, runAction, settings, unitTypeDraft, withChangeRequest]);
|
}, [canWriteModel, editingUnitTypeId, runAction, settings, unitTypeDraft, withChangeRequest]);
|
||||||
@@ -507,6 +527,8 @@ export default function OrganizationsPage({
|
|||||||
if (ok) {
|
if (ok) {
|
||||||
setStructureDraft(emptyStructureDraft());
|
setStructureDraft(emptyStructureDraft());
|
||||||
setEditingStructureId(null);
|
setEditingStructureId(null);
|
||||||
|
setActiveEditor(null);
|
||||||
|
setChangeRequestId("");
|
||||||
}
|
}
|
||||||
return ok;
|
return ok;
|
||||||
}, [canWriteModel, editingStructureId, runAction, settings, structureDraft, withChangeRequest]);
|
}, [canWriteModel, editingStructureId, runAction, settings, structureDraft, withChangeRequest]);
|
||||||
@@ -538,6 +560,8 @@ export default function OrganizationsPage({
|
|||||||
if (ok) {
|
if (ok) {
|
||||||
setRelationTypeDraft(emptyRelationTypeDraft());
|
setRelationTypeDraft(emptyRelationTypeDraft());
|
||||||
setEditingRelationTypeId(null);
|
setEditingRelationTypeId(null);
|
||||||
|
setActiveEditor(null);
|
||||||
|
setChangeRequestId("");
|
||||||
}
|
}
|
||||||
return ok;
|
return ok;
|
||||||
}, [canWriteModel, editingRelationTypeId, relationTypeDraft, runAction, settings, withChangeRequest]);
|
}, [canWriteModel, editingRelationTypeId, relationTypeDraft, runAction, settings, withChangeRequest]);
|
||||||
@@ -563,6 +587,8 @@ export default function OrganizationsPage({
|
|||||||
if (ok) {
|
if (ok) {
|
||||||
setUnitDraft(emptyUnitDraft());
|
setUnitDraft(emptyUnitDraft());
|
||||||
setEditingUnitId(null);
|
setEditingUnitId(null);
|
||||||
|
setActiveEditor(null);
|
||||||
|
setChangeRequestId("");
|
||||||
}
|
}
|
||||||
return ok;
|
return ok;
|
||||||
}, [canWriteUnits, editingUnitId, runAction, settings, unitDraft, withChangeRequest]);
|
}, [canWriteUnits, editingUnitId, runAction, settings, unitDraft, withChangeRequest]);
|
||||||
@@ -583,6 +609,8 @@ export default function OrganizationsPage({
|
|||||||
if (ok) {
|
if (ok) {
|
||||||
setRelationDraft(emptyRelationDraft());
|
setRelationDraft(emptyRelationDraft());
|
||||||
setEditingRelationId(null);
|
setEditingRelationId(null);
|
||||||
|
setActiveEditor(null);
|
||||||
|
setChangeRequestId("");
|
||||||
}
|
}
|
||||||
return ok;
|
return ok;
|
||||||
}, [canWriteUnits, editingRelationId, relationDraft, runAction, settings, withChangeRequest]);
|
}, [canWriteUnits, editingRelationId, relationDraft, runAction, settings, withChangeRequest]);
|
||||||
@@ -610,6 +638,8 @@ export default function OrganizationsPage({
|
|||||||
if (ok) {
|
if (ok) {
|
||||||
setFunctionTypeDraft(emptyFunctionTypeDraft());
|
setFunctionTypeDraft(emptyFunctionTypeDraft());
|
||||||
setEditingFunctionTypeId(null);
|
setEditingFunctionTypeId(null);
|
||||||
|
setActiveEditor(null);
|
||||||
|
setChangeRequestId("");
|
||||||
}
|
}
|
||||||
return ok;
|
return ok;
|
||||||
}, [canWriteModel, editingFunctionTypeId, functionTypeDraft, runAction, settings, withChangeRequest]);
|
}, [canWriteModel, editingFunctionTypeId, functionTypeDraft, runAction, settings, withChangeRequest]);
|
||||||
@@ -643,6 +673,8 @@ export default function OrganizationsPage({
|
|||||||
if (ok) {
|
if (ok) {
|
||||||
setFunctionDraft(emptyFunctionDraft());
|
setFunctionDraft(emptyFunctionDraft());
|
||||||
setEditingFunctionId(null);
|
setEditingFunctionId(null);
|
||||||
|
setActiveEditor(null);
|
||||||
|
setChangeRequestId("");
|
||||||
}
|
}
|
||||||
return ok;
|
return ok;
|
||||||
}, [canWriteFunctions, editingFunctionId, functionDraft, runAction, settings, withChangeRequest]);
|
}, [canWriteFunctions, editingFunctionId, functionDraft, runAction, settings, withChangeRequest]);
|
||||||
@@ -681,44 +713,68 @@ export default function OrganizationsPage({
|
|||||||
message: "i18n:govoplan-core.this_page_has_unsaved_changes_save_them_before_l.419a9d8b"
|
message: "i18n:govoplan-core.this_page_has_unsaved_changes_save_them_before_l.419a9d8b"
|
||||||
});
|
});
|
||||||
|
|
||||||
const toggleUnitType = useCallback((item: OrganizationUnitTypeItem) => {
|
const openUnitTypeCreate = useCallback(() => {
|
||||||
void runAction(() => patchUnitType(settings, item.id, withChangeRequest({ is_active: !item.is_active })));
|
setEditingUnitTypeId(null);
|
||||||
}, [runAction, settings, withChangeRequest]);
|
setUnitTypeDraft(emptySluggedDraft());
|
||||||
|
setActive("model");
|
||||||
|
setActiveEditor("unitType");
|
||||||
|
}, []);
|
||||||
|
|
||||||
const toggleStructure = useCallback((item: OrganizationStructureItem) => {
|
const openStructureCreate = useCallback(() => {
|
||||||
void runAction(() => patchStructure(settings, item.id, withChangeRequest({ is_active: !item.is_active })));
|
setEditingStructureId(null);
|
||||||
}, [runAction, settings, withChangeRequest]);
|
setStructureDraft(emptyStructureDraft());
|
||||||
|
setActive("model");
|
||||||
|
setActiveEditor("structure");
|
||||||
|
}, []);
|
||||||
|
|
||||||
const toggleRelationType = useCallback((item: OrganizationRelationTypeItem) => {
|
const openRelationTypeCreate = useCallback(() => {
|
||||||
void runAction(() => patchRelationType(settings, item.id, withChangeRequest({ is_active: !item.is_active })));
|
setEditingRelationTypeId(null);
|
||||||
}, [runAction, settings, withChangeRequest]);
|
setRelationTypeDraft(emptyRelationTypeDraft());
|
||||||
|
setActive("model");
|
||||||
|
setActiveEditor("relationType");
|
||||||
|
}, []);
|
||||||
|
|
||||||
const toggleUnit = useCallback((item: OrganizationUnitItem) => {
|
const openUnitCreate = useCallback((parentId = "") => {
|
||||||
void runAction(() => patchUnit(settings, item.id, withChangeRequest({ is_active: !item.is_active })));
|
setEditingUnitId(null);
|
||||||
}, [runAction, settings, withChangeRequest]);
|
setUnitDraft({ ...emptyUnitDraft(), parent_id: parentId });
|
||||||
|
if (parentId) setSelectedUnitId(parentId);
|
||||||
|
setActive("units");
|
||||||
|
setActiveEditor("unit");
|
||||||
|
}, []);
|
||||||
|
|
||||||
const toggleRelation = useCallback((item: OrganizationRelationItem) => {
|
const openRelationCreate = useCallback(() => {
|
||||||
void runAction(() => patchRelation(settings, item.id, withChangeRequest({ is_active: !item.is_active })));
|
setEditingRelationId(null);
|
||||||
}, [runAction, settings, withChangeRequest]);
|
setRelationDraft(emptyRelationDraft());
|
||||||
|
setActive("relations");
|
||||||
|
setActiveEditor("relation");
|
||||||
|
}, []);
|
||||||
|
|
||||||
const toggleFunctionType = useCallback((item: OrganizationFunctionTypeItem) => {
|
const openFunctionTypeCreate = useCallback(() => {
|
||||||
void runAction(() => patchFunctionType(settings, item.id, withChangeRequest({ is_active: !item.is_active })));
|
setEditingFunctionTypeId(null);
|
||||||
}, [runAction, settings, withChangeRequest]);
|
setFunctionTypeDraft(emptyFunctionTypeDraft());
|
||||||
|
setActive("model");
|
||||||
|
setActiveEditor("functionType");
|
||||||
|
}, []);
|
||||||
|
|
||||||
const toggleFunction = useCallback((item: OrganizationFunctionItem) => {
|
const openFunctionCreate = useCallback((organizationUnitId = "") => {
|
||||||
void runAction(() => patchFunction(settings, item.id, withChangeRequest({ is_active: !item.is_active })));
|
setEditingFunctionId(null);
|
||||||
}, [runAction, settings, withChangeRequest]);
|
setFunctionDraft({ ...emptyFunctionDraft(), organization_unit_id: organizationUnitId || selectedUnitId });
|
||||||
|
setActive("functions");
|
||||||
|
setActiveEditor("function");
|
||||||
|
}, [selectedUnitId]);
|
||||||
|
|
||||||
const editUnitType = useCallback((item: OrganizationUnitTypeItem) => {
|
const editUnitType = useCallback((item: OrganizationUnitTypeItem) => {
|
||||||
setEditingUnitTypeId(item.id);
|
setEditingUnitTypeId(item.id);
|
||||||
setUnitTypeDraft(sluggedDraftFrom(item));
|
setUnitTypeDraft(sluggedDraftFrom(item));
|
||||||
setActive("model");
|
setActive("model");
|
||||||
|
setActiveEditor("unitType");
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const editStructure = useCallback((item: OrganizationStructureItem) => {
|
const editStructure = useCallback((item: OrganizationStructureItem) => {
|
||||||
setEditingStructureId(item.id);
|
setEditingStructureId(item.id);
|
||||||
setStructureDraft({ ...sluggedDraftFrom(item), structure_kind: item.structure_kind as OrganizationStructureKind });
|
setStructureDraft({ ...sluggedDraftFrom(item), structure_kind: item.structure_kind as OrganizationStructureKind });
|
||||||
setActive("model");
|
setActive("model");
|
||||||
|
setActiveEditor("structure");
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const editRelationType = useCallback((item: OrganizationRelationTypeItem) => {
|
const editRelationType = useCallback((item: OrganizationRelationTypeItem) => {
|
||||||
@@ -732,6 +788,7 @@ export default function OrganizationsPage({
|
|||||||
allow_cycles: item.allow_cycles
|
allow_cycles: item.allow_cycles
|
||||||
});
|
});
|
||||||
setActive("model");
|
setActive("model");
|
||||||
|
setActiveEditor("relationType");
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const editUnit = useCallback((item: OrganizationUnitItem) => {
|
const editUnit = useCallback((item: OrganizationUnitItem) => {
|
||||||
@@ -739,14 +796,12 @@ export default function OrganizationsPage({
|
|||||||
setSelectedUnitId(item.id);
|
setSelectedUnitId(item.id);
|
||||||
setUnitDraft({ ...sluggedDraftFrom(item), unit_type_id: item.unit_type_id ?? "", parent_id: item.parent_id ?? "" });
|
setUnitDraft({ ...sluggedDraftFrom(item), unit_type_id: item.unit_type_id ?? "", parent_id: item.parent_id ?? "" });
|
||||||
setActive("units");
|
setActive("units");
|
||||||
|
setActiveEditor("unit");
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const addSubunit = useCallback((item: OrganizationUnitItem) => {
|
const addSubunit = useCallback((item: OrganizationUnitItem) => {
|
||||||
setSelectedUnitId(item.id);
|
openUnitCreate(item.id);
|
||||||
setEditingUnitId(null);
|
}, [openUnitCreate]);
|
||||||
setUnitDraft({ ...emptyUnitDraft(), parent_id: item.id });
|
|
||||||
setActive("units");
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const editRelation = useCallback((item: OrganizationRelationItem) => {
|
const editRelation = useCallback((item: OrganizationRelationItem) => {
|
||||||
setEditingRelationId(item.id);
|
setEditingRelationId(item.id);
|
||||||
@@ -758,6 +813,7 @@ export default function OrganizationsPage({
|
|||||||
is_active: item.is_active
|
is_active: item.is_active
|
||||||
});
|
});
|
||||||
setActive("relations");
|
setActive("relations");
|
||||||
|
setActiveEditor("relation");
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const editFunctionType = useCallback((item: OrganizationFunctionTypeItem) => {
|
const editFunctionType = useCallback((item: OrganizationFunctionTypeItem) => {
|
||||||
@@ -769,6 +825,7 @@ export default function OrganizationsPage({
|
|||||||
act_in_place_allowed: item.act_in_place_allowed
|
act_in_place_allowed: item.act_in_place_allowed
|
||||||
});
|
});
|
||||||
setActive("model");
|
setActive("model");
|
||||||
|
setActiveEditor("functionType");
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const editFunction = useCallback((item: OrganizationFunctionItem) => {
|
const editFunction = useCallback((item: OrganizationFunctionItem) => {
|
||||||
@@ -782,14 +839,39 @@ export default function OrganizationsPage({
|
|||||||
act_in_place_allowed: item.act_in_place_allowed
|
act_in_place_allowed: item.act_in_place_allowed
|
||||||
});
|
});
|
||||||
setActive("functions");
|
setActive("functions");
|
||||||
|
setActiveEditor("function");
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (appliedInitialQueryRef.current || loading) return;
|
||||||
|
if (initialQuery.functionId) {
|
||||||
|
const item = functionById.get(initialQuery.functionId);
|
||||||
|
if (item) {
|
||||||
|
appliedInitialQueryRef.current = true;
|
||||||
|
editFunction(item);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (initialQuery.unitId) {
|
||||||
|
const item = unitById.get(initialQuery.unitId);
|
||||||
|
if (item) {
|
||||||
|
appliedInitialQueryRef.current = true;
|
||||||
|
editUnit(item);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (initialQuery.section && sectionSet.has(initialQuery.section)) {
|
||||||
|
appliedInitialQueryRef.current = true;
|
||||||
|
setActive(initialQuery.section);
|
||||||
|
}
|
||||||
|
}, [editFunction, editUnit, functionById, initialQuery, loading, sectionSet, unitById]);
|
||||||
|
|
||||||
const unitTypeColumns: DataGridColumn<OrganizationUnitTypeItem>[] = [
|
const unitTypeColumns: DataGridColumn<OrganizationUnitTypeItem>[] = [
|
||||||
{ id: "name", header: "i18n:govoplan-organizations.name.709a2322", minWidth: 180, sortable: true, filterable: true, value: (row) => row.name },
|
{ id: "name", header: "i18n:govoplan-organizations.name.709a2322", minWidth: 180, sortable: true, filterable: true, value: (row) => row.name },
|
||||||
{ id: "slug", header: "i18n:govoplan-organizations.slug.094da9b9", width: 160, sortable: true, filterable: true, value: (row) => row.slug },
|
{ id: "slug", header: "i18n:govoplan-organizations.slug.094da9b9", width: 160, sortable: true, filterable: true, value: (row) => row.slug },
|
||||||
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
|
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
|
||||||
{ id: "description", header: "i18n:govoplan-organizations.description.55f8ebc8", minWidth: 220, value: (row) => row.description ?? "" },
|
{ id: "description", header: "i18n:govoplan-organizations.description.55f8ebc8", minWidth: 220, value: (row) => row.description ?? "" },
|
||||||
{ id: "actions", header: "", width: 220, sticky: "end", render: (row) => <RowActions item={row} disabled={!canWriteModel || busy} onEdit={() => editUnitType(row)} onToggle={() => toggleUnitType(row)} /> }
|
{ id: "actions", header: "i18n:govoplan-organizations.actions.c3cd636a", width: 72, sticky: "end", resizable: false, align: "right", render: (row) => <TableActionGroup actions={[{ id: "edit", label: "i18n:govoplan-organizations.edit.7dce1220", icon: <Edit3 size={16} aria-hidden="true" />, disabled: !canWriteModel || busy, onClick: () => editUnitType(row) }]} /> }
|
||||||
];
|
];
|
||||||
|
|
||||||
const structureColumns: DataGridColumn<OrganizationStructureItem>[] = [
|
const structureColumns: DataGridColumn<OrganizationStructureItem>[] = [
|
||||||
@@ -797,7 +879,7 @@ export default function OrganizationsPage({
|
|||||||
{ id: "kind", header: "i18n:govoplan-organizations.kind.794c9d9c", width: 140, sortable: true, filterable: true, value: (row) => row.structure_kind },
|
{ id: "kind", header: "i18n:govoplan-organizations.kind.794c9d9c", width: 140, sortable: true, filterable: true, value: (row) => row.structure_kind },
|
||||||
{ id: "slug", header: "i18n:govoplan-organizations.slug.094da9b9", width: 160, value: (row) => row.slug },
|
{ id: "slug", header: "i18n:govoplan-organizations.slug.094da9b9", width: 160, value: (row) => row.slug },
|
||||||
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
|
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
|
||||||
{ id: "actions", header: "", width: 220, sticky: "end", render: (row) => <RowActions item={row} disabled={!canWriteModel || busy} onEdit={() => editStructure(row)} onToggle={() => toggleStructure(row)} /> }
|
{ id: "actions", header: "i18n:govoplan-organizations.actions.c3cd636a", width: 72, sticky: "end", resizable: false, align: "right", render: (row) => <TableActionGroup actions={[{ id: "edit", label: "i18n:govoplan-organizations.edit.7dce1220", icon: <Edit3 size={16} aria-hidden="true" />, disabled: !canWriteModel || busy, onClick: () => editStructure(row) }]} /> }
|
||||||
];
|
];
|
||||||
|
|
||||||
const relationTypeColumns: DataGridColumn<OrganizationRelationTypeItem>[] = [
|
const relationTypeColumns: DataGridColumn<OrganizationRelationTypeItem>[] = [
|
||||||
@@ -807,7 +889,7 @@ export default function OrganizationsPage({
|
|||||||
{ id: "target", header: "i18n:govoplan-organizations.target_unit.a1507d86", minWidth: 170, render: (row) => optionalLabel(unitTypeById.get(row.target_unit_type_id || "")?.name) },
|
{ id: "target", header: "i18n:govoplan-organizations.target_unit.a1507d86", minWidth: 170, render: (row) => optionalLabel(unitTypeById.get(row.target_unit_type_id || "")?.name) },
|
||||||
{ id: "hierarchical", header: "i18n:govoplan-organizations.hierarchical.8964f313", width: 130, render: (row) => activeStatus(row.is_hierarchical) },
|
{ id: "hierarchical", header: "i18n:govoplan-organizations.hierarchical.8964f313", width: 130, render: (row) => activeStatus(row.is_hierarchical) },
|
||||||
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
|
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
|
||||||
{ id: "actions", header: "", width: 220, sticky: "end", render: (row) => <RowActions item={row} disabled={!canWriteModel || busy} onEdit={() => editRelationType(row)} onToggle={() => toggleRelationType(row)} /> }
|
{ id: "actions", header: "i18n:govoplan-organizations.actions.c3cd636a", width: 72, sticky: "end", resizable: false, align: "right", render: (row) => <TableActionGroup actions={[{ id: "edit", label: "i18n:govoplan-organizations.edit.7dce1220", icon: <Edit3 size={16} aria-hidden="true" />, disabled: !canWriteModel || busy, onClick: () => editRelationType(row) }]} /> }
|
||||||
];
|
];
|
||||||
|
|
||||||
const unitColumns: DataGridColumn<OrganizationUnitItem>[] = [
|
const unitColumns: DataGridColumn<OrganizationUnitItem>[] = [
|
||||||
@@ -816,7 +898,7 @@ export default function OrganizationsPage({
|
|||||||
{ id: "parent", header: "i18n:govoplan-organizations.parent_unit.1986c35e", minWidth: 180, render: (row) => optionalLabel(unitById.get(row.parent_id || "")?.name) },
|
{ id: "parent", header: "i18n:govoplan-organizations.parent_unit.1986c35e", minWidth: 180, render: (row) => optionalLabel(unitById.get(row.parent_id || "")?.name) },
|
||||||
{ id: "slug", header: "i18n:govoplan-organizations.slug.094da9b9", width: 160, value: (row) => row.slug },
|
{ id: "slug", header: "i18n:govoplan-organizations.slug.094da9b9", width: 160, value: (row) => row.slug },
|
||||||
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
|
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
|
||||||
{ id: "actions", header: "", width: 220, sticky: "end", render: (row) => <RowActions item={row} disabled={!canWriteUnits || busy} onEdit={() => editUnit(row)} onToggle={() => toggleUnit(row)} /> }
|
{ id: "actions", header: "i18n:govoplan-organizations.actions.c3cd636a", width: 72, sticky: "end", resizable: false, align: "right", render: (row) => <TableActionGroup actions={[{ id: "edit", label: "i18n:govoplan-organizations.edit.7dce1220", icon: <Edit3 size={16} aria-hidden="true" />, disabled: !canWriteUnits || busy, onClick: () => editUnit(row) }]} /> }
|
||||||
];
|
];
|
||||||
|
|
||||||
const relationColumns: DataGridColumn<OrganizationRelationItem>[] = [
|
const relationColumns: DataGridColumn<OrganizationRelationItem>[] = [
|
||||||
@@ -825,7 +907,7 @@ export default function OrganizationsPage({
|
|||||||
{ id: "source", header: "i18n:govoplan-organizations.source_unit.2fbd8baa", minWidth: 190, render: (row) => optionalLabel(unitById.get(row.source_unit_id)?.name) },
|
{ id: "source", header: "i18n:govoplan-organizations.source_unit.2fbd8baa", minWidth: 190, render: (row) => optionalLabel(unitById.get(row.source_unit_id)?.name) },
|
||||||
{ id: "target", header: "i18n:govoplan-organizations.target_unit.a1507d86", minWidth: 190, render: (row) => optionalLabel(unitById.get(row.target_unit_id)?.name) },
|
{ id: "target", header: "i18n:govoplan-organizations.target_unit.a1507d86", minWidth: 190, render: (row) => optionalLabel(unitById.get(row.target_unit_id)?.name) },
|
||||||
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
|
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
|
||||||
{ id: "actions", header: "", width: 220, sticky: "end", render: (row) => <RowActions item={row} disabled={!canWriteUnits || busy} onEdit={() => editRelation(row)} onToggle={() => toggleRelation(row)} /> }
|
{ id: "actions", header: "i18n:govoplan-organizations.actions.c3cd636a", width: 72, sticky: "end", resizable: false, align: "right", render: (row) => <TableActionGroup actions={[{ id: "edit", label: "i18n:govoplan-organizations.edit.7dce1220", icon: <Edit3 size={16} aria-hidden="true" />, disabled: !canWriteUnits || busy, onClick: () => editRelation(row) }]} /> }
|
||||||
];
|
];
|
||||||
|
|
||||||
const functionTypeColumns: DataGridColumn<OrganizationFunctionTypeItem>[] = [
|
const functionTypeColumns: DataGridColumn<OrganizationFunctionTypeItem>[] = [
|
||||||
@@ -834,7 +916,7 @@ export default function OrganizationsPage({
|
|||||||
{ id: "delegable", header: "i18n:govoplan-organizations.delegable.b4f0137d", width: 120, render: (row) => activeStatus(row.delegable) },
|
{ id: "delegable", header: "i18n:govoplan-organizations.delegable.b4f0137d", width: 120, render: (row) => activeStatus(row.delegable) },
|
||||||
{ id: "actInPlace", header: "i18n:govoplan-organizations.act_in_place.49b942bd", width: 140, render: (row) => activeStatus(row.act_in_place_allowed) },
|
{ id: "actInPlace", header: "i18n:govoplan-organizations.act_in_place.49b942bd", width: 140, render: (row) => activeStatus(row.act_in_place_allowed) },
|
||||||
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
|
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
|
||||||
{ id: "actions", header: "", width: 220, sticky: "end", render: (row) => <RowActions item={row} disabled={!canWriteModel || busy} onEdit={() => editFunctionType(row)} onToggle={() => toggleFunctionType(row)} /> }
|
{ id: "actions", header: "i18n:govoplan-organizations.actions.c3cd636a", width: 72, sticky: "end", resizable: false, align: "right", render: (row) => <TableActionGroup actions={[{ id: "edit", label: "i18n:govoplan-organizations.edit.7dce1220", icon: <Edit3 size={16} aria-hidden="true" />, disabled: !canWriteModel || busy, onClick: () => editFunctionType(row) }]} /> }
|
||||||
];
|
];
|
||||||
|
|
||||||
const functionColumns: DataGridColumn<OrganizationFunctionItem>[] = [
|
const functionColumns: DataGridColumn<OrganizationFunctionItem>[] = [
|
||||||
@@ -845,31 +927,31 @@ export default function OrganizationsPage({
|
|||||||
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
|
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
|
||||||
{
|
{
|
||||||
id: "actions",
|
id: "actions",
|
||||||
header: "",
|
header: "i18n:govoplan-organizations.actions.c3cd636a",
|
||||||
width: functionActionContributions.length ? 340 : 220,
|
width: 88 + (functionActionContributions.length * 40),
|
||||||
sticky: "end",
|
sticky: "end",
|
||||||
render: (row) => (
|
resizable: false,
|
||||||
<RowActions
|
align: "right",
|
||||||
item={row}
|
render: (row) => <TableActionGroup actions={[
|
||||||
disabled={!canWriteFunctions || busy}
|
{
|
||||||
onEdit={() => editFunction(row)}
|
id: "edit",
|
||||||
onToggle={() => toggleFunction(row)}
|
label: "i18n:govoplan-organizations.edit.7dce1220",
|
||||||
extra={functionActionContributions.map((contribution) => (
|
icon: <Edit3 size={16} aria-hidden="true" />,
|
||||||
<span className="organizations-contributed-action" key={contribution.id}>
|
disabled: !canWriteFunctions || busy,
|
||||||
{contribution.render({ settings, auth, function: row })}
|
onClick: () => editFunction(row)
|
||||||
</span>
|
},
|
||||||
))}
|
...functionActionContributions.map((contribution) => ({
|
||||||
/>
|
id: contribution.id,
|
||||||
)
|
label: contribution.label,
|
||||||
|
icon: contribution.icon,
|
||||||
|
onClick: () => contribution.onClick({ settings, auth, function: row })
|
||||||
|
}))
|
||||||
|
]} />
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const canWriteActive = active === "model" ? canWriteModel : active === "units" || active === "relations" ? canWriteUnits : canWriteFunctions;
|
const canWriteActive = active === "model" ? canWriteModel : active === "units" || active === "relations" ? canWriteUnits : canWriteFunctions;
|
||||||
|
|
||||||
function cancelEditButton(onCancel: () => void) {
|
|
||||||
return <Button type="button" variant="ghost" onClick={onCancel} disabled={busy}>i18n:govoplan-organizations.cancel_edit.309c2a6f</Button>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<div className={`${mode === "workspace" ? "content-pad " : ""}organizations-page ${mode === "admin" ? "organizations-admin-page" : ""}`.trim()}>
|
<div className={`${mode === "workspace" ? "content-pad " : ""}organizations-page ${mode === "admin" ? "organizations-admin-page" : ""}`.trim()}>
|
||||||
<div className="page-heading split organizations-heading">
|
<div className="page-heading split organizations-heading">
|
||||||
@@ -878,23 +960,9 @@ export default function OrganizationsPage({
|
|||||||
<p>{description}</p>
|
<p>{description}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="organizations-toolbar">
|
<div className="organizations-toolbar">
|
||||||
<label className="organizations-change-request">
|
|
||||||
<span>i18n:govoplan-organizations.change_request_id.a6e7fd6b</span>
|
|
||||||
<input value={changeRequestId} onChange={(event) => setChangeRequestId(event.target.value)} placeholder="cfgreq-..." disabled={busy} />
|
|
||||||
</label>
|
|
||||||
<Button type="button" onClick={() => void loadModel()} disabled={loading || busy} title="i18n:govoplan-organizations.reload.cce71553">
|
<Button type="button" onClick={() => void loadModel()} disabled={loading || busy} title="i18n:govoplan-organizations.reload.cce71553">
|
||||||
<RefreshCw size={16} aria-hidden="true" /> i18n:govoplan-organizations.reload.cce71553
|
<RefreshCw size={16} aria-hidden="true" /> i18n:govoplan-organizations.reload.cce71553
|
||||||
</Button>
|
</Button>
|
||||||
{hasDirtyDraft && (
|
|
||||||
<>
|
|
||||||
<Button type="button" onClick={() => void saveDrafts()} disabled={busy} title="i18n:govoplan-organizations.save_drafts.606f9401">
|
|
||||||
<Save size={16} aria-hidden="true" /> i18n:govoplan-organizations.save_drafts.606f9401
|
|
||||||
</Button>
|
|
||||||
<Button type="button" variant="ghost" onClick={discardDrafts} disabled={busy} title="i18n:govoplan-organizations.discard_organization_drafts.766d5f46">
|
|
||||||
<XCircle size={16} aria-hidden="true" /> i18n:govoplan-organizations.discard_organization_drafts.766d5f46
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -908,6 +976,7 @@ export default function OrganizationsPage({
|
|||||||
{active === "relations" && renderRelationsSection()}
|
{active === "relations" && renderRelationsSection()}
|
||||||
{active === "functions" && renderFunctionsSection()}
|
{active === "functions" && renderFunctionsSection()}
|
||||||
</LoadingFrame>
|
</LoadingFrame>
|
||||||
|
{renderEditorDialog()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -925,42 +994,181 @@ export default function OrganizationsPage({
|
|||||||
function renderModelSection() {
|
function renderModelSection() {
|
||||||
return (
|
return (
|
||||||
<div className="organizations-table-stack">
|
<div className="organizations-table-stack">
|
||||||
<div className="organizations-section-grid">
|
<Card title="i18n:govoplan-organizations.unit_types.c7afc174" actions={<AdminIconButton label="i18n:govoplan-organizations.add_unit_type.58f2c05a" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteModel || busy} onClick={openUnitTypeCreate} />}>
|
||||||
<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} />
|
|
||||||
<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">
|
|
||||||
<DataGrid id="organizations-unit-types" rows={model.unit_types} columns={unitTypeColumns} getRowKey={(row) => row.id} emptyText="i18n:govoplan-organizations.no_unit_types_found.c81fb2a7" initialFit="container" />
|
<DataGrid id="organizations-unit-types" rows={model.unit_types} columns={unitTypeColumns} getRowKey={(row) => row.id} emptyText="i18n:govoplan-organizations.no_unit_types_found.c81fb2a7" initialFit="container" />
|
||||||
</Card>
|
</Card>
|
||||||
|
<Card title="i18n:govoplan-organizations.structures.f9b7f3b4" actions={<AdminIconButton label="i18n:govoplan-organizations.add_structure.b722042a" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteModel || busy} onClick={openStructureCreate} />}>
|
||||||
|
<DataGrid id="organizations-structures" rows={model.structures} columns={structureColumns} getRowKey={(row) => row.id} emptyText="i18n:govoplan-organizations.no_structures_found.20b382d0" initialFit="container" />
|
||||||
|
</Card>
|
||||||
|
<Card title="i18n:govoplan-organizations.relation_types.e5890528" actions={<AdminIconButton label="i18n:govoplan-organizations.add_relation_type.2ad19d03" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteModel || busy} onClick={openRelationTypeCreate} />}>
|
||||||
|
<DataGrid id="organizations-relation-types" rows={model.relation_types} columns={relationTypeColumns} getRowKey={(row) => row.id} emptyText="i18n:govoplan-organizations.no_relation_types_found.6f90bb81" initialFit="container" />
|
||||||
|
</Card>
|
||||||
|
<Card title="i18n:govoplan-organizations.function_types.172c01fe" actions={<AdminIconButton label="i18n:govoplan-organizations.add_function_type.90d793f1" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteModel || busy} onClick={openFunctionTypeCreate} />}>
|
||||||
|
<DataGrid id="organizations-function-types" rows={model.function_types} columns={functionTypeColumns} getRowKey={(row) => row.id} emptyText="i18n:govoplan-organizations.no_function_types_found.5eef31b1" initialFit="container" />
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<div className="organizations-section-grid">
|
);
|
||||||
<Card title="i18n:govoplan-organizations.structures.f9b7f3b4">
|
}
|
||||||
<form className="organizations-form-grid" onSubmit={(event) => void submitStructure(event)}>
|
|
||||||
|
function renderUnitsSection() {
|
||||||
|
return (
|
||||||
|
<div className="organizations-table-stack">
|
||||||
|
<Card title="i18n:govoplan-organizations.organization_tree.e5bfb195" actions={<AdminIconButton label="i18n:govoplan-organizations.add_root_unit.1ef8a9f5" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteUnits || busy} onClick={() => openUnitCreate()} />}>
|
||||||
|
<div className="explorer-tree-toolbar">
|
||||||
|
{selectedUnitId && <Button type="button" variant="ghost" disabled={busy} onClick={() => setSelectedUnitId("")}>i18n:govoplan-organizations.none.334c4a4c</Button>}
|
||||||
|
</div>
|
||||||
|
{model.units.length ? (
|
||||||
|
<ExplorerTree
|
||||||
|
nodes={unitsByParentId.get("") ?? []}
|
||||||
|
getNodeId={(unit) => unit.id}
|
||||||
|
getNodeLabel={(unit) => unit.name}
|
||||||
|
getNodeChildren={(unit) => unitsByParentId.get(unit.id) ?? []}
|
||||||
|
activeId={selectedUnitId}
|
||||||
|
collapsible={false}
|
||||||
|
depth={0}
|
||||||
|
className="explorer-tree-scroll-region"
|
||||||
|
getNodeWrapStyle={(_unit, context) => ({ paddingLeft: `${Math.min(context.depth * 18, 72)}px` })}
|
||||||
|
onOpen={(unit) => setSelectedUnitId(unit.id)}
|
||||||
|
renderNodeContent={(unit) => (
|
||||||
|
<span className="explorer-tree-node-content">
|
||||||
|
<strong>{unit.name}</strong>
|
||||||
|
<small>{unitTypeById.get(unit.unit_type_id || "")?.name ?? "i18n:govoplan-organizations.no_unit_type.73e799f5"}</small>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
renderNodeActions={(unit) => (
|
||||||
|
<IconButton
|
||||||
|
label="i18n:govoplan-organizations.add_subunit.8256a8f7"
|
||||||
|
icon={<Plus size={16} aria-hidden="true" />}
|
||||||
|
variant="ghost"
|
||||||
|
disabled={!canWriteUnits || busy}
|
||||||
|
onClick={() => addSubunit(unit)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : <p className="muted small-note">i18n:govoplan-organizations.no_units_found.eea2dd0c</p>}
|
||||||
|
</Card>
|
||||||
|
<Card title="i18n:govoplan-organizations.units.e14d0d92" actions={<AdminIconButton label="i18n:govoplan-organizations.add_unit.8fa12fb1" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteUnits || busy} onClick={() => openUnitCreate()} />}>
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRelationsSection() {
|
||||||
|
return (
|
||||||
|
<div className="organizations-table-stack">
|
||||||
|
<Card title="i18n:govoplan-organizations.relations.1c796711" actions={<AdminIconButton label="i18n:govoplan-organizations.add_relation.6b6e67ea" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteUnits || busy} onClick={openRelationCreate} />}>
|
||||||
|
<DataGrid id="organizations-relations" rows={model.relations} columns={relationColumns} getRowKey={(row) => row.id} emptyText="i18n:govoplan-organizations.no_relations_found.4e75b11d" initialFit="container" />
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderFunctionsSection() {
|
||||||
|
return (
|
||||||
|
<div className="organizations-table-stack">
|
||||||
|
<Card title="i18n:govoplan-organizations.functions.805dc49b" actions={<AdminIconButton label="i18n:govoplan-organizations.add_function.6abafee0" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteFunctions || busy} onClick={() => openFunctionCreate()} />}>
|
||||||
|
<DataGrid id="organizations-functions" rows={model.functions} columns={functionColumns} getRowKey={(row) => row.id} emptyText="i18n:govoplan-organizations.no_functions_found.54e759a0" initialFit="container" />
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderChangeRequestField() {
|
||||||
|
return (
|
||||||
|
<div className="wide organizations-dialog-change-request">
|
||||||
|
<FormField label="i18n:govoplan-organizations.change_request_id.a6e7fd6b">
|
||||||
|
<input value={changeRequestId} onChange={(event) => setChangeRequestId(event.target.value)} placeholder="cfgreq-..." disabled={busy} />
|
||||||
|
</FormField>
|
||||||
|
<p className="organizations-field-note">i18n:govoplan-organizations.change_request_id_help.0c119a5d</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function editorTitle(): string {
|
||||||
|
switch (activeEditor) {
|
||||||
|
case "unitType": return editingUnitTypeId ? "i18n:govoplan-organizations.update_unit_type.b96fc0ad" : "i18n:govoplan-organizations.add_unit_type.58f2c05a";
|
||||||
|
case "structure": return editingStructureId ? "i18n:govoplan-organizations.update_structure.b2e25446" : "i18n:govoplan-organizations.add_structure.b722042a";
|
||||||
|
case "relationType": return editingRelationTypeId ? "i18n:govoplan-organizations.update_relation_type.e6e7f30c" : "i18n:govoplan-organizations.add_relation_type.2ad19d03";
|
||||||
|
case "unit": return editingUnitId ? "i18n:govoplan-organizations.update_unit.67e2500f" : "i18n:govoplan-organizations.add_unit.8fa12fb1";
|
||||||
|
case "relation": return editingRelationId ? "i18n:govoplan-organizations.update_relation.4c44ce83" : "i18n:govoplan-organizations.add_relation.6b6e67ea";
|
||||||
|
case "functionType": return editingFunctionTypeId ? "i18n:govoplan-organizations.update_function_type.0b7e07d3" : "i18n:govoplan-organizations.add_function_type.90d793f1";
|
||||||
|
case "function": return editingFunctionId ? "i18n:govoplan-organizations.update_function.504f03e5" : "i18n:govoplan-organizations.add_function.6abafee0";
|
||||||
|
default: return "i18n:govoplan-organizations.organizations.220edf64";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function editorSubmitDisabled(): boolean {
|
||||||
|
if (busy || !activeEditor) return true;
|
||||||
|
if ((activeEditor === "unitType" || activeEditor === "structure" || activeEditor === "relationType" || activeEditor === "functionType") && !canWriteModel) return true;
|
||||||
|
if ((activeEditor === "unit" || activeEditor === "relation") && !canWriteUnits) return true;
|
||||||
|
if (activeEditor === "function" && !canWriteFunctions) return true;
|
||||||
|
if (activeEditor === "unitType") return !unitTypeDraft.name.trim();
|
||||||
|
if (activeEditor === "structure") return !structureDraft.name.trim();
|
||||||
|
if (activeEditor === "relationType") return !relationTypeDraft.name.trim();
|
||||||
|
if (activeEditor === "unit") return !unitDraft.name.trim();
|
||||||
|
if (activeEditor === "relation") return !relationDraft.structure_id || !relationDraft.relation_type_id || !relationDraft.source_unit_id || !relationDraft.target_unit_id;
|
||||||
|
if (activeEditor === "functionType") return !functionTypeDraft.name.trim();
|
||||||
|
if (activeEditor === "function") return !functionDraft.name.trim() || !functionDraft.organization_unit_id;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderEditorDialog() {
|
||||||
|
if (!activeEditor) return null;
|
||||||
|
const formId = `organizations-${activeEditor}-editor`;
|
||||||
|
const submit = activeEditor === "unitType" ? submitUnitType :
|
||||||
|
activeEditor === "structure" ? submitStructure :
|
||||||
|
activeEditor === "relationType" ? submitRelationType :
|
||||||
|
activeEditor === "unit" ? submitUnit :
|
||||||
|
activeEditor === "relation" ? submitRelation :
|
||||||
|
activeEditor === "functionType" ? submitFunctionType :
|
||||||
|
submitFunction;
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={Boolean(activeEditor)}
|
||||||
|
title={editorTitle()}
|
||||||
|
onClose={() => !busy && discardDrafts()}
|
||||||
|
closeDisabled={busy}
|
||||||
|
className="admin-dialog admin-dialog-wide organizations-editor-dialog"
|
||||||
|
footer={(
|
||||||
|
<>
|
||||||
|
<Button type="button" onClick={discardDrafts} disabled={busy}>i18n:govoplan-organizations.cancel_edit.309c2a6f</Button>
|
||||||
|
<Button type="submit" form={formId} variant="primary" disabled={editorSubmitDisabled()}>{editorTitle()}</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<form id={formId} className="admin-form-grid two-columns" onSubmit={(event) => void submit(event)}>
|
||||||
|
{renderEditorFields()}
|
||||||
|
</form>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderEditorFields() {
|
||||||
|
if (activeEditor === "unitType") {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SluggedFields draft={unitTypeDraft} onChange={setUnitTypeDraft} disabled={!canWriteModel || busy} />
|
||||||
|
{renderChangeRequestField()}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (activeEditor === "structure") {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
<SluggedFields draft={structureDraft} onChange={setStructureDraft} disabled={!canWriteModel || busy} />
|
<SluggedFields draft={structureDraft} onChange={setStructureDraft} disabled={!canWriteModel || busy} />
|
||||||
<FormField label="i18n:govoplan-organizations.kind.794c9d9c">
|
<FormField label="i18n:govoplan-organizations.kind.794c9d9c">
|
||||||
<select value={structureDraft.structure_kind} disabled={!canWriteModel || busy} onChange={(event) => setStructureDraft({ ...structureDraft, structure_kind: event.target.value as OrganizationStructureKind })}>
|
<select value={structureDraft.structure_kind} disabled={!canWriteModel || busy} onChange={(event) => setStructureDraft({ ...structureDraft, structure_kind: event.target.value as OrganizationStructureKind })}>
|
||||||
{STRUCTURE_KINDS.map((item) => <option key={item.value} value={item.value}>{item.label}</option>)}
|
{STRUCTURE_KINDS.map((item) => <option key={item.value} value={item.value}>{item.label}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</FormField>
|
</FormField>
|
||||||
<div className="organizations-form-actions wide">
|
{renderChangeRequestField()}
|
||||||
<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>
|
if (activeEditor === "relationType") {
|
||||||
</Card>
|
return (
|
||||||
<Card title="i18n:govoplan-organizations.structures.f9b7f3b4">
|
<>
|
||||||
<DataGrid id="organizations-structures" rows={model.structures} columns={structureColumns} getRowKey={(row) => row.id} emptyText="i18n:govoplan-organizations.no_structures_found.20b382d0" initialFit="container" />
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
<div className="organizations-section-grid">
|
|
||||||
<Card title="i18n:govoplan-organizations.relation_types.e5890528">
|
|
||||||
<form className="organizations-form-grid" onSubmit={(event) => void submitRelationType(event)}>
|
|
||||||
<SluggedFields draft={relationTypeDraft} onChange={setRelationTypeDraft} disabled={!canWriteModel || busy} />
|
<SluggedFields draft={relationTypeDraft} onChange={setRelationTypeDraft} disabled={!canWriteModel || busy} />
|
||||||
<FormField label="i18n:govoplan-organizations.structure.7732fb0b">
|
<FormField label="i18n:govoplan-organizations.structure.7732fb0b">
|
||||||
<select value={relationTypeDraft.structure_id} disabled={!canWriteModel || busy} onChange={(event) => setRelationTypeDraft({ ...relationTypeDraft, structure_id: event.target.value })}>
|
<select value={relationTypeDraft.structure_id} disabled={!canWriteModel || busy} onChange={(event) => setRelationTypeDraft({ ...relationTypeDraft, structure_id: event.target.value })}>
|
||||||
@@ -981,88 +1189,16 @@ export default function OrganizationsPage({
|
|||||||
</select>
|
</select>
|
||||||
</FormField>
|
</FormField>
|
||||||
<div className="organizations-check-list">
|
<div className="organizations-check-list">
|
||||||
<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>
|
<ToggleSwitch checked={relationTypeDraft.is_hierarchical} disabled={!canWriteModel || busy} onChange={(checked) => setRelationTypeDraft({ ...relationTypeDraft, is_hierarchical: checked })} label="i18n:govoplan-organizations.hierarchical.8964f313" />
|
||||||
<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>
|
<ToggleSwitch checked={relationTypeDraft.allow_cycles} disabled={!canWriteModel || busy} onChange={(checked) => setRelationTypeDraft({ ...relationTypeDraft, allow_cycles: checked })} label="i18n:govoplan-organizations.allow_cycles.31327578" />
|
||||||
</div>
|
|
||||||
<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">
|
|
||||||
<DataGrid id="organizations-relation-types" rows={model.relation_types} columns={relationTypeColumns} getRowKey={(row) => row.id} emptyText="i18n:govoplan-organizations.no_relation_types_found.6f90bb81" initialFit="container" />
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
<div className="organizations-section-grid">
|
|
||||||
<Card title="i18n:govoplan-organizations.function_types.172c01fe">
|
|
||||||
<form className="organizations-form-grid" onSubmit={(event) => void submitFunctionType(event)}>
|
|
||||||
<SluggedFields draft={functionTypeDraft} onChange={setFunctionTypeDraft} disabled={!canWriteModel || busy} />
|
|
||||||
<FormField label="i18n:govoplan-organizations.unit_type.9e62810d">
|
|
||||||
<select value={functionTypeDraft.organization_unit_type_id} disabled={!canWriteModel || busy} onChange={(event) => setFunctionTypeDraft({ ...functionTypeDraft, organization_unit_type_id: event.target.value })}>
|
|
||||||
<option value="">i18n:govoplan-organizations.none.334c4a4c</option>
|
|
||||||
{model.unit_types.map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
|
|
||||||
</select>
|
|
||||||
</FormField>
|
|
||||||
<div className="organizations-check-list">
|
|
||||||
<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>
|
|
||||||
<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">
|
|
||||||
<DataGrid id="organizations-function-types" rows={model.function_types} columns={functionTypeColumns} getRowKey={(row) => row.id} emptyText="i18n:govoplan-organizations.no_function_types_found.5eef31b1" initialFit="container" />
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
{renderChangeRequestField()}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (activeEditor === "unit") {
|
||||||
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 (
|
return (
|
||||||
<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} />
|
<SluggedFields draft={unitDraft} onChange={setUnitDraft} disabled={!canWriteUnits || busy} />
|
||||||
<FormField label="i18n:govoplan-organizations.unit_type.9e62810d">
|
<FormField label="i18n:govoplan-organizations.unit_type.9e62810d">
|
||||||
<select value={unitDraft.unit_type_id} disabled={!canWriteUnits || busy} onChange={(event) => setUnitDraft({ ...unitDraft, unit_type_id: event.target.value })}>
|
<select value={unitDraft.unit_type_id} disabled={!canWriteUnits || busy} onChange={(event) => setUnitDraft({ ...unitDraft, unit_type_id: event.target.value })}>
|
||||||
@@ -1081,25 +1217,13 @@ export default function OrganizationsPage({
|
|||||||
i18n:govoplan-organizations.selected_parent.0e013d44 <strong>{unitById.get(unitDraft.parent_id)?.name ?? unitDraft.parent_id}</strong>
|
i18n:govoplan-organizations.selected_parent.0e013d44 <strong>{unitById.get(unitDraft.parent_id)?.name ?? unitDraft.parent_id}</strong>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<div className="organizations-form-actions wide">
|
{renderChangeRequestField()}
|
||||||
<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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (activeEditor === "relation") {
|
||||||
function renderRelationsSection() {
|
|
||||||
return (
|
return (
|
||||||
<div className="organizations-section-grid">
|
<>
|
||||||
<Card title="i18n:govoplan-organizations.add_relation.6b6e67ea">
|
|
||||||
<form className="organizations-form-grid" onSubmit={(event) => void submitRelation(event)}>
|
|
||||||
<FormField label="i18n:govoplan-organizations.structure.7732fb0b">
|
<FormField label="i18n:govoplan-organizations.structure.7732fb0b">
|
||||||
<select value={relationDraft.structure_id} disabled={!canWriteUnits || busy} onChange={(event) => setRelationDraft({ ...relationDraft, structure_id: event.target.value })}>
|
<select value={relationDraft.structure_id} disabled={!canWriteUnits || busy} onChange={(event) => setRelationDraft({ ...relationDraft, structure_id: event.target.value })}>
|
||||||
<option value="">i18n:govoplan-organizations.select_structure.c10f551c</option>
|
<option value="">i18n:govoplan-organizations.select_structure.c10f551c</option>
|
||||||
@@ -1124,28 +1248,38 @@ export default function OrganizationsPage({
|
|||||||
{model.units.map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
|
{model.units.map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</FormField>
|
</FormField>
|
||||||
<label className="organizations-inline-check wide">
|
<div className="wide">
|
||||||
<input type="checkbox" checked={relationDraft.is_active} disabled={!canWriteUnits || busy} onChange={(event) => setRelationDraft({ ...relationDraft, is_active: event.target.checked })} />
|
<ToggleSwitch
|
||||||
<span>i18n:govoplan-organizations.active.a733b809</span>
|
checked={relationDraft.is_active}
|
||||||
</label>
|
disabled={!canWriteUnits || busy}
|
||||||
<div className="organizations-form-actions wide">
|
onChange={(checked) => setRelationDraft({ ...relationDraft, is_active: checked })}
|
||||||
<Button type="submit" variant="primary" disabled={!canWriteUnits || busy}>{editingRelationId ? "i18n:govoplan-organizations.update_relation.4c44ce83" : "i18n:govoplan-organizations.add_relation.6b6e67ea"}</Button>
|
label="i18n:govoplan-organizations.active.a733b809"
|
||||||
{editingRelationId && cancelEditButton(() => { setEditingRelationId(null); setRelationDraft(emptyRelationDraft()); })}
|
/>
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Card>
|
|
||||||
<Card title="i18n:govoplan-organizations.relations.1c796711">
|
|
||||||
<DataGrid id="organizations-relations" rows={model.relations} columns={relationColumns} getRowKey={(row) => row.id} emptyText="i18n:govoplan-organizations.no_relations_found.4e75b11d" initialFit="container" />
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
|
{renderChangeRequestField()}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (activeEditor === "functionType") {
|
||||||
function renderFunctionsSection() {
|
|
||||||
return (
|
return (
|
||||||
<div className="organizations-section-grid">
|
<>
|
||||||
<Card title="i18n:govoplan-organizations.add_function.6abafee0">
|
<SluggedFields draft={functionTypeDraft} onChange={setFunctionTypeDraft} disabled={!canWriteModel || busy} />
|
||||||
<form className="organizations-form-grid" onSubmit={(event) => void submitFunction(event)}>
|
<FormField label="i18n:govoplan-organizations.unit_type.9e62810d">
|
||||||
|
<select value={functionTypeDraft.organization_unit_type_id} disabled={!canWriteModel || busy} onChange={(event) => setFunctionTypeDraft({ ...functionTypeDraft, organization_unit_type_id: event.target.value })}>
|
||||||
|
<option value="">i18n:govoplan-organizations.none.334c4a4c</option>
|
||||||
|
{model.unit_types.map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
|
||||||
|
</select>
|
||||||
|
</FormField>
|
||||||
|
<div className="organizations-check-list">
|
||||||
|
<ToggleSwitch checked={functionTypeDraft.delegable} disabled={!canWriteModel || busy} onChange={(checked) => setFunctionTypeDraft({ ...functionTypeDraft, delegable: checked })} label="i18n:govoplan-organizations.delegable.b4f0137d" />
|
||||||
|
<ToggleSwitch checked={functionTypeDraft.act_in_place_allowed} disabled={!canWriteModel || busy} onChange={(checked) => setFunctionTypeDraft({ ...functionTypeDraft, act_in_place_allowed: checked })} label="i18n:govoplan-organizations.act_in_place.49b942bd" />
|
||||||
|
</div>
|
||||||
|
{renderChangeRequestField()}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
<SluggedFields draft={functionDraft} onChange={setFunctionDraft} disabled={!canWriteFunctions || busy} />
|
<SluggedFields draft={functionDraft} onChange={setFunctionDraft} disabled={!canWriteFunctions || busy} />
|
||||||
<FormField label="i18n:govoplan-organizations.unit.8fe4d595">
|
<FormField label="i18n:govoplan-organizations.unit.8fe4d595">
|
||||||
<select value={functionDraft.organization_unit_id} disabled={!canWriteFunctions || busy} onChange={(event) => setFunctionDraft({ ...functionDraft, organization_unit_id: event.target.value })}>
|
<select value={functionDraft.organization_unit_id} disabled={!canWriteFunctions || busy} onChange={(event) => setFunctionDraft({ ...functionDraft, organization_unit_id: event.target.value })}>
|
||||||
@@ -1160,19 +1294,11 @@ export default function OrganizationsPage({
|
|||||||
</select>
|
</select>
|
||||||
</FormField>
|
</FormField>
|
||||||
<div className="organizations-check-list">
|
<div className="organizations-check-list">
|
||||||
<label><input type="checkbox" checked={functionDraft.delegable} disabled={!canWriteFunctions || busy} onChange={(event) => setFunctionDraft({ ...functionDraft, delegable: event.target.checked })} /> i18n:govoplan-organizations.delegable.b4f0137d</label>
|
<ToggleSwitch checked={functionDraft.delegable} disabled={!canWriteFunctions || busy} onChange={(checked) => setFunctionDraft({ ...functionDraft, delegable: checked })} label="i18n:govoplan-organizations.delegable.b4f0137d" />
|
||||||
<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>
|
<ToggleSwitch checked={functionDraft.act_in_place_allowed} disabled={!canWriteFunctions || busy} onChange={(checked) => setFunctionDraft({ ...functionDraft, act_in_place_allowed: checked })} label="i18n:govoplan-organizations.act_in_place.49b942bd" />
|
||||||
</div>
|
|
||||||
<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">
|
|
||||||
<DataGrid id="organizations-functions" rows={model.functions} columns={functionColumns} getRowKey={(row) => row.id} emptyText="i18n:govoplan-organizations.no_functions_found.54e759a0" initialFit="container" />
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
|
{renderChangeRequestField()}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.",
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user