feat: complete admission workspace
This commit is contained in:
@@ -191,6 +191,66 @@ export async function getOrdersWithItems(params: {
|
|||||||
|
|
||||||
## Advanced SQL Patterns
|
## Advanced SQL Patterns
|
||||||
|
|
||||||
|
## Scenario: Admission and Bed Mutations
|
||||||
|
|
||||||
|
### 1. Scope / Trigger
|
||||||
|
|
||||||
|
- Trigger: changing elder admission, transfer, discharge, or bed occupancy behavior.
|
||||||
|
- These mutations span `admissions`, `beds`, and `elders`, so partial writes are not acceptable.
|
||||||
|
|
||||||
|
### 2. Signatures
|
||||||
|
|
||||||
|
- `POST /api/admissions`: admit an elder or transfer an active elder to a new available bed.
|
||||||
|
- `PATCH /api/admissions/[id]`: discharge an active admission and release its bed.
|
||||||
|
- Mutations use `getDatabase().transaction(async (transaction) => ...)`.
|
||||||
|
|
||||||
|
### 3. Contracts
|
||||||
|
|
||||||
|
- `POST /api/admissions` request: `{ elderId: string; bedId: string; notes?: string }`.
|
||||||
|
- `PATCH /api/admissions/[id]` request: `{ notes?: string }`.
|
||||||
|
- Success responses include `{ success: true, reason: string, admission }`.
|
||||||
|
- Failure responses include `{ success: false, reason: string }`.
|
||||||
|
|
||||||
|
### 4. Validation & Error Matrix
|
||||||
|
|
||||||
|
- Missing active organization -> `400`.
|
||||||
|
- Missing elder or bed -> `404`.
|
||||||
|
- Target bed not `available` -> `409`.
|
||||||
|
- Discharge target not found -> `404`.
|
||||||
|
- Discharge target not `active` -> `409`.
|
||||||
|
- Insert/update returning no row -> `500`.
|
||||||
|
|
||||||
|
### 5. Good/Base/Bad Cases
|
||||||
|
|
||||||
|
- Good: return a typed mutation result from inside the transaction, then convert it to `jsonFailure` or `jsonSuccess` outside the transaction.
|
||||||
|
- Base: read-only admission lists may use the temporary `readData()` compatibility model while the domain helper layer is being extracted.
|
||||||
|
- Bad: throw ordinary `Error` for business conflicts such as occupied beds, because it loses the structured status/reason contract.
|
||||||
|
|
||||||
|
### 6. Tests Required
|
||||||
|
|
||||||
|
- `pnpm lint`
|
||||||
|
- `pnpm type-check`
|
||||||
|
- `pnpm build`
|
||||||
|
- Integration assertions: occupied-bed admission returns `409`; transfer closes the old active admission and frees the old bed; discharge closes the active admission, frees the bed, and marks the elder discharged.
|
||||||
|
|
||||||
|
### 7. Wrong vs Correct
|
||||||
|
|
||||||
|
#### Wrong
|
||||||
|
|
||||||
|
```ts
|
||||||
|
if (bed.status !== "available") {
|
||||||
|
throw new Error("床位不可分配");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Correct
|
||||||
|
|
||||||
|
```ts
|
||||||
|
if (bed.status !== "available") {
|
||||||
|
return { success: false, reason: "床位不可分配", status: 409 };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### JSON Column Operations
|
### JSON Column Operations
|
||||||
|
|
||||||
When using PostgreSQL JSON/JSONB columns, proper casting is required for JSON functions.
|
When using PostgreSQL JSON/JSONB columns, proper casting is required for JSON functions.
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { BedDouble, DoorOpen, History, Wrench } 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 { getCurrentAuthContext } from "@/modules/core/server/auth";
|
import { getCurrentAuthContext } from "@/modules/core/server/auth";
|
||||||
import { hasPermission } from "@/modules/core/server/permissions";
|
import { hasPermission } from "@/modules/core/server/permissions";
|
||||||
import { readData } from "@/modules/core/server/store";
|
import { readData } from "@/modules/core/server/store";
|
||||||
|
import { BedsWorkspace } from "@/modules/facilities/components/BedsWorkspace";
|
||||||
|
|
||||||
export default async function BedsPage(): Promise<React.ReactElement> {
|
export default async function BedsPage(): Promise<React.ReactElement> {
|
||||||
const context = await getCurrentAuthContext();
|
const context = await getCurrentAuthContext();
|
||||||
@@ -25,211 +22,15 @@ export default async function BedsPage(): Promise<React.ReactElement> {
|
|||||||
const admissions = organizationId
|
const admissions = organizationId
|
||||||
? data.admissions.filter((admission) => admission.organizationId === organizationId)
|
? data.admissions.filter((admission) => admission.organizationId === organizationId)
|
||||||
: data.admissions;
|
: data.admissions;
|
||||||
|
const elders = organizationId ? data.elders.filter((elder) => elder.organizationId === organizationId) : data.elders;
|
||||||
const occupiedBeds = beds.filter((bed) => bed.status === "occupied").length;
|
|
||||||
const maintenanceBeds = beds.filter((bed) => bed.status === "maintenance").length;
|
|
||||||
const occupancyRate = beds.length > 0 ? Math.round((occupiedBeds / beds.length) * 100) : 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto flex w-full max-w-7xl flex-col gap-5">
|
<BedsWorkspace
|
||||||
<section className="flex flex-col gap-4 border-b pb-5 lg:flex-row lg:items-end lg:justify-between">
|
initialAdmissions={admissions}
|
||||||
<div>
|
initialBeds={beds}
|
||||||
<Badge variant="success">Facility + Admission</Badge>
|
initialElders={elders}
|
||||||
<h1 className="mt-3 text-3xl font-semibold tracking-normal">床位房间</h1>
|
initialRooms={rooms}
|
||||||
<p className="mt-2 text-sm text-muted-foreground">房间主数据、床位状态、当前占用和入住历史。</p>
|
permissions={context.permissions}
|
||||||
</div>
|
/>
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0">
|
|
||||||
<div>
|
|
||||||
<CardDescription>房间</CardDescription>
|
|
||||||
<CardTitle className="mt-2 text-3xl">{rooms.length}</CardTitle>
|
|
||||||
</div>
|
|
||||||
<DoorOpen className="size-5 text-primary" aria-hidden="true" />
|
|
||||||
</CardHeader>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0">
|
|
||||||
<div>
|
|
||||||
<CardDescription>床位</CardDescription>
|
|
||||||
<CardTitle className="mt-2 text-3xl">{beds.length}</CardTitle>
|
|
||||||
</div>
|
|
||||||
<BedDouble className="size-5 text-primary" aria-hidden="true" />
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="pt-0">
|
|
||||||
<p className="text-sm text-muted-foreground">占用率 {occupancyRate}%</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card className={maintenanceBeds > 0 ? "border-amber-200 bg-amber-50/70" : undefined}>
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0">
|
|
||||||
<div>
|
|
||||||
<CardDescription className={maintenanceBeds > 0 ? "text-amber-700" : undefined}>维修床位</CardDescription>
|
|
||||||
<CardTitle className={maintenanceBeds > 0 ? "mt-2 text-3xl text-amber-700" : "mt-2 text-3xl"}>
|
|
||||||
{maintenanceBeds}
|
|
||||||
</CardTitle>
|
|
||||||
</div>
|
|
||||||
<Wrench className={maintenanceBeds > 0 ? "size-5 text-amber-700" : "size-5 text-primary"} aria-hidden="true" />
|
|
||||||
</CardHeader>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0">
|
|
||||||
<div>
|
|
||||||
<CardDescription>入住历史</CardDescription>
|
|
||||||
<CardTitle className="mt-2 text-3xl">{admissions.length}</CardTitle>
|
|
||||||
</div>
|
|
||||||
<History className="size-5 text-primary" aria-hidden="true" />
|
|
||||||
</CardHeader>
|
|
||||||
</Card>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="grid gap-4 xl:grid-cols-[0.9fr_1.1fr]">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>房间台账</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="overflow-x-auto rounded-md border">
|
|
||||||
<Table className="w-full min-w-[620px] 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.Row>
|
|
||||||
</Table.Header>
|
|
||||||
<Table.Body className="divide-y bg-card">
|
|
||||||
{rooms.map((room) => (
|
|
||||||
<Table.Row key={room.id}>
|
|
||||||
<Table.Cell className="px-4 py-3 font-medium">{room.name}</Table.Cell>
|
|
||||||
<Table.Cell className="px-4 py-3 text-muted-foreground">{room.code}</Table.Cell>
|
|
||||||
<Table.Cell className="px-4 py-3">{room.capacity}</Table.Cell>
|
|
||||||
<Table.Cell className="px-4 py-3">
|
|
||||||
{room.occupiedBedCount}/{room.bedCount}
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell className="px-4 py-3">
|
|
||||||
<Badge variant={room.status === "active" ? "success" : "danger"}>{room.status}</Badge>
|
|
||||||
</Table.Cell>
|
|
||||||
</Table.Row>
|
|
||||||
))}
|
|
||||||
{rooms.length === 0 ? (
|
|
||||||
<Table.Row>
|
|
||||||
<Table.Cell className="px-4 py-8 text-center text-muted-foreground" colSpan={5}>
|
|
||||||
暂无房间,请通过 /api/facilities/rooms 创建
|
|
||||||
</Table.Cell>
|
|
||||||
</Table.Row>
|
|
||||||
) : null}
|
|
||||||
</Table.Body>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>床位状态</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="overflow-x-auto rounded-md border">
|
|
||||||
<Table className="w-full min-w-[700px] 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.Row>
|
|
||||||
</Table.Header>
|
|
||||||
<Table.Body className="divide-y bg-card">
|
|
||||||
{beds.map((bed) => (
|
|
||||||
<Table.Row key={bed.id}>
|
|
||||||
<Table.Cell className="px-4 py-3">{bed.roomName}</Table.Cell>
|
|
||||||
<Table.Cell className="px-4 py-3 font-medium">{bed.code}</Table.Cell>
|
|
||||||
<Table.Cell className="px-4 py-3">
|
|
||||||
<Badge
|
|
||||||
variant={
|
|
||||||
bed.status === "available"
|
|
||||||
? "success"
|
|
||||||
: bed.status === "occupied"
|
|
||||||
? "secondary"
|
|
||||||
: bed.status === "maintenance"
|
|
||||||
? "warning"
|
|
||||||
: "danger"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{bed.status}
|
|
||||||
</Badge>
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell className="px-4 py-3">{bed.currentElderName ?? "-"}</Table.Cell>
|
|
||||||
<Table.Cell className="px-4 py-3 text-muted-foreground">{bed.notes || "-"}</Table.Cell>
|
|
||||||
</Table.Row>
|
|
||||||
))}
|
|
||||||
{beds.length === 0 ? (
|
|
||||||
<Table.Row>
|
|
||||||
<Table.Cell className="px-4 py-8 text-center text-muted-foreground" colSpan={5}>
|
|
||||||
暂无床位,请通过 /api/facilities/beds 创建
|
|
||||||
</Table.Cell>
|
|
||||||
</Table.Row>
|
|
||||||
) : null}
|
|
||||||
</Table.Body>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>入住与换床历史</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="overflow-x-auto rounded-md border">
|
|
||||||
<Table className="w-full min-w-[820px] 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.Row>
|
|
||||||
</Table.Header>
|
|
||||||
<Table.Body className="divide-y bg-card">
|
|
||||||
{admissions.map((admission) => (
|
|
||||||
<Table.Row key={admission.id}>
|
|
||||||
<Table.Cell className="px-4 py-3 font-medium">{admission.elderName}</Table.Cell>
|
|
||||||
<Table.Cell className="px-4 py-3">
|
|
||||||
{admission.roomName}-{admission.bedCode}
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell className="px-4 py-3">
|
|
||||||
<Badge variant={admission.status === "active" ? "success" : "secondary"}>{admission.status}</Badge>
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell className="px-4 py-3 text-muted-foreground">
|
|
||||||
{new Date(admission.admittedAt).toLocaleString("zh-CN")}
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell className="px-4 py-3 text-muted-foreground">
|
|
||||||
{admission.dischargedAt ? new Date(admission.dischargedAt).toLocaleString("zh-CN") : "-"}
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell className="px-4 py-3 text-muted-foreground">{admission.notes || "-"}</Table.Cell>
|
|
||||||
</Table.Row>
|
|
||||||
))}
|
|
||||||
{admissions.length === 0 ? (
|
|
||||||
<Table.Row>
|
|
||||||
<Table.Cell className="px-4 py-8 text-center text-muted-foreground" colSpan={6}>
|
|
||||||
暂无入住历史
|
|
||||||
</Table.Cell>
|
|
||||||
</Table.Row>
|
|
||||||
) : null}
|
|
||||||
</Table.Body>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,75 @@
|
|||||||
import { DashboardHome } from "@/modules/dashboard/components/DashboardHome";
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
export default function DashboardPage(): React.ReactElement {
|
import { getCurrentAuthContext } from "@/modules/core/server/auth";
|
||||||
return <DashboardHome />;
|
import { readData } from "@/modules/core/server/store";
|
||||||
|
import type { BedStatus } from "@/modules/core/types";
|
||||||
|
import { DashboardHome, type DashboardMetric } from "@/modules/dashboard/components/DashboardHome";
|
||||||
|
|
||||||
|
const BED_STATUSES: BedStatus[] = ["available", "occupied", "maintenance", "disabled"];
|
||||||
|
|
||||||
|
export default async function DashboardPage(): Promise<React.ReactElement> {
|
||||||
|
const context = await getCurrentAuthContext();
|
||||||
|
if (!context) {
|
||||||
|
redirect("/login");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await readData();
|
||||||
|
const organizationId = context.organization?.id;
|
||||||
|
const elders = organizationId ? data.elders.filter((elder) => elder.organizationId === organizationId) : data.elders;
|
||||||
|
const beds = organizationId ? data.beds.filter((bed) => bed.organizationId === organizationId) : data.beds;
|
||||||
|
const admissions = organizationId
|
||||||
|
? data.admissions.filter((admission) => admission.organizationId === organizationId)
|
||||||
|
: data.admissions;
|
||||||
|
const incidents = organizationId
|
||||||
|
? data.incidents.filter((incident) => !incident.organizationId || incident.organizationId === organizationId)
|
||||||
|
: data.incidents;
|
||||||
|
|
||||||
|
const activeElders = elders.filter((elder) => elder.status === "active").length;
|
||||||
|
const pendingElders = elders.filter((elder) => elder.status === "pending").length;
|
||||||
|
const occupiedBeds = beds.filter((bed) => bed.status === "occupied").length;
|
||||||
|
const availableBeds = beds.filter((bed) => bed.status === "available").length;
|
||||||
|
const activeAdmissions = admissions.filter((admission) => admission.status === "active").length;
|
||||||
|
const openIncidents = incidents.filter((incident) => incident.status !== "closed").length;
|
||||||
|
|
||||||
|
const metrics: DashboardMetric[] = [
|
||||||
|
{ label: "在院老人", value: String(activeElders), trend: `待入住 ${pendingElders}`, icon: "users" },
|
||||||
|
{ label: "床位占用", value: `${occupiedBeds}/${beds.length}`, trend: `空闲 ${availableBeds}`, icon: "beds" },
|
||||||
|
{ label: "当前入住", value: String(activeAdmissions), trend: `历史 ${admissions.length}`, icon: "admissions" },
|
||||||
|
{ label: "未关闭故障", value: String(openIncidents), trend: `总计 ${incidents.length}`, icon: "incidents" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const occupancyData = BED_STATUSES.map((status) => ({
|
||||||
|
label: status,
|
||||||
|
count: beds.filter((bed) => bed.status === status).length,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const recentAdmissions = admissions.slice(0, 6).map((admission) => ({
|
||||||
|
id: admission.id,
|
||||||
|
elderName: admission.elderName,
|
||||||
|
bedLabel: `${admission.roomName}-${admission.bedCode}`,
|
||||||
|
status: admission.status,
|
||||||
|
admittedAt: admission.admittedAt,
|
||||||
|
dischargedAt: admission.dischargedAt,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const visibleIncidents = incidents
|
||||||
|
.filter((incident) => incident.status !== "closed")
|
||||||
|
.slice(0, 6)
|
||||||
|
.map((incident) => ({
|
||||||
|
id: incident.id,
|
||||||
|
severity: incident.severity,
|
||||||
|
status: incident.status,
|
||||||
|
title: incident.title,
|
||||||
|
source: incident.source,
|
||||||
|
createdAt: incident.createdAt,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DashboardHome
|
||||||
|
incidents={visibleIncidents}
|
||||||
|
metrics={metrics}
|
||||||
|
occupancyData={occupancyData}
|
||||||
|
recentAdmissions={recentAdmissions}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,5 +11,10 @@ export default async function EldersPage(): Promise<React.ReactElement> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await readData();
|
const data = await readData();
|
||||||
return <EldersClient initialElders={data.elders} permissions={context.permissions} />;
|
const organizationId = context.organization?.id;
|
||||||
|
const elders = organizationId
|
||||||
|
? data.elders.filter((elder) => elder.organizationId === organizationId)
|
||||||
|
: data.elders;
|
||||||
|
|
||||||
|
return <EldersClient initialElders={elders} permissions={context.permissions} />;
|
||||||
}
|
}
|
||||||
|
|||||||
109
app/api/admissions/[id]/route.ts
Normal file
109
app/api/admissions/[id]/route.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
|
||||||
|
import { jsonFailure, jsonSuccess, readJsonBody } from "@/modules/core/server/api";
|
||||||
|
import { recordAuditLog } from "@/modules/core/server/audit";
|
||||||
|
import { requirePermission } from "@/modules/core/server/auth";
|
||||||
|
import { getDatabase } from "@/modules/core/server/db";
|
||||||
|
import { admissions, beds, elders } from "@/modules/core/server/schema";
|
||||||
|
|
||||||
|
type RouteContext = {
|
||||||
|
params: Promise<{
|
||||||
|
id: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AdmissionRow = typeof admissions.$inferSelect;
|
||||||
|
|
||||||
|
type DischargeResult =
|
||||||
|
| {
|
||||||
|
success: true;
|
||||||
|
admission: AdmissionRow;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
success: false;
|
||||||
|
reason: string;
|
||||||
|
status: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
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() : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function PATCH(request: Request, context: RouteContext): Promise<Response> {
|
||||||
|
const { id } = await context.params;
|
||||||
|
const auth = await requirePermission("admission:manage", {
|
||||||
|
action: "admission.discharge",
|
||||||
|
targetType: "admission",
|
||||||
|
targetId: id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!auth.success) {
|
||||||
|
return auth.response;
|
||||||
|
}
|
||||||
|
|
||||||
|
const organizationId = auth.context.organization?.id;
|
||||||
|
if (!organizationId) {
|
||||||
|
return jsonFailure("请选择机构后办理退住", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await readJsonBody(request);
|
||||||
|
const notes = isRecord(body) ? readString(body, "notes") : "";
|
||||||
|
|
||||||
|
const database = getDatabase();
|
||||||
|
const result = await database.transaction(async (transaction): Promise<DischargeResult> => {
|
||||||
|
const admissionRows = await transaction
|
||||||
|
.select()
|
||||||
|
.from(admissions)
|
||||||
|
.where(and(eq(admissions.id, id), eq(admissions.organizationId, organizationId)))
|
||||||
|
.limit(1);
|
||||||
|
const admission = admissionRows[0];
|
||||||
|
if (!admission) {
|
||||||
|
return { success: false, reason: "入住记录不存在", status: 404 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (admission.status !== "active") {
|
||||||
|
return { success: false, reason: "该入住记录已结束", status: 409 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedRows = await transaction
|
||||||
|
.update(admissions)
|
||||||
|
.set({
|
||||||
|
status: "discharged",
|
||||||
|
dischargedAt: new Date(),
|
||||||
|
notes: notes || admission.notes,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(eq(admissions.id, admission.id))
|
||||||
|
.returning();
|
||||||
|
const updated = updatedRows[0];
|
||||||
|
if (!updated) {
|
||||||
|
return { success: false, reason: "退住记录更新失败", status: 500 };
|
||||||
|
}
|
||||||
|
|
||||||
|
await transaction.update(beds).set({ status: "available", updatedAt: new Date() }).where(eq(beds.id, admission.bedId));
|
||||||
|
await transaction.update(elders).set({ status: "discharged", updatedAt: new Date() }).where(eq(elders.id, admission.elderId));
|
||||||
|
|
||||||
|
return { success: true, admission: updated };
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
return jsonFailure(result.reason, result.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
await recordAuditLog({
|
||||||
|
actor: auth.context.account,
|
||||||
|
organizationId,
|
||||||
|
action: "admission.discharge",
|
||||||
|
targetType: "admission",
|
||||||
|
targetId: result.admission.id,
|
||||||
|
result: "success",
|
||||||
|
reason: "办理退住",
|
||||||
|
});
|
||||||
|
|
||||||
|
return jsonSuccess("退住已办理", { admission: result.admission });
|
||||||
|
}
|
||||||
@@ -7,6 +7,19 @@ import { getDatabase } from "@/modules/core/server/db";
|
|||||||
import { admissions, beds, elders } from "@/modules/core/server/schema";
|
import { admissions, beds, elders } from "@/modules/core/server/schema";
|
||||||
import { readData } from "@/modules/core/server/store";
|
import { readData } from "@/modules/core/server/store";
|
||||||
|
|
||||||
|
type AdmissionRow = typeof admissions.$inferSelect;
|
||||||
|
|
||||||
|
type AdmissionMutationResult =
|
||||||
|
| {
|
||||||
|
success: true;
|
||||||
|
admission: AdmissionRow;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
success: false;
|
||||||
|
reason: string;
|
||||||
|
status: number;
|
||||||
|
};
|
||||||
|
|
||||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||||
}
|
}
|
||||||
@@ -62,7 +75,7 @@ export async function POST(request: Request): Promise<Response> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const database = getDatabase();
|
const database = getDatabase();
|
||||||
const admission = await database.transaction(async (transaction) => {
|
const mutation = await database.transaction(async (transaction): Promise<AdmissionMutationResult> => {
|
||||||
const elderRows = await transaction
|
const elderRows = await transaction
|
||||||
.select()
|
.select()
|
||||||
.from(elders)
|
.from(elders)
|
||||||
@@ -70,7 +83,7 @@ export async function POST(request: Request): Promise<Response> {
|
|||||||
.limit(1);
|
.limit(1);
|
||||||
const elder = elderRows[0];
|
const elder = elderRows[0];
|
||||||
if (!elder) {
|
if (!elder) {
|
||||||
throw new Error("老人档案不存在");
|
return { success: false, reason: "老人档案不存在", status: 404 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const bedRows = await transaction
|
const bedRows = await transaction
|
||||||
@@ -80,16 +93,22 @@ export async function POST(request: Request): Promise<Response> {
|
|||||||
.limit(1);
|
.limit(1);
|
||||||
const bed = bedRows[0];
|
const bed = bedRows[0];
|
||||||
if (!bed) {
|
if (!bed) {
|
||||||
throw new Error("床位不存在");
|
return { success: false, reason: "床位不存在", status: 404 };
|
||||||
}
|
}
|
||||||
if (bed.status !== "available") {
|
if (bed.status !== "available") {
|
||||||
throw new Error("床位不可分配");
|
return { success: false, reason: "床位不可分配", status: 409 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeAdmissions = await transaction
|
const activeAdmissions = await transaction
|
||||||
.select()
|
.select()
|
||||||
.from(admissions)
|
.from(admissions)
|
||||||
.where(and(eq(admissions.elderId, elderId), eq(admissions.status, "active")));
|
.where(
|
||||||
|
and(
|
||||||
|
eq(admissions.elderId, elderId),
|
||||||
|
eq(admissions.organizationId, organizationId),
|
||||||
|
eq(admissions.status, "active"),
|
||||||
|
),
|
||||||
|
);
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
activeAdmissions.map((activeAdmission) =>
|
activeAdmissions.map((activeAdmission) =>
|
||||||
transaction
|
transaction
|
||||||
@@ -116,23 +135,27 @@ export async function POST(request: Request): Promise<Response> {
|
|||||||
.returning();
|
.returning();
|
||||||
const row = rows[0];
|
const row = rows[0];
|
||||||
if (!row) {
|
if (!row) {
|
||||||
throw new Error("入住记录创建失败");
|
return { success: false, reason: "入住记录创建失败", status: 500 };
|
||||||
}
|
}
|
||||||
|
|
||||||
await transaction.update(beds).set({ status: "occupied", updatedAt: new Date() }).where(eq(beds.id, bedId));
|
await transaction.update(beds).set({ status: "occupied", updatedAt: new Date() }).where(eq(beds.id, bedId));
|
||||||
await transaction.update(elders).set({ status: "active", updatedAt: new Date() }).where(eq(elders.id, elderId));
|
await transaction.update(elders).set({ status: "active", updatedAt: new Date() }).where(eq(elders.id, elderId));
|
||||||
return row;
|
return { success: true, admission: row };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!mutation.success) {
|
||||||
|
return jsonFailure(mutation.reason, mutation.status);
|
||||||
|
}
|
||||||
|
|
||||||
await recordAuditLog({
|
await recordAuditLog({
|
||||||
actor: auth.context.account,
|
actor: auth.context.account,
|
||||||
organizationId,
|
organizationId,
|
||||||
action: "admission.create",
|
action: "admission.create",
|
||||||
targetType: "admission",
|
targetType: "admission",
|
||||||
targetId: admission.id,
|
targetId: mutation.admission.id,
|
||||||
result: "success",
|
result: "success",
|
||||||
reason: "办理入住/换床",
|
reason: "办理入住/换床",
|
||||||
});
|
});
|
||||||
|
|
||||||
return jsonSuccess("入住记录已创建", { admission }, 201);
|
return jsonSuccess("入住记录已创建", { admission: mutation.admission }, 201);
|
||||||
}
|
}
|
||||||
|
|||||||
28
modules/dashboard/components/BedOccupancyChart.tsx
Normal file
28
modules/dashboard/components/BedOccupancyChart.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Bar, BarChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
|
||||||
|
|
||||||
|
export type BedOccupancyDatum = {
|
||||||
|
label: string;
|
||||||
|
count: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type BedOccupancyChartProps = {
|
||||||
|
data: BedOccupancyDatum[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function BedOccupancyChart({ data }: BedOccupancyChartProps): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<div className="h-64 w-full">
|
||||||
|
<ResponsiveContainer height="100%" width="100%">
|
||||||
|
<BarChart data={data} margin={{ bottom: 8, left: -18, right: 8, top: 8 }}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" vertical={false} />
|
||||||
|
<XAxis dataKey="label" tickLine={false} />
|
||||||
|
<YAxis allowDecimals={false} tickLine={false} />
|
||||||
|
<Tooltip cursor={{ fill: "rgba(15, 23, 42, 0.06)" }} />
|
||||||
|
<Bar dataKey="count" fill="var(--primary)" radius={[6, 6, 0, 0]} />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,167 +1,196 @@
|
|||||||
import {
|
import { Activity, AlertTriangle, BedDouble, CheckCircle2, HeartPulse, Users } from "lucide-react";
|
||||||
Activity,
|
|
||||||
AlertTriangle,
|
|
||||||
BedDouble,
|
|
||||||
CheckCircle2,
|
|
||||||
Clock3,
|
|
||||||
HeartPulse,
|
|
||||||
ShieldAlert,
|
|
||||||
Users,
|
|
||||||
} from "lucide-react";
|
|
||||||
import type { LucideIcon } from "lucide-react";
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Table } from "@/components/ui/table";
|
import { Table } from "@/components/ui/table";
|
||||||
|
import type { AdmissionStatus, BedStatus, IncidentSeverity } from "@/modules/core/types";
|
||||||
|
import { BedOccupancyChart, type BedOccupancyDatum } from "@/modules/dashboard/components/BedOccupancyChart";
|
||||||
|
|
||||||
type Metric = {
|
export type DashboardMetric = {
|
||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
trend: string;
|
trend: string;
|
||||||
icon: LucideIcon;
|
icon: "users" | "beds" | "admissions" | "incidents";
|
||||||
};
|
};
|
||||||
|
|
||||||
const metrics: Metric[] = [
|
export type DashboardAdmission = {
|
||||||
{ label: "在院", value: "126", trend: "+6 本周", icon: Users },
|
id: string;
|
||||||
{ label: "床位", value: "82%", trend: "28 空闲", icon: BedDouble },
|
elderName: string;
|
||||||
{ label: "任务", value: "184", trend: "142 完成", icon: CheckCircle2 },
|
bedLabel: string;
|
||||||
{ label: "异常", value: "9", trend: "3 待复核", icon: HeartPulse },
|
status: AdmissionStatus;
|
||||||
];
|
admittedAt: string;
|
||||||
|
dischargedAt?: string;
|
||||||
|
};
|
||||||
|
|
||||||
const taskRows = [
|
export type DashboardIncident = {
|
||||||
{ name: "晨间生命体征记录", owner: "护理一组", status: "进行中", due: "09:30" },
|
id: string;
|
||||||
{ name: "二楼失能老人巡检", owner: "护理二组", status: "待执行", due: "10:00" },
|
severity: IncidentSeverity;
|
||||||
{ name: "高血压老人复测", owner: "医护组", status: "需复核", due: "10:30" },
|
status: string;
|
||||||
{ name: "晚间护理计划生成", owner: "护理主管", status: "待派发", due: "16:00" },
|
title: string;
|
||||||
];
|
source: string;
|
||||||
|
createdAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
const alerts = [
|
type DashboardHomeProps = {
|
||||||
{ label: "任务逾期", count: "4", tone: "warning" },
|
metrics: DashboardMetric[];
|
||||||
{ label: "设备故障", count: "2", tone: "danger" },
|
occupancyData: BedOccupancyDatum[];
|
||||||
{ label: "应急处理中", count: "1", tone: "danger" },
|
recentAdmissions: DashboardAdmission[];
|
||||||
{ label: "未读通知", count: "18", tone: "success" },
|
incidents: DashboardIncident[];
|
||||||
] as const;
|
};
|
||||||
|
|
||||||
export function DashboardHome(): React.ReactElement {
|
const metricIcons: Record<DashboardMetric["icon"], LucideIcon> = {
|
||||||
|
users: Users,
|
||||||
|
beds: BedDouble,
|
||||||
|
admissions: CheckCircle2,
|
||||||
|
incidents: HeartPulse,
|
||||||
|
};
|
||||||
|
|
||||||
|
const admissionStatusLabels: Record<AdmissionStatus, string> = {
|
||||||
|
active: "在住",
|
||||||
|
transferred: "已换床",
|
||||||
|
discharged: "已退住",
|
||||||
|
};
|
||||||
|
|
||||||
|
const bedStatusLabels: Record<BedStatus, string> = {
|
||||||
|
available: "空闲",
|
||||||
|
occupied: "占用",
|
||||||
|
maintenance: "维修",
|
||||||
|
disabled: "停用",
|
||||||
|
};
|
||||||
|
|
||||||
|
function formatDateTime(value: string): string {
|
||||||
|
return new Date(value).toLocaleString("zh-CN");
|
||||||
|
}
|
||||||
|
|
||||||
|
function incidentVariant(severity: IncidentSeverity): "secondary" | "warning" | "danger" {
|
||||||
|
switch (severity) {
|
||||||
|
case "critical":
|
||||||
|
return "danger";
|
||||||
|
case "warning":
|
||||||
|
return "warning";
|
||||||
|
case "info":
|
||||||
|
return "secondary";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DashboardHome({
|
||||||
|
metrics,
|
||||||
|
occupancyData,
|
||||||
|
recentAdmissions,
|
||||||
|
incidents,
|
||||||
|
}: DashboardHomeProps): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto flex w-full max-w-7xl flex-col gap-6">
|
<div className="mx-auto flex w-full max-w-7xl flex-col gap-6">
|
||||||
<section className="grid gap-5 border-b pb-5 lg:grid-cols-[1.15fr_0.85fr]">
|
<section className="flex flex-col gap-4 border-b pb-5 lg:flex-row lg:items-end lg:justify-between">
|
||||||
<div className="flex flex-col justify-end gap-4">
|
|
||||||
<Badge variant="success">一期</Badge>
|
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-semibold tracking-normal md:text-4xl">运营看板</h1>
|
<Badge variant="success">实时数据</Badge>
|
||||||
<p className="mt-2 text-sm text-muted-foreground">照护、床位、预警一屏处理。</p>
|
<h1 className="mt-3 text-3xl font-semibold tracking-normal md:text-4xl">运营看板</h1>
|
||||||
</div>
|
<p className="mt-2 text-sm text-muted-foreground">老人、床位、入住和系统故障概况。</p>
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
<Button>
|
|
||||||
<Users className="size-4" aria-hidden="true" />
|
|
||||||
老人建档
|
|
||||||
</Button>
|
|
||||||
<Button variant="outline">
|
|
||||||
<ShieldAlert className="size-4" aria-hidden="true" />
|
|
||||||
处理应急事件
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-3 rounded-lg border bg-white/72 p-4 shadow-sm">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium">今日闭环</p>
|
|
||||||
</div>
|
|
||||||
<Activity className="size-5 text-primary" aria-hidden="true" />
|
|
||||||
</div>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{["档案", "床位", "健康", "护理", "应急"].map((item, index) => (
|
|
||||||
<div key={item} className="flex items-center gap-3">
|
|
||||||
<span className="w-9 text-xs font-medium text-muted-foreground">{item}</span>
|
|
||||||
<div className="h-2 flex-1 rounded-full bg-muted">
|
|
||||||
<div
|
|
||||||
className="h-2 rounded-full bg-primary"
|
|
||||||
style={{ width: `${88 - index * 9}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span className="w-14 text-right text-xs text-muted-foreground">
|
|
||||||
{88 - index * 9}%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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-4">
|
||||||
{metrics.map((metric) => (
|
{metrics.map((metric) => {
|
||||||
|
const Icon = metricIcons[metric.icon];
|
||||||
|
|
||||||
|
return (
|
||||||
<Card key={metric.label}>
|
<Card key={metric.label}>
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0">
|
||||||
<CardDescription>{metric.label}</CardDescription>
|
<CardDescription>{metric.label}</CardDescription>
|
||||||
<metric.icon className="size-4 text-primary" aria-hidden="true" />
|
<Icon className="size-4 text-primary" aria-hidden="true" />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<CardTitle className="text-3xl">{metric.value}</CardTitle>
|
<CardTitle className="text-3xl">{metric.value}</CardTitle>
|
||||||
<p className="mt-2 text-xs text-muted-foreground">{metric.trend}</p>
|
<p className="mt-2 text-xs text-muted-foreground">{metric.trend}</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="grid gap-4 xl:grid-cols-[1.25fr_0.75fr]">
|
<section className="grid gap-4 xl:grid-cols-[0.95fr_1.05fr]">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>今日护理任务</CardTitle>
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Activity className="size-5 text-primary" aria-hidden="true" />
|
||||||
|
床位状态分布
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{occupancyData.map((item) => `${bedStatusLabels[item.label as BedStatus] ?? item.label} ${item.count}`).join(" / ")}
|
||||||
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="overflow-hidden rounded-md border">
|
<BedOccupancyChart data={occupancyData} />
|
||||||
<Table className="w-full text-sm">
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>最近入住记录</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="overflow-x-auto rounded-md border">
|
||||||
|
<Table className="w-full min-w-[620px] text-sm">
|
||||||
<Table.Header className="bg-secondary text-left text-xs text-muted-foreground">
|
<Table.Header className="bg-secondary text-left text-xs text-muted-foreground">
|
||||||
<Table.Row>
|
<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 text-right font-medium">截止</Table.Head>
|
<Table.Head className="px-4 py-3 font-medium">时间</Table.Head>
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
</Table.Header>
|
</Table.Header>
|
||||||
<Table.Body className="divide-y bg-card">
|
<Table.Body className="divide-y bg-card">
|
||||||
{taskRows.map((task) => (
|
{recentAdmissions.map((admission) => (
|
||||||
<Table.Row key={task.name}>
|
<Table.Row key={admission.id}>
|
||||||
<Table.Cell className="px-4 py-3 font-medium">{task.name}</Table.Cell>
|
<Table.Cell className="px-4 py-3 font-medium">{admission.elderName}</Table.Cell>
|
||||||
<Table.Cell className="px-4 py-3 text-muted-foreground">{task.owner}</Table.Cell>
|
<Table.Cell className="px-4 py-3 text-muted-foreground">{admission.bedLabel}</Table.Cell>
|
||||||
<Table.Cell className="px-4 py-3">
|
<Table.Cell className="px-4 py-3">
|
||||||
<Badge variant={task.status === "需复核" ? "warning" : "secondary"}>
|
<Badge variant={admission.status === "active" ? "success" : "secondary"}>
|
||||||
{task.status}
|
{admissionStatusLabels[admission.status]}
|
||||||
</Badge>
|
</Badge>
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
<Table.Cell className="px-4 py-3 text-right text-muted-foreground">{task.due}</Table.Cell>
|
<Table.Cell className="px-4 py-3 text-muted-foreground">
|
||||||
|
{formatDateTime(admission.dischargedAt ?? admission.admittedAt)}
|
||||||
|
</Table.Cell>
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
))}
|
))}
|
||||||
|
{recentAdmissions.length === 0 ? (
|
||||||
|
<Table.Row>
|
||||||
|
<Table.Cell className="px-4 py-8 text-center text-muted-foreground" colSpan={4}>
|
||||||
|
暂无入住记录
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
) : null}
|
||||||
</Table.Body>
|
</Table.Body>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</section>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>预警与工单</CardTitle>
|
<CardTitle>系统故障</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-3">
|
<CardContent className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
||||||
{alerts.map((alert) => (
|
{incidents.map((incident) => (
|
||||||
<div key={alert.label} className="flex items-center justify-between rounded-md border p-3">
|
<div key={incident.id} className="flex items-start justify-between gap-3 rounded-md border p-3">
|
||||||
<div className="flex items-center gap-3">
|
<div className="min-w-0">
|
||||||
{alert.tone === "danger" ? (
|
<div className="flex items-center gap-2">
|
||||||
<AlertTriangle className="size-4 text-red-600" aria-hidden="true" />
|
<AlertTriangle className="size-4 shrink-0 text-amber-600" aria-hidden="true" />
|
||||||
) : (
|
<p className="truncate text-sm font-medium">{incident.title}</p>
|
||||||
<Clock3 className="size-4 text-amber-600" aria-hidden="true" />
|
|
||||||
)}
|
|
||||||
<span className="text-sm font-medium">{alert.label}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<Badge variant={alert.tone}>{alert.count}</Badge>
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
|
{incident.source} / {formatDateTime(incident.createdAt)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Badge variant={incidentVariant(incident.severity)}>{incident.status}</Badge>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
{incidents.length === 0 ? <p className="text-sm text-muted-foreground">暂无未关闭故障</p> : null}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -439,7 +439,7 @@ export function EldersClient({ initialElders, permissions }: EldersClientProps):
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
description="保存后会写入服务端数据文件,并生成审计记录。"
|
description="保存后会写入 PostgreSQL,并生成审计记录。"
|
||||||
onClose={closeForm}
|
onClose={closeForm}
|
||||||
open={isFormOpen}
|
open={isFormOpen}
|
||||||
title={isEditing ? "编辑老人档案" : "新增老人档案"}
|
title={isEditing ? "编辑老人档案" : "新增老人档案"}
|
||||||
|
|||||||
578
modules/facilities/components/BedsWorkspace.tsx
Normal file
578
modules/facilities/components/BedsWorkspace.tsx
Normal file
@@ -0,0 +1,578 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { FormEvent, useMemo, useState } from "react";
|
||||||
|
import { BedDouble, DoorOpen, History, MoveRight, UserCheck, UserMinus, Wrench } 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 { Textarea } from "@/components/ui/input";
|
||||||
|
import { Select, type SelectOption } from "@/components/ui/select";
|
||||||
|
import { Table } from "@/components/ui/table";
|
||||||
|
import type { ApiResult } from "@/modules/core/server/api";
|
||||||
|
import type { Admission, BedStatus, FacilityBed, Permission, Room } from "@/modules/core/types";
|
||||||
|
import type { Elder } from "@/modules/elders/types";
|
||||||
|
|
||||||
|
type BedsWorkspaceProps = {
|
||||||
|
initialRooms: Room[];
|
||||||
|
initialBeds: FacilityBed[];
|
||||||
|
initialAdmissions: Admission[];
|
||||||
|
initialElders: Elder[];
|
||||||
|
permissions: Permission[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type WorkspaceTab = "overview" | "operations" | "history";
|
||||||
|
|
||||||
|
const bedStatusLabels: Record<BedStatus, string> = {
|
||||||
|
available: "空闲",
|
||||||
|
occupied: "占用",
|
||||||
|
maintenance: "维修",
|
||||||
|
disabled: "停用",
|
||||||
|
};
|
||||||
|
|
||||||
|
const admissionStatusLabels: Record<Admission["status"], string> = {
|
||||||
|
active: "在住",
|
||||||
|
transferred: "已换床",
|
||||||
|
discharged: "已退住",
|
||||||
|
};
|
||||||
|
|
||||||
|
function formatDateTime(value: string): string {
|
||||||
|
return new Date(value).toLocaleString("zh-CN");
|
||||||
|
}
|
||||||
|
|
||||||
|
function bedBadgeVariant(status: BedStatus): "success" | "secondary" | "warning" | "danger" {
|
||||||
|
switch (status) {
|
||||||
|
case "available":
|
||||||
|
return "success";
|
||||||
|
case "occupied":
|
||||||
|
return "secondary";
|
||||||
|
case "maintenance":
|
||||||
|
return "warning";
|
||||||
|
case "disabled":
|
||||||
|
return "danger";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function admissionBedLabel(admission: Admission): string {
|
||||||
|
return `${admission.roomName}-${admission.bedCode}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeSelectOptions(items: Array<{ id: string; label: string }>, emptyLabel: string): SelectOption[] {
|
||||||
|
return [
|
||||||
|
{ value: "", label: emptyLabel, disabled: true },
|
||||||
|
...items.map((item) => ({ value: item.id, label: item.label })),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BedsWorkspace({
|
||||||
|
initialRooms,
|
||||||
|
initialBeds,
|
||||||
|
initialAdmissions,
|
||||||
|
initialElders,
|
||||||
|
permissions,
|
||||||
|
}: BedsWorkspaceProps): React.ReactElement {
|
||||||
|
const [rooms, setRooms] = useState(initialRooms);
|
||||||
|
const [beds, setBeds] = useState(initialBeds);
|
||||||
|
const [admissions, setAdmissions] = useState(initialAdmissions);
|
||||||
|
const [elders, setElders] = useState(initialElders);
|
||||||
|
const [activeTab, setActiveTab] = useState<WorkspaceTab>("overview");
|
||||||
|
const [message, setMessage] = useState("");
|
||||||
|
const [isPending, setIsPending] = useState(false);
|
||||||
|
const [admitElderId, setAdmitElderId] = useState("");
|
||||||
|
const [admitBedId, setAdmitBedId] = useState("");
|
||||||
|
const [admitNotes, setAdmitNotes] = useState("");
|
||||||
|
const [transferAdmissionId, setTransferAdmissionId] = useState("");
|
||||||
|
const [transferBedId, setTransferBedId] = useState("");
|
||||||
|
const [transferNotes, setTransferNotes] = useState("");
|
||||||
|
const [dischargeAdmissionId, setDischargeAdmissionId] = useState("");
|
||||||
|
const [dischargeNotes, setDischargeNotes] = useState("");
|
||||||
|
|
||||||
|
const canManageAdmissions = permissions.includes("admission:manage");
|
||||||
|
const activeAdmissions = useMemo(() => admissions.filter((admission) => admission.status === "active"), [admissions]);
|
||||||
|
const activeElderIds = useMemo(() => new Set(activeAdmissions.map((admission) => admission.elderId)), [activeAdmissions]);
|
||||||
|
const availableBeds = useMemo(() => beds.filter((bed) => bed.status === "available"), [beds]);
|
||||||
|
const occupiedBeds = beds.filter((bed) => bed.status === "occupied").length;
|
||||||
|
const maintenanceBeds = beds.filter((bed) => bed.status === "maintenance").length;
|
||||||
|
const occupancyRate = beds.length > 0 ? Math.round((occupiedBeds / beds.length) * 100) : 0;
|
||||||
|
const admissionEligibleElders = elders.filter((elder) => !activeElderIds.has(elder.id));
|
||||||
|
|
||||||
|
const elderOptions = makeSelectOptions(
|
||||||
|
admissionEligibleElders.map((elder) => ({ id: elder.id, label: `${elder.name} / ${elder.phone}` })),
|
||||||
|
"选择老人",
|
||||||
|
);
|
||||||
|
const bedOptions = makeSelectOptions(
|
||||||
|
availableBeds.map((bed) => ({ id: bed.id, label: `${bed.roomName}-${bed.code}` })),
|
||||||
|
"选择空闲床位",
|
||||||
|
);
|
||||||
|
const activeAdmissionOptions = makeSelectOptions(
|
||||||
|
activeAdmissions.map((admission) => ({
|
||||||
|
id: admission.id,
|
||||||
|
label: `${admission.elderName} / ${admissionBedLabel(admission)}`,
|
||||||
|
})),
|
||||||
|
"选择在住记录",
|
||||||
|
);
|
||||||
|
|
||||||
|
async function refreshData(): Promise<void> {
|
||||||
|
const [eldersResponse, roomsResponse, bedsResponse, admissionsResponse] = await Promise.all([
|
||||||
|
fetch("/api/elders"),
|
||||||
|
fetch("/api/facilities/rooms"),
|
||||||
|
fetch("/api/facilities/beds"),
|
||||||
|
fetch("/api/admissions"),
|
||||||
|
]);
|
||||||
|
const [eldersResult, roomsResult, bedsResult, admissionsResult] = (await Promise.all([
|
||||||
|
eldersResponse.json(),
|
||||||
|
roomsResponse.json(),
|
||||||
|
bedsResponse.json(),
|
||||||
|
admissionsResponse.json(),
|
||||||
|
])) as [
|
||||||
|
ApiResult<{ elders: Elder[] }>,
|
||||||
|
ApiResult<{ rooms: Room[] }>,
|
||||||
|
ApiResult<{ beds: FacilityBed[] }>,
|
||||||
|
ApiResult<{ admissions: Admission[] }>,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (eldersResult.success) {
|
||||||
|
setElders(eldersResult.elders);
|
||||||
|
}
|
||||||
|
if (roomsResult.success) {
|
||||||
|
setRooms(roomsResult.rooms);
|
||||||
|
}
|
||||||
|
if (bedsResult.success) {
|
||||||
|
setBeds(bedsResult.beds);
|
||||||
|
}
|
||||||
|
if (admissionsResult.success) {
|
||||||
|
setAdmissions(admissionsResult.admissions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitAdmission(event: FormEvent<HTMLFormElement>): Promise<void> {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!canManageAdmissions) {
|
||||||
|
setMessage("当前角色无权办理入住");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!admitElderId || !admitBedId) {
|
||||||
|
setMessage("请选择老人和空闲床位");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsPending(true);
|
||||||
|
setMessage("");
|
||||||
|
const response = await fetch("/api/admissions", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: JSON.stringify({ elderId: admitElderId, bedId: admitBedId, notes: admitNotes }),
|
||||||
|
});
|
||||||
|
const result = (await response.json()) as ApiResult<{ admission: Admission }>;
|
||||||
|
setIsPending(false);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
setMessage(result.reason);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setAdmitElderId("");
|
||||||
|
setAdmitBedId("");
|
||||||
|
setAdmitNotes("");
|
||||||
|
await refreshData();
|
||||||
|
setMessage(result.reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitTransfer(event: FormEvent<HTMLFormElement>): Promise<void> {
|
||||||
|
event.preventDefault();
|
||||||
|
const admission = activeAdmissions.find((item) => item.id === transferAdmissionId);
|
||||||
|
if (!canManageAdmissions) {
|
||||||
|
setMessage("当前角色无权办理换床");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!admission || !transferBedId) {
|
||||||
|
setMessage("请选择在住记录和新床位");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsPending(true);
|
||||||
|
setMessage("");
|
||||||
|
const response = await fetch("/api/admissions", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: JSON.stringify({ elderId: admission.elderId, bedId: transferBedId, notes: transferNotes || "办理换床" }),
|
||||||
|
});
|
||||||
|
const result = (await response.json()) as ApiResult<{ admission: Admission }>;
|
||||||
|
setIsPending(false);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
setMessage(result.reason);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTransferAdmissionId("");
|
||||||
|
setTransferBedId("");
|
||||||
|
setTransferNotes("");
|
||||||
|
await refreshData();
|
||||||
|
setMessage("换床已办理");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitDischarge(event: FormEvent<HTMLFormElement>): Promise<void> {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!canManageAdmissions) {
|
||||||
|
setMessage("当前角色无权办理退住");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!dischargeAdmissionId) {
|
||||||
|
setMessage("请选择在住记录");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsPending(true);
|
||||||
|
setMessage("");
|
||||||
|
const response = await fetch(`/api/admissions/${dischargeAdmissionId}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: JSON.stringify({ notes: dischargeNotes }),
|
||||||
|
});
|
||||||
|
const result = (await response.json()) as ApiResult<{ admission: Admission }>;
|
||||||
|
setIsPending(false);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
setMessage(result.reason);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDischargeAdmissionId("");
|
||||||
|
setDischargeNotes("");
|
||||||
|
await refreshData();
|
||||||
|
setMessage(result.reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto flex w-full max-w-7xl flex-col gap-5">
|
||||||
|
<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">床位房间</h1>
|
||||||
|
<p className="mt-2 text-sm text-muted-foreground">查看占用、办理入住、换床和退住。</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2" role="tablist" aria-label="床位工作区">
|
||||||
|
{[
|
||||||
|
{ value: "overview", label: "概览" },
|
||||||
|
{ value: "operations", label: "办理" },
|
||||||
|
{ value: "history", label: "记录" },
|
||||||
|
].map((tab) => (
|
||||||
|
<Button
|
||||||
|
aria-selected={activeTab === tab.value}
|
||||||
|
key={tab.value}
|
||||||
|
onClick={() => setActiveTab(tab.value as WorkspaceTab)}
|
||||||
|
type="button"
|
||||||
|
variant={activeTab === tab.value ? "default" : "outline"}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{message ? (
|
||||||
|
<p className="rounded-md bg-secondary px-3 py-2 text-sm text-secondary-foreground" role="status">
|
||||||
|
{message}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{activeTab === "overview" ? (
|
||||||
|
<>
|
||||||
|
<section className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0">
|
||||||
|
<div>
|
||||||
|
<CardDescription>房间</CardDescription>
|
||||||
|
<CardTitle className="mt-2 text-3xl">{rooms.length}</CardTitle>
|
||||||
|
</div>
|
||||||
|
<DoorOpen className="size-5 text-primary" aria-hidden="true" />
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0">
|
||||||
|
<div>
|
||||||
|
<CardDescription>床位</CardDescription>
|
||||||
|
<CardTitle className="mt-2 text-3xl">{beds.length}</CardTitle>
|
||||||
|
</div>
|
||||||
|
<BedDouble className="size-5 text-primary" aria-hidden="true" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="pt-0">
|
||||||
|
<p className="text-sm text-muted-foreground">占用率 {occupancyRate}%</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card className={maintenanceBeds > 0 ? "border-amber-200 bg-amber-50/70" : undefined}>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0">
|
||||||
|
<div>
|
||||||
|
<CardDescription className={maintenanceBeds > 0 ? "text-amber-700" : undefined}>维修床位</CardDescription>
|
||||||
|
<CardTitle className={maintenanceBeds > 0 ? "mt-2 text-3xl text-amber-700" : "mt-2 text-3xl"}>
|
||||||
|
{maintenanceBeds}
|
||||||
|
</CardTitle>
|
||||||
|
</div>
|
||||||
|
<Wrench className={maintenanceBeds > 0 ? "size-5 text-amber-700" : "size-5 text-primary"} aria-hidden="true" />
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0">
|
||||||
|
<div>
|
||||||
|
<CardDescription>在住</CardDescription>
|
||||||
|
<CardTitle className="mt-2 text-3xl">{activeAdmissions.length}</CardTitle>
|
||||||
|
</div>
|
||||||
|
<History className="size-5 text-primary" aria-hidden="true" />
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="grid gap-4 xl:grid-cols-[0.9fr_1.1fr]">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>房间台账</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="overflow-x-auto rounded-md border">
|
||||||
|
<Table className="w-full min-w-[620px] 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.Row>
|
||||||
|
</Table.Header>
|
||||||
|
<Table.Body className="divide-y bg-card">
|
||||||
|
{rooms.map((room) => (
|
||||||
|
<Table.Row key={room.id}>
|
||||||
|
<Table.Cell className="px-4 py-3 font-medium">{room.name}</Table.Cell>
|
||||||
|
<Table.Cell className="px-4 py-3 text-muted-foreground">{room.code}</Table.Cell>
|
||||||
|
<Table.Cell className="px-4 py-3">{room.capacity}</Table.Cell>
|
||||||
|
<Table.Cell className="px-4 py-3">
|
||||||
|
{room.occupiedBedCount}/{room.bedCount}
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell className="px-4 py-3">
|
||||||
|
<Badge variant={room.status === "active" ? "success" : "danger"}>{room.status}</Badge>
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
))}
|
||||||
|
{rooms.length === 0 ? (
|
||||||
|
<Table.Row>
|
||||||
|
<Table.Cell className="px-4 py-8 text-center text-muted-foreground" colSpan={5}>
|
||||||
|
暂无房间主数据
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
) : null}
|
||||||
|
</Table.Body>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>床位状态</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="overflow-x-auto rounded-md border">
|
||||||
|
<Table className="w-full min-w-[700px] 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.Row>
|
||||||
|
</Table.Header>
|
||||||
|
<Table.Body className="divide-y bg-card">
|
||||||
|
{beds.map((bed) => (
|
||||||
|
<Table.Row key={bed.id}>
|
||||||
|
<Table.Cell className="px-4 py-3">{bed.roomName}</Table.Cell>
|
||||||
|
<Table.Cell className="px-4 py-3 font-medium">{bed.code}</Table.Cell>
|
||||||
|
<Table.Cell className="px-4 py-3">
|
||||||
|
<Badge variant={bedBadgeVariant(bed.status)}>{bedStatusLabels[bed.status]}</Badge>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell className="px-4 py-3">{bed.currentElderName ?? "-"}</Table.Cell>
|
||||||
|
<Table.Cell className="px-4 py-3 text-muted-foreground">{bed.notes || "-"}</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
))}
|
||||||
|
{beds.length === 0 ? (
|
||||||
|
<Table.Row>
|
||||||
|
<Table.Cell className="px-4 py-8 text-center text-muted-foreground" colSpan={5}>
|
||||||
|
暂无床位主数据
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
) : null}
|
||||||
|
</Table.Body>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{activeTab === "operations" ? (
|
||||||
|
<section className="grid gap-4 xl:grid-cols-3">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<UserCheck className="size-5 text-primary" aria-hidden="true" />
|
||||||
|
办理入住
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>选择未在住老人和空闲床位。</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form className="grid gap-3" onSubmit={(event) => void submitAdmission(event)}>
|
||||||
|
<Select
|
||||||
|
aria-label="入住老人"
|
||||||
|
disabled={!canManageAdmissions || isPending}
|
||||||
|
onValueChange={setAdmitElderId}
|
||||||
|
options={elderOptions}
|
||||||
|
value={admitElderId}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
aria-label="入住房间床位"
|
||||||
|
disabled={!canManageAdmissions || isPending}
|
||||||
|
onValueChange={setAdmitBedId}
|
||||||
|
options={bedOptions}
|
||||||
|
value={admitBedId}
|
||||||
|
/>
|
||||||
|
<Textarea
|
||||||
|
aria-label="入住备注"
|
||||||
|
disabled={!canManageAdmissions || isPending}
|
||||||
|
onChange={(event) => setAdmitNotes(event.target.value)}
|
||||||
|
placeholder="备注"
|
||||||
|
value={admitNotes}
|
||||||
|
/>
|
||||||
|
<Button disabled={!canManageAdmissions || isPending} type="submit">
|
||||||
|
<UserCheck className="size-4" aria-hidden="true" />
|
||||||
|
办理入住
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<MoveRight className="size-5 text-primary" aria-hidden="true" />
|
||||||
|
办理换床
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>换床会释放原床位并占用新床位。</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form className="grid gap-3" onSubmit={(event) => void submitTransfer(event)}>
|
||||||
|
<Select
|
||||||
|
aria-label="换床入住记录"
|
||||||
|
disabled={!canManageAdmissions || isPending}
|
||||||
|
onValueChange={setTransferAdmissionId}
|
||||||
|
options={activeAdmissionOptions}
|
||||||
|
value={transferAdmissionId}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
aria-label="换床新床位"
|
||||||
|
disabled={!canManageAdmissions || isPending}
|
||||||
|
onValueChange={setTransferBedId}
|
||||||
|
options={bedOptions}
|
||||||
|
value={transferBedId}
|
||||||
|
/>
|
||||||
|
<Textarea
|
||||||
|
aria-label="换床备注"
|
||||||
|
disabled={!canManageAdmissions || isPending}
|
||||||
|
onChange={(event) => setTransferNotes(event.target.value)}
|
||||||
|
placeholder="备注"
|
||||||
|
value={transferNotes}
|
||||||
|
/>
|
||||||
|
<Button disabled={!canManageAdmissions || isPending} type="submit" variant="outline">
|
||||||
|
<MoveRight className="size-4" aria-hidden="true" />
|
||||||
|
办理换床
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<UserMinus className="size-5 text-primary" aria-hidden="true" />
|
||||||
|
办理退住
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>退住会关闭入住记录并释放床位。</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form className="grid gap-3" onSubmit={(event) => void submitDischarge(event)}>
|
||||||
|
<Select
|
||||||
|
aria-label="退住入住记录"
|
||||||
|
disabled={!canManageAdmissions || isPending}
|
||||||
|
onValueChange={setDischargeAdmissionId}
|
||||||
|
options={activeAdmissionOptions}
|
||||||
|
value={dischargeAdmissionId}
|
||||||
|
/>
|
||||||
|
<Textarea
|
||||||
|
aria-label="退住备注"
|
||||||
|
disabled={!canManageAdmissions || isPending}
|
||||||
|
onChange={(event) => setDischargeNotes(event.target.value)}
|
||||||
|
placeholder="备注"
|
||||||
|
value={dischargeNotes}
|
||||||
|
/>
|
||||||
|
<Button disabled={!canManageAdmissions || isPending} type="submit" variant="destructive">
|
||||||
|
<UserMinus className="size-4" aria-hidden="true" />
|
||||||
|
办理退住
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</section>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{activeTab === "history" ? (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>入住、换床与退住记录</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="overflow-x-auto rounded-md border">
|
||||||
|
<Table className="w-full min-w-[820px] 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.Row>
|
||||||
|
</Table.Header>
|
||||||
|
<Table.Body className="divide-y bg-card">
|
||||||
|
{admissions.map((admission) => (
|
||||||
|
<Table.Row key={admission.id}>
|
||||||
|
<Table.Cell className="px-4 py-3 font-medium">{admission.elderName}</Table.Cell>
|
||||||
|
<Table.Cell className="px-4 py-3">{admissionBedLabel(admission)}</Table.Cell>
|
||||||
|
<Table.Cell className="px-4 py-3">
|
||||||
|
<Badge variant={admission.status === "active" ? "success" : "secondary"}>
|
||||||
|
{admissionStatusLabels[admission.status]}
|
||||||
|
</Badge>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell className="px-4 py-3 text-muted-foreground">{formatDateTime(admission.admittedAt)}</Table.Cell>
|
||||||
|
<Table.Cell className="px-4 py-3 text-muted-foreground">
|
||||||
|
{admission.dischargedAt ? formatDateTime(admission.dischargedAt) : "-"}
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell className="px-4 py-3 text-muted-foreground">{admission.notes || "-"}</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
))}
|
||||||
|
{admissions.length === 0 ? (
|
||||||
|
<Table.Row>
|
||||||
|
<Table.Cell className="px-4 py-8 text-center text-muted-foreground" colSpan={6}>
|
||||||
|
暂无入住记录
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
) : null}
|
||||||
|
</Table.Body>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
ClipboardCheck,
|
ClipboardCheck,
|
||||||
Globe2,
|
Globe2,
|
||||||
HeartPulse,
|
HeartPulse,
|
||||||
Home,
|
|
||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
ListChecks,
|
ListChecks,
|
||||||
Radio,
|
Radio,
|
||||||
@@ -19,7 +18,6 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type { LucideIcon } from "lucide-react";
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import type { Permission, PublicAccount } from "@/modules/core/types";
|
import type { Permission, PublicAccount } from "@/modules/core/types";
|
||||||
import { SignOutButton } from "@/modules/auth/components/SignOutButton";
|
import { SignOutButton } from "@/modules/auth/components/SignOutButton";
|
||||||
import { UserAvatar } from "@/modules/shared/components/UserAvatar";
|
import { UserAvatar } from "@/modules/shared/components/UserAvatar";
|
||||||
@@ -149,7 +147,6 @@ type AppShellProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function AppShell({ children, account, permissions }: AppShellProps): React.ReactElement {
|
export function AppShell({ children, account, permissions }: AppShellProps): React.ReactElement {
|
||||||
const canCreateElder = permissions.includes("elder:create");
|
|
||||||
const visibleNavGroups = navGroups
|
const visibleNavGroups = navGroups
|
||||||
.map((group) => ({
|
.map((group) => ({
|
||||||
...group,
|
...group,
|
||||||
@@ -216,10 +213,6 @@ export function AppShell({ children, account, permissions }: AppShellProps): Rea
|
|||||||
</div>
|
</div>
|
||||||
<UserAvatar avatarUrl={account.avatarUrl} name={account.name} size="sm" />
|
<UserAvatar avatarUrl={account.avatarUrl} name={account.name} size="sm" />
|
||||||
</div>
|
</div>
|
||||||
<Button size="sm" disabled={!canCreateElder}>
|
|
||||||
<Home className="size-4" aria-hidden="true" />
|
|
||||||
入住
|
|
||||||
</Button>
|
|
||||||
<SignOutButton />
|
<SignOutButton />
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
"postgres": "^3.4.9",
|
"postgres": "^3.4.9",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
"recharts": "^3.3.0",
|
||||||
"tailwind-merge": "^3.0.0"
|
"tailwind-merge": "^3.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
288
pnpm-lock.yaml
generated
288
pnpm-lock.yaml
generated
@@ -41,6 +41,9 @@ importers:
|
|||||||
react-dom:
|
react-dom:
|
||||||
specifier: ^19.0.0
|
specifier: ^19.0.0
|
||||||
version: 19.2.7(react@19.2.7)
|
version: 19.2.7(react@19.2.7)
|
||||||
|
recharts:
|
||||||
|
specifier: ^3.3.0
|
||||||
|
version: 3.9.1(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@16.13.1)(react@19.2.7)(redux@5.0.1)
|
||||||
tailwind-merge:
|
tailwind-merge:
|
||||||
specifier: ^3.0.0
|
specifier: ^3.0.0
|
||||||
version: 3.6.0
|
version: 3.6.0
|
||||||
@@ -931,6 +934,17 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@reduxjs/toolkit@2.12.0':
|
||||||
|
resolution: {integrity: sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.9.0 || ^17.0.0 || ^18 || ^19
|
||||||
|
react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
react-redux:
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@rtsao/scc@1.1.0':
|
'@rtsao/scc@1.1.0':
|
||||||
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
|
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
|
||||||
|
|
||||||
@@ -968,6 +982,12 @@ packages:
|
|||||||
'@shikijs/vscode-textmate@10.0.2':
|
'@shikijs/vscode-textmate@10.0.2':
|
||||||
resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
|
resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
|
||||||
|
|
||||||
|
'@standard-schema/spec@1.1.0':
|
||||||
|
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
|
||||||
|
|
||||||
|
'@standard-schema/utils@0.3.0':
|
||||||
|
resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
|
||||||
|
|
||||||
'@swc/helpers@0.5.15':
|
'@swc/helpers@0.5.15':
|
||||||
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
|
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
|
||||||
|
|
||||||
@@ -1066,6 +1086,33 @@ packages:
|
|||||||
'@tybys/wasm-util@0.10.3':
|
'@tybys/wasm-util@0.10.3':
|
||||||
resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==}
|
resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==}
|
||||||
|
|
||||||
|
'@types/d3-array@3.2.2':
|
||||||
|
resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==}
|
||||||
|
|
||||||
|
'@types/d3-color@3.1.3':
|
||||||
|
resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==}
|
||||||
|
|
||||||
|
'@types/d3-ease@3.0.2':
|
||||||
|
resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==}
|
||||||
|
|
||||||
|
'@types/d3-interpolate@3.0.4':
|
||||||
|
resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==}
|
||||||
|
|
||||||
|
'@types/d3-path@3.1.1':
|
||||||
|
resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==}
|
||||||
|
|
||||||
|
'@types/d3-scale@4.0.9':
|
||||||
|
resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==}
|
||||||
|
|
||||||
|
'@types/d3-shape@3.1.8':
|
||||||
|
resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==}
|
||||||
|
|
||||||
|
'@types/d3-time@3.0.4':
|
||||||
|
resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==}
|
||||||
|
|
||||||
|
'@types/d3-timer@3.0.2':
|
||||||
|
resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
|
||||||
|
|
||||||
'@types/estree@1.0.9':
|
'@types/estree@1.0.9':
|
||||||
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
|
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
|
||||||
|
|
||||||
@@ -1095,6 +1142,9 @@ packages:
|
|||||||
'@types/unist@3.0.3':
|
'@types/unist@3.0.3':
|
||||||
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
|
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
|
||||||
|
|
||||||
|
'@types/use-sync-external-store@0.0.6':
|
||||||
|
resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==}
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@8.62.1':
|
'@typescript-eslint/eslint-plugin@8.62.1':
|
||||||
resolution: {integrity: sha512-4EQM77WgVNxj7OkL/5b/D/xZsw00G577+UriYTC7JF5opcF3T2AuoeY7ueLaZgSVjSgCS6yOAJB5bRGLPSJUzA==}
|
resolution: {integrity: sha512-4EQM77WgVNxj7OkL/5b/D/xZsw00G577+UriYTC7JF5opcF3T2AuoeY7ueLaZgSVjSgCS6yOAJB5bRGLPSJUzA==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
@@ -1425,6 +1475,50 @@ packages:
|
|||||||
csstype@3.2.3:
|
csstype@3.2.3:
|
||||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||||
|
|
||||||
|
d3-array@3.2.4:
|
||||||
|
resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
d3-color@3.1.0:
|
||||||
|
resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
d3-ease@3.0.1:
|
||||||
|
resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
d3-format@3.1.2:
|
||||||
|
resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
d3-interpolate@3.0.1:
|
||||||
|
resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
d3-path@3.1.0:
|
||||||
|
resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
d3-scale@4.0.2:
|
||||||
|
resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
d3-shape@3.2.0:
|
||||||
|
resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
d3-time-format@4.1.0:
|
||||||
|
resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
d3-time@3.1.0:
|
||||||
|
resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
d3-timer@3.0.1:
|
||||||
|
resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
damerau-levenshtein@1.0.8:
|
damerau-levenshtein@1.0.8:
|
||||||
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
|
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
|
||||||
|
|
||||||
@@ -1463,6 +1557,9 @@ packages:
|
|||||||
supports-color:
|
supports-color:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
decimal.js-light@2.5.1:
|
||||||
|
resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
|
||||||
|
|
||||||
deep-is@0.1.4:
|
deep-is@0.1.4:
|
||||||
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
||||||
|
|
||||||
@@ -1632,6 +1729,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==}
|
resolution: {integrity: sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
es-toolkit@1.49.0:
|
||||||
|
resolution: {integrity: sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g==}
|
||||||
|
|
||||||
esbuild@0.18.20:
|
esbuild@0.18.20:
|
||||||
resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==}
|
resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -1771,6 +1871,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
|
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
eventemitter3@5.0.4:
|
||||||
|
resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==}
|
||||||
|
|
||||||
fast-deep-equal@3.1.3:
|
fast-deep-equal@3.1.3:
|
||||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||||
|
|
||||||
@@ -1934,6 +2037,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
|
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
|
||||||
engines: {node: '>= 4'}
|
engines: {node: '>= 4'}
|
||||||
|
|
||||||
|
immer@11.1.9:
|
||||||
|
resolution: {integrity: sha512-sc/z0Cyti70bZa0ZU4sWfAElfovFb9Ni8tArJZLuklYWxegPiK3pDOql1Rq5H0FIRAW9LSQRG6OX4KqBldbhBA==}
|
||||||
|
|
||||||
import-fresh@3.3.1:
|
import-fresh@3.3.1:
|
||||||
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
|
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -1946,6 +2052,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
|
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
internmap@2.0.3:
|
||||||
|
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
is-array-buffer@3.0.5:
|
is-array-buffer@3.0.5:
|
||||||
resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==}
|
resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -2422,10 +2532,38 @@ packages:
|
|||||||
react-is@16.13.1:
|
react-is@16.13.1:
|
||||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||||
|
|
||||||
|
react-redux@9.3.0:
|
||||||
|
resolution: {integrity: sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': ^18.2.25 || ^19
|
||||||
|
react: ^18.0 || ^19
|
||||||
|
redux: ^5.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
redux:
|
||||||
|
optional: true
|
||||||
|
|
||||||
react@19.2.7:
|
react@19.2.7:
|
||||||
resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==}
|
resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
recharts@3.9.1:
|
||||||
|
resolution: {integrity: sha512-WMcwlXcB7l+BbxiEdyClkG+1sxrMHNZpzT577LEvU4+rXPd8oTAy1wXk72hnk2KOOmxuLvw3z5DtXT7HEAydtg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
|
||||||
|
redux-thunk@3.1.0:
|
||||||
|
resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==}
|
||||||
|
peerDependencies:
|
||||||
|
redux: ^5.0.0
|
||||||
|
|
||||||
|
redux@5.0.1:
|
||||||
|
resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==}
|
||||||
|
|
||||||
reflect.getprototypeof@1.0.10:
|
reflect.getprototypeof@1.0.10:
|
||||||
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
|
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -2619,6 +2757,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==}
|
resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
tiny-invariant@1.3.3:
|
||||||
|
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
|
||||||
|
|
||||||
tinyglobby@0.2.17:
|
tinyglobby@0.2.17:
|
||||||
resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
|
resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
@@ -2718,6 +2859,9 @@ packages:
|
|||||||
vfile@6.0.3:
|
vfile@6.0.3:
|
||||||
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
|
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
|
||||||
|
|
||||||
|
victory-vendor@37.3.6:
|
||||||
|
resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==}
|
||||||
|
|
||||||
which-boxed-primitive@1.1.1:
|
which-boxed-primitive@1.1.1:
|
||||||
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
|
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -3321,6 +3465,18 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
|
|
||||||
|
'@reduxjs/toolkit@2.12.0(react-redux@9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1))(react@19.2.7)':
|
||||||
|
dependencies:
|
||||||
|
'@standard-schema/spec': 1.1.0
|
||||||
|
'@standard-schema/utils': 0.3.0
|
||||||
|
immer: 11.1.9
|
||||||
|
redux: 5.0.1
|
||||||
|
redux-thunk: 3.1.0(redux@5.0.1)
|
||||||
|
reselect: 5.2.0
|
||||||
|
optionalDependencies:
|
||||||
|
react: 19.2.7
|
||||||
|
react-redux: 9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1)
|
||||||
|
|
||||||
'@rtsao/scc@1.1.0': {}
|
'@rtsao/scc@1.1.0': {}
|
||||||
|
|
||||||
'@rushstack/eslint-patch@1.16.1': {}
|
'@rushstack/eslint-patch@1.16.1': {}
|
||||||
@@ -3365,6 +3521,10 @@ snapshots:
|
|||||||
|
|
||||||
'@shikijs/vscode-textmate@10.0.2': {}
|
'@shikijs/vscode-textmate@10.0.2': {}
|
||||||
|
|
||||||
|
'@standard-schema/spec@1.1.0': {}
|
||||||
|
|
||||||
|
'@standard-schema/utils@0.3.0': {}
|
||||||
|
|
||||||
'@swc/helpers@0.5.15':
|
'@swc/helpers@0.5.15':
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
@@ -3445,6 +3605,30 @@ snapshots:
|
|||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@types/d3-array@3.2.2': {}
|
||||||
|
|
||||||
|
'@types/d3-color@3.1.3': {}
|
||||||
|
|
||||||
|
'@types/d3-ease@3.0.2': {}
|
||||||
|
|
||||||
|
'@types/d3-interpolate@3.0.4':
|
||||||
|
dependencies:
|
||||||
|
'@types/d3-color': 3.1.3
|
||||||
|
|
||||||
|
'@types/d3-path@3.1.1': {}
|
||||||
|
|
||||||
|
'@types/d3-scale@4.0.9':
|
||||||
|
dependencies:
|
||||||
|
'@types/d3-time': 3.0.4
|
||||||
|
|
||||||
|
'@types/d3-shape@3.1.8':
|
||||||
|
dependencies:
|
||||||
|
'@types/d3-path': 3.1.1
|
||||||
|
|
||||||
|
'@types/d3-time@3.0.4': {}
|
||||||
|
|
||||||
|
'@types/d3-timer@3.0.2': {}
|
||||||
|
|
||||||
'@types/estree@1.0.9': {}
|
'@types/estree@1.0.9': {}
|
||||||
|
|
||||||
'@types/hast@3.0.4':
|
'@types/hast@3.0.4':
|
||||||
@@ -3473,6 +3657,8 @@ snapshots:
|
|||||||
|
|
||||||
'@types/unist@3.0.3': {}
|
'@types/unist@3.0.3': {}
|
||||||
|
|
||||||
|
'@types/use-sync-external-store@0.0.6': {}
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@8.62.1(@typescript-eslint/parser@8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)':
|
'@typescript-eslint/eslint-plugin@8.62.1(@typescript-eslint/parser@8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/regexpp': 4.12.2
|
'@eslint-community/regexpp': 4.12.2
|
||||||
@@ -3813,6 +3999,44 @@ snapshots:
|
|||||||
|
|
||||||
csstype@3.2.3: {}
|
csstype@3.2.3: {}
|
||||||
|
|
||||||
|
d3-array@3.2.4:
|
||||||
|
dependencies:
|
||||||
|
internmap: 2.0.3
|
||||||
|
|
||||||
|
d3-color@3.1.0: {}
|
||||||
|
|
||||||
|
d3-ease@3.0.1: {}
|
||||||
|
|
||||||
|
d3-format@3.1.2: {}
|
||||||
|
|
||||||
|
d3-interpolate@3.0.1:
|
||||||
|
dependencies:
|
||||||
|
d3-color: 3.1.0
|
||||||
|
|
||||||
|
d3-path@3.1.0: {}
|
||||||
|
|
||||||
|
d3-scale@4.0.2:
|
||||||
|
dependencies:
|
||||||
|
d3-array: 3.2.4
|
||||||
|
d3-format: 3.1.2
|
||||||
|
d3-interpolate: 3.0.1
|
||||||
|
d3-time: 3.1.0
|
||||||
|
d3-time-format: 4.1.0
|
||||||
|
|
||||||
|
d3-shape@3.2.0:
|
||||||
|
dependencies:
|
||||||
|
d3-path: 3.1.0
|
||||||
|
|
||||||
|
d3-time-format@4.1.0:
|
||||||
|
dependencies:
|
||||||
|
d3-time: 3.1.0
|
||||||
|
|
||||||
|
d3-time@3.1.0:
|
||||||
|
dependencies:
|
||||||
|
d3-array: 3.2.4
|
||||||
|
|
||||||
|
d3-timer@3.0.1: {}
|
||||||
|
|
||||||
damerau-levenshtein@1.0.8: {}
|
damerau-levenshtein@1.0.8: {}
|
||||||
|
|
||||||
data-view-buffer@1.0.2:
|
data-view-buffer@1.0.2:
|
||||||
@@ -3845,6 +4069,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
|
|
||||||
|
decimal.js-light@2.5.1: {}
|
||||||
|
|
||||||
deep-is@0.1.4: {}
|
deep-is@0.1.4: {}
|
||||||
|
|
||||||
define-data-property@1.1.4:
|
define-data-property@1.1.4:
|
||||||
@@ -4006,6 +4232,8 @@ snapshots:
|
|||||||
is-date-object: 1.1.0
|
is-date-object: 1.1.0
|
||||||
is-symbol: 1.1.1
|
is-symbol: 1.1.1
|
||||||
|
|
||||||
|
es-toolkit@1.49.0: {}
|
||||||
|
|
||||||
esbuild@0.18.20:
|
esbuild@0.18.20:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@esbuild/android-arm': 0.18.20
|
'@esbuild/android-arm': 0.18.20
|
||||||
@@ -4289,6 +4517,8 @@ snapshots:
|
|||||||
|
|
||||||
esutils@2.0.3: {}
|
esutils@2.0.3: {}
|
||||||
|
|
||||||
|
eventemitter3@5.0.4: {}
|
||||||
|
|
||||||
fast-deep-equal@3.1.3: {}
|
fast-deep-equal@3.1.3: {}
|
||||||
|
|
||||||
fast-glob@3.3.1:
|
fast-glob@3.3.1:
|
||||||
@@ -4458,6 +4688,8 @@ snapshots:
|
|||||||
|
|
||||||
ignore@7.0.5: {}
|
ignore@7.0.5: {}
|
||||||
|
|
||||||
|
immer@11.1.9: {}
|
||||||
|
|
||||||
import-fresh@3.3.1:
|
import-fresh@3.3.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
parent-module: 1.0.1
|
parent-module: 1.0.1
|
||||||
@@ -4471,6 +4703,8 @@ snapshots:
|
|||||||
hasown: 2.0.4
|
hasown: 2.0.4
|
||||||
side-channel: 1.1.1
|
side-channel: 1.1.1
|
||||||
|
|
||||||
|
internmap@2.0.3: {}
|
||||||
|
|
||||||
is-array-buffer@3.0.5:
|
is-array-buffer@3.0.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind: 1.0.9
|
call-bind: 1.0.9
|
||||||
@@ -4941,8 +5175,43 @@ snapshots:
|
|||||||
|
|
||||||
react-is@16.13.1: {}
|
react-is@16.13.1: {}
|
||||||
|
|
||||||
|
react-redux@9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1):
|
||||||
|
dependencies:
|
||||||
|
'@types/use-sync-external-store': 0.0.6
|
||||||
|
react: 19.2.7
|
||||||
|
use-sync-external-store: 1.6.0(react@19.2.7)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.2.17
|
||||||
|
redux: 5.0.1
|
||||||
|
|
||||||
react@19.2.7: {}
|
react@19.2.7: {}
|
||||||
|
|
||||||
|
recharts@3.9.1(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@16.13.1)(react@19.2.7)(redux@5.0.1):
|
||||||
|
dependencies:
|
||||||
|
'@reduxjs/toolkit': 2.12.0(react-redux@9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1))(react@19.2.7)
|
||||||
|
clsx: 2.1.1
|
||||||
|
decimal.js-light: 2.5.1
|
||||||
|
es-toolkit: 1.49.0
|
||||||
|
eventemitter3: 5.0.4
|
||||||
|
immer: 11.1.9
|
||||||
|
react: 19.2.7
|
||||||
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
|
react-is: 16.13.1
|
||||||
|
react-redux: 9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1)
|
||||||
|
reselect: 5.2.0
|
||||||
|
tiny-invariant: 1.3.3
|
||||||
|
use-sync-external-store: 1.6.0(react@19.2.7)
|
||||||
|
victory-vendor: 37.3.6
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@types/react'
|
||||||
|
- redux
|
||||||
|
|
||||||
|
redux-thunk@3.1.0(redux@5.0.1):
|
||||||
|
dependencies:
|
||||||
|
redux: 5.0.1
|
||||||
|
|
||||||
|
redux@5.0.1: {}
|
||||||
|
|
||||||
reflect.getprototypeof@1.0.10:
|
reflect.getprototypeof@1.0.10:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind: 1.0.9
|
call-bind: 1.0.9
|
||||||
@@ -5213,6 +5482,8 @@ snapshots:
|
|||||||
|
|
||||||
tapable@2.3.3: {}
|
tapable@2.3.3: {}
|
||||||
|
|
||||||
|
tiny-invariant@1.3.3: {}
|
||||||
|
|
||||||
tinyglobby@0.2.17:
|
tinyglobby@0.2.17:
|
||||||
dependencies:
|
dependencies:
|
||||||
fdir: 6.5.0(picomatch@4.0.4)
|
fdir: 6.5.0(picomatch@4.0.4)
|
||||||
@@ -5370,6 +5641,23 @@ snapshots:
|
|||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
vfile-message: 4.0.3
|
vfile-message: 4.0.3
|
||||||
|
|
||||||
|
victory-vendor@37.3.6:
|
||||||
|
dependencies:
|
||||||
|
'@types/d3-array': 3.2.2
|
||||||
|
'@types/d3-ease': 3.0.2
|
||||||
|
'@types/d3-interpolate': 3.0.4
|
||||||
|
'@types/d3-scale': 4.0.9
|
||||||
|
'@types/d3-shape': 3.1.8
|
||||||
|
'@types/d3-time': 3.0.4
|
||||||
|
'@types/d3-timer': 3.0.2
|
||||||
|
d3-array: 3.2.4
|
||||||
|
d3-ease: 3.0.1
|
||||||
|
d3-interpolate: 3.0.1
|
||||||
|
d3-scale: 4.0.2
|
||||||
|
d3-shape: 3.2.0
|
||||||
|
d3-time: 3.1.0
|
||||||
|
d3-timer: 3.0.1
|
||||||
|
|
||||||
which-boxed-primitive@1.1.1:
|
which-boxed-primitive@1.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-bigint: 1.1.0
|
is-bigint: 1.1.0
|
||||||
|
|||||||
Reference in New Issue
Block a user