diff --git a/README.md b/README.md index 976fe00..6096eef 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # GovOPlaN Organizations + +**Repository type:** module (platform). + + `govoplan-organizations` is the canonical organizational model module for GovOPlaN. diff --git a/package.json b/package.json index ea870b2..0d29109 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@govoplan/organizations-webui", - "version": "0.1.7", + "version": "0.1.8", "private": true, "type": "module", "main": "webui/src/index.ts", @@ -19,7 +19,7 @@ "LICENSE" ], "peerDependencies": { - "@govoplan/core-webui": "^0.1.7", + "@govoplan/core-webui": "^0.1.8", "@vitejs/plugin-react": "^4.3.4", "lucide-react": "^1.23.0", "react": "^19.0.0", diff --git a/pyproject.toml b/pyproject.toml index d8dca2b..2c26c9d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta" [project] name = "govoplan-organizations" -version = "0.1.7" +version = "0.1.8" description = "GovOPlaN organizational model module." readme = "README.md" requires-python = ">=3.12" authors = [{ name = "GovOPlaN" }] dependencies = [ - "govoplan-core>=0.1.7", - "govoplan-tenancy>=0.1.7", + "govoplan-core>=0.1.8", + "govoplan-tenancy>=0.1.8", ] [tool.setuptools.packages.find] diff --git a/src/govoplan_organizations/backend/manifest.py b/src/govoplan_organizations/backend/manifest.py index 1a488a4..a33ebcc 100644 --- a/src/govoplan_organizations/backend/manifest.py +++ b/src/govoplan_organizations/backend/manifest.py @@ -86,7 +86,7 @@ def _organization_directory(context: ModuleContext) -> object: manifest = ModuleManifest( id="organizations", name="Organizations", - version="0.1.7", + version="0.1.8", required_capabilities=(CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR), optional_dependencies=("tenancy", "access", "audit", "policy"), permissions=PERMISSIONS, diff --git a/src/govoplan_organizations/backend/migrations/versions/6d7e8f9a0b1c_organization_meta_model.py b/src/govoplan_organizations/backend/migrations/dev_versions/6d7e8f9a0b1c_organization_meta_model.py similarity index 100% rename from src/govoplan_organizations/backend/migrations/versions/6d7e8f9a0b1c_organization_meta_model.py rename to src/govoplan_organizations/backend/migrations/dev_versions/6d7e8f9a0b1c_organization_meta_model.py diff --git a/src/govoplan_organizations/backend/migrations/dev_versions/__init__.py b/src/govoplan_organizations/backend/migrations/dev_versions/__init__.py new file mode 100644 index 0000000..363497f --- /dev/null +++ b/src/govoplan_organizations/backend/migrations/dev_versions/__init__.py @@ -0,0 +1 @@ +"""Organizations Alembic revisions.""" diff --git a/src/govoplan_organizations/backend/migrations/versions/6d7e8f9a0b1c_v017_organizations_baseline.py b/src/govoplan_organizations/backend/migrations/versions/6d7e8f9a0b1c_v017_organizations_baseline.py new file mode 100644 index 0000000..cf331cd --- /dev/null +++ b/src/govoplan_organizations/backend/migrations/versions/6d7e8f9a0b1c_v017_organizations_baseline.py @@ -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') diff --git a/webui/package.json b/webui/package.json index 8c3fafc..3a46754 100644 --- a/webui/package.json +++ b/webui/package.json @@ -1,6 +1,6 @@ { "name": "@govoplan/organizations-webui", - "version": "0.1.7", + "version": "0.1.8", "private": true, "type": "module", "main": "src/index.ts", @@ -14,7 +14,7 @@ "./styles/organizations.css": "./src/styles/organizations.css" }, "peerDependencies": { - "@govoplan/core-webui": "^0.1.7", + "@govoplan/core-webui": "^0.1.8", "@vitejs/plugin-react": "^4.3.4", "lucide-react": "^1.23.0", "react": "^19.0.0", diff --git a/webui/src/features/organizations/OrganizationsPage.tsx b/webui/src/features/organizations/OrganizationsPage.tsx index bbda632..b25a47c 100644 --- a/webui/src/features/organizations/OrganizationsPage.tsx +++ b/webui/src/features/organizations/OrganizationsPage.tsx @@ -13,6 +13,7 @@ import { ModuleSubnav, PageTitle, StatusBadge, + ToggleSwitch, hasScope, useUnsavedDraftGuard, usePlatformUiCapabilities, @@ -324,10 +325,14 @@ function SluggedFields({