feat(web): 班次管理 Web UI — Phase 2a-2
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled

新增班次管理前端页面,接入后端 12 条孤立路由:
- API 模块: shifts.ts(班次 CRUD + 患者分配 + 批量分配 + 交接日志)
- 列表页: ShiftList.tsx(日期/班次/状态筛选 + 统计概览)
- 详情页: ShiftDetail.tsx(班次信息 + 患者分配 Tab + 交接记录 Tab)
- 路由注册: /health/shifts + /health/shifts/:id

权限: health.shifts.list / health.shifts.manage
This commit is contained in:
iven
2026-05-04 23:36:15 +08:00
parent 3aa436f872
commit 68ced2bae9
5 changed files with 834 additions and 0 deletions

View File

@@ -0,0 +1,247 @@
import client from '../client';
import type { PaginatedResponse } from '../types';
// --- Types ---
export interface Shift {
id: string;
tenant_id: string;
shift_date: string;
period: string;
nurse_id?: string;
status: string;
notes?: string;
created_at: string;
updated_at: string;
version: number;
patient_count?: number;
critical_count?: number;
attention_count?: number;
}
export interface PatientAssignment {
id: string;
tenant_id: string;
shift_id: string;
patient_id: string;
care_level: string;
notes?: string;
created_at: string;
updated_at: string;
version: number;
patient_name?: string;
}
export interface HandoffLog {
id: string;
tenant_id: string;
from_shift_id: string;
to_shift_id: string;
patient_id: string;
notes?: string;
pending_items?: Record<string, unknown>;
created_at: string;
updated_at: string;
version: number;
patient_name?: string;
}
export interface CreateShiftReq {
shift_date: string;
period: string;
nurse_id?: string;
notes?: string;
}
export interface UpdateShiftReq {
shift_date?: string;
period?: string;
nurse_id?: string;
status?: string;
notes?: string;
}
export interface ListShiftsParams {
page?: number;
page_size?: number;
shift_date?: string;
period?: string;
nurse_id?: string;
status?: string;
}
export interface CreatePatientAssignmentReq {
patient_id: string;
care_level?: string;
notes?: string;
}
export interface BatchAssignReq {
patient_ids: string[];
care_level?: string;
}
export interface UpdatePatientAssignmentReq {
care_level?: string;
notes?: string;
}
export interface CreateHandoffReq {
from_shift_id: string;
to_shift_id: string;
patient_id: string;
notes?: string;
pending_items?: Record<string, unknown>;
}
export interface ListHandoffParams {
page?: number;
page_size?: number;
from_shift_id?: string;
to_shift_id?: string;
}
// --- Constants ---
export const PERIOD_OPTIONS = [
{ label: '上午班', value: 'morning' },
{ label: '下午班', value: 'afternoon' },
{ label: '晚班', value: 'evening' },
{ label: '夜班', value: 'night' },
];
export const SHIFT_STATUS_OPTIONS = [
{ label: '待开始', value: 'scheduled' },
{ label: '进行中', value: 'in_progress' },
{ label: '已完成', value: 'completed' },
{ label: '已取消', value: 'cancelled' },
];
export const CARE_LEVEL_OPTIONS = [
{ label: '稳定', value: 'stable' },
{ label: '需关注', value: 'attention' },
{ label: '危重', value: 'critical' },
];
export const PERIOD_LABEL: Record<string, string> = Object.fromEntries(
PERIOD_OPTIONS.map((o) => [o.value, o.label]),
);
export const SHIFT_STATUS_LABEL: Record<string, string> = Object.fromEntries(
SHIFT_STATUS_OPTIONS.map((o) => [o.value, o.label]),
);
export const SHIFT_STATUS_COLOR: Record<string, string> = {
scheduled: 'default',
in_progress: 'processing',
completed: 'success',
cancelled: 'error',
};
export const CARE_LEVEL_LABEL: Record<string, string> = Object.fromEntries(
CARE_LEVEL_OPTIONS.map((o) => [o.value, o.label]),
);
export const CARE_LEVEL_COLOR: Record<string, string> = {
stable: 'green',
attention: 'orange',
critical: 'red',
};
// --- API ---
export const shiftApi = {
// --- Shifts ---
list: async (params: ListShiftsParams) => {
const { data } = await client.get<{
success: boolean;
data: PaginatedResponse<Shift>;
}>('/health/shifts', { params });
return data.data;
},
get: async (shiftId: string) => {
const { data } = await client.get<{
success: boolean;
data: Shift;
}>(`/health/shifts/${shiftId}`);
return data.data;
},
create: async (req: CreateShiftReq) => {
const { data } = await client.post<{
success: boolean;
data: Shift;
}>('/health/shifts', req);
return data.data;
},
update: async (shiftId: string, req: UpdateShiftReq & { version: number }) => {
const { data } = await client.put<{
success: boolean;
data: Shift;
}>(`/health/shifts/${shiftId}`, req);
return data.data;
},
delete: async (shiftId: string, version: number) => {
await client.delete(`/health/shifts/${shiftId}`, { data: { version } });
},
// --- Assignments ---
listAssignments: async (shiftId: string, params?: { page?: number; page_size?: number }) => {
const { data } = await client.get<{
success: boolean;
data: PaginatedResponse<PatientAssignment>;
}>(`/health/shifts/${shiftId}/assignments`, { params });
return data.data;
},
createAssignment: async (shiftId: string, req: CreatePatientAssignmentReq) => {
const { data } = await client.post<{
success: boolean;
data: PatientAssignment;
}>(`/health/shifts/${shiftId}/assignments`, req);
return data.data;
},
batchAssign: async (shiftId: string, req: BatchAssignReq) => {
const { data } = await client.post<{
success: boolean;
data: PatientAssignment[];
}>(`/health/shifts/${shiftId}/assignments/batch`, req);
return data.data;
},
updateAssignment: async (shiftId: string, assignmentId: string, req: UpdatePatientAssignmentReq & { version: number }) => {
const { data } = await client.put<{
success: boolean;
data: PatientAssignment;
}>(`/health/shifts/${shiftId}/assignments/${assignmentId}`, req);
return data.data;
},
deleteAssignment: async (shiftId: string, assignmentId: string, version: number) => {
await client.delete(`/health/shifts/${shiftId}/assignments/${assignmentId}`, { data: { version } });
},
// --- Handoff Logs ---
listHandoffs: async (params?: ListHandoffParams) => {
const { data } = await client.get<{
success: boolean;
data: PaginatedResponse<HandoffLog>;
}>('/health/handoff-logs', { params });
return data.data;
},
createHandoff: async (req: CreateHandoffReq) => {
const { data } = await client.post<{
success: boolean;
data: HandoffLog;
}>('/health/handoff-logs', req);
return data.data;
},
};