333 lines
8.5 KiB
TypeScript
333 lines
8.5 KiB
TypeScript
export const VITAL_SOURCE_VALUES = ["manual", "device", "import"] as const;
|
|
export type VitalSource = (typeof VITAL_SOURCE_VALUES)[number];
|
|
|
|
export const CHRONIC_CONDITION_STATUS_VALUES = ["active", "controlled", "resolved"] as const;
|
|
export type ChronicConditionStatus = (typeof CHRONIC_CONDITION_STATUS_VALUES)[number];
|
|
|
|
export const HEALTH_REVIEW_STATUS_VALUES = ["pending", "reviewed", "resolved"] as const;
|
|
export type HealthReviewStatus = (typeof HEALTH_REVIEW_STATUS_VALUES)[number];
|
|
|
|
export const HEALTH_REVIEW_SEVERITY_VALUES = ["info", "warning", "critical"] as const;
|
|
export type HealthReviewSeverity = (typeof HEALTH_REVIEW_SEVERITY_VALUES)[number];
|
|
|
|
export const VITAL_SOURCE_LABELS: Record<VitalSource, string> = {
|
|
manual: "手工录入",
|
|
device: "设备采集",
|
|
import: "批量导入",
|
|
};
|
|
|
|
export const CHRONIC_CONDITION_STATUS_LABELS: Record<ChronicConditionStatus, string> = {
|
|
active: "持续管理",
|
|
controlled: "控制稳定",
|
|
resolved: "已结束",
|
|
};
|
|
|
|
export const HEALTH_REVIEW_STATUS_LABELS: Record<HealthReviewStatus, string> = {
|
|
pending: "待复核",
|
|
reviewed: "已复核",
|
|
resolved: "已处理",
|
|
};
|
|
|
|
export const HEALTH_REVIEW_SEVERITY_LABELS: Record<HealthReviewSeverity, string> = {
|
|
info: "提示",
|
|
warning: "预警",
|
|
critical: "紧急",
|
|
};
|
|
|
|
export type HealthElderOption = {
|
|
id: string;
|
|
name: string;
|
|
careLevel: string;
|
|
status: string;
|
|
};
|
|
|
|
export type HealthProfile = {
|
|
id: string;
|
|
organizationId: string;
|
|
elderId: string;
|
|
elderName: string;
|
|
allergyNotes: string;
|
|
medicalHistory: string;
|
|
medicationNotes: string;
|
|
careRestrictions: string;
|
|
emergencyNotes: string;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
};
|
|
|
|
export type VitalRecord = {
|
|
id: string;
|
|
organizationId: string;
|
|
elderId: string;
|
|
elderName: string;
|
|
recordedAt: string;
|
|
source: VitalSource;
|
|
systolicBp?: number;
|
|
diastolicBp?: number;
|
|
heartRate?: number;
|
|
temperatureTenths?: number;
|
|
spo2?: number;
|
|
bloodGlucoseTenths?: number;
|
|
weightTenths?: number;
|
|
notes: string;
|
|
createdByAccountId?: string;
|
|
createdByName?: string;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
};
|
|
|
|
export type ChronicCondition = {
|
|
id: string;
|
|
organizationId: string;
|
|
elderId: string;
|
|
elderName: string;
|
|
name: string;
|
|
status: ChronicConditionStatus;
|
|
diagnosedAt?: string;
|
|
treatmentNotes: string;
|
|
followUpNotes: string;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
};
|
|
|
|
export type HealthAnomalyReview = {
|
|
id: string;
|
|
organizationId: string;
|
|
elderId: string;
|
|
elderName: string;
|
|
vitalRecordId?: string;
|
|
severity: HealthReviewSeverity;
|
|
status: HealthReviewStatus;
|
|
title: string;
|
|
description: string;
|
|
reviewedByAccountId?: string;
|
|
reviewedByName?: string;
|
|
reviewedAt?: string;
|
|
resolutionNotes: string;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
};
|
|
|
|
export type HealthAdminMetrics = {
|
|
eldersWithProfiles: number;
|
|
vitalsToday: number;
|
|
activeChronicConditions: number;
|
|
pendingReviews: number;
|
|
};
|
|
|
|
export type HealthAdminData = {
|
|
elders: HealthElderOption[];
|
|
profiles: HealthProfile[];
|
|
vitals: VitalRecord[];
|
|
chronicConditions: ChronicCondition[];
|
|
reviews: HealthAnomalyReview[];
|
|
metrics: HealthAdminMetrics;
|
|
};
|
|
|
|
export type HealthProfileInput = {
|
|
allergyNotes: string;
|
|
medicalHistory: string;
|
|
medicationNotes: string;
|
|
careRestrictions: string;
|
|
emergencyNotes: string;
|
|
};
|
|
|
|
export type VitalRecordInput = {
|
|
elderId: string;
|
|
recordedAt: Date;
|
|
source: VitalSource;
|
|
systolicBp?: number;
|
|
diastolicBp?: number;
|
|
heartRate?: number;
|
|
temperatureTenths?: number;
|
|
spo2?: number;
|
|
bloodGlucoseTenths?: number;
|
|
weightTenths?: number;
|
|
notes: string;
|
|
};
|
|
|
|
export type ChronicConditionInput = {
|
|
elderId: string;
|
|
name: string;
|
|
status: ChronicConditionStatus;
|
|
diagnosedAt?: Date;
|
|
treatmentNotes: string;
|
|
followUpNotes: string;
|
|
};
|
|
|
|
export type HealthReviewUpdateInput = {
|
|
status: HealthReviewStatus;
|
|
resolutionNotes: string;
|
|
};
|
|
|
|
type ValidationSuccess<T> = {
|
|
success: true;
|
|
data: T;
|
|
};
|
|
|
|
type ValidationFailure = {
|
|
success: false;
|
|
reason: string;
|
|
};
|
|
|
|
export type ValidationResult<T> = ValidationSuccess<T> | ValidationFailure;
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
}
|
|
|
|
function readString(source: Record<string, unknown>, key: string): string {
|
|
const value = source[key];
|
|
return typeof value === "string" ? value.trim() : "";
|
|
}
|
|
|
|
function readOptionalInteger(source: Record<string, unknown>, key: string): number | undefined | null {
|
|
const value = source[key];
|
|
if (value === undefined || value === null || value === "") {
|
|
return undefined;
|
|
}
|
|
|
|
const numeric = typeof value === "number" ? value : Number(value);
|
|
return Number.isInteger(numeric) ? numeric : null;
|
|
}
|
|
|
|
function readEnum<T extends string>(value: unknown, values: readonly T[]): T | null {
|
|
return typeof value === "string" ? values.find((item) => item === value) ?? null : null;
|
|
}
|
|
|
|
function readOptionalDate(source: Record<string, unknown>, key: string): Date | undefined | null {
|
|
const value = source[key];
|
|
if (value === undefined || value === null || value === "") {
|
|
return undefined;
|
|
}
|
|
|
|
if (typeof value !== "string") {
|
|
return null;
|
|
}
|
|
|
|
const date = new Date(value);
|
|
return Number.isNaN(date.getTime()) ? null : date;
|
|
}
|
|
|
|
export function validateHealthProfileInput(value: unknown): ValidationResult<HealthProfileInput> {
|
|
if (!isRecord(value)) {
|
|
return { success: false, reason: "请求数据格式无效" };
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
data: {
|
|
allergyNotes: readString(value, "allergyNotes"),
|
|
medicalHistory: readString(value, "medicalHistory"),
|
|
medicationNotes: readString(value, "medicationNotes"),
|
|
careRestrictions: readString(value, "careRestrictions"),
|
|
emergencyNotes: readString(value, "emergencyNotes"),
|
|
},
|
|
};
|
|
}
|
|
|
|
export function validateVitalRecordInput(value: unknown): ValidationResult<VitalRecordInput> {
|
|
if (!isRecord(value)) {
|
|
return { success: false, reason: "请求数据格式无效" };
|
|
}
|
|
|
|
const elderId = readString(value, "elderId");
|
|
if (!elderId) {
|
|
return { success: false, reason: "老人不能为空" };
|
|
}
|
|
|
|
const recordedAt = readOptionalDate(value, "recordedAt");
|
|
if (!recordedAt) {
|
|
return { success: false, reason: "记录时间无效" };
|
|
}
|
|
|
|
const source = readEnum(value.source, VITAL_SOURCE_VALUES);
|
|
if (!source) {
|
|
return { success: false, reason: "记录来源无效" };
|
|
}
|
|
|
|
const integerKeys = [
|
|
"systolicBp",
|
|
"diastolicBp",
|
|
"heartRate",
|
|
"temperatureTenths",
|
|
"spo2",
|
|
"bloodGlucoseTenths",
|
|
"weightTenths",
|
|
] as const;
|
|
const parsed: Partial<Record<(typeof integerKeys)[number], number>> = {};
|
|
for (const key of integerKeys) {
|
|
const numberValue = readOptionalInteger(value, key);
|
|
if (numberValue === null) {
|
|
return { success: false, reason: "生命体征数值需为整数" };
|
|
}
|
|
if (numberValue !== undefined) {
|
|
parsed[key] = numberValue;
|
|
}
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
data: {
|
|
elderId,
|
|
recordedAt,
|
|
source,
|
|
notes: readString(value, "notes"),
|
|
...parsed,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function validateChronicConditionInput(value: unknown): ValidationResult<ChronicConditionInput> {
|
|
if (!isRecord(value)) {
|
|
return { success: false, reason: "请求数据格式无效" };
|
|
}
|
|
|
|
const elderId = readString(value, "elderId");
|
|
const name = readString(value, "name");
|
|
if (!elderId || !name) {
|
|
return { success: false, reason: "老人和慢病名称不能为空" };
|
|
}
|
|
|
|
const status = readEnum(value.status, CHRONIC_CONDITION_STATUS_VALUES);
|
|
if (!status) {
|
|
return { success: false, reason: "慢病状态无效" };
|
|
}
|
|
|
|
const diagnosedAt = readOptionalDate(value, "diagnosedAt");
|
|
if (diagnosedAt === null) {
|
|
return { success: false, reason: "诊断日期无效" };
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
data: {
|
|
elderId,
|
|
name,
|
|
status,
|
|
diagnosedAt,
|
|
treatmentNotes: readString(value, "treatmentNotes"),
|
|
followUpNotes: readString(value, "followUpNotes"),
|
|
},
|
|
};
|
|
}
|
|
|
|
export function validateHealthReviewUpdateInput(value: unknown): ValidationResult<HealthReviewUpdateInput> {
|
|
if (!isRecord(value)) {
|
|
return { success: false, reason: "请求数据格式无效" };
|
|
}
|
|
|
|
const status = readEnum(value.status, HEALTH_REVIEW_STATUS_VALUES);
|
|
if (!status) {
|
|
return { success: false, reason: "复核状态无效" };
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
data: {
|
|
status,
|
|
resolutionNotes: readString(value, "resolutionNotes"),
|
|
},
|
|
};
|
|
}
|