分离体检详情面板
This commit is contained in:
373
src/components/exam/ExamDetailPanel.tsx
Normal file
373
src/components/exam/ExamDetailPanel.tsx
Normal file
@@ -0,0 +1,373 @@
|
|||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
CustomerAppointmentInfo,
|
||||||
|
CustomerExamAddItem,
|
||||||
|
CustomerInfo,
|
||||||
|
PhysicalExamProgressItem,
|
||||||
|
} from '../../api';
|
||||||
|
import { editCustomerDetail } from '../../api';
|
||||||
|
import type { ExamClient } from '../../data/mockData';
|
||||||
|
import { Button, Input } from '../ui';
|
||||||
|
|
||||||
|
interface ExamDetailPanelProps {
|
||||||
|
client: ExamClient;
|
||||||
|
customerInfo: CustomerInfo | null;
|
||||||
|
appointmentInfo: CustomerAppointmentInfo | null;
|
||||||
|
addItemInfoList: CustomerExamAddItem[] | null;
|
||||||
|
progressList: PhysicalExamProgressItem[] | null;
|
||||||
|
loading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMaritalCodeFromText = (text: string): number => {
|
||||||
|
if (text.includes('未婚') || text === '未婚') return 10;
|
||||||
|
if (text.includes('已婚') || text === '已婚') return 20;
|
||||||
|
return 20;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMaritalText = (code: number): string => (code === 10 ? '未婚' : '已婚');
|
||||||
|
|
||||||
|
export const ExamDetailPanel = ({
|
||||||
|
client,
|
||||||
|
customerInfo,
|
||||||
|
appointmentInfo,
|
||||||
|
addItemInfoList,
|
||||||
|
progressList,
|
||||||
|
loading,
|
||||||
|
}: ExamDetailPanelProps) => {
|
||||||
|
const basePhone = customerInfo?.phone || (client['mobile' as keyof ExamClient] as string | undefined) || '';
|
||||||
|
const baseMaritalText =
|
||||||
|
customerInfo?.patient_marital_status_name ||
|
||||||
|
(client['maritalStatus' as keyof ExamClient] as string | undefined) ||
|
||||||
|
'—';
|
||||||
|
const baseMaritalCode = baseMaritalText === '—' ? 20 : getMaritalCodeFromText(baseMaritalText);
|
||||||
|
|
||||||
|
const [phone, setPhone] = useState(basePhone || '—');
|
||||||
|
const [maritalCode, setMaritalCode] = useState(baseMaritalCode);
|
||||||
|
const [phoneEditing, setPhoneEditing] = useState(false);
|
||||||
|
const [maritalEditing, setMaritalEditing] = useState(false);
|
||||||
|
const [editLoading, setEditLoading] = useState(false);
|
||||||
|
const [editMessage, setEditMessage] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const customerChannel = client.customerType === '团客' ? '团体客户' : '散客客户';
|
||||||
|
const familyDoctor = customerInfo?.family_doctor_name || (client['familyDoctor' as keyof ExamClient] as string | undefined) || '—';
|
||||||
|
const groupTag = client['groupTag' as keyof ExamClient] || (client.customerType === '团客' ? '团检' : '—');
|
||||||
|
const bookingTime = appointmentInfo?.appointment_time || (client['bookingTime' as keyof ExamClient] || '—');
|
||||||
|
const signTime = appointmentInfo?.sign_in_time || (client['signTime' as keyof ExamClient] || '—');
|
||||||
|
const addonSummary =
|
||||||
|
addItemInfoList && addItemInfoList.length > 0
|
||||||
|
? addItemInfoList.map((i) => `${i.dept_name ?? ''} ${i.combination_name ?? ''}`.trim()).join('、')
|
||||||
|
: client['addonSummary' as keyof ExamClient] || '—';
|
||||||
|
|
||||||
|
const handleSavePhone = async () => {
|
||||||
|
if (!phone || phone.trim() === '' || phone === '—') {
|
||||||
|
setEditMessage('请输入联系电话');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setEditLoading(true);
|
||||||
|
setEditMessage(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await editCustomerDetail({
|
||||||
|
marital_status: maritalCode,
|
||||||
|
phone: phone.trim(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.Status === 200) {
|
||||||
|
setEditMessage('保存成功');
|
||||||
|
setPhoneEditing(false);
|
||||||
|
setTimeout(() => setEditMessage(null), 2000);
|
||||||
|
} else {
|
||||||
|
setEditMessage(res.Message || '保存失败');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('保存客户信息失败', err);
|
||||||
|
setEditMessage('保存失败,请稍后重试');
|
||||||
|
} finally {
|
||||||
|
setEditLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveMarital = async () => {
|
||||||
|
const phoneValue = phone === '—' ? '' : phone.trim();
|
||||||
|
|
||||||
|
if (!phoneValue) {
|
||||||
|
setEditMessage('联系电话不能为空');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setEditLoading(true);
|
||||||
|
setEditMessage(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await editCustomerDetail({
|
||||||
|
marital_status: maritalCode,
|
||||||
|
phone: phoneValue,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.Status === 200) {
|
||||||
|
setEditMessage('保存成功');
|
||||||
|
setMaritalEditing(false);
|
||||||
|
setTimeout(() => setEditMessage(null), 2000);
|
||||||
|
} else {
|
||||||
|
setEditMessage(res.Message || '保存失败');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('保存客户信息失败', err);
|
||||||
|
setEditMessage('保存失败,请稍后重试');
|
||||||
|
} finally {
|
||||||
|
setEditLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const progressGroups = useMemo(() => {
|
||||||
|
const checked: string[] = [];
|
||||||
|
const abandoned: string[] = [];
|
||||||
|
const pending: string[] = [];
|
||||||
|
const deferred: string[] = [];
|
||||||
|
(progressList || []).forEach((p) => {
|
||||||
|
const name = p.project_name || p.department_name || '项目';
|
||||||
|
switch (p.exam_status) {
|
||||||
|
case 1:
|
||||||
|
checked.push(name);
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
abandoned.push(name);
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
deferred.push(name);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
pending.push(name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { checked, abandoned, pending, deferred };
|
||||||
|
}, [progressList]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='space-y-4 text-sm'>
|
||||||
|
<div className='flex items-center gap-4'>
|
||||||
|
<div className='w-14 h-14 rounded-full bg-gray-200 flex-shrink-0 overflow-hidden'>
|
||||||
|
<div className='w-full h-full rounded-full bg-gray-300 flex items-center justify-center text-[10px] text-gray-500'>
|
||||||
|
头像
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='text-xs text-gray-500'>
|
||||||
|
{loading ? '加载中…' : '基础信息:头像、姓名、证件号、手机号等(点击图标可进行编辑)'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{editMessage && (
|
||||||
|
<div
|
||||||
|
className={`text-xs px-3 py-2 rounded-lg ${
|
||||||
|
editMessage.includes('成功') ? 'bg-green-50 text-green-600' : 'bg-amber-50 text-amber-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{editMessage}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className='space-y-2 text-xs text-gray-700'>
|
||||||
|
<div className='font-medium text-gray-900'>基础信息</div>
|
||||||
|
<div className='grid grid-cols-2 gap-x-8 gap-y-1'>
|
||||||
|
<div>
|
||||||
|
姓名:<span className='text-gray-900'>{customerInfo?.customer_name || client.name}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
证件号:<span className='text-gray-900'>{customerInfo?.id_no || '—'}</span>
|
||||||
|
</div>
|
||||||
|
<div className='flex items-center'>
|
||||||
|
<span>手机号:</span>
|
||||||
|
{!phoneEditing ? (
|
||||||
|
<span className='text-gray-900 flex items-center'>
|
||||||
|
{phone}
|
||||||
|
<button className='ml-1 text-blue-500 text-[11px] hover:underline' onClick={() => setPhoneEditing(true)}>
|
||||||
|
✏️ 编辑
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className='flex items-center gap-1'>
|
||||||
|
<input
|
||||||
|
className='w-28 rounded-xl border px-2 py-0.5 text-[11px] outline-none'
|
||||||
|
value={phone}
|
||||||
|
onChange={(e) => setPhone(e.target.value)}
|
||||||
|
disabled={editLoading}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className='px-2 py-0.5 rounded-xl border text-[11px] disabled:opacity-50'
|
||||||
|
onClick={handleSavePhone}
|
||||||
|
disabled={editLoading}
|
||||||
|
>
|
||||||
|
{editLoading ? '保存中...' : '保存'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className='px-2 py-0.5 rounded-xl border text-[11px]'
|
||||||
|
onClick={() => {
|
||||||
|
setPhoneEditing(false);
|
||||||
|
setPhone(basePhone || '—');
|
||||||
|
setEditMessage(null);
|
||||||
|
}}
|
||||||
|
disabled={editLoading}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
性别/年龄:
|
||||||
|
<span className='text-gray-900'>
|
||||||
|
{customerInfo?.gender_name || client.gender} / {client.age}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
客户级别:<span className='text-gray-900'>{client.level}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
所属渠道:<span className='text-gray-900'>{customerChannel}</span>
|
||||||
|
</div>
|
||||||
|
<div className='flex items-center'>
|
||||||
|
<span>婚姻状况:</span>
|
||||||
|
{!maritalEditing ? (
|
||||||
|
<span className='text-gray-900 flex items-center'>
|
||||||
|
{getMaritalText(maritalCode)}
|
||||||
|
<button className='ml-1 text-blue-500 text-[11px] hover:underline' onClick={() => setMaritalEditing(true)}>
|
||||||
|
✏️ 编辑
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className='flex items-center gap-2'>
|
||||||
|
<label className='flex items-center gap-1 cursor-pointer'>
|
||||||
|
<input
|
||||||
|
type='radio'
|
||||||
|
name='marital'
|
||||||
|
value='10'
|
||||||
|
checked={maritalCode === 10}
|
||||||
|
onChange={(e) => setMaritalCode(Number(e.target.value))}
|
||||||
|
disabled={editLoading}
|
||||||
|
className='w-3 h-3'
|
||||||
|
/>
|
||||||
|
<span className='text-[11px]'>未婚</span>
|
||||||
|
</label>
|
||||||
|
<label className='flex items-center gap-1 cursor-pointer'>
|
||||||
|
<input
|
||||||
|
type='radio'
|
||||||
|
name='marital'
|
||||||
|
value='20'
|
||||||
|
checked={maritalCode === 20}
|
||||||
|
onChange={(e) => setMaritalCode(Number(e.target.value))}
|
||||||
|
disabled={editLoading}
|
||||||
|
className='w-3 h-3'
|
||||||
|
/>
|
||||||
|
<span className='text-[11px]'>已婚</span>
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
className='px-2 py-0.5 rounded-xl border text-[11px] disabled:opacity-50'
|
||||||
|
onClick={handleSaveMarital}
|
||||||
|
disabled={editLoading}
|
||||||
|
>
|
||||||
|
{editLoading ? '保存中...' : '保存'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className='px-2 py-0.5 rounded-xl border text-[11px]'
|
||||||
|
onClick={() => {
|
||||||
|
setMaritalEditing(false);
|
||||||
|
setMaritalCode(baseMaritalCode);
|
||||||
|
setEditMessage(null);
|
||||||
|
}}
|
||||||
|
disabled={editLoading}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
家医:<span className='text-gray-900'>{familyDoctor}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
团标签:<span className='text-gray-900'>{groupTag as string}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='space-y-2 text-xs text-gray-700'>
|
||||||
|
<div className='font-medium text-gray-900'>预约信息</div>
|
||||||
|
<div className='grid grid-cols-2 gap-x-8 gap-y-1'>
|
||||||
|
<div>
|
||||||
|
预约时间:<span className='text-gray-900'>{bookingTime as string}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
签到时间:<span className='text-gray-900'>{signTime as string}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
已消耗时长:
|
||||||
|
<span className='text-gray-900'>
|
||||||
|
{appointmentInfo?.physical_exam_complete_time && appointmentInfo?.sign_in_time
|
||||||
|
? `${appointmentInfo.physical_exam_complete_time} - ${appointmentInfo.sign_in_time}`
|
||||||
|
: client.elapsed}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className='col-span-2'>
|
||||||
|
体检套餐名称:<span className='text-gray-900'>{client.packageName}</span>
|
||||||
|
</div>
|
||||||
|
<div className='col-span-2'>
|
||||||
|
加项内容:<span className='text-gray-900'>{addonSummary as string}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='grid grid-cols-2 gap-4 text-xs'>
|
||||||
|
<div className='p-3 rounded-2xl bg-green-50 border max-h-48 overflow-auto custom-scroll'>
|
||||||
|
<div className='font-medium mb-2'>已查项目 共 {progressGroups.checked.length} 项</div>
|
||||||
|
<div className='flex flex-wrap gap-2'>
|
||||||
|
{(progressGroups.checked.length ? progressGroups.checked : client.checkedItems).map((i) => (
|
||||||
|
<span key={i} className='px-2 py-0.5 rounded-full bg-white border text-[11px]'>
|
||||||
|
{i}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='p-3 rounded-2xl bg-red-50 border max-h-48 overflow-auto custom-scroll'>
|
||||||
|
<div className='font-medium mb-2'>弃检项目 共 {progressGroups.abandoned.length} 项</div>
|
||||||
|
<div className='flex flex-wrap gap-2'>
|
||||||
|
{progressGroups.abandoned.length ? (
|
||||||
|
progressGroups.abandoned.map((i) => (
|
||||||
|
<span key={i} className='px-2 py-0.5 rounded-full bg-white border text-[11px]'>
|
||||||
|
{i}
|
||||||
|
</span>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<span className='text-gray-400 text-[11px]'>暂无弃检项目</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='p-3 rounded-2xl bg-yellow-50 border max-h-48 overflow-auto custom-scroll'>
|
||||||
|
<div className='font-medium mb-2'>未查项目 共 {progressGroups.pending.length} 项</div>
|
||||||
|
<div className='flex flex-wrap gap-2'>
|
||||||
|
{(progressGroups.pending.length ? progressGroups.pending : client.pendingItems).map((i) => (
|
||||||
|
<span key={i} className='px-2 py-0.5 rounded-full bg-white border text-[11px]'>
|
||||||
|
{i}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='p-3 rounded-2xl bg-blue-50 border max-h-48 overflow-auto custom-scroll'>
|
||||||
|
<div className='font-medium mb-2'>延期项目 共 {progressGroups.deferred.length} 项</div>
|
||||||
|
<div className='flex flex-wrap gap-2'>
|
||||||
|
{progressGroups.deferred.length ? (
|
||||||
|
progressGroups.deferred.map((i) => (
|
||||||
|
<span key={i} className='px-2 py-0.5 rounded-full bg-white border text-[11px]'>
|
||||||
|
{i}
|
||||||
|
</span>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<span className='text-gray-400 text-[11px]'>暂无延期项目</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -1,16 +1,17 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import type { ExamClient, ExamModalTab } from '../../data/mockData';
|
import type { ExamClient, ExamModalTab } from '../../data/mockData';
|
||||||
import type {
|
import type {
|
||||||
CustomerAppointmentInfo,
|
CustomerAppointmentInfo,
|
||||||
CustomerExamAddItem,
|
CustomerExamAddItem,
|
||||||
CustomerInfo,
|
CustomerInfo,
|
||||||
OutputTongyishuFileInfo,
|
|
||||||
PhysicalExamProgressItem,
|
PhysicalExamProgressItem,
|
||||||
|
OutputTongyishuFileInfo,
|
||||||
} from '../../api';
|
} from '../../api';
|
||||||
import { getCustomerDetail, getPhysicalExamProgressDetail, signInMedicalExamCenter, getTongyishuPdf, submitTongyishuSign, editCustomerDetail } from '../../api';
|
import { getCustomerDetail, getPhysicalExamProgressDetail, signInMedicalExamCenter, getTongyishuPdf, submitTongyishuSign } from '../../api';
|
||||||
import type { SignaturePadHandle } from '../ui';
|
import type { SignaturePadHandle } from '../ui';
|
||||||
import { Button, Input, SignaturePad } from '../ui';
|
import { Button, Input, SignaturePad } from '../ui';
|
||||||
|
import { ExamDetailPanel } from './ExamDetailPanel';
|
||||||
|
|
||||||
interface ExamModalProps {
|
interface ExamModalProps {
|
||||||
client: ExamClient;
|
client: ExamClient;
|
||||||
@@ -157,7 +158,7 @@ export const ExamModal = ({ client, tab, onTabChange, onClose }: ExamModalProps)
|
|||||||
|
|
||||||
<div className='px-4 py-4 bg-gray-50/60'>
|
<div className='px-4 py-4 bg-gray-50/60'>
|
||||||
{tab === 'detail' && (
|
{tab === 'detail' && (
|
||||||
<ExamDetailInfo
|
<ExamDetailPanel
|
||||||
client={client}
|
client={client}
|
||||||
customerInfo={customerInfo}
|
customerInfo={customerInfo}
|
||||||
appointmentInfo={appointmentInfo}
|
appointmentInfo={appointmentInfo}
|
||||||
@@ -772,365 +773,6 @@ const ExamAddonPanel = ({ client }: { client: ExamClient }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ExamDetailInfo = ({
|
|
||||||
client,
|
|
||||||
customerInfo,
|
|
||||||
appointmentInfo,
|
|
||||||
addItemInfoList,
|
|
||||||
progressList,
|
|
||||||
loading,
|
|
||||||
}: {
|
|
||||||
client: ExamClient;
|
|
||||||
customerInfo: CustomerInfo | null;
|
|
||||||
appointmentInfo: CustomerAppointmentInfo | null;
|
|
||||||
addItemInfoList: CustomerExamAddItem[] | null;
|
|
||||||
progressList: PhysicalExamProgressItem[] | null;
|
|
||||||
loading: boolean;
|
|
||||||
}) => {
|
|
||||||
const basePhone = customerInfo?.phone || (client['mobile' as keyof ExamClient] as string | undefined) || '';
|
|
||||||
const baseMaritalText =
|
|
||||||
customerInfo?.patient_marital_status_name ||
|
|
||||||
(client['maritalStatus' as keyof ExamClient] as string | undefined) ||
|
|
||||||
'—';
|
|
||||||
// 将文本转换为数字:10-未婚,20-已婚
|
|
||||||
const getMaritalCodeFromText = (text: string): number => {
|
|
||||||
if (text.includes('未婚') || text === '未婚') return 10;
|
|
||||||
if (text.includes('已婚') || text === '已婚') return 20;
|
|
||||||
return 20; // 默认已婚
|
|
||||||
};
|
|
||||||
const baseMaritalCode = baseMaritalText === '—' ? 20 : getMaritalCodeFromText(baseMaritalText);
|
|
||||||
|
|
||||||
const [phone, setPhone] = useState(basePhone || '—');
|
|
||||||
const [maritalCode, setMaritalCode] = useState(baseMaritalCode);
|
|
||||||
const [phoneEditing, setPhoneEditing] = useState(false);
|
|
||||||
const [maritalEditing, setMaritalEditing] = useState(false);
|
|
||||||
const [editLoading, setEditLoading] = useState(false);
|
|
||||||
const [editMessage, setEditMessage] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const getMaritalText = (code: number): string => {
|
|
||||||
return code === 10 ? '未婚' : '已婚';
|
|
||||||
};
|
|
||||||
|
|
||||||
const customerChannel = client.customerType === '团客' ? '团体客户' : '散客客户';
|
|
||||||
const familyDoctor = customerInfo?.family_doctor_name || (client['familyDoctor' as keyof ExamClient] as string | undefined) || '—';
|
|
||||||
const groupTag = client['groupTag' as keyof ExamClient] || (client.customerType === '团客' ? '团检' : '—');
|
|
||||||
const bookingTime = appointmentInfo?.appointment_time || (client['bookingTime' as keyof ExamClient] || '—');
|
|
||||||
const signTime = appointmentInfo?.sign_in_time || (client['signTime' as keyof ExamClient] || '—');
|
|
||||||
const addonSummary =
|
|
||||||
addItemInfoList && addItemInfoList.length > 0
|
|
||||||
? addItemInfoList.map((i) => `${i.dept_name ?? ''} ${i.combination_name ?? ''}`.trim()).join('、')
|
|
||||||
: client['addonSummary' as keyof ExamClient] || '—';
|
|
||||||
|
|
||||||
const handleSavePhone = async () => {
|
|
||||||
if (!phone || phone.trim() === '' || phone === '—') {
|
|
||||||
setEditMessage('请输入联系电话');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setEditLoading(true);
|
|
||||||
setEditMessage(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await editCustomerDetail({
|
|
||||||
marital_status: maritalCode,
|
|
||||||
phone: phone.trim(),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.Status === 200) {
|
|
||||||
setEditMessage('保存成功');
|
|
||||||
setPhoneEditing(false);
|
|
||||||
// 2秒后清除消息
|
|
||||||
setTimeout(() => setEditMessage(null), 2000);
|
|
||||||
} else {
|
|
||||||
setEditMessage(res.Message || '保存失败');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('保存客户信息失败', err);
|
|
||||||
setEditMessage('保存失败,请稍后重试');
|
|
||||||
} finally {
|
|
||||||
setEditLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSaveMarital = async () => {
|
|
||||||
const phoneValue = phone === '—' ? '' : phone.trim();
|
|
||||||
|
|
||||||
if (!phoneValue) {
|
|
||||||
setEditMessage('联系电话不能为空');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setEditLoading(true);
|
|
||||||
setEditMessage(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await editCustomerDetail({
|
|
||||||
marital_status: maritalCode,
|
|
||||||
phone: phoneValue,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.Status === 200) {
|
|
||||||
setEditMessage('保存成功');
|
|
||||||
setMaritalEditing(false);
|
|
||||||
// 2秒后清除消息
|
|
||||||
setTimeout(() => setEditMessage(null), 2000);
|
|
||||||
} else {
|
|
||||||
setEditMessage(res.Message || '保存失败');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('保存客户信息失败', err);
|
|
||||||
setEditMessage('保存失败,请稍后重试');
|
|
||||||
} finally {
|
|
||||||
setEditLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const progressGroups = useMemo(() => {
|
|
||||||
const checked: string[] = [];
|
|
||||||
const abandoned: string[] = [];
|
|
||||||
const pending: string[] = [];
|
|
||||||
const deferred: string[] = [];
|
|
||||||
(progressList || []).forEach((p) => {
|
|
||||||
const name = p.project_name || p.department_name || '项目';
|
|
||||||
switch (p.exam_status) {
|
|
||||||
case 1:
|
|
||||||
checked.push(name);
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
abandoned.push(name);
|
|
||||||
break;
|
|
||||||
case 4:
|
|
||||||
deferred.push(name);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
pending.push(name);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return { checked, abandoned, pending, deferred };
|
|
||||||
}, [progressList]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='space-y-4 text-sm'>
|
|
||||||
<div className='flex items-center gap-4'>
|
|
||||||
<div className='w-14 h-14 rounded-full bg-gray-200 flex-shrink-0 overflow-hidden'>
|
|
||||||
<div className='w-full h-full rounded-full bg-gray-300 flex items-center justify-center text-[10px] text-gray-500'>
|
|
||||||
头像
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='text-xs text-gray-500'>
|
|
||||||
{loading ? '加载中…' : '基础信息:头像、姓名、证件号、手机号等(点击图标可进行编辑)'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{editMessage && (
|
|
||||||
<div className={`text-xs px-3 py-2 rounded-lg ${editMessage.includes('成功') ? 'bg-green-50 text-green-600' : 'bg-amber-50 text-amber-600'
|
|
||||||
}`}>
|
|
||||||
{editMessage}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className='space-y-2 text-xs text-gray-700'>
|
|
||||||
<div className='font-medium text-gray-900'>基础信息</div>
|
|
||||||
<div className='grid grid-cols-2 gap-x-8 gap-y-1'>
|
|
||||||
<div>
|
|
||||||
姓名:<span className='text-gray-900'>{customerInfo?.customer_name || client.name}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
证件号:<span className='text-gray-900'>{customerInfo?.id_no || '—'}</span>
|
|
||||||
</div>
|
|
||||||
<div className='flex items-center'>
|
|
||||||
<span>手机号:</span>
|
|
||||||
{!phoneEditing ? (
|
|
||||||
<span className='text-gray-900 flex items-center'>
|
|
||||||
{phone}
|
|
||||||
<button className='ml-1 text-blue-500 text-[11px] hover:underline' onClick={() => setPhoneEditing(true)}>
|
|
||||||
✏️ 编辑
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className='flex items-center gap-1'>
|
|
||||||
<input
|
|
||||||
className='w-28 rounded-xl border px-2 py-0.5 text-[11px] outline-none'
|
|
||||||
value={phone}
|
|
||||||
onChange={(e) => setPhone(e.target.value)}
|
|
||||||
disabled={editLoading}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
className='px-2 py-0.5 rounded-xl border text-[11px] disabled:opacity-50'
|
|
||||||
onClick={handleSavePhone}
|
|
||||||
disabled={editLoading}
|
|
||||||
>
|
|
||||||
{editLoading ? '保存中...' : '保存'}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className='px-2 py-0.5 rounded-xl border text-[11px]'
|
|
||||||
onClick={() => {
|
|
||||||
setPhoneEditing(false);
|
|
||||||
setPhone(basePhone || '—');
|
|
||||||
setEditMessage(null);
|
|
||||||
}}
|
|
||||||
disabled={editLoading}
|
|
||||||
>
|
|
||||||
取消
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
性别/年龄:
|
|
||||||
<span className='text-gray-900'>
|
|
||||||
{customerInfo?.gender_name || client.gender} / {client.age}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
客户级别:<span className='text-gray-900'>{client.level}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
所属渠道:<span className='text-gray-900'>{customerChannel}</span>
|
|
||||||
</div>
|
|
||||||
<div className='flex items-center'>
|
|
||||||
<span>婚姻状况:</span>
|
|
||||||
{!maritalEditing ? (
|
|
||||||
<span className='text-gray-900 flex items-center'>
|
|
||||||
{getMaritalText(maritalCode)}
|
|
||||||
<button className='ml-1 text-blue-500 text-[11px] hover:underline' onClick={() => setMaritalEditing(true)}>
|
|
||||||
✏️ 编辑
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className='flex items-center gap-2'>
|
|
||||||
<label className='flex items-center gap-1 cursor-pointer'>
|
|
||||||
<input
|
|
||||||
type='radio'
|
|
||||||
name='marital'
|
|
||||||
value='10'
|
|
||||||
checked={maritalCode === 10}
|
|
||||||
onChange={(e) => setMaritalCode(Number(e.target.value))}
|
|
||||||
disabled={editLoading}
|
|
||||||
className='w-3 h-3'
|
|
||||||
/>
|
|
||||||
<span className='text-[11px]'>未婚</span>
|
|
||||||
</label>
|
|
||||||
<label className='flex items-center gap-1 cursor-pointer'>
|
|
||||||
<input
|
|
||||||
type='radio'
|
|
||||||
name='marital'
|
|
||||||
value='20'
|
|
||||||
checked={maritalCode === 20}
|
|
||||||
onChange={(e) => setMaritalCode(Number(e.target.value))}
|
|
||||||
disabled={editLoading}
|
|
||||||
className='w-3 h-3'
|
|
||||||
/>
|
|
||||||
<span className='text-[11px]'>已婚</span>
|
|
||||||
</label>
|
|
||||||
<button
|
|
||||||
className='px-2 py-0.5 rounded-xl border text-[11px] disabled:opacity-50'
|
|
||||||
onClick={handleSaveMarital}
|
|
||||||
disabled={editLoading}
|
|
||||||
>
|
|
||||||
{editLoading ? '保存中...' : '保存'}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className='px-2 py-0.5 rounded-xl border text-[11px]'
|
|
||||||
onClick={() => {
|
|
||||||
setMaritalEditing(false);
|
|
||||||
setMaritalCode(baseMaritalCode);
|
|
||||||
setEditMessage(null);
|
|
||||||
}}
|
|
||||||
disabled={editLoading}
|
|
||||||
>
|
|
||||||
取消
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
家医:<span className='text-gray-900'>{familyDoctor}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
团标签:<span className='text-gray-900'>{groupTag as string}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='space-y-2 text-xs text-gray-700'>
|
|
||||||
<div className='font-medium text-gray-900'>预约信息</div>
|
|
||||||
<div className='grid grid-cols-2 gap-x-8 gap-y-1'>
|
|
||||||
<div>
|
|
||||||
预约时间:<span className='text-gray-900'>{bookingTime as string}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
签到时间:<span className='text-gray-900'>{signTime as string}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
已消耗时长:
|
|
||||||
<span className='text-gray-900'>
|
|
||||||
{appointmentInfo?.physical_exam_complete_time && appointmentInfo?.sign_in_time
|
|
||||||
? `${appointmentInfo.physical_exam_complete_time} - ${appointmentInfo.sign_in_time}`
|
|
||||||
: client.elapsed}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className='col-span-2'>
|
|
||||||
体检套餐名称:<span className='text-gray-900'>{client.packageName}</span>
|
|
||||||
</div>
|
|
||||||
<div className='col-span-2'>
|
|
||||||
加项内容:<span className='text-gray-900'>{addonSummary as string}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='grid grid-cols-2 gap-4 text-xs'>
|
|
||||||
<div className='p-3 rounded-2xl bg-green-50 border max-h-48 overflow-auto custom-scroll'>
|
|
||||||
<div className='font-medium mb-2'>已查项目 共 {progressGroups.checked.length} 项</div>
|
|
||||||
<div className='flex flex-wrap gap-2'>
|
|
||||||
{(progressGroups.checked.length ? progressGroups.checked : client.checkedItems).map((i) => (
|
|
||||||
<span key={i} className='px-2 py-0.5 rounded-full bg-white border text-[11px]'>
|
|
||||||
{i}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='p-3 rounded-2xl bg-red-50 border max-h-48 overflow-auto custom-scroll'>
|
|
||||||
<div className='font-medium mb-2'>弃检项目 共 {progressGroups.abandoned.length} 项</div>
|
|
||||||
<div className='flex flex-wrap gap-2'>
|
|
||||||
{progressGroups.abandoned.length ? (
|
|
||||||
progressGroups.abandoned.map((i) => (
|
|
||||||
<span key={i} className='px-2 py-0.5 rounded-full bg-white border text-[11px]'>
|
|
||||||
{i}
|
|
||||||
</span>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<span className='text-gray-400 text-[11px]'>暂无弃检项目</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='p-3 rounded-2xl bg-yellow-50 border max-h-48 overflow-auto custom-scroll'>
|
|
||||||
<div className='font-medium mb-2'>未查项目 共 {progressGroups.pending.length} 项</div>
|
|
||||||
<div className='flex flex-wrap gap-2'>
|
|
||||||
{(progressGroups.pending.length ? progressGroups.pending : client.pendingItems).map((i) => (
|
|
||||||
<span key={i} className='px-2 py-0.5 rounded-full bg-white border text-[11px]'>
|
|
||||||
{i}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='p-3 rounded-2xl bg-blue-50 border max-h-48 overflow-auto custom-scroll'>
|
|
||||||
<div className='font-medium mb-2'>延期项目 共 {progressGroups.deferred.length} 项</div>
|
|
||||||
<div className='flex flex-wrap gap-2'>
|
|
||||||
{progressGroups.deferred.length ? (
|
|
||||||
progressGroups.deferred.map((i) => (
|
|
||||||
<span key={i} className='px-2 py-0.5 rounded-full bg-white border text-[11px]'>
|
|
||||||
{i}
|
|
||||||
</span>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<span className='text-gray-400 text-[11px]'>暂无延期项目</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ExamDeliveryPanel = ({ client }: { client: ExamClient }) => (
|
const ExamDeliveryPanel = ({ client }: { client: ExamClient }) => (
|
||||||
<div className='flex justify-center'>
|
<div className='flex justify-center'>
|
||||||
<div className='w-full rounded-2xl border shadow-sm px-6 py-4 text-xs text-gray-800'>
|
<div className='w-full rounded-2xl border shadow-sm px-6 py-4 text-xs text-gray-800'>
|
||||||
|
|||||||
Reference in New Issue
Block a user