|
|
|
|
@@ -8,20 +8,25 @@ import {
|
|
|
|
|
DataGrid,
|
|
|
|
|
Dialog,
|
|
|
|
|
DismissibleAlert,
|
|
|
|
|
ExplorerTree,
|
|
|
|
|
FormField,
|
|
|
|
|
IconButton,
|
|
|
|
|
LoadingFrame,
|
|
|
|
|
ModuleSubnav,
|
|
|
|
|
PageTitle,
|
|
|
|
|
StatusBadge,
|
|
|
|
|
TableActionGroup,
|
|
|
|
|
ToggleSwitch,
|
|
|
|
|
hasScope,
|
|
|
|
|
isViewSurfaceVisible,
|
|
|
|
|
useEffectiveView,
|
|
|
|
|
useUnsavedDraftGuard,
|
|
|
|
|
usePlatformUiCapabilities,
|
|
|
|
|
useViewSurfaces,
|
|
|
|
|
type ApiSettings,
|
|
|
|
|
type AuthInfo,
|
|
|
|
|
type DataGridColumn,
|
|
|
|
|
type ModuleSubnavGroup,
|
|
|
|
|
type OrganizationFunctionActionContribution,
|
|
|
|
|
type OrganizationFunctionActionsUiCapability
|
|
|
|
|
} from "@govoplan/core-webui";
|
|
|
|
|
import {
|
|
|
|
|
@@ -294,15 +299,6 @@ 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,
|
|
|
|
|
@@ -385,6 +381,8 @@ export default function OrganizationsPage({
|
|
|
|
|
const [selectedUnitId, setSelectedUnitId] = useState("");
|
|
|
|
|
const [changeRequestId, setChangeRequestId] = useState("");
|
|
|
|
|
const functionActionCapabilities = usePlatformUiCapabilities<OrganizationFunctionActionsUiCapability>("organizations.functionActions");
|
|
|
|
|
const effectiveView = useEffectiveView();
|
|
|
|
|
const viewSurfaces = useViewSurfaces();
|
|
|
|
|
|
|
|
|
|
const canWriteModel = hasScope(auth, "organizations:model:write");
|
|
|
|
|
const canWriteUnits = hasScope(auth, "organizations:unit:write");
|
|
|
|
|
@@ -400,8 +398,11 @@ export default function OrganizationsPage({
|
|
|
|
|
() => functionActionCapabilities
|
|
|
|
|
.flatMap((capability) => capability.actions)
|
|
|
|
|
.filter((contribution) => contributionVisible(auth, contribution))
|
|
|
|
|
.filter((contribution) =>
|
|
|
|
|
isViewSurfaceVisible(effectiveView, contribution.surfaceId, viewSurfaces)
|
|
|
|
|
)
|
|
|
|
|
.sort((left, right) => (left.order ?? 100) - (right.order ?? 100)),
|
|
|
|
|
[auth, functionActionCapabilities]
|
|
|
|
|
[auth, effectiveView, functionActionCapabilities, viewSurfaces]
|
|
|
|
|
);
|
|
|
|
|
const unitsByParentId = useMemo(() => {
|
|
|
|
|
const mapped = new Map<string, OrganizationUnitItem[]>();
|
|
|
|
|
@@ -870,7 +871,7 @@ export default function OrganizationsPage({
|
|
|
|
|
{ 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)} /> }
|
|
|
|
|
{ id: "actions", header: "i18n:govoplan-organizations.actions.c3cd636a", width: 72, sticky: "end", resizable: false, align: "right", render: (row) => <TableActionGroup actions={[{ id: "edit", label: "i18n:govoplan-organizations.edit.7dce1220", icon: <Edit3 size={16} aria-hidden="true" />, disabled: !canWriteModel || busy, onClick: () => editUnitType(row) }]} /> }
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const structureColumns: DataGridColumn<OrganizationStructureItem>[] = [
|
|
|
|
|
@@ -878,7 +879,7 @@ export default function OrganizationsPage({
|
|
|
|
|
{ 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)} /> }
|
|
|
|
|
{ id: "actions", header: "i18n:govoplan-organizations.actions.c3cd636a", width: 72, sticky: "end", resizable: false, align: "right", render: (row) => <TableActionGroup actions={[{ id: "edit", label: "i18n:govoplan-organizations.edit.7dce1220", icon: <Edit3 size={16} aria-hidden="true" />, disabled: !canWriteModel || busy, onClick: () => editStructure(row) }]} /> }
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const relationTypeColumns: DataGridColumn<OrganizationRelationTypeItem>[] = [
|
|
|
|
|
@@ -888,7 +889,7 @@ export default function OrganizationsPage({
|
|
|
|
|
{ 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)} /> }
|
|
|
|
|
{ id: "actions", header: "i18n:govoplan-organizations.actions.c3cd636a", width: 72, sticky: "end", resizable: false, align: "right", render: (row) => <TableActionGroup actions={[{ id: "edit", label: "i18n:govoplan-organizations.edit.7dce1220", icon: <Edit3 size={16} aria-hidden="true" />, disabled: !canWriteModel || busy, onClick: () => editRelationType(row) }]} /> }
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const unitColumns: DataGridColumn<OrganizationUnitItem>[] = [
|
|
|
|
|
@@ -897,7 +898,7 @@ export default function OrganizationsPage({
|
|
|
|
|
{ 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)} /> }
|
|
|
|
|
{ id: "actions", header: "i18n:govoplan-organizations.actions.c3cd636a", width: 72, sticky: "end", resizable: false, align: "right", render: (row) => <TableActionGroup actions={[{ id: "edit", label: "i18n:govoplan-organizations.edit.7dce1220", icon: <Edit3 size={16} aria-hidden="true" />, disabled: !canWriteUnits || busy, onClick: () => editUnit(row) }]} /> }
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const relationColumns: DataGridColumn<OrganizationRelationItem>[] = [
|
|
|
|
|
@@ -906,7 +907,7 @@ export default function OrganizationsPage({
|
|
|
|
|
{ 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)} /> }
|
|
|
|
|
{ id: "actions", header: "i18n:govoplan-organizations.actions.c3cd636a", width: 72, sticky: "end", resizable: false, align: "right", render: (row) => <TableActionGroup actions={[{ id: "edit", label: "i18n:govoplan-organizations.edit.7dce1220", icon: <Edit3 size={16} aria-hidden="true" />, disabled: !canWriteUnits || busy, onClick: () => editRelation(row) }]} /> }
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const functionTypeColumns: DataGridColumn<OrganizationFunctionTypeItem>[] = [
|
|
|
|
|
@@ -915,7 +916,7 @@ export default function OrganizationsPage({
|
|
|
|
|
{ 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)} /> }
|
|
|
|
|
{ id: "actions", header: "i18n:govoplan-organizations.actions.c3cd636a", width: 72, sticky: "end", resizable: false, align: "right", render: (row) => <TableActionGroup actions={[{ id: "edit", label: "i18n:govoplan-organizations.edit.7dce1220", icon: <Edit3 size={16} aria-hidden="true" />, disabled: !canWriteModel || busy, onClick: () => editFunctionType(row) }]} /> }
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const functionColumns: DataGridColumn<OrganizationFunctionItem>[] = [
|
|
|
|
|
@@ -926,20 +927,26 @@ export default function OrganizationsPage({
|
|
|
|
|
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
|
|
|
|
|
{
|
|
|
|
|
id: "actions",
|
|
|
|
|
header: "",
|
|
|
|
|
width: functionActionContributions.length ? 220 : 88,
|
|
|
|
|
header: "i18n:govoplan-organizations.actions.c3cd636a",
|
|
|
|
|
width: 88 + (functionActionContributions.length * 40),
|
|
|
|
|
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>
|
|
|
|
|
))}
|
|
|
|
|
/>
|
|
|
|
|
)
|
|
|
|
|
resizable: false,
|
|
|
|
|
align: "right",
|
|
|
|
|
render: (row) => <TableActionGroup actions={[
|
|
|
|
|
{
|
|
|
|
|
id: "edit",
|
|
|
|
|
label: "i18n:govoplan-organizations.edit.7dce1220",
|
|
|
|
|
icon: <Edit3 size={16} aria-hidden="true" />,
|
|
|
|
|
disabled: !canWriteFunctions || busy,
|
|
|
|
|
onClick: () => editFunction(row)
|
|
|
|
|
},
|
|
|
|
|
...functionActionContributions.map((contribution) => ({
|
|
|
|
|
id: contribution.id,
|
|
|
|
|
label: contribution.label,
|
|
|
|
|
icon: contribution.icon,
|
|
|
|
|
onClick: () => contribution.onClick({ settings, auth, function: row })
|
|
|
|
|
}))
|
|
|
|
|
]} />
|
|
|
|
|
}
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
@@ -1003,41 +1010,42 @@ export default function OrganizationsPage({
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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">
|
|
|
|
|
<div className="explorer-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>
|
|
|
|
|
{model.units.length ? (
|
|
|
|
|
<ExplorerTree
|
|
|
|
|
nodes={unitsByParentId.get("") ?? []}
|
|
|
|
|
getNodeId={(unit) => unit.id}
|
|
|
|
|
getNodeLabel={(unit) => unit.name}
|
|
|
|
|
getNodeChildren={(unit) => unitsByParentId.get(unit.id) ?? []}
|
|
|
|
|
activeId={selectedUnitId}
|
|
|
|
|
collapsible={false}
|
|
|
|
|
depth={0}
|
|
|
|
|
className="explorer-tree-scroll-region"
|
|
|
|
|
getNodeWrapStyle={(_unit, context) => ({ paddingLeft: `${Math.min(context.depth * 18, 72)}px` })}
|
|
|
|
|
onOpen={(unit) => setSelectedUnitId(unit.id)}
|
|
|
|
|
renderNodeContent={(unit) => (
|
|
|
|
|
<span className="explorer-tree-node-content">
|
|
|
|
|
<strong>{unit.name}</strong>
|
|
|
|
|
<small>{unitTypeById.get(unit.unit_type_id || "")?.name ?? "i18n:govoplan-organizations.no_unit_type.73e799f5"}</small>
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
renderNodeActions={(unit) => (
|
|
|
|
|
<IconButton
|
|
|
|
|
label="i18n:govoplan-organizations.add_subunit.8256a8f7"
|
|
|
|
|
icon={<Plus size={16} aria-hidden="true" />}
|
|
|
|
|
variant="ghost"
|
|
|
|
|
disabled={!canWriteUnits || busy}
|
|
|
|
|
onClick={() => addSubunit(unit)}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
) : <p className="muted small-note">i18n:govoplan-organizations.no_units_found.eea2dd0c</p>}
|
|
|
|
|
</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" />
|
|
|
|
|
@@ -1129,7 +1137,7 @@ export default function OrganizationsPage({
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<form id={formId} className="organizations-form-grid" onSubmit={(event) => void submit(event)}>
|
|
|
|
|
<form id={formId} className="admin-form-grid two-columns" onSubmit={(event) => void submit(event)}>
|
|
|
|
|
{renderEditorFields()}
|
|
|
|
|
</form>
|
|
|
|
|
</Dialog>
|
|
|
|
|
|