diff --git a/apps/portal/src/app/features/profile/components/OperationsFormContent.tsx b/apps/portal/src/app/features/profile/components/OperationsFormContent.tsx index e643ddd95..4d1babe08 100644 --- a/apps/portal/src/app/features/profile/components/OperationsFormContent.tsx +++ b/apps/portal/src/app/features/profile/components/OperationsFormContent.tsx @@ -20,6 +20,7 @@ import { useGetMyOperatorTypesQuery, useUpdateMyOperatorTypesMutation, } from '@ema-platform/api'; +import { useUpdateMyAccountTypeMutation } from '@ema-platform/auth'; import { notify, ModalFooter } from '@ema-platform/ui'; import { useDateDisplayer } from '@ema-platform/shared'; @@ -35,6 +36,29 @@ const PERSONAL_REGISTRATION_KEYS = [ 'VESSEL_REGISTRATION', ]; +/** + * Declared modes -> portal account type. + * + * The account type is what the server computes portal permissions from, and + * therefore what decides which services appear in the sidebar at all. Modes + * of operation grant nothing on their own: an applicant who declared + * "freight forwarder" here was still left on the seafarer default and never + * saw the logistics or vessel entries. + * + * One type per account against a multi-select, so the widest capability wins: + * a company representative who also sails still needs the company services, + * and the seafarer side stays reachable through their own registration. + */ +function accountTypeFor(keys: string[]): string | null { + if (keys.some((key) => !PERSONAL_REGISTRATION_KEYS.includes(key))) { + // Historical enum value for the logistics company representative. + return 'FRIGHTER'; + } + if (keys.includes('VESSEL_REGISTRATION')) return 'VESSEL_OWNER'; + if (keys.includes('SEAFARER_REGISTRATION')) return 'SEAFARER'; + return null; +} + /** * The applicant's modes of operation — what they do, and therefore which * licences the portal offers them. @@ -58,6 +82,7 @@ export function OperationsFormContent({ const { data: catalogue, isLoading: loadingTypes } = useGetLicenseTypesQuery(); const { data: mine, isLoading: loadingMine } = useGetMyOperatorTypesQuery(); const [save, { isLoading: saving }] = useUpdateMyOperatorTypesMutation(); + const [setAccountType] = useUpdateMyAccountTypeMutation(); const localized = useLocalized(); const declaredIds = useMemo( @@ -118,14 +143,25 @@ export function OperationsFormContent({ async function persist() { try { await save({ licenseTypeIds: selected }).unwrap(); + const selectedKeys = options + .filter((t) => selected.includes(t.id)) + .map((t) => t.key); + + const type = accountTypeFor(selectedKeys); + if (type) { + // Refused for a registered seafarer, whose type the authority owns + // (`registered_seafarer_type_is_fixed`), and for staff. The modes are + // already stored either way, so a refusal here is not the applicant's + // problem to see. + await setAccountType({ type }).unwrap().catch(() => undefined); + } + setConfirmingRemoval(false); notify.success( t('profileOperations.updateSuccessBody'), t('profileOperations.updateSuccessTitle'), ); - onSaved?.( - options.filter((t) => selected.includes(t.id)).map((t) => t.key), - ); + onSaved?.(selectedKeys); } catch (err) { notify.error(extractErrorMessage(err), t('profileOperations.updateErrorTitle')); } diff --git a/libs/auth/src/index.ts b/libs/auth/src/index.ts index a0f46f25f..5b1190836 100644 --- a/libs/auth/src/index.ts +++ b/libs/auth/src/index.ts @@ -37,6 +37,7 @@ export { useGetMyProfileQuery, useUpdateMyProfileMutation, useUpdateMyAddressMutation, + useUpdateMyAccountTypeMutation, PROFILE_FIELDS, PROFILE_FIELD_SECTION, } from "./lib/hooks/useCurrentProfile"; diff --git a/libs/auth/src/lib/hooks/useCurrentProfile.ts b/libs/auth/src/lib/hooks/useCurrentProfile.ts index 6cea8dc9f..985ee61df 100644 --- a/libs/auth/src/lib/hooks/useCurrentProfile.ts +++ b/libs/auth/src/lib/hooks/useCurrentProfile.ts @@ -119,6 +119,22 @@ const profileApi = baseApi query: ({ id, body }) => ({ url: `/profiles/${id}`, method: 'PUT', body }), invalidatesTags: ['CurrentProfile'], }), + /** + * The portal role — seafarer, vessel owner or logistics operator. + * + * Sidebar and route permissions are computed from this server-side + * (`profiles.type`), not from the declared modes of operation, so the + * Operations tab writes it here too. Invalidates the profile so the + * shell repermissions itself without a reload. + */ + updateMyAccountType: builder.mutation({ + query: (body) => ({ + url: '/profiles/me/account-type', + method: 'PUT', + body, + }), + invalidatesTags: (_result, error) => (error ? [] : ['CurrentProfile']), + }), updateMyAddress: builder.mutation< unknown, { id: string; body: Record } @@ -134,6 +150,7 @@ export const { useGetMyProfileQuery, useUpdateMyProfileMutation, useUpdateMyAddressMutation, + useUpdateMyAccountTypeMutation, } = profileApi; export const currentProfileApi = profileApi;