import { useEffect, useMemo } from 'react'; import { useDispatch } from 'react-redux'; import { baseApi } from '@ema-platform/api'; import { authStorage } from '../utils/auth-storage'; import { setCurrentProfile } from '../store/auth.slice'; import type { CurrentProfile } from '../types/auth.types'; /** * Profile fields the portal knows how to collect. * * Mirrors `PROFILE_FIELDS` in the API (`module/profile/profile-completeness.ts`). * Screens name the fields they need from this list rather than checking * properties ad hoc, so "what is missing" has one definition. */ export const PROFILE_FIELDS = [ 'firstName', 'middleName', 'lastName', 'gender', 'dob', 'pob', 'maritalStatus', 'professionId', 'idType', 'idNumber', 'nationality', 'primaryPhoneNumber', 'email', 'regionId', 'cityId', 'subCityId', 'woredaId', 'streetAddress', 'emergencyContactName', 'emergencyContactPhone', 'emergencyContactRelation', ] as const; export type ProfileField = (typeof PROFILE_FIELDS)[number]; /** * Which `/profile` tab collects a field — used to build deep links. * * `personal` here means the Personal tab (account details: name, username, * email, phone), which is `/profile`'s `'personal'` panel. Name/DOB/gender/ * profession live on the *Maritime Profile* panel instead, whose tab key is * `'profile'` — easy to conflate with the field-section name of the same * word, so it's called out here rather than left implicit. */ export const PROFILE_FIELD_SECTION: Record< ProfileField, 'personal' | 'profile' | 'address' | 'emergency' > = { firstName: 'profile', middleName: 'profile', lastName: 'profile', gender: 'profile', dob: 'profile', pob: 'profile', maritalStatus: 'profile', professionId: 'profile', idType: 'address', idNumber: 'address', nationality: 'address', primaryPhoneNumber: 'personal', email: 'personal', regionId: 'address', cityId: 'address', subCityId: 'address', woredaId: 'address', streetAddress: 'address', emergencyContactName: 'emergency', emergencyContactPhone: 'emergency', emergencyContactRelation: 'emergency', }; /** What a screen needs before it can do its job. */ export interface ProfileRequirement { fields: ProfileField[]; /** Shown to the applicant — why this is being asked for, in plain language. */ reason: string; } export interface ProfileMeResponse { profile: CurrentProfile; completeness: number; missing: ProfileField[]; /** * Every permission key the caller effectively holds (role + portal * account-type + position grants), computed server-side. Feeds * `usePermissions()`. */ permissions?: string[]; /** * Whether the profile can apply for a CoC/CoP right now: seafarer * registration approved, plus a verified sea service record and a * verified medical certificate. Computed server-side so the "Apply" * button and the API's own eligibility check can never disagree. */ eligibleForCoc: boolean; } const profileApi = baseApi .enhanceEndpoints({ addTagTypes: ['CurrentProfile'] as const }) .injectEndpoints({ endpoints: (builder) => ({ getMyProfile: builder.query({ query: () => ({ url: '/profiles/me' }), providesTags: ['CurrentProfile'], }), /** * Saves one tab of `/profile`. Invalidates the profile so the * completeness meter and every requirement gate recompute at once. */ updateMyProfile: builder.mutation< unknown, { id: string; body: Record } >({ query: ({ id, body }) => ({ url: `/profiles/${id}`, method: 'PUT', body }), invalidatesTags: ['CurrentProfile'], }), updateMyAddress: builder.mutation< unknown, { id: string; body: Record } >({ query: ({ id, body }) => ({ url: `/addresses/${id}`, method: 'PATCH', body }), invalidatesTags: ['CurrentProfile'], }), }), overrideExisting: false, }); export const { useGetMyProfileQuery, useUpdateMyProfileMutation, useUpdateMyAddressMutation, } = profileApi; export const currentProfileApi = profileApi; /** * The one way to get at the signed-in user's profile. * * Replaces `authStorage.getProfileId()`, which only ever held a value if the * user had been through the (now deleted) setup wizard. Pages that read it * directly did `if (!profileId) return;` and rendered blank forever for anyone * who signed up afterwards. * * Resolution order: RTK Query cache → `authStorage` (so the id is available * synchronously on the very first render) → `GET /profiles/me`, which * provisions a profile if the user has none. The resolved id is written back * to storage. Never blocks render: `profileId` may be undefined for a tick, * and callers should show a loading or empty state rather than bail out. */ export function useCurrentProfile() { const dispatch = useDispatch(); const { data, isLoading, isFetching, error, refetch } = useGetMyProfileQuery(); const profileId = data?.profile?.id ?? authStorage.getProfileId(); useEffect(() => { if (!data?.profile) return; authStorage.setProfileId(data.profile.id); dispatch(setCurrentProfile(data.profile)); }, [data?.profile, dispatch]); const missing = useMemo(() => data?.missing ?? [], [data?.missing]); return { profileId, profile: data?.profile, isLoading, isFetching, error, refetch, completeness: data?.completeness ?? 0, missing, eligibleForCoc: data?.eligibleForCoc ?? false, /** * True when nothing the requirement asks for is still blank. Unknown * profile (still loading) reads as not-ready, so a caller never submits * against data it has not seen. */ isReadyFor: (requirement: ProfileRequirement) => Boolean(data) && requirement.fields.every((field) => !missing.includes(field)), /** The subset of a requirement that is still outstanding. */ gapsFor: (requirement: ProfileRequirement) => requirement.fields.filter((field) => missing.includes(field)), }; }