fix(backoffice): scope copy-permissions-from to the selected unit (EDRFREIGHT-308)

The "Copy Permissions From" dropdown in the position-type form only
filtered candidates by the selected organization's full unit set,
ignoring the form's own "Select Unit" field entirely — so switching
units never re-scoped the list, unlike the equivalent unit filter on
the position-management table.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Nathnael
2026-07-28 12:27:14 +00:00
parent e823874d39
commit c2375a5306
4 changed files with 26 additions and 23 deletions

View File

@@ -2653,6 +2653,7 @@
"copyPermissionsHint": "የነበረ የቦታ አይነት ይምረጡ፤ ፍቃዶቹ አስቀድመው ይሞላሉ፣ ከታች ማስተካከል ይችላሉ።",
"copyPermissionsFailed": "ፍቃዶችን መቅዳት አልተቻለም",
"selectOrganizationToCopy": "መቅዳት የሚችሏቸውን የቦታ ዓይነቶች ለማየት መጀመሪያ ድርጅት ይምረጡ",
"selectUnitToCopy": "መቅዳት የሚችሏቸውን የቦታ ዓይነቶች ለማየት መጀመሪያ ክፍል ይምረጡ",
"cannotClearAllPermissions": "ተቀምጧል። ፍቃዶቹ አልተቀየሩም — ይህ የቦታ ዓይነት ቢያንስ አንድ ፍቃድ ሊኖረው ይገባል።",
"permissionsSelected": "{{count}} ተመርጠዋል",
"positionTypeCreated": "የቦታ ዓይነት ተፈጥሯል",

View File

@@ -2762,6 +2762,7 @@
"copyPermissionsHint": "Pick an existing position type to pre-fill its permissions, then edit below.",
"copyPermissionsFailed": "Failed to copy permissions",
"selectOrganizationToCopy": "Select an organization to see the position types you can copy from",
"selectUnitToCopy": "Select a unit to see the position types you can copy from",
"cannotClearAllPermissions": "Saved. Permissions were left unchanged — this position type must keep at least one permission.",
"permissionsSelected": "{{count}} selected",
"positionTypeCreated": "Position type created",

View File

@@ -1888,6 +1888,7 @@
"copyPermissionsHint": "Choisissez un type de poste existant pour préremplir ses autorisations, puis modifiez ci-dessous.",
"copyPermissionsFailed": "Échec de la copie des autorisations",
"selectOrganizationToCopy": "Sélectionnez une organisation pour voir les types de poste que vous pouvez copier",
"selectUnitToCopy": "Sélectionnez une unité pour voir les types de poste que vous pouvez copier",
"cannotClearAllPermissions": "Enregistré. Les autorisations n'ont pas été modifiées — ce type de poste doit conserver au moins une autorisation.",
"permissionsSelected": "{{count}} sélectionné(s)",
"positionTypeCreated": "Type de poste créé",

View File

@@ -115,6 +115,7 @@ export const CreatePositionForm = ({
});
const selectedOrganizationId = form.watch("organizationId");
const selectedUnitId = form.watch("unitId");
const { organizationsResponse, isLoading: isLoadingOrgs } = useOrganizations(
"Org",
@@ -163,36 +164,32 @@ export const CreatePositionForm = ({
enabled: mode === "edit" && !!positionTypeId,
});
// A position type belongs to a unit, and a unit to an organization — IAM has
// no organizationId on the type itself and no organization-scoped route, so
// the picked org narrows the list through its units. isSystem types are the
// shared "commons" and stay available to every organization.
const orgUnitIds = useMemo(
() =>
new Set(
(unitsResponse?.data?.items ?? []).map((unit: UnitDto) => unit.id),
),
[unitsResponse],
);
// A position type belongs to a single unit — scope copy sources to the
// selected unit, same as the "Select Unit" filter on the position list page.
// isSystem types are the shared "commons" and stay available everywhere.
const copyFromOptions = useMemo(() => {
if (!selectedOrganizationId) return [];
if (!selectedUnitId) return [];
return positionTypes.filter(
(type: PositionTypeDto) =>
type.id !== positionTypeId &&
(type.isSystem || (!!type.unitId && orgUnitIds.has(type.unitId))),
(type.isSystem || type.unitId === selectedUnitId),
);
}, [positionTypes, orgUnitIds, selectedOrganizationId, positionTypeId]);
}, [positionTypes, selectedUnitId, positionTypeId]);
// Reset the selected unit when the organization changes so a unit from a
// different org can't be submitted by mistake. The copy source is cleared
// too — it is scoped to the old organization.
// different org can't be submitted by mistake.
useEffect(() => {
if (mode === "edit") return;
form.setValue("unitId", "");
setCopyFromPositionId("");
}, [selectedOrganizationId, mode, form]);
// The copy source is scoped to the selected unit — clear it whenever the
// unit changes (including as a side effect of the org reset above) so a
// stale selection from a different unit can't be submitted.
useEffect(() => {
setCopyFromPositionId("");
}, [selectedUnitId]);
useEffect(() => {
if (mode !== "edit" || !initialValues || !positionTypeId) return;
if (hasLoadedEditData.current) return;
@@ -335,11 +332,13 @@ export const CreatePositionForm = ({
const copyFromPlaceholder = !selectedOrganizationId
? t("contentManagement.selectOrganizationToCopy")
: isCopying || isLoadingPositionTypes || isLoadingUnits
? t("common.loading")
: isErrorPositionTypes
? t("contentManagement.failedToLoadPositionTypes")
: t("contentManagement.selectPositionToCopy");
: !selectedUnitId
? t("contentManagement.selectUnitToCopy")
: isCopying || isLoadingPositionTypes || isLoadingUnits
? t("common.loading")
: isErrorPositionTypes
? t("contentManagement.failedToLoadPositionTypes")
: t("contentManagement.selectPositionToCopy");
return (
<Form {...form}>
@@ -454,6 +453,7 @@ export const CreatePositionForm = ({
onValueChange={handleCopyFrom}
disabled={
!selectedOrganizationId ||
!selectedUnitId ||
isLoadingPositionTypes ||
isLoadingUnits ||
isCopying