diff --git a/.trellis/spec/backend/drizzle-postgres-current.md b/.trellis/spec/backend/drizzle-postgres-current.md index 6bb5259..15139c9 100644 --- a/.trellis/spec/backend/drizzle-postgres-current.md +++ b/.trellis/spec/backend/drizzle-postgres-current.md @@ -270,3 +270,70 @@ await database .set({ status, updatedAt: new Date() }) .where(and(eq(healthAnomalyReviews.id, id), eq(healthAnomalyReviews.organizationId, organizationId))); ``` + +## Scenario: Care Execution Workspace + +### 1. Scope / Trigger + +- Trigger: adding or changing persisted daily care task execution behavior. +- Applies when replacing reserved operational module placeholders with real care task data. Care records must be Drizzle-backed and organization-scoped. + +### 2. Signatures + +- `GET /api/care/tasks` +- `PATCH /api/care/tasks/[id]` +- `listCareExecutionData(organizationId: string): Promise` +- `updateCareTaskStatus(input): Promise` +- Drizzle table: `care_tasks` + +### 3. Contracts + +- `/app/care` and `/app/{organizationSlug}/care` render the real care workspace once `care_tasks` exists. +- Reads require `care:read`; mutations require `care:manage`. +- `GET /api/care/tasks` returns `{ success: true; reason: string; data: CareExecutionData }`; client refresh code must read `result.data`. +- `PATCH /api/care/tasks/[id]` request: `{ status: "pending" | "in_progress" | "completed" | "cancelled"; executionNotes?: string }`. +- Completing a task sets `completedAt` and `completedByAccountId`; moving a task away from `completed` clears completion metadata. +- Mutations update by both `id` and active `organizationId`, then write an audit log after success. + +### 4. Validation & Error Matrix + +- Missing active organization on list -> `400` / `请选择机构后查看护理任务`. +- Missing active organization on mutation -> `400` / `请选择机构后处理护理任务`. +- Invalid status -> `400` / `护理任务状态无效`. +- Missing or cross-organization task ID -> `404` / `护理任务不存在`. +- Missing permission -> `403` from `requirePermission`; route must not call domain helpers. +- Update returning no row -> structured mutation failure. + +### 5. Good/Base/Bad Cases + +- Good: keep care task examples in default workspace seed tied to real seeded elders. +- Good: use one additive `care_tasks` table for MVP execution records instead of building a full recurring care-plan engine. +- Good: preserve `/app/health` separation; care execution is operational and not the health admin settings page. +- Base: `elderId` can be nullable for future public-area checks, but seeded MVP examples should use real elders. +- Bad: render fake care metrics in `ModulePage` after the module becomes real. +- Bad: update task status by ID alone without `organizationId`. +- Bad: only disable buttons in the UI while leaving Route Handlers on broad elder permissions. + +### 6. Tests Required + +- `pnpm test` with API assertions for list and status update routes. +- Route tests must cover happy path, permission denial, missing organization, invalid status, and missing/cross-organization task IDs. +- `pnpm db:generate` after schema changes and review generated SQL for only care enum/table/index/FK changes. +- `pnpm lint`, `pnpm type-check`, and `pnpm build`. + +### 7. Wrong vs Correct + +#### Wrong + +```ts +await database.update(careTasks).set({ status }).where(eq(careTasks.id, id)); +``` + +#### Correct + +```ts +await database + .update(careTasks) + .set({ status, updatedAt: new Date() }) + .where(and(eq(careTasks.id, id), eq(careTasks.organizationId, organizationId))); +``` diff --git a/.trellis/tasks/07-02-care-execution-workspace/design.md b/.trellis/tasks/07-02-care-execution-workspace/design.md new file mode 100644 index 0000000..585665d --- /dev/null +++ b/.trellis/tasks/07-02-care-execution-workspace/design.md @@ -0,0 +1,104 @@ +# Care Execution Workspace Design + +## Summary + +Replace the reserved `/care` module page with a real operational workspace backed by persisted care execution records. This task owns daily care execution only: task list, status movement, completion notes, and seeded examples. + +## Route Boundary + +- Unscoped route: `app/(app)/app/care/page.tsx` +- Scoped route: `app/(app)/app/[organizationSlug]/care/page.tsx` +- Navigation item remains in the `运营` group. +- Permission should move from `elder:update` to explicit care permissions: + - `care:read` + - `care:manage` + +The unscoped page should: + +1. Load `getCurrentAuthContext()`. +2. Redirect unauthenticated users to `/login`. +3. Redirect users without `care:read` to the workspace dashboard. +4. Require an active organization. +5. Load care data through a focused server helper. +6. Render a client workspace with `canManage` based on `care:manage`. + +## Data Model + +Add one MVP table: `care_tasks`. + +- `id` +- `organizationId` +- `elderId` nullable, because some tasks can be room/public-area checks later +- `title` +- `careType`: `daily_care | meal | medication | rehab | inspection | cleaning | other` +- `priority`: `low | normal | high | urgent` +- `status`: `pending | in_progress | completed | cancelled` +- `scheduledAt` +- `assigneeLabel` +- `executionNotes` +- `completedAt` +- `createdByAccountId` +- `completedByAccountId` +- `createdAt` +- `updatedAt` + +Indexes: + +- `(organizationId, status, priority)` for queues. +- `(organizationId, scheduledAt)` for daily schedule ordering. + +## Server Helpers + +Create `modules/care/`: + +- `modules/care/types.ts` + - enum values, labels, DTOs, validators. +- `modules/care/server/operations.ts` + - `listCareExecutionData(organizationId: string)` + - `updateCareTaskStatus(input)` +- `modules/care/components/CareWorkspaceClient.tsx` + - dense tabs/filters/table and status action dialogs. + +Reads should batch the care tasks and elder names in one query shape. Mutations must filter by `organizationId` and update by task ID plus organization ID. + +## API Design + +- `GET /api/care/tasks` + - permission: `care:read` + - response: `{ success: true; reason: string; data: CareExecutionData }` +- `PATCH /api/care/tasks/[id]` + - permission: `care:manage` + - request: `{ status: "pending" | "in_progress" | "completed" | "cancelled"; executionNotes?: string }` + - completion sets `completedAt` and `completedByAccountId` + - moving out of completed clears completion metadata + - records audit log + +All failures use `{ success: false; reason: string }`. + +## UI Design + +The care workspace should be operational and compact: + +- Metrics: pending, in-progress, completed today, high/urgent. +- Filters: status, priority, care type, search by elder/title/assignee. +- Table columns: task, elder, type, priority, status, scheduled time, assignee, completed time, actions. +- Actions: + - pending -> start + - in-progress/pending -> complete with notes + - completed/cancelled are visible but not primary work queue items +- Users without `care:manage` can view but action buttons are disabled or hidden. + +## Default Workspace Data + +Extend `seedDefaultWorkspaceData()` with care tasks after seeded elders exist. Examples should cover: + +- pending morning care +- in-progress rehabilitation or inspection +- completed meal/medication task +- urgent/high overdue-style task scheduled in the past + +## Compatibility And Rollback + +- Existing `/app/care` placeholder is replaced only for this module. +- New tables are additive; rollback is dropping generated care migration and removing `modules/care`, care API routes, and route/page changes. +- Dashboard integration remains out of scope until the dedicated dashboard task. diff --git a/.trellis/tasks/07-02-care-execution-workspace/implement.md b/.trellis/tasks/07-02-care-execution-workspace/implement.md new file mode 100644 index 0000000..64b045f --- /dev/null +++ b/.trellis/tasks/07-02-care-execution-workspace/implement.md @@ -0,0 +1,61 @@ +# Implementation Plan + +## 1. Schema And Migration + +- Add care enums and `care_tasks` table to `modules/core/server/schema.ts`. +- Add indexes for queue and schedule queries. +- Run `pnpm db:generate`. +- Review generated SQL. + +## 2. Types And Permissions + +- Add `care:read` and `care:manage` to `modules/core/types.ts`. +- Add permission definitions and default role grants in `modules/core/server/permissions.ts`. +- Move navigation `/care` permission to `care:read`. +- Add care DTOs, labels, and validators in `modules/care/types.ts`. + +## 3. TDD API Tests + +- Add `app/api/care/care-routes.test.ts` before implementation. +- Cover: + - list care tasks + - permission denial + - missing active organization + - start task + - complete task with notes + - invalid status + - missing/cross-organization task + +## 4. Server Operations And API Routes + +- Add `modules/care/server/operations.ts`. +- Add `GET /api/care/tasks`. +- Add `PATCH /api/care/tasks/[id]`. +- Use `requirePermission`, structured failures, organization scoping, and audit logs. + +## 5. Workspace UI + +- Replace `app/(app)/app/care/page.tsx` placeholder with real Server Component. +- Keep scoped route delegating to the unscoped route. +- Add `modules/care/components/CareWorkspaceClient.tsx`. +- Implement metrics, filters, table, and start/complete actions. + +## 6. Default Workspace Data + +- Extend `modules/core/server/default-workspace-data.ts` with care task examples tied to seeded elders. +- Keep seed insertion inside the first-run default workspace transaction. + +## 7. Verification + +- `pnpm test` +- `pnpm lint` +- `pnpm type-check` +- `pnpm db:generate` +- Review generated SQL. +- `pnpm build` + +## Rollback Points + +- Before migration generation: remove schema and care module files. +- After migration generation: remove generated care migration plus schema changes. +- After UI/API: remove care API routes, `modules/care`, page changes, permissions, nav permission change, and seed rows. diff --git a/.trellis/tasks/07-02-care-execution-workspace/task.json b/.trellis/tasks/07-02-care-execution-workspace/task.json index 53fe1dd..0709524 100644 --- a/.trellis/tasks/07-02-care-execution-workspace/task.json +++ b/.trellis/tasks/07-02-care-execution-workspace/task.json @@ -3,7 +3,7 @@ "name": "care-execution-workspace", "title": "护理服务执行工作台", "description": "", - "status": "planning", + "status": "in_progress", "dev_type": null, "scope": null, "package": null, diff --git a/app/(app)/app/[organizationSlug]/care/page.tsx b/app/(app)/app/[organizationSlug]/care/page.tsx index 2d726b6..05dddf5 100644 --- a/app/(app)/app/[organizationSlug]/care/page.tsx +++ b/app/(app)/app/[organizationSlug]/care/page.tsx @@ -1,5 +1,5 @@ import CarePage from "../../care/page"; -export default function ScopedCarePage(): React.ReactElement { +export default async function ScopedCarePage(): Promise { return CarePage(); } diff --git a/app/(app)/app/care/page.tsx b/app/(app)/app/care/page.tsx index 696029b..6428bd8 100644 --- a/app/(app)/app/care/page.tsx +++ b/app/(app)/app/care/page.tsx @@ -1,15 +1,26 @@ -import { ClipboardCheck } from "lucide-react"; +import { redirect } from "next/navigation"; -import { ModulePage } from "@/modules/shared/components/ModulePage"; +import { getCurrentAuthContext } from "@/modules/core/server/auth"; +import { hasPermission } from "@/modules/core/server/permissions"; +import { listCareExecutionData } from "@/modules/care/server/operations"; +import { CareWorkspaceClient } from "@/modules/care/components/CareWorkspaceClient"; +import { getWorkspaceHref } from "@/modules/shared/lib/workspace-routing"; -export default function CarePage(): React.ReactElement { - return ( - - ); +export default async function CarePage(): Promise { + const context = await getCurrentAuthContext(); + if (!context) { + redirect("/login"); + } + + if (!hasPermission(context.permissions, "care:read")) { + redirect(getWorkspaceHref(context.organization?.slug, "/dashboard")); + } + + const organizationId = context.organization?.id; + if (!organizationId) { + redirect(getWorkspaceHref(context.organization?.slug, "/dashboard")); + } + + const data = await listCareExecutionData(organizationId); + return ; } diff --git a/app/api/care/care-routes.test.ts b/app/api/care/care-routes.test.ts new file mode 100644 index 0000000..17c6bdf --- /dev/null +++ b/app/api/care/care-routes.test.ts @@ -0,0 +1,234 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { recordAuditLog } from "@/modules/core/server/audit"; +import { requirePermission } from "@/modules/core/server/auth"; +import type { AuthContext } from "@/modules/core/types"; +import type { CareExecutionData, CareTask } from "@/modules/care/types"; +import { listCareExecutionData, updateCareTaskStatus } from "@/modules/care/server/operations"; + +vi.mock("@/modules/core/server/auth", () => ({ + requirePermission: vi.fn(), +})); + +vi.mock("@/modules/core/server/audit", () => ({ + recordAuditLog: vi.fn(), +})); + +vi.mock("@/modules/care/server/operations", () => ({ + isCareMutationFailure: vi.fn((value: unknown) => typeof value === "object" && value !== null && "success" in value && value.success === false), + listCareExecutionData: vi.fn(), + updateCareTaskStatus: vi.fn(), +})); + +const account: AuthContext["account"] = { + id: "account-1", + name: "Admin", + email: "admin@example.com", + avatarUrl: "", + role: "org_admin", + status: "active", + createdAt: "2026-07-02T00:00:00.000Z", + updatedAt: "2026-07-02T00:00:00.000Z", +}; + +function createAuthContext(overrides: Partial = {}): AuthContext { + return { + account, + organization: { + id: "org-1", + name: "Demo Org", + slug: "demo-org", + status: "active", + registrationEnabled: true, + oidcEnabled: false, + oidcIssuerUrl: "", + oidcClientId: "", + oidcHasClientSecret: false, + oidcScopes: "openid profile email", + oidcRedirectUri: "", + oidcAvatarClaim: "picture", + oidcAutoProvision: false, + createdAt: "2026-07-02T00:00:00.000Z", + updatedAt: "2026-07-02T00:00:00.000Z", + }, + organizations: [], + permissions: ["care:read", "care:manage"], + session: { + id: "session-1", + accountId: "account-1", + activeOrganizationId: "org-1", + createdAt: "2026-07-02T00:00:00.000Z", + expiresAt: "2026-07-09T00:00:00.000Z", + }, + ...overrides, + }; +} + +const careTask: CareTask = { + id: "task-1", + organizationId: "org-1", + elderId: "elder-1", + elderName: "王桂兰", + title: "晨间协助洗漱", + careType: "daily_care", + priority: "normal", + status: "pending", + scheduledAt: "2026-07-02T08:00:00.000Z", + assigneeLabel: "一号护理组", + executionNotes: "", + completedAt: undefined, + completedByAccountId: undefined, + completedByName: undefined, + createdAt: "2026-07-02T00:00:00.000Z", + updatedAt: "2026-07-02T00:00:00.000Z", +}; + +const careExecutionData: CareExecutionData = { + metrics: { + pending: 1, + inProgress: 1, + completedToday: 1, + highPriority: 1, + }, + tasks: [careTask], +}; + +function allowAuth(): void { + vi.mocked(requirePermission).mockResolvedValue({ + success: true, + context: createAuthContext(), + }); +} + +function jsonRequest(body: Record): Request { + return new Request("http://localhost/api/care/tasks", { + method: "PATCH", + headers: { "content-type": "application/json" }, + body: JSON.stringify(body), + }); +} + +async function readJson(response: Response): Promise> { + return (await response.json()) as Record; +} + +beforeEach(() => { + vi.clearAllMocks(); + allowAuth(); +}); + +describe("care execution API routes", () => { + it("loads care execution data for the active organization", async () => { + vi.mocked(listCareExecutionData).mockResolvedValue(careExecutionData); + + const { GET } = await import("./tasks/route"); + const response = await GET(); + const body = await readJson(response); + + expect(response.status).toBe(200); + expect(body.success).toBe(true); + expect(body.data).toEqual(careExecutionData); + expect(listCareExecutionData).toHaveBeenCalledWith("org-1"); + }); + + it("returns permission failures before loading care tasks", async () => { + const denied = Response.json({ success: false, reason: "权限不足" }, { status: 403 }); + vi.mocked(requirePermission).mockResolvedValue({ success: false, response: denied }); + + const { GET } = await import("./tasks/route"); + const response = await GET(); + const body = await readJson(response); + + expect(response.status).toBe(403); + expect(body.reason).toBe("权限不足"); + expect(listCareExecutionData).not.toHaveBeenCalled(); + }); + + it("requires an active organization before loading care tasks", async () => { + vi.mocked(requirePermission).mockResolvedValue({ + success: true, + context: createAuthContext({ organization: undefined }), + }); + + const { GET } = await import("./tasks/route"); + const response = await GET(); + const body = await readJson(response); + + expect(response.status).toBe(400); + expect(body.reason).toBe("请选择机构后查看护理任务"); + expect(listCareExecutionData).not.toHaveBeenCalled(); + }); + + it("marks a care task as in progress", async () => { + vi.mocked(updateCareTaskStatus).mockResolvedValue({ + ...careTask, + status: "in_progress", + updatedAt: "2026-07-02T08:10:00.000Z", + }); + + const { PATCH } = await import("./tasks/[id]/route"); + const response = await PATCH(jsonRequest({ status: "in_progress" }), { + params: Promise.resolve({ id: "task-1" }), + }); + const body = await readJson(response); + + expect(response.status).toBe(200); + expect(body.success).toBe(true); + expect(updateCareTaskStatus).toHaveBeenCalledWith(expect.objectContaining({ accountId: "account-1", id: "task-1", organizationId: "org-1" })); + expect(recordAuditLog).toHaveBeenCalledWith(expect.objectContaining({ action: "care.task.update" })); + }); + + it("completes a care task with notes", async () => { + vi.mocked(updateCareTaskStatus).mockResolvedValue({ + ...careTask, + status: "completed", + executionNotes: "已完成并观察无异常", + completedAt: "2026-07-02T08:30:00.000Z", + completedByAccountId: "account-1", + completedByName: "Admin", + updatedAt: "2026-07-02T08:30:00.000Z", + }); + + const { PATCH } = await import("./tasks/[id]/route"); + const response = await PATCH(jsonRequest({ status: "completed", executionNotes: "已完成并观察无异常" }), { + params: Promise.resolve({ id: "task-1" }), + }); + const body = await readJson(response); + + expect(response.status).toBe(200); + expect(body.success).toBe(true); + expect(updateCareTaskStatus).toHaveBeenCalledWith(expect.objectContaining({ executionNotes: "已完成并观察无异常", status: "completed" })); + expect(recordAuditLog).toHaveBeenCalledWith(expect.objectContaining({ action: "care.task.update" })); + }); + + it("rejects invalid care task status", async () => { + const { PATCH } = await import("./tasks/[id]/route"); + const response = await PATCH(jsonRequest({ status: "done" }), { + params: Promise.resolve({ id: "task-1" }), + }); + const body = await readJson(response); + + expect(response.status).toBe(400); + expect(body.reason).toBe("护理任务状态无效"); + expect(updateCareTaskStatus).not.toHaveBeenCalled(); + expect(recordAuditLog).not.toHaveBeenCalled(); + }); + + it("returns structured failures for missing or cross-organization care tasks", async () => { + vi.mocked(updateCareTaskStatus).mockResolvedValue({ + success: false, + reason: "护理任务不存在", + status: 404, + }); + + const { PATCH } = await import("./tasks/[id]/route"); + const response = await PATCH(jsonRequest({ status: "in_progress" }), { + params: Promise.resolve({ id: "task-from-other-org" }), + }); + const body = await readJson(response); + + expect(response.status).toBe(404); + expect(body.reason).toBe("护理任务不存在"); + expect(recordAuditLog).not.toHaveBeenCalled(); + }); +}); diff --git a/app/api/care/tasks/[id]/route.ts b/app/api/care/tasks/[id]/route.ts new file mode 100644 index 0000000..9b03321 --- /dev/null +++ b/app/api/care/tasks/[id]/route.ts @@ -0,0 +1,56 @@ +import { jsonFailure, jsonSuccess, readJsonBody } from "@/modules/core/server/api"; +import { recordAuditLog } from "@/modules/core/server/audit"; +import { requirePermission } from "@/modules/core/server/auth"; +import { isCareMutationFailure, updateCareTaskStatus } from "@/modules/care/server/operations"; +import { validateCareTaskStatusUpdateInput } from "@/modules/care/types"; + +type RouteContext = { + params: Promise<{ + id: string; + }>; +}; + +export async function PATCH(request: Request, context: RouteContext): Promise { + const { id } = await context.params; + const auth = await requirePermission("care:manage", { + action: "care.task.update", + targetType: "care_task", + targetId: id, + }); + + if (!auth.success) { + return auth.response; + } + + const organizationId = auth.context.organization?.id; + if (!organizationId) { + return jsonFailure("请选择机构后处理护理任务", 400); + } + + const input = validateCareTaskStatusUpdateInput(await readJsonBody(request)); + if (!input.success) { + return jsonFailure(input.reason); + } + + const task = await updateCareTaskStatus({ + ...input.data, + id, + organizationId, + accountId: auth.context.account.id, + }); + if (isCareMutationFailure(task)) { + return jsonFailure(task.reason, task.status); + } + + await recordAuditLog({ + actor: auth.context.account, + organizationId, + action: "care.task.update", + targetType: "care_task", + targetId: task.id, + result: "success", + reason: `处理护理任务:${task.title}`, + }); + + return jsonSuccess("护理任务已更新", { task }); +} diff --git a/app/api/care/tasks/route.ts b/app/api/care/tasks/route.ts new file mode 100644 index 0000000..41521ca --- /dev/null +++ b/app/api/care/tasks/route.ts @@ -0,0 +1,22 @@ +import { jsonFailure, jsonSuccess } from "@/modules/core/server/api"; +import { requirePermission } from "@/modules/core/server/auth"; +import { listCareExecutionData } from "@/modules/care/server/operations"; + +export async function GET(): Promise { + const auth = await requirePermission("care:read", { + action: "care.task.list", + targetType: "care_task", + }); + + if (!auth.success) { + return auth.response; + } + + const organizationId = auth.context.organization?.id; + if (!organizationId) { + return jsonFailure("请选择机构后查看护理任务", 400); + } + + const data = await listCareExecutionData(organizationId); + return jsonSuccess("护理任务已加载", { data }); +} diff --git a/drizzle/0004_silly_carnage.sql b/drizzle/0004_silly_carnage.sql new file mode 100644 index 0000000..bfd4de9 --- /dev/null +++ b/drizzle/0004_silly_carnage.sql @@ -0,0 +1,27 @@ +CREATE TYPE "public"."care_task_priority" AS ENUM('low', 'normal', 'high', 'urgent');--> statement-breakpoint +CREATE TYPE "public"."care_task_status" AS ENUM('pending', 'in_progress', 'completed', 'cancelled');--> statement-breakpoint +CREATE TYPE "public"."care_task_type" AS ENUM('daily_care', 'meal', 'medication', 'rehab', 'inspection', 'cleaning', 'other');--> statement-breakpoint +CREATE TABLE "care_tasks" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "organization_id" uuid NOT NULL, + "elder_id" uuid, + "title" text NOT NULL, + "care_type" "care_task_type" NOT NULL, + "priority" "care_task_priority" DEFAULT 'normal' NOT NULL, + "status" "care_task_status" DEFAULT 'pending' NOT NULL, + "scheduled_at" timestamp with time zone NOT NULL, + "assignee_label" text DEFAULT '' NOT NULL, + "execution_notes" text DEFAULT '' NOT NULL, + "completed_at" timestamp with time zone, + "created_by_account_id" uuid, + "completed_by_account_id" uuid, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "care_tasks" ADD CONSTRAINT "care_tasks_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "care_tasks" ADD CONSTRAINT "care_tasks_elder_id_elders_id_fk" FOREIGN KEY ("elder_id") REFERENCES "public"."elders"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "care_tasks" ADD CONSTRAINT "care_tasks_created_by_account_id_accounts_id_fk" FOREIGN KEY ("created_by_account_id") REFERENCES "public"."accounts"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "care_tasks" ADD CONSTRAINT "care_tasks_completed_by_account_id_accounts_id_fk" FOREIGN KEY ("completed_by_account_id") REFERENCES "public"."accounts"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "care_tasks_queue_lookup" ON "care_tasks" USING btree ("organization_id","status","priority");--> statement-breakpoint +CREATE INDEX "care_tasks_schedule_lookup" ON "care_tasks" USING btree ("organization_id","scheduled_at"); \ No newline at end of file diff --git a/drizzle/meta/0004_snapshot.json b/drizzle/meta/0004_snapshot.json new file mode 100644 index 0000000..890d92d --- /dev/null +++ b/drizzle/meta/0004_snapshot.json @@ -0,0 +1,3000 @@ +{ + "id": "4d819091-9b70-4d7e-b656-4b71cff5d1a4", + "prevId": "24d5b494-ea3c-4f12-acf7-06223055954d", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "platform_role_id": { + "name": "platform_role_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_salt": { + "name": "password_salt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "account_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "accounts_email_unique": { + "name": "accounts_email_unique", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "accounts_platform_role_id_roles_id_fk": { + "name": "accounts_platform_role_id_roles_id_fk", + "tableFrom": "accounts", + "tableTo": "roles", + "columnsFrom": [ + "platform_role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.admissions": { + "name": "admissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "elder_id": { + "name": "elder_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "bed_id": { + "name": "bed_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "admission_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "admitted_at": { + "name": "admitted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "discharged_at": { + "name": "discharged_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "admissions_organization_id_organizations_id_fk": { + "name": "admissions_organization_id_organizations_id_fk", + "tableFrom": "admissions", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "admissions_elder_id_elders_id_fk": { + "name": "admissions_elder_id_elders_id_fk", + "tableFrom": "admissions", + "tableTo": "elders", + "columnsFrom": [ + "elder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "admissions_bed_id_beds_id_fk": { + "name": "admissions_bed_id_beds_id_fk", + "tableFrom": "admissions", + "tableTo": "beds", + "columnsFrom": [ + "bed_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "actor_account_id": { + "name": "actor_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result": { + "name": "result", + "type": "audit_result", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "audit_logs_organization_id_organizations_id_fk": { + "name": "audit_logs_organization_id_organizations_id_fk", + "tableFrom": "audit_logs", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "audit_logs_actor_account_id_accounts_id_fk": { + "name": "audit_logs_actor_account_id_accounts_id_fk", + "tableFrom": "audit_logs", + "tableTo": "accounts", + "columnsFrom": [ + "actor_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beds": { + "name": "beds", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "bed_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'available'" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "beds_code_room_unique": { + "name": "beds_code_room_unique", + "columns": [ + { + "expression": "room_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "beds_organization_id_organizations_id_fk": { + "name": "beds_organization_id_organizations_id_fk", + "tableFrom": "beds", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "beds_room_id_rooms_id_fk": { + "name": "beds_room_id_rooms_id_fk", + "tableFrom": "beds", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.buildings": { + "name": "buildings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "campus_id": { + "name": "campus_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "buildings_organization_id_organizations_id_fk": { + "name": "buildings_organization_id_organizations_id_fk", + "tableFrom": "buildings", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "buildings_campus_id_campuses_id_fk": { + "name": "buildings_campus_id_campuses_id_fk", + "tableFrom": "buildings", + "tableTo": "campuses", + "columnsFrom": [ + "campus_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.campuses": { + "name": "campuses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "campuses_organization_id_organizations_id_fk": { + "name": "campuses_organization_id_organizations_id_fk", + "tableFrom": "campuses", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.care_tasks": { + "name": "care_tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "elder_id": { + "name": "elder_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "care_type": { + "name": "care_type", + "type": "care_task_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "care_task_priority", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'normal'" + }, + "status": { + "name": "status", + "type": "care_task_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "scheduled_at": { + "name": "scheduled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "assignee_label": { + "name": "assignee_label", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "execution_notes": { + "name": "execution_notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "completed_by_account_id": { + "name": "completed_by_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "care_tasks_queue_lookup": { + "name": "care_tasks_queue_lookup", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "care_tasks_schedule_lookup": { + "name": "care_tasks_schedule_lookup", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scheduled_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "care_tasks_organization_id_organizations_id_fk": { + "name": "care_tasks_organization_id_organizations_id_fk", + "tableFrom": "care_tasks", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "care_tasks_elder_id_elders_id_fk": { + "name": "care_tasks_elder_id_elders_id_fk", + "tableFrom": "care_tasks", + "tableTo": "elders", + "columnsFrom": [ + "elder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "care_tasks_created_by_account_id_accounts_id_fk": { + "name": "care_tasks_created_by_account_id_accounts_id_fk", + "tableFrom": "care_tasks", + "tableTo": "accounts", + "columnsFrom": [ + "created_by_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "care_tasks_completed_by_account_id_accounts_id_fk": { + "name": "care_tasks_completed_by_account_id_accounts_id_fk", + "tableFrom": "care_tasks", + "tableTo": "accounts", + "columnsFrom": [ + "completed_by_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chronic_conditions": { + "name": "chronic_conditions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "elder_id": { + "name": "elder_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "chronic_condition_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "diagnosed_at": { + "name": "diagnosed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "treatment_notes": { + "name": "treatment_notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "follow_up_notes": { + "name": "follow_up_notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "chronic_conditions_elder_status_lookup": { + "name": "chronic_conditions_elder_status_lookup", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "elder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chronic_conditions_organization_id_organizations_id_fk": { + "name": "chronic_conditions_organization_id_organizations_id_fk", + "tableFrom": "chronic_conditions", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chronic_conditions_elder_id_elders_id_fk": { + "name": "chronic_conditions_elder_id_elders_id_fk", + "tableFrom": "chronic_conditions", + "tableTo": "elders", + "columnsFrom": [ + "elder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.elders": { + "name": "elders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "gender": { + "name": "gender", + "type": "gender", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "age": { + "name": "age", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "care_level": { + "name": "care_level", + "type": "care_level", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "elder_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "primary_contact": { + "name": "primary_contact", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "medical_notes": { + "name": "medical_notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "elders_organization_id_organizations_id_fk": { + "name": "elders_organization_id_organizations_id_fk", + "tableFrom": "elders", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.floors": { + "name": "floors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "building_id": { + "name": "building_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "floors_organization_id_organizations_id_fk": { + "name": "floors_organization_id_organizations_id_fk", + "tableFrom": "floors", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "floors_building_id_buildings_id_fk": { + "name": "floors_building_id_buildings_id_fk", + "tableFrom": "floors", + "tableTo": "buildings", + "columnsFrom": [ + "building_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.health_anomaly_reviews": { + "name": "health_anomaly_reviews", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "elder_id": { + "name": "elder_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "vital_record_id": { + "name": "vital_record_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "severity": { + "name": "severity", + "type": "incident_severity", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "health_review_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reviewed_by_account_id": { + "name": "reviewed_by_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reviewed_at": { + "name": "reviewed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "resolution_notes": { + "name": "resolution_notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "health_anomaly_reviews_queue_lookup": { + "name": "health_anomaly_reviews_queue_lookup", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "severity", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "health_anomaly_reviews_organization_id_organizations_id_fk": { + "name": "health_anomaly_reviews_organization_id_organizations_id_fk", + "tableFrom": "health_anomaly_reviews", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "health_anomaly_reviews_elder_id_elders_id_fk": { + "name": "health_anomaly_reviews_elder_id_elders_id_fk", + "tableFrom": "health_anomaly_reviews", + "tableTo": "elders", + "columnsFrom": [ + "elder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "health_anomaly_reviews_vital_record_id_vital_records_id_fk": { + "name": "health_anomaly_reviews_vital_record_id_vital_records_id_fk", + "tableFrom": "health_anomaly_reviews", + "tableTo": "vital_records", + "columnsFrom": [ + "vital_record_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "health_anomaly_reviews_reviewed_by_account_id_accounts_id_fk": { + "name": "health_anomaly_reviews_reviewed_by_account_id_accounts_id_fk", + "tableFrom": "health_anomaly_reviews", + "tableTo": "accounts", + "columnsFrom": [ + "reviewed_by_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.health_profiles": { + "name": "health_profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "elder_id": { + "name": "elder_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "allergy_notes": { + "name": "allergy_notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "medical_history": { + "name": "medical_history", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "medication_notes": { + "name": "medication_notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "care_restrictions": { + "name": "care_restrictions", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "emergency_notes": { + "name": "emergency_notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "health_profiles_organization_elder_unique": { + "name": "health_profiles_organization_elder_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "elder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "health_profiles_organization_id_organizations_id_fk": { + "name": "health_profiles_organization_id_organizations_id_fk", + "tableFrom": "health_profiles", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "health_profiles_elder_id_elders_id_fk": { + "name": "health_profiles_elder_id_elders_id_fk", + "tableFrom": "health_profiles", + "tableTo": "elders", + "columnsFrom": [ + "elder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.join_requests": { + "name": "join_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "join_request_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "reviewed_by_account_id": { + "name": "reviewed_by_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reviewed_at": { + "name": "reviewed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "join_requests_account_id_accounts_id_fk": { + "name": "join_requests_account_id_accounts_id_fk", + "tableFrom": "join_requests", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "join_requests_organization_id_organizations_id_fk": { + "name": "join_requests_organization_id_organizations_id_fk", + "tableFrom": "join_requests", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "join_requests_reviewed_by_account_id_accounts_id_fk": { + "name": "join_requests_reviewed_by_account_id_accounts_id_fk", + "tableFrom": "join_requests", + "tableTo": "accounts", + "columnsFrom": [ + "reviewed_by_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memberships": { + "name": "memberships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "membership_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "memberships_account_organization_unique": { + "name": "memberships_account_organization_unique", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memberships_account_id_accounts_id_fk": { + "name": "memberships_account_id_accounts_id_fk", + "tableFrom": "memberships", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "memberships_organization_id_organizations_id_fk": { + "name": "memberships_organization_id_organizations_id_fk", + "tableFrom": "memberships", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "memberships_role_id_roles_id_fk": { + "name": "memberships_role_id_roles_id_fk", + "tableFrom": "memberships", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_invitations": { + "name": "organization_invitations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "accepted_by_account_id": { + "name": "accepted_by_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organization_invitations_token_unique": { + "name": "organization_invitations_token_unique", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_invitations_organization_id_organizations_id_fk": { + "name": "organization_invitations_organization_id_organizations_id_fk", + "tableFrom": "organization_invitations", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_invitations_role_id_roles_id_fk": { + "name": "organization_invitations_role_id_roles_id_fk", + "tableFrom": "organization_invitations", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "organization_invitations_created_by_account_id_accounts_id_fk": { + "name": "organization_invitations_created_by_account_id_accounts_id_fk", + "tableFrom": "organization_invitations", + "tableTo": "accounts", + "columnsFrom": [ + "created_by_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "organization_invitations_accepted_by_account_id_accounts_id_fk": { + "name": "organization_invitations_accepted_by_account_id_accounts_id_fk", + "tableFrom": "organization_invitations", + "tableTo": "accounts", + "columnsFrom": [ + "accepted_by_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "organization_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "registration_enabled": { + "name": "registration_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "oidc_enabled": { + "name": "oidc_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "oidc_issuer_url": { + "name": "oidc_issuer_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "oidc_client_id": { + "name": "oidc_client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "oidc_client_secret": { + "name": "oidc_client_secret", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "oidc_scopes": { + "name": "oidc_scopes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'openid profile email'" + }, + "oidc_redirect_uri": { + "name": "oidc_redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "oidc_avatar_claim": { + "name": "oidc_avatar_claim", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'picture'" + }, + "oidc_auto_provision": { + "name": "oidc_auto_provision", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organizations_slug_unique": { + "name": "organizations_slug_unique", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permissions": { + "name": "permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.role_permissions": { + "name": "role_permissions", + "schema": "", + "columns": { + "role_id": { + "name": "role_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "permission_id": { + "name": "permission_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "role_permissions_role_id_roles_id_fk": { + "name": "role_permissions_role_id_roles_id_fk", + "tableFrom": "role_permissions", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "role_permissions_permission_id_permissions_id_fk": { + "name": "role_permissions_permission_id_permissions_id_fk", + "tableFrom": "role_permissions", + "tableTo": "permissions", + "columnsFrom": [ + "permission_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "role_permissions_role_id_permission_id_pk": { + "name": "role_permissions_role_id_permission_id_pk", + "columns": [ + "role_id", + "permission_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "role_scope", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "roles_key_organization_unique": { + "name": "roles_key_organization_unique", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "roles_organization_id_organizations_id_fk": { + "name": "roles_organization_id_organizations_id_fk", + "tableFrom": "roles", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rooms": { + "name": "rooms", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "floor_id": { + "name": "floor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "capacity": { + "name": "capacity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "status": { + "name": "status", + "type": "organization_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "rooms_code_organization_unique": { + "name": "rooms_code_organization_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "rooms_organization_id_organizations_id_fk": { + "name": "rooms_organization_id_organizations_id_fk", + "tableFrom": "rooms", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "rooms_floor_id_floors_id_fk": { + "name": "rooms_floor_id_floors_id_fk", + "tableFrom": "rooms", + "tableTo": "floors", + "columnsFrom": [ + "floor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_account_id_accounts_id_fk": { + "name": "sessions_account_id_accounts_id_fk", + "tableFrom": "sessions", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sessions_active_organization_id_organizations_id_fk": { + "name": "sessions_active_organization_id_organizations_id_fk", + "tableFrom": "sessions", + "tableTo": "organizations", + "columnsFrom": [ + "active_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.system_incidents": { + "name": "system_incidents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "severity": { + "name": "severity", + "type": "incident_severity", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "incident_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "acknowledged_by_account_id": { + "name": "acknowledged_by_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "acknowledged_at": { + "name": "acknowledged_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "resolved_by_account_id": { + "name": "resolved_by_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "system_incidents_organization_id_organizations_id_fk": { + "name": "system_incidents_organization_id_organizations_id_fk", + "tableFrom": "system_incidents", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "system_incidents_acknowledged_by_account_id_accounts_id_fk": { + "name": "system_incidents_acknowledged_by_account_id_accounts_id_fk", + "tableFrom": "system_incidents", + "tableTo": "accounts", + "columnsFrom": [ + "acknowledged_by_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "system_incidents_resolved_by_account_id_accounts_id_fk": { + "name": "system_incidents_resolved_by_account_id_accounts_id_fk", + "tableFrom": "system_incidents", + "tableTo": "accounts", + "columnsFrom": [ + "resolved_by_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.system_settings": { + "name": "system_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "'global'" + }, + "registration_enabled": { + "name": "registration_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "oidc_enabled": { + "name": "oidc_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "oidc_issuer_url": { + "name": "oidc_issuer_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "oidc_client_id": { + "name": "oidc_client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "oidc_client_secret": { + "name": "oidc_client_secret", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "oidc_scopes": { + "name": "oidc_scopes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'openid profile email'" + }, + "oidc_redirect_uri": { + "name": "oidc_redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "oidc_avatar_claim": { + "name": "oidc_avatar_claim", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'picture'" + }, + "oidc_auto_provision": { + "name": "oidc_auto_provision", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vital_records": { + "name": "vital_records", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "elder_id": { + "name": "elder_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "recorded_at": { + "name": "recorded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "vital_record_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "systolic_bp": { + "name": "systolic_bp", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "diastolic_bp": { + "name": "diastolic_bp", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "heart_rate": { + "name": "heart_rate", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "temperature_tenths": { + "name": "temperature_tenths", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "spo2": { + "name": "spo2", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "blood_glucose_tenths": { + "name": "blood_glucose_tenths", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "weight_tenths": { + "name": "weight_tenths", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "vital_records_recent_lookup": { + "name": "vital_records_recent_lookup", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "elder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "recorded_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vital_records_organization_id_organizations_id_fk": { + "name": "vital_records_organization_id_organizations_id_fk", + "tableFrom": "vital_records", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vital_records_elder_id_elders_id_fk": { + "name": "vital_records_elder_id_elders_id_fk", + "tableFrom": "vital_records", + "tableTo": "elders", + "columnsFrom": [ + "elder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vital_records_created_by_account_id_accounts_id_fk": { + "name": "vital_records_created_by_account_id_accounts_id_fk", + "tableFrom": "vital_records", + "tableTo": "accounts", + "columnsFrom": [ + "created_by_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.account_status": { + "name": "account_status", + "schema": "public", + "values": [ + "active", + "disabled", + "pending" + ] + }, + "public.admission_status": { + "name": "admission_status", + "schema": "public", + "values": [ + "active", + "transferred", + "discharged" + ] + }, + "public.audit_result": { + "name": "audit_result", + "schema": "public", + "values": [ + "success", + "denied", + "failure" + ] + }, + "public.bed_status": { + "name": "bed_status", + "schema": "public", + "values": [ + "available", + "occupied", + "maintenance", + "disabled" + ] + }, + "public.care_level": { + "name": "care_level", + "schema": "public", + "values": [ + "self-care", + "semi-assisted", + "assisted", + "intensive" + ] + }, + "public.care_task_priority": { + "name": "care_task_priority", + "schema": "public", + "values": [ + "low", + "normal", + "high", + "urgent" + ] + }, + "public.care_task_status": { + "name": "care_task_status", + "schema": "public", + "values": [ + "pending", + "in_progress", + "completed", + "cancelled" + ] + }, + "public.care_task_type": { + "name": "care_task_type", + "schema": "public", + "values": [ + "daily_care", + "meal", + "medication", + "rehab", + "inspection", + "cleaning", + "other" + ] + }, + "public.chronic_condition_status": { + "name": "chronic_condition_status", + "schema": "public", + "values": [ + "active", + "controlled", + "resolved" + ] + }, + "public.elder_status": { + "name": "elder_status", + "schema": "public", + "values": [ + "active", + "pending", + "discharged" + ] + }, + "public.gender": { + "name": "gender", + "schema": "public", + "values": [ + "male", + "female", + "other" + ] + }, + "public.health_review_status": { + "name": "health_review_status", + "schema": "public", + "values": [ + "pending", + "reviewed", + "resolved" + ] + }, + "public.incident_severity": { + "name": "incident_severity", + "schema": "public", + "values": [ + "info", + "warning", + "critical" + ] + }, + "public.incident_status": { + "name": "incident_status", + "schema": "public", + "values": [ + "open", + "acknowledged", + "resolved", + "closed" + ] + }, + "public.join_request_status": { + "name": "join_request_status", + "schema": "public", + "values": [ + "pending", + "approved", + "rejected" + ] + }, + "public.membership_status": { + "name": "membership_status", + "schema": "public", + "values": [ + "active", + "disabled", + "pending" + ] + }, + "public.organization_status": { + "name": "organization_status", + "schema": "public", + "values": [ + "active", + "disabled" + ] + }, + "public.role_scope": { + "name": "role_scope", + "schema": "public", + "values": [ + "platform", + "organization" + ] + }, + "public.vital_record_source": { + "name": "vital_record_source", + "schema": "public", + "values": [ + "manual", + "device", + "import" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 062132c..64f591d 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -29,6 +29,13 @@ "when": 1783063091807, "tag": "0003_gray_dust", "breakpoints": true + }, + { + "idx": 4, + "version": "7", + "when": 1783064001398, + "tag": "0004_silly_carnage", + "breakpoints": true } ] } \ No newline at end of file diff --git a/modules/care/components/CareWorkspaceClient.tsx b/modules/care/components/CareWorkspaceClient.tsx new file mode 100644 index 0000000..be1fb57 --- /dev/null +++ b/modules/care/components/CareWorkspaceClient.tsx @@ -0,0 +1,287 @@ +"use client"; + +import { FormEvent, useMemo, useState } from "react"; +import { CheckCircle2, Clock3, Play, Search, Siren } from "lucide-react"; + +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Dialog } from "@/components/ui/dialog"; +import { Input, Textarea } from "@/components/ui/input"; +import { Select } from "@/components/ui/select"; +import { Table } from "@/components/ui/table"; +import type { ApiResult } from "@/modules/core/server/api"; +import type { CareExecutionData, CareTask, CareTaskPriority, CareTaskStatus, CareTaskType } from "@/modules/care/types"; +import { CARE_TASK_PRIORITY_LABELS, CARE_TASK_STATUS_LABELS, CARE_TASK_TYPE_LABELS } from "@/modules/care/types"; + +type CareWorkspaceClientProps = { + canManage: boolean; + initialData: CareExecutionData; +}; + +function formatDateTime(value: string | undefined): string { + return value ? new Date(value).toLocaleString("zh-CN") : "-"; +} + +function statusVariant(status: CareTaskStatus): "success" | "secondary" | "warning" { + if (status === "completed") { + return "success"; + } + + return status === "pending" ? "warning" : "secondary"; +} + +function priorityVariant(priority: CareTaskPriority): "danger" | "secondary" | "warning" { + if (priority === "urgent") { + return "danger"; + } + + return priority === "high" ? "warning" : "secondary"; +} + +export function CareWorkspaceClient({ canManage, initialData }: CareWorkspaceClientProps): React.ReactElement { + const [data, setData] = useState(initialData); + const [query, setQuery] = useState(""); + const [statusFilter, setStatusFilter] = useState<"all" | CareTaskStatus>("all"); + const [priorityFilter, setPriorityFilter] = useState<"all" | CareTaskPriority>("all"); + const [typeFilter, setTypeFilter] = useState<"all" | CareTaskType>("all"); + const [message, setMessage] = useState(""); + const [isPending, setIsPending] = useState(false); + const [completeTarget, setCompleteTarget] = useState(); + const [executionNotes, setExecutionNotes] = useState(""); + + const normalizedQuery = query.trim().toLowerCase(); + const filteredTasks = useMemo( + () => + data.tasks.filter((task) => { + const matchesQuery = + !normalizedQuery || + [task.title, task.elderName, task.assigneeLabel, task.executionNotes].join(" ").toLowerCase().includes(normalizedQuery); + const matchesStatus = statusFilter === "all" || task.status === statusFilter; + const matchesPriority = priorityFilter === "all" || task.priority === priorityFilter; + const matchesType = typeFilter === "all" || task.careType === typeFilter; + return matchesQuery && matchesStatus && matchesPriority && matchesType; + }), + [data.tasks, normalizedQuery, priorityFilter, statusFilter, typeFilter], + ); + + async function refreshData(): Promise { + const response = await fetch("/api/care/tasks"); + const result = (await response.json()) as ApiResult<{ data: CareExecutionData }>; + if (result.success) { + setData(result.data); + } + } + + async function updateTask(task: CareTask, status: CareTaskStatus, notes = task.executionNotes): Promise { + if (!canManage) { + setMessage("当前角色无权处理护理任务"); + return; + } + + setIsPending(true); + setMessage(""); + const response = await fetch(`/api/care/tasks/${task.id}`, { + method: "PATCH", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ status, executionNotes: notes }), + }); + const result = (await response.json()) as ApiResult<{ task: CareTask }>; + setIsPending(false); + setMessage(result.reason); + + if (result.success) { + setCompleteTarget(undefined); + setExecutionNotes(""); + await refreshData(); + } + } + + function openCompleteDialog(task: CareTask): void { + setCompleteTarget(task); + setExecutionNotes(task.executionNotes); + setMessage(""); + } + + function closeCompleteDialog(): void { + if (isPending) { + return; + } + + setCompleteTarget(undefined); + setExecutionNotes(""); + } + + async function submitComplete(event: FormEvent): Promise { + event.preventDefault(); + if (!completeTarget) { + return; + } + + await updateTask(completeTarget, "completed", executionNotes); + } + + return ( +
+
+ + + + +
+ +
+
+

护理服务

+

按老人、任务类型、优先级和执行状态推进每日护理服务。

+
+
+ + setPriorityFilter(value as "all" | CareTaskPriority)} + options={[ + { value: "all", label: "全部优先级" }, + ...Object.entries(CARE_TASK_PRIORITY_LABELS).map(([value, label]) => ({ value, label })), + ]} + value={priorityFilter} + /> +