完善体检用餐登记面板
This commit is contained in:
@@ -1,24 +1,101 @@
|
||||
import type { ExamClient } from '../../data/mockData';
|
||||
import { EXAM_CLIENTS } from '../../data/mockData';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import type { PoPhysicalExamDiningLog } from '../../api';
|
||||
import { getPhysicalExamDiningList, registerPhysicalExamDining } from '../../api';
|
||||
import { InfoCard } from '../ui';
|
||||
|
||||
interface MealRegistrationModalProps {
|
||||
onClose: () => void;
|
||||
totalExamCount: number;
|
||||
mealCount: number;
|
||||
notMealCount: number;
|
||||
mealDoneIds: string[];
|
||||
onMealDone: (id: string) => void;
|
||||
}
|
||||
|
||||
export const MealRegistrationModal = ({
|
||||
onClose,
|
||||
totalExamCount,
|
||||
mealCount,
|
||||
notMealCount,
|
||||
mealDoneIds,
|
||||
onMealDone,
|
||||
}: MealRegistrationModalProps) => {
|
||||
export const MealRegistrationModal = ({ onClose }: MealRegistrationModalProps) => {
|
||||
const [diningType, setDiningType] = useState<number>(1); // 1-全部 2-已用餐 3-未用餐
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [diningData, setDiningData] = useState<{
|
||||
today_exam_count: number;
|
||||
dined_count: number;
|
||||
not_dined_count: number;
|
||||
DiningList: PoPhysicalExamDiningLog[];
|
||||
} | null>(null);
|
||||
const [registeringIds, setRegisteringIds] = useState<Set<number>>(new Set());
|
||||
|
||||
// 获取用餐列表
|
||||
useEffect(() => {
|
||||
const fetchDiningList = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await getPhysicalExamDiningList({ dining_type: diningType });
|
||||
if (res.Status === 200 && res.Data) {
|
||||
setDiningData({
|
||||
today_exam_count: res.Data.today_exam_count || 0,
|
||||
dined_count: res.Data.dined_count || 0,
|
||||
not_dined_count: res.Data.not_dined_count || 0,
|
||||
DiningList: res.Data.DiningList || [],
|
||||
});
|
||||
} else {
|
||||
setError(res.Message || '获取用餐列表失败');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取用餐列表失败', err);
|
||||
setError('获取用餐列表失败,请稍后重试');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchDiningList();
|
||||
}, [diningType]);
|
||||
|
||||
// 处理用餐登记
|
||||
const handleMealToggle = async (item: PoPhysicalExamDiningLog) => {
|
||||
if (!item.physical_exam_id || !item.exam_no || !item.customer_name) {
|
||||
return;
|
||||
}
|
||||
|
||||
const examId = item.physical_exam_id;
|
||||
const isCurrentlyDined = item.is_dining === 1;
|
||||
|
||||
// 如果已经用餐,则不处理(取消用餐可能需要其他接口)
|
||||
if (isCurrentlyDined) {
|
||||
return;
|
||||
}
|
||||
|
||||
setRegisteringIds((prev) => new Set(prev).add(examId));
|
||||
try {
|
||||
const res = await registerPhysicalExamDining({
|
||||
physical_exam_id: examId,
|
||||
exam_no: item.exam_no,
|
||||
customer_name: item.customer_name,
|
||||
});
|
||||
|
||||
if (res.Status === 200) {
|
||||
// 刷新列表
|
||||
const refreshRes = await getPhysicalExamDiningList({ dining_type: diningType });
|
||||
if (refreshRes.Status === 200 && refreshRes.Data) {
|
||||
setDiningData({
|
||||
today_exam_count: refreshRes.Data.today_exam_count || 0,
|
||||
dined_count: refreshRes.Data.dined_count || 0,
|
||||
not_dined_count: refreshRes.Data.not_dined_count || 0,
|
||||
DiningList: refreshRes.Data.DiningList || [],
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setError(res.Message || '用餐登记失败');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('用餐登记失败', err);
|
||||
setError('用餐登记失败,请稍后重试');
|
||||
} finally {
|
||||
setRegisteringIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(examId);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='fixed inset-0 z-40 flex items-center justify-center bg-black/30'>
|
||||
<div className='w-[560px] max-w-[95vw] bg-white rounded-3xl shadow-xl overflow-hidden text-sm'>
|
||||
@@ -30,30 +107,83 @@ export const MealRegistrationModal = ({
|
||||
</div>
|
||||
<div className='px-4 py-4 bg-gray-50/60'>
|
||||
<div className='space-y-3'>
|
||||
<div className='grid grid-cols-3 gap-3 text-xs'>
|
||||
<InfoCard label='今日体检人数' value={totalExamCount} />
|
||||
<InfoCard label='已用餐人数' value={mealCount} />
|
||||
<InfoCard label='未用餐人数' value={notMealCount} />
|
||||
{/* 筛选按钮 */}
|
||||
<div className='flex items-center gap-2'>
|
||||
<button
|
||||
className={`px-3 py-1 rounded-2xl border text-xs ${diningType === 1 ? 'bg-blue-600 text-white border-blue-600' : 'bg-white text-gray-700'
|
||||
}`}
|
||||
onClick={() => setDiningType(1)}
|
||||
>
|
||||
全部
|
||||
</button>
|
||||
<button
|
||||
className={`px-3 py-1 rounded-2xl border text-xs ${diningType === 2 ? 'bg-blue-600 text-white border-blue-600' : 'bg-white text-gray-700'
|
||||
}`}
|
||||
onClick={() => setDiningType(2)}
|
||||
>
|
||||
已用餐
|
||||
</button>
|
||||
<button
|
||||
className={`px-3 py-1 rounded-2xl border text-xs ${diningType === 3 ? 'bg-blue-600 text-white border-blue-600' : 'bg-white text-gray-700'
|
||||
}`}
|
||||
onClick={() => setDiningType(3)}
|
||||
>
|
||||
未用餐
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 统计信息 */}
|
||||
{diningData && (
|
||||
<div className='grid grid-cols-3 gap-3 text-xs'>
|
||||
<InfoCard label='今日体检人数' value={diningData.today_exam_count} />
|
||||
<InfoCard label='已用餐人数' value={diningData.dined_count} />
|
||||
<InfoCard label='未用餐人数' value={diningData.not_dined_count} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 错误提示 */}
|
||||
{error && <div className='text-xs text-amber-600'>{error}</div>}
|
||||
|
||||
{/* 客户列表 */}
|
||||
<div className='text-xs text-gray-600 mt-2 mb-1'>
|
||||
{diningType === 1 && '选择已用餐客户进行登记:'}
|
||||
{diningType === 2 && '已用餐客户列表:'}
|
||||
{diningType === 3 && '选择未用餐客户进行登记:'}
|
||||
</div>
|
||||
<div className='text-xs text-gray-600 mt-2 mb-1'>选择已用餐客户进行登记:</div>
|
||||
<div className='max-h-60 overflow-auto border rounded-2xl bg-white p-2 text-xs'>
|
||||
{EXAM_CLIENTS.map((c: ExamClient) => {
|
||||
const checked = mealDoneIds.includes(c.id);
|
||||
{loading ? (
|
||||
<div className='flex items-center justify-center py-4 text-gray-500'>加载中...</div>
|
||||
) : diningData && diningData.DiningList.length > 0 ? (
|
||||
diningData.DiningList.map((item) => {
|
||||
const isDined = item.is_dining === 1;
|
||||
const isRegistering = item.physical_exam_id ? registeringIds.has(item.physical_exam_id) : false;
|
||||
const canToggle = !isDined && !isRegistering;
|
||||
|
||||
return (
|
||||
<label
|
||||
key={c.id}
|
||||
className='flex items-center justify-between px-3 py-1.5 rounded-2xl hover:bg-gray-50 cursor-pointer'
|
||||
key={item.physical_exam_id || item.exam_no}
|
||||
className={`flex items-center justify-between px-3 py-1.5 rounded-2xl ${canToggle ? 'hover:bg-gray-50 cursor-pointer' : 'cursor-not-allowed opacity-60'
|
||||
}`}
|
||||
>
|
||||
<span>
|
||||
{c.name} <span className='text-gray-400 text-[11px]'>({c.id})</span>
|
||||
{item.customer_name || '未知'} <span className='text-gray-400 text-[11px]'>({item.exam_no || 'N/A'})</span>
|
||||
</span>
|
||||
<span className='flex items-center gap-2'>
|
||||
<span className='text-gray-400'>{c.status}</span>
|
||||
<input type='checkbox' checked={checked} onChange={() => onMealDone(c.id)} />
|
||||
<span className='text-gray-400'>{item.physical_exam_status_name || '未知状态'}</span>
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={isDined}
|
||||
disabled={!canToggle || isRegistering}
|
||||
onChange={() => handleMealToggle(item)}
|
||||
/>
|
||||
{isRegistering && <span className='text-[11px] text-gray-400'>登记中...</span>}
|
||||
</span>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
})
|
||||
) : (
|
||||
<div className='flex items-center justify-center py-4 text-gray-500'>暂无数据</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,11 +8,6 @@ interface QuickActionModalProps {
|
||||
noteText: string;
|
||||
onNoteChange: (v: string) => void;
|
||||
onClose: () => void;
|
||||
totalExamCount: number;
|
||||
mealCount: number;
|
||||
notMealCount: number;
|
||||
mealDoneIds: string[];
|
||||
onMealDone: (id: string) => void;
|
||||
}
|
||||
|
||||
export const QuickActionModal = ({
|
||||
@@ -20,27 +15,13 @@ export const QuickActionModal = ({
|
||||
noteText,
|
||||
onNoteChange,
|
||||
onClose,
|
||||
totalExamCount,
|
||||
mealCount,
|
||||
notMealCount,
|
||||
mealDoneIds,
|
||||
onMealDone,
|
||||
}: QuickActionModalProps) => {
|
||||
if (action === 'none') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (action === 'meal') {
|
||||
return (
|
||||
<MealRegistrationModal
|
||||
onClose={onClose}
|
||||
totalExamCount={totalExamCount}
|
||||
mealCount={mealCount}
|
||||
notMealCount={notMealCount}
|
||||
mealDoneIds={mealDoneIds}
|
||||
onMealDone={onMealDone}
|
||||
/>
|
||||
);
|
||||
return <MealRegistrationModal onClose={onClose} />;
|
||||
}
|
||||
|
||||
if (action === 'note') {
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import type { QuickActionType } from '../data/mockData';
|
||||
import { EXAM_CLIENTS } from '../data/mockData';
|
||||
import { QuickActionModal } from '../components/modals/QuickActionModal';
|
||||
import { LoginModal } from '../components/modals/LoginModal';
|
||||
import { Sidebar, type SectionKey } from '../components/layout/Sidebar';
|
||||
@@ -34,13 +33,6 @@ export const MainLayout = () => {
|
||||
const [noteText, setNoteText] = useState('');
|
||||
const [loginModalOpen, setLoginModalOpen] = useState(false);
|
||||
const [operatorName, setOperatorName] = useState<string>('');
|
||||
const [mealDoneIds, setMealDoneIds] = useState<string[]>(
|
||||
EXAM_CLIENTS.filter((c) => c.status === '用餐').map((c) => c.id),
|
||||
);
|
||||
|
||||
const totalExamCount = EXAM_CLIENTS.length;
|
||||
const mealCount = mealDoneIds.length;
|
||||
const notMealCount = totalExamCount - mealCount;
|
||||
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
@@ -54,10 +46,6 @@ export const MainLayout = () => {
|
||||
navigate(sectionToRoute[section]);
|
||||
};
|
||||
|
||||
const handleMealDone = (id: string) => {
|
||||
setMealDoneIds((prev) => (prev.includes(id) ? prev : prev.concat(id)));
|
||||
};
|
||||
|
||||
const handleLoginSuccess = (phone: string) => {
|
||||
// 实际项目中应该从后端获取用户信息
|
||||
// 这里暂时使用手机号后4位作为操作员名称
|
||||
@@ -106,11 +94,6 @@ export const MainLayout = () => {
|
||||
noteText={noteText}
|
||||
onNoteChange={setNoteText}
|
||||
onClose={() => setQuickAction('none')}
|
||||
totalExamCount={totalExamCount}
|
||||
mealCount={mealCount}
|
||||
notMealCount={notMealCount}
|
||||
mealDoneIds={mealDoneIds}
|
||||
onMealDone={handleMealDone}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user