feat: integrate operations dashboard summaries
This commit is contained in:
@@ -1,17 +1,23 @@
|
||||
import { Activity, AlertTriangle, BedDouble, CheckCircle2, HeartPulse, Users } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { Activity, AlertTriangle, BedDouble, CheckCircle2, HeartPulse, ListChecks, Users } from "lucide-react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Table } from "@/components/ui/table";
|
||||
import type { AdmissionStatus, BedStatus, IncidentSeverity } from "@/modules/core/types";
|
||||
import type { CareTaskPriority, CareTaskStatus } from "@/modules/care/types";
|
||||
import { CARE_TASK_PRIORITY_LABELS, CARE_TASK_STATUS_LABELS } from "@/modules/care/types";
|
||||
import { BedOccupancyChart, type BedOccupancyDatum } from "@/modules/dashboard/components/BedOccupancyChart";
|
||||
import { INCIDENT_SEVERITY_LABELS, INCIDENT_STATUS_LABELS } from "@/modules/emergency/types";
|
||||
import type { HealthReviewSeverity, HealthReviewStatus } from "@/modules/health/types";
|
||||
import { HEALTH_REVIEW_SEVERITY_LABELS, HEALTH_REVIEW_STATUS_LABELS } from "@/modules/health/types";
|
||||
|
||||
export type DashboardMetric = {
|
||||
label: string;
|
||||
value: string;
|
||||
trend: string;
|
||||
icon: "users" | "beds" | "admissions" | "incidents";
|
||||
icon: "admissions" | "beds" | "care" | "health" | "incidents" | "users";
|
||||
};
|
||||
|
||||
export type DashboardAdmission = {
|
||||
@@ -32,7 +38,32 @@ export type DashboardIncident = {
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
export type DashboardCareTask = {
|
||||
id: string;
|
||||
assigneeLabel: string;
|
||||
elderName: string;
|
||||
priority: CareTaskPriority;
|
||||
scheduledAt: string;
|
||||
status: CareTaskStatus;
|
||||
title: string;
|
||||
};
|
||||
|
||||
export type DashboardHealthReview = {
|
||||
id: string;
|
||||
elderName: string;
|
||||
severity: HealthReviewSeverity;
|
||||
status: HealthReviewStatus;
|
||||
title: string;
|
||||
};
|
||||
|
||||
type DashboardHomeProps = {
|
||||
bedsHref: string;
|
||||
careHref: string;
|
||||
careTasks: DashboardCareTask[];
|
||||
emergencyHref: string;
|
||||
emergencyIncidents: DashboardIncident[];
|
||||
healthHref: string;
|
||||
healthReviews: DashboardHealthReview[];
|
||||
metrics: DashboardMetric[];
|
||||
occupancyData: BedOccupancyDatum[];
|
||||
recentAdmissions: DashboardAdmission[];
|
||||
@@ -40,10 +71,12 @@ type DashboardHomeProps = {
|
||||
};
|
||||
|
||||
const metricIcons: Record<DashboardMetric["icon"], LucideIcon> = {
|
||||
users: Users,
|
||||
beds: BedDouble,
|
||||
admissions: CheckCircle2,
|
||||
beds: BedDouble,
|
||||
care: ListChecks,
|
||||
health: HeartPulse,
|
||||
incidents: HeartPulse,
|
||||
users: Users,
|
||||
};
|
||||
|
||||
const admissionStatusLabels: Record<AdmissionStatus, string> = {
|
||||
@@ -75,6 +108,13 @@ function incidentVariant(severity: IncidentSeverity): "secondary" | "warning" |
|
||||
}
|
||||
|
||||
export function DashboardHome({
|
||||
bedsHref,
|
||||
careHref,
|
||||
careTasks,
|
||||
emergencyHref,
|
||||
emergencyIncidents,
|
||||
healthHref,
|
||||
healthReviews,
|
||||
metrics,
|
||||
occupancyData,
|
||||
recentAdmissions,
|
||||
@@ -84,13 +124,12 @@ export function DashboardHome({
|
||||
<div className="mx-auto flex w-full max-w-7xl flex-col gap-6">
|
||||
<section className="flex flex-col gap-4 border-b pb-5 lg:flex-row lg:items-end lg:justify-between">
|
||||
<div>
|
||||
<Badge variant="success">实时数据</Badge>
|
||||
<h1 className="mt-3 text-3xl font-semibold tracking-normal md:text-4xl">运营看板</h1>
|
||||
<p className="mt-2 text-sm text-muted-foreground">老人、床位、入住和系统故障概况。</p>
|
||||
<h1 className="text-3xl font-semibold tracking-normal md:text-4xl">运营看板</h1>
|
||||
<p className="mt-2 text-sm text-muted-foreground">老人、床位、护理、健康和应急事件概况。</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<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-3">
|
||||
{metrics.map((metric) => {
|
||||
const Icon = metricIcons[metric.icon];
|
||||
|
||||
@@ -109,6 +148,48 @@ export function DashboardHome({
|
||||
})}
|
||||
</section>
|
||||
|
||||
<section className="grid gap-4 xl:grid-cols-3">
|
||||
<QueueCard
|
||||
emptyText="暂无待处理护理任务"
|
||||
href={careHref}
|
||||
items={careTasks.map((task) => ({
|
||||
id: task.id,
|
||||
title: task.title,
|
||||
detail: `${task.elderName || "公共区域"} / ${task.assigneeLabel || "未分配"} / ${formatDateTime(task.scheduledAt)}`,
|
||||
badge: CARE_TASK_STATUS_LABELS[task.status],
|
||||
variant: task.priority === "urgent" ? "danger" : task.priority === "high" ? "warning" : "secondary",
|
||||
meta: CARE_TASK_PRIORITY_LABELS[task.priority],
|
||||
}))}
|
||||
title="护理待办"
|
||||
/>
|
||||
<QueueCard
|
||||
emptyText="暂无待复核健康异常"
|
||||
href={healthHref}
|
||||
items={healthReviews.map((review) => ({
|
||||
id: review.id,
|
||||
title: review.title,
|
||||
detail: review.elderName,
|
||||
badge: HEALTH_REVIEW_STATUS_LABELS[review.status],
|
||||
variant: review.severity === "critical" ? "danger" : "warning",
|
||||
meta: HEALTH_REVIEW_SEVERITY_LABELS[review.severity],
|
||||
}))}
|
||||
title="健康复核"
|
||||
/>
|
||||
<QueueCard
|
||||
emptyText="暂无待处理应急事件"
|
||||
href={emergencyHref}
|
||||
items={emergencyIncidents.map((incident) => ({
|
||||
id: incident.id,
|
||||
title: incident.title,
|
||||
detail: `${incident.source} / ${formatDateTime(incident.createdAt)}`,
|
||||
badge: INCIDENT_STATUS_LABELS[incident.status as keyof typeof INCIDENT_STATUS_LABELS],
|
||||
variant: incidentVariant(incident.severity),
|
||||
meta: INCIDENT_SEVERITY_LABELS[incident.severity],
|
||||
}))}
|
||||
title="安全应急"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section className="grid gap-4 xl:grid-cols-[0.95fr_1.05fr]">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -121,7 +202,9 @@ export function DashboardHome({
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<BedOccupancyChart data={occupancyData} />
|
||||
<Link className="block" href={bedsHref}>
|
||||
<BedOccupancyChart data={occupancyData} />
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -171,7 +254,7 @@ export function DashboardHome({
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>系统故障</CardTitle>
|
||||
<CardTitle>未关闭故障</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
||||
{incidents.map((incident) => (
|
||||
@@ -194,3 +277,52 @@ export function DashboardHome({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type QueueItem = {
|
||||
id: string;
|
||||
badge: string;
|
||||
detail: string;
|
||||
meta: string;
|
||||
title: string;
|
||||
variant: "danger" | "secondary" | "warning";
|
||||
};
|
||||
|
||||
function QueueCard({
|
||||
emptyText,
|
||||
href,
|
||||
items,
|
||||
title,
|
||||
}: {
|
||||
emptyText: string;
|
||||
href: string;
|
||||
items: QueueItem[];
|
||||
title: string;
|
||||
}): React.ReactElement {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between gap-3">
|
||||
<CardTitle>{title}</CardTitle>
|
||||
<Link className="text-sm font-medium text-primary hover:underline" href={href}>
|
||||
进入
|
||||
</Link>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-2">
|
||||
{items.map((item) => (
|
||||
<Link className="rounded-md border p-3 hover:bg-secondary/60" href={href} key={item.id}>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-medium">{item.title}</p>
|
||||
<p className="mt-1 truncate text-xs text-muted-foreground">{item.detail}</p>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<span className="text-xs text-muted-foreground">{item.meta}</span>
|
||||
<Badge variant={item.variant}>{item.badge}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
{items.length === 0 ? <p className="text-sm text-muted-foreground">{emptyText}</p> : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user