完善体检用餐登记面板
This commit is contained in:
@@ -1,24 +1,101 @@
|
|||||||
import type { ExamClient } from '../../data/mockData';
|
import { useEffect, useState } from 'react';
|
||||||
import { EXAM_CLIENTS } from '../../data/mockData';
|
|
||||||
|
import type { PoPhysicalExamDiningLog } from '../../api';
|
||||||
|
import { getPhysicalExamDiningList, registerPhysicalExamDining } from '../../api';
|
||||||
import { InfoCard } from '../ui';
|
import { InfoCard } from '../ui';
|
||||||
|
|
||||||
interface MealRegistrationModalProps {
|
interface MealRegistrationModalProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
totalExamCount: number;
|
|
||||||
mealCount: number;
|
|
||||||
notMealCount: number;
|
|
||||||
mealDoneIds: string[];
|
|
||||||
onMealDone: (id: string) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MealRegistrationModal = ({
|
export const MealRegistrationModal = ({ onClose }: MealRegistrationModalProps) => {
|
||||||
onClose,
|
const [diningType, setDiningType] = useState<number>(1); // 1-全部 2-已用餐 3-未用餐
|
||||||
totalExamCount,
|
const [loading, setLoading] = useState(false);
|
||||||
mealCount,
|
const [error, setError] = useState<string | null>(null);
|
||||||
notMealCount,
|
const [diningData, setDiningData] = useState<{
|
||||||
mealDoneIds,
|
today_exam_count: number;
|
||||||
onMealDone,
|
dined_count: number;
|
||||||
}: MealRegistrationModalProps) => {
|
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 (
|
return (
|
||||||
<div className='fixed inset-0 z-40 flex items-center justify-center bg-black/30'>
|
<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'>
|
<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>
|
||||||
<div className='px-4 py-4 bg-gray-50/60'>
|
<div className='px-4 py-4 bg-gray-50/60'>
|
||||||
<div className='space-y-3'>
|
<div className='space-y-3'>
|
||||||
<div className='grid grid-cols-3 gap-3 text-xs'>
|
{/* 筛选按钮 */}
|
||||||
<InfoCard label='今日体检人数' value={totalExamCount} />
|
<div className='flex items-center gap-2'>
|
||||||
<InfoCard label='已用餐人数' value={mealCount} />
|
<button
|
||||||
<InfoCard label='未用餐人数' value={notMealCount} />
|
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>
|
||||||
<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'>
|
<div className='max-h-60 overflow-auto border rounded-2xl bg-white p-2 text-xs'>
|
||||||
{EXAM_CLIENTS.map((c: ExamClient) => {
|
{loading ? (
|
||||||
const checked = mealDoneIds.includes(c.id);
|
<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 (
|
return (
|
||||||
<label
|
<label
|
||||||
key={c.id}
|
key={item.physical_exam_id || item.exam_no}
|
||||||
className='flex items-center justify-between px-3 py-1.5 rounded-2xl hover:bg-gray-50 cursor-pointer'
|
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>
|
<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>
|
||||||
<span className='flex items-center gap-2'>
|
<span className='flex items-center gap-2'>
|
||||||
<span className='text-gray-400'>{c.status}</span>
|
<span className='text-gray-400'>{item.physical_exam_status_name || '未知状态'}</span>
|
||||||
<input type='checkbox' checked={checked} onChange={() => onMealDone(c.id)} />
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
checked={isDined}
|
||||||
|
disabled={!canToggle || isRegistering}
|
||||||
|
onChange={() => handleMealToggle(item)}
|
||||||
|
/>
|
||||||
|
{isRegistering && <span className='text-[11px] text-gray-400'>登记中...</span>}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
);
|
);
|
||||||
})}
|
})
|
||||||
|
) : (
|
||||||
|
<div className='flex items-center justify-center py-4 text-gray-500'>暂无数据</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,11 +8,6 @@ interface QuickActionModalProps {
|
|||||||
noteText: string;
|
noteText: string;
|
||||||
onNoteChange: (v: string) => void;
|
onNoteChange: (v: string) => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
totalExamCount: number;
|
|
||||||
mealCount: number;
|
|
||||||
notMealCount: number;
|
|
||||||
mealDoneIds: string[];
|
|
||||||
onMealDone: (id: string) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const QuickActionModal = ({
|
export const QuickActionModal = ({
|
||||||
@@ -20,27 +15,13 @@ export const QuickActionModal = ({
|
|||||||
noteText,
|
noteText,
|
||||||
onNoteChange,
|
onNoteChange,
|
||||||
onClose,
|
onClose,
|
||||||
totalExamCount,
|
|
||||||
mealCount,
|
|
||||||
notMealCount,
|
|
||||||
mealDoneIds,
|
|
||||||
onMealDone,
|
|
||||||
}: QuickActionModalProps) => {
|
}: QuickActionModalProps) => {
|
||||||
if (action === 'none') {
|
if (action === 'none') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action === 'meal') {
|
if (action === 'meal') {
|
||||||
return (
|
return <MealRegistrationModal onClose={onClose} />;
|
||||||
<MealRegistrationModal
|
|
||||||
onClose={onClose}
|
|
||||||
totalExamCount={totalExamCount}
|
|
||||||
mealCount={mealCount}
|
|
||||||
notMealCount={notMealCount}
|
|
||||||
mealDoneIds={mealDoneIds}
|
|
||||||
onMealDone={onMealDone}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action === 'note') {
|
if (action === 'note') {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { Outlet, useLocation, useNavigate } from 'react-router-dom';
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import type { QuickActionType } from '../data/mockData';
|
import type { QuickActionType } from '../data/mockData';
|
||||||
import { EXAM_CLIENTS } from '../data/mockData';
|
|
||||||
import { QuickActionModal } from '../components/modals/QuickActionModal';
|
import { QuickActionModal } from '../components/modals/QuickActionModal';
|
||||||
import { LoginModal } from '../components/modals/LoginModal';
|
import { LoginModal } from '../components/modals/LoginModal';
|
||||||
import { Sidebar, type SectionKey } from '../components/layout/Sidebar';
|
import { Sidebar, type SectionKey } from '../components/layout/Sidebar';
|
||||||
@@ -34,13 +33,6 @@ export const MainLayout = () => {
|
|||||||
const [noteText, setNoteText] = useState('');
|
const [noteText, setNoteText] = useState('');
|
||||||
const [loginModalOpen, setLoginModalOpen] = useState(false);
|
const [loginModalOpen, setLoginModalOpen] = useState(false);
|
||||||
const [operatorName, setOperatorName] = useState<string>('');
|
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 navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@@ -54,10 +46,6 @@ export const MainLayout = () => {
|
|||||||
navigate(sectionToRoute[section]);
|
navigate(sectionToRoute[section]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMealDone = (id: string) => {
|
|
||||||
setMealDoneIds((prev) => (prev.includes(id) ? prev : prev.concat(id)));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLoginSuccess = (phone: string) => {
|
const handleLoginSuccess = (phone: string) => {
|
||||||
// 实际项目中应该从后端获取用户信息
|
// 实际项目中应该从后端获取用户信息
|
||||||
// 这里暂时使用手机号后4位作为操作员名称
|
// 这里暂时使用手机号后4位作为操作员名称
|
||||||
@@ -106,11 +94,6 @@ export const MainLayout = () => {
|
|||||||
noteText={noteText}
|
noteText={noteText}
|
||||||
onNoteChange={setNoteText}
|
onNoteChange={setNoteText}
|
||||||
onClose={() => setQuickAction('none')}
|
onClose={() => setQuickAction('none')}
|
||||||
totalExamCount={totalExamCount}
|
|
||||||
mealCount={mealCount}
|
|
||||||
notMealCount={notMealCount}
|
|
||||||
mealDoneIds={mealDoneIds}
|
|
||||||
onMealDone={handleMealDone}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user