diff --git a/app/expert-finder/library/[searchId]/components/ExpertEditModal.tsx b/app/expert-finder/library/[searchId]/components/ExpertEditModal.tsx deleted file mode 100644 index e9d4a38fb..000000000 --- a/app/expert-finder/library/[searchId]/components/ExpertEditModal.tsx +++ /dev/null @@ -1,142 +0,0 @@ -'use client'; - -import { useState, useEffect } from 'react'; -import { BaseModal } from '@/components/ui/BaseModal'; -import { Button } from '@/components/ui/Button'; -import { Input } from '@/components/ui/form/Input'; -import { LoadingButton } from '@/components/ui/LoadingButton'; -import type { ExpertResult } from '@/types/expertFinder'; -import type { PatchExpertPayload } from '@/services/expertFinder.service'; -import { extractApiErrorMessage } from '@/services/lib/serviceUtils'; - -export interface ExpertEditModalProps { - isOpen: boolean; - onClose: () => void; - expert: ExpertResult; - expertId: number; - onSave: (expertId: number, payload: PatchExpertPayload) => Promise; -} - -function buildPayload(values: { - honorific: string; - firstName: string; - middleName: string; - lastName: string; - nameSuffix: string; -}): PatchExpertPayload { - const payload: PatchExpertPayload = {}; - const t = (s: string) => s.trim(); - - if (t(values.honorific)) payload.honorific = t(values.honorific); - if (t(values.firstName)) payload.first_name = t(values.firstName); - if (t(values.middleName)) payload.middle_name = t(values.middleName); - if (t(values.lastName)) payload.last_name = t(values.lastName); - if (t(values.nameSuffix)) payload.name_suffix = t(values.nameSuffix); - - return payload; -} - -export function ExpertEditModal({ - isOpen, - onClose, - expert, - expertId, - onSave, -}: ExpertEditModalProps) { - const [honorific, setHonorific] = useState(''); - const [firstName, setFirstName] = useState(''); - const [middleName, setMiddleName] = useState(''); - const [lastName, setLastName] = useState(''); - const [nameSuffix, setNameSuffix] = useState(''); - const [submitError, setSubmitError] = useState(null); - const [isSubmitting, setIsSubmitting] = useState(false); - - useEffect(() => { - if (!isOpen) return; - setHonorific(expert.honorific); - setFirstName(expert.firstName); - setMiddleName(expert.middleName); - setLastName(expert.lastName); - setNameSuffix(expert.nameSuffix); - setSubmitError(null); - }, [isOpen, expert]); - - const handleSubmit = async () => { - setSubmitError(null); - setIsSubmitting(true); - try { - const payload = buildPayload({ - honorific, - firstName, - middleName, - lastName, - nameSuffix, - }); - if (Object.keys(payload).length === 0) { - setSubmitError('Change at least one field to save.'); - setIsSubmitting(false); - return; - } - await onSave(expertId, payload); - onClose(); - } catch (err) { - setSubmitError(extractApiErrorMessage(err, 'Failed to update expert')); - } finally { - setIsSubmitting(false); - } - }; - - return ( - -
-
- setHonorific(e.target.value)} - placeholder="e.g. Dr, Prof, Mr, Ms" - /> - setFirstName(e.target.value)} - /> - setMiddleName(e.target.value)} - /> - setLastName(e.target.value)} /> - setNameSuffix(e.target.value)} - placeholder="e.g. Jr., Sr., III, IV" - /> -
- {submitError ?

{submitError}

: null} -
- - void handleSubmit()} - isLoading={isSubmitting} - loadingText="Saving…" - > - Save - -
-
-
- ); -} diff --git a/app/expert-finder/library/[searchId]/components/ExpertFormModal.tsx b/app/expert-finder/library/[searchId]/components/ExpertFormModal.tsx new file mode 100644 index 000000000..67fb21342 --- /dev/null +++ b/app/expert-finder/library/[searchId]/components/ExpertFormModal.tsx @@ -0,0 +1,257 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { BaseModal } from '@/components/ui/BaseModal'; +import { Button } from '@/components/ui/Button'; +import { Input } from '@/components/ui/form/Input'; +import { Textarea } from '@/components/ui/form/Textarea'; +import { LoadingButton } from '@/components/ui/LoadingButton'; +import type { ExpertResult } from '@/types/expertFinder'; +import type { AddExpertPayload, PatchExpertPayload } from '@/services/expertFinder.service'; +import { extractApiErrorMessage } from '@/services/lib/serviceUtils'; +import { usePatchExpert, useAddExpert } from '@/hooks/useExpertFinder'; + +export interface ExpertFormModalProps { + isOpen: boolean; + onClose: () => void; + searchId: string; + expert?: ExpertResult; + onSuccess: () => Promise; +} + +function buildEditPayload(values: { + honorific: string; + firstName: string; + middleName: string; + lastName: string; + nameSuffix: string; +}): PatchExpertPayload { + const payload: PatchExpertPayload = {}; + const t = (s: string) => s.trim(); + + if (t(values.honorific)) payload.honorific = t(values.honorific); + if (t(values.firstName)) payload.first_name = t(values.firstName); + if (t(values.middleName)) payload.middle_name = t(values.middleName); + if (t(values.lastName)) payload.last_name = t(values.lastName); + if (t(values.nameSuffix)) payload.name_suffix = t(values.nameSuffix); + + return payload; +} + +function buildAddPayload(values: { + email: string; + honorific: string; + firstName: string; + middleName: string; + lastName: string; + nameSuffix: string; + academicTitle: string; + affiliation: string; + expertise: string; + notes: string; +}): AddExpertPayload { + const t = (s: string) => s.trim(); + const payload: AddExpertPayload = { email: t(values.email).toLowerCase() }; + + if (t(values.honorific)) payload.honorific = t(values.honorific); + if (t(values.firstName)) payload.first_name = t(values.firstName); + if (t(values.middleName)) payload.middle_name = t(values.middleName); + if (t(values.lastName)) payload.last_name = t(values.lastName); + if (t(values.nameSuffix)) payload.name_suffix = t(values.nameSuffix); + if (t(values.academicTitle)) payload.academic_title = t(values.academicTitle); + if (t(values.affiliation)) payload.affiliation = t(values.affiliation); + if (t(values.expertise)) payload.expertise = t(values.expertise); + if (t(values.notes)) payload.notes = t(values.notes); + + return payload; +} + +export function ExpertFormModal({ + isOpen, + onClose, + searchId, + expert, + onSuccess, +}: ExpertFormModalProps) { + const [, patchExpert] = usePatchExpert(); + const [, createExpert] = useAddExpert(); + const isAdd = expert == null; + + const [email, setEmail] = useState(''); + const [honorific, setHonorific] = useState(''); + const [firstName, setFirstName] = useState(''); + const [middleName, setMiddleName] = useState(''); + const [lastName, setLastName] = useState(''); + const [nameSuffix, setNameSuffix] = useState(''); + const [academicTitle, setAcademicTitle] = useState(''); + const [affiliation, setAffiliation] = useState(''); + const [expertise, setExpertise] = useState(''); + const [notes, setNotes] = useState(''); + const [submitError, setSubmitError] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + useEffect(() => { + if (!isOpen) return; + setSubmitError(null); + if (expert != null) { + setHonorific(expert.honorific); + setFirstName(expert.firstName); + setMiddleName(expert.middleName); + setLastName(expert.lastName); + setNameSuffix(expert.nameSuffix); + } else { + setEmail(''); + setHonorific(''); + setFirstName(''); + setMiddleName(''); + setLastName(''); + setNameSuffix(''); + setAcademicTitle(''); + setAffiliation(''); + setExpertise(''); + setNotes(''); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isOpen, isAdd]); + + const handleSubmit = async () => { + setSubmitError(null); + setIsSubmitting(true); + try { + if (expert != null) { + const payload = buildEditPayload({ + honorific, + firstName, + middleName, + lastName, + nameSuffix, + }); + if (Object.keys(payload).length === 0) { + setSubmitError('Change at least one field to save.'); + return; + } + await patchExpert(expert.expertId!, payload); + } else { + if (!email.trim()) { + setSubmitError('Email is required.'); + return; + } + await createExpert( + searchId, + buildAddPayload({ + email, + honorific, + firstName, + middleName, + lastName, + nameSuffix, + academicTitle, + affiliation, + expertise, + notes, + }) + ); + } + await onSuccess(); + onClose(); + } catch (err) { + setSubmitError(extractApiErrorMessage(err, 'Failed to save expert')); + } finally { + setIsSubmitting(false); + } + }; + + return ( + +
+ {isAdd && ( + setEmail(e.target.value)} + placeholder="jane.doe@university.edu" + /> + )} +
+ setHonorific(e.target.value)} + placeholder="e.g. Dr, Prof, Mr, Ms" + /> + setFirstName(e.target.value)} + /> + setMiddleName(e.target.value)} + /> + setLastName(e.target.value)} /> + setNameSuffix(e.target.value)} + placeholder="e.g. Jr., Sr., III, IV" + /> + {isAdd && ( + setAcademicTitle(e.target.value)} + placeholder="e.g. Associate Professor" + /> + )} +
+ {isAdd && ( + <> + setAffiliation(e.target.value)} + placeholder="e.g. MIT" + /> + setExpertise(e.target.value)} + placeholder="e.g. Quantum computing" + /> +