Add organizations module surface
This commit is contained in:
31
webui/package.json
Normal file
31
webui/package.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "@govoplan/organizations-webui",
|
||||
"version": "0.1.6",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"module": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"import": "./src/index.ts"
|
||||
},
|
||||
"./styles/organizations.css": "./src/styles/organizations.css"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@govoplan/core-webui": "^0.1.6",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"lucide-react": "^1.23.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^7.1.1",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^6.0.6"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@govoplan/core-webui": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
270
webui/src/api/organizations.ts
Normal file
270
webui/src/api/organizations.ts
Normal file
@@ -0,0 +1,270 @@
|
||||
import { apiFetch, type ApiSettings } from "@govoplan/core-webui";
|
||||
|
||||
export type OrganizationUnitTypeItem = {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
is_active: boolean;
|
||||
settings: Record<string, unknown>;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type OrganizationStructureKind = "hierarchy" | "network" | "membership" | "classification";
|
||||
|
||||
export type OrganizationStructureItem = {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
structure_kind: OrganizationStructureKind | string;
|
||||
is_active: boolean;
|
||||
settings: Record<string, unknown>;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type OrganizationRelationTypeItem = {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
structure_id?: string | null;
|
||||
slug: string;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
source_unit_type_id?: string | null;
|
||||
target_unit_type_id?: string | null;
|
||||
is_hierarchical: boolean;
|
||||
allow_cycles: boolean;
|
||||
is_active: boolean;
|
||||
settings: Record<string, unknown>;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type OrganizationUnitItem = {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
unit_type_id?: string | null;
|
||||
parent_id?: string | null;
|
||||
slug: string;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
is_active: boolean;
|
||||
settings: Record<string, unknown>;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type OrganizationRelationItem = {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
structure_id: string;
|
||||
relation_type_id: string;
|
||||
source_unit_id: string;
|
||||
target_unit_id: string;
|
||||
valid_from?: string | null;
|
||||
valid_until?: string | null;
|
||||
is_active: boolean;
|
||||
settings: Record<string, unknown>;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type OrganizationFunctionTypeItem = {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
organization_unit_type_id?: string | null;
|
||||
delegable: boolean;
|
||||
act_in_place_allowed: boolean;
|
||||
is_active: boolean;
|
||||
settings: Record<string, unknown>;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type OrganizationFunctionItem = {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
function_type_id?: string | null;
|
||||
organization_unit_id: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
delegable: boolean;
|
||||
act_in_place_allowed: boolean;
|
||||
is_active: boolean;
|
||||
settings: Record<string, unknown>;
|
||||
created_at: string;
|
||||
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[];
|
||||
relation_types: OrganizationRelationTypeItem[];
|
||||
units: OrganizationUnitItem[];
|
||||
relations: OrganizationRelationItem[];
|
||||
function_types: OrganizationFunctionTypeItem[];
|
||||
functions: OrganizationFunctionItem[];
|
||||
function_assignments: OrganizationFunctionAssignmentItem[];
|
||||
};
|
||||
|
||||
export type SluggedCreatePayload = {
|
||||
slug?: string | null;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
is_active?: boolean;
|
||||
settings?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type StructureCreatePayload = SluggedCreatePayload & {
|
||||
structure_kind: OrganizationStructureKind;
|
||||
};
|
||||
|
||||
export type RelationTypeCreatePayload = SluggedCreatePayload & {
|
||||
structure_id?: string | null;
|
||||
source_unit_type_id?: string | null;
|
||||
target_unit_type_id?: string | null;
|
||||
is_hierarchical: boolean;
|
||||
allow_cycles: boolean;
|
||||
};
|
||||
|
||||
export type UnitCreatePayload = SluggedCreatePayload & {
|
||||
unit_type_id?: string | null;
|
||||
parent_id?: string | null;
|
||||
};
|
||||
|
||||
export type RelationCreatePayload = {
|
||||
structure_id: string;
|
||||
relation_type_id: string;
|
||||
source_unit_id: string;
|
||||
target_unit_id: string;
|
||||
is_active?: boolean;
|
||||
settings?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type FunctionTypeCreatePayload = SluggedCreatePayload & {
|
||||
organization_unit_type_id?: string | null;
|
||||
delegable: boolean;
|
||||
act_in_place_allowed: boolean;
|
||||
};
|
||||
|
||||
export type FunctionCreatePayload = SluggedCreatePayload & {
|
||||
organization_unit_id: string;
|
||||
function_type_id?: string | null;
|
||||
delegable?: boolean | null;
|
||||
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>;
|
||||
};
|
||||
|
||||
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) });
|
||||
}
|
||||
|
||||
function patch<T, P extends Record<string, unknown>>(settings: ApiSettings, path: string, payload: P): Promise<T> {
|
||||
return apiFetch<T>(settings, path, { method: "PATCH", body: JSON.stringify(payload) });
|
||||
}
|
||||
|
||||
export function getOrganizationModel(settings: ApiSettings): Promise<OrganizationModel> {
|
||||
return apiFetch<OrganizationModel>(settings, "/api/v1/organizations/model");
|
||||
}
|
||||
|
||||
export function createUnitType(settings: ApiSettings, payload: SluggedCreatePayload): Promise<OrganizationUnitTypeItem> {
|
||||
return post(settings, "/api/v1/organizations/unit-types", payload);
|
||||
}
|
||||
|
||||
export function patchUnitType(settings: ApiSettings, id: string, payload: Partial<SluggedCreatePayload>): Promise<OrganizationUnitTypeItem> {
|
||||
return patch(settings, `/api/v1/organizations/unit-types/${encodeURIComponent(id)}`, payload);
|
||||
}
|
||||
|
||||
export function createStructure(settings: ApiSettings, payload: StructureCreatePayload): Promise<OrganizationStructureItem> {
|
||||
return post(settings, "/api/v1/organizations/structures", payload);
|
||||
}
|
||||
|
||||
export function patchStructure(settings: ApiSettings, id: string, payload: Partial<StructureCreatePayload>): Promise<OrganizationStructureItem> {
|
||||
return patch(settings, `/api/v1/organizations/structures/${encodeURIComponent(id)}`, payload);
|
||||
}
|
||||
|
||||
export function createRelationType(settings: ApiSettings, payload: RelationTypeCreatePayload): Promise<OrganizationRelationTypeItem> {
|
||||
return post(settings, "/api/v1/organizations/relation-types", payload);
|
||||
}
|
||||
|
||||
export function patchRelationType(settings: ApiSettings, id: string, payload: Partial<RelationTypeCreatePayload>): Promise<OrganizationRelationTypeItem> {
|
||||
return patch(settings, `/api/v1/organizations/relation-types/${encodeURIComponent(id)}`, payload);
|
||||
}
|
||||
|
||||
export function createUnit(settings: ApiSettings, payload: UnitCreatePayload): Promise<OrganizationUnitItem> {
|
||||
return post(settings, "/api/v1/organizations/units", payload);
|
||||
}
|
||||
|
||||
export function patchUnit(settings: ApiSettings, id: string, payload: Partial<UnitCreatePayload>): Promise<OrganizationUnitItem> {
|
||||
return patch(settings, `/api/v1/organizations/units/${encodeURIComponent(id)}`, payload);
|
||||
}
|
||||
|
||||
export function createRelation(settings: ApiSettings, payload: RelationCreatePayload): Promise<OrganizationRelationItem> {
|
||||
return post(settings, "/api/v1/organizations/relations", payload);
|
||||
}
|
||||
|
||||
export function patchRelation(settings: ApiSettings, id: string, payload: Partial<RelationCreatePayload>): Promise<OrganizationRelationItem> {
|
||||
return patch(settings, `/api/v1/organizations/relations/${encodeURIComponent(id)}`, payload);
|
||||
}
|
||||
|
||||
export function createFunctionType(settings: ApiSettings, payload: FunctionTypeCreatePayload): Promise<OrganizationFunctionTypeItem> {
|
||||
return post(settings, "/api/v1/organizations/function-types", payload);
|
||||
}
|
||||
|
||||
export function patchFunctionType(settings: ApiSettings, id: string, payload: Partial<FunctionTypeCreatePayload>): Promise<OrganizationFunctionTypeItem> {
|
||||
return patch(settings, `/api/v1/organizations/function-types/${encodeURIComponent(id)}`, payload);
|
||||
}
|
||||
|
||||
export function createFunction(settings: ApiSettings, payload: FunctionCreatePayload): Promise<OrganizationFunctionItem> {
|
||||
return post(settings, "/api/v1/organizations/functions", payload);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
18
webui/src/features/organizations/OrganizationsAdminPanel.tsx
Normal file
18
webui/src/features/organizations/OrganizationsAdminPanel.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { ApiSettings, AuthInfo } from "@govoplan/core-webui";
|
||||
import OrganizationsPage, { type OrganizationSection } from "./OrganizationsPage";
|
||||
|
||||
const MODEL_SECTIONS: OrganizationSection[] = ["model"];
|
||||
|
||||
export default function OrganizationsAdminPanel({ settings, auth }: { settings: ApiSettings; auth: AuthInfo }) {
|
||||
return (
|
||||
<OrganizationsPage
|
||||
settings={settings}
|
||||
auth={auth}
|
||||
mode="admin"
|
||||
initialSection="model"
|
||||
availableSections={MODEL_SECTIONS}
|
||||
title="i18n:govoplan-organizations.organization_model.4f924c0e"
|
||||
description="i18n:govoplan-organizations.organization_model_admin_description.35dc9f10"
|
||||
/>
|
||||
);
|
||||
}
|
||||
974
webui/src/features/organizations/OrganizationsPage.tsx
Normal file
974
webui/src/features/organizations/OrganizationsPage.tsx
Normal file
@@ -0,0 +1,974 @@
|
||||
import { useCallback, useEffect, useMemo, useState, type FormEvent } from "react";
|
||||
import { RefreshCw, Save, XCircle } from "lucide-react";
|
||||
import {
|
||||
ApiError,
|
||||
Button,
|
||||
Card,
|
||||
DataGrid,
|
||||
DismissibleAlert,
|
||||
FormField,
|
||||
LoadingFrame,
|
||||
ModuleSubnav,
|
||||
PageTitle,
|
||||
StatusBadge,
|
||||
hasScope,
|
||||
useUnsavedDraftGuard,
|
||||
type ApiSettings,
|
||||
type AuthInfo,
|
||||
type DataGridColumn,
|
||||
type ModuleSubnavGroup
|
||||
} from "@govoplan/core-webui";
|
||||
import {
|
||||
createFunction,
|
||||
createFunctionAssignment,
|
||||
createFunctionType,
|
||||
createRelation,
|
||||
createRelationType,
|
||||
createStructure,
|
||||
createUnit,
|
||||
createUnitType,
|
||||
getOrganizationModel,
|
||||
patchFunction,
|
||||
patchFunctionAssignment,
|
||||
patchFunctionType,
|
||||
patchRelation,
|
||||
patchRelationType,
|
||||
patchStructure,
|
||||
patchUnit,
|
||||
patchUnitType,
|
||||
type FunctionAssignmentCreatePayload,
|
||||
type FunctionCreatePayload,
|
||||
type FunctionTypeCreatePayload,
|
||||
type OrganizationFunctionAssignmentItem,
|
||||
type OrganizationFunctionItem,
|
||||
type OrganizationFunctionTypeItem,
|
||||
type OrganizationModel,
|
||||
type OrganizationRelationItem,
|
||||
type OrganizationRelationTypeItem,
|
||||
type OrganizationStructureItem,
|
||||
type OrganizationStructureKind,
|
||||
type OrganizationUnitItem,
|
||||
type OrganizationUnitTypeItem,
|
||||
type RelationCreatePayload,
|
||||
type RelationTypeCreatePayload,
|
||||
type SluggedCreatePayload,
|
||||
type StructureCreatePayload,
|
||||
type UnitCreatePayload
|
||||
} from "../../api/organizations";
|
||||
|
||||
export type OrganizationSection = "model" | "units" | "relations" | "functions" | "assignments";
|
||||
type OrganizationsPageMode = "workspace" | "admin";
|
||||
|
||||
type OrganizationsPageProps = {
|
||||
settings: ApiSettings;
|
||||
auth: AuthInfo;
|
||||
mode?: OrganizationsPageMode;
|
||||
initialSection?: OrganizationSection;
|
||||
availableSections?: OrganizationSection[];
|
||||
title?: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
type SluggedDraft = {
|
||||
name: string;
|
||||
slug: string;
|
||||
description: string;
|
||||
is_active: boolean;
|
||||
};
|
||||
|
||||
type StructureDraft = SluggedDraft & {
|
||||
structure_kind: OrganizationStructureKind;
|
||||
};
|
||||
|
||||
type RelationTypeDraft = SluggedDraft & {
|
||||
structure_id: string;
|
||||
source_unit_type_id: string;
|
||||
target_unit_type_id: string;
|
||||
is_hierarchical: boolean;
|
||||
allow_cycles: boolean;
|
||||
};
|
||||
|
||||
type UnitDraft = SluggedDraft & {
|
||||
unit_type_id: string;
|
||||
parent_id: string;
|
||||
};
|
||||
|
||||
type RelationDraft = {
|
||||
structure_id: string;
|
||||
relation_type_id: string;
|
||||
source_unit_id: string;
|
||||
target_unit_id: string;
|
||||
is_active: boolean;
|
||||
};
|
||||
|
||||
type FunctionTypeDraft = SluggedDraft & {
|
||||
organization_unit_type_id: string;
|
||||
delegable: boolean;
|
||||
act_in_place_allowed: boolean;
|
||||
};
|
||||
|
||||
type FunctionDraft = SluggedDraft & {
|
||||
organization_unit_id: string;
|
||||
function_type_id: string;
|
||||
delegable: boolean;
|
||||
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;
|
||||
};
|
||||
|
||||
const EMPTY_MODEL: OrganizationModel = {
|
||||
unit_types: [],
|
||||
structures: [],
|
||||
relation_types: [],
|
||||
units: [],
|
||||
relations: [],
|
||||
function_types: [],
|
||||
functions: [],
|
||||
function_assignments: []
|
||||
};
|
||||
|
||||
const SECTION_GROUPS: ModuleSubnavGroup<OrganizationSection>[] = [
|
||||
{
|
||||
title: "i18n:govoplan-organizations.organizations.220edf64",
|
||||
items: [
|
||||
{ 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" }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const STRUCTURE_KINDS: Array<{ value: OrganizationStructureKind; label: string }> = [
|
||||
{ value: "hierarchy", label: "i18n:govoplan-organizations.hierarchy.74797f37" },
|
||||
{ value: "network", label: "i18n:govoplan-organizations.network.a24d31c3" },
|
||||
{ value: "membership", label: "i18n:govoplan-organizations.membership.4531d86d" },
|
||||
{ value: "classification", label: "i18n:govoplan-organizations.classification.3e9f6c3a" }
|
||||
];
|
||||
|
||||
const ALL_SECTIONS: OrganizationSection[] = ["model", "units", "relations", "functions", "assignments"];
|
||||
|
||||
function emptySluggedDraft(): SluggedDraft {
|
||||
return { name: "", slug: "", description: "", is_active: true };
|
||||
}
|
||||
|
||||
function emptyStructureDraft(): StructureDraft {
|
||||
return { ...emptySluggedDraft(), structure_kind: "hierarchy" };
|
||||
}
|
||||
|
||||
function emptyRelationTypeDraft(): RelationTypeDraft {
|
||||
return {
|
||||
...emptySluggedDraft(),
|
||||
structure_id: "",
|
||||
source_unit_type_id: "",
|
||||
target_unit_type_id: "",
|
||||
is_hierarchical: true,
|
||||
allow_cycles: false
|
||||
};
|
||||
}
|
||||
|
||||
function emptyUnitDraft(): UnitDraft {
|
||||
return { ...emptySluggedDraft(), unit_type_id: "", parent_id: "" };
|
||||
}
|
||||
|
||||
function emptyRelationDraft(): RelationDraft {
|
||||
return { structure_id: "", relation_type_id: "", source_unit_id: "", target_unit_id: "", is_active: true };
|
||||
}
|
||||
|
||||
function emptyFunctionTypeDraft(): FunctionTypeDraft {
|
||||
return { ...emptySluggedDraft(), organization_unit_type_id: "", delegable: false, act_in_place_allowed: false };
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function sluggedPayload(draft: SluggedDraft): SluggedCreatePayload {
|
||||
return {
|
||||
name: draft.name.trim(),
|
||||
slug: textOrNull(draft.slug),
|
||||
description: textOrNull(draft.description),
|
||||
is_active: draft.is_active,
|
||||
settings: {}
|
||||
};
|
||||
}
|
||||
|
||||
function isSluggedDirty(draft: SluggedDraft): boolean {
|
||||
return draft.name.trim() !== "" || draft.slug.trim() !== "" || draft.description.trim() !== "" || !draft.is_active;
|
||||
}
|
||||
|
||||
function isRelationTypeDirty(draft: RelationTypeDraft): boolean {
|
||||
return isSluggedDirty(draft) || Boolean(draft.structure_id || draft.source_unit_type_id || draft.target_unit_type_id) || !draft.is_hierarchical || draft.allow_cycles;
|
||||
}
|
||||
|
||||
function isUnitDirty(draft: UnitDraft): boolean {
|
||||
return isSluggedDirty(draft) || Boolean(draft.unit_type_id || draft.parent_id);
|
||||
}
|
||||
|
||||
function isRelationDirty(draft: RelationDraft): boolean {
|
||||
return Boolean(draft.structure_id || draft.relation_type_id || draft.source_unit_id || draft.target_unit_id) || !draft.is_active;
|
||||
}
|
||||
|
||||
function isFunctionTypeDirty(draft: FunctionTypeDraft): boolean {
|
||||
return isSluggedDirty(draft) || Boolean(draft.organization_unit_type_id) || draft.delegable || draft.act_in_place_allowed;
|
||||
}
|
||||
|
||||
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]));
|
||||
}
|
||||
|
||||
function sectionGroupsFor(sections: ReadonlySet<OrganizationSection>): ModuleSubnavGroup<OrganizationSection>[] {
|
||||
return SECTION_GROUPS.map((group) => ({
|
||||
...group,
|
||||
items: group.items.filter((item) => sections.has(item.id))
|
||||
})).filter((group) => group.items.length > 0);
|
||||
}
|
||||
|
||||
function apiErrorMessage(error: unknown): string {
|
||||
if (error instanceof ApiError) return error.message;
|
||||
if (error instanceof Error) return error.message;
|
||||
return String(error);
|
||||
}
|
||||
|
||||
function optionalLabel(label: string | null | undefined): JSX.Element {
|
||||
return label ? <span>{label}</span> : <span className="organizations-empty">i18n:govoplan-organizations.none.334c4a4c</span>;
|
||||
}
|
||||
|
||||
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 ActiveToggleButton({ item, disabled, onToggle }: { item: ActiveItem; disabled: boolean; onToggle: () => void }) {
|
||||
const label = item.is_active ? "i18n:govoplan-organizations.deactivate.46ab070d" : "i18n:govoplan-organizations.reactivate.58f2855d";
|
||||
return (
|
||||
<Button type="button" variant={item.is_active ? "secondary" : "primary"} disabled={disabled} onClick={onToggle} title={label}>
|
||||
{label}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function SluggedFields({
|
||||
draft,
|
||||
onChange,
|
||||
disabled
|
||||
}: {
|
||||
draft: SluggedDraft;
|
||||
onChange: (next: SluggedDraft) => void;
|
||||
disabled: boolean;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<FormField label="i18n:govoplan-organizations.name.709a2322">
|
||||
<input value={draft.name} disabled={disabled} onChange={(event) => onChange({ ...draft, name: event.target.value })} />
|
||||
</FormField>
|
||||
<FormField label="i18n:govoplan-organizations.slug.094da9b9">
|
||||
<input value={draft.slug} disabled={disabled} onChange={(event) => onChange({ ...draft, slug: event.target.value })} />
|
||||
</FormField>
|
||||
<div className="wide">
|
||||
<FormField label="i18n:govoplan-organizations.description.55f8ebc8">
|
||||
<textarea rows={3} value={draft.description} disabled={disabled} onChange={(event) => onChange({ ...draft, description: event.target.value })} />
|
||||
</FormField>
|
||||
</div>
|
||||
<label className="organizations-inline-check wide">
|
||||
<input type="checkbox" checked={draft.is_active} disabled={disabled} onChange={(event) => onChange({ ...draft, is_active: event.target.checked })} />
|
||||
<span>i18n:govoplan-organizations.active.a733b809</span>
|
||||
</label>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function OrganizationsPage({
|
||||
settings,
|
||||
auth,
|
||||
mode = "workspace",
|
||||
initialSection = "model",
|
||||
availableSections = ALL_SECTIONS,
|
||||
title = "i18n:govoplan-organizations.organizations.220edf64",
|
||||
description = "i18n:govoplan-organizations.organizations_intro.4e67c4bb"
|
||||
}: OrganizationsPageProps) {
|
||||
const sectionSet = useMemo(() => new Set(availableSections.length ? availableSections : ALL_SECTIONS), [availableSections]);
|
||||
const firstAvailableSection = availableSections.find((section) => sectionSet.has(section)) ?? "model";
|
||||
const [active, setActive] = useState<OrganizationSection>(sectionSet.has(initialSection) ? initialSection : firstAvailableSection);
|
||||
const visibleSectionGroups = useMemo(() => sectionGroupsFor(sectionSet), [sectionSet]);
|
||||
const [model, setModel] = useState<OrganizationModel>(EMPTY_MODEL);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [success, setSuccess] = useState("");
|
||||
|
||||
const [unitTypeDraft, setUnitTypeDraft] = useState<SluggedDraft>(() => emptySluggedDraft());
|
||||
const [structureDraft, setStructureDraft] = useState<StructureDraft>(() => emptyStructureDraft());
|
||||
const [relationTypeDraft, setRelationTypeDraft] = useState<RelationTypeDraft>(() => emptyRelationTypeDraft());
|
||||
const [unitDraft, setUnitDraft] = useState<UnitDraft>(() => emptyUnitDraft());
|
||||
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 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]);
|
||||
const relationTypeById = useMemo(() => mapById(model.relation_types), [model.relation_types]);
|
||||
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 hasDirtyDraft = isSluggedDirty(unitTypeDraft) ||
|
||||
isSluggedDirty(structureDraft) ||
|
||||
isRelationTypeDirty(relationTypeDraft) ||
|
||||
isUnitDirty(unitDraft) ||
|
||||
isRelationDirty(relationDraft) ||
|
||||
isFunctionTypeDirty(functionTypeDraft) ||
|
||||
isFunctionDirty(functionDraft) ||
|
||||
isAssignmentDirty(assignmentDraft);
|
||||
|
||||
const loadModel = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
try {
|
||||
setModel(await getOrganizationModel(settings));
|
||||
} catch (caught) {
|
||||
setError(apiErrorMessage(caught));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [settings]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadModel();
|
||||
}, [loadModel]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!sectionSet.has(active)) setActive(firstAvailableSection);
|
||||
}, [active, firstAvailableSection, sectionSet]);
|
||||
|
||||
const runAction = useCallback(async (action: () => Promise<unknown>, successMessage = ""): Promise<boolean> => {
|
||||
setBusy(true);
|
||||
setError("");
|
||||
setSuccess("");
|
||||
try {
|
||||
await action();
|
||||
if (successMessage) setSuccess(successMessage);
|
||||
await loadModel();
|
||||
return true;
|
||||
} catch (caught) {
|
||||
setError(apiErrorMessage(caught));
|
||||
return false;
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}, [loadModel]);
|
||||
|
||||
const discardDrafts = useCallback(() => {
|
||||
setUnitTypeDraft(emptySluggedDraft());
|
||||
setStructureDraft(emptyStructureDraft());
|
||||
setRelationTypeDraft(emptyRelationTypeDraft());
|
||||
setUnitDraft(emptyUnitDraft());
|
||||
setRelationDraft(emptyRelationDraft());
|
||||
setFunctionTypeDraft(emptyFunctionTypeDraft());
|
||||
setFunctionDraft(emptyFunctionDraft());
|
||||
setAssignmentDraft(emptyAssignmentDraft());
|
||||
}, []);
|
||||
|
||||
function rejectMissingName(): Promise<boolean> {
|
||||
setError("i18n:govoplan-organizations.name_is_required.27cd3782");
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
function rejectMissingWritePermission(): Promise<boolean> {
|
||||
setError("i18n:govoplan-organizations.write_permission_required.8b09fd67");
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
const submitUnitType = useCallback(async (event?: FormEvent): Promise<boolean> => {
|
||||
event?.preventDefault();
|
||||
if (!canWriteModel) return rejectMissingWritePermission();
|
||||
if (!unitTypeDraft.name.trim()) return rejectMissingName();
|
||||
const ok = await runAction(() => createUnitType(settings, sluggedPayload(unitTypeDraft)), "i18n:govoplan-organizations.unit_type_added.e017dc8c");
|
||||
if (ok) setUnitTypeDraft(emptySluggedDraft());
|
||||
return ok;
|
||||
}, [canWriteModel, runAction, settings, unitTypeDraft]);
|
||||
|
||||
const submitStructure = useCallback(async (event?: FormEvent): Promise<boolean> => {
|
||||
event?.preventDefault();
|
||||
if (!canWriteModel) return rejectMissingWritePermission();
|
||||
if (!structureDraft.name.trim()) return rejectMissingName();
|
||||
const payload: StructureCreatePayload = { ...sluggedPayload(structureDraft), structure_kind: structureDraft.structure_kind };
|
||||
const ok = await runAction(() => createStructure(settings, payload), "i18n:govoplan-organizations.structure_added.f6cf8e74");
|
||||
if (ok) setStructureDraft(emptyStructureDraft());
|
||||
return ok;
|
||||
}, [canWriteModel, runAction, settings, structureDraft]);
|
||||
|
||||
const submitRelationType = useCallback(async (event?: FormEvent): Promise<boolean> => {
|
||||
event?.preventDefault();
|
||||
if (!canWriteModel) return rejectMissingWritePermission();
|
||||
if (!relationTypeDraft.name.trim()) return rejectMissingName();
|
||||
const payload: RelationTypeCreatePayload = {
|
||||
...sluggedPayload(relationTypeDraft),
|
||||
structure_id: textOrNull(relationTypeDraft.structure_id),
|
||||
source_unit_type_id: textOrNull(relationTypeDraft.source_unit_type_id),
|
||||
target_unit_type_id: textOrNull(relationTypeDraft.target_unit_type_id),
|
||||
is_hierarchical: relationTypeDraft.is_hierarchical,
|
||||
allow_cycles: relationTypeDraft.allow_cycles
|
||||
};
|
||||
const ok = await runAction(() => createRelationType(settings, payload), "i18n:govoplan-organizations.relation_type_added.6f1edff1");
|
||||
if (ok) setRelationTypeDraft(emptyRelationTypeDraft());
|
||||
return ok;
|
||||
}, [canWriteModel, relationTypeDraft, runAction, settings]);
|
||||
|
||||
const submitUnit = useCallback(async (event?: FormEvent): Promise<boolean> => {
|
||||
event?.preventDefault();
|
||||
if (!canWriteUnits) return rejectMissingWritePermission();
|
||||
if (!unitDraft.name.trim()) return rejectMissingName();
|
||||
const payload: UnitCreatePayload = {
|
||||
...sluggedPayload(unitDraft),
|
||||
unit_type_id: textOrNull(unitDraft.unit_type_id),
|
||||
parent_id: textOrNull(unitDraft.parent_id)
|
||||
};
|
||||
const ok = await runAction(() => createUnit(settings, payload), "i18n:govoplan-organizations.unit_added.760a8512");
|
||||
if (ok) setUnitDraft(emptyUnitDraft());
|
||||
return ok;
|
||||
}, [canWriteUnits, runAction, settings, unitDraft]);
|
||||
|
||||
const submitRelation = useCallback(async (event?: FormEvent): Promise<boolean> => {
|
||||
event?.preventDefault();
|
||||
if (!canWriteUnits) return rejectMissingWritePermission();
|
||||
if (!relationDraft.structure_id || !relationDraft.relation_type_id || !relationDraft.source_unit_id || !relationDraft.target_unit_id) {
|
||||
setError("i18n:govoplan-organizations.select_structure.c10f551c");
|
||||
return false;
|
||||
}
|
||||
const payload: RelationCreatePayload = { ...relationDraft, settings: {} };
|
||||
const ok = await runAction(() => createRelation(settings, payload), "i18n:govoplan-organizations.relation_added.ca90461a");
|
||||
if (ok) setRelationDraft(emptyRelationDraft());
|
||||
return ok;
|
||||
}, [canWriteUnits, relationDraft, runAction, settings]);
|
||||
|
||||
const submitFunctionType = useCallback(async (event?: FormEvent): Promise<boolean> => {
|
||||
event?.preventDefault();
|
||||
if (!canWriteModel) return rejectMissingWritePermission();
|
||||
if (!functionTypeDraft.name.trim()) return rejectMissingName();
|
||||
const payload: FunctionTypeCreatePayload = {
|
||||
...sluggedPayload(functionTypeDraft),
|
||||
organization_unit_type_id: textOrNull(functionTypeDraft.organization_unit_type_id),
|
||||
delegable: functionTypeDraft.delegable,
|
||||
act_in_place_allowed: functionTypeDraft.act_in_place_allowed
|
||||
};
|
||||
const ok = await runAction(() => createFunctionType(settings, payload), "i18n:govoplan-organizations.function_type_added.2cd9e899");
|
||||
if (ok) setFunctionTypeDraft(emptyFunctionTypeDraft());
|
||||
return ok;
|
||||
}, [canWriteModel, functionTypeDraft, runAction, settings]);
|
||||
|
||||
const submitFunction = useCallback(async (event?: FormEvent): Promise<boolean> => {
|
||||
event?.preventDefault();
|
||||
if (!canWriteFunctions) return rejectMissingWritePermission();
|
||||
if (!functionDraft.name.trim()) return rejectMissingName();
|
||||
if (!functionDraft.organization_unit_id) {
|
||||
setError("i18n:govoplan-organizations.select_unit.013bf13a");
|
||||
return false;
|
||||
}
|
||||
const payload: FunctionCreatePayload = {
|
||||
...sluggedPayload(functionDraft),
|
||||
organization_unit_id: functionDraft.organization_unit_id,
|
||||
function_type_id: textOrNull(functionDraft.function_type_id),
|
||||
delegable: functionDraft.delegable,
|
||||
act_in_place_allowed: functionDraft.act_in_place_allowed
|
||||
};
|
||||
const ok = await runAction(() => createFunction(settings, payload), "i18n:govoplan-organizations.function_added.e2266702");
|
||||
if (ok) setFunctionDraft(emptyFunctionDraft());
|
||||
return ok;
|
||||
}, [canWriteFunctions, functionDraft, runAction, settings]);
|
||||
|
||||
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(() => createFunctionAssignment(settings, payload), "i18n:govoplan-organizations.assignment_added.94263d1b");
|
||||
if (ok) setAssignmentDraft(emptyAssignmentDraft());
|
||||
return ok;
|
||||
}, [assignmentDraft, canAssignFunctions, runAction, settings]);
|
||||
|
||||
const saveDrafts = useCallback(async (): Promise<boolean> => {
|
||||
if (isSluggedDirty(unitTypeDraft) && !(await submitUnitType())) return false;
|
||||
if (isSluggedDirty(structureDraft) && !(await submitStructure())) return false;
|
||||
if (isRelationTypeDirty(relationTypeDraft) && !(await submitRelationType())) return false;
|
||||
if (isUnitDirty(unitDraft) && !(await submitUnit())) return false;
|
||||
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,
|
||||
submitRelationType,
|
||||
submitStructure,
|
||||
submitUnit,
|
||||
submitUnitType,
|
||||
unitDraft,
|
||||
unitTypeDraft
|
||||
]);
|
||||
|
||||
useUnsavedDraftGuard({
|
||||
dirty: hasDirtyDraft,
|
||||
onSave: saveDrafts,
|
||||
onDiscard: discardDrafts,
|
||||
title: "i18n:govoplan-core.unsaved_changes.29267269",
|
||||
message: "i18n:govoplan-core.this_page_has_unsaved_changes_save_them_before_l.419a9d8b"
|
||||
});
|
||||
|
||||
const toggleUnitType = useCallback((item: OrganizationUnitTypeItem) => {
|
||||
void runAction(() => patchUnitType(settings, item.id, { is_active: !item.is_active }));
|
||||
}, [runAction, settings]);
|
||||
|
||||
const toggleStructure = useCallback((item: OrganizationStructureItem) => {
|
||||
void runAction(() => patchStructure(settings, item.id, { is_active: !item.is_active }));
|
||||
}, [runAction, settings]);
|
||||
|
||||
const toggleRelationType = useCallback((item: OrganizationRelationTypeItem) => {
|
||||
void runAction(() => patchRelationType(settings, item.id, { is_active: !item.is_active }));
|
||||
}, [runAction, settings]);
|
||||
|
||||
const toggleUnit = useCallback((item: OrganizationUnitItem) => {
|
||||
void runAction(() => patchUnit(settings, item.id, { is_active: !item.is_active }));
|
||||
}, [runAction, settings]);
|
||||
|
||||
const toggleRelation = useCallback((item: OrganizationRelationItem) => {
|
||||
void runAction(() => patchRelation(settings, item.id, { is_active: !item.is_active }));
|
||||
}, [runAction, settings]);
|
||||
|
||||
const toggleFunctionType = useCallback((item: OrganizationFunctionTypeItem) => {
|
||||
void runAction(() => patchFunctionType(settings, item.id, { is_active: !item.is_active }));
|
||||
}, [runAction, settings]);
|
||||
|
||||
const toggleFunction = useCallback((item: OrganizationFunctionItem) => {
|
||||
void runAction(() => patchFunction(settings, item.id, { is_active: !item.is_active }));
|
||||
}, [runAction, settings]);
|
||||
|
||||
const toggleAssignment = useCallback((item: OrganizationFunctionAssignmentItem) => {
|
||||
void runAction(() => patchFunctionAssignment(settings, item.id, { is_active: !item.is_active }));
|
||||
}, [runAction, settings]);
|
||||
|
||||
const unitTypeColumns: DataGridColumn<OrganizationUnitTypeItem>[] = [
|
||||
{ id: "name", header: "i18n:govoplan-organizations.name.709a2322", minWidth: 180, sortable: true, filterable: true, value: (row) => row.name },
|
||||
{ id: "slug", header: "i18n:govoplan-organizations.slug.094da9b9", width: 160, sortable: true, filterable: true, value: (row) => row.slug },
|
||||
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
|
||||
{ id: "description", header: "i18n:govoplan-organizations.description.55f8ebc8", minWidth: 220, value: (row) => row.description ?? "" },
|
||||
{ id: "actions", header: "", width: 130, sticky: "end", render: (row) => <ActiveToggleButton item={row} disabled={!canWriteModel || busy} onToggle={() => toggleUnitType(row)} /> }
|
||||
];
|
||||
|
||||
const structureColumns: DataGridColumn<OrganizationStructureItem>[] = [
|
||||
{ id: "name", header: "i18n:govoplan-organizations.name.709a2322", minWidth: 180, sortable: true, filterable: true, value: (row) => row.name },
|
||||
{ id: "kind", header: "i18n:govoplan-organizations.kind.794c9d9c", width: 140, sortable: true, filterable: true, value: (row) => row.structure_kind },
|
||||
{ id: "slug", header: "i18n:govoplan-organizations.slug.094da9b9", width: 160, value: (row) => row.slug },
|
||||
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
|
||||
{ id: "actions", header: "", width: 130, sticky: "end", render: (row) => <ActiveToggleButton item={row} disabled={!canWriteModel || busy} onToggle={() => toggleStructure(row)} /> }
|
||||
];
|
||||
|
||||
const relationTypeColumns: DataGridColumn<OrganizationRelationTypeItem>[] = [
|
||||
{ id: "name", header: "i18n:govoplan-organizations.name.709a2322", minWidth: 180, value: (row) => row.name, sortable: true, filterable: true },
|
||||
{ id: "structure", header: "i18n:govoplan-organizations.structure.7732fb0b", minWidth: 170, render: (row) => optionalLabel(structureById.get(row.structure_id || "")?.name) },
|
||||
{ id: "source", header: "i18n:govoplan-organizations.source_unit.2fbd8baa", minWidth: 170, render: (row) => optionalLabel(unitTypeById.get(row.source_unit_type_id || "")?.name) },
|
||||
{ id: "target", header: "i18n:govoplan-organizations.target_unit.a1507d86", minWidth: 170, render: (row) => optionalLabel(unitTypeById.get(row.target_unit_type_id || "")?.name) },
|
||||
{ id: "hierarchical", header: "i18n:govoplan-organizations.hierarchical.8964f313", width: 130, render: (row) => activeStatus(row.is_hierarchical) },
|
||||
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
|
||||
{ id: "actions", header: "", width: 130, sticky: "end", render: (row) => <ActiveToggleButton item={row} disabled={!canWriteModel || busy} onToggle={() => toggleRelationType(row)} /> }
|
||||
];
|
||||
|
||||
const unitColumns: DataGridColumn<OrganizationUnitItem>[] = [
|
||||
{ id: "name", header: "i18n:govoplan-organizations.name.709a2322", minWidth: 190, sortable: true, filterable: true, value: (row) => row.name },
|
||||
{ id: "type", header: "i18n:govoplan-organizations.type.599dcce2", minWidth: 160, render: (row) => optionalLabel(unitTypeById.get(row.unit_type_id || "")?.name) },
|
||||
{ id: "parent", header: "i18n:govoplan-organizations.parent_unit.1986c35e", minWidth: 180, render: (row) => optionalLabel(unitById.get(row.parent_id || "")?.name) },
|
||||
{ id: "slug", header: "i18n:govoplan-organizations.slug.094da9b9", width: 160, value: (row) => row.slug },
|
||||
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
|
||||
{ id: "actions", header: "", width: 130, sticky: "end", render: (row) => <ActiveToggleButton item={row} disabled={!canWriteUnits || busy} onToggle={() => toggleUnit(row)} /> }
|
||||
];
|
||||
|
||||
const relationColumns: DataGridColumn<OrganizationRelationItem>[] = [
|
||||
{ id: "structure", header: "i18n:govoplan-organizations.structure.7732fb0b", minWidth: 170, render: (row) => optionalLabel(structureById.get(row.structure_id)?.name) },
|
||||
{ id: "type", header: "i18n:govoplan-organizations.relation_type.d0aee2e7", minWidth: 170, render: (row) => optionalLabel(relationTypeById.get(row.relation_type_id)?.name) },
|
||||
{ id: "source", header: "i18n:govoplan-organizations.source_unit.2fbd8baa", minWidth: 190, render: (row) => optionalLabel(unitById.get(row.source_unit_id)?.name) },
|
||||
{ id: "target", header: "i18n:govoplan-organizations.target_unit.a1507d86", minWidth: 190, render: (row) => optionalLabel(unitById.get(row.target_unit_id)?.name) },
|
||||
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
|
||||
{ id: "actions", header: "", width: 130, sticky: "end", render: (row) => <ActiveToggleButton item={row} disabled={!canWriteUnits || busy} onToggle={() => toggleRelation(row)} /> }
|
||||
];
|
||||
|
||||
const functionTypeColumns: DataGridColumn<OrganizationFunctionTypeItem>[] = [
|
||||
{ id: "name", header: "i18n:govoplan-organizations.name.709a2322", minWidth: 180, sortable: true, filterable: true, value: (row) => row.name },
|
||||
{ id: "unitType", header: "i18n:govoplan-organizations.unit_type.9e62810d", minWidth: 180, render: (row) => optionalLabel(unitTypeById.get(row.organization_unit_type_id || "")?.name) },
|
||||
{ id: "delegable", header: "i18n:govoplan-organizations.delegable.b4f0137d", width: 120, render: (row) => activeStatus(row.delegable) },
|
||||
{ id: "actInPlace", header: "i18n:govoplan-organizations.act_in_place.49b942bd", width: 140, render: (row) => activeStatus(row.act_in_place_allowed) },
|
||||
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
|
||||
{ id: "actions", header: "", width: 130, sticky: "end", render: (row) => <ActiveToggleButton item={row} disabled={!canWriteModel || busy} onToggle={() => toggleFunctionType(row)} /> }
|
||||
];
|
||||
|
||||
const functionColumns: DataGridColumn<OrganizationFunctionItem>[] = [
|
||||
{ id: "name", header: "i18n:govoplan-organizations.name.709a2322", minWidth: 180, sortable: true, filterable: true, value: (row) => row.name },
|
||||
{ id: "unit", header: "i18n:govoplan-organizations.unit.8fe4d595", minWidth: 190, render: (row) => optionalLabel(unitById.get(row.organization_unit_id)?.name) },
|
||||
{ id: "type", header: "i18n:govoplan-organizations.function_type.501fe7c0", minWidth: 180, render: (row) => optionalLabel(functionTypeById.get(row.function_type_id || "")?.name) },
|
||||
{ id: "delegable", header: "i18n:govoplan-organizations.delegable.b4f0137d", width: 120, render: (row) => activeStatus(row.delegable) },
|
||||
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
|
||||
{ id: "actions", header: "", width: 130, sticky: "end", render: (row) => <ActiveToggleButton item={row} disabled={!canWriteFunctions || busy} onToggle={() => toggleFunction(row)} /> }
|
||||
];
|
||||
|
||||
const assignmentColumns: DataGridColumn<OrganizationFunctionAssignmentItem>[] = [
|
||||
{ id: "identity", header: "i18n:govoplan-organizations.identity_id.b6ef5010", minWidth: 220, render: (row) => <span className="organizations-id">{row.identity_id}</span> },
|
||||
{ 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: 130, sticky: "end", render: (row) => <ActiveToggleButton item={row} disabled={!canAssignFunctions || busy} onToggle={() => toggleAssignment(row)} /> }
|
||||
];
|
||||
|
||||
const canWriteActive = active === "model" ? canWriteModel : active === "units" || active === "relations" ? canWriteUnits : active === "functions" ? canWriteFunctions : canAssignFunctions;
|
||||
|
||||
const content = (
|
||||
<div className={`${mode === "workspace" ? "content-pad " : ""}organizations-page ${mode === "admin" ? "organizations-admin-page" : ""}`.trim()}>
|
||||
<div className="page-heading split organizations-heading">
|
||||
<div>
|
||||
<PageTitle loading={loading}>{title}</PageTitle>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
<div className="organizations-toolbar">
|
||||
<Button type="button" onClick={() => void loadModel()} disabled={loading || busy} title="i18n:govoplan-organizations.reload.cce71553">
|
||||
<RefreshCw size={16} aria-hidden="true" /> i18n:govoplan-organizations.reload.cce71553
|
||||
</Button>
|
||||
{hasDirtyDraft && (
|
||||
<>
|
||||
<Button type="button" onClick={() => void saveDrafts()} disabled={busy} title="i18n:govoplan-organizations.save_drafts.606f9401">
|
||||
<Save size={16} aria-hidden="true" /> i18n:govoplan-organizations.save_drafts.606f9401
|
||||
</Button>
|
||||
<Button type="button" variant="ghost" onClick={discardDrafts} disabled={busy} title="i18n:govoplan-organizations.discard_organization_drafts.766d5f46">
|
||||
<XCircle size={16} aria-hidden="true" /> i18n:govoplan-organizations.discard_organization_drafts.766d5f46
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
|
||||
{success && !error && <DismissibleAlert tone="success" resetKey={success} floating>{success}</DismissibleAlert>}
|
||||
{!canWriteActive && <DismissibleAlert tone="warning" dismissible={false}>i18n:govoplan-organizations.write_permission_required.8b09fd67</DismissibleAlert>}
|
||||
|
||||
<LoadingFrame loading={loading || busy} label="i18n:govoplan-organizations.loading_organization_model.846aa317">
|
||||
{active === "model" && renderModelSection()}
|
||||
{active === "units" && renderUnitsSection()}
|
||||
{active === "relations" && renderRelationsSection()}
|
||||
{active === "functions" && renderFunctionsSection()}
|
||||
{active === "assignments" && renderAssignmentsSection()}
|
||||
</LoadingFrame>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (mode === "admin") return content;
|
||||
|
||||
return (
|
||||
<div className="workspace organizations-workspace">
|
||||
<ModuleSubnav active={active} groups={visibleSectionGroups} onSelect={setActive} />
|
||||
<main className="workspace-content">
|
||||
{content}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
||||
function renderModelSection() {
|
||||
return (
|
||||
<div className="organizations-table-stack">
|
||||
<div className="organizations-section-grid">
|
||||
<Card title="i18n:govoplan-organizations.unit_types.c7afc174">
|
||||
<form className="organizations-form-grid" onSubmit={(event) => void submitUnitType(event)}>
|
||||
<SluggedFields draft={unitTypeDraft} onChange={setUnitTypeDraft} disabled={!canWriteModel || busy} />
|
||||
<Button className="wide" type="submit" variant="primary" disabled={!canWriteModel || busy}>i18n:govoplan-organizations.add_unit_type.58f2c05a</Button>
|
||||
</form>
|
||||
</Card>
|
||||
<Card title="i18n:govoplan-organizations.unit_types.c7afc174">
|
||||
<DataGrid id="organizations-unit-types" rows={model.unit_types} columns={unitTypeColumns} getRowKey={(row) => row.id} emptyText="i18n:govoplan-organizations.no_unit_types_found.c81fb2a7" initialFit="container" />
|
||||
</Card>
|
||||
</div>
|
||||
<div className="organizations-section-grid">
|
||||
<Card title="i18n:govoplan-organizations.structures.f9b7f3b4">
|
||||
<form className="organizations-form-grid" onSubmit={(event) => void submitStructure(event)}>
|
||||
<SluggedFields draft={structureDraft} onChange={setStructureDraft} disabled={!canWriteModel || busy} />
|
||||
<FormField label="i18n:govoplan-organizations.kind.794c9d9c">
|
||||
<select value={structureDraft.structure_kind} disabled={!canWriteModel || busy} onChange={(event) => setStructureDraft({ ...structureDraft, structure_kind: event.target.value as OrganizationStructureKind })}>
|
||||
{STRUCTURE_KINDS.map((item) => <option key={item.value} value={item.value}>{item.label}</option>)}
|
||||
</select>
|
||||
</FormField>
|
||||
<Button className="wide" type="submit" variant="primary" disabled={!canWriteModel || busy}>i18n:govoplan-organizations.add_structure.b722042a</Button>
|
||||
</form>
|
||||
</Card>
|
||||
<Card title="i18n:govoplan-organizations.structures.f9b7f3b4">
|
||||
<DataGrid id="organizations-structures" rows={model.structures} columns={structureColumns} getRowKey={(row) => row.id} emptyText="i18n:govoplan-organizations.no_structures_found.20b382d0" initialFit="container" />
|
||||
</Card>
|
||||
</div>
|
||||
<div className="organizations-section-grid">
|
||||
<Card title="i18n:govoplan-organizations.relation_types.e5890528">
|
||||
<form className="organizations-form-grid" onSubmit={(event) => void submitRelationType(event)}>
|
||||
<SluggedFields draft={relationTypeDraft} onChange={setRelationTypeDraft} disabled={!canWriteModel || busy} />
|
||||
<FormField label="i18n:govoplan-organizations.structure.7732fb0b">
|
||||
<select value={relationTypeDraft.structure_id} disabled={!canWriteModel || busy} onChange={(event) => setRelationTypeDraft({ ...relationTypeDraft, structure_id: event.target.value })}>
|
||||
<option value="">i18n:govoplan-organizations.none.334c4a4c</option>
|
||||
{model.structures.map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
|
||||
</select>
|
||||
</FormField>
|
||||
<FormField label="i18n:govoplan-organizations.source_unit.2fbd8baa">
|
||||
<select value={relationTypeDraft.source_unit_type_id} disabled={!canWriteModel || busy} onChange={(event) => setRelationTypeDraft({ ...relationTypeDraft, source_unit_type_id: event.target.value })}>
|
||||
<option value="">i18n:govoplan-organizations.none.334c4a4c</option>
|
||||
{model.unit_types.map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
|
||||
</select>
|
||||
</FormField>
|
||||
<FormField label="i18n:govoplan-organizations.target_unit.a1507d86">
|
||||
<select value={relationTypeDraft.target_unit_type_id} disabled={!canWriteModel || busy} onChange={(event) => setRelationTypeDraft({ ...relationTypeDraft, target_unit_type_id: event.target.value })}>
|
||||
<option value="">i18n:govoplan-organizations.none.334c4a4c</option>
|
||||
{model.unit_types.map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
|
||||
</select>
|
||||
</FormField>
|
||||
<div className="organizations-check-list">
|
||||
<label><input type="checkbox" checked={relationTypeDraft.is_hierarchical} disabled={!canWriteModel || busy} onChange={(event) => setRelationTypeDraft({ ...relationTypeDraft, is_hierarchical: event.target.checked })} /> i18n:govoplan-organizations.hierarchical.8964f313</label>
|
||||
<label><input type="checkbox" checked={relationTypeDraft.allow_cycles} disabled={!canWriteModel || busy} onChange={(event) => setRelationTypeDraft({ ...relationTypeDraft, allow_cycles: event.target.checked })} /> i18n:govoplan-organizations.allow_cycles.31327578</label>
|
||||
</div>
|
||||
<Button className="wide" type="submit" variant="primary" disabled={!canWriteModel || busy}>i18n:govoplan-organizations.add_relation_type.2ad19d03</Button>
|
||||
</form>
|
||||
</Card>
|
||||
<Card title="i18n:govoplan-organizations.relation_types.e5890528">
|
||||
<DataGrid id="organizations-relation-types" rows={model.relation_types} columns={relationTypeColumns} getRowKey={(row) => row.id} emptyText="i18n:govoplan-organizations.no_relation_types_found.6f90bb81" initialFit="container" />
|
||||
</Card>
|
||||
</div>
|
||||
<div className="organizations-section-grid">
|
||||
<Card title="i18n:govoplan-organizations.function_types.172c01fe">
|
||||
<form className="organizations-form-grid" onSubmit={(event) => void submitFunctionType(event)}>
|
||||
<SluggedFields draft={functionTypeDraft} onChange={setFunctionTypeDraft} disabled={!canWriteModel || busy} />
|
||||
<FormField label="i18n:govoplan-organizations.unit_type.9e62810d">
|
||||
<select value={functionTypeDraft.organization_unit_type_id} disabled={!canWriteModel || busy} onChange={(event) => setFunctionTypeDraft({ ...functionTypeDraft, organization_unit_type_id: event.target.value })}>
|
||||
<option value="">i18n:govoplan-organizations.none.334c4a4c</option>
|
||||
{model.unit_types.map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
|
||||
</select>
|
||||
</FormField>
|
||||
<div className="organizations-check-list">
|
||||
<label><input type="checkbox" checked={functionTypeDraft.delegable} disabled={!canWriteModel || busy} onChange={(event) => setFunctionTypeDraft({ ...functionTypeDraft, delegable: event.target.checked })} /> i18n:govoplan-organizations.delegable.b4f0137d</label>
|
||||
<label><input type="checkbox" checked={functionTypeDraft.act_in_place_allowed} disabled={!canWriteModel || busy} onChange={(event) => setFunctionTypeDraft({ ...functionTypeDraft, act_in_place_allowed: event.target.checked })} /> i18n:govoplan-organizations.act_in_place.49b942bd</label>
|
||||
</div>
|
||||
<Button className="wide" type="submit" variant="primary" disabled={!canWriteModel || busy}>i18n:govoplan-organizations.add_function_type.90d793f1</Button>
|
||||
</form>
|
||||
</Card>
|
||||
<Card title="i18n:govoplan-organizations.function_types.172c01fe">
|
||||
<DataGrid id="organizations-function-types" rows={model.function_types} columns={functionTypeColumns} getRowKey={(row) => row.id} emptyText="i18n:govoplan-organizations.no_function_types_found.5eef31b1" initialFit="container" />
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderUnitsSection() {
|
||||
return (
|
||||
<div className="organizations-section-grid">
|
||||
<Card title="i18n:govoplan-organizations.add_unit.8fa12fb1">
|
||||
<form className="organizations-form-grid" onSubmit={(event) => void submitUnit(event)}>
|
||||
<SluggedFields draft={unitDraft} onChange={setUnitDraft} disabled={!canWriteUnits || busy} />
|
||||
<FormField label="i18n:govoplan-organizations.unit_type.9e62810d">
|
||||
<select value={unitDraft.unit_type_id} disabled={!canWriteUnits || busy} onChange={(event) => setUnitDraft({ ...unitDraft, unit_type_id: event.target.value })}>
|
||||
<option value="">i18n:govoplan-organizations.none.334c4a4c</option>
|
||||
{model.unit_types.map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
|
||||
</select>
|
||||
</FormField>
|
||||
<FormField label="i18n:govoplan-organizations.parent_unit.1986c35e">
|
||||
<select value={unitDraft.parent_id} disabled={!canWriteUnits || busy} onChange={(event) => setUnitDraft({ ...unitDraft, parent_id: event.target.value })}>
|
||||
<option value="">i18n:govoplan-organizations.none.334c4a4c</option>
|
||||
{model.units.map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
|
||||
</select>
|
||||
</FormField>
|
||||
<Button className="wide" type="submit" variant="primary" disabled={!canWriteUnits || busy}>i18n:govoplan-organizations.add_unit.8fa12fb1</Button>
|
||||
</form>
|
||||
</Card>
|
||||
<Card title="i18n:govoplan-organizations.units.e14d0d92">
|
||||
<DataGrid id="organizations-units" rows={model.units} columns={unitColumns} getRowKey={(row) => row.id} emptyText="i18n:govoplan-organizations.no_units_found.eea2dd0c" initialFit="container" />
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderRelationsSection() {
|
||||
return (
|
||||
<div className="organizations-section-grid">
|
||||
<Card title="i18n:govoplan-organizations.add_relation.6b6e67ea">
|
||||
<form className="organizations-form-grid" onSubmit={(event) => void submitRelation(event)}>
|
||||
<FormField label="i18n:govoplan-organizations.structure.7732fb0b">
|
||||
<select value={relationDraft.structure_id} disabled={!canWriteUnits || busy} onChange={(event) => setRelationDraft({ ...relationDraft, structure_id: event.target.value })}>
|
||||
<option value="">i18n:govoplan-organizations.select_structure.c10f551c</option>
|
||||
{model.structures.map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
|
||||
</select>
|
||||
</FormField>
|
||||
<FormField label="i18n:govoplan-organizations.relation_type.d0aee2e7">
|
||||
<select value={relationDraft.relation_type_id} disabled={!canWriteUnits || busy} onChange={(event) => setRelationDraft({ ...relationDraft, relation_type_id: event.target.value })}>
|
||||
<option value="">i18n:govoplan-organizations.select_relation_type.73f92478</option>
|
||||
{model.relation_types.map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
|
||||
</select>
|
||||
</FormField>
|
||||
<FormField label="i18n:govoplan-organizations.source_unit.2fbd8baa">
|
||||
<select value={relationDraft.source_unit_id} disabled={!canWriteUnits || busy} onChange={(event) => setRelationDraft({ ...relationDraft, source_unit_id: event.target.value })}>
|
||||
<option value="">i18n:govoplan-organizations.select_source_unit.9b5a29c8</option>
|
||||
{model.units.map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
|
||||
</select>
|
||||
</FormField>
|
||||
<FormField label="i18n:govoplan-organizations.target_unit.a1507d86">
|
||||
<select value={relationDraft.target_unit_id} disabled={!canWriteUnits || busy} onChange={(event) => setRelationDraft({ ...relationDraft, target_unit_id: event.target.value })}>
|
||||
<option value="">i18n:govoplan-organizations.select_target_unit.8d607541</option>
|
||||
{model.units.map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
|
||||
</select>
|
||||
</FormField>
|
||||
<label className="organizations-inline-check wide">
|
||||
<input type="checkbox" checked={relationDraft.is_active} disabled={!canWriteUnits || busy} onChange={(event) => setRelationDraft({ ...relationDraft, is_active: event.target.checked })} />
|
||||
<span>i18n:govoplan-organizations.active.a733b809</span>
|
||||
</label>
|
||||
<Button className="wide" type="submit" variant="primary" disabled={!canWriteUnits || busy}>i18n:govoplan-organizations.add_relation.6b6e67ea</Button>
|
||||
</form>
|
||||
</Card>
|
||||
<Card title="i18n:govoplan-organizations.relations.1c796711">
|
||||
<DataGrid id="organizations-relations" rows={model.relations} columns={relationColumns} getRowKey={(row) => row.id} emptyText="i18n:govoplan-organizations.no_relations_found.4e75b11d" initialFit="container" />
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderFunctionsSection() {
|
||||
return (
|
||||
<div className="organizations-section-grid">
|
||||
<Card title="i18n:govoplan-organizations.add_function.6abafee0">
|
||||
<form className="organizations-form-grid" onSubmit={(event) => void submitFunction(event)}>
|
||||
<SluggedFields draft={functionDraft} onChange={setFunctionDraft} disabled={!canWriteFunctions || busy} />
|
||||
<FormField label="i18n:govoplan-organizations.unit.8fe4d595">
|
||||
<select value={functionDraft.organization_unit_id} disabled={!canWriteFunctions || busy} onChange={(event) => setFunctionDraft({ ...functionDraft, organization_unit_id: event.target.value })}>
|
||||
<option value="">i18n:govoplan-organizations.select_unit.013bf13a</option>
|
||||
{model.units.map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
|
||||
</select>
|
||||
</FormField>
|
||||
<FormField label="i18n:govoplan-organizations.function_type.501fe7c0">
|
||||
<select value={functionDraft.function_type_id} disabled={!canWriteFunctions || busy} onChange={(event) => setFunctionDraft({ ...functionDraft, function_type_id: event.target.value })}>
|
||||
<option value="">i18n:govoplan-organizations.none.334c4a4c</option>
|
||||
{model.function_types.map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
|
||||
</select>
|
||||
</FormField>
|
||||
<div className="organizations-check-list">
|
||||
<label><input type="checkbox" checked={functionDraft.delegable} disabled={!canWriteFunctions || busy} onChange={(event) => setFunctionDraft({ ...functionDraft, delegable: event.target.checked })} /> i18n:govoplan-organizations.delegable.b4f0137d</label>
|
||||
<label><input type="checkbox" checked={functionDraft.act_in_place_allowed} disabled={!canWriteFunctions || busy} onChange={(event) => setFunctionDraft({ ...functionDraft, act_in_place_allowed: event.target.checked })} /> i18n:govoplan-organizations.act_in_place.49b942bd</label>
|
||||
</div>
|
||||
<Button className="wide" type="submit" variant="primary" disabled={!canWriteFunctions || busy}>i18n:govoplan-organizations.add_function.6abafee0</Button>
|
||||
</form>
|
||||
</Card>
|
||||
<Card title="i18n:govoplan-organizations.functions.805dc49b">
|
||||
<DataGrid id="organizations-functions" rows={model.functions} columns={functionColumns} getRowKey={(row) => row.id} emptyText="i18n:govoplan-organizations.no_functions_found.54e759a0" initialFit="container" />
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderAssignmentsSection() {
|
||||
return (
|
||||
<div className="organizations-section-grid">
|
||||
<Card title="i18n:govoplan-organizations.add_assignment.6682f5f5">
|
||||
<form className="organizations-form-grid" onSubmit={(event) => void submitAssignment(event)}>
|
||||
<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">
|
||||
<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>
|
||||
<Button className="wide" type="submit" variant="primary" disabled={!canAssignFunctions || busy}>i18n:govoplan-organizations.add_assignment.6682f5f5</Button>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
}
|
||||
184
webui/src/i18n/generatedTranslations.ts
Normal file
184
webui/src/i18n/generatedTranslations.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
import type { PlatformTranslations } from "@govoplan/core-webui";
|
||||
|
||||
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",
|
||||
"i18n:govoplan-organizations.add_relation_type.2ad19d03": "Add relation type",
|
||||
"i18n:govoplan-organizations.add_structure.b722042a": "Add structure",
|
||||
"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.assignments.278f513e": "Assignments",
|
||||
"i18n:govoplan-organizations.classification.3e9f6c3a": "Classification",
|
||||
"i18n:govoplan-organizations.concrete_model.4e9c531a": "Concrete model",
|
||||
"i18n:govoplan-organizations.deactivate.46ab070d": "Deactivate",
|
||||
"i18n:govoplan-organizations.delegable.b4f0137d": "Delegable",
|
||||
"i18n:govoplan-organizations.description.55f8ebc8": "Description",
|
||||
"i18n:govoplan-organizations.direct_assignment.2a068aac": "direct",
|
||||
"i18n:govoplan-organizations.discard_organization_drafts.766d5f46": "Discard organization drafts",
|
||||
"i18n:govoplan-organizations.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_types.172c01fe": "Function types",
|
||||
"i18n:govoplan-organizations.functions.805dc49b": "Functions",
|
||||
"i18n:govoplan-organizations.hierarchical.8964f313": "Hierarchical",
|
||||
"i18n:govoplan-organizations.hierarchy.74797f37": "Hierarchy",
|
||||
"i18n:govoplan-organizations.identity_id.b6ef5010": "Identity ID",
|
||||
"i18n:govoplan-organizations.identity_id_placeholder.298cbd85": "identity UUID",
|
||||
"i18n:govoplan-organizations.inactive.09af574c": "Inactive",
|
||||
"i18n:govoplan-organizations.kind.794c9d9c": "Kind",
|
||||
"i18n:govoplan-organizations.loading_organization_model.846aa317": "Loading organization model...",
|
||||
"i18n:govoplan-organizations.membership.4531d86d": "Membership",
|
||||
"i18n:govoplan-organizations.meta_model.7398487c": "Meta-model",
|
||||
"i18n:govoplan-organizations.model.20f35e63": "Model",
|
||||
"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.",
|
||||
"i18n:govoplan-organizations.no_relations_found.4e75b11d": "No relations found.",
|
||||
"i18n:govoplan-organizations.no_structures_found.20b382d0": "No structures found.",
|
||||
"i18n:govoplan-organizations.no_unit_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.organizations.220edf64": "Organizations",
|
||||
"i18n:govoplan-organizations.organizations_intro.4e67c4bb": "Model organization types, concrete units, structures, functions, and identity-held function assignments.",
|
||||
"i18n:govoplan-organizations.parent_unit.1986c35e": "Parent unit",
|
||||
"i18n:govoplan-organizations.reactivate.58f2855d": "Reactivate",
|
||||
"i18n:govoplan-organizations.relation_added.ca90461a": "Relation added.",
|
||||
"i18n:govoplan-organizations.relation_type.d0aee2e7": "Relation type",
|
||||
"i18n:govoplan-organizations.relation_type_added.6f1edff1": "Relation type added.",
|
||||
"i18n:govoplan-organizations.relation_types.e5890528": "Relation types",
|
||||
"i18n:govoplan-organizations.relations.1c796711": "Relations",
|
||||
"i18n:govoplan-organizations.reload.cce71553": "Reload",
|
||||
"i18n:govoplan-organizations.save_drafts.606f9401": "Save drafts",
|
||||
"i18n:govoplan-organizations.select_function.4895a67d": "Select function",
|
||||
"i18n:govoplan-organizations.select_function_type.2f426c43": "Select function type",
|
||||
"i18n:govoplan-organizations.select_relation_type.73f92478": "Select relation type",
|
||||
"i18n:govoplan-organizations.select_source_unit.9b5a29c8": "Select source unit",
|
||||
"i18n:govoplan-organizations.select_structure.c10f551c": "Select structure",
|
||||
"i18n:govoplan-organizations.select_target_unit.8d607541": "Select target unit",
|
||||
"i18n:govoplan-organizations.select_unit.013bf13a": "Select unit",
|
||||
"i18n:govoplan-organizations.select_unit_type.2c71d14f": "Select unit type",
|
||||
"i18n:govoplan-organizations.slug.094da9b9": "Slug",
|
||||
"i18n:govoplan-organizations.source.6da13add": "Source",
|
||||
"i18n:govoplan-organizations.source_unit.2fbd8baa": "Source unit",
|
||||
"i18n:govoplan-organizations.status.bae7d5be": "Status",
|
||||
"i18n:govoplan-organizations.structure.7732fb0b": "Structure",
|
||||
"i18n:govoplan-organizations.structure_added.f6cf8e74": "Structure added.",
|
||||
"i18n:govoplan-organizations.structures.f9b7f3b4": "Structures",
|
||||
"i18n:govoplan-organizations.target_unit.a1507d86": "Target unit",
|
||||
"i18n:govoplan-organizations.type.599dcce2": "Type",
|
||||
"i18n:govoplan-organizations.unit.8fe4d595": "Unit",
|
||||
"i18n:govoplan-organizations.unit_added.760a8512": "Unit added.",
|
||||
"i18n:govoplan-organizations.unit_type.9e62810d": "Unit type",
|
||||
"i18n:govoplan-organizations.unit_type_added.e017dc8c": "Unit type added.",
|
||||
"i18n:govoplan-organizations.unit_types.c7afc174": "Unit types",
|
||||
"i18n:govoplan-organizations.units.e14d0d92": "Units",
|
||||
"i18n:govoplan-organizations.write_permission_required.8b09fd67": "You do not have write permission for this organization section."
|
||||
},
|
||||
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",
|
||||
"i18n:govoplan-organizations.add_relation_type.2ad19d03": "Beziehungstyp hinzufügen",
|
||||
"i18n:govoplan-organizations.add_structure.b722042a": "Struktur hinzufügen",
|
||||
"i18n:govoplan-organizations.add_unit.8fa12fb1": "Einheit hinzufügen",
|
||||
"i18n:govoplan-organizations.add_unit_type.58f2c05a": "Einheitstyp hinzufügen",
|
||||
"i18n:govoplan-organizations.allow_cycles.31327578": "Zyklen erlauben",
|
||||
"i18n:govoplan-organizations.applies_to_subunits.7a15f741": "Gilt für Untereinheiten",
|
||||
"i18n:govoplan-organizations.assignment_added.94263d1b": "Funktionszuordnung hinzugefügt.",
|
||||
"i18n:govoplan-organizations.assignments.278f513e": "Zuordnungen",
|
||||
"i18n:govoplan-organizations.classification.3e9f6c3a": "Klassifikation",
|
||||
"i18n:govoplan-organizations.concrete_model.4e9c531a": "Konkretes Modell",
|
||||
"i18n:govoplan-organizations.deactivate.46ab070d": "Deaktivieren",
|
||||
"i18n:govoplan-organizations.delegable.b4f0137d": "Delegierbar",
|
||||
"i18n:govoplan-organizations.description.55f8ebc8": "Beschreibung",
|
||||
"i18n:govoplan-organizations.direct_assignment.2a068aac": "direkt",
|
||||
"i18n:govoplan-organizations.discard_organization_drafts.766d5f46": "Organisationsentwürfe verwerfen",
|
||||
"i18n:govoplan-organizations.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_types.172c01fe": "Funktionstypen",
|
||||
"i18n:govoplan-organizations.functions.805dc49b": "Funktionen",
|
||||
"i18n:govoplan-organizations.hierarchical.8964f313": "Hierarchisch",
|
||||
"i18n:govoplan-organizations.hierarchy.74797f37": "Hierarchie",
|
||||
"i18n:govoplan-organizations.identity_id.b6ef5010": "Identitäts-ID",
|
||||
"i18n:govoplan-organizations.identity_id_placeholder.298cbd85": "Identitäts-UUID",
|
||||
"i18n:govoplan-organizations.inactive.09af574c": "Inaktiv",
|
||||
"i18n:govoplan-organizations.kind.794c9d9c": "Art",
|
||||
"i18n:govoplan-organizations.loading_organization_model.846aa317": "Organisationsmodell wird geladen...",
|
||||
"i18n:govoplan-organizations.membership.4531d86d": "Mitgliedschaft",
|
||||
"i18n:govoplan-organizations.meta_model.7398487c": "Metamodell",
|
||||
"i18n:govoplan-organizations.model.20f35e63": "Modell",
|
||||
"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.",
|
||||
"i18n:govoplan-organizations.no_relations_found.4e75b11d": "Keine Beziehungen gefunden.",
|
||||
"i18n:govoplan-organizations.no_structures_found.20b382d0": "Keine Strukturen gefunden.",
|
||||
"i18n:govoplan-organizations.no_unit_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.organizations.220edf64": "Organisationen",
|
||||
"i18n:govoplan-organizations.organizations_intro.4e67c4bb": "Modelliere Organisationstypen, konkrete Einheiten, Strukturen, Funktionen und identitätsbezogene Funktionszuordnungen.",
|
||||
"i18n:govoplan-organizations.parent_unit.1986c35e": "Übergeordnete Einheit",
|
||||
"i18n:govoplan-organizations.reactivate.58f2855d": "Reaktivieren",
|
||||
"i18n:govoplan-organizations.relation_added.ca90461a": "Beziehung hinzugefügt.",
|
||||
"i18n:govoplan-organizations.relation_type.d0aee2e7": "Beziehungstyp",
|
||||
"i18n:govoplan-organizations.relation_type_added.6f1edff1": "Beziehungstyp hinzugefügt.",
|
||||
"i18n:govoplan-organizations.relation_types.e5890528": "Beziehungstypen",
|
||||
"i18n:govoplan-organizations.relations.1c796711": "Beziehungen",
|
||||
"i18n:govoplan-organizations.reload.cce71553": "Neu laden",
|
||||
"i18n:govoplan-organizations.save_drafts.606f9401": "Entwürfe speichern",
|
||||
"i18n:govoplan-organizations.select_function.4895a67d": "Funktion auswählen",
|
||||
"i18n:govoplan-organizations.select_function_type.2f426c43": "Funktionstyp auswählen",
|
||||
"i18n:govoplan-organizations.select_relation_type.73f92478": "Beziehungstyp auswählen",
|
||||
"i18n:govoplan-organizations.select_source_unit.9b5a29c8": "Quell-Einheit auswählen",
|
||||
"i18n:govoplan-organizations.select_structure.c10f551c": "Struktur auswählen",
|
||||
"i18n:govoplan-organizations.select_target_unit.8d607541": "Ziel-Einheit auswählen",
|
||||
"i18n:govoplan-organizations.select_unit.013bf13a": "Einheit auswählen",
|
||||
"i18n:govoplan-organizations.select_unit_type.2c71d14f": "Einheitstyp auswählen",
|
||||
"i18n:govoplan-organizations.slug.094da9b9": "Slug",
|
||||
"i18n:govoplan-organizations.source.6da13add": "Quelle",
|
||||
"i18n:govoplan-organizations.source_unit.2fbd8baa": "Quell-Einheit",
|
||||
"i18n:govoplan-organizations.status.bae7d5be": "Status",
|
||||
"i18n:govoplan-organizations.structure.7732fb0b": "Struktur",
|
||||
"i18n:govoplan-organizations.structure_added.f6cf8e74": "Struktur hinzugefügt.",
|
||||
"i18n:govoplan-organizations.structures.f9b7f3b4": "Strukturen",
|
||||
"i18n:govoplan-organizations.target_unit.a1507d86": "Ziel-Einheit",
|
||||
"i18n:govoplan-organizations.type.599dcce2": "Typ",
|
||||
"i18n:govoplan-organizations.unit.8fe4d595": "Einheit",
|
||||
"i18n:govoplan-organizations.unit_added.760a8512": "Einheit hinzugefügt.",
|
||||
"i18n:govoplan-organizations.unit_type.9e62810d": "Einheitstyp",
|
||||
"i18n:govoplan-organizations.unit_type_added.e017dc8c": "Einheitstyp hinzugefügt.",
|
||||
"i18n:govoplan-organizations.unit_types.c7afc174": "Einheitstypen",
|
||||
"i18n:govoplan-organizations.units.e14d0d92": "Einheiten",
|
||||
"i18n:govoplan-organizations.write_permission_required.8b09fd67": "Du hast keine Schreibberechtigung für diesen Organisationsbereich."
|
||||
}
|
||||
};
|
||||
5
webui/src/index.ts
Normal file
5
webui/src/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export { default } from "./module";
|
||||
export * from "./module";
|
||||
export { default as OrganizationsPage } from "./features/organizations/OrganizationsPage";
|
||||
export * from "./api/organizations";
|
||||
export type { PlatformWebModule, PlatformNavItem, PlatformRouteContribution, PlatformRouteContext } from "@govoplan/core-webui";
|
||||
63
webui/src/module.ts
Normal file
63
webui/src/module.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { createElement, lazy } from "react";
|
||||
import type { AdminSectionsUiCapability, PlatformWebModule } from "@govoplan/core-webui";
|
||||
import { generatedTranslations } from "./i18n/generatedTranslations";
|
||||
import "./styles/organizations.css";
|
||||
|
||||
const OrganizationsPage = lazy(() => import("./features/organizations/OrganizationsPage"));
|
||||
const OrganizationsAdminPanel = lazy(() => import("./features/organizations/OrganizationsAdminPanel"));
|
||||
|
||||
const organizationReadScopes = [
|
||||
"organizations:model:read",
|
||||
"organizations:unit:read",
|
||||
"organizations:function:read",
|
||||
"admin:settings:read"
|
||||
];
|
||||
|
||||
const translations = {
|
||||
en: generatedTranslations.en,
|
||||
de: generatedTranslations.de
|
||||
};
|
||||
|
||||
const organizationAdminSections: AdminSectionsUiCapability = {
|
||||
sections: [
|
||||
{
|
||||
id: "tenant-organization-model",
|
||||
label: "i18n:govoplan-organizations.organization_model.4f924c0e",
|
||||
group: "TENANT",
|
||||
order: 85,
|
||||
anyOf: ["organizations:model:read", "admin:settings:read"],
|
||||
render: ({ settings, auth }) => createElement(OrganizationsAdminPanel, { settings, auth })
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export const organizationsModule: PlatformWebModule = {
|
||||
id: "organizations",
|
||||
label: "i18n:govoplan-organizations.organizations.220edf64",
|
||||
version: "1.0.0",
|
||||
dependencies: ["access"],
|
||||
optionalDependencies: ["identity", "admin"],
|
||||
translations,
|
||||
navItems: [
|
||||
{
|
||||
to: "/organizations",
|
||||
label: "i18n:govoplan-organizations.organizations.220edf64",
|
||||
iconName: "users",
|
||||
anyOf: organizationReadScopes,
|
||||
order: 70
|
||||
}
|
||||
],
|
||||
routes: [
|
||||
{
|
||||
path: "/organizations",
|
||||
anyOf: organizationReadScopes,
|
||||
order: 70,
|
||||
render: ({ settings, auth }) => createElement(OrganizationsPage, { settings, auth })
|
||||
}
|
||||
],
|
||||
uiCapabilities: {
|
||||
"admin.sections": organizationAdminSections
|
||||
}
|
||||
};
|
||||
|
||||
export default organizationsModule;
|
||||
106
webui/src/styles/organizations.css
Normal file
106
webui/src/styles/organizations.css
Normal file
@@ -0,0 +1,106 @@
|
||||
.organizations-workspace {
|
||||
grid-template-columns: 230px minmax(0, 1fr);
|
||||
background: var(--bg, #f8f7f4);
|
||||
}
|
||||
|
||||
.organizations-page {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
width: 100%;
|
||||
max-width: 1480px;
|
||||
}
|
||||
|
||||
.organizations-admin-page {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.organizations-heading {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.organizations-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.organizations-section-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(280px, 360px) minmax(0, 1fr);
|
||||
gap: 18px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.organizations-form-panel,
|
||||
.organizations-table-panel {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.organizations-form-panel .card-body,
|
||||
.organizations-table-panel .card-body {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.organizations-form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.organizations-form-grid .wide {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.organizations-check-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.organizations-check-list label,
|
||||
.organizations-inline-check {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.organizations-table-stack {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.organizations-field-note {
|
||||
margin: 8px 0 0;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.organizations-id {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.organizations-empty {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.organizations-section-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.organizations-workspace {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.organizations-form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user