feat: add care execution workspace
This commit is contained in:
287
modules/care/components/CareWorkspaceClient.tsx
Normal file
287
modules/care/components/CareWorkspaceClient.tsx
Normal file
@@ -0,0 +1,287 @@
|
||||
"use client";
|
||||
|
||||
import { FormEvent, useMemo, useState } from "react";
|
||||
import { CheckCircle2, Clock3, Play, Search, Siren } from "lucide-react";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Dialog } from "@/components/ui/dialog";
|
||||
import { Input, Textarea } from "@/components/ui/input";
|
||||
import { Select } from "@/components/ui/select";
|
||||
import { Table } from "@/components/ui/table";
|
||||
import type { ApiResult } from "@/modules/core/server/api";
|
||||
import type { CareExecutionData, CareTask, CareTaskPriority, CareTaskStatus, CareTaskType } from "@/modules/care/types";
|
||||
import { CARE_TASK_PRIORITY_LABELS, CARE_TASK_STATUS_LABELS, CARE_TASK_TYPE_LABELS } from "@/modules/care/types";
|
||||
|
||||
type CareWorkspaceClientProps = {
|
||||
canManage: boolean;
|
||||
initialData: CareExecutionData;
|
||||
};
|
||||
|
||||
function formatDateTime(value: string | undefined): string {
|
||||
return value ? new Date(value).toLocaleString("zh-CN") : "-";
|
||||
}
|
||||
|
||||
function statusVariant(status: CareTaskStatus): "success" | "secondary" | "warning" {
|
||||
if (status === "completed") {
|
||||
return "success";
|
||||
}
|
||||
|
||||
return status === "pending" ? "warning" : "secondary";
|
||||
}
|
||||
|
||||
function priorityVariant(priority: CareTaskPriority): "danger" | "secondary" | "warning" {
|
||||
if (priority === "urgent") {
|
||||
return "danger";
|
||||
}
|
||||
|
||||
return priority === "high" ? "warning" : "secondary";
|
||||
}
|
||||
|
||||
export function CareWorkspaceClient({ canManage, initialData }: CareWorkspaceClientProps): React.ReactElement {
|
||||
const [data, setData] = useState(initialData);
|
||||
const [query, setQuery] = useState("");
|
||||
const [statusFilter, setStatusFilter] = useState<"all" | CareTaskStatus>("all");
|
||||
const [priorityFilter, setPriorityFilter] = useState<"all" | CareTaskPriority>("all");
|
||||
const [typeFilter, setTypeFilter] = useState<"all" | CareTaskType>("all");
|
||||
const [message, setMessage] = useState("");
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
const [completeTarget, setCompleteTarget] = useState<CareTask | undefined>();
|
||||
const [executionNotes, setExecutionNotes] = useState("");
|
||||
|
||||
const normalizedQuery = query.trim().toLowerCase();
|
||||
const filteredTasks = useMemo(
|
||||
() =>
|
||||
data.tasks.filter((task) => {
|
||||
const matchesQuery =
|
||||
!normalizedQuery ||
|
||||
[task.title, task.elderName, task.assigneeLabel, task.executionNotes].join(" ").toLowerCase().includes(normalizedQuery);
|
||||
const matchesStatus = statusFilter === "all" || task.status === statusFilter;
|
||||
const matchesPriority = priorityFilter === "all" || task.priority === priorityFilter;
|
||||
const matchesType = typeFilter === "all" || task.careType === typeFilter;
|
||||
return matchesQuery && matchesStatus && matchesPriority && matchesType;
|
||||
}),
|
||||
[data.tasks, normalizedQuery, priorityFilter, statusFilter, typeFilter],
|
||||
);
|
||||
|
||||
async function refreshData(): Promise<void> {
|
||||
const response = await fetch("/api/care/tasks");
|
||||
const result = (await response.json()) as ApiResult<{ data: CareExecutionData }>;
|
||||
if (result.success) {
|
||||
setData(result.data);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateTask(task: CareTask, status: CareTaskStatus, notes = task.executionNotes): Promise<void> {
|
||||
if (!canManage) {
|
||||
setMessage("当前角色无权处理护理任务");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsPending(true);
|
||||
setMessage("");
|
||||
const response = await fetch(`/api/care/tasks/${task.id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ status, executionNotes: notes }),
|
||||
});
|
||||
const result = (await response.json()) as ApiResult<{ task: CareTask }>;
|
||||
setIsPending(false);
|
||||
setMessage(result.reason);
|
||||
|
||||
if (result.success) {
|
||||
setCompleteTarget(undefined);
|
||||
setExecutionNotes("");
|
||||
await refreshData();
|
||||
}
|
||||
}
|
||||
|
||||
function openCompleteDialog(task: CareTask): void {
|
||||
setCompleteTarget(task);
|
||||
setExecutionNotes(task.executionNotes);
|
||||
setMessage("");
|
||||
}
|
||||
|
||||
function closeCompleteDialog(): void {
|
||||
if (isPending) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCompleteTarget(undefined);
|
||||
setExecutionNotes("");
|
||||
}
|
||||
|
||||
async function submitComplete(event: FormEvent<HTMLFormElement>): Promise<void> {
|
||||
event.preventDefault();
|
||||
if (!completeTarget) {
|
||||
return;
|
||||
}
|
||||
|
||||
await updateTask(completeTarget, "completed", executionNotes);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex w-full max-w-7xl flex-col gap-5">
|
||||
<section className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<MetricCard icon={Clock3} label="待处理" value={data.metrics.pending} />
|
||||
<MetricCard icon={Play} label="进行中" value={data.metrics.inProgress} />
|
||||
<MetricCard icon={CheckCircle2} label="今日完成" value={data.metrics.completedToday} />
|
||||
<MetricCard icon={Siren} label="高优先级" value={data.metrics.highPriority} />
|
||||
</section>
|
||||
|
||||
<section className="flex flex-col gap-3 border-b pb-4 xl:flex-row xl:items-center xl:justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-semibold tracking-normal">护理服务</h1>
|
||||
<p className="mt-2 text-sm text-muted-foreground">按老人、任务类型、优先级和执行状态推进每日护理服务。</p>
|
||||
</div>
|
||||
<div className="grid gap-2 sm:grid-cols-2 xl:flex xl:items-center">
|
||||
<label className="relative block sm:col-span-2 xl:col-span-1">
|
||||
<Search className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
className="w-full pl-9 xl:w-72"
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder="搜索老人、任务或执行人"
|
||||
value={query}
|
||||
/>
|
||||
</label>
|
||||
<Select
|
||||
aria-label="任务状态"
|
||||
onValueChange={(value) => setStatusFilter(value as "all" | CareTaskStatus)}
|
||||
options={[
|
||||
{ value: "all", label: "全部状态" },
|
||||
...Object.entries(CARE_TASK_STATUS_LABELS).map(([value, label]) => ({ value, label })),
|
||||
]}
|
||||
value={statusFilter}
|
||||
/>
|
||||
<Select
|
||||
aria-label="优先级"
|
||||
onValueChange={(value) => setPriorityFilter(value as "all" | CareTaskPriority)}
|
||||
options={[
|
||||
{ value: "all", label: "全部优先级" },
|
||||
...Object.entries(CARE_TASK_PRIORITY_LABELS).map(([value, label]) => ({ value, label })),
|
||||
]}
|
||||
value={priorityFilter}
|
||||
/>
|
||||
<Select
|
||||
aria-label="护理类型"
|
||||
onValueChange={(value) => setTypeFilter(value as "all" | CareTaskType)}
|
||||
options={[
|
||||
{ value: "all", label: "全部类型" },
|
||||
...Object.entries(CARE_TASK_TYPE_LABELS).map(([value, label]) => ({ value, label })),
|
||||
]}
|
||||
value={typeFilter}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{message ? (
|
||||
<p className="rounded-md bg-secondary px-3 py-2 text-sm text-secondary-foreground" role="status">
|
||||
{message}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>护理任务</CardTitle>
|
||||
<CardDescription>任务状态更新会写入审计日志,完成时保存执行备注和完成时间。</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="overflow-x-auto">
|
||||
<Table className="w-full min-w-[980px] text-sm">
|
||||
<Table.Header className="bg-secondary text-left text-xs text-muted-foreground">
|
||||
<Table.Row>
|
||||
<Table.Head className="px-4 py-3 font-medium">任务</Table.Head>
|
||||
<Table.Head className="px-4 py-3 font-medium">老人</Table.Head>
|
||||
<Table.Head className="px-4 py-3 font-medium">类型</Table.Head>
|
||||
<Table.Head className="px-4 py-3 font-medium">优先级</Table.Head>
|
||||
<Table.Head className="px-4 py-3 font-medium">状态</Table.Head>
|
||||
<Table.Head className="px-4 py-3 font-medium">计划时间</Table.Head>
|
||||
<Table.Head className="px-4 py-3 font-medium">执行人</Table.Head>
|
||||
<Table.Head className="px-4 py-3 font-medium">完成时间</Table.Head>
|
||||
<Table.Head className="px-4 py-3 text-right font-medium">操作</Table.Head>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body className="divide-y bg-card">
|
||||
{filteredTasks.map((task) => (
|
||||
<Table.Row key={task.id}>
|
||||
<Table.Cell className="px-4 py-3">
|
||||
<p className="font-medium">{task.title}</p>
|
||||
<p className="text-xs text-muted-foreground">{task.executionNotes || "-"}</p>
|
||||
</Table.Cell>
|
||||
<Table.Cell className="px-4 py-3 text-muted-foreground">{task.elderName || "公共区域"}</Table.Cell>
|
||||
<Table.Cell className="px-4 py-3 text-muted-foreground">{CARE_TASK_TYPE_LABELS[task.careType]}</Table.Cell>
|
||||
<Table.Cell className="px-4 py-3">
|
||||
<Badge variant={priorityVariant(task.priority)}>{CARE_TASK_PRIORITY_LABELS[task.priority]}</Badge>
|
||||
</Table.Cell>
|
||||
<Table.Cell className="px-4 py-3">
|
||||
<Badge variant={statusVariant(task.status)}>{CARE_TASK_STATUS_LABELS[task.status]}</Badge>
|
||||
</Table.Cell>
|
||||
<Table.Cell className="px-4 py-3 text-muted-foreground">{formatDateTime(task.scheduledAt)}</Table.Cell>
|
||||
<Table.Cell className="px-4 py-3 text-muted-foreground">{task.assigneeLabel || "-"}</Table.Cell>
|
||||
<Table.Cell className="px-4 py-3 text-muted-foreground">{formatDateTime(task.completedAt)}</Table.Cell>
|
||||
<Table.Cell className="px-4 py-3">
|
||||
<div className="flex justify-end gap-2">
|
||||
{task.status === "pending" ? (
|
||||
<Button disabled={!canManage || isPending} onClick={() => updateTask(task, "in_progress")} size="sm" type="button" variant="outline">
|
||||
开始
|
||||
</Button>
|
||||
) : null}
|
||||
{task.status === "pending" || task.status === "in_progress" ? (
|
||||
<Button disabled={!canManage || isPending} onClick={() => openCompleteDialog(task)} size="sm" type="button">
|
||||
完成
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
))}
|
||||
{filteredTasks.length === 0 ? (
|
||||
<Table.Row>
|
||||
<Table.Cell className="px-4 py-8 text-center text-muted-foreground" colSpan={9}>
|
||||
暂无护理任务
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
) : null}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Dialog className="max-w-lg" description={completeTarget?.title} onClose={closeCompleteDialog} open={completeTarget !== undefined} title="完成护理任务">
|
||||
<form className="grid gap-3" onSubmit={submitComplete}>
|
||||
<Textarea label="执行备注" onChange={(event) => setExecutionNotes(event.target.value)} value={executionNotes} />
|
||||
<div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
|
||||
<Button disabled={isPending} onClick={closeCompleteDialog} type="button" variant="outline">
|
||||
取消
|
||||
</Button>
|
||||
<Button disabled={isPending} type="submit">
|
||||
保存完成
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MetricCard({
|
||||
icon: Icon,
|
||||
label,
|
||||
value,
|
||||
}: {
|
||||
icon: React.ComponentType<{ "aria-hidden"?: boolean; className?: string }>;
|
||||
label: string;
|
||||
value: number;
|
||||
}): React.ReactElement {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between p-4">
|
||||
<div>
|
||||
<CardDescription>{label}</CardDescription>
|
||||
<CardTitle className="mt-2 text-3xl">{value}</CardTitle>
|
||||
</div>
|
||||
<Icon className="size-5 text-primary" aria-hidden={true} />
|
||||
</CardHeader>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
141
modules/care/server/operations.ts
Normal file
141
modules/care/server/operations.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { and, desc, eq, inArray } from "drizzle-orm";
|
||||
|
||||
import { getDatabase } from "@/modules/core/server/db";
|
||||
import { accounts, careTasks, elders } from "@/modules/core/server/schema";
|
||||
import type { CareExecutionData, CareTask, CareTaskStatusUpdateInput } from "@/modules/care/types";
|
||||
|
||||
export type CareMutationFailure = {
|
||||
success: false;
|
||||
reason: string;
|
||||
status: number;
|
||||
};
|
||||
|
||||
type AccountNameRow = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
type ElderNameRow = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export function isCareMutationFailure(value: unknown): value is CareMutationFailure {
|
||||
return (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
"success" in value &&
|
||||
(value as { success?: unknown }).success === false
|
||||
);
|
||||
}
|
||||
|
||||
function iso(value: Date): string {
|
||||
return value.toISOString();
|
||||
}
|
||||
|
||||
function optionalIso(value: Date | null): string | undefined {
|
||||
return value ? iso(value) : undefined;
|
||||
}
|
||||
|
||||
function buildNameMap(rows: AccountNameRow[] | ElderNameRow[]): Map<string, string> {
|
||||
return new Map(rows.map((row) => [row.id, row.name]));
|
||||
}
|
||||
|
||||
function toCareTask(
|
||||
row: typeof careTasks.$inferSelect,
|
||||
elderNameById: Map<string, string>,
|
||||
accountNameById: Map<string, string>,
|
||||
): CareTask {
|
||||
const elderId = row.elderId ?? undefined;
|
||||
const completedByAccountId = row.completedByAccountId ?? undefined;
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
organizationId: row.organizationId,
|
||||
elderId,
|
||||
elderName: elderId ? elderNameById.get(elderId) ?? "" : "",
|
||||
title: row.title,
|
||||
careType: row.careType,
|
||||
priority: row.priority,
|
||||
status: row.status,
|
||||
scheduledAt: iso(row.scheduledAt),
|
||||
assigneeLabel: row.assigneeLabel,
|
||||
executionNotes: row.executionNotes,
|
||||
completedAt: optionalIso(row.completedAt),
|
||||
completedByAccountId,
|
||||
completedByName: completedByAccountId ? accountNameById.get(completedByAccountId) : undefined,
|
||||
createdAt: iso(row.createdAt),
|
||||
updatedAt: iso(row.updatedAt),
|
||||
};
|
||||
}
|
||||
|
||||
export async function listCareExecutionData(organizationId: string): Promise<CareExecutionData> {
|
||||
const database = getDatabase();
|
||||
const todayStart = new Date();
|
||||
todayStart.setHours(0, 0, 0, 0);
|
||||
|
||||
const taskRows = await database
|
||||
.select()
|
||||
.from(careTasks)
|
||||
.where(eq(careTasks.organizationId, organizationId))
|
||||
.orderBy(desc(careTasks.scheduledAt));
|
||||
|
||||
const elderIds = Array.from(new Set(taskRows.map((task) => task.elderId).filter((elderId): elderId is string => typeof elderId === "string")));
|
||||
const accountIds = Array.from(
|
||||
new Set(taskRows.map((task) => task.completedByAccountId).filter((accountId): accountId is string => typeof accountId === "string")),
|
||||
);
|
||||
|
||||
const [elderRows, accountRows] = await Promise.all([
|
||||
elderIds.length > 0
|
||||
? database.select({ id: elders.id, name: elders.name }).from(elders).where(and(eq(elders.organizationId, organizationId), inArray(elders.id, elderIds)))
|
||||
: [],
|
||||
accountIds.length > 0 ? database.select({ id: accounts.id, name: accounts.name }).from(accounts).where(inArray(accounts.id, accountIds)) : [],
|
||||
]);
|
||||
|
||||
const tasks = taskRows.map((task) => toCareTask(task, buildNameMap(elderRows), buildNameMap(accountRows)));
|
||||
|
||||
return {
|
||||
metrics: {
|
||||
pending: taskRows.filter((task) => task.status === "pending").length,
|
||||
inProgress: taskRows.filter((task) => task.status === "in_progress").length,
|
||||
completedToday: taskRows.filter((task) => task.status === "completed" && task.completedAt !== null && task.completedAt >= todayStart).length,
|
||||
highPriority: taskRows.filter((task) => task.priority === "high" || task.priority === "urgent").length,
|
||||
},
|
||||
tasks,
|
||||
};
|
||||
}
|
||||
|
||||
export async function updateCareTaskStatus(
|
||||
input: CareTaskStatusUpdateInput & { accountId: string; id: string; organizationId: string },
|
||||
): Promise<CareTask | CareMutationFailure> {
|
||||
const database = getDatabase();
|
||||
const completedAt = input.status === "completed" ? new Date() : null;
|
||||
const completedByAccountId = input.status === "completed" ? input.accountId : null;
|
||||
|
||||
const rows = await database
|
||||
.update(careTasks)
|
||||
.set({
|
||||
status: input.status,
|
||||
executionNotes: input.executionNotes,
|
||||
completedAt,
|
||||
completedByAccountId,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(and(eq(careTasks.id, input.id), eq(careTasks.organizationId, input.organizationId)))
|
||||
.returning();
|
||||
const task = rows[0];
|
||||
if (!task) {
|
||||
return { success: false, reason: "护理任务不存在", status: 404 };
|
||||
}
|
||||
|
||||
const [elderRows, accountRows] = await Promise.all([
|
||||
task.elderId
|
||||
? database.select({ id: elders.id, name: elders.name }).from(elders).where(and(eq(elders.id, task.elderId), eq(elders.organizationId, input.organizationId)))
|
||||
: [],
|
||||
task.completedByAccountId
|
||||
? database.select({ id: accounts.id, name: accounts.name }).from(accounts).where(eq(accounts.id, task.completedByAccountId))
|
||||
: [],
|
||||
]);
|
||||
|
||||
return toCareTask(task, buildNameMap(elderRows), buildNameMap(accountRows));
|
||||
}
|
||||
112
modules/care/types.ts
Normal file
112
modules/care/types.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
export const CARE_TASK_TYPE_VALUES = ["daily_care", "meal", "medication", "rehab", "inspection", "cleaning", "other"] as const;
|
||||
export type CareTaskType = (typeof CARE_TASK_TYPE_VALUES)[number];
|
||||
|
||||
export const CARE_TASK_PRIORITY_VALUES = ["low", "normal", "high", "urgent"] as const;
|
||||
export type CareTaskPriority = (typeof CARE_TASK_PRIORITY_VALUES)[number];
|
||||
|
||||
export const CARE_TASK_STATUS_VALUES = ["pending", "in_progress", "completed", "cancelled"] as const;
|
||||
export type CareTaskStatus = (typeof CARE_TASK_STATUS_VALUES)[number];
|
||||
|
||||
export const CARE_TASK_TYPE_LABELS: Record<CareTaskType, string> = {
|
||||
daily_care: "日常照护",
|
||||
meal: "助餐",
|
||||
medication: "用药",
|
||||
rehab: "康复",
|
||||
inspection: "巡检",
|
||||
cleaning: "清洁",
|
||||
other: "其他",
|
||||
};
|
||||
|
||||
export const CARE_TASK_PRIORITY_LABELS: Record<CareTaskPriority, string> = {
|
||||
low: "低",
|
||||
normal: "普通",
|
||||
high: "高",
|
||||
urgent: "紧急",
|
||||
};
|
||||
|
||||
export const CARE_TASK_STATUS_LABELS: Record<CareTaskStatus, string> = {
|
||||
pending: "待处理",
|
||||
in_progress: "进行中",
|
||||
completed: "已完成",
|
||||
cancelled: "已取消",
|
||||
};
|
||||
|
||||
export type CareTask = {
|
||||
id: string;
|
||||
organizationId: string;
|
||||
elderId?: string;
|
||||
elderName: string;
|
||||
title: string;
|
||||
careType: CareTaskType;
|
||||
priority: CareTaskPriority;
|
||||
status: CareTaskStatus;
|
||||
scheduledAt: string;
|
||||
assigneeLabel: string;
|
||||
executionNotes: string;
|
||||
completedAt?: string;
|
||||
completedByAccountId?: string;
|
||||
completedByName?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type CareExecutionMetrics = {
|
||||
pending: number;
|
||||
inProgress: number;
|
||||
completedToday: number;
|
||||
highPriority: number;
|
||||
};
|
||||
|
||||
export type CareExecutionData = {
|
||||
metrics: CareExecutionMetrics;
|
||||
tasks: CareTask[];
|
||||
};
|
||||
|
||||
export type CareTaskStatusUpdateInput = {
|
||||
status: CareTaskStatus;
|
||||
executionNotes: 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 readEnum<T extends string>(value: unknown, values: readonly T[]): T | null {
|
||||
return typeof value === "string" ? values.find((item) => item === value) ?? null : null;
|
||||
}
|
||||
|
||||
export function validateCareTaskStatusUpdateInput(value: unknown): ValidationResult<CareTaskStatusUpdateInput> {
|
||||
if (!isRecord(value)) {
|
||||
return { success: false, reason: "请求数据格式无效" };
|
||||
}
|
||||
|
||||
const status = readEnum(value.status, CARE_TASK_STATUS_VALUES);
|
||||
if (!status) {
|
||||
return { success: false, reason: "护理任务状态无效" };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
status,
|
||||
executionNotes: readString(value, "executionNotes"),
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user