diff --git a/src/apps/profiles/src/member-profile/work-expirence/ModifyWorkExpirenceModal/ModifyWorkExpirenceModal.tsx b/src/apps/profiles/src/member-profile/work-expirence/ModifyWorkExpirenceModal/ModifyWorkExpirenceModal.tsx index fe56f0b97..a23d08e6c 100644 --- a/src/apps/profiles/src/member-profile/work-expirence/ModifyWorkExpirenceModal/ModifyWorkExpirenceModal.tsx +++ b/src/apps/profiles/src/member-profile/work-expirence/ModifyWorkExpirenceModal/ModifyWorkExpirenceModal.tsx @@ -1,17 +1,18 @@ /* eslint-disable complexity */ -import { Dispatch, FC, MutableRefObject, SetStateAction, useRef, useState } from 'react' +import { ChangeEvent, Dispatch, FC, MutableRefObject, SetStateAction, useEffect, useRef, useState } from 'react' import { bind, sortBy, trim } from 'lodash' import { toast } from 'react-toastify' import classNames from 'classnames' -import { BaseModal, Button, IconOutline, InputDatePicker, InputSelect, InputText } from '~/libs/ui' +import { BaseModal, Button, IconOutline, InputDatePicker, InputSelect, InputText, InputTextarea } from '~/libs/ui' import { updateDeleteOrCreateMemberTraitAsync, UserProfile, UserTrait, UserTraitCategoryNames, UserTraitIds, } from '~/libs/core' -import { getIndustryOptionLabel, getIndustryOptionValue, INDUSTRIES_OPTIONS } from '~/libs/shared' +import { getIndustryOptionLabel, getIndustryOptionValue, INDUSTRIES_OPTIONS, InputSkillSelector } from '~/libs/shared' +import { fetchSkillsByIds } from '~/libs/shared/lib/services/standard-skills' import { WorkExpirenceCard } from '../WorkExpirenceCard' @@ -32,10 +33,10 @@ const ModifyWorkExpirenceModal: FC = (props: Modi = useState(props.workExpirence?.length === 0 || false) const [formValues, setFormValues]: [ - { [key: string]: string | boolean | Date | undefined }, - Dispatch> + { [key: string]: string | boolean | Date | any[] | undefined }, + Dispatch> ] - = useState<{ [key: string]: string | boolean | Date | undefined }>({}) + = useState<{ [key: string]: string | boolean | Date | any[] | undefined }>({}) const [formErrors, setFormErrors]: [ { [key: string]: string }, @@ -56,6 +57,86 @@ const ModifyWorkExpirenceModal: FC = (props: Modi ] = useState(props.workExpirence) + const [skillNamesMap, setSkillNamesMap] = useState>({}) + const [loadingSkills, setLoadingSkills] = useState(false) + const fetchedSkillIdsRef = useRef>(new Set()) + + useEffect(() => { + if (!workExpirence) { + setLoadingSkills(false) + return + } + + const allSkillIds = new Set() + workExpirence.forEach((work: UserTrait) => { + if (work.associatedSkills && Array.isArray(work.associatedSkills)) { + work.associatedSkills.forEach((skillId: string) => { + if (skillId && typeof skillId === 'string') { + allSkillIds.add(skillId) + } + }) + } + }) + + if (allSkillIds.size > 0) { + const skillIdsToFetch = Array.from(allSkillIds) + .filter(id => !fetchedSkillIdsRef.current.has(id)) + + if (skillIdsToFetch.length > 0) { + setLoadingSkills(true) + skillIdsToFetch.forEach(id => fetchedSkillIdsRef.current.add(id)) + + fetchSkillsByIds(skillIdsToFetch) + .then(skills => { + setSkillNamesMap(prevMap => { + const newMap: Record = { ...prevMap } + skills.forEach(skill => { + if (skill.id && skill.name) { + newMap[skill.id] = skill.name + } + }) + skillIdsToFetch.forEach(skillId => { + if (!newMap[skillId]) { + newMap[skillId] = skillId + } + }) + return newMap + }) + }) + .catch(() => { + setSkillNamesMap(prevMap => { + const fallbackMap: Record = { ...prevMap } + skillIdsToFetch.forEach(skillId => { + if (!fallbackMap[skillId]) { + fallbackMap[skillId] = skillId + } + }) + return fallbackMap + }) + }) + .finally(() => { + setLoadingSkills(false) + }) + } else { + setLoadingSkills(false) + } + } else { + // No skills to fetch + setLoadingSkills(false) + } + }, [workExpirence]) + + const areSkillsLoaded = (work: UserTrait): boolean => { + if (!work.associatedSkills || !Array.isArray(work.associatedSkills) || work.associatedSkills.length === 0) { + return true + } + + return work.associatedSkills.every((skillId: string) => { + const skillName = skillNamesMap[skillId] + return skillName && skillName !== skillId + }) + } + const industryOptions: any = sortBy(INDUSTRIES_OPTIONS) .map(v => ({ label: getIndustryOptionLabel(v), @@ -89,13 +170,16 @@ const ModifyWorkExpirenceModal: FC = (props: Modi }) } - function handleFormValueChange(key: string, event: React.ChangeEvent): void { + function handleFormValueChange( + key: string, + event: React.ChangeEvent, + ): void { let value: string | boolean | Date | undefined const oldFormValues = { ...formValues } switch (key) { case 'currentlyWorking': - value = event.target.checked + value = (event.target as HTMLInputElement).checked if (value) { oldFormValues.endDate = undefined } @@ -116,6 +200,17 @@ const ModifyWorkExpirenceModal: FC = (props: Modi }) } + function handleSkillsChange(event: ChangeEvent): void { + const selectedSkills = (event.target as any).value || [] + setFormValues({ + ...formValues, + associatedSkills: selectedSkills.map((skill: any) => ({ + id: skill.value || skill.id, + name: skill.label || skill.name, + })), + }) + } + function resetForm(): void { setFormValues({}) setFormErrors({}) @@ -173,9 +268,10 @@ const ModifyWorkExpirenceModal: FC = (props: Modi : undefined const updatedWorkExpirence: UserTrait = { - cityTown: formValues.city, + associatedSkills: (formValues.associatedSkills as any[])?.map((s: any) => s.id || s) || [], company: companyName, companyName, + description: (formValues.description as string) || undefined, endDate: endDateIso, industry: formValues.industry, position: formValues.position, @@ -200,20 +296,41 @@ const ModifyWorkExpirenceModal: FC = (props: Modi resetForm() } - function handleWorkExpirenceEdit(indx: number): void { + async function handleWorkExpirenceEdit(indx: number): Promise { const work: UserTrait = workExpirence ? workExpirence[indx] : {} setEditedItemIndex(indx) + let associatedSkills: any[] = [] + if (work.associatedSkills && Array.isArray(work.associatedSkills) && work.associatedSkills.length > 0) { + try { + const skills = await fetchSkillsByIds( + work.associatedSkills.filter((id): id is string => typeof id === 'string'), + ) + const skillsMap = new Map(skills.map(s => [s.id, s.name])) + + associatedSkills = work.associatedSkills.map((skillId: string) => ({ + id: skillId, + name: skillsMap.get(skillId) || '', + })) + } catch { + associatedSkills = work.associatedSkills.map((skillId: string) => ({ + id: skillId, + name: skillNamesMap[skillId] || '', + })) + } + } + setFormValues({ - city: work.cityTown || work.city, - company: work.company || work.companyName, - currentlyWorking: work.working, + associatedSkills, + company: (work.company || work.companyName || '') as string, + currentlyWorking: work.working || false, + description: work.description || '', endDate: work.timePeriodTo ? new Date(work.timePeriodTo) : (work.endDate ? new Date(work.endDate) : undefined), - industry: work.industry, - position: work.position, + industry: work.industry || '', + position: (work.position || '') as string, startDate: work.timePeriodFrom ? new Date(work.timePeriodFrom) : (work.startDate ? new Date(work.startDate) : undefined), @@ -286,7 +403,12 @@ const ModifyWorkExpirenceModal: FC = (props: Modi className={styles.workExpirenceCardWrap} key={uniqueKey || `${work.position}-${indx}`} > - +
) } diff --git a/src/libs/shared/lib/services/standard-skills/standard-skills.service.ts b/src/libs/shared/lib/services/standard-skills/standard-skills.service.ts index d7c225254..346261290 100644 --- a/src/libs/shared/lib/services/standard-skills/standard-skills.service.ts +++ b/src/libs/shared/lib/services/standard-skills/standard-skills.service.ts @@ -1,3 +1,6 @@ +import { useMemo } from 'react' +import useSWR, { SWRResponse } from 'swr' + import { EnvironmentConfig } from '~/config' import { UserSkill, xhrGetAsync, xhrPostAsync, xhrPutAsync } from '~/libs/core' @@ -43,3 +46,57 @@ export async function updateMemberSkills( skills, }) } + +/** + * Fetcher function for useSWR to fetch skills by their IDs + * @param skillIds Array of skill UUIDs + * @returns Promise with array of UserSkill objects + */ +async function fetchSkillsByIdsFetcher(skillIds: string[]): Promise { + if (!skillIds || skillIds.length === 0) { + return [] + } + + try { + const skillPromises = skillIds.map(skillId => xhrGetAsync(`${baseUrl}/skills/${skillId}`) + .catch(() => undefined)) + const results = await Promise.all(skillPromises) + return results.filter((skill): skill is UserSkill => skill !== null && skill !== undefined) + } catch { + return [] + } +} + +/** + * Hook to fetch skills by their IDs using SWR + * @param skillIds Array of skill UUIDs + * @returns SWRResponse with array of UserSkill objects + */ +export function useSkillsByIds(skillIds: string[] | undefined): SWRResponse { + const swrKey = useMemo(() => { + if (!skillIds || skillIds.length === 0) { + return undefined + } + + return ['skills-by-ids', [...skillIds].sort() + .join(',')] + }, [skillIds]) + + return useSWR( + swrKey, + () => fetchSkillsByIdsFetcher(skillIds!), + { + revalidateOnFocus: false, + revalidateOnReconnect: false, + }, + ) +} + +/** + * Fetch skills by their IDs (legacy async function for backward compatibility) + * @param skillIds Array of skill UUIDs + * @returns Promise with array of UserSkill objects + */ +export async function fetchSkillsByIds(skillIds: string[]): Promise { + return fetchSkillsByIdsFetcher(skillIds) +}