Compare commits
6 Commits
ee7dd0d6e1
...
093b9fe9a3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
093b9fe9a3 | ||
|
|
a7b5548b35 | ||
|
|
d70b027f20 | ||
|
|
4b40d47b71 | ||
|
|
21481dbd88 | ||
|
|
fd994edf3e |
@@ -21,11 +21,7 @@ const ERROR_CODE_MAP: Record<string, string> = {
|
||||
};
|
||||
|
||||
function safeGet(key: string): string {
|
||||
try {
|
||||
return secureGet(key);
|
||||
} catch {
|
||||
return Taro.getStorageSync(key) || '';
|
||||
}
|
||||
return secureGet(key);
|
||||
}
|
||||
|
||||
// --- Concurrency limiter ---
|
||||
|
||||
@@ -4,11 +4,9 @@ import * as authApi from '@/services/auth';
|
||||
import { secureGet, secureSet, secureRemove } from '@/utils/secure-storage';
|
||||
import { clearRequestCache, markLoggingOut, clearLoggingOut, setCachedPatientId } from '@/services/request';
|
||||
|
||||
// secureGet fallback: _es_ 加密键为空时尝试明文键(兼容 MCP 注入等场景)
|
||||
// secureGet 已内置明文键 fallback,无需再手动 fallback
|
||||
function storageGet(key: string): string {
|
||||
const val = secureGet(key);
|
||||
if (val) return val;
|
||||
return Taro.getStorageSync(key) || '';
|
||||
return secureGet(key);
|
||||
}
|
||||
import { resetAllStores } from './index';
|
||||
|
||||
|
||||
@@ -12,11 +12,9 @@ function xorEncrypt(data: string, key: string): string {
|
||||
|
||||
function toBase64(str: string): string {
|
||||
try {
|
||||
const buffer = new Uint8Array(str.length);
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
buffer[i] = str.charCodeAt(i);
|
||||
}
|
||||
return Taro.arrayBufferToBase64(buffer.buffer);
|
||||
const encoder = new TextEncoder();
|
||||
const uint8 = encoder.encode(str);
|
||||
return Taro.arrayBufferToBase64(uint8.buffer as ArrayBuffer);
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
@@ -25,12 +23,8 @@ function toBase64(str: string): string {
|
||||
function fromBase64(b64: string): string {
|
||||
try {
|
||||
const buffer = Taro.base64ToArrayBuffer(b64);
|
||||
const arr = new Uint8Array(buffer);
|
||||
let result = '';
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
result += String.fromCharCode(arr[i]);
|
||||
}
|
||||
return result;
|
||||
const decoder = new TextDecoder();
|
||||
return decoder.decode(new Uint8Array(buffer));
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
@@ -55,7 +49,11 @@ export function secureSet(key: string, value: string): void {
|
||||
export function secureGet(key: string): string {
|
||||
const prefixedKey = STORAGE_PREFIX + key;
|
||||
const raw = Taro.getStorageSync(prefixedKey);
|
||||
if (!raw || typeof raw !== 'string') return '';
|
||||
if (!raw || typeof raw !== 'string') {
|
||||
// fallback: 尝试读取明文键(兼容 MCP 注入等场景)
|
||||
const plain = Taro.getStorageSync(key);
|
||||
return (plain && typeof plain === 'string') ? plain : '';
|
||||
}
|
||||
|
||||
// 始终尝试 base64 解码 + XOR 解密(secureSet 的写入格式)
|
||||
try {
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"antd": "^6.3.5",
|
||||
"axios": "^1.15.0",
|
||||
"dayjs": "^1.11.20",
|
||||
"dompurify": "^3.4.5",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-router-dom": "^7.14.0",
|
||||
@@ -36,6 +37,7 @@
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@types/dompurify": "^3.2.0",
|
||||
"@types/node": "^24.12.2",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
|
||||
27
apps/web/pnpm-lock.yaml
generated
27
apps/web/pnpm-lock.yaml
generated
@@ -38,6 +38,9 @@ importers:
|
||||
dayjs:
|
||||
specifier: ^1.11.20
|
||||
version: 1.11.20
|
||||
dompurify:
|
||||
specifier: ^3.4.5
|
||||
version: 3.4.5
|
||||
react:
|
||||
specifier: ^19.2.4
|
||||
version: 19.2.5
|
||||
@@ -69,6 +72,9 @@ importers:
|
||||
'@testing-library/react':
|
||||
specifier: ^16.3.2
|
||||
version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||
'@types/dompurify':
|
||||
specifier: ^3.2.0
|
||||
version: 3.2.0
|
||||
'@types/node':
|
||||
specifier: ^24.12.2
|
||||
version: 24.12.2
|
||||
@@ -1172,6 +1178,10 @@ packages:
|
||||
'@types/deep-eql@4.0.2':
|
||||
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
|
||||
|
||||
'@types/dompurify@3.2.0':
|
||||
resolution: {integrity: sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg==}
|
||||
deprecated: This is a stub types definition. dompurify provides its own type definitions, so you do not need this installed.
|
||||
|
||||
'@types/estree@1.0.8':
|
||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||
|
||||
@@ -1201,6 +1211,9 @@ packages:
|
||||
'@types/statuses@2.0.6':
|
||||
resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==}
|
||||
|
||||
'@types/trusted-types@2.0.7':
|
||||
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.58.1':
|
||||
resolution: {integrity: sha512-eSkwoemjo76bdXl2MYqtxg51HNwUSkWfODUOQ3PaTLZGh9uIWWFZIjyjaJnex7wXDu+TRx+ATsnSxdN9YWfRTQ==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
@@ -1769,6 +1782,9 @@ packages:
|
||||
dom7@3.0.0:
|
||||
resolution: {integrity: sha512-oNlcUdHsC4zb7Msx7JN3K0Nro1dzJ48knvBOnDPKJ2GV9wl1i5vydJZUSyOfrkKFDZEud/jBsTk92S/VGSAe/g==}
|
||||
|
||||
dompurify@3.4.5:
|
||||
resolution: {integrity: sha512-OrwIBKsdNSVEeubdJ1HBv/wNENRM9ytAVCv7YXt//A3vPdVMNuACRqK9mXCGCBW2ln7BT/A4X0jXHo2Gu89miA==}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -4199,6 +4215,10 @@ snapshots:
|
||||
|
||||
'@types/deep-eql@4.0.2': {}
|
||||
|
||||
'@types/dompurify@3.2.0':
|
||||
dependencies:
|
||||
dompurify: 3.4.5
|
||||
|
||||
'@types/estree@1.0.8': {}
|
||||
|
||||
'@types/event-emitter@0.3.5': {}
|
||||
@@ -4225,6 +4245,9 @@ snapshots:
|
||||
|
||||
'@types/statuses@2.0.6': {}
|
||||
|
||||
'@types/trusted-types@2.0.7':
|
||||
optional: true
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.58.1(@typescript-eslint/parser@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2)':
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.12.2
|
||||
@@ -4906,6 +4929,10 @@ snapshots:
|
||||
dependencies:
|
||||
ssr-window: 3.0.0
|
||||
|
||||
dompurify@3.4.5:
|
||||
optionalDependencies:
|
||||
'@types/trusted-types': 2.0.7
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
|
||||
@@ -80,6 +80,8 @@ export const appointmentApi = {
|
||||
patient_id?: string;
|
||||
doctor_id?: string;
|
||||
date?: string;
|
||||
search?: string;
|
||||
appointment_type?: string;
|
||||
}) => {
|
||||
const { data } = await client.get<{
|
||||
success: boolean;
|
||||
|
||||
@@ -46,8 +46,16 @@ export function DrawerForm({
|
||||
}, [open, initialValues, form]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const values = await form.validateFields();
|
||||
await onSubmit(values);
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
await onSubmit(values);
|
||||
} catch (error: unknown) {
|
||||
// validateFields 失败时 error 包含 errorFields(预期行为,不记录)
|
||||
// 其他类型的错误才记录
|
||||
if (error && typeof error === 'object' && !('errorFields' in error)) {
|
||||
console.error('[DrawerForm] submit error:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const gridStyle: React.CSSProperties =
|
||||
|
||||
@@ -81,7 +81,8 @@ export function usePaginatedData<T, F = string>(
|
||||
filtersRef.current ?? searchTextRef.current,
|
||||
);
|
||||
setState({ data: result.data, total: result.total, page: targetPage, loading: false });
|
||||
} catch {
|
||||
} catch (err) {
|
||||
console.warn('[usePaginatedData] 加载数据失败:', err);
|
||||
message.error('加载数据失败');
|
||||
setState((s) => ({ ...s, loading: false }));
|
||||
}
|
||||
@@ -89,26 +90,22 @@ export function usePaginatedData<T, F = string>(
|
||||
[pageSize],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldAutoFetch) {
|
||||
|
||||
refresh(1);
|
||||
}
|
||||
}, [shouldAutoFetch, refresh]);
|
||||
|
||||
// 筛选条件变化时自动刷新(解决 FollowUpTaskList 等组件直接调用 setFilters 不触发刷新的问题)
|
||||
// 合并初始 fetch 和 filters 变化时的 fetch,消除双重请求
|
||||
const isFirstRender = useRef(true);
|
||||
useEffect(() => {
|
||||
if (isFirstRender.current) {
|
||||
isFirstRender.current = false;
|
||||
if (shouldAutoFetch) {
|
||||
refresh(1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (shouldAutoFetch) {
|
||||
|
||||
refresh(1);
|
||||
}
|
||||
// refresh 每次渲染都稳定,不放入依赖数组;filters 变化触发重新 fetch
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [filters]);
|
||||
}, [shouldAutoFetch, filters]);
|
||||
|
||||
return { ...state, searchText, setSearchText, filters, setFilters, refresh };
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
/* Text Colors — Deep navy */
|
||||
--erp-text-primary: #0f172a;
|
||||
--erp-text-secondary: #475569;
|
||||
--erp-text-tertiary: #94a3b8;
|
||||
--erp-text-tertiary: #64748b;
|
||||
--erp-text-inverse: #ffffff;
|
||||
--erp-text-sidebar: #475569;
|
||||
--erp-text-sidebar-active: #2563eb;
|
||||
@@ -273,7 +273,7 @@
|
||||
--login-form-bg: #ffffff;
|
||||
--login-form-text: #0f172a;
|
||||
--login-form-text-secondary: #475569;
|
||||
--login-input-icon-color: #94a3b8;
|
||||
--login-input-icon-color: #64748b;
|
||||
}
|
||||
|
||||
[data-theme='warm'] {
|
||||
|
||||
@@ -221,7 +221,7 @@ export default function Home() {
|
||||
if (role === 'doctor' || role === 'nurse') {
|
||||
pointsApi.getPersonalStats()
|
||||
.then((data) => { if (!cancelled) setPersonalStats(data); })
|
||||
.catch(() => {})
|
||||
.catch((err) => console.warn('[Home] 获取个人积分统计失败:', err))
|
||||
.finally(() => { if (!cancelled) setPersonalLoading(false); });
|
||||
} else {
|
||||
setPersonalLoading(false);
|
||||
@@ -229,13 +229,13 @@ export default function Home() {
|
||||
|
||||
listPendingTasks(1, 5)
|
||||
.then((result) => { if (!cancelled) setPendingTasks(result.data); })
|
||||
.catch(() => {});
|
||||
.catch((err) => console.warn('[Home] 获取待办任务失败:', err));
|
||||
|
||||
listAuditLogs({ page: 1, page_size: 5 })
|
||||
.then((result) => {
|
||||
if (!cancelled) setRecentActivities(result.data.filter((a) => a.action !== 'login_failed'));
|
||||
})
|
||||
.catch(() => {})
|
||||
.catch((err) => console.warn('[Home] 获取审计日志失败:', err))
|
||||
.finally(() => { if (!cancelled) setActivitiesLoading(false); });
|
||||
|
||||
return () => { cancelled = true; };
|
||||
|
||||
@@ -155,6 +155,7 @@ export function usePluginData(
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
@@ -179,7 +180,7 @@ export function usePluginData(
|
||||
setResolvedLabels(result.labels);
|
||||
setLabelMeta(result.meta as Record<string, { plugin_installed: boolean }>);
|
||||
})
|
||||
.catch(() => {});
|
||||
.catch((err) => console.warn('[usePluginData] 获取标签元数据失败:', err));
|
||||
}, [records, fields, pluginId, entityName]);
|
||||
|
||||
const handleFilterChange = (fieldName: string, value: string | undefined) => {
|
||||
|
||||
@@ -251,7 +251,7 @@ export function PluginDashboardPage() {
|
||||
}, [pluginId, selectedEntity, filterableFields, entityStats]);
|
||||
useEffect(() => {
|
||||
const cleanup = loadData();
|
||||
return () => { cleanup?.then((fn) => fn?.()).catch(() => {}); };
|
||||
return () => { cleanup?.then((fn) => fn?.()).catch((err) => console.warn('[PluginDashboard] 清理失败:', err)); };
|
||||
}, [loadData]);
|
||||
// 当前选中实体的总数
|
||||
const currentTotal = useMemo(
|
||||
|
||||
@@ -43,7 +43,7 @@ export default function Roles() {
|
||||
const [selectedPermIds, setSelectedPermIds] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
listPermissions().then(setPermissions).catch(() => {});
|
||||
listPermissions().then(setPermissions).catch((err) => console.warn('[Roles] 获取权限列表失败:', err));
|
||||
}, []);
|
||||
|
||||
const roleDrawer = useCrudDrawer<RoleInfo>({
|
||||
|
||||
@@ -109,9 +109,11 @@ export default function AppointmentList() {
|
||||
status: filters.status || undefined,
|
||||
date: dateStart === dateEnd ? dateStart : undefined,
|
||||
patient_id: urlPatientId || undefined,
|
||||
search: filters.patientSearch || undefined,
|
||||
appointment_type: filters.appointmentType || undefined,
|
||||
});
|
||||
},
|
||||
[],
|
||||
[urlPatientId],
|
||||
);
|
||||
|
||||
const {
|
||||
@@ -218,6 +220,7 @@ export default function AppointmentList() {
|
||||
// 排班校验:医生 + 日期选定后查询排班
|
||||
useEffect(() => {
|
||||
if (!selectedDoctorId || !selectedDate || !drawerOpen) {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setScheduleHint(null);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ export default function ArticleManageList() {
|
||||
useEffect(() => {
|
||||
articleCategoryApi.list()
|
||||
.then((cats) => setCategories(cats.map((c) => ({ id: c.id, name: c.name }))))
|
||||
.catch(() => {});
|
||||
.catch((err) => console.warn('[ArticleManageList] 获取文章分类失败:', err));
|
||||
}, []);
|
||||
|
||||
const handleDelete = async (id: string, version: number) => {
|
||||
|
||||
@@ -46,7 +46,7 @@ export default function DialysisManageList() {
|
||||
if (urlPatientId) {
|
||||
patientApi.get(urlPatientId).then((p) => {
|
||||
if (p) setPatientOptions([{ id: p.id, name: p.name }]);
|
||||
}).catch(() => {});
|
||||
}).catch((err) => console.warn('[DialysisManageList] 获取患者信息失败:', err));
|
||||
}
|
||||
}, [urlPatientId]);
|
||||
|
||||
|
||||
@@ -10,6 +10,12 @@ import {
|
||||
} from '../../../api/health/points';
|
||||
import { doctorApi } from '../../../api/health/doctors';
|
||||
|
||||
// 全局缓存:多组件实例共享数据,避免重复请求
|
||||
let cachedStats: Record<string, unknown> | null = null;
|
||||
let cachedAt = 0;
|
||||
const CACHE_TTL = 5 * 60 * 1000; // 5 分钟
|
||||
let fetchPromise: Promise<Record<string, unknown>> | null = null;
|
||||
|
||||
export interface StatsData {
|
||||
patientStats: PatientStatistics | null;
|
||||
consultationStats: ConsultationStatistics | null;
|
||||
@@ -36,41 +42,101 @@ export function useStatsData(): StatsData {
|
||||
const [doctorCount, setDoctorCount] = useState(0);
|
||||
|
||||
const fetchAllStats = useCallback(async () => {
|
||||
// 缓存未过期,直接使用
|
||||
if (cachedStats && Date.now() - cachedAt < CACHE_TTL) {
|
||||
const c = cachedStats;
|
||||
setPatientStats(c.patientStats as PatientStatistics | null);
|
||||
setConsultationStats(c.consultationStats as ConsultationStatistics | null);
|
||||
setFollowUpStats(c.followUpStats as FollowUpStatistics | null);
|
||||
setPointsStats(c.pointsStats as PointsStatistics | null);
|
||||
setHealthDataStats(c.healthDataStats as HealthDataStats | null);
|
||||
setDialysisStats(c.dialysisStats as DialysisStatistics | null);
|
||||
setDoctorCount(c.doctorCount as number);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 已有正在进行的请求,等待它完成
|
||||
if (fetchPromise) {
|
||||
const c = await fetchPromise;
|
||||
setPatientStats(c.patientStats as PatientStatistics | null);
|
||||
setConsultationStats(c.consultationStats as ConsultationStatistics | null);
|
||||
setFollowUpStats(c.followUpStats as FollowUpStatistics | null);
|
||||
setPointsStats(c.pointsStats as PointsStatistics | null);
|
||||
setHealthDataStats(c.healthDataStats as HealthDataStats | null);
|
||||
setDialysisStats(c.dialysisStats as DialysisStatistics | null);
|
||||
setDoctorCount(c.doctorCount as number);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
let hasAnyError = false;
|
||||
const errors: string[] = [];
|
||||
// 创建新请求
|
||||
fetchPromise = (async () => {
|
||||
let hasAnyError = false;
|
||||
const errors: string[] = [];
|
||||
|
||||
const tryFetch = async <T,>(fn: () => Promise<T>, setter: (v: T) => void, label: string) => {
|
||||
try {
|
||||
const data = await fn();
|
||||
setter(data);
|
||||
} catch {
|
||||
hasAnyError = true;
|
||||
errors.push(label);
|
||||
const results: Record<string, unknown> = {
|
||||
patientStats: null,
|
||||
consultationStats: null,
|
||||
followUpStats: null,
|
||||
pointsStats: null,
|
||||
healthDataStats: null,
|
||||
dialysisStats: null,
|
||||
doctorCount: 0,
|
||||
};
|
||||
|
||||
const tryFetch = async <T,>(fn: () => Promise<T>, key: string, label: string) => {
|
||||
try {
|
||||
const data = await fn();
|
||||
results[key] = data;
|
||||
} catch {
|
||||
hasAnyError = true;
|
||||
errors.push(label);
|
||||
}
|
||||
};
|
||||
|
||||
await Promise.all([
|
||||
tryFetch(() => pointsApi.getPatientStats({ silent: true }), 'patientStats', '患者'),
|
||||
tryFetch(() => pointsApi.getConsultationStats({ silent: true }), 'consultationStats', '咨询'),
|
||||
tryFetch(() => pointsApi.getFollowUpStats({ silent: true }), 'followUpStats', '随访'),
|
||||
tryFetch(() => pointsApi.getStatistics({ silent: true }), 'pointsStats', '积分'),
|
||||
tryFetch(() => pointsApi.getHealthDataStats({ silent: true }), 'healthDataStats', '健康数据'),
|
||||
tryFetch(() => pointsApi.getDialysisStats({ silent: true }), 'dialysisStats', '透析'),
|
||||
tryFetch(
|
||||
async () => { const r = await doctorApi.list({ page: 1, page_size: 1 }); return r.total; },
|
||||
'doctorCount',
|
||||
'医护',
|
||||
),
|
||||
]);
|
||||
|
||||
if (!hasAnyError || errors.length < 7) {
|
||||
cachedStats = results;
|
||||
cachedAt = Date.now();
|
||||
}
|
||||
};
|
||||
|
||||
await Promise.all([
|
||||
tryFetch(() => pointsApi.getPatientStats({ silent: true }), setPatientStats, '患者'),
|
||||
tryFetch(() => pointsApi.getConsultationStats({ silent: true }), setConsultationStats, '咨询'),
|
||||
tryFetch(() => pointsApi.getFollowUpStats({ silent: true }), setFollowUpStats, '随访'),
|
||||
tryFetch(() => pointsApi.getStatistics({ silent: true }), setPointsStats, '积分'),
|
||||
tryFetch(() => pointsApi.getHealthDataStats({ silent: true }), setHealthDataStats, '健康数据'),
|
||||
tryFetch(() => pointsApi.getDialysisStats({ silent: true }), setDialysisStats, '透析'),
|
||||
tryFetch(
|
||||
async () => { const r = await doctorApi.list({ page: 1, page_size: 1 }); return r.total; },
|
||||
setDoctorCount,
|
||||
'医护',
|
||||
),
|
||||
]);
|
||||
if (hasAnyError && errors.length === 7) {
|
||||
setError('加载统计数据失败');
|
||||
}
|
||||
|
||||
if (hasAnyError && errors.length === 7) {
|
||||
setError('加载统计数据失败');
|
||||
return results;
|
||||
})();
|
||||
|
||||
try {
|
||||
const c = await fetchPromise;
|
||||
setPatientStats(c.patientStats as PatientStatistics | null);
|
||||
setConsultationStats(c.consultationStats as ConsultationStatistics | null);
|
||||
setFollowUpStats(c.followUpStats as FollowUpStatistics | null);
|
||||
setPointsStats(c.pointsStats as PointsStatistics | null);
|
||||
setHealthDataStats(c.healthDataStats as HealthDataStats | null);
|
||||
setDialysisStats(c.dialysisStats as DialysisStatistics | null);
|
||||
setDoctorCount(c.doctorCount as number);
|
||||
} finally {
|
||||
fetchPromise = null;
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useMemo } from 'react';
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
interface ArticlePhonePreviewProps {
|
||||
title: string;
|
||||
@@ -240,7 +241,7 @@ export default function ArticlePhonePreview({
|
||||
|
||||
<div className="mp-content">
|
||||
{content && content !== '<p><br></p>' ? (
|
||||
<div dangerouslySetInnerHTML={{ __html: content }} />
|
||||
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(content) }} />
|
||||
) : (
|
||||
<div className="mp-empty">
|
||||
在左侧编辑器中输入内容
|
||||
|
||||
@@ -25,7 +25,7 @@ export function DoctorSelect({ value, onChange, placeholder }: Props) {
|
||||
})),
|
||||
);
|
||||
}
|
||||
}).catch(() => {});
|
||||
}).catch((err) => console.warn('[DoctorSelect] 获取医生列表失败:', err));
|
||||
return () => { cancelled = true; };
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -271,7 +271,7 @@ export default function AdminDashboard() {
|
||||
statsData.patientStats?.total_patients ??
|
||||
0,
|
||||
pct: 100,
|
||||
color: "#94A3B8",
|
||||
color: "#64748B",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -294,7 +294,7 @@ export default function AdminDashboard() {
|
||||
gap: 12,
|
||||
marginBottom: 20,
|
||||
padding: "14px 20px",
|
||||
background: "#fff",
|
||||
background: "var(--erp-bg-container, #fff)",
|
||||
borderRadius: 12,
|
||||
border: "1px solid #E2E8F0",
|
||||
}}
|
||||
@@ -348,7 +348,7 @@ export default function AdminDashboard() {
|
||||
<div
|
||||
key={card.label}
|
||||
style={{
|
||||
background: "#fff",
|
||||
background: "var(--erp-bg-container, #fff)",
|
||||
borderRadius: 12,
|
||||
border: "1px solid #E2E8F0",
|
||||
overflow: "hidden",
|
||||
@@ -358,13 +358,13 @@ export default function AdminDashboard() {
|
||||
>
|
||||
<div style={{ height: 3, background: card.gradient }} />
|
||||
<div style={{ padding: "14px 18px" }}>
|
||||
<div style={{ fontSize: 12, color: "#94A3B8", marginBottom: 4 }}>
|
||||
<div style={{ fontSize: 12, color: "#64748B", marginBottom: 4 }}>
|
||||
{card.label}
|
||||
</div>
|
||||
<div style={{ fontSize: 26, fontWeight: 700, color: card.color }}>
|
||||
{card.value}
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: "#94A3B8", marginTop: 3 }}>
|
||||
<div style={{ fontSize: 11, color: "#64748B", marginTop: 3 }}>
|
||||
{card.sub}
|
||||
</div>
|
||||
</div>
|
||||
@@ -384,7 +384,7 @@ export default function AdminDashboard() {
|
||||
{/* 最近审计日志 */}
|
||||
<div
|
||||
style={{
|
||||
background: "#fff",
|
||||
background: "var(--erp-bg-container, #fff)",
|
||||
borderRadius: 12,
|
||||
border: "1px solid #E2E8F0",
|
||||
overflow: "hidden",
|
||||
@@ -412,7 +412,7 @@ export default function AdminDashboard() {
|
||||
style={{
|
||||
padding: 24,
|
||||
textAlign: "center",
|
||||
color: "#94A3B8",
|
||||
color: "#64748B",
|
||||
fontSize: 13,
|
||||
}}
|
||||
>
|
||||
@@ -488,7 +488,7 @@ export default function AdminDashboard() {
|
||||
{actionLabel}了{resourceLabel}
|
||||
</span>
|
||||
<span
|
||||
style={{ fontSize: 11, color: "#94A3B8", flexShrink: 0 }}
|
||||
style={{ fontSize: 11, color: "#64748B", flexShrink: 0 }}
|
||||
>
|
||||
{formatTimeAgo(log.created_at)}
|
||||
</span>
|
||||
@@ -501,7 +501,7 @@ export default function AdminDashboard() {
|
||||
{/* 模块状态 */}
|
||||
<div
|
||||
style={{
|
||||
background: "#fff",
|
||||
background: "var(--erp-bg-container, #fff)",
|
||||
borderRadius: 12,
|
||||
border: "1px solid #E2E8F0",
|
||||
overflow: "hidden",
|
||||
@@ -539,7 +539,7 @@ export default function AdminDashboard() {
|
||||
<div style={{ fontSize: 13, fontWeight: 500 }}>
|
||||
{mod.display_name}
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: "#94A3B8" }}>
|
||||
<div style={{ fontSize: 11, color: "#64748B" }}>
|
||||
{mod.description}
|
||||
</div>
|
||||
</div>
|
||||
@@ -550,7 +550,7 @@ export default function AdminDashboard() {
|
||||
borderRadius: 10,
|
||||
fontWeight: 500,
|
||||
background: mod.active ? "#F0FDF4" : "#F1F5F9",
|
||||
color: mod.active ? "#16A34A" : "#94A3B8",
|
||||
color: mod.active ? "#16A34A" : "#64748B",
|
||||
}}
|
||||
>
|
||||
{mod.active ? "运行中" : "未启用"}
|
||||
@@ -565,7 +565,7 @@ export default function AdminDashboard() {
|
||||
{/* 用户活跃度 */}
|
||||
<div
|
||||
style={{
|
||||
background: "#fff",
|
||||
background: "var(--erp-bg-container, #fff)",
|
||||
borderRadius: 12,
|
||||
border: "1px solid #E2E8F0",
|
||||
overflow: "hidden",
|
||||
@@ -627,7 +627,7 @@ export default function AdminDashboard() {
|
||||
width: 40,
|
||||
textAlign: "right",
|
||||
flexShrink: 0,
|
||||
color: item.color === "#94A3B8" ? "#475569" : item.color,
|
||||
color: item.color === "#64748B" ? "#475569" : item.color,
|
||||
}}
|
||||
>
|
||||
{item.value}
|
||||
@@ -642,13 +642,13 @@ export default function AdminDashboard() {
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: 11, color: "#94A3B8" }}>按角色分布</div>
|
||||
<div style={{ fontSize: 11, color: "#64748B" }}>按角色分布</div>
|
||||
<div style={{ display: "flex", gap: 10, fontSize: 11 }}>
|
||||
{userActivity?.by_role.map((r) => (
|
||||
<span key={r.role}>
|
||||
{r.role} {r.count}
|
||||
</span>
|
||||
)) ?? <span style={{ color: "#94A3B8" }}>加载中...</span>}
|
||||
)) ?? <span style={{ color: "#64748B" }}>加载中...</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -656,7 +656,7 @@ export default function AdminDashboard() {
|
||||
{/* 快捷管理入口 */}
|
||||
<div
|
||||
style={{
|
||||
background: "#fff",
|
||||
background: "var(--erp-bg-container, #fff)",
|
||||
borderRadius: 12,
|
||||
border: "1px solid #E2E8F0",
|
||||
overflow: "hidden",
|
||||
|
||||
@@ -32,19 +32,19 @@ export default function OperatorWorkbench() {
|
||||
useEffect(() => {
|
||||
actionInboxApi.stats()
|
||||
.then((s) => setStats(s ?? null))
|
||||
.catch(() => {});
|
||||
.catch((err) => console.warn('[OperatorWorkbench] 获取行动收件箱统计失败:', err));
|
||||
|
||||
actionInboxApi.list({ status: 'pending', page: 1, page_size: 5 })
|
||||
.then((r) => setActionItems(r.data))
|
||||
.catch(() => {});
|
||||
.catch((err) => console.warn('[OperatorWorkbench] 获取行动列表失败:', err));
|
||||
|
||||
dashboardApi.getPointsRecentActivity()
|
||||
.then((d) => setPointsActivity(d ?? []))
|
||||
.catch(() => {});
|
||||
.catch((err) => console.warn('[OperatorWorkbench] 获取积分活动失败:', err));
|
||||
|
||||
dashboardApi.getArticleStats()
|
||||
.then((d) => setArticleStats(d ?? null))
|
||||
.catch(() => {});
|
||||
.catch((err) => console.warn('[OperatorWorkbench] 获取文章统计失败:', err));
|
||||
}, []);
|
||||
|
||||
const firstName = user?.display_name ?? user?.username ?? '运营';
|
||||
|
||||
@@ -75,7 +75,7 @@ pub struct ArticleListParams {
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct CreateArticleReq {
|
||||
#[validate(length(min = 1, max = 500, message = "文章标题长度须在1-500之间"))]
|
||||
#[validate(length(min = 1, max = 200, message = "文章标题长度须在1-200之间"))]
|
||||
pub title: String,
|
||||
#[validate(length(max = 2000, message = "摘要最多2000字"))]
|
||||
pub summary: Option<String>,
|
||||
@@ -113,7 +113,7 @@ impl CreateArticleReq {
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct UpdateArticleReq {
|
||||
#[validate(length(min = 1, max = 500, message = "文章标题长度须在1-500之间"))]
|
||||
#[validate(length(min = 1, max = 200, message = "文章标题长度须在1-200之间"))]
|
||||
pub title: Option<String>,
|
||||
#[validate(length(max = 2000, message = "摘要最多2000字"))]
|
||||
pub summary: Option<String>,
|
||||
|
||||
@@ -37,10 +37,12 @@ pub struct MessageResp {
|
||||
}
|
||||
|
||||
/// 发送消息请求体 — 不含 sender_id/sender_role,由服务端从 JWT 注入。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct CreateMessageReq {
|
||||
pub session_id: Uuid,
|
||||
#[validate(length(max = 50, message = "内容类型最多 50 字"))]
|
||||
pub content_type: Option<String>,
|
||||
#[validate(length(min = 1, max = 5000, message = "消息内容长度须在 1-5000 之间"))]
|
||||
pub content: String,
|
||||
/// 关联的媒体文件 ID(当 content_type 为 image/file/voice 时必填)
|
||||
pub media_id: Option<Uuid>,
|
||||
@@ -52,10 +54,11 @@ impl CreateMessageReq {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct CreateSessionReq {
|
||||
pub patient_id: Uuid,
|
||||
pub doctor_id: Option<Uuid>,
|
||||
#[validate(length(max = 50, message = "咨询类型最多 50 字"))]
|
||||
pub consultation_type: Option<String>,
|
||||
}
|
||||
|
||||
@@ -70,11 +73,13 @@ pub struct SessionQuery {
|
||||
|
||||
/// 从咨询会话创建随访任务请求体 — 仅需填写随访类型和计划日期,
|
||||
/// patient_id / source_type / source_id 由服务端自动填充。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct CreateFollowUpFromConsultationReq {
|
||||
#[validate(length(min = 1, max = 50, message = "随访类型长度须在 1-50 之间"))]
|
||||
pub follow_up_type: String,
|
||||
pub planned_date: chrono::NaiveDate,
|
||||
pub assigned_to: Option<Uuid>,
|
||||
#[validate(length(max = 2000, message = "内容模板最多 2000 字"))]
|
||||
pub content_template: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ use erp_core::sanitize::sanitize_option;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use uuid::Uuid;
|
||||
use validator::Validate;
|
||||
|
||||
type Decimal = f64;
|
||||
|
||||
@@ -10,18 +11,27 @@ type Decimal = f64;
|
||||
// 日常监测
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct CreateDailyMonitoringReq {
|
||||
pub patient_id: Uuid,
|
||||
pub record_date: NaiveDate,
|
||||
#[validate(range(min = 40, max = 300, message = "收缩压范围 40-300 mmHg"))]
|
||||
pub morning_bp_systolic: Option<i32>,
|
||||
#[validate(range(min = 20, max = 200, message = "舒张压范围 20-200 mmHg"))]
|
||||
pub morning_bp_diastolic: Option<i32>,
|
||||
#[validate(range(min = 40, max = 300, message = "收缩压范围 40-300 mmHg"))]
|
||||
pub evening_bp_systolic: Option<i32>,
|
||||
#[validate(range(min = 20, max = 200, message = "舒张压范围 20-200 mmHg"))]
|
||||
pub evening_bp_diastolic: Option<i32>,
|
||||
#[validate(range(min = 0.5, max = 500.0, message = "体重范围 0.5-500 kg"))]
|
||||
pub weight: Option<Decimal>,
|
||||
#[validate(range(min = 0.5, max = 33.3, message = "血糖范围 0.5-33.3 mmol/L"))]
|
||||
pub blood_sugar: Option<Decimal>,
|
||||
#[validate(range(min = 0, max = 10000, message = "入液量范围 0-10000 ml"))]
|
||||
pub fluid_intake: Option<i32>,
|
||||
#[validate(range(min = 0, max = 10000, message = "尿量范围 0-10000 ml"))]
|
||||
pub urine_output: Option<i32>,
|
||||
#[validate(length(max = 2000, message = "备注最多 2000 字"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub notes: Option<String>,
|
||||
}
|
||||
@@ -32,17 +42,26 @@ impl CreateDailyMonitoringReq {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct UpdateDailyMonitoringReq {
|
||||
pub record_date: Option<NaiveDate>,
|
||||
#[validate(range(min = 40, max = 300, message = "收缩压范围 40-300 mmHg"))]
|
||||
pub morning_bp_systolic: Option<i32>,
|
||||
#[validate(range(min = 20, max = 200, message = "舒张压范围 20-200 mmHg"))]
|
||||
pub morning_bp_diastolic: Option<i32>,
|
||||
#[validate(range(min = 40, max = 300, message = "收缩压范围 40-300 mmHg"))]
|
||||
pub evening_bp_systolic: Option<i32>,
|
||||
#[validate(range(min = 20, max = 200, message = "舒张压范围 20-200 mmHg"))]
|
||||
pub evening_bp_diastolic: Option<i32>,
|
||||
#[validate(range(min = 0.5, max = 500.0, message = "体重范围 0.5-500 kg"))]
|
||||
pub weight: Option<Decimal>,
|
||||
#[validate(range(min = 0.5, max = 33.3, message = "血糖范围 0.5-33.3 mmol/L"))]
|
||||
pub blood_sugar: Option<Decimal>,
|
||||
#[validate(range(min = 0, max = 10000, message = "入液量范围 0-10000 ml"))]
|
||||
pub fluid_intake: Option<i32>,
|
||||
#[validate(range(min = 0, max = 10000, message = "尿量范围 0-10000 ml"))]
|
||||
pub urine_output: Option<i32>,
|
||||
#[validate(length(max = 2000, message = "备注最多 2000 字"))]
|
||||
pub notes: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ use erp_core::sanitize::sanitize_option;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use uuid::Uuid;
|
||||
use validator::Validate;
|
||||
|
||||
/// 用 f64 表示 Decimal 值以满足 utoipa ToSchema 要求。
|
||||
/// 对于健康数值(血压 60-200mmHg、血糖 3.9-11.1mmol/L、体重 30-300kg),
|
||||
@@ -10,23 +11,37 @@ use uuid::Uuid;
|
||||
/// 数据库层仍使用 SeaORM Decimal 类型,转换仅在 DTO 边界进行。
|
||||
type Decimal = f64;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct CreateVitalSignsReq {
|
||||
pub record_date: NaiveDate,
|
||||
#[validate(range(min = 40, max = 300, message = "收缩压范围 40-300 mmHg"))]
|
||||
pub systolic_bp_morning: Option<i32>,
|
||||
#[validate(range(min = 20, max = 200, message = "舒张压范围 20-200 mmHg"))]
|
||||
pub diastolic_bp_morning: Option<i32>,
|
||||
#[validate(range(min = 40, max = 300, message = "收缩压范围 40-300 mmHg"))]
|
||||
pub systolic_bp_evening: Option<i32>,
|
||||
#[validate(range(min = 20, max = 200, message = "舒张压范围 20-200 mmHg"))]
|
||||
pub diastolic_bp_evening: Option<i32>,
|
||||
#[validate(range(min = 20, max = 250, message = "心率范围 20-250 bpm"))]
|
||||
pub heart_rate: Option<i32>,
|
||||
#[validate(range(min = 0.5, max = 500.0, message = "体重范围 0.5-500 kg"))]
|
||||
pub weight: Option<Decimal>,
|
||||
#[validate(range(min = 0.5, max = 33.3, message = "血糖范围 0.5-33.3 mmol/L"))]
|
||||
pub blood_sugar: Option<Decimal>,
|
||||
#[validate(range(min = 30.0, max = 45.0, message = "体温范围 30-45 °C"))]
|
||||
pub body_temperature: Option<Decimal>,
|
||||
#[validate(range(min = 50, max = 100, message = "血氧范围 50-100%"))]
|
||||
pub spo2: Option<i32>,
|
||||
/// fasting / postprandial / random / ogtt
|
||||
#[validate(length(max = 50, message = "血糖类型最多 50 字"))]
|
||||
pub blood_sugar_type: Option<String>,
|
||||
#[validate(range(min = 0, max = 10000, message = "入液量范围 0-10000 ml"))]
|
||||
pub water_intake_ml: Option<i32>,
|
||||
#[validate(range(min = 0, max = 10000, message = "尿量范围 0-10000 ml"))]
|
||||
pub urine_output_ml: Option<i32>,
|
||||
#[validate(length(max = 2000, message = "备注最多 2000 字"))]
|
||||
pub notes: Option<String>,
|
||||
#[validate(length(max = 50, message = "来源最多 50 字"))]
|
||||
pub source: Option<String>,
|
||||
}
|
||||
|
||||
@@ -36,21 +51,34 @@ impl CreateVitalSignsReq {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct UpdateVitalSignsReq {
|
||||
pub record_date: Option<NaiveDate>,
|
||||
#[validate(range(min = 40, max = 300, message = "收缩压范围 40-300 mmHg"))]
|
||||
pub systolic_bp_morning: Option<i32>,
|
||||
#[validate(range(min = 20, max = 200, message = "舒张压范围 20-200 mmHg"))]
|
||||
pub diastolic_bp_morning: Option<i32>,
|
||||
#[validate(range(min = 40, max = 300, message = "收缩压范围 40-300 mmHg"))]
|
||||
pub systolic_bp_evening: Option<i32>,
|
||||
#[validate(range(min = 20, max = 200, message = "舒张压范围 20-200 mmHg"))]
|
||||
pub diastolic_bp_evening: Option<i32>,
|
||||
#[validate(range(min = 20, max = 250, message = "心率范围 20-250 bpm"))]
|
||||
pub heart_rate: Option<i32>,
|
||||
#[validate(range(min = 0.5, max = 500.0, message = "体重范围 0.5-500 kg"))]
|
||||
pub weight: Option<Decimal>,
|
||||
#[validate(range(min = 0.5, max = 33.3, message = "血糖范围 0.5-33.3 mmol/L"))]
|
||||
pub blood_sugar: Option<Decimal>,
|
||||
#[validate(range(min = 30.0, max = 45.0, message = "体温范围 30-45 °C"))]
|
||||
pub body_temperature: Option<Decimal>,
|
||||
#[validate(range(min = 50, max = 100, message = "血氧范围 50-100%"))]
|
||||
pub spo2: Option<i32>,
|
||||
#[validate(length(max = 50, message = "血糖类型最多 50 字"))]
|
||||
pub blood_sugar_type: Option<String>,
|
||||
#[validate(range(min = 0, max = 10000, message = "入液量范围 0-10000 ml"))]
|
||||
pub water_intake_ml: Option<i32>,
|
||||
#[validate(range(min = 0, max = 10000, message = "尿量范围 0-10000 ml"))]
|
||||
pub urine_output_ml: Option<i32>,
|
||||
#[validate(length(max = 2000, message = "备注最多 2000 字"))]
|
||||
pub notes: Option<String>,
|
||||
}
|
||||
|
||||
@@ -84,7 +112,7 @@ pub struct VitalSignsResp {
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct CreateLabReportReq {
|
||||
pub report_date: NaiveDate,
|
||||
pub report_type: String,
|
||||
@@ -102,7 +130,7 @@ impl CreateLabReportReq {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct UpdateLabReportReq {
|
||||
pub report_date: Option<NaiveDate>,
|
||||
pub report_type: Option<String>,
|
||||
@@ -136,7 +164,7 @@ pub struct LabReportResp {
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct CreateHealthRecordReq {
|
||||
pub record_type: Option<String>,
|
||||
pub record_date: NaiveDate,
|
||||
@@ -154,7 +182,7 @@ impl CreateHealthRecordReq {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct UpdateHealthRecordReq {
|
||||
pub record_type: Option<String>,
|
||||
pub record_date: Option<NaiveDate>,
|
||||
@@ -264,7 +292,7 @@ pub struct MiniTodayResp {
|
||||
pub weight: Option<IndicatorSummary>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Deserialize, ToSchema)]
|
||||
#[derive(Debug, Clone, serde::Deserialize, ToSchema, Validate)]
|
||||
pub struct ReviewLabReportReq {
|
||||
pub doctor_notes: Option<String>,
|
||||
pub items: Option<serde_json::Value>,
|
||||
|
||||
@@ -33,7 +33,7 @@ where
|
||||
{
|
||||
require_permission(&ctx, "health.alerts.list")?;
|
||||
let page = query.page.unwrap_or(1);
|
||||
let page_size = query.page_size.unwrap_or(20);
|
||||
let page_size = query.page_size.unwrap_or(20).min(100);
|
||||
|
||||
let (items, total) = alert_service::list_alerts(
|
||||
&state,
|
||||
|
||||
@@ -36,7 +36,7 @@ where
|
||||
{
|
||||
require_permission(&ctx, "health.alert-rules.list")?;
|
||||
let page = query.page.unwrap_or(1);
|
||||
let page_size = query.page_size.unwrap_or(20);
|
||||
let page_size = query.page_size.unwrap_or(20).min(100);
|
||||
|
||||
let (items, total) = alert_rule_service::list_rules(
|
||||
&state,
|
||||
|
||||
@@ -62,7 +62,7 @@ where
|
||||
{
|
||||
require_permission(&ctx, "health.appointment.list")?;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let page_size = params.page_size.unwrap_or(20).min(100);
|
||||
let result = appointment_service::list_appointments(
|
||||
&state,
|
||||
ctx.tenant_id,
|
||||
@@ -148,7 +148,7 @@ where
|
||||
{
|
||||
require_permission(&ctx, "health.appointment.list")?;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let page_size = params.page_size.unwrap_or(20).min(100);
|
||||
let result = appointment_service::list_schedules(
|
||||
&state,
|
||||
ctx.tenant_id,
|
||||
|
||||
@@ -25,7 +25,7 @@ where
|
||||
{
|
||||
require_permission(&ctx, "health.articles.list")?;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let page_size = params.page_size.unwrap_or(20).min(100);
|
||||
// 非管理权限用户只能查看已发布文章,防止草稿泄露
|
||||
let status =
|
||||
if require_any_permission(&ctx, &["health.articles.manage", "health.articles.review"])
|
||||
@@ -58,7 +58,7 @@ pub async fn list_public_articles(
|
||||
.tenant_id
|
||||
.ok_or_else(|| AppError::Validation("tenant_id is required".into()))?;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let page_size = params.page_size.unwrap_or(20).min(100);
|
||||
let result = article_service::list_articles(
|
||||
&state,
|
||||
tenant_id,
|
||||
@@ -307,7 +307,7 @@ where
|
||||
{
|
||||
require_permission(&ctx, "health.articles.list")?;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let page_size = params.page_size.unwrap_or(20).min(100);
|
||||
let result =
|
||||
article_service::list_revisions(&state, ctx.tenant_id, id, page, page_size).await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
|
||||
@@ -138,7 +138,7 @@ where
|
||||
{
|
||||
require_permission(&ctx, "health.ble-gateways.list")?;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let page_size = params.page_size.unwrap_or(20).min(100);
|
||||
let result =
|
||||
ble_gateway_service::list_bindings(&state, ctx.tenant_id, gateway_id, page, page_size)
|
||||
.await?;
|
||||
|
||||
@@ -118,7 +118,7 @@ where
|
||||
{
|
||||
require_permission(&ctx, "health.care-plan.list")?;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let page_size = params.page_size.unwrap_or(20).min(100);
|
||||
let result =
|
||||
care_plan_service::list_care_plan_items(&state, ctx.tenant_id, plan_id, page, page_size)
|
||||
.await?;
|
||||
@@ -211,7 +211,7 @@ where
|
||||
{
|
||||
require_permission(&ctx, "health.care-plan.list")?;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let page_size = params.page_size.unwrap_or(20).min(100);
|
||||
let result =
|
||||
care_plan_service::list_care_plan_outcomes(&state, ctx.tenant_id, plan_id, page, page_size)
|
||||
.await?;
|
||||
|
||||
@@ -35,7 +35,7 @@ where
|
||||
{
|
||||
require_permission(&ctx, "health.consent.list")?;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let page_size = params.page_size.unwrap_or(20).min(100);
|
||||
let result =
|
||||
consent_service::list_consents(&state, ctx.tenant_id, patient_id, page, page_size).await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
|
||||
@@ -68,6 +68,8 @@ where
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.consultation.manage")?;
|
||||
req.validate()
|
||||
.map_err(|e| AppError::Validation(e.to_string()))?;
|
||||
let result =
|
||||
consultation_service::create_session(&state, ctx.tenant_id, Some(ctx.user_id), req).await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
@@ -84,7 +86,7 @@ where
|
||||
{
|
||||
require_permission(&ctx, "health.consultation.list")?;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let page_size = params.page_size.unwrap_or(20).min(100);
|
||||
let result = consultation_service::list_sessions(
|
||||
&state,
|
||||
ctx.tenant_id,
|
||||
@@ -124,7 +126,7 @@ where
|
||||
{
|
||||
require_permission(&ctx, "health.consultation.list")?;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let page_size = params.page_size.unwrap_or(20).min(100);
|
||||
let result = consultation_service::list_messages(
|
||||
&state,
|
||||
ctx.tenant_id,
|
||||
@@ -209,6 +211,9 @@ where
|
||||
content: req.content,
|
||||
media_id: None,
|
||||
};
|
||||
msg_req
|
||||
.validate()
|
||||
.map_err(|e| AppError::Validation(e.to_string()))?;
|
||||
msg_req.sanitize();
|
||||
let result = consultation_service::create_message(
|
||||
&state,
|
||||
|
||||
@@ -29,7 +29,7 @@ where
|
||||
{
|
||||
require_permission(&ctx, "health.critical-alerts.list")?;
|
||||
let page = query.page.unwrap_or(1);
|
||||
let page_size = query.page_size.unwrap_or(20);
|
||||
let page_size = query.page_size.unwrap_or(20).min(100);
|
||||
|
||||
let (items, total) =
|
||||
critical_alert_service::list_pending_alerts(&state, ctx.tenant_id, page, page_size)
|
||||
|
||||
@@ -3,6 +3,7 @@ use axum::extract::{FromRef, Json, Path, Query, State};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
use uuid::Uuid;
|
||||
use validator::Validate;
|
||||
|
||||
use erp_core::error::AppError;
|
||||
use erp_core::rbac::require_permission;
|
||||
@@ -38,7 +39,7 @@ where
|
||||
{
|
||||
require_permission(&ctx, "health.daily-monitoring.list")?;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let page_size = params.page_size.unwrap_or(20).min(100);
|
||||
let result = daily_monitoring_service::list_daily_monitoring(
|
||||
&state,
|
||||
ctx.tenant_id,
|
||||
@@ -76,6 +77,8 @@ where
|
||||
{
|
||||
require_permission(&ctx, "health.daily-monitoring.manage")?;
|
||||
let mut req = req;
|
||||
req.validate()
|
||||
.map_err(|e| AppError::Validation(e.to_string()))?;
|
||||
req.sanitize();
|
||||
let result = daily_monitoring_service::create_daily_monitoring(
|
||||
&state,
|
||||
@@ -99,6 +102,8 @@ where
|
||||
{
|
||||
require_permission(&ctx, "health.daily-monitoring.manage")?;
|
||||
let mut data = req.data;
|
||||
data.validate()
|
||||
.map_err(|e| AppError::Validation(e.to_string()))?;
|
||||
data.sanitize();
|
||||
let result = daily_monitoring_service::update_daily_monitoring(
|
||||
&state,
|
||||
|
||||
@@ -48,7 +48,7 @@ where
|
||||
{
|
||||
require_permission(&ctx, "health.devices.list")?;
|
||||
let page = query.page.unwrap_or(1);
|
||||
let page_size = query.page_size.unwrap_or(20);
|
||||
let page_size = query.page_size.unwrap_or(20).min(100);
|
||||
|
||||
let (items, total) = device_service::list_devices(
|
||||
&state,
|
||||
|
||||
@@ -76,7 +76,7 @@ where
|
||||
{
|
||||
require_permission(&ctx, "health.device-readings.list")?;
|
||||
let page = query.page.unwrap_or(1);
|
||||
let page_size = query.page_size.unwrap_or(20);
|
||||
let page_size = query.page_size.unwrap_or(20).min(100);
|
||||
let result = device_reading_service::query_device_readings(
|
||||
&state,
|
||||
ctx.tenant_id,
|
||||
@@ -109,7 +109,7 @@ where
|
||||
{
|
||||
require_permission(&ctx, "health.device-readings.list")?;
|
||||
let page = query.page.unwrap_or(1);
|
||||
let page_size = query.page_size.unwrap_or(20);
|
||||
let page_size = query.page_size.unwrap_or(20).min(100);
|
||||
let days = query.days.unwrap_or(7);
|
||||
let result = device_reading_service::query_hourly_readings(
|
||||
&state,
|
||||
|
||||
@@ -35,7 +35,7 @@ where
|
||||
{
|
||||
require_permission(&ctx, "health.health-data.list")?;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let page_size = params.page_size.unwrap_or(20).min(100);
|
||||
let result =
|
||||
diagnosis_service::list_diagnoses(&state, ctx.tenant_id, patient_id, page, page_size)
|
||||
.await?;
|
||||
|
||||
@@ -40,7 +40,7 @@ where
|
||||
{
|
||||
require_permission(&ctx, "health.doctor.list")?;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let page_size = params.page_size.unwrap_or(20).min(100);
|
||||
let result = doctor_service::list_doctors(
|
||||
&state,
|
||||
ctx.tenant_id,
|
||||
|
||||
@@ -119,7 +119,7 @@ where
|
||||
{
|
||||
require_permission(&ctx, "health.follow-up.list")?;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let page_size = params.page_size.unwrap_or(20).min(100);
|
||||
let result = follow_up_service::list_tasks(
|
||||
&state,
|
||||
ctx.tenant_id,
|
||||
@@ -239,7 +239,7 @@ where
|
||||
{
|
||||
require_permission(&ctx, "health.follow-up.list")?;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let page_size = params.page_size.unwrap_or(20).min(100);
|
||||
let result = follow_up_service::list_records(
|
||||
&state,
|
||||
ctx.tenant_id,
|
||||
|
||||
@@ -39,7 +39,7 @@ where
|
||||
{
|
||||
require_permission(&ctx, "health.follow-up-templates.list")?;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let page_size = params.page_size.unwrap_or(20).min(100);
|
||||
let result = follow_up_template_service::list_templates(
|
||||
&state,
|
||||
ctx.tenant_id,
|
||||
|
||||
@@ -8,6 +8,8 @@ use erp_core::error::AppError;
|
||||
use erp_core::rbac::require_permission;
|
||||
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
||||
|
||||
use validator::Validate;
|
||||
|
||||
use crate::dto::DeleteWithVersion;
|
||||
use crate::dto::health_data_dto::*;
|
||||
use crate::service::health_data_service;
|
||||
@@ -58,7 +60,7 @@ where
|
||||
{
|
||||
require_permission(&ctx, "health.health-data.list")?;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let page_size = params.page_size.unwrap_or(20).min(100);
|
||||
let result =
|
||||
health_data_service::list_vital_signs(&state, ctx.tenant_id, patient_id, page, page_size)
|
||||
.await?;
|
||||
@@ -77,6 +79,8 @@ where
|
||||
{
|
||||
require_permission(&ctx, "health.health-data.manage")?;
|
||||
let mut req = req;
|
||||
req.validate()
|
||||
.map_err(|e| AppError::Validation(e.to_string()))?;
|
||||
req.sanitize();
|
||||
let result = health_data_service::create_vital_signs(
|
||||
&state,
|
||||
@@ -101,6 +105,8 @@ where
|
||||
{
|
||||
require_permission(&ctx, "health.health-data.manage")?;
|
||||
let mut data = req.data;
|
||||
data.validate()
|
||||
.map_err(|e| AppError::Validation(e.to_string()))?;
|
||||
data.sanitize();
|
||||
let result = health_data_service::update_vital_signs(
|
||||
&state,
|
||||
@@ -153,7 +159,7 @@ where
|
||||
{
|
||||
require_permission(&ctx, "health.health-data.list")?;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let page_size = params.page_size.unwrap_or(20).min(100);
|
||||
let result =
|
||||
health_data_service::list_lab_reports(&state, ctx.tenant_id, patient_id, page, page_size)
|
||||
.await?;
|
||||
@@ -172,6 +178,8 @@ where
|
||||
{
|
||||
require_permission(&ctx, "health.health-data.manage")?;
|
||||
let mut req = req;
|
||||
req.validate()
|
||||
.map_err(|e| AppError::Validation(e.to_string()))?;
|
||||
req.sanitize();
|
||||
let result = health_data_service::create_lab_report(
|
||||
&state,
|
||||
@@ -196,6 +204,8 @@ where
|
||||
{
|
||||
require_permission(&ctx, "health.health-data.manage")?;
|
||||
let mut data = req.data;
|
||||
data.validate()
|
||||
.map_err(|e| AppError::Validation(e.to_string()))?;
|
||||
data.sanitize();
|
||||
let result = health_data_service::update_lab_report(
|
||||
&state,
|
||||
@@ -244,6 +254,8 @@ where
|
||||
{
|
||||
require_permission(&ctx, "health.health-data.manage")?;
|
||||
let mut data = req.data;
|
||||
data.validate()
|
||||
.map_err(|e| AppError::Validation(e.to_string()))?;
|
||||
data.sanitize();
|
||||
let result = health_data_service::review_lab_report(
|
||||
&state,
|
||||
@@ -274,7 +286,7 @@ where
|
||||
{
|
||||
require_permission(&ctx, "health.health-data.list")?;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let page_size = params.page_size.unwrap_or(20).min(100);
|
||||
let result = health_data_service::list_health_records(
|
||||
&state,
|
||||
ctx.tenant_id,
|
||||
@@ -298,6 +310,8 @@ where
|
||||
{
|
||||
require_permission(&ctx, "health.health-data.manage")?;
|
||||
let mut req = req;
|
||||
req.validate()
|
||||
.map_err(|e| AppError::Validation(e.to_string()))?;
|
||||
req.sanitize();
|
||||
let result = health_data_service::create_health_record(
|
||||
&state,
|
||||
@@ -322,6 +336,8 @@ where
|
||||
{
|
||||
require_permission(&ctx, "health.health-data.manage")?;
|
||||
let mut data = req.data;
|
||||
data.validate()
|
||||
.map_err(|e| AppError::Validation(e.to_string()))?;
|
||||
data.sanitize();
|
||||
let result = health_data_service::update_health_record(
|
||||
&state,
|
||||
@@ -374,7 +390,7 @@ where
|
||||
{
|
||||
require_permission(&ctx, "health.health-data.list")?;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let page_size = params.page_size.unwrap_or(20).min(100);
|
||||
let result =
|
||||
trend_service::list_trends(&state, ctx.tenant_id, patient_id, page, page_size).await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
|
||||
@@ -29,7 +29,7 @@ where
|
||||
{
|
||||
require_permission(&ctx, "health.medication-records.list")?;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let page_size = params.page_size.unwrap_or(20).min(100);
|
||||
let result = medication_record_service::list_medications(
|
||||
&state,
|
||||
ctx.tenant_id,
|
||||
|
||||
@@ -28,7 +28,7 @@ where
|
||||
{
|
||||
require_permission(&ctx, "health.medication-reminders.list")?;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let page_size = params.page_size.unwrap_or(20).min(100);
|
||||
let result = medication_reminder_service::list_reminders(
|
||||
&state,
|
||||
ctx.tenant_id,
|
||||
|
||||
@@ -44,7 +44,7 @@ where
|
||||
{
|
||||
require_permission(&ctx, "health.patient.list")?;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let page_size = params.page_size.unwrap_or(20).min(100);
|
||||
let result = patient_service::list_patients(
|
||||
&state,
|
||||
ctx.tenant_id,
|
||||
|
||||
@@ -91,7 +91,7 @@ where
|
||||
require_permission(&ctx, "health.points.list")?;
|
||||
let patient_id = resolve_patient_id(&state, ctx.tenant_id, ctx.user_id).await?;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let page_size = params.page_size.unwrap_or(20).min(100);
|
||||
let result =
|
||||
points_service::list_transactions(&state, ctx.tenant_id, patient_id, page, page_size)
|
||||
.await?;
|
||||
@@ -110,7 +110,7 @@ where
|
||||
{
|
||||
require_permission(&ctx, "health.points.list")?;
|
||||
let p = page.page.unwrap_or(1);
|
||||
let ps = page.page_size.unwrap_or(20);
|
||||
let ps = page.page_size.unwrap_or(20).min(100);
|
||||
let result =
|
||||
points_service::list_products(&state, ctx.tenant_id, params.product_type, p, ps).await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
@@ -159,7 +159,7 @@ where
|
||||
require_permission(&ctx, "health.points.list")?;
|
||||
let patient_id = resolve_patient_id(&state, ctx.tenant_id, ctx.user_id).await?;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let page_size = params.page_size.unwrap_or(20).min(100);
|
||||
let result =
|
||||
points_service::list_orders(&state, ctx.tenant_id, patient_id, page, page_size).await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
@@ -182,7 +182,7 @@ where
|
||||
// 患者端端点:验证当前用户有关联的患者档案
|
||||
let _patient_id = resolve_patient_id(&state, ctx.tenant_id, ctx.user_id).await?;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let page_size = params.page_size.unwrap_or(20).min(100);
|
||||
let result =
|
||||
points_service::list_offline_events(&state, ctx.tenant_id, page, page_size).await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
@@ -318,7 +318,7 @@ where
|
||||
{
|
||||
require_permission(&ctx, "health.points.list")?;
|
||||
let p = page.page.unwrap_or(1);
|
||||
let ps = page.page_size.unwrap_or(20);
|
||||
let ps = page.page_size.unwrap_or(20).min(100);
|
||||
let result = points_service::admin_list_products(
|
||||
&state,
|
||||
ctx.tenant_id,
|
||||
@@ -406,7 +406,7 @@ where
|
||||
{
|
||||
require_permission(&ctx, "health.points.list")?;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let page_size = params.page_size.unwrap_or(20).min(100);
|
||||
// 管理端查看所有订单 — 不按 patient_id 过滤
|
||||
let result = points_service::admin_list_orders(&state, ctx.tenant_id, page, page_size).await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
@@ -498,7 +498,7 @@ where
|
||||
{
|
||||
require_permission(&ctx, "health.points.list")?;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let page_size = params.page_size.unwrap_or(20).min(100);
|
||||
let result = points_service::admin_list_offline_events(
|
||||
&state,
|
||||
ctx.tenant_id,
|
||||
@@ -579,7 +579,7 @@ where
|
||||
{
|
||||
require_permission(&ctx, "health.points.list")?;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let page_size = params.page_size.unwrap_or(20).min(100);
|
||||
let result =
|
||||
points_service::list_transactions(&state, ctx.tenant_id, patient_id, page, page_size)
|
||||
.await?;
|
||||
|
||||
@@ -118,7 +118,7 @@ where
|
||||
{
|
||||
require_permission(&ctx, "health.shifts.list")?;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let page_size = params.page_size.unwrap_or(20).min(100);
|
||||
let result =
|
||||
shift_service::list_assignments(&state, ctx.tenant_id, shift_id, page, page_size).await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
|
||||
Reference in New Issue
Block a user