新增设置页
This commit is contained in:
parent
2cf1fae21d
commit
6a65d956a8
31
app/api/push/test/route.ts
Normal file
31
app/api/push/test/route.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
import { sendCaregiverTestPush } from "@/lib/push";
|
||||||
|
import { getCaregiverDisplayName, getCaregiverSession } from "@/lib/session";
|
||||||
|
|
||||||
|
export async function POST() {
|
||||||
|
const caregiver = await getCaregiverSession();
|
||||||
|
|
||||||
|
if (!caregiver) {
|
||||||
|
return NextResponse.json({ error: "请先登录。" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await sendCaregiverTestPush({
|
||||||
|
caregiverId: caregiver.id,
|
||||||
|
caregiverLabel: getCaregiverDisplayName(caregiver),
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
ok: true,
|
||||||
|
...result,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: error instanceof Error ? error.message : "测试推送发送失败。",
|
||||||
|
},
|
||||||
|
{ status: 400 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,7 +2,6 @@ import Link from "next/link";
|
|||||||
|
|
||||||
import { BindDeviceForm } from "@/components/dashboard-actions";
|
import { BindDeviceForm } from "@/components/dashboard-actions";
|
||||||
import { PanelShell } from "@/components/panel-shell";
|
import { PanelShell } from "@/components/panel-shell";
|
||||||
import { PwaControls } from "@/components/pwa-controls";
|
|
||||||
import { getCaregiverDevices } from "@/lib/caregiver-panel";
|
import { getCaregiverDevices } from "@/lib/caregiver-panel";
|
||||||
import { formatDateTime, getDeviceName, truncateText } from "@/lib/panel-format";
|
import { formatDateTime, getDeviceName, truncateText } from "@/lib/panel-format";
|
||||||
import { requireCaregiverSession } from "@/lib/page-auth";
|
import { requireCaregiverSession } from "@/lib/page-auth";
|
||||||
@ -20,7 +19,6 @@ export default async function DevicesPage() {
|
|||||||
title="我的设备"
|
title="我的设备"
|
||||||
description="管理已连接的长辈设备,点击进入设备详情查看留言和使用动态。"
|
description="管理已连接的长辈设备,点击进入设备详情查看留言和使用动态。"
|
||||||
caregiverLabel={getCaregiverDisplayName(caregiver)}
|
caregiverLabel={getCaregiverDisplayName(caregiver)}
|
||||||
actions={<PwaControls />}
|
|
||||||
>
|
>
|
||||||
<section className="grid gap-6 xl:grid-cols-[0.95fr_1.05fr]">
|
<section className="grid gap-6 xl:grid-cols-[0.95fr_1.05fr]">
|
||||||
<BindDeviceForm />
|
<BindDeviceForm />
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
import { PanelShell } from "@/components/panel-shell";
|
import { PanelShell } from "@/components/panel-shell";
|
||||||
import { PwaControls } from "@/components/pwa-controls";
|
|
||||||
import { getCaregiverMessages } from "@/lib/caregiver-panel";
|
import { getCaregiverMessages } from "@/lib/caregiver-panel";
|
||||||
import {
|
import {
|
||||||
formatDateTime,
|
formatDateTime,
|
||||||
@ -35,7 +34,6 @@ export default async function MessagesPage() {
|
|||||||
title="留言板"
|
title="留言板"
|
||||||
description="长辈捆来的话和您发出的问候,都在这里。"
|
description="长辈捆来的话和您发出的问候,都在这里。"
|
||||||
caregiverLabel={getCaregiverDisplayName(caregiver)}
|
caregiverLabel={getCaregiverDisplayName(caregiver)}
|
||||||
actions={<PwaControls />}
|
|
||||||
>
|
>
|
||||||
<section className="grid gap-4 md:grid-cols-3">
|
<section className="grid gap-4 md:grid-cols-3">
|
||||||
<div className="rounded-[28px] border border-white/80 bg-white/88 p-5">
|
<div className="rounded-[28px] border border-white/80 bg-white/88 p-5">
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import Link from "next/link";
|
|||||||
import { MessageCircle, Smartphone, Activity, ScanLine } from "lucide-react";
|
import { MessageCircle, Smartphone, Activity, ScanLine } from "lucide-react";
|
||||||
|
|
||||||
import { PanelShell } from "@/components/panel-shell";
|
import { PanelShell } from "@/components/panel-shell";
|
||||||
import { PwaControls } from "@/components/pwa-controls";
|
|
||||||
import { getCaregiverOverview } from "@/lib/caregiver-panel";
|
import { getCaregiverOverview } from "@/lib/caregiver-panel";
|
||||||
import {
|
import {
|
||||||
formatDateTime,
|
formatDateTime,
|
||||||
@ -40,7 +39,6 @@ export default async function Home() {
|
|||||||
title="今日概览"
|
title="今日概览"
|
||||||
description="随时了解长辈的近况,留言和动态一目了然。"
|
description="随时了解长辈的近况,留言和动态一目了然。"
|
||||||
caregiverLabel={getCaregiverDisplayName(dashboard.caregiver)}
|
caregiverLabel={getCaregiverDisplayName(dashboard.caregiver)}
|
||||||
actions={<PwaControls />}
|
|
||||||
>
|
>
|
||||||
<section className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
<section className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||||
<div className="rounded-[28px] border border-white/80 bg-white/88 p-5 shadow-[0_20px_50px_rgba(115,76,42,0.10)]">
|
<div className="rounded-[28px] border border-white/80 bg-white/88 p-5 shadow-[0_20px_50px_rgba(115,76,42,0.10)]">
|
||||||
|
|||||||
45
app/settings/page.tsx
Normal file
45
app/settings/page.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { PanelShell } from "@/components/panel-shell";
|
||||||
|
import { PwaControls } from "@/components/pwa-controls";
|
||||||
|
import { PushTestPanel } from "@/components/push-test-panel";
|
||||||
|
import { getCaregiverSettingsOverview } from "@/lib/caregiver-panel";
|
||||||
|
import { requireCaregiverSession } from "@/lib/page-auth";
|
||||||
|
import { getCaregiverDisplayName } from "@/lib/session";
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
export default async function SettingsPage() {
|
||||||
|
const caregiver = await requireCaregiverSession("/settings");
|
||||||
|
const overview = await getCaregiverSettingsOverview(caregiver.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PanelShell
|
||||||
|
currentPath="/settings"
|
||||||
|
title="设置"
|
||||||
|
description="桌面版安装、消息提醒和推送测试统一放在这里,不再分散显示在各个页面。"
|
||||||
|
caregiverLabel={getCaregiverDisplayName(caregiver)}
|
||||||
|
>
|
||||||
|
<section className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||||
|
<div className="rounded-[28px] border border-white/80 bg-white/88 p-5 shadow-[0_20px_50px_rgba(115,76,42,0.10)]">
|
||||||
|
<p className="text-sm text-[var(--muted)]">已连接设备</p>
|
||||||
|
<p className="mt-3 font-[family-name:var(--font-display)] text-4xl text-[var(--ink)]">
|
||||||
|
{overview.deviceCount}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-[28px] border border-white/80 bg-white/88 p-5 shadow-[0_20px_50px_rgba(115,76,42,0.10)]">
|
||||||
|
<p className="text-sm text-[var(--muted)]">已开启提醒的设备</p>
|
||||||
|
<p className="mt-3 font-[family-name:var(--font-display)] text-4xl text-[var(--ink)]">
|
||||||
|
{overview.pushSubscriptionCount}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="grid gap-6 xl:grid-cols-2">
|
||||||
|
<PwaControls />
|
||||||
|
<PushTestPanel
|
||||||
|
initialSubscriptionCount={overview.pushSubscriptionCount}
|
||||||
|
deviceCount={overview.deviceCount}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</PanelShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,5 +1,12 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Home, MessageCircle, Smartphone, Activity, ScanLine } from "lucide-react";
|
import {
|
||||||
|
Home,
|
||||||
|
MessageCircle,
|
||||||
|
Smartphone,
|
||||||
|
Activity,
|
||||||
|
ScanLine,
|
||||||
|
Settings,
|
||||||
|
} from "lucide-react";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
const navigationItems = [
|
const navigationItems = [
|
||||||
@ -8,6 +15,7 @@ const navigationItems = [
|
|||||||
{ href: "/devices", label: "设备", icon: Smartphone },
|
{ href: "/devices", label: "设备", icon: Smartphone },
|
||||||
{ href: "/activity", label: "动态", icon: Activity },
|
{ href: "/activity", label: "动态", icon: Activity },
|
||||||
{ href: "/scan", label: "扫码", icon: ScanLine },
|
{ href: "/scan", label: "扫码", icon: ScanLine },
|
||||||
|
{ href: "/settings", label: "设置", icon: Settings },
|
||||||
];
|
];
|
||||||
|
|
||||||
function isActivePath(currentPath: string, href: string) {
|
function isActivePath(currentPath: string, href: string) {
|
||||||
|
|||||||
120
components/push-test-panel.tsx
Normal file
120
components/push-test-panel.tsx
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
type PushTestPanelProps = {
|
||||||
|
initialSubscriptionCount: number;
|
||||||
|
deviceCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function PushTestPanel({
|
||||||
|
initialSubscriptionCount,
|
||||||
|
deviceCount,
|
||||||
|
}: PushTestPanelProps) {
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
const [subscriptionCount, setSubscriptionCount] = useState(
|
||||||
|
initialSubscriptionCount,
|
||||||
|
);
|
||||||
|
const [notice, setNotice] = useState<string | null>(null);
|
||||||
|
|
||||||
|
async function handleTestPush() {
|
||||||
|
setSubmitting(true);
|
||||||
|
setNotice(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/push/test", {
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
|
||||||
|
const payload = (await response.json().catch(() => null)) as
|
||||||
|
| {
|
||||||
|
error?: string;
|
||||||
|
targetedCount?: number;
|
||||||
|
sentCount?: number;
|
||||||
|
removedCount?: number;
|
||||||
|
failedCount?: number;
|
||||||
|
}
|
||||||
|
| null;
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(payload?.error || "测试推送发送失败。",);
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetedCount = payload?.targetedCount || 0;
|
||||||
|
const removedCount = payload?.removedCount || 0;
|
||||||
|
const sentCount = payload?.sentCount || 0;
|
||||||
|
const failedCount = payload?.failedCount || 0;
|
||||||
|
|
||||||
|
setSubscriptionCount(Math.max(0, targetedCount - removedCount));
|
||||||
|
|
||||||
|
if (targetedCount === 0) {
|
||||||
|
setNotice("当前账号还没有任何开启消息提醒的设备,先在要接收通知的设备上打开消息提醒。\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fragments = [`已向 ${targetedCount} 台设备发出测试通知。`];
|
||||||
|
|
||||||
|
if (sentCount > 0) {
|
||||||
|
fragments.push(`成功送出 ${sentCount} 条。`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removedCount > 0) {
|
||||||
|
fragments.push(`清理了 ${removedCount} 个失效订阅。`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failedCount > 0) {
|
||||||
|
fragments.push(`${failedCount} 台设备本次发送失败。`);
|
||||||
|
}
|
||||||
|
|
||||||
|
setNotice(`${fragments.join(" ")}\n`);
|
||||||
|
} catch (error) {
|
||||||
|
setNotice(
|
||||||
|
error instanceof Error ? error.message : "测试推送发送失败。",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="rounded-[28px] border border-white/80 bg-white/88 p-5 shadow-[0_22px_55px_rgba(53,84,141,0.12)]">
|
||||||
|
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
|
||||||
|
推送测试
|
||||||
|
</p>
|
||||||
|
<h2 className="mt-2 font-[family-name:var(--font-display)] text-2xl text-[var(--ink)]">
|
||||||
|
给当前账号的所有设备发一条测试通知
|
||||||
|
</h2>
|
||||||
|
<p className="mt-3 text-sm leading-7 text-[var(--muted)]">
|
||||||
|
用来确认这位家属账号下已经开启提醒的设备,是否都能正常收到推送。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-4 flex flex-wrap gap-3 text-sm text-[var(--muted)]">
|
||||||
|
<span className="rounded-full border border-[var(--line)] bg-[var(--paper-soft)] px-4 py-2">
|
||||||
|
已连接设备 {deviceCount}
|
||||||
|
</span>
|
||||||
|
<span className="rounded-full border border-[var(--line)] bg-[var(--paper-soft)] px-4 py-2">
|
||||||
|
已开启提醒的设备 {subscriptionCount}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-5 flex flex-wrap gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
void handleTestPush();
|
||||||
|
}}
|
||||||
|
disabled={submitting}
|
||||||
|
className="inline-flex h-12 items-center justify-center rounded-full bg-[var(--copper)] px-5 text-sm font-semibold text-white transition hover:bg-[var(--copper-deep)] disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
|
>
|
||||||
|
{submitting ? "正在发送测试通知" : "发送测试推送"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{notice ? (
|
||||||
|
<p className="mt-4 whitespace-pre-line text-sm leading-7 text-[var(--muted)]">
|
||||||
|
{notice}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -284,3 +284,19 @@ export async function getCaregiverActivity(caregiverId: string) {
|
|||||||
toolCalls,
|
toolCalls,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getCaregiverSettingsOverview(caregiverId: string) {
|
||||||
|
const [deviceCount, pushSubscriptionCount] = await prisma.$transaction([
|
||||||
|
prisma.deviceBinding.count({
|
||||||
|
where: { caregiverId },
|
||||||
|
}),
|
||||||
|
prisma.pushSubscription.count({
|
||||||
|
where: { caregiverId },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
deviceCount,
|
||||||
|
pushSubscriptionCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
143
lib/push.ts
143
lib/push.ts
@ -13,6 +13,20 @@ type SerializablePushSubscription = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type StoredPushSubscription = {
|
||||||
|
endpoint: string;
|
||||||
|
expirationTime: Date | null;
|
||||||
|
p256dh: string;
|
||||||
|
auth: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PushDeliverySummary = {
|
||||||
|
targetedCount: number;
|
||||||
|
sentCount: number;
|
||||||
|
removedCount: number;
|
||||||
|
failedCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
const APP_BASE_URL = (
|
const APP_BASE_URL = (
|
||||||
process.env.NEXT_PUBLIC_APP_URL || "https://digital-human.xn--876a.net"
|
process.env.NEXT_PUBLIC_APP_URL || "https://digital-human.xn--876a.net"
|
||||||
).replace(/\/+$/, "");
|
).replace(/\/+$/, "");
|
||||||
@ -119,6 +133,70 @@ export async function removePushSubscription(input: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deliverPushToSubscriptions(input: {
|
||||||
|
subscriptions: StoredPushSubscription[];
|
||||||
|
payload: string;
|
||||||
|
topic: string;
|
||||||
|
}) {
|
||||||
|
if (input.subscriptions.length === 0) {
|
||||||
|
return {
|
||||||
|
targetedCount: 0,
|
||||||
|
sentCount: 0,
|
||||||
|
removedCount: 0,
|
||||||
|
failedCount: 0,
|
||||||
|
} satisfies PushDeliverySummary;
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureVapidDetails();
|
||||||
|
|
||||||
|
const results = await Promise.all(
|
||||||
|
input.subscriptions.map(async (subscription) => {
|
||||||
|
try {
|
||||||
|
await webpush.sendNotification(
|
||||||
|
{
|
||||||
|
endpoint: subscription.endpoint,
|
||||||
|
expirationTime: subscription.expirationTime?.getTime() || null,
|
||||||
|
keys: {
|
||||||
|
p256dh: subscription.p256dh,
|
||||||
|
auth: subscription.auth,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
input.payload,
|
||||||
|
{
|
||||||
|
TTL: 60 * 30,
|
||||||
|
urgency: "high",
|
||||||
|
topic: input.topic,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return "sent" as const;
|
||||||
|
} catch (error) {
|
||||||
|
const statusCode =
|
||||||
|
error && typeof error === "object" && "statusCode" in error
|
||||||
|
? Number((error as { statusCode?: number }).statusCode)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
if (statusCode === 404 || statusCode === 410) {
|
||||||
|
await prisma.pushSubscription.deleteMany({
|
||||||
|
where: { endpoint: subscription.endpoint },
|
||||||
|
});
|
||||||
|
|
||||||
|
return "removed" as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "failed" as const;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
targetedCount: input.subscriptions.length,
|
||||||
|
sentCount: results.filter((result) => result === "sent").length,
|
||||||
|
removedCount: results.filter((result) => result === "removed").length,
|
||||||
|
failedCount: results.filter((result) => result === "failed").length,
|
||||||
|
} satisfies PushDeliverySummary;
|
||||||
|
}
|
||||||
|
|
||||||
export async function sendIncomingMessagePush(publicId: number) {
|
export async function sendIncomingMessagePush(publicId: number) {
|
||||||
if (!hasPushConfiguration()) {
|
if (!hasPushConfiguration()) {
|
||||||
return;
|
return;
|
||||||
@ -172,37 +250,46 @@ export async function sendIncomingMessagePush(publicId: number) {
|
|||||||
tag: `message-${message.publicId}`,
|
tag: `message-${message.publicId}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await Promise.allSettled(
|
await deliverPushToSubscriptions({
|
||||||
subscriptions.map(async (subscription) => {
|
subscriptions,
|
||||||
try {
|
|
||||||
await webpush.sendNotification(
|
|
||||||
{
|
|
||||||
endpoint: subscription.endpoint,
|
|
||||||
expirationTime: subscription.expirationTime?.getTime() || null,
|
|
||||||
keys: {
|
|
||||||
p256dh: subscription.p256dh,
|
|
||||||
auth: subscription.auth,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
payload,
|
payload,
|
||||||
{
|
|
||||||
TTL: 60 * 30,
|
|
||||||
urgency: "high",
|
|
||||||
topic: `msg-${message.publicId}`,
|
topic: `msg-${message.publicId}`,
|
||||||
},
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
const statusCode =
|
|
||||||
error && typeof error === "object" && "statusCode" in error
|
|
||||||
? Number((error as { statusCode?: number }).statusCode)
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
if (statusCode === 404 || statusCode === 410) {
|
|
||||||
await prisma.pushSubscription.deleteMany({
|
|
||||||
where: { endpoint: subscription.endpoint },
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}),
|
export async function sendCaregiverTestPush(input: {
|
||||||
|
caregiverId: string;
|
||||||
|
caregiverLabel: string;
|
||||||
|
}) {
|
||||||
|
if (!hasPushConfiguration()) {
|
||||||
|
throw new Error("当前环境还没有配置推送服务。",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const subscriptions = await prisma.pushSubscription.findMany({
|
||||||
|
where: {
|
||||||
|
caregiverId: input.caregiverId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
endpoint: true,
|
||||||
|
expirationTime: true,
|
||||||
|
p256dh: true,
|
||||||
|
auth: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const payload = JSON.stringify({
|
||||||
|
title: "安智伴推送测试",
|
||||||
|
body: `${input.caregiverLabel} 账号下的通知链路工作正常。`,
|
||||||
|
url: absoluteUrl("/settings"),
|
||||||
|
icon: absoluteUrl("/pwa/icon-192x192.png"),
|
||||||
|
badge: absoluteUrl("/pwa/badge-96x96.png"),
|
||||||
|
tag: `push-test-${input.caregiverId}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return deliverPushToSubscriptions({
|
||||||
|
subscriptions,
|
||||||
|
payload,
|
||||||
|
topic: `test-${input.caregiverId.slice(0, 20)}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user