sozsoft-platform/ui/src/views/admin/user-management/HealthCardTab.tsx

535 lines
20 KiB
TypeScript
Raw Normal View History

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