sozsoft-platform/ui/src/views/admin/user-management/HealthCardTab.tsx
2026-10-01 10:01:21 +03:00

534 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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