Initial commit: HealthManager full-stack health management platform
Backend: .NET 10 + PostgreSQL + EF Core + JWT + SignalR Frontend patient: React 19 + TypeScript + Vite (mobile H5) Frontend doctor: React 19 + TypeScript + Vite (desktop web)
This commit is contained in:
93
frontend-patient/src/services/api-client.ts
Normal file
93
frontend-patient/src/services/api-client.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Real HTTP API client — replaces mockApiResponse with actual fetch calls.
|
||||
* Backend base: http://localhost:5000
|
||||
*/
|
||||
|
||||
interface ApiResponse<T> {
|
||||
code: number;
|
||||
data: T;
|
||||
message: string;
|
||||
}
|
||||
|
||||
const BASE_URL = 'http://localhost:5000';
|
||||
|
||||
// Endpoints that should NEVER include auth token
|
||||
const PUBLIC_ENDPOINTS = ['/api/auth/login', '/api/auth/register', '/api/auth/send-sms', '/api/auth/refresh'];
|
||||
|
||||
function getToken(): string | null {
|
||||
try {
|
||||
const raw = localStorage.getItem('hrt_auth');
|
||||
if (!raw) return null;
|
||||
const state = JSON.parse(raw);
|
||||
return state?.state?.token ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function clearAuth() {
|
||||
localStorage.removeItem('hrt_auth');
|
||||
}
|
||||
|
||||
async function request<T>(
|
||||
method: string,
|
||||
path: string,
|
||||
body?: unknown,
|
||||
): Promise<ApiResponse<T>> {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
// Only attach token for non-public endpoints
|
||||
if (!PUBLIC_ENDPOINTS.includes(path)) {
|
||||
const token = getToken();
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(`${BASE_URL}${path}`, {
|
||||
method,
|
||||
headers,
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
|
||||
// Handle 401 — clear stored token and redirect to login
|
||||
if (response.status === 401) {
|
||||
clearAuth();
|
||||
// Only redirect if not already on login page
|
||||
if (!window.location.pathname.includes('/login')) {
|
||||
window.location.href = '/login';
|
||||
}
|
||||
throw new Error('登录已过期,请重新登录');
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
|
||||
let data: T;
|
||||
try {
|
||||
const json = JSON.parse(text);
|
||||
if (json.code !== undefined && json.data !== undefined) {
|
||||
return json as ApiResponse<T>;
|
||||
}
|
||||
data = json as T;
|
||||
} catch {
|
||||
data = text as unknown as T;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const msg = (data as Record<string, unknown>)?.message || response.statusText;
|
||||
throw new Error(String(msg));
|
||||
}
|
||||
|
||||
return { code: response.status, data, message: 'success' };
|
||||
}
|
||||
|
||||
export const api = {
|
||||
get: <T>(path: string) => request<T>('GET', path),
|
||||
post: <T>(path: string, body?: unknown) => request<T>('POST', path, body),
|
||||
put: <T>(path: string, body?: unknown) => request<T>('PUT', path, body),
|
||||
del: <T>(path: string) => request<T>('DELETE', path),
|
||||
};
|
||||
|
||||
export type { ApiResponse };
|
||||
1
frontend-patient/src/services/api.ts
Normal file
1
frontend-patient/src/services/api.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { api as apiRequest, type ApiResponse } from './api-client';
|
||||
90
frontend-patient/src/services/auth.service.ts
Normal file
90
frontend-patient/src/services/auth.service.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { api, type ApiResponse } from './api-client';
|
||||
import type { User } from '@/types';
|
||||
|
||||
interface AuthResponseData {
|
||||
userId: string;
|
||||
name: string;
|
||||
role: string;
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
}
|
||||
|
||||
function mapUser(data: AuthResponseData): User {
|
||||
return {
|
||||
id: data.userId,
|
||||
phone: '',
|
||||
nickname: data.name,
|
||||
avatar: '',
|
||||
gender: 'unknown',
|
||||
birthday: '',
|
||||
height: 0,
|
||||
weight: 0,
|
||||
medicalHistory: [],
|
||||
stentImplantDate: '',
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
export async function login(phone: string, smsCode: string): Promise<{ token: string; user: User }> {
|
||||
const res: ApiResponse<AuthResponseData> = await api.post('/api/auth/login', {
|
||||
phone,
|
||||
smsCode,
|
||||
});
|
||||
return { token: res.data.accessToken, user: mapUser(res.data) };
|
||||
}
|
||||
|
||||
export async function register(phone: string, smsCode: string, nickname: string): Promise<{ token: string; user: User }> {
|
||||
const res: ApiResponse<AuthResponseData> = await api.post('/api/auth/register', {
|
||||
phone,
|
||||
smsCode,
|
||||
name: nickname,
|
||||
});
|
||||
return { token: res.data.accessToken, user: mapUser(res.data) };
|
||||
}
|
||||
|
||||
export async function sendSmsCode(phone: string): Promise<boolean> {
|
||||
await api.post('/api/auth/send-sms', { phone });
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function getProfile(): Promise<User> {
|
||||
const res = await api.get<{
|
||||
id: string; name: string; phone: string; role: string;
|
||||
gender: string; birthday: string; heightCm: number; weightKg: number;
|
||||
medicalHistory: string[]; stentDate: string; stentType: string;
|
||||
department: string; title: string; specialty: string[]; introduction: string;
|
||||
}>('/api/auth/me');
|
||||
|
||||
// Update stored user info
|
||||
try {
|
||||
const raw = localStorage.getItem('hrt_auth');
|
||||
if (raw) {
|
||||
const state = JSON.parse(raw);
|
||||
if (state?.state) {
|
||||
state.state.user = {
|
||||
...state.state.user,
|
||||
id: res.data.id,
|
||||
phone: res.data.phone,
|
||||
nickname: res.data.name,
|
||||
gender: res.data.gender || 'unknown',
|
||||
birthday: res.data.birthday || '',
|
||||
height: res.data.heightCm || 0,
|
||||
weight: res.data.weightKg || 0,
|
||||
medicalHistory: res.data.medicalHistory || [],
|
||||
stentImplantDate: res.data.stentDate || '',
|
||||
};
|
||||
localStorage.setItem('hrt_auth', JSON.stringify(state));
|
||||
}
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
|
||||
return res.data as unknown as User;
|
||||
}
|
||||
|
||||
export async function updateProfile(data: Record<string, unknown>): Promise<void> {
|
||||
await api.put('/api/auth/me', data);
|
||||
}
|
||||
|
||||
export async function logout(): Promise<void> {
|
||||
localStorage.removeItem('hrt_auth');
|
||||
}
|
||||
114
frontend-patient/src/services/consultation.service.ts
Normal file
114
frontend-patient/src/services/consultation.service.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { api } from './api-client';
|
||||
import type { Doctor, Consultation, ConsultationMessage } from '@/types';
|
||||
|
||||
interface RawDoctor {
|
||||
id: string;
|
||||
name: string;
|
||||
department: string;
|
||||
title: string;
|
||||
specialty: string[];
|
||||
introduction: string;
|
||||
isAvailable: boolean;
|
||||
avatarUrl?: string | null;
|
||||
}
|
||||
|
||||
interface RawConsultation {
|
||||
id: string;
|
||||
patientId: string;
|
||||
doctorId: string;
|
||||
subject?: string | null;
|
||||
status: string;
|
||||
startedAt: string;
|
||||
patientName?: string;
|
||||
doctorName?: string | null;
|
||||
closedAt?: string | null;
|
||||
summary?: string | null;
|
||||
}
|
||||
|
||||
interface RawMessage {
|
||||
id: string;
|
||||
senderId: string;
|
||||
senderRole: string;
|
||||
content: string;
|
||||
contentType: string;
|
||||
imageUrl?: string | null;
|
||||
isRead: boolean;
|
||||
createdAt: string;
|
||||
senderName?: string;
|
||||
}
|
||||
|
||||
function mapDoctor(d: RawDoctor): Doctor {
|
||||
return {
|
||||
id: d.id,
|
||||
name: d.name,
|
||||
department: d.department,
|
||||
title: d.title,
|
||||
specialty: d.specialty,
|
||||
introduction: d.introduction,
|
||||
isAvailable: d.isAvailable,
|
||||
avatarUrl: d.avatarUrl,
|
||||
};
|
||||
}
|
||||
|
||||
function mapMessage(m: RawMessage): ConsultationMessage {
|
||||
return {
|
||||
id: m.id,
|
||||
senderId: m.senderId,
|
||||
senderRole: m.senderRole,
|
||||
content: m.content,
|
||||
contentType: m.contentType,
|
||||
imageUrl: m.imageUrl,
|
||||
isRead: m.isRead,
|
||||
createdAt: m.createdAt,
|
||||
senderName: m.senderName,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getDoctors(_department?: string): Promise<Doctor[]> {
|
||||
const res = await api.get<RawDoctor[]>('/api/consultations/doctors');
|
||||
return res.data.map(mapDoctor);
|
||||
}
|
||||
|
||||
export async function getDoctor(id: string): Promise<Doctor | undefined> {
|
||||
const res = await api.get<RawDoctor[]>('/api/consultations/doctors');
|
||||
const d = res.data.find((d) => d.id === id);
|
||||
return d ? mapDoctor(d) : undefined;
|
||||
}
|
||||
|
||||
export async function getConsultation(doctorId: string): Promise<Consultation | undefined> {
|
||||
const res = await api.get<RawConsultation[]>('/api/consultations');
|
||||
const c = res.data.find((c) => c.doctorId === doctorId && c.status === 'active');
|
||||
return c ?? undefined;
|
||||
}
|
||||
|
||||
export async function startConsultation(doctorId: string, subject?: string): Promise<Consultation> {
|
||||
const existing = await getConsultation(doctorId);
|
||||
if (existing) return existing;
|
||||
|
||||
const res = await api.post<RawConsultation>('/api/consultations', {
|
||||
doctorId,
|
||||
subject: subject || '在线咨询',
|
||||
});
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function sendMessage(
|
||||
consultationId: string,
|
||||
text: string,
|
||||
): Promise<ConsultationMessage> {
|
||||
const res = await api.post<RawMessage>(`/api/consultations/${consultationId}/messages`, {
|
||||
content: text,
|
||||
});
|
||||
return mapMessage(res.data);
|
||||
}
|
||||
|
||||
export async function getDoctorReply(consultationId: string): Promise<ConsultationMessage | null> {
|
||||
const res = await api.get<RawMessage[]>(`/api/consultations/${consultationId}/messages`);
|
||||
const msgs = res.data;
|
||||
if (msgs.length === 0) return null;
|
||||
const lastMsg = msgs[msgs.length - 1];
|
||||
if (lastMsg.senderRole === 'doctor') {
|
||||
return mapMessage(lastMsg);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
19
frontend-patient/src/services/device.service.ts
Normal file
19
frontend-patient/src/services/device.service.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { api } from './api-client';
|
||||
import type { Device } from '@/types';
|
||||
|
||||
// Backend doesn't have a dedicated device API yet — return empty for now
|
||||
export async function getBoundDevices(): Promise<Device[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
export async function scanDevices(): Promise<Device[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
export async function bindDevice(_deviceId: string): Promise<Device> {
|
||||
return {} as Device;
|
||||
}
|
||||
|
||||
export async function unbindDevice(_deviceId: string): Promise<Device> {
|
||||
return {} as Device;
|
||||
}
|
||||
102
frontend-patient/src/services/exercise-diet.service.ts
Normal file
102
frontend-patient/src/services/exercise-diet.service.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { api } from './api-client';
|
||||
import type { ExerciseRecord, DietRecord } from '@/types';
|
||||
import { EXERCISE_RECOMMENDATIONS, DIET_RECOMMENDATIONS } from '@/utils/constants';
|
||||
|
||||
interface RawRecord {
|
||||
id: string;
|
||||
type: string;
|
||||
value: string;
|
||||
unit: string;
|
||||
recordedAt: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export async function getExerciseLogs(): Promise<ExerciseRecord[]> {
|
||||
try {
|
||||
const res = await api.get<RawRecord[]>('/api/health-records?type=exercise&days=30');
|
||||
return res.data.map((r) => {
|
||||
const val = JSON.parse(r.value || '{}');
|
||||
return {
|
||||
id: r.id,
|
||||
userId: '',
|
||||
type: val.type || 'walking',
|
||||
duration: val.duration || 0,
|
||||
intensity: val.intensity,
|
||||
caloriesBurned: val.caloriesBurned || val.calories,
|
||||
date: r.recordedAt.split('T')[0],
|
||||
notes: r.notes,
|
||||
};
|
||||
});
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function addExerciseLog(data: Omit<ExerciseRecord, 'id' | 'userId'>): Promise<ExerciseRecord> {
|
||||
const valueJson = JSON.stringify({
|
||||
type: data.type,
|
||||
duration: data.duration,
|
||||
intensity: data.intensity,
|
||||
caloriesBurned: data.caloriesBurned || data.calories,
|
||||
});
|
||||
const res = await api.post<RawRecord>('/api/health-records', {
|
||||
type: 'exercise',
|
||||
valueJson,
|
||||
unit: 'min',
|
||||
recordedAt: data.date,
|
||||
notes: data.notes,
|
||||
});
|
||||
return {
|
||||
id: res.data.id,
|
||||
userId: '',
|
||||
...data,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getDietLogs(): Promise<DietRecord[]> {
|
||||
try {
|
||||
const res = await api.get<RawRecord[]>('/api/health-records?type=diet&days=30');
|
||||
return res.data.map((r) => {
|
||||
const val = JSON.parse(r.value || '{}');
|
||||
return {
|
||||
id: r.id,
|
||||
userId: '',
|
||||
mealType: val.mealType || val.meal,
|
||||
foods: val.foods || [],
|
||||
totalCalories: val.totalCalories,
|
||||
date: r.recordedAt.split('T')[0],
|
||||
notes: r.notes,
|
||||
};
|
||||
});
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function addDietLog(data: Omit<DietRecord, 'id' | 'userId'>): Promise<DietRecord> {
|
||||
const valueJson = JSON.stringify({
|
||||
mealType: data.mealType || data.meal,
|
||||
foods: data.foods,
|
||||
totalCalories: data.totalCalories,
|
||||
});
|
||||
const res = await api.post<RawRecord>('/api/health-records', {
|
||||
type: 'diet',
|
||||
valueJson,
|
||||
unit: '',
|
||||
recordedAt: data.date,
|
||||
notes: data.notes,
|
||||
});
|
||||
return {
|
||||
id: res.data.id,
|
||||
userId: '',
|
||||
...data,
|
||||
};
|
||||
}
|
||||
|
||||
export function getExerciseRecommendations() {
|
||||
return EXERCISE_RECOMMENDATIONS;
|
||||
}
|
||||
|
||||
export function getDietRecommendations() {
|
||||
return DIET_RECOMMENDATIONS;
|
||||
}
|
||||
65
frontend-patient/src/services/followup.service.ts
Normal file
65
frontend-patient/src/services/followup.service.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { api } from './api-client';
|
||||
import type { FollowUp } from '@/types';
|
||||
|
||||
interface RawFollowUp {
|
||||
id: string;
|
||||
patientId: string;
|
||||
doctorId?: string | null;
|
||||
title: string;
|
||||
description?: string | null;
|
||||
scheduledAt: string;
|
||||
status: string;
|
||||
notes?: string | null;
|
||||
reminderEnabled: boolean;
|
||||
createdAt: string;
|
||||
patientName?: string;
|
||||
doctorName?: string | null;
|
||||
}
|
||||
|
||||
function mapFollowUp(f: RawFollowUp): FollowUp {
|
||||
return {
|
||||
id: f.id,
|
||||
patientId: f.patientId,
|
||||
doctorId: f.doctorId,
|
||||
title: f.title,
|
||||
description: f.description,
|
||||
scheduledAt: f.scheduledAt,
|
||||
status: f.status as FollowUp['status'],
|
||||
notes: f.notes,
|
||||
reminderEnabled: f.reminderEnabled,
|
||||
createdAt: f.createdAt,
|
||||
patientName: f.patientName,
|
||||
doctorName: f.doctorName,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getFollowUps(): Promise<FollowUp[]> {
|
||||
const res = await api.get<RawFollowUp[]>('/api/follow-ups');
|
||||
return res.data.map(mapFollowUp);
|
||||
}
|
||||
|
||||
export async function addFollowUp(data: Omit<FollowUp, 'id' | 'patientId' | 'createdAt'>): Promise<FollowUp> {
|
||||
const res = await api.post<RawFollowUp>('/api/follow-ups', {
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
scheduledAt: data.scheduledAt,
|
||||
reminderEnabled: data.reminderEnabled,
|
||||
notes: data.notes,
|
||||
});
|
||||
return mapFollowUp(res.data);
|
||||
}
|
||||
|
||||
export async function updateFollowUp(id: string, data: Partial<FollowUp>): Promise<FollowUp> {
|
||||
const res = await api.put<RawFollowUp>(`/api/follow-ups/${id}`, {
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
scheduledAt: data.scheduledAt,
|
||||
status: data.status,
|
||||
notes: data.notes,
|
||||
});
|
||||
return mapFollowUp(res.data);
|
||||
}
|
||||
|
||||
export async function deleteFollowUp(id: string): Promise<void> {
|
||||
await api.del(`/api/follow-ups/${id}`);
|
||||
}
|
||||
123
frontend-patient/src/services/health.service.ts
Normal file
123
frontend-patient/src/services/health.service.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { api } from './api-client';
|
||||
import type { HealthRecord, HealthStats, MeasurementType } from '@/types';
|
||||
|
||||
interface RawRecord {
|
||||
id: string;
|
||||
type: string;
|
||||
value: string; // JSON string from JSONB
|
||||
unit: string;
|
||||
recordedAt: string;
|
||||
source: string;
|
||||
notes?: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
function parseValue(type: string, raw: string): number | { systolic: number; diastolic: number } {
|
||||
try {
|
||||
const obj = JSON.parse(raw);
|
||||
if (type === 'blood_pressure') {
|
||||
return { systolic: obj.systolic ?? 0, diastolic: obj.diastolic ?? 0 };
|
||||
}
|
||||
return obj.value ?? obj;
|
||||
} catch {
|
||||
return Number(raw) || 0;
|
||||
}
|
||||
}
|
||||
|
||||
function mapRecord(r: RawRecord): HealthRecord {
|
||||
return {
|
||||
id: r.id,
|
||||
userId: '',
|
||||
type: r.type as MeasurementType,
|
||||
value: parseValue(r.type, r.value),
|
||||
unit: r.unit,
|
||||
recordedAt: r.recordedAt,
|
||||
recordedDate: r.recordedAt.split('T')[0],
|
||||
note: r.notes,
|
||||
source: r.source as 'manual' | 'device',
|
||||
};
|
||||
}
|
||||
|
||||
export async function getRecords(params: {
|
||||
type?: MeasurementType;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}): Promise<HealthRecord[]> {
|
||||
const query = new URLSearchParams();
|
||||
if (params.type) query.set('type', params.type);
|
||||
query.set('days', '90');
|
||||
const res = await api.get<RawRecord[]>(`/api/health-records?${query}`);
|
||||
let records = res.data.map(mapRecord);
|
||||
if (params.startDate) records = records.filter((r) => r.recordedDate >= params.startDate!);
|
||||
if (params.endDate) records = records.filter((r) => r.recordedDate <= params.endDate!);
|
||||
records.sort((a, b) => b.recordedAt.localeCompare(a.recordedAt));
|
||||
return records;
|
||||
}
|
||||
|
||||
export async function addRecord(record: Omit<HealthRecord, 'id' | 'userId'>): Promise<HealthRecord> {
|
||||
// Build JSON value to match backend
|
||||
let valueJson: string;
|
||||
if (typeof record.value === 'object') {
|
||||
valueJson = JSON.stringify(record.value);
|
||||
} else {
|
||||
valueJson = JSON.stringify({ value: record.value });
|
||||
}
|
||||
|
||||
const res = await api.post<RawRecord>('/api/health-records', {
|
||||
type: record.type,
|
||||
valueJson,
|
||||
unit: record.unit,
|
||||
recordedAt: record.recordedAt,
|
||||
notes: record.note,
|
||||
});
|
||||
return mapRecord(res.data);
|
||||
}
|
||||
|
||||
export async function getLatestStats(): Promise<HealthStats[]> {
|
||||
const res = await api.get<RawRecord[]>('/api/health-records?days=7');
|
||||
const records = res.data.map(mapRecord);
|
||||
const types: MeasurementType[] = ['blood_pressure', 'heart_rate', 'blood_sugar', 'spo2', 'weight', 'steps'];
|
||||
const statsList: HealthStats[] = [];
|
||||
|
||||
for (const type of types) {
|
||||
const typeRecords = records.filter((r) => r.type === type);
|
||||
const latest = typeRecords[0] || null;
|
||||
|
||||
const values = typeRecords.map((r) =>
|
||||
typeof r.value === 'object' ? (r.value.systolic + r.value.diastolic) / 2 : r.value,
|
||||
);
|
||||
|
||||
const avg7Days = values.length ? values.reduce((a, b) => a + b, 0) / values.length : 0;
|
||||
const min7Days = values.length ? Math.min(...values) : 0;
|
||||
const max7Days = values.length ? Math.max(...values) : 0;
|
||||
|
||||
const mid = Math.floor(values.length / 2);
|
||||
const olderAvg = values.slice(0, mid).reduce((a, b) => a + b, 0) / (mid || 1);
|
||||
const newerAvg = values.slice(mid).reduce((a, b) => a + b, 0) / (values.length - mid || 1);
|
||||
|
||||
let trend: 'up' | 'down' | 'stable' = 'stable';
|
||||
if (newerAvg > olderAvg * 1.03) trend = 'up';
|
||||
else if (newerAvg < olderAvg * 0.97) trend = 'down';
|
||||
|
||||
const unitMap: Record<string, string> = {
|
||||
blood_pressure: 'mmHg', heart_rate: 'bpm', blood_sugar: 'mmol/L',
|
||||
spo2: '%', weight: 'kg', steps: '步',
|
||||
};
|
||||
|
||||
statsList.push({
|
||||
type, latest,
|
||||
avg7Days: +avg7Days.toFixed(1),
|
||||
min7Days: +min7Days.toFixed(1),
|
||||
max7Days: +max7Days.toFixed(1),
|
||||
trend,
|
||||
unit: unitMap[type],
|
||||
});
|
||||
}
|
||||
|
||||
return statsList;
|
||||
}
|
||||
|
||||
export async function getTrendData(type: MeasurementType, days: number): Promise<HealthRecord[]> {
|
||||
const res = await api.get<RawRecord[]>(`/api/health-records?type=${type}&days=${days}`);
|
||||
return res.data.map(mapRecord).sort((a, b) => a.recordedAt.localeCompare(b.recordedAt));
|
||||
}
|
||||
96
frontend-patient/src/services/medication.service.ts
Normal file
96
frontend-patient/src/services/medication.service.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { api } from './api-client';
|
||||
import type { Medication, MedicationAdherence, MedicationRecord } from '@/types';
|
||||
|
||||
interface RawMedication {
|
||||
id: string;
|
||||
userId: string;
|
||||
drugName: string;
|
||||
dosage: string;
|
||||
frequency: string;
|
||||
timeSlots: string[];
|
||||
startDate: string;
|
||||
endDate?: string | null;
|
||||
notes?: string | null;
|
||||
status: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
interface RawMedRecord {
|
||||
id: string;
|
||||
medicationId: string;
|
||||
timeSlot: string;
|
||||
takenAt?: string | null;
|
||||
isTaken: boolean;
|
||||
skippedReason?: string | null;
|
||||
}
|
||||
|
||||
function mapMedication(m: RawMedication): Medication {
|
||||
return {
|
||||
id: m.id,
|
||||
userId: m.userId,
|
||||
drugName: m.drugName,
|
||||
dosage: m.dosage,
|
||||
frequency: m.frequency,
|
||||
timeSlots: m.timeSlots,
|
||||
startDate: m.startDate,
|
||||
endDate: m.endDate ?? undefined,
|
||||
notes: m.notes ?? undefined,
|
||||
status: m.status as Medication['status'],
|
||||
createdAt: m.createdAt,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getMedications(): Promise<Medication[]> {
|
||||
const res = await api.get<RawMedication[]>('/api/medications');
|
||||
return res.data.map(mapMedication);
|
||||
}
|
||||
|
||||
export async function addMedication(data: Omit<Medication, 'id' | 'userId' | 'createdAt'>): Promise<Medication> {
|
||||
const res = await api.post<RawMedication>('/api/medications', {
|
||||
drugName: data.drugName,
|
||||
dosage: data.dosage,
|
||||
frequency: data.frequency,
|
||||
timeSlots: data.timeSlots,
|
||||
startDate: data.startDate,
|
||||
endDate: data.endDate ?? null,
|
||||
notes: data.notes ?? null,
|
||||
});
|
||||
return mapMedication(res.data);
|
||||
}
|
||||
|
||||
export async function updateMedication(id: string, data: Partial<Medication>): Promise<Medication> {
|
||||
const res = await api.put<RawMedication>(`/api/medications/${id}`, {
|
||||
drugName: data.drugName,
|
||||
dosage: data.dosage,
|
||||
frequency: data.frequency,
|
||||
timeSlots: data.timeSlots,
|
||||
notes: data.notes ?? null,
|
||||
status: data.status,
|
||||
});
|
||||
return mapMedication(res.data);
|
||||
}
|
||||
|
||||
export async function deleteMedication(id: string): Promise<void> {
|
||||
await api.del(`/api/medications/${id}`);
|
||||
}
|
||||
|
||||
export async function markTaken(medicationId: string, slot: string): Promise<void> {
|
||||
await api.post(`/api/medications/${medicationId}/take`, { timeSlot: slot });
|
||||
}
|
||||
|
||||
export async function getMedicationRecords(medicationId: string): Promise<MedicationRecord[]> {
|
||||
const res = await api.get<RawMedRecord[]>(`/api/medications/${medicationId}/records`);
|
||||
return res.data.map((r) => ({
|
||||
id: r.id,
|
||||
medicationId: r.medicationId,
|
||||
timeSlot: r.timeSlot,
|
||||
takenAt: r.takenAt,
|
||||
isTaken: r.isTaken,
|
||||
skippedReason: r.skippedReason,
|
||||
}));
|
||||
}
|
||||
|
||||
export async function getAdherence(medicationId: string): Promise<MedicationAdherence> {
|
||||
const res = await api.get<MedicationAdherence>(`/api/medications/${medicationId}/adherence`);
|
||||
return res.data;
|
||||
}
|
||||
46
frontend-patient/src/services/notification.service.ts
Normal file
46
frontend-patient/src/services/notification.service.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { api } from './api-client';
|
||||
import type { Notification } from '@/types';
|
||||
|
||||
interface RawNotification {
|
||||
id: string;
|
||||
userId: string;
|
||||
title: string;
|
||||
content: string;
|
||||
type: string;
|
||||
isRead: boolean;
|
||||
readAt?: string | null;
|
||||
createdAt: string;
|
||||
relatedId?: string | null;
|
||||
}
|
||||
|
||||
function mapNotification(n: RawNotification): Notification {
|
||||
return {
|
||||
id: n.id,
|
||||
userId: n.userId,
|
||||
title: n.title,
|
||||
content: n.content,
|
||||
type: n.type as Notification['type'],
|
||||
isRead: n.isRead,
|
||||
readAt: n.readAt,
|
||||
createdAt: n.createdAt,
|
||||
relatedId: n.relatedId,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getNotifications(): Promise<Notification[]> {
|
||||
const res = await api.get<RawNotification[]>('/api/notifications');
|
||||
return res.data.map(mapNotification).sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
||||
}
|
||||
|
||||
export async function getUnreadCount(): Promise<number> {
|
||||
const res = await api.get<{ count: number }>('/api/notifications/unread-count');
|
||||
return res.data.count;
|
||||
}
|
||||
|
||||
export async function markAsRead(id: string): Promise<void> {
|
||||
await api.put(`/api/notifications/${id}/read`);
|
||||
}
|
||||
|
||||
export async function markAllAsRead(): Promise<void> {
|
||||
await api.put('/api/notifications/read-all');
|
||||
}
|
||||
67
frontend-patient/src/services/report.service.ts
Normal file
67
frontend-patient/src/services/report.service.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { api } from './api-client';
|
||||
import type { Report } from '@/types';
|
||||
|
||||
interface RawReport {
|
||||
id: string;
|
||||
patientId: string;
|
||||
title: string;
|
||||
category: string;
|
||||
imageUrls: string[];
|
||||
status: string;
|
||||
result?: string;
|
||||
createdAt: string;
|
||||
interpretedAt?: string;
|
||||
interpretedBy?: string;
|
||||
}
|
||||
|
||||
function mapReport(r: RawReport): Report {
|
||||
let result: Report['result'] | undefined;
|
||||
if (r.result) {
|
||||
try {
|
||||
const parsed = JSON.parse(r.result);
|
||||
result = {
|
||||
summary: parsed.summary || '',
|
||||
findings: parsed.findings || [],
|
||||
suggestions: parsed.suggestions || [],
|
||||
interpretedAt: r.interpretedAt || '',
|
||||
interpretedBy: r.interpretedBy || '',
|
||||
riskLevel: parsed.riskLevel || 'normal',
|
||||
};
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
return {
|
||||
id: r.id,
|
||||
userId: r.patientId,
|
||||
title: r.title,
|
||||
imageUrls: r.imageUrls,
|
||||
uploadAt: r.createdAt,
|
||||
status: r.status as Report['status'],
|
||||
category: r.category,
|
||||
result,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getReports(): Promise<Report[]> {
|
||||
const res = await api.get<RawReport[]>('/api/reports');
|
||||
return res.data.map(mapReport);
|
||||
}
|
||||
|
||||
export async function uploadReport(data: { title: string; category: string }): Promise<Report> {
|
||||
const res = await api.post<RawReport>('/api/reports', {
|
||||
title: data.title,
|
||||
category: data.category,
|
||||
});
|
||||
return mapReport(res.data);
|
||||
}
|
||||
|
||||
export async function getReport(id: string): Promise<Report | undefined> {
|
||||
const res = await api.get<RawReport>(`/api/reports/${id}`);
|
||||
return mapReport(res.data);
|
||||
}
|
||||
|
||||
export async function completeInterpretation(id: string): Promise<Report> {
|
||||
// Backend handles interpretation; we just fetch the updated report
|
||||
const res = await api.get<RawReport>(`/api/reports/${id}`);
|
||||
return mapReport(res.data);
|
||||
}
|
||||
Reference in New Issue
Block a user