534 lines
20 KiB
TypeScript
534 lines
20 KiB
TypeScript
import {
|
||
Button,
|
||
FormContainer,
|
||
FormItem,
|
||
Input,
|
||
Notification,
|
||
Radio,
|
||
Select,
|
||
Tabs,
|
||
toast,
|
||
} from '@/components/ui'
|
||
import TabContent from '@/components/ui/Tabs/TabContent'
|
||
import TabList from '@/components/ui/Tabs/TabList'
|
||
import TabNav from '@/components/ui/Tabs/TabNav'
|
||
import { UserInfoViewModel } from '@/proxy/admin/models'
|
||
import { dynamicFetch } from '@/services/form.service'
|
||
import { putUserDetail } from '@/services/identity.service'
|
||
import { SelectBoxOption } from '@/types/shared'
|
||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||
import dayjs from 'dayjs'
|
||
import { Field, FieldProps, Formik, FormikProps } from 'formik'
|
||
import { useEffect, useMemo, useState } from 'react'
|
||
import PlatformViewHost from '@/components/componentEditor/PlatformViewHost'
|
||
|
||
type HealthValues = UserInfoViewModel & Record<string, unknown>
|
||
|
||
interface DoctorOption {
|
||
key: string
|
||
name: string
|
||
diplomaNo?: string
|
||
diplomaDate?: string
|
||
ohsCertificateNo?: string
|
||
ohsCertificateDate?: string
|
||
}
|
||
|
||
const H = '::Abp.Identity.User.HealthCard'
|
||
|
||
const familyRows = ['Mother', 'Father', 'Sibling', 'Child']
|
||
const vaccinationRows = ['TetanusVaccination', 'HepatitisVaccination', 'OtherVaccination']
|
||
|
||
/** [alan, girintili mi] — görseldeki "- Kan" gibi alt kalemler girintilidir. */
|
||
const labRows: [string, boolean][] = [
|
||
['LabBiologicalAnalysis', false],
|
||
['LabBlood', true],
|
||
['LabUrine', true],
|
||
['LabRadiological', false],
|
||
['LabRadiologicalPhysiological', false],
|
||
['LabAudiometry', true],
|
||
['LabPulmonaryFunction', true],
|
||
['LabPsychologicalTests', false],
|
||
['LabOther', false],
|
||
]
|
||
const examRows: [string, boolean][] = [
|
||
['ExamSensoryOrgans', false],
|
||
['ExamEye', true],
|
||
['ExamEnt', true],
|
||
['ExamSkin', true],
|
||
['ExamCardiovascular', false],
|
||
['ExamRespiratory', false],
|
||
['ExamDigestive', false],
|
||
['ExamUrogenital', false],
|
||
['ExamMusculoskeletal', false],
|
||
]
|
||
const otherExamRows: [string, boolean][] = [
|
||
['ExamNeurological', false],
|
||
['ExamPsychiatric', false],
|
||
['ExamOther', false],
|
||
['ExamBloodPressure', true],
|
||
['ExamPulse', true],
|
||
]
|
||
const complaintRows = [
|
||
'ComplaintProductiveCough',
|
||
'ComplaintDyspnea',
|
||
'ComplaintChestPain',
|
||
'ComplaintPalpitation',
|
||
'ComplaintBackPain',
|
||
'ComplaintDiarrheaConstipation',
|
||
'ComplaintJointPain',
|
||
]
|
||
const diagnosisRows = [
|
||
'DiagnosisHeartDisease',
|
||
'DiagnosisDiabetes',
|
||
'DiagnosisKidneyDisease',
|
||
'DiagnosisJaundice',
|
||
'DiagnosisPepticUlcer',
|
||
'DiagnosisHearingLoss',
|
||
'DiagnosisVisionImpairment',
|
||
'DiagnosisNervousSystemDisease',
|
||
'DiagnosisSkinDisease',
|
||
'DiagnosisFoodPoisoning',
|
||
]
|
||
const questionRows = [
|
||
'Hospitalized',
|
||
'HadSurgery',
|
||
'HadWorkAccident',
|
||
'OccupationalDiseaseExam',
|
||
'DisabilityPension',
|
||
'UnderTreatment',
|
||
]
|
||
const bloodTypes = ['A Rh+', 'A Rh-', 'B Rh+', 'B Rh-', 'AB Rh+', 'AB Rh-', '0 Rh+', '0 Rh-']
|
||
|
||
/** Kullanıcıya göre süzülen alt listeler; sekme adı = ListForm kodunun son parçası. */
|
||
const healthLists = ['UserPeriodicExamination', 'UserPatientProtocol', 'UserSickLeave']
|
||
|
||
const fieldName = (name: string) => name.charAt(0).toLowerCase() + name.slice(1)
|
||
|
||
const formatDateNo = (date?: string, no?: string) =>
|
||
`${date ? dayjs(date).format('DD.MM.YYYY') : ''} / ${no ?? ''}`
|
||
|
||
interface HealthCardTabProps {
|
||
userDetails: UserInfoViewModel
|
||
onSaved: () => void
|
||
}
|
||
|
||
function HealthCardTab({ userDetails, onSaved }: HealthCardTabProps) {
|
||
const { translate } = useLocalization()
|
||
const [doctors, setDoctors] = useState<DoctorOption[]>([])
|
||
const [activeTab, setActiveTab] = useState('history')
|
||
// Yeni satırın UserId değeri de bu filtreden dolar (applyNewRowDefaults).
|
||
const userSearchParams = useMemo(
|
||
() => new URLSearchParams({ filter: JSON.stringify(['UserId', '=', userDetails.id]) }),
|
||
[userDetails.id],
|
||
)
|
||
const isListTab = healthLists.includes(activeTab)
|
||
|
||
useEffect(() => {
|
||
// Hekim listesi kullanıcı ekranının alan lookup'ından gelir; sorgu kiracıya göre süzülür.
|
||
const loadDoctors = async () => {
|
||
try {
|
||
const response = await dynamicFetch('list-form-select/lookup', 'POST', null, {
|
||
listFormCode: 'AbpIdentity.Users',
|
||
listFormFieldName: 'FitnessDoctorId',
|
||
})
|
||
const rows = Array.isArray(response?.data) ? response.data : []
|
||
setDoctors(
|
||
rows.map((row: Record<string, string>) => ({
|
||
key: row.Key,
|
||
name: row.Name,
|
||
diplomaNo: row.DiplomaNo,
|
||
diplomaDate: row.DiplomaDate,
|
||
ohsCertificateNo: row.OhsCertificateNo,
|
||
ohsCertificateDate: row.OhsCertificateDate,
|
||
})),
|
||
)
|
||
} catch (error) {
|
||
console.error('Doctors could not be loaded:', error)
|
||
setDoctors([])
|
||
}
|
||
}
|
||
void loadDoctors()
|
||
}, [])
|
||
|
||
const healthStatusOptions: SelectBoxOption[] = [
|
||
{ value: 'Alive', label: translate('::App.StaticLookup.Alive') },
|
||
{ value: 'Deceased', label: translate('::App.StaticLookup.Deceased') },
|
||
]
|
||
const bloodTypeOptions: SelectBoxOption[] = bloodTypes.map((bloodType) => ({
|
||
value: bloodType,
|
||
label: bloodType,
|
||
}))
|
||
const smokingOptions: SelectBoxOption[] = [
|
||
{ value: 'No', label: translate('::App.Platform.No') },
|
||
{ value: 'Yes', label: translate('::App.Platform.Yes') },
|
||
{ value: 'Quit', label: translate('::App.StaticLookup.Quit') },
|
||
]
|
||
const alcoholOptions: SelectBoxOption[] = [
|
||
{ value: 'No', label: translate('::App.Platform.No') },
|
||
{ value: 'Occasionally', label: translate('::App.StaticLookup.Occasionally') },
|
||
{ value: 'Yes', label: translate('::App.Platform.Yes') },
|
||
]
|
||
const doctorOptions: SelectBoxOption[] = doctors.map((doctor) => ({
|
||
value: doctor.key,
|
||
label: doctor.name,
|
||
}))
|
||
|
||
const label = (name: string) => translate(`${H}.${name}`)
|
||
|
||
const renderSelect = (name: string, options: SelectBoxOption[], values: HealthValues) => (
|
||
<Field name={name}>
|
||
{({ field, form }: FieldProps<SelectBoxOption>) => (
|
||
<Select
|
||
field={field}
|
||
form={form}
|
||
options={options}
|
||
isClearable={true}
|
||
value={options.filter((option) => option.value === values[name])}
|
||
onChange={(option) => form.setFieldValue(field.name, option?.value ?? null)}
|
||
/>
|
||
)}
|
||
</Field>
|
||
)
|
||
|
||
const renderText = (name: string, placeholder: string) => (
|
||
<Field type="text" name={name} placeholder={placeholder} component={Input} />
|
||
)
|
||
|
||
const renderYesNo = (name: string, { values, setFieldValue }: FormikProps<HealthValues>) => (
|
||
<div className="flex gap-4 items-center h-full">
|
||
{[true, false].map((answer) => (
|
||
<Radio
|
||
key={String(answer)}
|
||
name={name}
|
||
checked={values[name] === answer}
|
||
onChange={() => setFieldValue(name, answer)}
|
||
>
|
||
{translate(answer ? '::App.Platform.Yes' : '::App.Platform.No')}
|
||
</Radio>
|
||
))}
|
||
</div>
|
||
)
|
||
|
||
/** Görseldeki üst "Evet / Hayır" seçimi: bloğun bütün sorularını tek seferde işaretler. */
|
||
const renderSetAll = (names: string[], { setFieldValue }: FormikProps<HealthValues>) => (
|
||
<div className="flex gap-2 mb-2">
|
||
{[true, false].map((answer) => (
|
||
<Button
|
||
key={String(answer)}
|
||
size="xs"
|
||
type="button"
|
||
onClick={() => names.forEach((name) => setFieldValue(fieldName(name), answer))}
|
||
>
|
||
{translate(answer ? '::App.Platform.Yes' : '::App.Platform.No')}
|
||
</Button>
|
||
))}
|
||
</div>
|
||
)
|
||
|
||
const renderTextRows = (rows: [string, boolean][]) =>
|
||
rows.map(([name, indented]) => (
|
||
<div key={name} className="grid grid-cols-[180px_1fr] gap-x-3 items-start">
|
||
<div className={`font-semibold pt-2 ${indented ? 'pl-4' : ''}`}>
|
||
{indented ? '- ' : ''}
|
||
{label(name)}
|
||
</div>
|
||
<FormItem>{renderText(fieldName(name), label(name))}</FormItem>
|
||
</div>
|
||
))
|
||
|
||
const renderYesNoRows = (rows: string[], formik: FormikProps<HealthValues>) => (
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-6">
|
||
{rows.map((name) => (
|
||
<div key={name} className="grid grid-cols-[1fr_150px] gap-x-3 items-center mb-2">
|
||
<div className="font-semibold">{label(name)}</div>
|
||
{renderYesNo(fieldName(name), formik)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
)
|
||
|
||
const renderDoctor = (name: string, values: HealthValues) => {
|
||
const doctor = doctors.find((item) => item.key === values[fieldName(name)])
|
||
|
||
return (
|
||
<div className="grid grid-cols-1 md:grid-cols-[180px_1fr_180px_1fr_240px_1fr] gap-x-3 items-start">
|
||
<div className="font-semibold pt-2">{label(name)}</div>
|
||
<FormItem>{renderSelect(fieldName(name), doctorOptions, values)}</FormItem>
|
||
<div className="font-semibold pt-2">{label('DiplomaDateNo')}</div>
|
||
<FormItem>
|
||
<Input disabled value={formatDateNo(doctor?.diplomaDate, doctor?.diplomaNo)} />
|
||
</FormItem>
|
||
<div className="font-semibold pt-2">{label('OhsCertificateDateNo')}</div>
|
||
<FormItem>
|
||
<Input
|
||
disabled
|
||
value={formatDateNo(doctor?.ohsCertificateDate, doctor?.ohsCertificateNo)}
|
||
/>
|
||
</FormItem>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const bodyMassIndex = (values: HealthValues) => {
|
||
const height = Number(values.examHeight)
|
||
const weight = Number(values.examWeight)
|
||
if (!height || !weight) return 0
|
||
return Number((weight / (height / 100) ** 2).toFixed(2))
|
||
}
|
||
|
||
return (
|
||
<Formik
|
||
enableReinitialize
|
||
initialValues={userDetails as HealthValues}
|
||
onSubmit={async (values, { setSubmitting }) => {
|
||
setSubmitting(true)
|
||
// Sayı kutusu metin döndürür; API decimal bekler.
|
||
const toNumber = (value: unknown) =>
|
||
value === '' || value === null || value === undefined ? undefined : Number(value)
|
||
await putUserDetail({
|
||
...values,
|
||
examHeight: toNumber(values.examHeight),
|
||
examWeight: toNumber(values.examWeight),
|
||
})
|
||
|
||
toast.push(
|
||
<Notification type="success" duration={2000}>
|
||
{translate('::App.Platform.Save')}
|
||
</Notification>,
|
||
{
|
||
placement: 'bottom-end',
|
||
},
|
||
)
|
||
|
||
onSaved()
|
||
setSubmitting(false)
|
||
}}
|
||
>
|
||
{(formik) => {
|
||
const { isSubmitting, values } = formik
|
||
|
||
return (
|
||
// Alt listeler bu bileşenin içinde çizildiği için form etiketi kullanılmaz; liste
|
||
// düğmeleri kart formunu göndermesin.
|
||
<div>
|
||
<FormContainer size="md">
|
||
<Tabs value={activeTab} variant="pill" onChange={(tab) => setActiveTab(String(tab))}>
|
||
<TabList>
|
||
<TabNav value="history">{label('History')}</TabNav>
|
||
<TabNav value="labPhysical">{label('LabPhysical')}</TabNav>
|
||
<TabNav value="anamnesis">{label('Anamnesis')}</TabNav>
|
||
<TabNav value="result">{label('Result')}</TabNav>
|
||
{healthLists.map((code) => (
|
||
<TabNav key={code} value={code}>
|
||
{translate(`::App.IdentityManagement.${code}`)}
|
||
</TabNav>
|
||
))}
|
||
</TabList>
|
||
|
||
<TabContent value="history">
|
||
<div className="pt-4">
|
||
<h6 className="mb-4">{label('FamilyHistory')}</h6>
|
||
{familyRows.map((member) => (
|
||
<div
|
||
key={member}
|
||
className="grid grid-cols-1 md:grid-cols-[200px_200px_1fr] gap-x-5 items-start"
|
||
>
|
||
<div className="font-semibold pt-2">{label(`${member}HealthStatus`)}</div>
|
||
<FormItem>
|
||
{renderSelect(
|
||
fieldName(`${member}HealthStatus`),
|
||
healthStatusOptions,
|
||
values,
|
||
)}
|
||
</FormItem>
|
||
<FormItem>
|
||
{renderText(
|
||
fieldName(`${member}HealthNote`),
|
||
label(`${member}HealthNote`),
|
||
)}
|
||
</FormItem>
|
||
</div>
|
||
))}
|
||
|
||
<h6 className="mt-4 mb-4">{label('PersonalHistory')}</h6>
|
||
<div className="grid grid-cols-1 md:grid-cols-[200px_1fr] gap-x-5 items-start">
|
||
<div className="font-semibold pt-2">
|
||
{translate('::Abp.Account.BloodType')}
|
||
</div>
|
||
<FormItem>{renderSelect('bloodType', bloodTypeOptions, values)}</FormItem>
|
||
|
||
<div className="font-semibold pt-2">{label('ChronicDisease')}</div>
|
||
<FormItem>{renderText('chronicDisease', label('ChronicDisease'))}</FormItem>
|
||
|
||
<div className="font-semibold pt-2 md:col-span-2 mb-2">
|
||
{label('Immunization')}
|
||
</div>
|
||
{vaccinationRows.map((vaccination) => (
|
||
<div key={vaccination} className="contents">
|
||
<div className="font-semibold pt-2 md:pl-4">- {label(vaccination)}</div>
|
||
<FormItem>
|
||
{renderText(fieldName(vaccination), label(vaccination))}
|
||
</FormItem>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</TabContent>
|
||
|
||
<TabContent value="labPhysical">
|
||
<div className="pt-4 grid grid-cols-1 xl:grid-cols-[1fr_2fr] gap-x-8">
|
||
<div>
|
||
<h6 className="mb-4">{label('LaboratoryFindings')}</h6>
|
||
{renderTextRows(labRows)}
|
||
</div>
|
||
<div>
|
||
<h6 className="mb-4">{label('PhysicalExamResults')}</h6>
|
||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-x-6">
|
||
<div>{renderTextRows(examRows)}</div>
|
||
<div>
|
||
{renderTextRows(otherExamRows)}
|
||
{['ExamHeight', 'ExamWeight'].map((name) => (
|
||
<div
|
||
key={name}
|
||
className="grid grid-cols-[180px_1fr] gap-x-3 items-start"
|
||
>
|
||
<div className="font-semibold pt-2 pl-4">- {label(name)}</div>
|
||
<FormItem>
|
||
<Field
|
||
type="number"
|
||
name={fieldName(name)}
|
||
placeholder={label(name)}
|
||
component={Input}
|
||
/>
|
||
</FormItem>
|
||
</div>
|
||
))}
|
||
<div className="grid grid-cols-[180px_1fr] gap-x-3 items-start">
|
||
<div className="font-semibold pt-2 pl-4">
|
||
- {label('BodyMassIndex')}
|
||
</div>
|
||
<FormItem>
|
||
<Input disabled value={bodyMassIndex(values)} />
|
||
</FormItem>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</TabContent>
|
||
|
||
<TabContent value="anamnesis">
|
||
<div className="pt-4">
|
||
{renderSetAll([...complaintRows, ...diagnosisRows], formik)}
|
||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-x-8">
|
||
<div>
|
||
<h6 className="mb-4">{label('ComplaintsQuestion')}</h6>
|
||
{renderYesNoRows(complaintRows, formik)}
|
||
</div>
|
||
<div>
|
||
<h6 className="mb-4">{label('DiagnosesQuestion')}</h6>
|
||
{renderYesNoRows(diagnosisRows, formik)}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="mt-6">
|
||
{renderSetAll(questionRows, formik)}
|
||
{questionRows.map((name) => (
|
||
<div key={name}>
|
||
<h6 className="mb-2">{label(name)}</h6>
|
||
<div className="grid grid-cols-1 md:grid-cols-[150px_1fr] gap-x-3 items-start">
|
||
{renderYesNo(fieldName(name), formik)}
|
||
<FormItem>
|
||
{renderText(fieldName(`${name}Note`), label(`${name}Note`))}
|
||
</FormItem>
|
||
</div>
|
||
</div>
|
||
))}
|
||
{(
|
||
[
|
||
['Smoking', smokingOptions],
|
||
['Alcohol', alcoholOptions],
|
||
] as const
|
||
).map(([name, options]) => (
|
||
<div key={name}>
|
||
<h6 className="mb-2">{label(name)}</h6>
|
||
<div className="grid grid-cols-1 md:grid-cols-[150px_1fr] gap-x-3 items-start">
|
||
<FormItem>
|
||
{renderSelect(fieldName(name), [...options], values)}
|
||
</FormItem>
|
||
<FormItem>
|
||
{renderText(fieldName(`${name}Note`), label(`${name}Note`))}
|
||
</FormItem>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</TabContent>
|
||
|
||
<TabContent value="result">
|
||
<div className="pt-4">
|
||
<div className="grid grid-cols-1 md:grid-cols-[40px_1fr_auto] gap-x-3 items-start">
|
||
<div className="font-semibold pt-2">1.</div>
|
||
<FormItem>{renderText('fitForWorkJob', label('FitForWorkJob'))}</FormItem>
|
||
<div className="font-semibold pt-2">{label('FitForWorkJobSuffix')}</div>
|
||
|
||
<div className="font-semibold pt-2">2.</div>
|
||
<FormItem>
|
||
{renderText('fitForWorkCondition', label('FitForWorkCondition'))}
|
||
</FormItem>
|
||
<div className="font-semibold pt-2">{label('FitForWorkConditionSuffix')}</div>
|
||
</div>
|
||
{renderDoctor('FitnessDoctorId', values)}
|
||
|
||
<p className="my-6 text-center font-semibold">{label('ShiftNightInfo')}</p>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-[200px_150px] gap-2 items-center mb-4">
|
||
{['ShiftWorkFit', 'NightWorkFit'].map((name) => (
|
||
<div key={name} className="contents">
|
||
<div className="font-semibold">{label(name)}</div>
|
||
{renderYesNo(fieldName(name), formik)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
{renderDoctor('ShiftNightDoctorId', values)}
|
||
</div>
|
||
</TabContent>
|
||
|
||
{healthLists.map((code) => (
|
||
<TabContent key={code} value={code}>
|
||
<div className="pt-4">
|
||
<PlatformViewHost
|
||
view="List"
|
||
listFormCode={`App.IdentityManagement.${code}`}
|
||
height="320px"
|
||
searchParams={userSearchParams}
|
||
/>
|
||
</div>
|
||
</TabContent>
|
||
))}
|
||
</Tabs>
|
||
</FormContainer>
|
||
|
||
{!isListTab && (
|
||
<div className="mt-4 ltr:text-right">
|
||
<Button
|
||
variant="solid"
|
||
loading={isSubmitting}
|
||
type="button"
|
||
onClick={() => void formik.submitForm()}
|
||
>
|
||
{isSubmitting
|
||
? translate('::App.Platform.Saving')
|
||
: translate('::App.Platform.Save')}
|
||
</Button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}}
|
||
</Formik>
|
||
)
|
||
}
|
||
|
||
export default HealthCardTab
|