Add organizations module surface

This commit is contained in:
2026-07-10 17:33:53 +02:00
parent 34133ba9cd
commit ad2561b50f
22 changed files with 3284 additions and 35 deletions

31
webui/package.json Normal file
View 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
}
}
}

View 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);
}

View 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"
/>
);
}

View 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>
);
}
}

View 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
View 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
View 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;

View 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;
}
}