Files
govoplan-organizations/webui/src/features/organizations/OrganizationFunctionPicker.tsx
T
2026-07-10 21:57:27 +02:00

115 lines
4.1 KiB
TypeScript

import { useEffect, useMemo, useState } from "react";
import type {
OrganizationFunctionLabelContext,
OrganizationFunctionPickerContext,
OrganizationFunctionSelection
} from "@govoplan/core-webui";
import { getOrganizationModel, type OrganizationFunctionItem, type OrganizationModel, type OrganizationUnitItem } from "../../api/organizations";
type FunctionOption = OrganizationFunctionSelection & {
slug: string;
active: boolean;
};
export function OrganizationFunctionPicker({ settings, auth, value, disabled, onChange }: OrganizationFunctionPickerContext) {
const [model, setModel] = useState<OrganizationModel | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const tenantId = (auth.active_tenant ?? auth.tenant).id;
useEffect(() => {
let cancelled = false;
setLoading(true);
setError("");
getOrganizationModel(settings)
.then((nextModel) => {
if (!cancelled) setModel(nextModel);
})
.catch((err: unknown) => {
if (!cancelled) setError(err instanceof Error ? err.message : String(err));
})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => {
cancelled = true;
};
}, [settings.accessToken, settings.apiBaseUrl, tenantId]);
const options = useMemo(() => functionOptions(model), [model]);
const selectedId = value?.functionId ?? "";
return (
<>
<select
value={selectedId}
disabled={disabled || loading || Boolean(error)}
onChange={(event) => {
const selected = options.find((option) => option.functionId === event.target.value);
onChange(selected ?? null);
}}
>
<option value="">{loading ? "i18n:govoplan-organizations.loading_functions.77e8e684" : "i18n:govoplan-organizations.select_function.4895a67d"}</option>
{options.map((option) => (
<option key={option.functionId} value={option.functionId}>
{option.label}{option.organizationUnitLabel ? ` (${option.organizationUnitLabel})` : ""}
</option>
))}
</select>
{error && <p className="muted small-note">i18n:govoplan-organizations.functions_unavailable.4fd6765d</p>}
</>
);
}
export function OrganizationFunctionLabel({ settings, auth, sourceModule, functionId, fallback }: OrganizationFunctionLabelContext) {
const [model, setModel] = useState<OrganizationModel | null>(null);
const tenantId = (auth.active_tenant ?? auth.tenant).id;
useEffect(() => {
let cancelled = false;
getOrganizationModel(settings)
.then((nextModel) => {
if (!cancelled) setModel(nextModel);
})
.catch(() => {
if (!cancelled) setModel(null);
});
return () => {
cancelled = true;
};
}, [settings.accessToken, settings.apiBaseUrl, tenantId]);
const option = useMemo(() => functionOptions(model).find((item) => item.functionId === functionId), [functionId, model]);
if (sourceModule !== "organizations" || !option) return <>{fallback ?? functionId}</>;
return (
<div>
<strong>{option.label}</strong>
<div className="muted small-note">{option.organizationUnitLabel ?? option.slug}</div>
</div>
);
}
function functionOptions(model: OrganizationModel | null): FunctionOption[] {
if (!model) return [];
const unitById = new Map<string, OrganizationUnitItem>(model.units.map((unit) => [unit.id, unit]));
return model.functions
.filter((item) => item.is_active)
.map((item) => toOption(item, unitById.get(item.organization_unit_id)))
.sort((left, right) => {
const unitDelta = (left.organizationUnitLabel ?? "").localeCompare(right.organizationUnitLabel ?? "");
return unitDelta !== 0 ? unitDelta : (left.label ?? left.functionId).localeCompare(right.label ?? right.functionId);
});
}
function toOption(item: OrganizationFunctionItem, unit: OrganizationUnitItem | undefined): FunctionOption {
return {
sourceModule: "organizations",
functionId: item.id,
label: item.name,
organizationUnitId: item.organization_unit_id,
organizationUnitLabel: unit?.name ?? null,
slug: item.slug,
active: item.is_active
};
}