Files
govoplan-organizations/webui/src/features/organizations/OrganizationsPage.tsx
2026-07-11 16:49:03 +02:00

1298 lines
63 KiB
TypeScript

import { useCallback, useEffect, useMemo, useRef, useState, type FormEvent } from "react";
import { Edit3, Plus, RefreshCw } from "lucide-react";
import {
AdminIconButton,
ApiError,
Button,
Card,
DataGrid,
Dialog,
DismissibleAlert,
FormField,
LoadingFrame,
ModuleSubnav,
PageTitle,
StatusBadge,
ToggleSwitch,
hasScope,
useUnsavedDraftGuard,
usePlatformUiCapabilities,
type ApiSettings,
type AuthInfo,
type DataGridColumn,
type ModuleSubnavGroup,
type OrganizationFunctionActionContribution,
type OrganizationFunctionActionsUiCapability
} from "@govoplan/core-webui";
import {
createFunction,
createFunctionType,
createRelation,
createRelationType,
createStructure,
createUnit,
createUnitType,
getOrganizationModel,
patchFunction,
patchFunctionType,
patchRelation,
patchRelationType,
patchStructure,
patchUnit,
patchUnitType,
type FunctionCreatePayload,
type FunctionTypeCreatePayload,
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";
type OrganizationsPageMode = "workspace" | "admin";
type OrganizationEditor = "unitType" | "structure" | "relationType" | "unit" | "relation" | "functionType" | "function";
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 OrganizationsInitialQuery = {
section: OrganizationSection | "";
unitId: string;
functionId: string;
};
function organizationSectionFromQuery(value: string | null): OrganizationSection | "" {
return value === "model" || value === "units" || value === "relations" || value === "functions" ? value : "";
}
function organizationsInitialQuery(): OrganizationsInitialQuery {
if (typeof window === "undefined") return { section: "", unitId: "", functionId: "" };
const params = new URLSearchParams(window.location.search);
return {
section: organizationSectionFromQuery(params.get("section")),
unitId: params.get("unit_id") || "",
functionId: params.get("function_id") || ""
};
}
const EMPTY_MODEL: OrganizationModel = {
unit_types: [],
structures: [],
relation_types: [],
units: [],
relations: [],
function_types: [],
functions: []
};
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" }
]
}
];
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"];
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 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 sluggedPatchPayload(draft: SluggedDraft): Partial<SluggedCreatePayload> {
return {
name: draft.name.trim(),
slug: textOrNull(draft.slug),
description: textOrNull(draft.description),
is_active: draft.is_active
};
}
function sluggedDraftFrom(item: { name: string; slug: string; description?: string | null; is_active: boolean }): SluggedDraft {
return {
name: item.name,
slug: item.slug,
description: item.description ?? "",
is_active: item.is_active
};
}
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 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) {
try {
const parsed = JSON.parse(error.body) as { detail?: string | { message?: string } };
if (typeof parsed.detail === "string") return parsed.detail;
if (parsed.detail && typeof parsed.detail.message === "string") return parsed.detail.message;
} catch {
// Fall back to the transport message below.
}
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 RowActions({ disabled, onEdit, extra }: { disabled: boolean; onEdit: () => void; extra?: JSX.Element[] }) {
return (
<div className="organizations-row-actions">
<AdminIconButton label="i18n:govoplan-organizations.edit.7dce1220" icon={<Edit3 size={16} aria-hidden="true" />} disabled={disabled} onClick={onEdit} />
{extra}
</div>
);
}
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>
<div className="wide">
<ToggleSwitch
checked={draft.is_active}
disabled={disabled}
onChange={(checked) => onChange({ ...draft, is_active: checked })}
label="i18n:govoplan-organizations.active.a733b809"
/>
</div>
</>
);
}
function contributionVisible(auth: AuthInfo, contribution: { anyOf?: string[]; allOf?: string[] }): boolean {
const anyOf = contribution.anyOf ?? [];
const allOf = contribution.allOf ?? [];
if (anyOf.length && !anyOf.some((scope) => hasScope(auth, scope))) return false;
if (allOf.length && !allOf.every((scope) => hasScope(auth, scope))) return false;
return true;
}
export default function OrganizationsPage({
settings,
auth,
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 initialQuery = useMemo(() => organizationsInitialQuery(), []);
const requestedInitialSection = initialQuery.section && sectionSet.has(initialQuery.section) ? initialQuery.section : initialSection;
const [active, setActive] = useState<OrganizationSection>(sectionSet.has(requestedInitialSection) ? requestedInitialSection : firstAvailableSection);
const appliedInitialQueryRef = useRef(false);
const visibleSectionGroups = useMemo(() => sectionGroupsFor(sectionSet), [sectionSet]);
const [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 [editingUnitTypeId, setEditingUnitTypeId] = useState<string | null>(null);
const [editingStructureId, setEditingStructureId] = useState<string | null>(null);
const [editingRelationTypeId, setEditingRelationTypeId] = useState<string | null>(null);
const [editingUnitId, setEditingUnitId] = useState<string | null>(null);
const [editingRelationId, setEditingRelationId] = useState<string | null>(null);
const [editingFunctionTypeId, setEditingFunctionTypeId] = useState<string | null>(null);
const [editingFunctionId, setEditingFunctionId] = useState<string | null>(null);
const [activeEditor, setActiveEditor] = useState<OrganizationEditor | null>(null);
const [selectedUnitId, setSelectedUnitId] = useState("");
const [changeRequestId, setChangeRequestId] = useState("");
const functionActionCapabilities = usePlatformUiCapabilities<OrganizationFunctionActionsUiCapability>("organizations.functionActions");
const canWriteModel = hasScope(auth, "organizations:model:write");
const canWriteUnits = hasScope(auth, "organizations:unit:write");
const canWriteFunctions = hasScope(auth, "organizations:function:write");
const 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 functionActionContributions = useMemo(
() => functionActionCapabilities
.flatMap((capability) => capability.actions)
.filter((contribution) => contributionVisible(auth, contribution))
.sort((left, right) => (left.order ?? 100) - (right.order ?? 100)),
[auth, functionActionCapabilities]
);
const unitsByParentId = useMemo(() => {
const mapped = new Map<string, OrganizationUnitItem[]>();
for (const unit of model.units) {
const key = unit.parent_id || "";
mapped.set(key, [...(mapped.get(key) ?? []), unit]);
}
for (const units of mapped.values()) units.sort((left, right) => left.name.localeCompare(right.name));
return mapped;
}, [model.units]);
const hasDirtyDraft = isSluggedDirty(unitTypeDraft) ||
isSluggedDirty(structureDraft) ||
isRelationTypeDirty(relationTypeDraft) ||
isUnitDirty(unitDraft) ||
isRelationDirty(relationDraft) ||
isFunctionTypeDirty(functionTypeDraft) ||
isFunctionDirty(functionDraft);
const loadModel = useCallback(async () => {
setLoading(true);
setError("");
try {
setModel(await getOrganizationModel(settings));
} catch (caught) {
setError(apiErrorMessage(caught));
} finally {
setLoading(false);
}
}, [settings]);
const withChangeRequest = useCallback(<T extends object,>(payload: T): T & { change_request_id?: string | null } => {
const requestId = textOrNull(changeRequestId);
return requestId ? { ...payload, change_request_id: requestId } : payload;
}, [changeRequestId]);
useEffect(() => {
void loadModel();
}, [loadModel]);
useEffect(() => {
if (!sectionSet.has(active)) setActive(firstAvailableSection);
}, [active, firstAvailableSection, sectionSet]);
useEffect(() => {
if (selectedUnitId && !unitById.has(selectedUnitId)) setSelectedUnitId("");
}, [selectedUnitId, unitById]);
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());
setEditingUnitTypeId(null);
setEditingStructureId(null);
setEditingRelationTypeId(null);
setEditingUnitId(null);
setEditingRelationId(null);
setEditingFunctionTypeId(null);
setEditingFunctionId(null);
setActiveEditor(null);
setChangeRequestId("");
}, []);
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(
() => editingUnitTypeId ? patchUnitType(settings, editingUnitTypeId, withChangeRequest(sluggedPatchPayload(unitTypeDraft))) : createUnitType(settings, withChangeRequest(sluggedPayload(unitTypeDraft))),
editingUnitTypeId ? "i18n:govoplan-organizations.unit_type_updated.6d8f2a16" : "i18n:govoplan-organizations.unit_type_added.e017dc8c"
);
if (ok) {
setUnitTypeDraft(emptySluggedDraft());
setEditingUnitTypeId(null);
setActiveEditor(null);
setChangeRequestId("");
}
return ok;
}, [canWriteModel, editingUnitTypeId, runAction, settings, unitTypeDraft, withChangeRequest]);
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 patchPayload: Partial<StructureCreatePayload> = { ...sluggedPatchPayload(structureDraft), structure_kind: structureDraft.structure_kind };
const ok = await runAction(
() => editingStructureId ? patchStructure(settings, editingStructureId, withChangeRequest(patchPayload)) : createStructure(settings, withChangeRequest(payload)),
editingStructureId ? "i18n:govoplan-organizations.structure_updated.2591e75f" : "i18n:govoplan-organizations.structure_added.f6cf8e74"
);
if (ok) {
setStructureDraft(emptyStructureDraft());
setEditingStructureId(null);
setActiveEditor(null);
setChangeRequestId("");
}
return ok;
}, [canWriteModel, editingStructureId, runAction, settings, structureDraft, withChangeRequest]);
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 patchPayload: Partial<RelationTypeCreatePayload> = {
...sluggedPatchPayload(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(
() => editingRelationTypeId ? patchRelationType(settings, editingRelationTypeId, withChangeRequest(patchPayload)) : createRelationType(settings, withChangeRequest(payload)),
editingRelationTypeId ? "i18n:govoplan-organizations.relation_type_updated.5ac57ec7" : "i18n:govoplan-organizations.relation_type_added.6f1edff1"
);
if (ok) {
setRelationTypeDraft(emptyRelationTypeDraft());
setEditingRelationTypeId(null);
setActiveEditor(null);
setChangeRequestId("");
}
return ok;
}, [canWriteModel, editingRelationTypeId, relationTypeDraft, runAction, settings, withChangeRequest]);
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 patchPayload: Partial<UnitCreatePayload> = {
...sluggedPatchPayload(unitDraft),
unit_type_id: textOrNull(unitDraft.unit_type_id),
parent_id: textOrNull(unitDraft.parent_id)
};
const ok = await runAction(
() => editingUnitId ? patchUnit(settings, editingUnitId, withChangeRequest(patchPayload)) : createUnit(settings, withChangeRequest(payload)),
editingUnitId ? "i18n:govoplan-organizations.unit_updated.15f02216" : "i18n:govoplan-organizations.unit_added.760a8512"
);
if (ok) {
setUnitDraft(emptyUnitDraft());
setEditingUnitId(null);
setActiveEditor(null);
setChangeRequestId("");
}
return ok;
}, [canWriteUnits, editingUnitId, runAction, settings, unitDraft, withChangeRequest]);
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 patchPayload: Partial<RelationCreatePayload> = { ...relationDraft };
const ok = await runAction(
() => editingRelationId ? patchRelation(settings, editingRelationId, withChangeRequest(patchPayload)) : createRelation(settings, withChangeRequest(payload)),
editingRelationId ? "i18n:govoplan-organizations.relation_updated.c6212776" : "i18n:govoplan-organizations.relation_added.ca90461a"
);
if (ok) {
setRelationDraft(emptyRelationDraft());
setEditingRelationId(null);
setActiveEditor(null);
setChangeRequestId("");
}
return ok;
}, [canWriteUnits, editingRelationId, relationDraft, runAction, settings, withChangeRequest]);
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 patchPayload: Partial<FunctionTypeCreatePayload> = {
...sluggedPatchPayload(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(
() => editingFunctionTypeId ? patchFunctionType(settings, editingFunctionTypeId, withChangeRequest(patchPayload)) : createFunctionType(settings, withChangeRequest(payload)),
editingFunctionTypeId ? "i18n:govoplan-organizations.function_type_updated.1a4f29f6" : "i18n:govoplan-organizations.function_type_added.2cd9e899"
);
if (ok) {
setFunctionTypeDraft(emptyFunctionTypeDraft());
setEditingFunctionTypeId(null);
setActiveEditor(null);
setChangeRequestId("");
}
return ok;
}, [canWriteModel, editingFunctionTypeId, functionTypeDraft, runAction, settings, withChangeRequest]);
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 patchPayload: Partial<FunctionCreatePayload> = {
...sluggedPatchPayload(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(
() => editingFunctionId ? patchFunction(settings, editingFunctionId, withChangeRequest(patchPayload)) : createFunction(settings, withChangeRequest(payload)),
editingFunctionId ? "i18n:govoplan-organizations.function_updated.65016009" : "i18n:govoplan-organizations.function_added.e2266702"
);
if (ok) {
setFunctionDraft(emptyFunctionDraft());
setEditingFunctionId(null);
setActiveEditor(null);
setChangeRequestId("");
}
return ok;
}, [canWriteFunctions, editingFunctionId, functionDraft, runAction, settings, withChangeRequest]);
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;
return true;
}, [
functionDraft,
functionTypeDraft,
relationDraft,
relationTypeDraft,
structureDraft,
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 openUnitTypeCreate = useCallback(() => {
setEditingUnitTypeId(null);
setUnitTypeDraft(emptySluggedDraft());
setActive("model");
setActiveEditor("unitType");
}, []);
const openStructureCreate = useCallback(() => {
setEditingStructureId(null);
setStructureDraft(emptyStructureDraft());
setActive("model");
setActiveEditor("structure");
}, []);
const openRelationTypeCreate = useCallback(() => {
setEditingRelationTypeId(null);
setRelationTypeDraft(emptyRelationTypeDraft());
setActive("model");
setActiveEditor("relationType");
}, []);
const openUnitCreate = useCallback((parentId = "") => {
setEditingUnitId(null);
setUnitDraft({ ...emptyUnitDraft(), parent_id: parentId });
if (parentId) setSelectedUnitId(parentId);
setActive("units");
setActiveEditor("unit");
}, []);
const openRelationCreate = useCallback(() => {
setEditingRelationId(null);
setRelationDraft(emptyRelationDraft());
setActive("relations");
setActiveEditor("relation");
}, []);
const openFunctionTypeCreate = useCallback(() => {
setEditingFunctionTypeId(null);
setFunctionTypeDraft(emptyFunctionTypeDraft());
setActive("model");
setActiveEditor("functionType");
}, []);
const openFunctionCreate = useCallback((organizationUnitId = "") => {
setEditingFunctionId(null);
setFunctionDraft({ ...emptyFunctionDraft(), organization_unit_id: organizationUnitId || selectedUnitId });
setActive("functions");
setActiveEditor("function");
}, [selectedUnitId]);
const editUnitType = useCallback((item: OrganizationUnitTypeItem) => {
setEditingUnitTypeId(item.id);
setUnitTypeDraft(sluggedDraftFrom(item));
setActive("model");
setActiveEditor("unitType");
}, []);
const editStructure = useCallback((item: OrganizationStructureItem) => {
setEditingStructureId(item.id);
setStructureDraft({ ...sluggedDraftFrom(item), structure_kind: item.structure_kind as OrganizationStructureKind });
setActive("model");
setActiveEditor("structure");
}, []);
const editRelationType = useCallback((item: OrganizationRelationTypeItem) => {
setEditingRelationTypeId(item.id);
setRelationTypeDraft({
...sluggedDraftFrom(item),
structure_id: item.structure_id ?? "",
source_unit_type_id: item.source_unit_type_id ?? "",
target_unit_type_id: item.target_unit_type_id ?? "",
is_hierarchical: item.is_hierarchical,
allow_cycles: item.allow_cycles
});
setActive("model");
setActiveEditor("relationType");
}, []);
const editUnit = useCallback((item: OrganizationUnitItem) => {
setEditingUnitId(item.id);
setSelectedUnitId(item.id);
setUnitDraft({ ...sluggedDraftFrom(item), unit_type_id: item.unit_type_id ?? "", parent_id: item.parent_id ?? "" });
setActive("units");
setActiveEditor("unit");
}, []);
const addSubunit = useCallback((item: OrganizationUnitItem) => {
openUnitCreate(item.id);
}, [openUnitCreate]);
const editRelation = useCallback((item: OrganizationRelationItem) => {
setEditingRelationId(item.id);
setRelationDraft({
structure_id: item.structure_id,
relation_type_id: item.relation_type_id,
source_unit_id: item.source_unit_id,
target_unit_id: item.target_unit_id,
is_active: item.is_active
});
setActive("relations");
setActiveEditor("relation");
}, []);
const editFunctionType = useCallback((item: OrganizationFunctionTypeItem) => {
setEditingFunctionTypeId(item.id);
setFunctionTypeDraft({
...sluggedDraftFrom(item),
organization_unit_type_id: item.organization_unit_type_id ?? "",
delegable: item.delegable,
act_in_place_allowed: item.act_in_place_allowed
});
setActive("model");
setActiveEditor("functionType");
}, []);
const editFunction = useCallback((item: OrganizationFunctionItem) => {
setEditingFunctionId(item.id);
setSelectedUnitId(item.organization_unit_id);
setFunctionDraft({
...sluggedDraftFrom(item),
organization_unit_id: item.organization_unit_id,
function_type_id: item.function_type_id ?? "",
delegable: item.delegable,
act_in_place_allowed: item.act_in_place_allowed
});
setActive("functions");
setActiveEditor("function");
}, []);
useEffect(() => {
if (appliedInitialQueryRef.current || loading) return;
if (initialQuery.functionId) {
const item = functionById.get(initialQuery.functionId);
if (item) {
appliedInitialQueryRef.current = true;
editFunction(item);
return;
}
}
if (initialQuery.unitId) {
const item = unitById.get(initialQuery.unitId);
if (item) {
appliedInitialQueryRef.current = true;
editUnit(item);
return;
}
}
if (initialQuery.section && sectionSet.has(initialQuery.section)) {
appliedInitialQueryRef.current = true;
setActive(initialQuery.section);
}
}, [editFunction, editUnit, functionById, initialQuery, loading, sectionSet, unitById]);
const unitTypeColumns: DataGridColumn<OrganizationUnitTypeItem>[] = [
{ 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: 88, sticky: "end", render: (row) => <RowActions disabled={!canWriteModel || busy} onEdit={() => editUnitType(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: 88, sticky: "end", render: (row) => <RowActions disabled={!canWriteModel || busy} onEdit={() => editStructure(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: 88, sticky: "end", render: (row) => <RowActions disabled={!canWriteModel || busy} onEdit={() => editRelationType(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: 88, sticky: "end", render: (row) => <RowActions disabled={!canWriteUnits || busy} onEdit={() => editUnit(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: 88, sticky: "end", render: (row) => <RowActions disabled={!canWriteUnits || busy} onEdit={() => editRelation(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: 88, sticky: "end", render: (row) => <RowActions disabled={!canWriteModel || busy} onEdit={() => editFunctionType(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: functionActionContributions.length ? 220 : 88,
sticky: "end",
render: (row) => (
<RowActions
disabled={!canWriteFunctions || busy}
onEdit={() => editFunction(row)}
extra={functionActionContributions.map((contribution) => (
<span className="organizations-contributed-action" key={contribution.id}>
{contribution.render({ settings, auth, function: row })}
</span>
))}
/>
)
}
];
const canWriteActive = active === "model" ? canWriteModel : active === "units" || active === "relations" ? canWriteUnits : canWriteFunctions;
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>
</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()}
</LoadingFrame>
{renderEditorDialog()}
</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">
<Card title="i18n:govoplan-organizations.unit_types.c7afc174" actions={<AdminIconButton label="i18n:govoplan-organizations.add_unit_type.58f2c05a" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteModel || busy} onClick={openUnitTypeCreate} />}>
<DataGrid id="organizations-unit-types" rows={model.unit_types} columns={unitTypeColumns} getRowKey={(row) => row.id} emptyText="i18n:govoplan-organizations.no_unit_types_found.c81fb2a7" initialFit="container" />
</Card>
<Card title="i18n:govoplan-organizations.structures.f9b7f3b4" actions={<AdminIconButton label="i18n:govoplan-organizations.add_structure.b722042a" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteModel || busy} onClick={openStructureCreate} />}>
<DataGrid id="organizations-structures" rows={model.structures} columns={structureColumns} getRowKey={(row) => row.id} emptyText="i18n:govoplan-organizations.no_structures_found.20b382d0" initialFit="container" />
</Card>
<Card title="i18n:govoplan-organizations.relation_types.e5890528" actions={<AdminIconButton label="i18n:govoplan-organizations.add_relation_type.2ad19d03" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteModel || busy} onClick={openRelationTypeCreate} />}>
<DataGrid id="organizations-relation-types" rows={model.relation_types} columns={relationTypeColumns} getRowKey={(row) => row.id} emptyText="i18n:govoplan-organizations.no_relation_types_found.6f90bb81" initialFit="container" />
</Card>
<Card title="i18n:govoplan-organizations.function_types.172c01fe" actions={<AdminIconButton label="i18n:govoplan-organizations.add_function_type.90d793f1" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteModel || busy} onClick={openFunctionTypeCreate} />}>
<DataGrid id="organizations-function-types" rows={model.function_types} columns={functionTypeColumns} getRowKey={(row) => row.id} emptyText="i18n:govoplan-organizations.no_function_types_found.5eef31b1" initialFit="container" />
</Card>
</div>
);
}
function renderUnitTreeNodes(parentId = "", depth = 0, seen: ReadonlySet<string> = new Set()): JSX.Element[] {
return (unitsByParentId.get(parentId) ?? []).flatMap((unit) => {
if (seen.has(unit.id)) return [];
const nextSeen = new Set(seen);
nextSeen.add(unit.id);
return [
<div className="organizations-tree-row" key={unit.id}>
<button
type="button"
className={`organizations-tree-node ${selectedUnitId === unit.id ? "active" : ""}`.trim()}
style={{ paddingLeft: `${10 + depth * 18}px` }}
onClick={() => setSelectedUnitId(unit.id)}
>
<strong>{unit.name}</strong>
<span>{unitTypeById.get(unit.unit_type_id || "")?.name ?? "i18n:govoplan-organizations.no_unit_type.73e799f5"}</span>
</button>
<Button type="button" variant="ghost" disabled={!canWriteUnits || busy} onClick={() => addSubunit(unit)} title="i18n:govoplan-organizations.add_subunit.8256a8f7">
<Plus size={16} aria-hidden="true" /> i18n:govoplan-organizations.add_subunit.8256a8f7
</Button>
</div>,
...renderUnitTreeNodes(unit.id, depth + 1, nextSeen)
];
});
}
function renderUnitsSection() {
return (
<div className="organizations-table-stack">
<Card title="i18n:govoplan-organizations.organization_tree.e5bfb195" actions={<AdminIconButton label="i18n:govoplan-organizations.add_root_unit.1ef8a9f5" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteUnits || busy} onClick={() => openUnitCreate()} />}>
<div className="organizations-tree-toolbar">
{selectedUnitId && <Button type="button" variant="ghost" disabled={busy} onClick={() => setSelectedUnitId("")}>i18n:govoplan-organizations.none.334c4a4c</Button>}
</div>
<div className="organizations-tree-list">
{model.units.length ? renderUnitTreeNodes() : <p className="muted small-note">i18n:govoplan-organizations.no_units_found.eea2dd0c</p>}
</div>
</Card>
<Card title="i18n:govoplan-organizations.units.e14d0d92" actions={<AdminIconButton label="i18n:govoplan-organizations.add_unit.8fa12fb1" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteUnits || busy} onClick={() => openUnitCreate()} />}>
<DataGrid id="organizations-units" rows={model.units} columns={unitColumns} getRowKey={(row) => row.id} emptyText="i18n:govoplan-organizations.no_units_found.eea2dd0c" initialFit="container" />
</Card>
</div>
);
}
function renderRelationsSection() {
return (
<div className="organizations-table-stack">
<Card title="i18n:govoplan-organizations.relations.1c796711" actions={<AdminIconButton label="i18n:govoplan-organizations.add_relation.6b6e67ea" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteUnits || busy} onClick={openRelationCreate} />}>
<DataGrid id="organizations-relations" rows={model.relations} columns={relationColumns} getRowKey={(row) => row.id} emptyText="i18n:govoplan-organizations.no_relations_found.4e75b11d" initialFit="container" />
</Card>
</div>
);
}
function renderFunctionsSection() {
return (
<div className="organizations-table-stack">
<Card title="i18n:govoplan-organizations.functions.805dc49b" actions={<AdminIconButton label="i18n:govoplan-organizations.add_function.6abafee0" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteFunctions || busy} onClick={() => openFunctionCreate()} />}>
<DataGrid id="organizations-functions" rows={model.functions} columns={functionColumns} getRowKey={(row) => row.id} emptyText="i18n:govoplan-organizations.no_functions_found.54e759a0" initialFit="container" />
</Card>
</div>
);
}
function renderChangeRequestField() {
return (
<div className="wide organizations-dialog-change-request">
<FormField label="i18n:govoplan-organizations.change_request_id.a6e7fd6b">
<input value={changeRequestId} onChange={(event) => setChangeRequestId(event.target.value)} placeholder="cfgreq-..." disabled={busy} />
</FormField>
<p className="organizations-field-note">i18n:govoplan-organizations.change_request_id_help.0c119a5d</p>
</div>
);
}
function editorTitle(): string {
switch (activeEditor) {
case "unitType": return editingUnitTypeId ? "i18n:govoplan-organizations.update_unit_type.b96fc0ad" : "i18n:govoplan-organizations.add_unit_type.58f2c05a";
case "structure": return editingStructureId ? "i18n:govoplan-organizations.update_structure.b2e25446" : "i18n:govoplan-organizations.add_structure.b722042a";
case "relationType": return editingRelationTypeId ? "i18n:govoplan-organizations.update_relation_type.e6e7f30c" : "i18n:govoplan-organizations.add_relation_type.2ad19d03";
case "unit": return editingUnitId ? "i18n:govoplan-organizations.update_unit.67e2500f" : "i18n:govoplan-organizations.add_unit.8fa12fb1";
case "relation": return editingRelationId ? "i18n:govoplan-organizations.update_relation.4c44ce83" : "i18n:govoplan-organizations.add_relation.6b6e67ea";
case "functionType": return editingFunctionTypeId ? "i18n:govoplan-organizations.update_function_type.0b7e07d3" : "i18n:govoplan-organizations.add_function_type.90d793f1";
case "function": return editingFunctionId ? "i18n:govoplan-organizations.update_function.504f03e5" : "i18n:govoplan-organizations.add_function.6abafee0";
default: return "i18n:govoplan-organizations.organizations.220edf64";
}
}
function editorSubmitDisabled(): boolean {
if (busy || !activeEditor) return true;
if ((activeEditor === "unitType" || activeEditor === "structure" || activeEditor === "relationType" || activeEditor === "functionType") && !canWriteModel) return true;
if ((activeEditor === "unit" || activeEditor === "relation") && !canWriteUnits) return true;
if (activeEditor === "function" && !canWriteFunctions) return true;
if (activeEditor === "unitType") return !unitTypeDraft.name.trim();
if (activeEditor === "structure") return !structureDraft.name.trim();
if (activeEditor === "relationType") return !relationTypeDraft.name.trim();
if (activeEditor === "unit") return !unitDraft.name.trim();
if (activeEditor === "relation") return !relationDraft.structure_id || !relationDraft.relation_type_id || !relationDraft.source_unit_id || !relationDraft.target_unit_id;
if (activeEditor === "functionType") return !functionTypeDraft.name.trim();
if (activeEditor === "function") return !functionDraft.name.trim() || !functionDraft.organization_unit_id;
return true;
}
function renderEditorDialog() {
if (!activeEditor) return null;
const formId = `organizations-${activeEditor}-editor`;
const submit = activeEditor === "unitType" ? submitUnitType :
activeEditor === "structure" ? submitStructure :
activeEditor === "relationType" ? submitRelationType :
activeEditor === "unit" ? submitUnit :
activeEditor === "relation" ? submitRelation :
activeEditor === "functionType" ? submitFunctionType :
submitFunction;
return (
<Dialog
open={Boolean(activeEditor)}
title={editorTitle()}
onClose={() => !busy && discardDrafts()}
closeDisabled={busy}
className="admin-dialog admin-dialog-wide organizations-editor-dialog"
footer={(
<>
<Button type="button" onClick={discardDrafts} disabled={busy}>i18n:govoplan-organizations.cancel_edit.309c2a6f</Button>
<Button type="submit" form={formId} variant="primary" disabled={editorSubmitDisabled()}>{editorTitle()}</Button>
</>
)}
>
<form id={formId} className="organizations-form-grid" onSubmit={(event) => void submit(event)}>
{renderEditorFields()}
</form>
</Dialog>
);
}
function renderEditorFields() {
if (activeEditor === "unitType") {
return (
<>
<SluggedFields draft={unitTypeDraft} onChange={setUnitTypeDraft} disabled={!canWriteModel || busy} />
{renderChangeRequestField()}
</>
);
}
if (activeEditor === "structure") {
return (
<>
<SluggedFields draft={structureDraft} onChange={setStructureDraft} disabled={!canWriteModel || busy} />
<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>
{renderChangeRequestField()}
</>
);
}
if (activeEditor === "relationType") {
return (
<>
<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">
<ToggleSwitch checked={relationTypeDraft.is_hierarchical} disabled={!canWriteModel || busy} onChange={(checked) => setRelationTypeDraft({ ...relationTypeDraft, is_hierarchical: checked })} label="i18n:govoplan-organizations.hierarchical.8964f313" />
<ToggleSwitch checked={relationTypeDraft.allow_cycles} disabled={!canWriteModel || busy} onChange={(checked) => setRelationTypeDraft({ ...relationTypeDraft, allow_cycles: checked })} label="i18n:govoplan-organizations.allow_cycles.31327578" />
</div>
{renderChangeRequestField()}
</>
);
}
if (activeEditor === "unit") {
return (
<>
<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.filter((item) => item.id !== editingUnitId).map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
</select>
</FormField>
{unitDraft.parent_id && (
<p className="organizations-field-note wide">
i18n:govoplan-organizations.selected_parent.0e013d44 <strong>{unitById.get(unitDraft.parent_id)?.name ?? unitDraft.parent_id}</strong>
</p>
)}
{renderChangeRequestField()}
</>
);
}
if (activeEditor === "relation") {
return (
<>
<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>
<div className="wide">
<ToggleSwitch
checked={relationDraft.is_active}
disabled={!canWriteUnits || busy}
onChange={(checked) => setRelationDraft({ ...relationDraft, is_active: checked })}
label="i18n:govoplan-organizations.active.a733b809"
/>
</div>
{renderChangeRequestField()}
</>
);
}
if (activeEditor === "functionType") {
return (
<>
<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">
<ToggleSwitch checked={functionTypeDraft.delegable} disabled={!canWriteModel || busy} onChange={(checked) => setFunctionTypeDraft({ ...functionTypeDraft, delegable: checked })} label="i18n:govoplan-organizations.delegable.b4f0137d" />
<ToggleSwitch checked={functionTypeDraft.act_in_place_allowed} disabled={!canWriteModel || busy} onChange={(checked) => setFunctionTypeDraft({ ...functionTypeDraft, act_in_place_allowed: checked })} label="i18n:govoplan-organizations.act_in_place.49b942bd" />
</div>
{renderChangeRequestField()}
</>
);
}
return (
<>
<SluggedFields draft={functionDraft} onChange={setFunctionDraft} disabled={!canWriteFunctions || busy} />
<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">
<ToggleSwitch checked={functionDraft.delegable} disabled={!canWriteFunctions || busy} onChange={(checked) => setFunctionDraft({ ...functionDraft, delegable: checked })} label="i18n:govoplan-organizations.delegable.b4f0137d" />
<ToggleSwitch checked={functionDraft.act_in_place_allowed} disabled={!canWriteFunctions || busy} onChange={(checked) => setFunctionDraft({ ...functionDraft, act_in_place_allowed: checked })} label="i18n:govoplan-organizations.act_in_place.49b942bd" />
</div>
{renderChangeRequestField()}
</>
);
}
}