Sync GovOPlaN module state
This commit is contained in:
267
.gitignore
vendored
267
.gitignore
vendored
@@ -6,3 +6,270 @@ __pycache__/
|
||||
.ruff_cache/
|
||||
build/
|
||||
dist/
|
||||
|
||||
# GovOPlaN shared ignore rules from govoplan-core
|
||||
# ---> Node
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
.cache
|
||||
# vitepress build output
|
||||
**/.vitepress/dist
|
||||
# vitepress cache directory
|
||||
**/.vitepress/cache
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
# Local WebUI test/build scratch directories
|
||||
.component-test-build/
|
||||
.module-test-build/
|
||||
.policy-test-build/
|
||||
.template-preview-test-build/
|
||||
.import-test-build/
|
||||
webui/.component-test-build/
|
||||
webui/.module-test-build/
|
||||
webui/.policy-test-build/
|
||||
webui/.template-preview-test-build/
|
||||
webui/.import-test-build/
|
||||
# ---> Python
|
||||
# Byte-compiled / optimized / DLL files
|
||||
*$py.class
|
||||
# C extensions
|
||||
*.so
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
develop-eggs/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
cover/
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
# UV
|
||||
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
#uv.lock
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
||||
.pdm.toml
|
||||
.pdm-python
|
||||
.pdm-build/
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
# mkdocs documentation
|
||||
/site
|
||||
# mypy
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
# Ruff stuff:
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
# ---> VisualStudioCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
!.vscode/*.code-snippets
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
*.db
|
||||
# GovOPlaN local runtime state
|
||||
runtime/
|
||||
# GovOPlaN WebUI test output
|
||||
webui/.module-test-build/
|
||||
webui/.component-test-build/
|
||||
|
||||
13
README.md
13
README.md
@@ -3,10 +3,10 @@
|
||||
`govoplan-organizations` is the canonical organizational model module for
|
||||
GovOPlaN.
|
||||
|
||||
It owns organization unit types, organization structures, concrete units,
|
||||
functions, and identity-held function assignments. These facts exist
|
||||
independently of authentication and authorization. Identity links people to
|
||||
accounts, and Access projects organization facts into roles and rights.
|
||||
It owns organization unit types, organization structures, concrete units, and
|
||||
functions. These facts exist independently of authentication and authorization.
|
||||
Identity links people to accounts, IDM links identities to organization
|
||||
functions, and Access projects accepted facts into roles and rights.
|
||||
|
||||
## Boundary
|
||||
|
||||
@@ -17,19 +17,18 @@ Organizations owns:
|
||||
- parallel organization structures and relation types
|
||||
- functions in organization units
|
||||
- whether a function may be delegated or used for acting in place
|
||||
- identity-held function assignments, optionally constrained to one account
|
||||
- whether an assignment applies only to one unit or also to subunits
|
||||
|
||||
Organizations does not own:
|
||||
|
||||
- login accounts, identity lifecycle, or account linking
|
||||
- identity-to-function assignments or candidate search; those bridges are owned by `govoplan-idm`
|
||||
- role and permission evaluation
|
||||
- postboxes, workflows, portals, files, or cases
|
||||
- external IDM connector internals
|
||||
|
||||
The WebUI exposed by this repository is a normal module UI at
|
||||
`/organizations`. It is the editing surface for the meta-model, concrete
|
||||
organization tree, structures, functions, and assignments.
|
||||
organization tree, structures, and functions.
|
||||
|
||||
Admin-only controls are contributed by this module through the platform
|
||||
`admin.sections` WebUI capability, so the admin shell can show them when the
|
||||
|
||||
@@ -15,24 +15,23 @@ The organization model answers where responsibility lives.
|
||||
school, institute, or other unit.
|
||||
- Function: a named responsibility in an organization unit, such as clerk,
|
||||
reviewer, approver, committee secretary, intake desk, or resource manager.
|
||||
- Function assignment: an identity holds a function in an organization unit,
|
||||
optionally for that unit and all subunits. The assignment may be constrained
|
||||
to a specific account, but the real-world responsibility belongs to the
|
||||
identity.
|
||||
|
||||
## Boundary With Identity
|
||||
## Boundary With Identity And IDM
|
||||
|
||||
Organization assignments refer to identity IDs and may optionally carry account
|
||||
IDs for account-specific exercise of a function. This module does not own login
|
||||
accounts, identity lifecycle, or account linking. `govoplan-identity` owns the
|
||||
identity/account directory. The organization module records that an identity
|
||||
currently holds an organizational function.
|
||||
This module does not own login accounts, identity lifecycle, account linking,
|
||||
or identity-to-function assignments. `govoplan-identity` owns the
|
||||
identity/account directory. `govoplan-idm` records that an identity currently
|
||||
holds an organizational function and may constrain that link to one account.
|
||||
|
||||
Resolving accounts to identities, choosing candidate identities for an
|
||||
organization assignment, and importing those mappings from external directories
|
||||
belongs to `govoplan-idm`, not to organizations.
|
||||
|
||||
## Boundary With Access
|
||||
|
||||
A function assignment does not grant rights on its own. `govoplan-access`
|
||||
maps organization facts to roles, rights, permission decisions, delegation
|
||||
enforcement, and explain responses.
|
||||
An IDM function assignment does not grant rights on its own. `govoplan-access`
|
||||
maps organization and IDM facts to roles, rights, permission decisions,
|
||||
delegation enforcement, and explain responses.
|
||||
|
||||
This separation keeps the institution model stable even when authorization
|
||||
policy changes.
|
||||
@@ -40,10 +39,10 @@ policy changes.
|
||||
## UI Boundary
|
||||
|
||||
The organization workspace at `/organizations` is the primary module UI for
|
||||
modelling the meta-model, concrete units, structures, functions, and
|
||||
assignments. Admin-only organization controls are still owned by this module,
|
||||
but are contributed to the platform admin shell through the `admin.sections`
|
||||
WebUI capability.
|
||||
modelling the meta-model, concrete units, structures, and function definitions.
|
||||
Admin-only organization controls are still owned by this
|
||||
module, but are contributed to the platform admin shell through the
|
||||
`admin.sections` WebUI capability.
|
||||
|
||||
The admin contribution must not duplicate the organization editor. It is for
|
||||
governance and policy settings: whether tenant admins may customize the
|
||||
@@ -52,7 +51,9 @@ audit detail and retention behave for organization changes.
|
||||
|
||||
## Boundary With IDM
|
||||
|
||||
`govoplan-idm` may import organization units, functions, and assignments from
|
||||
external directories. It should write through organization capabilities or APIs
|
||||
after preview, approval, and rollback planning. IDM is a synchronization layer,
|
||||
not the canonical owner of the organization model.
|
||||
`govoplan-idm` may import organization-unit and organization-function
|
||||
definitions from external directories. It should write those definitions
|
||||
through organization capabilities or APIs after preview, approval, and rollback
|
||||
planning. IDM owns identity-to-function assignments and bridge views that
|
||||
combine identity and organization facts, such as identity candidates for
|
||||
function assignments.
|
||||
|
||||
@@ -17,7 +17,6 @@ from govoplan_core.core.configuration_control import (
|
||||
from govoplan_core.db.session import get_session
|
||||
from govoplan_organizations.backend.db.models import (
|
||||
OrganizationFunction,
|
||||
OrganizationFunctionAssignment,
|
||||
OrganizationFunctionType,
|
||||
OrganizationRelation,
|
||||
OrganizationRelationType,
|
||||
@@ -28,13 +27,10 @@ from govoplan_organizations.backend.db.models import (
|
||||
)
|
||||
|
||||
from .schemas import (
|
||||
FunctionAssignmentCreateRequest,
|
||||
FunctionAssignmentUpdateRequest,
|
||||
FunctionCreateRequest,
|
||||
FunctionTypeCreateRequest,
|
||||
FunctionTypeUpdateRequest,
|
||||
FunctionUpdateRequest,
|
||||
OrganizationFunctionAssignmentItem,
|
||||
OrganizationFunctionItem,
|
||||
OrganizationFunctionTypeItem,
|
||||
OrganizationModelResponse,
|
||||
@@ -69,7 +65,6 @@ ORG_READ_SCOPES = (
|
||||
ORG_MODEL_WRITE_SCOPES = ("organizations:model:write",)
|
||||
ORG_UNIT_WRITE_SCOPES = ("organizations:unit:write",)
|
||||
ORG_FUNCTION_WRITE_SCOPES = ("organizations:function:write",)
|
||||
ORG_ASSIGN_SCOPES = ("organizations:function:assign",)
|
||||
ORG_SETTINGS_READ_SCOPES = ("organizations:settings:read", "organizations:model:read", "admin:settings:read")
|
||||
ORG_SETTINGS_WRITE_SCOPES = ("organizations:settings:write", "admin:settings:write")
|
||||
ORG_CHANGE_CONTROL_KEY = "organizations.model"
|
||||
@@ -262,10 +257,6 @@ def _item_function(item: OrganizationFunction) -> OrganizationFunctionItem:
|
||||
return OrganizationFunctionItem(**_row_fields(item))
|
||||
|
||||
|
||||
def _item_assignment(item: OrganizationFunctionAssignment) -> OrganizationFunctionAssignmentItem:
|
||||
return OrganizationFunctionAssignmentItem(**_row_fields(item))
|
||||
|
||||
|
||||
def _row_fields(item: object) -> dict[str, Any]:
|
||||
keys = [column.name for column in item.__table__.columns] # type: ignore[attr-defined]
|
||||
return {key: getattr(item, key) for key in keys}
|
||||
@@ -352,7 +343,6 @@ def get_organization_model(
|
||||
relations=[_item_relation(item) for item in session.query(OrganizationRelation).filter(OrganizationRelation.tenant_id == tenant_id).order_by(OrganizationRelation.created_at.asc()).all()],
|
||||
function_types=[_item_function_type(item) for item in session.query(OrganizationFunctionType).filter(OrganizationFunctionType.tenant_id == tenant_id).order_by(OrganizationFunctionType.name.asc()).all()],
|
||||
functions=[_item_function(item) for item in session.query(OrganizationFunction).filter(OrganizationFunction.tenant_id == tenant_id).order_by(OrganizationFunction.name.asc()).all()],
|
||||
function_assignments=[_item_assignment(item) for item in session.query(OrganizationFunctionAssignment).filter(OrganizationFunctionAssignment.tenant_id == tenant_id).order_by(OrganizationFunctionAssignment.created_at.asc()).all()],
|
||||
)
|
||||
|
||||
|
||||
@@ -738,89 +728,6 @@ def update_function(
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/function-assignments", response_model=OrganizationFunctionAssignmentItem, status_code=status.HTTP_201_CREATED)
|
||||
def create_function_assignment(
|
||||
payload: FunctionAssignmentCreateRequest,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(require_any_scope(*ORG_ASSIGN_SCOPES)),
|
||||
) -> OrganizationFunctionAssignmentItem:
|
||||
tenant_id = _tenant_id(principal)
|
||||
approval, target, _value = _ensure_organization_change_allowed(session, principal, tenant_id=tenant_id, resource_type="function_assignment", operation="created", payload=payload)
|
||||
function = _get_tenant_row(session, OrganizationFunction, payload.function_id, tenant_id, "Organization function")
|
||||
item = OrganizationFunctionAssignment(
|
||||
tenant_id=tenant_id,
|
||||
identity_id=payload.identity_id,
|
||||
account_id=payload.account_id,
|
||||
function_id=function.id,
|
||||
organization_unit_id=function.organization_unit_id,
|
||||
applies_to_subunits=payload.applies_to_subunits,
|
||||
source=payload.source,
|
||||
delegated_from_assignment_id=payload.delegated_from_assignment_id,
|
||||
acting_for_account_id=payload.acting_for_account_id,
|
||||
valid_from=payload.valid_from,
|
||||
valid_until=payload.valid_until,
|
||||
is_active=payload.is_active,
|
||||
settings=payload.settings,
|
||||
)
|
||||
if item.delegated_from_assignment_id is not None:
|
||||
_get_tenant_row(session, OrganizationFunctionAssignment, item.delegated_from_assignment_id, tenant_id, "Delegated function assignment")
|
||||
session.add(item)
|
||||
saved = _commit(session, item)
|
||||
result = _item_assignment(saved)
|
||||
_record_organization_change_applied(session, principal, approval=approval, target={**target, "resource_id": saved.id}, before=None, after=result.model_dump(mode="json"))
|
||||
return result
|
||||
|
||||
|
||||
@router.patch("/function-assignments/{item_id}", response_model=OrganizationFunctionAssignmentItem)
|
||||
def update_function_assignment(
|
||||
item_id: str,
|
||||
payload: FunctionAssignmentUpdateRequest,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(require_any_scope(*ORG_ASSIGN_SCOPES)),
|
||||
) -> OrganizationFunctionAssignmentItem:
|
||||
tenant_id = _tenant_id(principal)
|
||||
item = _get_tenant_row(session, OrganizationFunctionAssignment, item_id, tenant_id, "Organization function assignment")
|
||||
before = _row_fields(item)
|
||||
approval, target, _value = _ensure_organization_change_allowed(session, principal, tenant_id=tenant_id, resource_type="function_assignment", operation="updated", payload=payload, resource_id=item_id)
|
||||
if "function_id" in payload.model_fields_set:
|
||||
if payload.function_id is None:
|
||||
raise _invalid("Function is required.")
|
||||
function = _get_tenant_row(session, OrganizationFunction, payload.function_id, tenant_id, "Organization function")
|
||||
item.function_id = function.id
|
||||
item.organization_unit_id = function.organization_unit_id
|
||||
if "identity_id" in payload.model_fields_set and payload.identity_id is None:
|
||||
raise _invalid("Identity is required.")
|
||||
if "source" in payload.model_fields_set and payload.source is None:
|
||||
raise _invalid("Assignment source is required.")
|
||||
if "applies_to_subunits" in payload.model_fields_set and payload.applies_to_subunits is None:
|
||||
raise _invalid("Subunit applicability cannot be empty.")
|
||||
if "is_active" in payload.model_fields_set and payload.is_active is None:
|
||||
raise _invalid("Active state cannot be empty.")
|
||||
if "settings" in payload.model_fields_set and payload.settings is None:
|
||||
raise _invalid("Settings cannot be empty.")
|
||||
for field in (
|
||||
"identity_id",
|
||||
"account_id",
|
||||
"applies_to_subunits",
|
||||
"source",
|
||||
"delegated_from_assignment_id",
|
||||
"acting_for_account_id",
|
||||
"valid_from",
|
||||
"valid_until",
|
||||
"is_active",
|
||||
"settings",
|
||||
):
|
||||
if field in payload.model_fields_set:
|
||||
setattr(item, field, getattr(payload, field))
|
||||
if item.delegated_from_assignment_id == item.id:
|
||||
raise _invalid("A function assignment cannot delegate from itself.")
|
||||
if item.delegated_from_assignment_id is not None:
|
||||
_get_tenant_row(session, OrganizationFunctionAssignment, item.delegated_from_assignment_id, tenant_id, "Delegated function assignment")
|
||||
result = _item_assignment(_commit(session, item))
|
||||
_record_organization_change_applied(session, principal, approval=approval, target=target, before=before, after=result.model_dump(mode="json"))
|
||||
return result
|
||||
|
||||
|
||||
def _optional_function_type(session: Session, tenant_id: str, function_type_id: str | None) -> OrganizationFunctionType | None:
|
||||
if function_type_id is None:
|
||||
return None
|
||||
|
||||
@@ -132,25 +132,6 @@ class OrganizationFunctionItem(BaseModel):
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class OrganizationFunctionAssignmentItem(BaseModel):
|
||||
id: str
|
||||
tenant_id: str
|
||||
identity_id: str
|
||||
account_id: str | None = None
|
||||
function_id: str
|
||||
organization_unit_id: str
|
||||
applies_to_subunits: bool
|
||||
source: str
|
||||
delegated_from_assignment_id: str | None = None
|
||||
acting_for_account_id: str | None = None
|
||||
valid_from: datetime | None = None
|
||||
valid_until: datetime | None = None
|
||||
is_active: bool
|
||||
settings: dict[str, Any]
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class OrganizationModelResponse(BaseModel):
|
||||
unit_types: list[OrganizationUnitTypeItem]
|
||||
structures: list[OrganizationStructureItem]
|
||||
@@ -159,7 +140,6 @@ class OrganizationModelResponse(BaseModel):
|
||||
relations: list[OrganizationRelationItem]
|
||||
function_types: list[OrganizationFunctionTypeItem]
|
||||
functions: list[OrganizationFunctionItem]
|
||||
function_assignments: list[OrganizationFunctionAssignmentItem]
|
||||
|
||||
|
||||
class SluggedCreateRequest(BaseModel):
|
||||
@@ -270,33 +250,3 @@ class FunctionUpdateRequest(SluggedUpdateRequest):
|
||||
function_type_id: str | None = None
|
||||
delegable: bool | None = None
|
||||
act_in_place_allowed: bool | None = None
|
||||
|
||||
|
||||
class FunctionAssignmentCreateRequest(BaseModel):
|
||||
identity_id: str
|
||||
function_id: str
|
||||
account_id: str | None = None
|
||||
applies_to_subunits: bool = False
|
||||
source: str = "direct"
|
||||
delegated_from_assignment_id: str | None = None
|
||||
acting_for_account_id: str | None = None
|
||||
valid_from: datetime | None = None
|
||||
valid_until: datetime | None = None
|
||||
is_active: bool = True
|
||||
settings: dict[str, Any] = Field(default_factory=dict)
|
||||
change_request_id: str | None = None
|
||||
|
||||
|
||||
class FunctionAssignmentUpdateRequest(BaseModel):
|
||||
identity_id: str | None = None
|
||||
function_id: str | None = None
|
||||
account_id: str | None = None
|
||||
applies_to_subunits: bool | None = None
|
||||
source: str | None = None
|
||||
delegated_from_assignment_id: str | None = None
|
||||
acting_for_account_id: str | None = None
|
||||
valid_from: datetime | None = None
|
||||
valid_until: datetime | None = None
|
||||
is_active: bool | None = None
|
||||
settings: dict[str, Any] | None = None
|
||||
change_request_id: str | None = None
|
||||
|
||||
@@ -150,37 +150,8 @@ class OrganizationFunction(Base, TimestampMixin):
|
||||
settings: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
|
||||
|
||||
|
||||
class OrganizationFunctionAssignment(Base, TimestampMixin):
|
||||
__tablename__ = "organizations_function_assignments"
|
||||
__table_args__ = (
|
||||
UniqueConstraint(
|
||||
"tenant_id",
|
||||
"identity_id",
|
||||
"function_id",
|
||||
"organization_unit_id",
|
||||
name="uq_organizations_function_assignments_identity_scope",
|
||||
),
|
||||
)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
||||
tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
|
||||
identity_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
|
||||
account_id: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True)
|
||||
function_id: Mapped[str] = mapped_column(ForeignKey("organizations_functions.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
organization_unit_id: Mapped[str] = mapped_column(ForeignKey("organizations_units.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
applies_to_subunits: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||
source: Mapped[str] = mapped_column(String(50), default="direct", nullable=False)
|
||||
delegated_from_assignment_id: Mapped[str | None] = mapped_column(ForeignKey("organizations_function_assignments.id", ondelete="SET NULL"), nullable=True, index=True)
|
||||
acting_for_account_id: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True)
|
||||
valid_from: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
valid_until: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
settings: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"OrganizationFunction",
|
||||
"OrganizationFunctionAssignment",
|
||||
"OrganizationFunctionType",
|
||||
"OrganizationRelation",
|
||||
"OrganizationRelationType",
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlalchemy import or_
|
||||
|
||||
from govoplan_core.core.organizations import (
|
||||
OrganizationDirectory,
|
||||
OrganizationFunctionAssignmentRef,
|
||||
OrganizationFunctionRef,
|
||||
OrganizationUnitRef,
|
||||
)
|
||||
from govoplan_core.core.identity import IdentityDirectory
|
||||
from govoplan_core.db.session import get_database
|
||||
from govoplan_core.security.time import utc_now
|
||||
from govoplan_organizations.backend.db.models import (
|
||||
OrganizationFunction,
|
||||
OrganizationFunctionAssignment,
|
||||
OrganizationUnit,
|
||||
)
|
||||
|
||||
@@ -50,28 +44,7 @@ def _function_ref(item: OrganizationFunction) -> OrganizationFunctionRef:
|
||||
)
|
||||
|
||||
|
||||
def _assignment_ref(item: OrganizationFunctionAssignment) -> OrganizationFunctionAssignmentRef:
|
||||
return OrganizationFunctionAssignmentRef(
|
||||
id=item.id,
|
||||
tenant_id=item.tenant_id,
|
||||
identity_id=item.identity_id,
|
||||
account_id=item.account_id,
|
||||
function_id=item.function_id,
|
||||
organization_unit_id=item.organization_unit_id,
|
||||
applies_to_subunits=item.applies_to_subunits,
|
||||
source=item.source, # type: ignore[arg-type]
|
||||
delegated_from_assignment_id=item.delegated_from_assignment_id,
|
||||
acting_for_account_id=item.acting_for_account_id,
|
||||
valid_from=item.valid_from,
|
||||
valid_until=item.valid_until,
|
||||
status=_status(item.is_active), # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
|
||||
class SqlOrganizationDirectory(OrganizationDirectory):
|
||||
def __init__(self, *, identity_directory: IdentityDirectory | None = None) -> None:
|
||||
self._identity_directory = identity_directory
|
||||
|
||||
def get_organization_unit(self, organization_unit_id: str) -> OrganizationUnitRef | None:
|
||||
with get_database().session() as session:
|
||||
item = session.get(OrganizationUnit, organization_unit_id)
|
||||
@@ -121,70 +94,3 @@ class SqlOrganizationDirectory(OrganizationDirectory):
|
||||
.all()
|
||||
)
|
||||
return tuple(_function_ref(item) for item in rows)
|
||||
|
||||
def get_function_assignment(self, assignment_id: str) -> OrganizationFunctionAssignmentRef | None:
|
||||
with get_database().session() as session:
|
||||
item = session.get(OrganizationFunctionAssignment, assignment_id)
|
||||
return _assignment_ref(item) if item is not None else None
|
||||
|
||||
def function_assignments_for_account(
|
||||
self,
|
||||
account_id: str,
|
||||
*,
|
||||
tenant_id: str | None = None,
|
||||
) -> tuple[OrganizationFunctionAssignmentRef, ...]:
|
||||
identity_ids: set[str] = set()
|
||||
if self._identity_directory is not None:
|
||||
identity = self._identity_directory.identity_for_account(account_id)
|
||||
if identity is not None:
|
||||
identity_ids.add(identity.id)
|
||||
if not identity_ids:
|
||||
return self._function_assignments_for_identity_ids(
|
||||
identity_ids=(),
|
||||
tenant_id=tenant_id,
|
||||
fallback_account_id=account_id,
|
||||
)
|
||||
return self._function_assignments_for_identity_ids(
|
||||
identity_ids=tuple(identity_ids),
|
||||
tenant_id=tenant_id,
|
||||
fallback_account_id=account_id,
|
||||
)
|
||||
|
||||
def function_assignments_for_identity(
|
||||
self,
|
||||
identity_id: str,
|
||||
*,
|
||||
tenant_id: str | None = None,
|
||||
) -> tuple[OrganizationFunctionAssignmentRef, ...]:
|
||||
return self._function_assignments_for_identity_ids(identity_ids=(identity_id,), tenant_id=tenant_id)
|
||||
|
||||
def _function_assignments_for_identity_ids(
|
||||
self,
|
||||
*,
|
||||
identity_ids: tuple[str, ...],
|
||||
tenant_id: str | None = None,
|
||||
fallback_account_id: str | None = None,
|
||||
) -> tuple[OrganizationFunctionAssignmentRef, ...]:
|
||||
now = utc_now()
|
||||
with get_database().session() as session:
|
||||
identity_or_account_filter = OrganizationFunctionAssignment.identity_id.in_(identity_ids)
|
||||
if fallback_account_id:
|
||||
identity_or_account_filter = or_(
|
||||
identity_or_account_filter,
|
||||
OrganizationFunctionAssignment.account_id == fallback_account_id,
|
||||
)
|
||||
query = (
|
||||
session.query(OrganizationFunctionAssignment)
|
||||
.join(OrganizationFunction, OrganizationFunction.id == OrganizationFunctionAssignment.function_id)
|
||||
.filter(
|
||||
identity_or_account_filter,
|
||||
OrganizationFunctionAssignment.is_active.is_(True),
|
||||
OrganizationFunction.is_active.is_(True),
|
||||
or_(OrganizationFunctionAssignment.valid_from.is_(None), OrganizationFunctionAssignment.valid_from <= now),
|
||||
or_(OrganizationFunctionAssignment.valid_until.is_(None), OrganizationFunctionAssignment.valid_until > now),
|
||||
)
|
||||
.order_by(OrganizationFunctionAssignment.created_at.asc())
|
||||
)
|
||||
if tenant_id is not None:
|
||||
query = query.filter(OrganizationFunctionAssignment.tenant_id == tenant_id)
|
||||
return tuple(_assignment_ref(item) for item in query.all())
|
||||
|
||||
@@ -3,7 +3,6 @@ from __future__ import annotations
|
||||
from pathlib import Path
|
||||
|
||||
from govoplan_core.core.access import CAPABILITY_AUTH_PERMISSION_EVALUATOR, CAPABILITY_AUTH_PRINCIPAL_RESOLVER
|
||||
from govoplan_core.core.identity import CAPABILITY_IDENTITY_DIRECTORY, IdentityDirectory
|
||||
from govoplan_core.core.module_guards import persistent_table_uninstall_guard
|
||||
from govoplan_core.core.modules import (
|
||||
DocumentationTopic,
|
||||
@@ -50,22 +49,21 @@ PERMISSIONS = (
|
||||
_permission("organizations:settings:write", "Manage organization settings", "Edit organization governance, audit, and retention settings."),
|
||||
_permission("organizations:unit:read", "View organization units", "Read concrete organization units and relations."),
|
||||
_permission("organizations:unit:write", "Manage organization units", "Create and edit concrete organization units and relations."),
|
||||
_permission("organizations:function:read", "View organization functions", "Read function definitions and identity-held assignments."),
|
||||
_permission("organizations:function:read", "View organization functions", "Read function definitions."),
|
||||
_permission("organizations:function:write", "Manage organization functions", "Create and edit function definitions."),
|
||||
_permission("organizations:function:assign", "Assign organization functions", "Assign identity-held functions in organization units."),
|
||||
)
|
||||
|
||||
ROLE_TEMPLATES = (
|
||||
RoleTemplate(
|
||||
slug="organization_modeler",
|
||||
name="Organization modeler",
|
||||
description="Manage organization meta-model, concrete units, structures, functions, and assignments.",
|
||||
description="Manage organization meta-model, concrete units, structures, and functions.",
|
||||
permissions=tuple(permission.scope for permission in PERMISSIONS),
|
||||
),
|
||||
RoleTemplate(
|
||||
slug="organization_viewer",
|
||||
name="Organization viewer",
|
||||
description="Read organization model, organization units, and function assignments.",
|
||||
description="Read organization model, organization units, and functions.",
|
||||
permissions=("organizations:model:read", "organizations:settings:read", "organizations:unit:read", "organizations:function:read"),
|
||||
),
|
||||
)
|
||||
@@ -79,15 +77,10 @@ def _route_factory(context: ModuleContext):
|
||||
|
||||
|
||||
def _organization_directory(context: ModuleContext) -> object:
|
||||
identity_directory: IdentityDirectory | None = None
|
||||
registry = context.registry
|
||||
if hasattr(registry, "has_capability") and registry.has_capability(CAPABILITY_IDENTITY_DIRECTORY):
|
||||
capability = registry.require_capability(CAPABILITY_IDENTITY_DIRECTORY)
|
||||
if isinstance(capability, IdentityDirectory):
|
||||
identity_directory = capability
|
||||
del context
|
||||
from govoplan_organizations.backend.directory import SqlOrganizationDirectory
|
||||
|
||||
return SqlOrganizationDirectory(identity_directory=identity_directory)
|
||||
return SqlOrganizationDirectory()
|
||||
|
||||
|
||||
manifest = ModuleManifest(
|
||||
@@ -95,7 +88,7 @@ manifest = ModuleManifest(
|
||||
name="Organizations",
|
||||
version="0.1.6",
|
||||
required_capabilities=(CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR),
|
||||
optional_dependencies=("tenancy", "identity", "access", "audit", "policy"),
|
||||
optional_dependencies=("tenancy", "access", "audit", "policy"),
|
||||
permissions=PERMISSIONS,
|
||||
role_templates=ROLE_TEMPLATES,
|
||||
route_factory=_route_factory,
|
||||
@@ -121,7 +114,6 @@ manifest = ModuleManifest(
|
||||
organization_models.OrganizationUnit,
|
||||
organization_models.OrganizationFunctionType,
|
||||
organization_models.OrganizationFunction,
|
||||
organization_models.OrganizationFunctionAssignment,
|
||||
label="Organizations",
|
||||
),
|
||||
),
|
||||
@@ -132,11 +124,11 @@ manifest = ModuleManifest(
|
||||
DocumentationTopic(
|
||||
id="organizations.model",
|
||||
title="Organization model",
|
||||
summary="Organizations owns units, hierarchy, functions, and function assignments. Access maps those facts to roles and rights.",
|
||||
summary="Organizations owns units, hierarchy, and functions. IDM links identities to functions, and Access maps accepted facts to roles and rights.",
|
||||
body=(
|
||||
"Use organization unit types, structures, and relation types to model how the institution describes itself. "
|
||||
"A concrete organization unit can participate in several structures at the same time, such as an employer hierarchy and an academic structure. "
|
||||
"Functions are held by identities in organization units. Accounts only exercise those functions through identity and access policy."
|
||||
"Functions describe responsibilities in organization units. IDM links identities to those functions, and Access maps accepted facts to roles and rights."
|
||||
),
|
||||
layer="configured",
|
||||
documentation_types=("admin", "user"),
|
||||
|
||||
@@ -239,54 +239,6 @@ def _create_functions() -> None:
|
||||
_create_index_if_missing(op.f(f"ix_organizations_functions_{column}"), "organizations_functions", [column])
|
||||
|
||||
|
||||
def _create_function_assignments() -> None:
|
||||
if "organizations_function_assignments" not in _tables():
|
||||
op.create_table(
|
||||
"organizations_function_assignments",
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("tenant_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("identity_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("account_id", sa.String(length=36), nullable=True),
|
||||
sa.Column("function_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("organization_unit_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("applies_to_subunits", sa.Boolean(), nullable=False),
|
||||
sa.Column("source", sa.String(length=50), nullable=False),
|
||||
sa.Column("delegated_from_assignment_id", sa.String(length=36), nullable=True),
|
||||
sa.Column("acting_for_account_id", sa.String(length=36), nullable=True),
|
||||
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),
|
||||
_json_column("settings"),
|
||||
*_timestamp_columns(),
|
||||
sa.ForeignKeyConstraint(["delegated_from_assignment_id"], ["organizations_function_assignments.id"], name=op.f("fk_organizations_function_assignments_delegated_from_assignment_id_organizations_function_assignments"), ondelete="SET NULL"),
|
||||
sa.ForeignKeyConstraint(["function_id"], ["organizations_functions.id"], name=op.f("fk_organizations_function_assignments_function_id_organizations_functions"), ondelete="CASCADE"),
|
||||
sa.ForeignKeyConstraint(["organization_unit_id"], ["organizations_units.id"], name=op.f("fk_organizations_function_assignments_organization_unit_id_organizations_units"), ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id", name=op.f("pk_organizations_function_assignments")),
|
||||
sa.UniqueConstraint("tenant_id", "identity_id", "function_id", "organization_unit_id", name="uq_organizations_function_assignments_identity_scope"),
|
||||
)
|
||||
else:
|
||||
columns = _columns("organizations_function_assignments")
|
||||
if "identity_id" not in columns:
|
||||
op.add_column("organizations_function_assignments", sa.Column("identity_id", sa.String(length=36), nullable=True))
|
||||
op.execute(sa.text("UPDATE organizations_function_assignments SET identity_id = account_id WHERE identity_id IS NULL"))
|
||||
for column in (
|
||||
"account_id",
|
||||
"acting_for_account_id",
|
||||
"delegated_from_assignment_id",
|
||||
"function_id",
|
||||
"identity_id",
|
||||
"organization_unit_id",
|
||||
"tenant_id",
|
||||
):
|
||||
_create_index_if_missing(op.f(f"ix_organizations_function_assignments_{column}"), "organizations_function_assignments", [column])
|
||||
_create_index_if_missing(
|
||||
"uq_organizations_function_assignments_identity_scope",
|
||||
"organizations_function_assignments",
|
||||
["tenant_id", "identity_id", "function_id", "organization_unit_id"],
|
||||
unique=True,
|
||||
)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
_create_unit_types()
|
||||
_create_tenant_settings()
|
||||
@@ -296,12 +248,10 @@ def upgrade() -> None:
|
||||
_create_relations()
|
||||
_create_function_types()
|
||||
_create_functions()
|
||||
_create_function_assignments()
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
for table_name, indexes in (
|
||||
("organizations_function_assignments", ("uq_organizations_function_assignments_identity_scope",)),
|
||||
("organizations_relations", ("ix_organizations_relations_structure_source", "ix_organizations_relations_structure_target")),
|
||||
("organizations_relation_types", ("ix_organizations_relation_types_structure",)),
|
||||
):
|
||||
|
||||
@@ -125,25 +125,6 @@ export type OrganizationFunctionItem = {
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type OrganizationFunctionAssignmentItem = {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
identity_id: string;
|
||||
account_id?: string | null;
|
||||
function_id: string;
|
||||
organization_unit_id: string;
|
||||
applies_to_subunits: boolean;
|
||||
source: string;
|
||||
delegated_from_assignment_id?: string | null;
|
||||
acting_for_account_id?: string | null;
|
||||
valid_from?: string | null;
|
||||
valid_until?: string | null;
|
||||
is_active: boolean;
|
||||
settings: Record<string, unknown>;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type OrganizationModel = {
|
||||
unit_types: OrganizationUnitTypeItem[];
|
||||
structures: OrganizationStructureItem[];
|
||||
@@ -152,21 +133,6 @@ export type OrganizationModel = {
|
||||
relations: OrganizationRelationItem[];
|
||||
function_types: OrganizationFunctionTypeItem[];
|
||||
functions: OrganizationFunctionItem[];
|
||||
function_assignments: OrganizationFunctionAssignmentItem[];
|
||||
};
|
||||
|
||||
export type IdentityOption = {
|
||||
id: string;
|
||||
display_name?: string | null;
|
||||
external_subject?: string | null;
|
||||
source: string;
|
||||
primary_account_id?: string | null;
|
||||
account_ids: string[];
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type IdentityListResponse = {
|
||||
identities: IdentityOption[];
|
||||
};
|
||||
|
||||
export type OrganizationChangeRequestPayload = {
|
||||
@@ -220,18 +186,6 @@ export type FunctionCreatePayload = SluggedCreatePayload & {
|
||||
act_in_place_allowed?: boolean | null;
|
||||
};
|
||||
|
||||
export type FunctionAssignmentCreatePayload = {
|
||||
identity_id: string;
|
||||
account_id?: string | null;
|
||||
function_id: string;
|
||||
applies_to_subunits: boolean;
|
||||
source: string;
|
||||
delegated_from_assignment_id?: string | null;
|
||||
acting_for_account_id?: string | null;
|
||||
is_active?: boolean;
|
||||
settings?: Record<string, unknown>;
|
||||
} & OrganizationChangeRequestPayload;
|
||||
|
||||
function post<T, P extends Record<string, unknown>>(settings: ApiSettings, path: string, payload: P): Promise<T> {
|
||||
return apiFetch<T>(settings, path, { method: "POST", body: JSON.stringify(payload) });
|
||||
}
|
||||
@@ -244,15 +198,6 @@ export function getOrganizationModel(settings: ApiSettings): Promise<Organizatio
|
||||
return apiFetch<OrganizationModel>(settings, "/api/v1/organizations/model");
|
||||
}
|
||||
|
||||
export async function searchIdentityOptions(settings: ApiSettings, query = "", limit = 25): Promise<IdentityOption[]> {
|
||||
const params = new URLSearchParams();
|
||||
const trimmed = query.trim();
|
||||
if (trimmed) params.set("query", trimmed);
|
||||
params.set("limit", String(limit));
|
||||
const response = await apiFetch<IdentityListResponse>(settings, `/api/v1/identity/identities?${params.toString()}`);
|
||||
return response.identities;
|
||||
}
|
||||
|
||||
export function getOrganizationSettings(settings: ApiSettings): Promise<OrganizationSettingsItem> {
|
||||
return apiFetch<OrganizationSettingsItem>(settings, "/api/v1/organizations/settings");
|
||||
}
|
||||
@@ -316,11 +261,3 @@ export function createFunction(settings: ApiSettings, payload: FunctionCreatePay
|
||||
export function patchFunction(settings: ApiSettings, id: string, payload: Partial<FunctionCreatePayload>): Promise<OrganizationFunctionItem> {
|
||||
return patch(settings, `/api/v1/organizations/functions/${encodeURIComponent(id)}`, payload);
|
||||
}
|
||||
|
||||
export function createFunctionAssignment(settings: ApiSettings, payload: FunctionAssignmentCreatePayload): Promise<OrganizationFunctionAssignmentItem> {
|
||||
return post(settings, "/api/v1/organizations/function-assignments", payload);
|
||||
}
|
||||
|
||||
export function patchFunctionAssignment(settings: ApiSettings, id: string, payload: Partial<FunctionAssignmentCreatePayload>): Promise<OrganizationFunctionAssignmentItem> {
|
||||
return patch(settings, `/api/v1/organizations/function-assignments/${encodeURIComponent(id)}`, payload);
|
||||
}
|
||||
|
||||
114
webui/src/features/organizations/OrganizationFunctionPicker.tsx
Normal file
114
webui/src/features/organizations/OrganizationFunctionPicker.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import type {
|
||||
OrganizationFunctionLabelContext,
|
||||
OrganizationFunctionPickerContext,
|
||||
OrganizationFunctionSelection
|
||||
} from "@govoplan/core-webui";
|
||||
import { getOrganizationModel, type OrganizationFunctionItem, type OrganizationModel, type OrganizationUnitItem } from "../../api/organizations";
|
||||
|
||||
type FunctionOption = OrganizationFunctionSelection & {
|
||||
slug: string;
|
||||
active: boolean;
|
||||
};
|
||||
|
||||
export function OrganizationFunctionPicker({ settings, auth, value, disabled, onChange }: OrganizationFunctionPickerContext) {
|
||||
const [model, setModel] = useState<OrganizationModel | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
const tenantId = (auth.active_tenant ?? auth.tenant).id;
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
setError("");
|
||||
getOrganizationModel(settings)
|
||||
.then((nextModel) => {
|
||||
if (!cancelled) setModel(nextModel);
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
if (!cancelled) setError(err instanceof Error ? err.message : String(err));
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setLoading(false);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [settings.accessToken, settings.apiBaseUrl, tenantId]);
|
||||
|
||||
const options = useMemo(() => functionOptions(model), [model]);
|
||||
const selectedId = value?.functionId ?? "";
|
||||
|
||||
return (
|
||||
<>
|
||||
<select
|
||||
value={selectedId}
|
||||
disabled={disabled || loading || Boolean(error)}
|
||||
onChange={(event) => {
|
||||
const selected = options.find((option) => option.functionId === event.target.value);
|
||||
onChange(selected ?? null);
|
||||
}}
|
||||
>
|
||||
<option value="">{loading ? "i18n:govoplan-organizations.loading_functions.77e8e684" : "i18n:govoplan-organizations.select_function.4895a67d"}</option>
|
||||
{options.map((option) => (
|
||||
<option key={option.functionId} value={option.functionId}>
|
||||
{option.label}{option.organizationUnitLabel ? ` (${option.organizationUnitLabel})` : ""}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{error && <p className="muted small-note">i18n:govoplan-organizations.functions_unavailable.4fd6765d</p>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function OrganizationFunctionLabel({ settings, auth, sourceModule, functionId, fallback }: OrganizationFunctionLabelContext) {
|
||||
const [model, setModel] = useState<OrganizationModel | null>(null);
|
||||
const tenantId = (auth.active_tenant ?? auth.tenant).id;
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
getOrganizationModel(settings)
|
||||
.then((nextModel) => {
|
||||
if (!cancelled) setModel(nextModel);
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setModel(null);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [settings.accessToken, settings.apiBaseUrl, tenantId]);
|
||||
|
||||
const option = useMemo(() => functionOptions(model).find((item) => item.functionId === functionId), [functionId, model]);
|
||||
if (sourceModule !== "organizations" || !option) return <>{fallback ?? functionId}</>;
|
||||
return (
|
||||
<div>
|
||||
<strong>{option.label}</strong>
|
||||
<div className="muted small-note">{option.organizationUnitLabel ?? option.slug}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function functionOptions(model: OrganizationModel | null): FunctionOption[] {
|
||||
if (!model) return [];
|
||||
const unitById = new Map<string, OrganizationUnitItem>(model.units.map((unit) => [unit.id, unit]));
|
||||
return model.functions
|
||||
.filter((item) => item.is_active)
|
||||
.map((item) => toOption(item, unitById.get(item.organization_unit_id)))
|
||||
.sort((left, right) => {
|
||||
const unitDelta = (left.organizationUnitLabel ?? "").localeCompare(right.organizationUnitLabel ?? "");
|
||||
return unitDelta !== 0 ? unitDelta : (left.label ?? left.functionId).localeCompare(right.label ?? right.functionId);
|
||||
});
|
||||
}
|
||||
|
||||
function toOption(item: OrganizationFunctionItem, unit: OrganizationUnitItem | undefined): FunctionOption {
|
||||
return {
|
||||
sourceModule: "organizations",
|
||||
functionId: item.id,
|
||||
label: item.name,
|
||||
organizationUnitId: item.organization_unit_id,
|
||||
organizationUnitLabel: unit?.name ?? null,
|
||||
slug: item.slug,
|
||||
active: item.is_active
|
||||
};
|
||||
}
|
||||
@@ -13,14 +13,16 @@ import {
|
||||
StatusBadge,
|
||||
hasScope,
|
||||
useUnsavedDraftGuard,
|
||||
usePlatformUiCapabilities,
|
||||
type ApiSettings,
|
||||
type AuthInfo,
|
||||
type DataGridColumn,
|
||||
type ModuleSubnavGroup
|
||||
type ModuleSubnavGroup,
|
||||
type OrganizationFunctionActionContribution,
|
||||
type OrganizationFunctionActionsUiCapability
|
||||
} from "@govoplan/core-webui";
|
||||
import {
|
||||
createFunction,
|
||||
createFunctionAssignment,
|
||||
createFunctionType,
|
||||
createRelation,
|
||||
createRelationType,
|
||||
@@ -29,19 +31,14 @@ import {
|
||||
createUnitType,
|
||||
getOrganizationModel,
|
||||
patchFunction,
|
||||
patchFunctionAssignment,
|
||||
patchFunctionType,
|
||||
patchRelation,
|
||||
patchRelationType,
|
||||
patchStructure,
|
||||
patchUnit,
|
||||
patchUnitType,
|
||||
searchIdentityOptions,
|
||||
type FunctionAssignmentCreatePayload,
|
||||
type FunctionCreatePayload,
|
||||
type FunctionTypeCreatePayload,
|
||||
type IdentityOption,
|
||||
type OrganizationFunctionAssignmentItem,
|
||||
type OrganizationFunctionItem,
|
||||
type OrganizationFunctionTypeItem,
|
||||
type OrganizationModel,
|
||||
@@ -58,7 +55,7 @@ import {
|
||||
type UnitCreatePayload
|
||||
} from "../../api/organizations";
|
||||
|
||||
export type OrganizationSection = "model" | "units" | "relations" | "functions" | "assignments";
|
||||
export type OrganizationSection = "model" | "units" | "relations" | "functions";
|
||||
type OrganizationsPageMode = "workspace" | "admin";
|
||||
|
||||
type OrganizationsPageProps = {
|
||||
@@ -116,17 +113,6 @@ type FunctionDraft = SluggedDraft & {
|
||||
act_in_place_allowed: boolean;
|
||||
};
|
||||
|
||||
type AssignmentDraft = {
|
||||
identity_id: string;
|
||||
account_id: string;
|
||||
function_id: string;
|
||||
applies_to_subunits: boolean;
|
||||
source: string;
|
||||
delegated_from_assignment_id: string;
|
||||
acting_for_account_id: string;
|
||||
is_active: boolean;
|
||||
};
|
||||
|
||||
type ActiveItem = {
|
||||
id: string;
|
||||
is_active: boolean;
|
||||
@@ -139,8 +125,7 @@ const EMPTY_MODEL: OrganizationModel = {
|
||||
units: [],
|
||||
relations: [],
|
||||
function_types: [],
|
||||
functions: [],
|
||||
function_assignments: []
|
||||
functions: []
|
||||
};
|
||||
|
||||
const SECTION_GROUPS: ModuleSubnavGroup<OrganizationSection>[] = [
|
||||
@@ -150,8 +135,7 @@ const SECTION_GROUPS: ModuleSubnavGroup<OrganizationSection>[] = [
|
||||
{ id: "model", label: "i18n:govoplan-organizations.meta_model.7398487c", primary: true },
|
||||
{ id: "units", label: "i18n:govoplan-organizations.units.e14d0d92" },
|
||||
{ id: "relations", label: "i18n:govoplan-organizations.relations.1c796711" },
|
||||
{ id: "functions", label: "i18n:govoplan-organizations.functions.805dc49b" },
|
||||
{ id: "assignments", label: "i18n:govoplan-organizations.assignments.278f513e" }
|
||||
{ id: "functions", label: "i18n:govoplan-organizations.functions.805dc49b" }
|
||||
]
|
||||
}
|
||||
];
|
||||
@@ -163,7 +147,7 @@ const STRUCTURE_KINDS: Array<{ value: OrganizationStructureKind; label: string }
|
||||
{ value: "classification", label: "i18n:govoplan-organizations.classification.3e9f6c3a" }
|
||||
];
|
||||
|
||||
const ALL_SECTIONS: OrganizationSection[] = ["model", "units", "relations", "functions", "assignments"];
|
||||
const ALL_SECTIONS: OrganizationSection[] = ["model", "units", "relations", "functions"];
|
||||
|
||||
function emptySluggedDraft(): SluggedDraft {
|
||||
return { name: "", slug: "", description: "", is_active: true };
|
||||
@@ -200,19 +184,6 @@ function emptyFunctionDraft(): FunctionDraft {
|
||||
return { ...emptySluggedDraft(), organization_unit_id: "", function_type_id: "", delegable: false, act_in_place_allowed: false };
|
||||
}
|
||||
|
||||
function emptyAssignmentDraft(): AssignmentDraft {
|
||||
return {
|
||||
identity_id: "",
|
||||
account_id: "",
|
||||
function_id: "",
|
||||
applies_to_subunits: false,
|
||||
source: "direct",
|
||||
delegated_from_assignment_id: "",
|
||||
acting_for_account_id: "",
|
||||
is_active: true
|
||||
};
|
||||
}
|
||||
|
||||
function textOrNull(value: string): string | null {
|
||||
const trimmed = value.trim();
|
||||
return trimmed ? trimmed : null;
|
||||
@@ -270,19 +241,6 @@ function isFunctionDirty(draft: FunctionDraft): boolean {
|
||||
return isSluggedDirty(draft) || Boolean(draft.organization_unit_id || draft.function_type_id) || draft.delegable || draft.act_in_place_allowed;
|
||||
}
|
||||
|
||||
function isAssignmentDirty(draft: AssignmentDraft): boolean {
|
||||
return Boolean(
|
||||
draft.identity_id.trim() ||
|
||||
draft.account_id.trim() ||
|
||||
draft.function_id ||
|
||||
draft.applies_to_subunits ||
|
||||
draft.source.trim() !== "direct" ||
|
||||
draft.delegated_from_assignment_id ||
|
||||
draft.acting_for_account_id ||
|
||||
!draft.is_active
|
||||
);
|
||||
}
|
||||
|
||||
function mapById<T extends { id: string }>(items: T[]): Map<string, T> {
|
||||
return new Map(items.map((item) => [item.id, item]));
|
||||
}
|
||||
@@ -317,21 +275,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"} />;
|
||||
}
|
||||
|
||||
function identityOptionLabel(identity: IdentityOption): string {
|
||||
return identity.display_name || identity.external_subject || identity.primary_account_id || identity.id;
|
||||
}
|
||||
|
||||
function identityDisplay(identityId: string, identities: Map<string, IdentityOption>): JSX.Element {
|
||||
const identity = identities.get(identityId);
|
||||
if (!identity) return <span className="organizations-id">{identityId}</span>;
|
||||
return (
|
||||
<span className="organizations-identity">
|
||||
<span>{identityOptionLabel(identity)}</span>
|
||||
<span className="organizations-id">{identity.id}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
@@ -341,13 +284,14 @@ function ActiveToggleButton({ item, disabled, onToggle }: { item: ActiveItem; di
|
||||
);
|
||||
}
|
||||
|
||||
function RowActions({ item, disabled, onEdit, onToggle }: { item: ActiveItem; disabled: boolean; onEdit: () => void; onToggle: () => void }) {
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -382,6 +326,14 @@ function SluggedFields({
|
||||
);
|
||||
}
|
||||
|
||||
function contributionVisible(auth: AuthInfo, contribution: { anyOf?: string[]; allOf?: string[] }): boolean {
|
||||
const anyOf = contribution.anyOf ?? [];
|
||||
const allOf = contribution.allOf ?? [];
|
||||
if (anyOf.length && !anyOf.some((scope) => hasScope(auth, scope))) return false;
|
||||
if (allOf.length && !allOf.every((scope) => hasScope(auth, scope))) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export default function OrganizationsPage({
|
||||
settings,
|
||||
auth,
|
||||
@@ -408,7 +360,6 @@ export default function OrganizationsPage({
|
||||
const [relationDraft, setRelationDraft] = useState<RelationDraft>(() => emptyRelationDraft());
|
||||
const [functionTypeDraft, setFunctionTypeDraft] = useState<FunctionTypeDraft>(() => emptyFunctionTypeDraft());
|
||||
const [functionDraft, setFunctionDraft] = useState<FunctionDraft>(() => emptyFunctionDraft());
|
||||
const [assignmentDraft, setAssignmentDraft] = useState<AssignmentDraft>(() => emptyAssignmentDraft());
|
||||
const [editingUnitTypeId, setEditingUnitTypeId] = useState<string | null>(null);
|
||||
const [editingStructureId, setEditingStructureId] = useState<string | null>(null);
|
||||
const [editingRelationTypeId, setEditingRelationTypeId] = useState<string | null>(null);
|
||||
@@ -416,18 +367,13 @@ export default function OrganizationsPage({
|
||||
const [editingRelationId, setEditingRelationId] = useState<string | null>(null);
|
||||
const [editingFunctionTypeId, setEditingFunctionTypeId] = useState<string | null>(null);
|
||||
const [editingFunctionId, setEditingFunctionId] = useState<string | null>(null);
|
||||
const [editingAssignmentId, setEditingAssignmentId] = useState<string | null>(null);
|
||||
const [selectedUnitId, setSelectedUnitId] = useState("");
|
||||
const [changeRequestId, setChangeRequestId] = useState("");
|
||||
const [identitySearch, setIdentitySearch] = useState("");
|
||||
const [identityOptions, setIdentityOptions] = useState<IdentityOption[]>([]);
|
||||
const [identityLoading, setIdentityLoading] = useState(false);
|
||||
const [identityLookupAvailable, setIdentityLookupAvailable] = useState(true);
|
||||
const functionActionCapabilities = usePlatformUiCapabilities<OrganizationFunctionActionsUiCapability>("organizations.functionActions");
|
||||
|
||||
const canWriteModel = hasScope(auth, "organizations:model:write");
|
||||
const canWriteUnits = hasScope(auth, "organizations:unit:write");
|
||||
const canWriteFunctions = hasScope(auth, "organizations:function:write");
|
||||
const canAssignFunctions = hasScope(auth, "organizations:function:assign");
|
||||
|
||||
const unitTypeById = useMemo(() => mapById(model.unit_types), [model.unit_types]);
|
||||
const structureById = useMemo(() => mapById(model.structures), [model.structures]);
|
||||
@@ -435,7 +381,13 @@ export default function OrganizationsPage({
|
||||
const unitById = useMemo(() => mapById(model.units), [model.units]);
|
||||
const functionTypeById = useMemo(() => mapById(model.function_types), [model.function_types]);
|
||||
const functionById = useMemo(() => mapById(model.functions), [model.functions]);
|
||||
const identityOptionById = useMemo(() => mapById(identityOptions), [identityOptions]);
|
||||
const functionActionContributions = useMemo(
|
||||
() => functionActionCapabilities
|
||||
.flatMap((capability) => capability.actions)
|
||||
.filter((contribution) => contributionVisible(auth, contribution))
|
||||
.sort((left, right) => (left.order ?? 100) - (right.order ?? 100)),
|
||||
[auth, functionActionCapabilities]
|
||||
);
|
||||
const unitsByParentId = useMemo(() => {
|
||||
const mapped = new Map<string, OrganizationUnitItem[]>();
|
||||
for (const unit of model.units) {
|
||||
@@ -452,8 +404,7 @@ export default function OrganizationsPage({
|
||||
isUnitDirty(unitDraft) ||
|
||||
isRelationDirty(relationDraft) ||
|
||||
isFunctionTypeDirty(functionTypeDraft) ||
|
||||
isFunctionDirty(functionDraft) ||
|
||||
isAssignmentDirty(assignmentDraft);
|
||||
isFunctionDirty(functionDraft);
|
||||
|
||||
const loadModel = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -484,36 +435,6 @@ export default function OrganizationsPage({
|
||||
if (selectedUnitId && !unitById.has(selectedUnitId)) setSelectedUnitId("");
|
||||
}, [selectedUnitId, unitById]);
|
||||
|
||||
useEffect(() => {
|
||||
if (active !== "assignments" || !identityLookupAvailable) return undefined;
|
||||
let cancelled = false;
|
||||
const timeout = window.setTimeout(() => {
|
||||
setIdentityLoading(true);
|
||||
searchIdentityOptions(settings, identitySearch, 50).
|
||||
then((items) => {
|
||||
if (cancelled) return;
|
||||
setIdentityOptions(items);
|
||||
setIdentityLookupAvailable(true);
|
||||
}).
|
||||
catch((caught) => {
|
||||
if (cancelled) return;
|
||||
if (caught instanceof ApiError && (caught.status === 403 || caught.status === 404)) {
|
||||
setIdentityLookupAvailable(false);
|
||||
setIdentityOptions([]);
|
||||
return;
|
||||
}
|
||||
setError(apiErrorMessage(caught));
|
||||
}).
|
||||
finally(() => {
|
||||
if (!cancelled) setIdentityLoading(false);
|
||||
});
|
||||
}, 250);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
window.clearTimeout(timeout);
|
||||
};
|
||||
}, [active, identityLookupAvailable, identitySearch, settings]);
|
||||
|
||||
const runAction = useCallback(async (action: () => Promise<unknown>, successMessage = ""): Promise<boolean> => {
|
||||
setBusy(true);
|
||||
setError("");
|
||||
@@ -539,7 +460,6 @@ export default function OrganizationsPage({
|
||||
setRelationDraft(emptyRelationDraft());
|
||||
setFunctionTypeDraft(emptyFunctionTypeDraft());
|
||||
setFunctionDraft(emptyFunctionDraft());
|
||||
setAssignmentDraft(emptyAssignmentDraft());
|
||||
setEditingUnitTypeId(null);
|
||||
setEditingStructureId(null);
|
||||
setEditingRelationTypeId(null);
|
||||
@@ -547,7 +467,6 @@ export default function OrganizationsPage({
|
||||
setEditingRelationId(null);
|
||||
setEditingFunctionTypeId(null);
|
||||
setEditingFunctionId(null);
|
||||
setEditingAssignmentId(null);
|
||||
}, []);
|
||||
|
||||
function rejectMissingName(): Promise<boolean> {
|
||||
@@ -728,35 +647,6 @@ export default function OrganizationsPage({
|
||||
return ok;
|
||||
}, [canWriteFunctions, editingFunctionId, functionDraft, runAction, settings, withChangeRequest]);
|
||||
|
||||
const submitAssignment = useCallback(async (event?: FormEvent): Promise<boolean> => {
|
||||
event?.preventDefault();
|
||||
if (!canAssignFunctions) return rejectMissingWritePermission();
|
||||
if (!assignmentDraft.identity_id.trim() || !assignmentDraft.function_id) {
|
||||
setError("i18n:govoplan-organizations.select_function.4895a67d");
|
||||
return false;
|
||||
}
|
||||
const payload: FunctionAssignmentCreatePayload = {
|
||||
identity_id: assignmentDraft.identity_id.trim(),
|
||||
account_id: textOrNull(assignmentDraft.account_id),
|
||||
function_id: assignmentDraft.function_id,
|
||||
applies_to_subunits: assignmentDraft.applies_to_subunits,
|
||||
source: assignmentDraft.source.trim() || "direct",
|
||||
delegated_from_assignment_id: textOrNull(assignmentDraft.delegated_from_assignment_id),
|
||||
acting_for_account_id: textOrNull(assignmentDraft.acting_for_account_id),
|
||||
is_active: assignmentDraft.is_active,
|
||||
settings: {}
|
||||
};
|
||||
const ok = await runAction(
|
||||
() => editingAssignmentId ? patchFunctionAssignment(settings, editingAssignmentId, withChangeRequest(payload)) : createFunctionAssignment(settings, withChangeRequest(payload)),
|
||||
editingAssignmentId ? "i18n:govoplan-organizations.assignment_updated.680b6e33" : "i18n:govoplan-organizations.assignment_added.94263d1b"
|
||||
);
|
||||
if (ok) {
|
||||
setAssignmentDraft(emptyAssignmentDraft());
|
||||
setEditingAssignmentId(null);
|
||||
}
|
||||
return ok;
|
||||
}, [assignmentDraft, canAssignFunctions, editingAssignmentId, runAction, settings, withChangeRequest]);
|
||||
|
||||
const saveDrafts = useCallback(async (): Promise<boolean> => {
|
||||
if (isSluggedDirty(unitTypeDraft) && !(await submitUnitType())) return false;
|
||||
if (isSluggedDirty(structureDraft) && !(await submitStructure())) return false;
|
||||
@@ -765,16 +655,13 @@ export default function OrganizationsPage({
|
||||
if (isRelationDirty(relationDraft) && !(await submitRelation())) return false;
|
||||
if (isFunctionTypeDirty(functionTypeDraft) && !(await submitFunctionType())) return false;
|
||||
if (isFunctionDirty(functionDraft) && !(await submitFunction())) return false;
|
||||
if (isAssignmentDirty(assignmentDraft) && !(await submitAssignment())) return false;
|
||||
return true;
|
||||
}, [
|
||||
assignmentDraft,
|
||||
functionDraft,
|
||||
functionTypeDraft,
|
||||
relationDraft,
|
||||
relationTypeDraft,
|
||||
structureDraft,
|
||||
submitAssignment,
|
||||
submitFunction,
|
||||
submitFunctionType,
|
||||
submitRelation,
|
||||
@@ -822,10 +709,6 @@ export default function OrganizationsPage({
|
||||
void runAction(() => patchFunction(settings, item.id, withChangeRequest({ is_active: !item.is_active })));
|
||||
}, [runAction, settings, withChangeRequest]);
|
||||
|
||||
const toggleAssignment = useCallback((item: OrganizationFunctionAssignmentItem) => {
|
||||
void runAction(() => patchFunctionAssignment(settings, item.id, withChangeRequest({ is_active: !item.is_active })));
|
||||
}, [runAction, settings, withChangeRequest]);
|
||||
|
||||
const editUnitType = useCallback((item: OrganizationUnitTypeItem) => {
|
||||
setEditingUnitTypeId(item.id);
|
||||
setUnitTypeDraft(sluggedDraftFrom(item));
|
||||
@@ -901,22 +784,6 @@ export default function OrganizationsPage({
|
||||
setActive("functions");
|
||||
}, []);
|
||||
|
||||
const editAssignment = useCallback((item: OrganizationFunctionAssignmentItem) => {
|
||||
setEditingAssignmentId(item.id);
|
||||
setSelectedUnitId(item.organization_unit_id);
|
||||
setAssignmentDraft({
|
||||
identity_id: item.identity_id,
|
||||
account_id: item.account_id ?? "",
|
||||
function_id: item.function_id,
|
||||
applies_to_subunits: item.applies_to_subunits,
|
||||
source: item.source,
|
||||
delegated_from_assignment_id: item.delegated_from_assignment_id ?? "",
|
||||
acting_for_account_id: item.acting_for_account_id ?? "",
|
||||
is_active: item.is_active
|
||||
});
|
||||
setActive("assignments");
|
||||
}, []);
|
||||
|
||||
const unitTypeColumns: DataGridColumn<OrganizationUnitTypeItem>[] = [
|
||||
{ id: "name", header: "i18n:govoplan-organizations.name.709a2322", minWidth: 180, sortable: true, filterable: true, value: (row) => row.name },
|
||||
{ id: "slug", header: "i18n:govoplan-organizations.slug.094da9b9", width: 160, sortable: true, filterable: true, value: (row) => row.slug },
|
||||
@@ -976,20 +843,28 @@ export default function OrganizationsPage({
|
||||
{ id: "type", header: "i18n:govoplan-organizations.function_type.501fe7c0", minWidth: 180, render: (row) => optionalLabel(functionTypeById.get(row.function_type_id || "")?.name) },
|
||||
{ id: "delegable", header: "i18n:govoplan-organizations.delegable.b4f0137d", width: 120, render: (row) => activeStatus(row.delegable) },
|
||||
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
|
||||
{ id: "actions", header: "", width: 220, sticky: "end", render: (row) => <RowActions item={row} disabled={!canWriteFunctions || busy} onEdit={() => editFunction(row)} onToggle={() => toggleFunction(row)} /> }
|
||||
{
|
||||
id: "actions",
|
||||
header: "",
|
||||
width: functionActionContributions.length ? 340 : 220,
|
||||
sticky: "end",
|
||||
render: (row) => (
|
||||
<RowActions
|
||||
item={row}
|
||||
disabled={!canWriteFunctions || busy}
|
||||
onEdit={() => editFunction(row)}
|
||||
onToggle={() => toggleFunction(row)}
|
||||
extra={functionActionContributions.map((contribution) => (
|
||||
<span className="organizations-contributed-action" key={contribution.id}>
|
||||
{contribution.render({ settings, auth, function: row })}
|
||||
</span>
|
||||
))}
|
||||
/>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
const assignmentColumns: DataGridColumn<OrganizationFunctionAssignmentItem>[] = [
|
||||
{ id: "identity", header: "i18n:govoplan-organizations.identity.3252f35f", minWidth: 220, render: (row) => identityDisplay(row.identity_id, identityOptionById) },
|
||||
{ id: "function", header: "i18n:govoplan-organizations.function.28822f3a", minWidth: 190, render: (row) => optionalLabel(functionById.get(row.function_id)?.name) },
|
||||
{ id: "unit", header: "i18n:govoplan-organizations.unit.8fe4d595", minWidth: 180, render: (row) => optionalLabel(unitById.get(row.organization_unit_id)?.name) },
|
||||
{ id: "subunits", header: "i18n:govoplan-organizations.applies_to_subunits.7a15f741", width: 150, render: (row) => activeStatus(row.applies_to_subunits) },
|
||||
{ id: "source", header: "i18n:govoplan-organizations.source.6da13add", width: 130, value: (row) => row.source },
|
||||
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
|
||||
{ id: "actions", header: "", width: 220, sticky: "end", render: (row) => <RowActions item={row} disabled={!canAssignFunctions || busy} onEdit={() => editAssignment(row)} onToggle={() => toggleAssignment(row)} /> }
|
||||
];
|
||||
|
||||
const canWriteActive = active === "model" ? canWriteModel : active === "units" || active === "relations" ? canWriteUnits : active === "functions" ? canWriteFunctions : canAssignFunctions;
|
||||
const canWriteActive = active === "model" ? canWriteModel : active === "units" || active === "relations" ? canWriteUnits : canWriteFunctions;
|
||||
|
||||
function cancelEditButton(onCancel: () => void) {
|
||||
return <Button type="button" variant="ghost" onClick={onCancel} disabled={busy}>i18n:govoplan-organizations.cancel_edit.309c2a6f</Button>;
|
||||
@@ -1032,7 +907,6 @@ export default function OrganizationsPage({
|
||||
{active === "units" && renderUnitsSection()}
|
||||
{active === "relations" && renderRelationsSection()}
|
||||
{active === "functions" && renderFunctionsSection()}
|
||||
{active === "assignments" && renderAssignmentsSection()}
|
||||
</LoadingFrame>
|
||||
</div>
|
||||
);
|
||||
@@ -1302,77 +1176,4 @@ export default function OrganizationsPage({
|
||||
);
|
||||
}
|
||||
|
||||
function renderAssignmentsSection() {
|
||||
const selectedIdentity = identityOptionById.get(assignmentDraft.identity_id);
|
||||
return (
|
||||
<div className="organizations-section-grid">
|
||||
<Card title="i18n:govoplan-organizations.add_assignment.6682f5f5">
|
||||
<form className="organizations-form-grid" onSubmit={(event) => void submitAssignment(event)}>
|
||||
{identityLookupAvailable ? (
|
||||
<div className="organizations-identity-picker wide">
|
||||
<FormField label="i18n:govoplan-organizations.identity_search.eff6eb16">
|
||||
<input value={identitySearch} placeholder="i18n:govoplan-organizations.search_identities.004afdfd" disabled={!canAssignFunctions || busy} onChange={(event) => setIdentitySearch(event.target.value)} />
|
||||
</FormField>
|
||||
<FormField label="i18n:govoplan-organizations.identity.3252f35f">
|
||||
<select
|
||||
value={assignmentDraft.identity_id}
|
||||
disabled={!canAssignFunctions || busy || identityLoading}
|
||||
onChange={(event) => {
|
||||
const identity = identityOptionById.get(event.target.value);
|
||||
setAssignmentDraft({
|
||||
...assignmentDraft,
|
||||
identity_id: event.target.value,
|
||||
account_id: identity?.primary_account_id ?? identity?.account_ids[0] ?? ""
|
||||
});
|
||||
}}
|
||||
>
|
||||
<option value="">{identityLoading ? "i18n:govoplan-organizations.loading_identities.9c31d2b5" : "i18n:govoplan-organizations.select_identity.510f1134"}</option>
|
||||
{assignmentDraft.identity_id && !identityOptionById.has(assignmentDraft.identity_id) && <option value={assignmentDraft.identity_id}>{assignmentDraft.identity_id}</option>}
|
||||
{identityOptions.map((identity) => <option key={identity.id} value={identity.id}>{identityOptionLabel(identity)}</option>)}
|
||||
</select>
|
||||
</FormField>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<p className="organizations-muted wide">i18n:govoplan-organizations.identity_lookup_unavailable.5a3e77b2</p>
|
||||
<FormField label="i18n:govoplan-organizations.identity_id.b6ef5010">
|
||||
<input value={assignmentDraft.identity_id} placeholder="i18n:govoplan-organizations.identity_id_placeholder.298cbd85" disabled={!canAssignFunctions || busy} onChange={(event) => setAssignmentDraft({ ...assignmentDraft, identity_id: event.target.value })} />
|
||||
</FormField>
|
||||
</>
|
||||
)}
|
||||
<FormField label="i18n:govoplan-organizations.optional_account_id.5fcf495c">
|
||||
{selectedIdentity && selectedIdentity.account_ids.length > 0 ? (
|
||||
<select value={assignmentDraft.account_id} disabled={!canAssignFunctions || busy} onChange={(event) => setAssignmentDraft({ ...assignmentDraft, account_id: event.target.value })}>
|
||||
<option value="">i18n:govoplan-organizations.none.334c4a4c</option>
|
||||
{selectedIdentity.account_ids.map((accountId) => <option key={accountId} value={accountId}>{accountId}</option>)}
|
||||
</select>
|
||||
) : (
|
||||
<input value={assignmentDraft.account_id} disabled={!canAssignFunctions || busy} onChange={(event) => setAssignmentDraft({ ...assignmentDraft, account_id: event.target.value })} />
|
||||
)}
|
||||
</FormField>
|
||||
<FormField label="i18n:govoplan-organizations.function.28822f3a">
|
||||
<select value={assignmentDraft.function_id} disabled={!canAssignFunctions || busy} onChange={(event) => setAssignmentDraft({ ...assignmentDraft, function_id: event.target.value })}>
|
||||
<option value="">i18n:govoplan-organizations.select_function.4895a67d</option>
|
||||
{model.functions.map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
|
||||
</select>
|
||||
</FormField>
|
||||
<FormField label="i18n:govoplan-organizations.source.6da13add">
|
||||
<input value={assignmentDraft.source} disabled={!canAssignFunctions || busy} onChange={(event) => setAssignmentDraft({ ...assignmentDraft, source: event.target.value })} />
|
||||
</FormField>
|
||||
<div className="organizations-check-list">
|
||||
<label><input type="checkbox" checked={assignmentDraft.applies_to_subunits} disabled={!canAssignFunctions || busy} onChange={(event) => setAssignmentDraft({ ...assignmentDraft, applies_to_subunits: event.target.checked })} /> i18n:govoplan-organizations.applies_to_subunits.7a15f741</label>
|
||||
<label><input type="checkbox" checked={assignmentDraft.is_active} disabled={!canAssignFunctions || busy} onChange={(event) => setAssignmentDraft({ ...assignmentDraft, is_active: event.target.checked })} /> i18n:govoplan-organizations.active.a733b809</label>
|
||||
</div>
|
||||
<div className="organizations-form-actions wide">
|
||||
<Button type="submit" variant="primary" disabled={!canAssignFunctions || busy}>{editingAssignmentId ? "i18n:govoplan-organizations.update_assignment.bf130723" : "i18n:govoplan-organizations.add_assignment.6682f5f5"}</Button>
|
||||
{editingAssignmentId && cancelEditButton(() => { setEditingAssignmentId(null); setAssignmentDraft(emptyAssignmentDraft()); })}
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
<Card title="i18n:govoplan-organizations.function_assignments.4c5c6dae">
|
||||
<DataGrid id="organizations-assignments" rows={model.function_assignments} columns={assignmentColumns} getRowKey={(row) => row.id} emptyText="i18n:govoplan-organizations.no_assignments_found.7ecc5bb7" initialFit="container" />
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
en: {
|
||||
"i18n:govoplan-organizations.act_in_place.49b942bd": "Act in place",
|
||||
"i18n:govoplan-organizations.active.a733b809": "Active",
|
||||
"i18n:govoplan-organizations.add_assignment.6682f5f5": "Add assignment",
|
||||
"i18n:govoplan-organizations.add_function.6abafee0": "Add function",
|
||||
"i18n:govoplan-organizations.add_function_type.90d793f1": "Add function type",
|
||||
"i18n:govoplan-organizations.add_relation.6b6e67ea": "Add relation",
|
||||
@@ -15,10 +14,6 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-organizations.add_unit.8fa12fb1": "Add unit",
|
||||
"i18n:govoplan-organizations.add_unit_type.58f2c05a": "Add unit type",
|
||||
"i18n:govoplan-organizations.allow_cycles.31327578": "Allow cycles",
|
||||
"i18n:govoplan-organizations.applies_to_subunits.7a15f741": "Applies to subunits",
|
||||
"i18n:govoplan-organizations.assignment_added.94263d1b": "Function assignment added.",
|
||||
"i18n:govoplan-organizations.assignment_updated.680b6e33": "Function assignment updated.",
|
||||
"i18n:govoplan-organizations.assignments.278f513e": "Assignments",
|
||||
"i18n:govoplan-organizations.audit_and_retention.3ba1d2fc": "Audit and retention",
|
||||
"i18n:govoplan-organizations.audit_detail_level.7397355d": "Audit detail level",
|
||||
"i18n:govoplan-organizations.audit_full.0c83669c": "Full",
|
||||
@@ -34,28 +29,22 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-organizations.deactivate.46ab070d": "Deactivate",
|
||||
"i18n:govoplan-organizations.delegable.b4f0137d": "Delegable",
|
||||
"i18n:govoplan-organizations.description.55f8ebc8": "Description",
|
||||
"i18n:govoplan-organizations.direct_assignment.2a068aac": "direct",
|
||||
"i18n:govoplan-organizations.discard_organization_drafts.766d5f46": "Discard organization drafts",
|
||||
"i18n:govoplan-organizations.edit.7dce1220": "Edit",
|
||||
"i18n:govoplan-organizations.function.28822f3a": "Function",
|
||||
"i18n:govoplan-organizations.function_added.e2266702": "Function added.",
|
||||
"i18n:govoplan-organizations.function_assignments.4c5c6dae": "Function assignments",
|
||||
"i18n:govoplan-organizations.function_type.501fe7c0": "Function type",
|
||||
"i18n:govoplan-organizations.function_type_added.2cd9e899": "Function type added.",
|
||||
"i18n:govoplan-organizations.function_type_updated.1a4f29f6": "Function type updated.",
|
||||
"i18n:govoplan-organizations.function_types.172c01fe": "Function types",
|
||||
"i18n:govoplan-organizations.function_updated.65016009": "Function updated.",
|
||||
"i18n:govoplan-organizations.functions_unavailable.4fd6765d": "Functions are unavailable.",
|
||||
"i18n:govoplan-organizations.functions.805dc49b": "Functions",
|
||||
"i18n:govoplan-organizations.hierarchical.8964f313": "Hierarchical",
|
||||
"i18n:govoplan-organizations.hierarchy.74797f37": "Hierarchy",
|
||||
"i18n:govoplan-organizations.identity.3252f35f": "Identity",
|
||||
"i18n:govoplan-organizations.identity_id.b6ef5010": "Identity ID",
|
||||
"i18n:govoplan-organizations.identity_id_placeholder.298cbd85": "identity UUID",
|
||||
"i18n:govoplan-organizations.identity_lookup_unavailable.5a3e77b2": "Identity lookup is unavailable. Enable the identity module or enter an identity ID manually.",
|
||||
"i18n:govoplan-organizations.identity_search.eff6eb16": "Identity search",
|
||||
"i18n:govoplan-organizations.inactive.09af574c": "Inactive",
|
||||
"i18n:govoplan-organizations.kind.794c9d9c": "Kind",
|
||||
"i18n:govoplan-organizations.loading_identities.9c31d2b5": "Loading identities...",
|
||||
"i18n:govoplan-organizations.loading_functions.77e8e684": "Loading functions...",
|
||||
"i18n:govoplan-organizations.loading_organization_model.846aa317": "Loading organization model...",
|
||||
"i18n:govoplan-organizations.loading_organization_settings.c6008db8": "Loading organization settings...",
|
||||
"i18n:govoplan-organizations.membership.4531d86d": "Membership",
|
||||
@@ -66,7 +55,6 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-organizations.name.709a2322": "Name",
|
||||
"i18n:govoplan-organizations.name_is_required.27cd3782": "Name is required.",
|
||||
"i18n:govoplan-organizations.network.a24d31c3": "Network",
|
||||
"i18n:govoplan-organizations.no_assignments_found.7ecc5bb7": "No function assignments found.",
|
||||
"i18n:govoplan-organizations.no_function_types_found.5eef31b1": "No function types found.",
|
||||
"i18n:govoplan-organizations.no_functions_found.54e759a0": "No functions found.",
|
||||
"i18n:govoplan-organizations.no_relation_types_found.6f90bb81": "No relation types found.",
|
||||
@@ -76,7 +64,6 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-organizations.no_unit_types_found.c81fb2a7": "No unit types found.",
|
||||
"i18n:govoplan-organizations.no_units_found.eea2dd0c": "No units found.",
|
||||
"i18n:govoplan-organizations.none.334c4a4c": "None",
|
||||
"i18n:govoplan-organizations.optional_account_id.5fcf495c": "Optional account ID",
|
||||
"i18n:govoplan-organizations.organization_model.4f924c0e": "Organization model",
|
||||
"i18n:govoplan-organizations.organization_model_admin_description.35dc9f10": "Configure tenant-local unit types, parallel structures, relation types, and function types used by the organization module.",
|
||||
"i18n:govoplan-organizations.organization_settings.c9ab9829": "Organization settings",
|
||||
@@ -84,7 +71,7 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-organizations.organization_settings_saved.bfbcfdfa": "Organization settings saved.",
|
||||
"i18n:govoplan-organizations.organization_tree.e5bfb195": "Organization tree",
|
||||
"i18n:govoplan-organizations.organizations.220edf64": "Organizations",
|
||||
"i18n:govoplan-organizations.organizations_intro.4e67c4bb": "Model organization types, concrete units, structures, functions, and identity-held function assignments.",
|
||||
"i18n:govoplan-organizations.organizations_intro.4e67c4bb": "Model organization types, concrete units, structures, and functions.",
|
||||
"i18n:govoplan-organizations.parent_unit.1986c35e": "Parent unit",
|
||||
"i18n:govoplan-organizations.reactivate.58f2855d": "Reactivate",
|
||||
"i18n:govoplan-organizations.relation_added.ca90461a": "Relation added.",
|
||||
@@ -99,10 +86,8 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-organizations.save_settings.913aba9f": "Save settings",
|
||||
"i18n:govoplan-organizations.save_drafts.606f9401": "Save drafts",
|
||||
"i18n:govoplan-organizations.saving.56a2285c": "Saving...",
|
||||
"i18n:govoplan-organizations.search_identities.004afdfd": "Search by name, subject, account, or ID",
|
||||
"i18n:govoplan-organizations.select_function.4895a67d": "Select function",
|
||||
"i18n:govoplan-organizations.select_function_type.2f426c43": "Select function type",
|
||||
"i18n:govoplan-organizations.select_identity.510f1134": "Select identity",
|
||||
"i18n:govoplan-organizations.select_relation_type.73f92478": "Select relation type",
|
||||
"i18n:govoplan-organizations.select_source_unit.9b5a29c8": "Select source unit",
|
||||
"i18n:govoplan-organizations.select_structure.c10f551c": "Select structure",
|
||||
@@ -129,7 +114,6 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-organizations.unit_types.c7afc174": "Unit types",
|
||||
"i18n:govoplan-organizations.units.e14d0d92": "Units",
|
||||
"i18n:govoplan-organizations.unlimited.35569464": "Unlimited",
|
||||
"i18n:govoplan-organizations.update_assignment.bf130723": "Update assignment",
|
||||
"i18n:govoplan-organizations.update_function.504f03e5": "Update function",
|
||||
"i18n:govoplan-organizations.update_function_type.0b7e07d3": "Update function type",
|
||||
"i18n:govoplan-organizations.update_relation.4c44ce83": "Update relation",
|
||||
@@ -142,7 +126,6 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
de: {
|
||||
"i18n:govoplan-organizations.act_in_place.49b942bd": "In Vertretung handeln",
|
||||
"i18n:govoplan-organizations.active.a733b809": "Aktiv",
|
||||
"i18n:govoplan-organizations.add_assignment.6682f5f5": "Zuordnung hinzufügen",
|
||||
"i18n:govoplan-organizations.add_function.6abafee0": "Funktion hinzufügen",
|
||||
"i18n:govoplan-organizations.add_function_type.90d793f1": "Funktionstyp hinzufügen",
|
||||
"i18n:govoplan-organizations.add_relation.6b6e67ea": "Beziehung hinzufügen",
|
||||
@@ -153,10 +136,6 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-organizations.add_unit.8fa12fb1": "Einheit hinzufügen",
|
||||
"i18n:govoplan-organizations.add_unit_type.58f2c05a": "Einheitstyp hinzufügen",
|
||||
"i18n:govoplan-organizations.allow_cycles.31327578": "Zyklen erlauben",
|
||||
"i18n:govoplan-organizations.applies_to_subunits.7a15f741": "Gilt für Untereinheiten",
|
||||
"i18n:govoplan-organizations.assignment_added.94263d1b": "Funktionszuordnung hinzugefügt.",
|
||||
"i18n:govoplan-organizations.assignment_updated.680b6e33": "Funktionszuordnung aktualisiert.",
|
||||
"i18n:govoplan-organizations.assignments.278f513e": "Zuordnungen",
|
||||
"i18n:govoplan-organizations.audit_and_retention.3ba1d2fc": "Audit und Aufbewahrung",
|
||||
"i18n:govoplan-organizations.audit_detail_level.7397355d": "Audit-Detailgrad",
|
||||
"i18n:govoplan-organizations.audit_full.0c83669c": "Vollständig",
|
||||
@@ -172,28 +151,22 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-organizations.deactivate.46ab070d": "Deaktivieren",
|
||||
"i18n:govoplan-organizations.delegable.b4f0137d": "Delegierbar",
|
||||
"i18n:govoplan-organizations.description.55f8ebc8": "Beschreibung",
|
||||
"i18n:govoplan-organizations.direct_assignment.2a068aac": "direkt",
|
||||
"i18n:govoplan-organizations.discard_organization_drafts.766d5f46": "Organisationsentwürfe verwerfen",
|
||||
"i18n:govoplan-organizations.edit.7dce1220": "Bearbeiten",
|
||||
"i18n:govoplan-organizations.function.28822f3a": "Funktion",
|
||||
"i18n:govoplan-organizations.function_added.e2266702": "Funktion hinzugefügt.",
|
||||
"i18n:govoplan-organizations.function_assignments.4c5c6dae": "Funktionszuordnungen",
|
||||
"i18n:govoplan-organizations.function_type.501fe7c0": "Funktionstyp",
|
||||
"i18n:govoplan-organizations.function_type_added.2cd9e899": "Funktionstyp hinzugefügt.",
|
||||
"i18n:govoplan-organizations.function_type_updated.1a4f29f6": "Funktionstyp aktualisiert.",
|
||||
"i18n:govoplan-organizations.function_types.172c01fe": "Funktionstypen",
|
||||
"i18n:govoplan-organizations.function_updated.65016009": "Funktion aktualisiert.",
|
||||
"i18n:govoplan-organizations.functions_unavailable.4fd6765d": "Funktionen sind nicht verfügbar.",
|
||||
"i18n:govoplan-organizations.functions.805dc49b": "Funktionen",
|
||||
"i18n:govoplan-organizations.hierarchical.8964f313": "Hierarchisch",
|
||||
"i18n:govoplan-organizations.hierarchy.74797f37": "Hierarchie",
|
||||
"i18n:govoplan-organizations.identity.3252f35f": "Identität",
|
||||
"i18n:govoplan-organizations.identity_id.b6ef5010": "Identitäts-ID",
|
||||
"i18n:govoplan-organizations.identity_id_placeholder.298cbd85": "Identitäts-UUID",
|
||||
"i18n:govoplan-organizations.identity_lookup_unavailable.5a3e77b2": "Die Identitätssuche ist nicht verfügbar. Aktiviere das Identitätsmodul oder trage eine Identitäts-ID manuell ein.",
|
||||
"i18n:govoplan-organizations.identity_search.eff6eb16": "Identitätssuche",
|
||||
"i18n:govoplan-organizations.inactive.09af574c": "Inaktiv",
|
||||
"i18n:govoplan-organizations.kind.794c9d9c": "Art",
|
||||
"i18n:govoplan-organizations.loading_identities.9c31d2b5": "Identitäten werden geladen...",
|
||||
"i18n:govoplan-organizations.loading_functions.77e8e684": "Funktionen werden geladen...",
|
||||
"i18n:govoplan-organizations.loading_organization_model.846aa317": "Organisationsmodell wird geladen...",
|
||||
"i18n:govoplan-organizations.loading_organization_settings.c6008db8": "Organisationseinstellungen werden geladen...",
|
||||
"i18n:govoplan-organizations.membership.4531d86d": "Mitgliedschaft",
|
||||
@@ -204,7 +177,6 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-organizations.name.709a2322": "Name",
|
||||
"i18n:govoplan-organizations.name_is_required.27cd3782": "Name ist erforderlich.",
|
||||
"i18n:govoplan-organizations.network.a24d31c3": "Netzwerk",
|
||||
"i18n:govoplan-organizations.no_assignments_found.7ecc5bb7": "Keine Funktionszuordnungen gefunden.",
|
||||
"i18n:govoplan-organizations.no_function_types_found.5eef31b1": "Keine Funktionstypen gefunden.",
|
||||
"i18n:govoplan-organizations.no_functions_found.54e759a0": "Keine Funktionen gefunden.",
|
||||
"i18n:govoplan-organizations.no_relation_types_found.6f90bb81": "Keine Beziehungstypen gefunden.",
|
||||
@@ -214,7 +186,6 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-organizations.no_unit_types_found.c81fb2a7": "Keine Einheitstypen gefunden.",
|
||||
"i18n:govoplan-organizations.no_units_found.eea2dd0c": "Keine Einheiten gefunden.",
|
||||
"i18n:govoplan-organizations.none.334c4a4c": "Keine",
|
||||
"i18n:govoplan-organizations.optional_account_id.5fcf495c": "Optionale Konto-ID",
|
||||
"i18n:govoplan-organizations.organization_model.4f924c0e": "Organisationsmodell",
|
||||
"i18n:govoplan-organizations.organization_model_admin_description.35dc9f10": "Konfiguriere mandantenbezogene Einheitstypen, parallele Strukturen, Beziehungstypen und Funktionstypen des Organisationsmoduls.",
|
||||
"i18n:govoplan-organizations.organization_settings.c9ab9829": "Organisationseinstellungen",
|
||||
@@ -222,7 +193,7 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-organizations.organization_settings_saved.bfbcfdfa": "Organisationseinstellungen gespeichert.",
|
||||
"i18n:govoplan-organizations.organization_tree.e5bfb195": "Organisationsbaum",
|
||||
"i18n:govoplan-organizations.organizations.220edf64": "Organisationen",
|
||||
"i18n:govoplan-organizations.organizations_intro.4e67c4bb": "Modelliere Organisationstypen, konkrete Einheiten, Strukturen, Funktionen und identitätsbezogene Funktionszuordnungen.",
|
||||
"i18n:govoplan-organizations.organizations_intro.4e67c4bb": "Modelliere Organisationstypen, konkrete Einheiten, Strukturen und Funktionen.",
|
||||
"i18n:govoplan-organizations.parent_unit.1986c35e": "Übergeordnete Einheit",
|
||||
"i18n:govoplan-organizations.reactivate.58f2855d": "Reaktivieren",
|
||||
"i18n:govoplan-organizations.relation_added.ca90461a": "Beziehung hinzugefügt.",
|
||||
@@ -237,10 +208,8 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-organizations.save_settings.913aba9f": "Einstellungen speichern",
|
||||
"i18n:govoplan-organizations.save_drafts.606f9401": "Entwürfe speichern",
|
||||
"i18n:govoplan-organizations.saving.56a2285c": "Speichern...",
|
||||
"i18n:govoplan-organizations.search_identities.004afdfd": "Nach Name, Subject, Konto oder ID suchen",
|
||||
"i18n:govoplan-organizations.select_function.4895a67d": "Funktion auswählen",
|
||||
"i18n:govoplan-organizations.select_function_type.2f426c43": "Funktionstyp auswählen",
|
||||
"i18n:govoplan-organizations.select_identity.510f1134": "Identität auswählen",
|
||||
"i18n:govoplan-organizations.select_relation_type.73f92478": "Beziehungstyp auswählen",
|
||||
"i18n:govoplan-organizations.select_source_unit.9b5a29c8": "Quell-Einheit auswählen",
|
||||
"i18n:govoplan-organizations.select_structure.c10f551c": "Struktur auswählen",
|
||||
@@ -267,7 +236,6 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-organizations.unit_types.c7afc174": "Einheitstypen",
|
||||
"i18n:govoplan-organizations.units.e14d0d92": "Einheiten",
|
||||
"i18n:govoplan-organizations.unlimited.35569464": "Unbegrenzt",
|
||||
"i18n:govoplan-organizations.update_assignment.bf130723": "Zuordnung aktualisieren",
|
||||
"i18n:govoplan-organizations.update_function.504f03e5": "Funktion aktualisieren",
|
||||
"i18n:govoplan-organizations.update_function_type.0b7e07d3": "Funktionstyp aktualisieren",
|
||||
"i18n:govoplan-organizations.update_relation.4c44ce83": "Beziehung aktualisieren",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createElement, lazy } from "react";
|
||||
import type { AdminSectionsUiCapability, PlatformWebModule } from "@govoplan/core-webui";
|
||||
import type { AdminSectionsUiCapability, OrganizationFunctionPickerUiCapability, PlatformWebModule } from "@govoplan/core-webui";
|
||||
import { generatedTranslations } from "./i18n/generatedTranslations";
|
||||
import { OrganizationFunctionLabel, OrganizationFunctionPicker } from "./features/organizations/OrganizationFunctionPicker";
|
||||
import "./styles/organizations.css";
|
||||
|
||||
const OrganizationsPage = lazy(() => import("./features/organizations/OrganizationsPage"));
|
||||
@@ -31,12 +32,18 @@ const organizationAdminSections: AdminSectionsUiCapability = {
|
||||
]
|
||||
};
|
||||
|
||||
const organizationFunctionPicker: OrganizationFunctionPickerUiCapability = {
|
||||
sourceModule: "organizations",
|
||||
renderPicker: (context) => createElement(OrganizationFunctionPicker, context),
|
||||
renderLabel: (context) => createElement(OrganizationFunctionLabel, context)
|
||||
};
|
||||
|
||||
export const organizationsModule: PlatformWebModule = {
|
||||
id: "organizations",
|
||||
label: "i18n:govoplan-organizations.organizations.220edf64",
|
||||
version: "1.0.0",
|
||||
dependencies: ["access"],
|
||||
optionalDependencies: ["identity", "admin"],
|
||||
optionalDependencies: ["admin"],
|
||||
translations,
|
||||
navItems: [
|
||||
{
|
||||
@@ -56,7 +63,8 @@ export const organizationsModule: PlatformWebModule = {
|
||||
}
|
||||
],
|
||||
uiCapabilities: {
|
||||
"admin.sections": organizationAdminSections
|
||||
"admin.sections": organizationAdminSections,
|
||||
"organizations.functionPicker": organizationFunctionPicker
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -90,6 +90,10 @@
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.organizations-contributed-action {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.organizations-check-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
@@ -167,18 +171,6 @@
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.organizations-identity-picker {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.organizations-identity {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.organizations-id {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: 12px;
|
||||
|
||||
Reference in New Issue
Block a user