mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-30 06:28:13 +00:00
Initial End to End functionality
This commit is contained in:
164
libs/auth/src/lib/hooks/useCurrentProfile.ts
Normal file
164
libs/auth/src/lib/hooks/useCurrentProfile.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
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. */
|
||||
export const PROFILE_FIELD_SECTION: Record<ProfileField, 'personal' | 'address' | 'emergency'> = {
|
||||
firstName: 'personal',
|
||||
middleName: 'personal',
|
||||
lastName: 'personal',
|
||||
gender: 'personal',
|
||||
dob: 'personal',
|
||||
pob: 'personal',
|
||||
maritalStatus: 'personal',
|
||||
professionId: 'personal',
|
||||
idType: 'address',
|
||||
idNumber: 'address',
|
||||
nationality: 'address',
|
||||
primaryPhoneNumber: 'address',
|
||||
email: 'address',
|
||||
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[];
|
||||
}
|
||||
|
||||
const profileApi = baseApi
|
||||
.enhanceEndpoints({ addTagTypes: ['CurrentProfile'] as const })
|
||||
.injectEndpoints({
|
||||
endpoints: (builder) => ({
|
||||
getMyProfile: builder.query<ProfileMeResponse, void>({
|
||||
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<string, unknown> }
|
||||
>({
|
||||
query: ({ id, body }) => ({ url: `/profiles/${id}`, method: 'PATCH', body }),
|
||||
invalidatesTags: ['CurrentProfile'],
|
||||
}),
|
||||
updateMyAddress: builder.mutation<
|
||||
unknown,
|
||||
{ id: string; body: Record<string, unknown> }
|
||||
>({
|
||||
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,
|
||||
/**
|
||||
* 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)),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user