feat: add health data management workspace
This commit is contained in:
332
modules/health/types.ts
Normal file
332
modules/health/types.ts
Normal file
@@ -0,0 +1,332 @@
|
||||
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"),
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user