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

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