212 lines
6.8 KiB
TypeScript
212 lines
6.8 KiB
TypeScript
import { useState, useCallback } from 'react';
|
|
import {
|
|
Button, Card, Descriptions, Form, Input, message, Modal, Popconfirm, Result, Select, Space, Table, Tag,
|
|
} from 'antd';
|
|
import type { ColumnsType } from 'antd/es/table';
|
|
import dayjs from 'dayjs';
|
|
|
|
import {
|
|
familyProxyApi,
|
|
type FamilyPatientSummary,
|
|
type FamilyHealthSummary,
|
|
ACCESS_LEVEL_OPTIONS,
|
|
ACCESS_LEVEL_LABEL,
|
|
CONSENT_STATUS_LABEL,
|
|
CONSENT_STATUS_COLOR,
|
|
} from '../../api/health/familyProxy';
|
|
import { PageContainer } from '../../components/PageContainer';
|
|
|
|
export default function FamilyProxyPage() {
|
|
const [patients, setPatients] = useState<FamilyPatientSummary[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [loaded, setLoaded] = useState(false);
|
|
|
|
const [selectedPatientId, setSelectedPatientId] = useState<string | null>(null);
|
|
const [healthSummary, setHealthSummary] = useState<FamilyHealthSummary | null>(null);
|
|
const [summaryLoading, setSummaryLoading] = useState(false);
|
|
|
|
const [grantModalOpen, setGrantModalOpen] = useState(false);
|
|
const [grantTarget, setGrantTarget] = useState<FamilyPatientSummary | null>(null);
|
|
const [grantSubmitting, setGrantSubmitting] = useState(false);
|
|
const [grantForm] = Form.useForm();
|
|
|
|
const fetchPatients = useCallback(async () => {
|
|
setLoading(true);
|
|
try {
|
|
const list = await familyProxyApi.listMyPatients();
|
|
setPatients(list);
|
|
setLoaded(true);
|
|
} catch {
|
|
message.error('加载关联患者失败');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
const handleViewSummary = async (patientId: string) => {
|
|
setSelectedPatientId(patientId);
|
|
setSummaryLoading(true);
|
|
try {
|
|
const summary = await familyProxyApi.getHealthSummary(patientId);
|
|
setHealthSummary(summary);
|
|
} catch {
|
|
message.error('加载健康摘要失败');
|
|
} finally {
|
|
setSummaryLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleGrantAccess = (record: FamilyPatientSummary) => {
|
|
setGrantTarget(record);
|
|
grantForm.resetFields();
|
|
grantForm.setFieldsValue({ access_level: 'read_only' });
|
|
setGrantModalOpen(true);
|
|
};
|
|
|
|
const handleRevokeAccess = async (record: FamilyPatientSummary) => {
|
|
try {
|
|
await familyProxyApi.revokeAccess(record.patient_id, record.family_member_id, 0);
|
|
message.success('已撤销访问权限');
|
|
fetchPatients();
|
|
} catch {
|
|
message.error('撤销失败');
|
|
}
|
|
};
|
|
|
|
const handleSubmitGrant = async () => {
|
|
try {
|
|
const values = await grantForm.validateFields();
|
|
setGrantSubmitting(true);
|
|
await familyProxyApi.grantAccess(
|
|
grantTarget!.patient_id,
|
|
grantTarget!.family_member_id,
|
|
values,
|
|
0,
|
|
);
|
|
message.success('访问权限已授权');
|
|
setGrantModalOpen(false);
|
|
fetchPatients();
|
|
} catch {
|
|
// validation
|
|
} finally {
|
|
setGrantSubmitting(false);
|
|
}
|
|
};
|
|
|
|
const patientColumns: ColumnsType<FamilyPatientSummary> = [
|
|
{
|
|
title: '患者姓名',
|
|
dataIndex: 'patient_name',
|
|
width: 120,
|
|
},
|
|
{
|
|
title: '关系',
|
|
dataIndex: 'relationship',
|
|
width: 80,
|
|
},
|
|
{
|
|
title: '同意状态',
|
|
dataIndex: 'consent_status',
|
|
width: 100,
|
|
render: (v: string) => (
|
|
<Tag color={CONSENT_STATUS_COLOR[v] ?? 'default'}>{CONSENT_STATUS_LABEL[v] ?? v}</Tag>
|
|
),
|
|
},
|
|
{
|
|
title: '访问级别',
|
|
dataIndex: 'access_level',
|
|
width: 100,
|
|
render: (v: string) => ACCESS_LEVEL_LABEL[v] ?? v ?? '-',
|
|
},
|
|
{
|
|
title: '授权时间',
|
|
dataIndex: 'consented_at',
|
|
width: 170,
|
|
render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD HH:mm') : '-',
|
|
},
|
|
{
|
|
title: '操作',
|
|
width: 240,
|
|
render: (_, record) => (
|
|
<Space>
|
|
<Button size="small" onClick={() => handleViewSummary(record.patient_id)}>
|
|
健康摘要
|
|
</Button>
|
|
{record.consent_status !== 'granted' && (
|
|
<Button size="small" type="primary" onClick={() => handleGrantAccess(record)}>
|
|
授权访问
|
|
</Button>
|
|
)}
|
|
{record.consent_status === 'granted' && (
|
|
<Popconfirm title="确定撤销访问权限?" onConfirm={() => handleRevokeAccess(record)}>
|
|
<Button size="small" danger>撤销</Button>
|
|
</Popconfirm>
|
|
)}
|
|
</Space>
|
|
),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<PageContainer
|
|
title="家庭健康代理"
|
|
actions={<Button type="primary" onClick={fetchPatients} loading={loading}>{loaded ? '刷新' : '加载关联患者'}</Button>}
|
|
>
|
|
{loaded && patients.length === 0 ? (
|
|
<Result title="暂无关联患者" subTitle="您尚未被添加为任何患者的家庭成员" />
|
|
) : (
|
|
<>
|
|
<Table<FamilyPatientSummary>
|
|
rowKey="family_member_id"
|
|
columns={patientColumns}
|
|
dataSource={patients}
|
|
loading={loading}
|
|
pagination={false}
|
|
/>
|
|
|
|
{selectedPatientId && (
|
|
<Card title="健康摘要" style={{ marginTop: 24 }} loading={summaryLoading}>
|
|
{healthSummary && (
|
|
<Descriptions bordered column={2}>
|
|
<Descriptions.Item label="患者">{healthSummary.patient_name}</Descriptions.Item>
|
|
<Descriptions.Item label="近期告警数">{healthSummary.recent_alerts_count}</Descriptions.Item>
|
|
<Descriptions.Item label="最新体征" span={2}>
|
|
{healthSummary.latest_vital_signs
|
|
? JSON.stringify(healthSummary.latest_vital_signs, null, 2)
|
|
: '暂无数据'}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label="活跃护理计划" span={2}>
|
|
{healthSummary.active_care_plan
|
|
? JSON.stringify(healthSummary.active_care_plan, null, 2)
|
|
: '暂无'}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label="下次预约" span={2}>
|
|
{healthSummary.next_appointment
|
|
? JSON.stringify(healthSummary.next_appointment, null, 2)
|
|
: '暂无'}
|
|
</Descriptions.Item>
|
|
</Descriptions>
|
|
)}
|
|
</Card>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
<Modal
|
|
title={`授权访问 — ${grantTarget?.patient_name ?? ''}`}
|
|
open={grantModalOpen}
|
|
onOk={handleSubmitGrant}
|
|
onCancel={() => setGrantModalOpen(false)}
|
|
confirmLoading={grantSubmitting}
|
|
width={400}
|
|
>
|
|
<Form form={grantForm} layout="vertical">
|
|
<Form.Item name="access_level" label="访问级别" rules={[{ required: true }]}>
|
|
<Select options={ACCESS_LEVEL_OPTIONS} />
|
|
</Form.Item>
|
|
</Form>
|
|
</Modal>
|
|
</PageContainer>
|
|
);
|
|
}
|