diff --git a/.trellis/spec/backend/drizzle-postgres-current.md b/.trellis/spec/backend/drizzle-postgres-current.md index 15139c9..972d806 100644 --- a/.trellis/spec/backend/drizzle-postgres-current.md +++ b/.trellis/spec/backend/drizzle-postgres-current.md @@ -251,6 +251,73 @@ return jsonSuccess("健康数据已加载", data); #### Correct +## Scenario: Collaboration Module Data Management + +### 1. Scope / Trigger + +- Trigger: changing 设备运维, 公告通知, 规则预警, or 家属服务 pages, APIs, seed data, or schema. +- Applies because these modules are real Drizzle/PostgreSQL-backed collaboration workspaces, not reserved/static module pages. + +### 2. Signatures + +- `GET /api/devices/assets`, `POST /api/devices/assets`, `PATCH|DELETE /api/devices/assets/[id]` +- `POST /api/devices/tickets`, `PATCH|DELETE /api/devices/tickets/[id]` +- `GET|POST /api/notices`, `POST|PATCH|DELETE /api/notices/[id]` +- `GET|POST /api/alerts/rules`, `PATCH|DELETE /api/alerts/rules/[id]` +- `POST /api/alerts/triggers`, `PATCH|DELETE /api/alerts/triggers/[id]` +- `GET|POST /api/family/contacts`, `PATCH|DELETE /api/family/contacts/[id]` +- `POST /api/family/visits`, `PATCH|DELETE /api/family/visits/[id]` +- `POST /api/family/feedback`, `PATCH|DELETE /api/family/feedback/[id]` + +### 3. Contracts + +- Reads require module read permission: `device:read`, `notice:read`, `alert:read`, or `family:read`. +- Mutations require module manage permission: `device:manage`, `notice:manage`, `alert:manage`, or `family:manage`. +- All records must be scoped by active `organizationId`; updates/deletes must filter by both `id` and `organizationId`. +- Read APIs return `{ success: true, reason, data }` where `data` is the module workspace DTO. +- Mutation APIs return the changed resource under a resource-specific key and write an audit log after successful persistence. +- Seed demo rows only inside `seedDefaultWorkspaceData(organizationId)` and connect them to real seeded elders/devices/rules where applicable. + +### 4. Validation & Error Matrix + +- Missing session or permission -> response from `requirePermission`; domain helper must not be called. +- Missing active organization -> `400` with a Chinese reason asking the user to select an organization. +- Invalid enum, empty required title/name/content, invalid date -> `400`. +- Cross-organization or missing referenced record -> `404` with entity-specific reason. +- Insert/update returning no row -> structured mutation failure with status `500` or `404`. + +### 5. Good/Base/Bad Cases + +- Good: Server page checks read permission, loads persisted workspace data, and passes serializable DTOs to a Client Component. +- Good: Client refreshes by calling the module read API after mutations. +- Good: notice read receipts use `POST /api/notices/[id]` with read permission; create/update/delete use manage permission. +- Base: alert rules and triggers are manually managed in v1; no background rule engine is implied. +- Bad: rendering fake collaboration counters or rows on reserved pages after these modules have real tables. +- Bad: updating a record by `id` alone without the active organization filter. + +### 6. Tests Required + +- `pnpm db:generate`, then review SQL for additive collaboration enums/tables/indexes/FKs. +- `pnpm lint`, `pnpm type-check`, `pnpm test`, and `pnpm build`. +- API route tests should cover permission denial, missing active organization, successful create/update audit, and missing/cross-organization mutation failures. + +### 7. Wrong vs Correct + +#### Wrong + +```ts +await database.update(alertTriggers).set({ status }).where(eq(alertTriggers.id, id)); +``` + +#### Correct + +```ts +await database + .update(alertTriggers) + .set({ status, updatedAt: new Date() }) + .where(and(eq(alertTriggers.id, id), eq(alertTriggers.organizationId, organizationId))); +``` + ```ts const data = await listHealthAdminData(organizationId); return jsonSuccess("健康数据已加载", { data }); diff --git a/.trellis/tasks/07-03-collaboration-modules/check.jsonl b/.trellis/tasks/07-03-collaboration-modules/check.jsonl new file mode 100644 index 0000000..9dd3234 --- /dev/null +++ b/.trellis/tasks/07-03-collaboration-modules/check.jsonl @@ -0,0 +1 @@ +{"_example": "Fill with {\"file\": \"\", \"reason\": \"\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."} diff --git a/.trellis/tasks/07-03-collaboration-modules/design.md b/.trellis/tasks/07-03-collaboration-modules/design.md new file mode 100644 index 0000000..f784152 --- /dev/null +++ b/.trellis/tasks/07-03-collaboration-modules/design.md @@ -0,0 +1,50 @@ +# Design + +## Architecture + +Add four module slices following existing patterns: + +- `modules/devices`, `modules/notices`, `modules/alerts`, and `modules/family` +- each module owns `types.ts`, `server/operations.ts`, and a client workspace component +- route handlers under `app/api/` validate permissions, input, organization context, and audit mutations +- app route pages remain Server Components and pass initial data plus `canManage` into client components + +## Data Model + +Add Postgres enums and tables to `modules/core/server/schema.ts`: + +- devices: asset status, ticket status, ticket priority; device assets and maintenance tickets +- notices: notice status; notices and notice read receipts +- alerts: rule type/status, trigger status; alert rules and alert triggers +- family: relation/status, visit status, feedback status/type; family contacts, visit appointments, feedback records + +All business tables include `organizationId`, timestamps, and indexes for list/status lookups. FK references use cascade for organization-owned children and set-null where historical records should survive related record deletion. + +## Permissions + +Extend `Permission` and seeded permission definitions: + +- `device:read`, `device:manage` +- `notice:read`, `notice:manage` +- `alert:read`, `alert:manage` +- `family:read`, `family:manage` + +`org_admin` and `manager` receive read/manage for all four modules. `viewer` receives read permissions. `caregiver` receives read permissions for devices, alerts, and family plus existing care/health/facility access. + +## UI Flow + +Each workspace should provide: + +- metric cards for key counts +- search and status/type filters +- tables with stable widths and empty states +- dialogs for create/edit/delete confirmation or status updates +- optimistic local refresh by refetching the module API after successful mutations + +## Compatibility + +Generate a Drizzle migration from schema changes. Existing installations need to run migrations before using the pages. Existing reserved-page component can remain for future modules but must no longer be used by these four routes. + +## Rollback + +The rollback point is the generated migration plus module files. If implementation gets too large, keep schema and API for all modules but reduce UI duplication by sharing small local helper components only where it does not obscure module boundaries. diff --git a/.trellis/tasks/07-03-collaboration-modules/implement.jsonl b/.trellis/tasks/07-03-collaboration-modules/implement.jsonl new file mode 100644 index 0000000..9dd3234 --- /dev/null +++ b/.trellis/tasks/07-03-collaboration-modules/implement.jsonl @@ -0,0 +1 @@ +{"_example": "Fill with {\"file\": \"\", \"reason\": \"\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."} diff --git a/.trellis/tasks/07-03-collaboration-modules/implement.md b/.trellis/tasks/07-03-collaboration-modules/implement.md new file mode 100644 index 0000000..07cf65e --- /dev/null +++ b/.trellis/tasks/07-03-collaboration-modules/implement.md @@ -0,0 +1,26 @@ +# Implementation Plan + +## Steps + +1. Load relevant frontend, backend, and shared Trellis specs. +2. Add schema enums/tables and update core permission types/seed role definitions. +3. Generate the Drizzle migration. +4. Add module type contracts, validators, and server operations for devices, notices, alerts, and family. +5. Add API route handlers and route tests for read/manage permission behavior and organization scoping. +6. Add local seed records for the four modules using existing organization, elder, bed, incident, health, and account context where available. +7. Replace reserved route pages with data-backed Server Components and client workspace components. +8. Run `pnpm type-check`, `pnpm test`, and targeted lint/build checks as time allows. +9. Run Trellis check/update-spec finish steps and commit task changes. + +## Validation Commands + +- `pnpm db:generate` +- `pnpm type-check` +- `pnpm test` +- `pnpm lint` + +## Risk Points + +- Schema migration generation changes tracked Drizzle metadata; inspect before finalizing. +- Avoid touching unrelated user-modified files except where required by shared types/navigation/seed integration. +- Keep organization scoping explicit in every list and mutation query. diff --git a/.trellis/tasks/07-03-collaboration-modules/prd.md b/.trellis/tasks/07-03-collaboration-modules/prd.md new file mode 100644 index 0000000..5140b4b --- /dev/null +++ b/.trellis/tasks/07-03-collaboration-modules/prd.md @@ -0,0 +1,42 @@ +# Build collaboration modules + +## Goal + +Replace the four reserved "协同" navigation pages with real persisted business modules for 设备运维, 公告通知, 规则预警, and 家属服务 so the local养老机构工作台 can exercise end-to-end CRUD workflows backed by Drizzle/Postgres data. + +## Confirmed Facts + +- The current navigation already exposes `/devices`, `/notices`, `/alerts`, and `/family`. +- These routes currently render `ReservedModulePages`, and the frontend spec requires reserved modules not to fabricate operational data. +- The stack uses Next.js App Router, React client components, project-owned Kumo UI adapters, Drizzle/Postgres, route handlers under `app/api`, and permission gates through `requirePermission`. +- Existing seed data is organization-scoped and idempotent for already-initialized workspaces. + +## Requirements + +- Add real schema, migrations, server operations, API routes, UI pages, permissions, and seed data for all four collaboration modules. +- Implement complete CRUD for each module's core records, plus the key status transitions needed for daily operations. +- Keep all records organization-scoped and prevent cross-organization reads or mutations. +- Add audit logs for create, update, delete, and status transition mutations. +- Replace reserved pages for both unscoped and organization-scoped app routes. +- Keep UI consistent with existing workspaces: server page loads initial data, client workspace handles filters, tables, dialogs, and mutations. +- Preserve TypeScript strictness and avoid new external dependencies. + +## Module Scope + +- 设备运维: equipment/device assets and maintenance tickets. +- 公告通知: notices, publish/retract lifecycle, and account read receipts. +- 规则预警: alert rules and triggered alert records. V1 persists and manages rules/triggers but does not implement a background rule engine. +- 家属服务: family contacts, visit appointments, and family feedback. V1 does not add family login or external messaging. + +## Acceptance Criteria + +- [ ] Four collaboration nav pages render real data-backed workspaces instead of reserved module placeholders. +- [ ] Each module supports list, create, edit, delete, and relevant status transitions through API routes. +- [ ] New data is seeded for local/demo organizations without overwriting existing workspace data. +- [ ] New permissions are registered and assigned to system roles consistently. +- [ ] Mutation API routes require manage permissions, read API routes require read permissions, and cross-organization records cannot be mutated. +- [ ] `pnpm db:generate`, `pnpm type-check`, and `pnpm test` pass. + +## Out of Scope + +- Family-member authentication, family-facing mobile/client portal, SMS/push delivery, and automatic alert-rule evaluation jobs. diff --git a/.trellis/tasks/07-03-collaboration-modules/task.json b/.trellis/tasks/07-03-collaboration-modules/task.json new file mode 100644 index 0000000..2a8a6e1 --- /dev/null +++ b/.trellis/tasks/07-03-collaboration-modules/task.json @@ -0,0 +1,26 @@ +{ + "id": "collaboration-modules", + "name": "collaboration-modules", + "title": "Build collaboration modules", + "description": "", + "status": "in_progress", + "dev_type": null, + "scope": null, + "package": null, + "priority": "P2", + "creator": "TalexDreamSoul", + "assignee": "TalexDreamSoul", + "createdAt": "2026-07-03", + "completedAt": null, + "branch": null, + "base_branch": "main", + "worktree_path": null, + "commit": null, + "pr_url": null, + "subtasks": [], + "children": [], + "parent": null, + "relatedFiles": [], + "notes": "", + "meta": {} +} \ No newline at end of file diff --git a/app/(app)/app/[organizationSlug]/alerts/page.tsx b/app/(app)/app/[organizationSlug]/alerts/page.tsx index 53b3915..248e890 100644 --- a/app/(app)/app/[organizationSlug]/alerts/page.tsx +++ b/app/(app)/app/[organizationSlug]/alerts/page.tsx @@ -1,5 +1,5 @@ -import { AlertsModulePage } from "@/modules/shared/components/ReservedModulePages"; +import AlertsPage from "../../alerts/page"; -export default function ScopedAlertsPage(): React.ReactElement { - return ; +export default async function ScopedAlertsPage(): Promise { + return AlertsPage(); } diff --git a/app/(app)/app/[organizationSlug]/devices/page.tsx b/app/(app)/app/[organizationSlug]/devices/page.tsx index dda3413..9f52bd6 100644 --- a/app/(app)/app/[organizationSlug]/devices/page.tsx +++ b/app/(app)/app/[organizationSlug]/devices/page.tsx @@ -1,5 +1,5 @@ -import { DevicesModulePage } from "@/modules/shared/components/ReservedModulePages"; +import DevicesPage from "../../devices/page"; -export default function ScopedDevicesPage(): React.ReactElement { - return ; +export default async function ScopedDevicesPage(): Promise { + return DevicesPage(); } diff --git a/app/(app)/app/[organizationSlug]/family/page.tsx b/app/(app)/app/[organizationSlug]/family/page.tsx index ef33028..1c51e86 100644 --- a/app/(app)/app/[organizationSlug]/family/page.tsx +++ b/app/(app)/app/[organizationSlug]/family/page.tsx @@ -1,5 +1,5 @@ -import { FamilyModulePage } from "@/modules/shared/components/ReservedModulePages"; +import FamilyPage from "../../family/page"; -export default function ScopedFamilyPage(): React.ReactElement { - return ; +export default async function ScopedFamilyPage(): Promise { + return FamilyPage(); } diff --git a/app/(app)/app/[organizationSlug]/notices/page.tsx b/app/(app)/app/[organizationSlug]/notices/page.tsx index 655e5bc..c7e7880 100644 --- a/app/(app)/app/[organizationSlug]/notices/page.tsx +++ b/app/(app)/app/[organizationSlug]/notices/page.tsx @@ -1,5 +1,5 @@ -import { NoticesModulePage } from "@/modules/shared/components/ReservedModulePages"; +import NoticesPage from "../../notices/page"; -export default function ScopedNoticesPage(): React.ReactElement { - return ; +export default async function ScopedNoticesPage(): Promise { + return NoticesPage(); } diff --git a/app/(app)/app/alerts/page.tsx b/app/(app)/app/alerts/page.tsx index be4c71f..3e6ba7b 100644 --- a/app/(app)/app/alerts/page.tsx +++ b/app/(app)/app/alerts/page.tsx @@ -1,5 +1,26 @@ -import { AlertsModulePage } from "@/modules/shared/components/ReservedModulePages"; +import { redirect } from "next/navigation"; -export default function AlertsPage(): React.ReactElement { - return ; +import { getCurrentAuthContext } from "@/modules/core/server/auth"; +import { hasPermission } from "@/modules/core/server/permissions"; +import { AlertsWorkspaceClient } from "@/modules/alerts/components/AlertsWorkspaceClient"; +import { listAlertCenterData } from "@/modules/alerts/server/operations"; +import { getWorkspaceHref } from "@/modules/shared/lib/workspace-routing"; + +export default async function AlertsPage(): Promise { + const context = await getCurrentAuthContext(); + if (!context) { + redirect("/login"); + } + + if (!hasPermission(context.permissions, "alert:read")) { + redirect(getWorkspaceHref(context.organization?.slug, "/dashboard")); + } + + const organizationId = context.organization?.id; + if (!organizationId) { + redirect(getWorkspaceHref(context.organization?.slug, "/dashboard")); + } + + const data = await listAlertCenterData(organizationId); + return ; } diff --git a/app/(app)/app/devices/page.tsx b/app/(app)/app/devices/page.tsx index d5f1cf3..8503c6e 100644 --- a/app/(app)/app/devices/page.tsx +++ b/app/(app)/app/devices/page.tsx @@ -1,5 +1,26 @@ -import { DevicesModulePage } from "@/modules/shared/components/ReservedModulePages"; +import { redirect } from "next/navigation"; -export default function DevicesPage(): React.ReactElement { - return ; +import { getCurrentAuthContext } from "@/modules/core/server/auth"; +import { hasPermission } from "@/modules/core/server/permissions"; +import { DevicesWorkspaceClient } from "@/modules/devices/components/DevicesWorkspaceClient"; +import { listDeviceOperationsData } from "@/modules/devices/server/operations"; +import { getWorkspaceHref } from "@/modules/shared/lib/workspace-routing"; + +export default async function DevicesPage(): Promise { + const context = await getCurrentAuthContext(); + if (!context) { + redirect("/login"); + } + + if (!hasPermission(context.permissions, "device:read")) { + redirect(getWorkspaceHref(context.organization?.slug, "/dashboard")); + } + + const organizationId = context.organization?.id; + if (!organizationId) { + redirect(getWorkspaceHref(context.organization?.slug, "/dashboard")); + } + + const data = await listDeviceOperationsData(organizationId); + return ; } diff --git a/app/(app)/app/family/page.tsx b/app/(app)/app/family/page.tsx index bdf4ec9..41d9a6b 100644 --- a/app/(app)/app/family/page.tsx +++ b/app/(app)/app/family/page.tsx @@ -1,5 +1,26 @@ -import { FamilyModulePage } from "@/modules/shared/components/ReservedModulePages"; +import { redirect } from "next/navigation"; -export default function FamilyPage(): React.ReactElement { - return ; +import { getCurrentAuthContext } from "@/modules/core/server/auth"; +import { hasPermission } from "@/modules/core/server/permissions"; +import { FamilyWorkspaceClient } from "@/modules/family/components/FamilyWorkspaceClient"; +import { listFamilyServiceData } from "@/modules/family/server/operations"; +import { getWorkspaceHref } from "@/modules/shared/lib/workspace-routing"; + +export default async function FamilyPage(): Promise { + const context = await getCurrentAuthContext(); + if (!context) { + redirect("/login"); + } + + if (!hasPermission(context.permissions, "family:read")) { + redirect(getWorkspaceHref(context.organization?.slug, "/dashboard")); + } + + const organizationId = context.organization?.id; + if (!organizationId) { + redirect(getWorkspaceHref(context.organization?.slug, "/dashboard")); + } + + const data = await listFamilyServiceData(organizationId); + return ; } diff --git a/app/(app)/app/notices/page.tsx b/app/(app)/app/notices/page.tsx index 1124480..e06264f 100644 --- a/app/(app)/app/notices/page.tsx +++ b/app/(app)/app/notices/page.tsx @@ -1,5 +1,26 @@ -import { NoticesModulePage } from "@/modules/shared/components/ReservedModulePages"; +import { redirect } from "next/navigation"; -export default function NoticesPage(): React.ReactElement { - return ; +import { getCurrentAuthContext } from "@/modules/core/server/auth"; +import { hasPermission } from "@/modules/core/server/permissions"; +import { NoticesWorkspaceClient } from "@/modules/notices/components/NoticesWorkspaceClient"; +import { listNoticeCenterData } from "@/modules/notices/server/operations"; +import { getWorkspaceHref } from "@/modules/shared/lib/workspace-routing"; + +export default async function NoticesPage(): Promise { + const context = await getCurrentAuthContext(); + if (!context) { + redirect("/login"); + } + + if (!hasPermission(context.permissions, "notice:read")) { + redirect(getWorkspaceHref(context.organization?.slug, "/dashboard")); + } + + const organizationId = context.organization?.id; + if (!organizationId) { + redirect(getWorkspaceHref(context.organization?.slug, "/dashboard")); + } + + const data = await listNoticeCenterData(organizationId, context.account.id); + return ; } diff --git a/app/api/alerts/rules/[id]/route.ts b/app/api/alerts/rules/[id]/route.ts new file mode 100644 index 0000000..0f4e3ea --- /dev/null +++ b/app/api/alerts/rules/[id]/route.ts @@ -0,0 +1,74 @@ +import { jsonFailure, jsonSuccess, readJsonBody } from "@/modules/core/server/api"; +import { recordAuditLog } from "@/modules/core/server/audit"; +import { requirePermission } from "@/modules/core/server/auth"; +import { deleteAlertRule, isAlertMutationFailure, updateAlertRule } from "@/modules/alerts/server/operations"; +import { validateAlertRuleInput } from "@/modules/alerts/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("alert:manage", { action: "alert.rule.update", targetType: "alert_rule", targetId: id }); + if (!auth.success) { + return auth.response; + } + + const organizationId = auth.context.organization?.id; + if (!organizationId) { + return jsonFailure("请选择机构后维护规则", 400); + } + + const input = validateAlertRuleInput(await readJsonBody(request)); + if (!input.success) { + return jsonFailure(input.reason); + } + + const rule = await updateAlertRule({ ...input.data, id, organizationId }); + if (isAlertMutationFailure(rule)) { + return jsonFailure(rule.reason, rule.status); + } + + await recordAuditLog({ + actor: auth.context.account, + organizationId, + action: "alert.rule.update", + targetType: "alert_rule", + targetId: rule.id, + result: "success", + reason: `更新预警规则:${rule.name}`, + }); + + return jsonSuccess("预警规则已更新", { rule }); +} + +export async function DELETE(_request: Request, context: RouteContext): Promise { + const { id } = await context.params; + const auth = await requirePermission("alert:manage", { action: "alert.rule.delete", targetType: "alert_rule", targetId: id }); + if (!auth.success) { + return auth.response; + } + + const organizationId = auth.context.organization?.id; + if (!organizationId) { + return jsonFailure("请选择机构后维护规则", 400); + } + + const rule = await deleteAlertRule({ id, organizationId }); + if (isAlertMutationFailure(rule)) { + return jsonFailure(rule.reason, rule.status); + } + + await recordAuditLog({ + actor: auth.context.account, + organizationId, + action: "alert.rule.delete", + targetType: "alert_rule", + targetId: rule.id, + result: "success", + reason: `删除预警规则:${rule.name}`, + }); + + return jsonSuccess("预警规则已删除", { rule }); +} diff --git a/app/api/alerts/rules/route.ts b/app/api/alerts/rules/route.ts new file mode 100644 index 0000000..05b7390 --- /dev/null +++ b/app/api/alerts/rules/route.ts @@ -0,0 +1,54 @@ +import { jsonFailure, jsonSuccess, readJsonBody } from "@/modules/core/server/api"; +import { recordAuditLog } from "@/modules/core/server/audit"; +import { requirePermission } from "@/modules/core/server/auth"; +import { createAlertRule, isAlertMutationFailure, listAlertCenterData } from "@/modules/alerts/server/operations"; +import { validateAlertRuleInput } from "@/modules/alerts/types"; + +export async function GET(): Promise { + const auth = await requirePermission("alert:read", { action: "alert.rule.list", targetType: "alert_rule" }); + if (!auth.success) { + return auth.response; + } + + const organizationId = auth.context.organization?.id; + if (!organizationId) { + return jsonFailure("请选择机构后查看规则预警", 400); + } + + const data = await listAlertCenterData(organizationId); + return jsonSuccess("规则预警已加载", { data }); +} + +export async function POST(request: Request): Promise { + const auth = await requirePermission("alert:manage", { action: "alert.rule.create", targetType: "alert_rule" }); + if (!auth.success) { + return auth.response; + } + + const organizationId = auth.context.organization?.id; + if (!organizationId) { + return jsonFailure("请选择机构后维护规则", 400); + } + + const input = validateAlertRuleInput(await readJsonBody(request)); + if (!input.success) { + return jsonFailure(input.reason); + } + + const rule = await createAlertRule({ ...input.data, organizationId }); + if (isAlertMutationFailure(rule)) { + return jsonFailure(rule.reason, rule.status); + } + + await recordAuditLog({ + actor: auth.context.account, + organizationId, + action: "alert.rule.create", + targetType: "alert_rule", + targetId: rule.id, + result: "success", + reason: `创建预警规则:${rule.name}`, + }); + + return jsonSuccess("预警规则已创建", { rule }, 201); +} diff --git a/app/api/alerts/triggers/[id]/route.ts b/app/api/alerts/triggers/[id]/route.ts new file mode 100644 index 0000000..3eaf5f9 --- /dev/null +++ b/app/api/alerts/triggers/[id]/route.ts @@ -0,0 +1,74 @@ +import { jsonFailure, jsonSuccess, readJsonBody } from "@/modules/core/server/api"; +import { recordAuditLog } from "@/modules/core/server/audit"; +import { requirePermission } from "@/modules/core/server/auth"; +import { deleteAlertTrigger, isAlertMutationFailure, updateAlertTrigger } from "@/modules/alerts/server/operations"; +import { validateAlertTriggerInput } from "@/modules/alerts/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("alert:manage", { action: "alert.trigger.update", targetType: "alert_trigger", targetId: id }); + if (!auth.success) { + return auth.response; + } + + const organizationId = auth.context.organization?.id; + if (!organizationId) { + return jsonFailure("请选择机构后维护触发记录", 400); + } + + const input = validateAlertTriggerInput(await readJsonBody(request)); + if (!input.success) { + return jsonFailure(input.reason); + } + + const trigger = await updateAlertTrigger({ ...input.data, id, organizationId, accountId: auth.context.account.id }); + if (isAlertMutationFailure(trigger)) { + return jsonFailure(trigger.reason, trigger.status); + } + + await recordAuditLog({ + actor: auth.context.account, + organizationId, + action: "alert.trigger.update", + targetType: "alert_trigger", + targetId: trigger.id, + result: "success", + reason: `更新预警触发记录:${trigger.title}`, + }); + + return jsonSuccess("预警触发记录已更新", { trigger }); +} + +export async function DELETE(_request: Request, context: RouteContext): Promise { + const { id } = await context.params; + const auth = await requirePermission("alert:manage", { action: "alert.trigger.delete", targetType: "alert_trigger", targetId: id }); + if (!auth.success) { + return auth.response; + } + + const organizationId = auth.context.organization?.id; + if (!organizationId) { + return jsonFailure("请选择机构后维护触发记录", 400); + } + + const trigger = await deleteAlertTrigger({ id, organizationId }); + if (isAlertMutationFailure(trigger)) { + return jsonFailure(trigger.reason, trigger.status); + } + + await recordAuditLog({ + actor: auth.context.account, + organizationId, + action: "alert.trigger.delete", + targetType: "alert_trigger", + targetId: trigger.id, + result: "success", + reason: `删除预警触发记录:${trigger.title}`, + }); + + return jsonSuccess("预警触发记录已删除", { trigger }); +} diff --git a/app/api/alerts/triggers/route.ts b/app/api/alerts/triggers/route.ts new file mode 100644 index 0000000..dd2fa15 --- /dev/null +++ b/app/api/alerts/triggers/route.ts @@ -0,0 +1,39 @@ +import { jsonFailure, jsonSuccess, readJsonBody } from "@/modules/core/server/api"; +import { recordAuditLog } from "@/modules/core/server/audit"; +import { requirePermission } from "@/modules/core/server/auth"; +import { createAlertTrigger, isAlertMutationFailure } from "@/modules/alerts/server/operations"; +import { validateAlertTriggerInput } from "@/modules/alerts/types"; + +export async function POST(request: Request): Promise { + const auth = await requirePermission("alert:manage", { action: "alert.trigger.create", targetType: "alert_trigger" }); + if (!auth.success) { + return auth.response; + } + + const organizationId = auth.context.organization?.id; + if (!organizationId) { + return jsonFailure("请选择机构后维护触发记录", 400); + } + + const input = validateAlertTriggerInput(await readJsonBody(request)); + if (!input.success) { + return jsonFailure(input.reason); + } + + const trigger = await createAlertTrigger({ ...input.data, organizationId, accountId: auth.context.account.id }); + if (isAlertMutationFailure(trigger)) { + return jsonFailure(trigger.reason, trigger.status); + } + + await recordAuditLog({ + actor: auth.context.account, + organizationId, + action: "alert.trigger.create", + targetType: "alert_trigger", + targetId: trigger.id, + result: "success", + reason: `创建预警触发记录:${trigger.title}`, + }); + + return jsonSuccess("预警触发记录已创建", { trigger }, 201); +} diff --git a/app/api/collaboration-routes.test.ts b/app/api/collaboration-routes.test.ts new file mode 100644 index 0000000..e5fc3d6 --- /dev/null +++ b/app/api/collaboration-routes.test.ts @@ -0,0 +1,336 @@ +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 { AlertCenterData, AlertRule } from "@/modules/alerts/types"; +import type { DeviceAsset, DeviceOperationsData } from "@/modules/devices/types"; +import type { FamilyContact, FamilyServiceData } from "@/modules/family/types"; +import type { Notice } from "@/modules/notices/types"; +import { + createAlertRule, + listAlertCenterData, + updateAlertTrigger, +} from "@/modules/alerts/server/operations"; +import { + createDeviceAsset, + listDeviceOperationsData, + updateMaintenanceTicket, +} from "@/modules/devices/server/operations"; +import { + createFamilyContact, + listFamilyServiceData, + updateFamilyVisit, +} from "@/modules/family/server/operations"; +import { + createNotice, + listNoticeCenterData, + markNoticeRead, +} from "@/modules/notices/server/operations"; + +vi.mock("@/modules/core/server/auth", () => ({ + requirePermission: vi.fn(), +})); + +vi.mock("@/modules/core/server/audit", () => ({ + recordAuditLog: vi.fn(), +})); + +vi.mock("@/modules/devices/server/operations", () => ({ + createDeviceAsset: vi.fn(), + isDeviceMutationFailure: vi.fn((value: unknown) => typeof value === "object" && value !== null && "success" in value && value.success === false), + listDeviceOperationsData: vi.fn(), + updateMaintenanceTicket: vi.fn(), +})); + +vi.mock("@/modules/notices/server/operations", () => ({ + createNotice: vi.fn(), + isNoticeMutationFailure: vi.fn((value: unknown) => typeof value === "object" && value !== null && "success" in value && value.success === false), + listNoticeCenterData: vi.fn(), + markNoticeRead: vi.fn(), +})); + +vi.mock("@/modules/alerts/server/operations", () => ({ + createAlertRule: vi.fn(), + isAlertMutationFailure: vi.fn((value: unknown) => typeof value === "object" && value !== null && "success" in value && value.success === false), + listAlertCenterData: vi.fn(), + updateAlertTrigger: vi.fn(), +})); + +vi.mock("@/modules/family/server/operations", () => ({ + createFamilyContact: vi.fn(), + isFamilyMutationFailure: vi.fn((value: unknown) => typeof value === "object" && value !== null && "success" in value && value.success === false), + listFamilyServiceData: vi.fn(), + updateFamilyVisit: 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: ["device:read", "device:manage", "notice:read", "notice:manage", "alert:read", "alert:manage", "family:read", "family: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 device: DeviceAsset = { + id: "device-1", + organizationId: "org-1", + name: "床头呼叫器", + code: "DEV-1", + category: "呼叫系统", + location: "A102", + status: "active", + lastInspectedAt: undefined, + notes: "", + createdAt: "2026-07-02T00:00:00.000Z", + updatedAt: "2026-07-02T00:00:00.000Z", +}; + +const deviceData: DeviceOperationsData = { + metrics: { activeDevices: 1, maintenanceDevices: 0, openTickets: 0, urgentTickets: 0 }, + devices: [device], + tickets: [], +}; + +const notice: Notice = { + id: "notice-1", + organizationId: "org-1", + title: "探访安排", + content: "周六探访安排", + audience: "全体员工", + status: "published", + publishedAt: "2026-07-02T00:00:00.000Z", + createdByAccountId: "account-1", + createdByName: "Admin", + updatedByAccountId: "account-1", + updatedByName: "Admin", + readCount: 0, + hasRead: false, + createdAt: "2026-07-02T00:00:00.000Z", + updatedAt: "2026-07-02T00:00:00.000Z", +}; + +const alertRule: AlertRule = { + id: "rule-1", + organizationId: "org-1", + name: "血氧低值连续告警", + ruleType: "health", + severity: "critical", + status: "enabled", + conditionSummary: "血氧低于阈值", + suggestion: "通知医生", + createdAt: "2026-07-02T00:00:00.000Z", + updatedAt: "2026-07-02T00:00:00.000Z", +}; + +const alertData: AlertCenterData = { + metrics: { enabledRules: 1, openTriggers: 0, criticalTriggers: 0, resolvedTriggers: 0 }, + rules: [alertRule], + triggers: [], +}; + +const familyContact: FamilyContact = { + id: "contact-1", + organizationId: "org-1", + elderId: "elder-1", + elderName: "王桂兰", + name: "张敏", + relationship: "女儿", + phone: "13800000001", + status: "active", + notes: "", + createdAt: "2026-07-02T00:00:00.000Z", + updatedAt: "2026-07-02T00:00:00.000Z", +}; + +const familyData: FamilyServiceData = { + metrics: { activeContacts: 1, pendingVisits: 0, openFeedback: 0, resolvedFeedback: 0 }, + elderOptions: [{ id: "elder-1", name: "王桂兰" }], + contacts: [familyContact], + visits: [], + feedback: [], +}; + +function allowAuth(overrides: Partial = {}): void { + vi.mocked(requirePermission).mockResolvedValue({ success: true, context: createAuthContext(overrides) }); +} + +function jsonRequest(body: Record): Request { + return new Request("http://localhost/api/collaboration", { + method: "POST", + 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("collaboration API routes", () => { + it("loads device operations for the active organization", async () => { + vi.mocked(listDeviceOperationsData).mockResolvedValue(deviceData); + + const { GET } = await import("./devices/assets/route"); + const response = await GET(); + const body = await readJson(response); + + expect(response.status).toBe(200); + expect(body.data).toEqual(deviceData); + expect(listDeviceOperationsData).toHaveBeenCalledWith("org-1"); + }); + + it("requires active organization before loading notices", async () => { + allowAuth({ organization: undefined }); + + const { GET } = await import("./notices/route"); + const response = await GET(); + const body = await readJson(response); + + expect(response.status).toBe(400); + expect(body.reason).toBe("请选择机构后查看公告"); + expect(listNoticeCenterData).not.toHaveBeenCalled(); + }); + + it("creates a device asset and records audit", async () => { + vi.mocked(createDeviceAsset).mockResolvedValue(device); + + const { POST } = await import("./devices/assets/route"); + const response = await POST(jsonRequest({ name: "床头呼叫器", code: "DEV-1", status: "active" })); + const body = await readJson(response); + + expect(response.status).toBe(201); + expect(body.success).toBe(true); + expect(createDeviceAsset).toHaveBeenCalledWith(expect.objectContaining({ organizationId: "org-1" })); + expect(recordAuditLog).toHaveBeenCalledWith(expect.objectContaining({ action: "device.asset.create" })); + }); + + it("updates a maintenance ticket and propagates cross-organization failures", async () => { + vi.mocked(updateMaintenanceTicket).mockResolvedValue({ success: false, reason: "维修工单不存在", status: 404 }); + + const { PATCH } = await import("./devices/tickets/[id]/route"); + const response = await PATCH(jsonRequest({ title: "保养", priority: "normal", status: "open" }), { + params: Promise.resolve({ id: "ticket-from-other-org" }), + }); + const body = await readJson(response); + + expect(response.status).toBe(404); + expect(body.reason).toBe("维修工单不存在"); + expect(recordAuditLog).not.toHaveBeenCalled(); + }); + + it("creates and marks notices read", async () => { + vi.mocked(createNotice).mockResolvedValue(notice); + vi.mocked(markNoticeRead).mockResolvedValue({ ...notice, hasRead: true, readCount: 1 }); + + const { POST } = await import("./notices/route"); + const createResponse = await POST(jsonRequest({ title: "探访安排", content: "周六探访安排", status: "published" })); + expect(createResponse.status).toBe(201); + expect(recordAuditLog).toHaveBeenCalledWith(expect.objectContaining({ action: "notice.create" })); + + vi.clearAllMocks(); + allowAuth(); + const { POST: markReadPost } = await import("./notices/[id]/route"); + const readResponse = await markReadPost(jsonRequest({}), { params: Promise.resolve({ id: "notice-1" }) }); + expect(readResponse.status).toBe(200); + expect(markNoticeRead).toHaveBeenCalledWith({ accountId: "account-1", id: "notice-1", organizationId: "org-1" }); + }); + + it("loads and creates alert rules", async () => { + vi.mocked(listAlertCenterData).mockResolvedValue(alertData); + vi.mocked(createAlertRule).mockResolvedValue(alertRule); + + const { GET, POST } = await import("./alerts/rules/route"); + const listResponse = await GET(); + expect(listResponse.status).toBe(200); + expect(listAlertCenterData).toHaveBeenCalledWith("org-1"); + + const createResponse = await POST(jsonRequest({ name: "血氧低值连续告警", ruleType: "health", severity: "critical", status: "enabled" })); + expect(createResponse.status).toBe(201); + expect(recordAuditLog).toHaveBeenCalledWith(expect.objectContaining({ action: "alert.rule.create" })); + }); + + it("updates alert trigger failures without audit", async () => { + vi.mocked(updateAlertTrigger).mockResolvedValue({ success: false, reason: "预警触发记录不存在", status: 404 }); + + const { PATCH } = await import("./alerts/triggers/[id]/route"); + const response = await PATCH(jsonRequest({ title: "触发", status: "resolved" }), { params: Promise.resolve({ id: "trigger-other-org" }) }); + const body = await readJson(response); + + expect(response.status).toBe(404); + expect(body.reason).toBe("预警触发记录不存在"); + expect(recordAuditLog).not.toHaveBeenCalled(); + }); + + it("loads family services and creates contacts", async () => { + vi.mocked(listFamilyServiceData).mockResolvedValue(familyData); + vi.mocked(createFamilyContact).mockResolvedValue(familyContact); + + const { GET, POST } = await import("./family/contacts/route"); + const listResponse = await GET(); + expect(listResponse.status).toBe(200); + expect(listFamilyServiceData).toHaveBeenCalledWith("org-1"); + + const createResponse = await POST( + jsonRequest({ elderId: "elder-1", name: "张敏", relationship: "女儿", phone: "13800000001", status: "active" }), + ); + expect(createResponse.status).toBe(201); + expect(recordAuditLog).toHaveBeenCalledWith(expect.objectContaining({ action: "family.contact.create" })); + }); + + it("updates family visit failures without audit", async () => { + vi.mocked(updateFamilyVisit).mockResolvedValue({ success: false, reason: "探访预约不存在", status: 404 }); + + const { PATCH } = await import("./family/visits/[id]/route"); + const response = await PATCH( + jsonRequest({ elderId: "elder-1", scheduledAt: "2026-07-02T08:00:00.000Z", status: "approved" }), + { params: Promise.resolve({ id: "visit-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/devices/assets/[id]/route.ts b/app/api/devices/assets/[id]/route.ts new file mode 100644 index 0000000..70b1706 --- /dev/null +++ b/app/api/devices/assets/[id]/route.ts @@ -0,0 +1,74 @@ +import { jsonFailure, jsonSuccess, readJsonBody } from "@/modules/core/server/api"; +import { recordAuditLog } from "@/modules/core/server/audit"; +import { requirePermission } from "@/modules/core/server/auth"; +import { deleteDeviceAsset, isDeviceMutationFailure, updateDeviceAsset } from "@/modules/devices/server/operations"; +import { validateDeviceAssetInput } from "@/modules/devices/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("device:manage", { action: "device.asset.update", targetType: "device_asset", targetId: id }); + if (!auth.success) { + return auth.response; + } + + const organizationId = auth.context.organization?.id; + if (!organizationId) { + return jsonFailure("请选择机构后维护设备", 400); + } + + const input = validateDeviceAssetInput(await readJsonBody(request)); + if (!input.success) { + return jsonFailure(input.reason); + } + + const device = await updateDeviceAsset({ ...input.data, id, organizationId }); + if (isDeviceMutationFailure(device)) { + return jsonFailure(device.reason, device.status); + } + + await recordAuditLog({ + actor: auth.context.account, + organizationId, + action: "device.asset.update", + targetType: "device_asset", + targetId: device.id, + result: "success", + reason: `更新设备:${device.name}`, + }); + + return jsonSuccess("设备已更新", { device }); +} + +export async function DELETE(_request: Request, context: RouteContext): Promise { + const { id } = await context.params; + const auth = await requirePermission("device:manage", { action: "device.asset.delete", targetType: "device_asset", targetId: id }); + if (!auth.success) { + return auth.response; + } + + const organizationId = auth.context.organization?.id; + if (!organizationId) { + return jsonFailure("请选择机构后维护设备", 400); + } + + const device = await deleteDeviceAsset({ id, organizationId }); + if (isDeviceMutationFailure(device)) { + return jsonFailure(device.reason, device.status); + } + + await recordAuditLog({ + actor: auth.context.account, + organizationId, + action: "device.asset.delete", + targetType: "device_asset", + targetId: device.id, + result: "success", + reason: `删除设备:${device.name}`, + }); + + return jsonSuccess("设备已删除", { device }); +} diff --git a/app/api/devices/assets/route.ts b/app/api/devices/assets/route.ts new file mode 100644 index 0000000..7b156b8 --- /dev/null +++ b/app/api/devices/assets/route.ts @@ -0,0 +1,54 @@ +import { jsonFailure, jsonSuccess, readJsonBody } from "@/modules/core/server/api"; +import { recordAuditLog } from "@/modules/core/server/audit"; +import { requirePermission } from "@/modules/core/server/auth"; +import { createDeviceAsset, isDeviceMutationFailure, listDeviceOperationsData } from "@/modules/devices/server/operations"; +import { validateDeviceAssetInput } from "@/modules/devices/types"; + +export async function GET(): Promise { + const auth = await requirePermission("device:read", { action: "device.asset.list", targetType: "device_asset" }); + if (!auth.success) { + return auth.response; + } + + const organizationId = auth.context.organization?.id; + if (!organizationId) { + return jsonFailure("请选择机构后查看设备运维", 400); + } + + const data = await listDeviceOperationsData(organizationId); + return jsonSuccess("设备运维数据已加载", { data }); +} + +export async function POST(request: Request): Promise { + const auth = await requirePermission("device:manage", { action: "device.asset.create", targetType: "device_asset" }); + if (!auth.success) { + return auth.response; + } + + const organizationId = auth.context.organization?.id; + if (!organizationId) { + return jsonFailure("请选择机构后维护设备", 400); + } + + const input = validateDeviceAssetInput(await readJsonBody(request)); + if (!input.success) { + return jsonFailure(input.reason); + } + + const device = await createDeviceAsset({ ...input.data, organizationId }); + if (isDeviceMutationFailure(device)) { + return jsonFailure(device.reason, device.status); + } + + await recordAuditLog({ + actor: auth.context.account, + organizationId, + action: "device.asset.create", + targetType: "device_asset", + targetId: device.id, + result: "success", + reason: `创建设备:${device.name}`, + }); + + return jsonSuccess("设备已创建", { device }, 201); +} diff --git a/app/api/devices/tickets/[id]/route.ts b/app/api/devices/tickets/[id]/route.ts new file mode 100644 index 0000000..2c49ec0 --- /dev/null +++ b/app/api/devices/tickets/[id]/route.ts @@ -0,0 +1,74 @@ +import { jsonFailure, jsonSuccess, readJsonBody } from "@/modules/core/server/api"; +import { recordAuditLog } from "@/modules/core/server/audit"; +import { requirePermission } from "@/modules/core/server/auth"; +import { deleteMaintenanceTicket, isDeviceMutationFailure, updateMaintenanceTicket } from "@/modules/devices/server/operations"; +import { validateMaintenanceTicketInput } from "@/modules/devices/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("device:manage", { action: "device.ticket.update", targetType: "maintenance_ticket", targetId: id }); + if (!auth.success) { + return auth.response; + } + + const organizationId = auth.context.organization?.id; + if (!organizationId) { + return jsonFailure("请选择机构后维护工单", 400); + } + + const input = validateMaintenanceTicketInput(await readJsonBody(request)); + if (!input.success) { + return jsonFailure(input.reason); + } + + const ticket = await updateMaintenanceTicket({ ...input.data, id, organizationId }); + if (isDeviceMutationFailure(ticket)) { + return jsonFailure(ticket.reason, ticket.status); + } + + await recordAuditLog({ + actor: auth.context.account, + organizationId, + action: "device.ticket.update", + targetType: "maintenance_ticket", + targetId: ticket.id, + result: "success", + reason: `更新维修工单:${ticket.title}`, + }); + + return jsonSuccess("维修工单已更新", { ticket }); +} + +export async function DELETE(_request: Request, context: RouteContext): Promise { + const { id } = await context.params; + const auth = await requirePermission("device:manage", { action: "device.ticket.delete", targetType: "maintenance_ticket", targetId: id }); + if (!auth.success) { + return auth.response; + } + + const organizationId = auth.context.organization?.id; + if (!organizationId) { + return jsonFailure("请选择机构后维护工单", 400); + } + + const ticket = await deleteMaintenanceTicket({ id, organizationId }); + if (isDeviceMutationFailure(ticket)) { + return jsonFailure(ticket.reason, ticket.status); + } + + await recordAuditLog({ + actor: auth.context.account, + organizationId, + action: "device.ticket.delete", + targetType: "maintenance_ticket", + targetId: ticket.id, + result: "success", + reason: `删除维修工单:${ticket.title}`, + }); + + return jsonSuccess("维修工单已删除", { ticket }); +} diff --git a/app/api/devices/tickets/route.ts b/app/api/devices/tickets/route.ts new file mode 100644 index 0000000..d4c7ccd --- /dev/null +++ b/app/api/devices/tickets/route.ts @@ -0,0 +1,39 @@ +import { jsonFailure, jsonSuccess, readJsonBody } from "@/modules/core/server/api"; +import { recordAuditLog } from "@/modules/core/server/audit"; +import { requirePermission } from "@/modules/core/server/auth"; +import { createMaintenanceTicket, isDeviceMutationFailure } from "@/modules/devices/server/operations"; +import { validateMaintenanceTicketInput } from "@/modules/devices/types"; + +export async function POST(request: Request): Promise { + const auth = await requirePermission("device:manage", { action: "device.ticket.create", targetType: "maintenance_ticket" }); + if (!auth.success) { + return auth.response; + } + + const organizationId = auth.context.organization?.id; + if (!organizationId) { + return jsonFailure("请选择机构后维护工单", 400); + } + + const input = validateMaintenanceTicketInput(await readJsonBody(request)); + if (!input.success) { + return jsonFailure(input.reason); + } + + const ticket = await createMaintenanceTicket({ ...input.data, organizationId }); + if (isDeviceMutationFailure(ticket)) { + return jsonFailure(ticket.reason, ticket.status); + } + + await recordAuditLog({ + actor: auth.context.account, + organizationId, + action: "device.ticket.create", + targetType: "maintenance_ticket", + targetId: ticket.id, + result: "success", + reason: `创建维修工单:${ticket.title}`, + }); + + return jsonSuccess("维修工单已创建", { ticket }, 201); +} diff --git a/app/api/family/contacts/[id]/route.ts b/app/api/family/contacts/[id]/route.ts new file mode 100644 index 0000000..ac624b4 --- /dev/null +++ b/app/api/family/contacts/[id]/route.ts @@ -0,0 +1,74 @@ +import { jsonFailure, jsonSuccess, readJsonBody } from "@/modules/core/server/api"; +import { recordAuditLog } from "@/modules/core/server/audit"; +import { requirePermission } from "@/modules/core/server/auth"; +import { deleteFamilyContact, isFamilyMutationFailure, updateFamilyContact } from "@/modules/family/server/operations"; +import { validateFamilyContactInput } from "@/modules/family/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("family:manage", { action: "family.contact.update", targetType: "family_contact", targetId: id }); + if (!auth.success) { + return auth.response; + } + + const organizationId = auth.context.organization?.id; + if (!organizationId) { + return jsonFailure("请选择机构后维护家属联系人", 400); + } + + const input = validateFamilyContactInput(await readJsonBody(request)); + if (!input.success) { + return jsonFailure(input.reason); + } + + const contact = await updateFamilyContact({ ...input.data, id, organizationId }); + if (isFamilyMutationFailure(contact)) { + return jsonFailure(contact.reason, contact.status); + } + + await recordAuditLog({ + actor: auth.context.account, + organizationId, + action: "family.contact.update", + targetType: "family_contact", + targetId: contact.id, + result: "success", + reason: `更新家属联系人:${contact.name}`, + }); + + return jsonSuccess("家属联系人已更新", { contact }); +} + +export async function DELETE(_request: Request, context: RouteContext): Promise { + const { id } = await context.params; + const auth = await requirePermission("family:manage", { action: "family.contact.delete", targetType: "family_contact", targetId: id }); + if (!auth.success) { + return auth.response; + } + + const organizationId = auth.context.organization?.id; + if (!organizationId) { + return jsonFailure("请选择机构后维护家属联系人", 400); + } + + const contact = await deleteFamilyContact({ id, organizationId }); + if (isFamilyMutationFailure(contact)) { + return jsonFailure(contact.reason, contact.status); + } + + await recordAuditLog({ + actor: auth.context.account, + organizationId, + action: "family.contact.delete", + targetType: "family_contact", + targetId: contact.id, + result: "success", + reason: `删除家属联系人:${contact.name}`, + }); + + return jsonSuccess("家属联系人已删除", { contact }); +} diff --git a/app/api/family/contacts/route.ts b/app/api/family/contacts/route.ts new file mode 100644 index 0000000..191d17d --- /dev/null +++ b/app/api/family/contacts/route.ts @@ -0,0 +1,54 @@ +import { jsonFailure, jsonSuccess, readJsonBody } from "@/modules/core/server/api"; +import { recordAuditLog } from "@/modules/core/server/audit"; +import { requirePermission } from "@/modules/core/server/auth"; +import { createFamilyContact, isFamilyMutationFailure, listFamilyServiceData } from "@/modules/family/server/operations"; +import { validateFamilyContactInput } from "@/modules/family/types"; + +export async function GET(): Promise { + const auth = await requirePermission("family:read", { action: "family.contact.list", targetType: "family_contact" }); + if (!auth.success) { + return auth.response; + } + + const organizationId = auth.context.organization?.id; + if (!organizationId) { + return jsonFailure("请选择机构后查看家属服务", 400); + } + + const data = await listFamilyServiceData(organizationId); + return jsonSuccess("家属服务已加载", { data }); +} + +export async function POST(request: Request): Promise { + const auth = await requirePermission("family:manage", { action: "family.contact.create", targetType: "family_contact" }); + if (!auth.success) { + return auth.response; + } + + const organizationId = auth.context.organization?.id; + if (!organizationId) { + return jsonFailure("请选择机构后维护家属联系人", 400); + } + + const input = validateFamilyContactInput(await readJsonBody(request)); + if (!input.success) { + return jsonFailure(input.reason); + } + + const contact = await createFamilyContact({ ...input.data, organizationId }); + if (isFamilyMutationFailure(contact)) { + return jsonFailure(contact.reason, contact.status); + } + + await recordAuditLog({ + actor: auth.context.account, + organizationId, + action: "family.contact.create", + targetType: "family_contact", + targetId: contact.id, + result: "success", + reason: `创建家属联系人:${contact.name}`, + }); + + return jsonSuccess("家属联系人已创建", { contact }, 201); +} diff --git a/app/api/family/feedback/[id]/route.ts b/app/api/family/feedback/[id]/route.ts new file mode 100644 index 0000000..2ce8114 --- /dev/null +++ b/app/api/family/feedback/[id]/route.ts @@ -0,0 +1,74 @@ +import { jsonFailure, jsonSuccess, readJsonBody } from "@/modules/core/server/api"; +import { recordAuditLog } from "@/modules/core/server/audit"; +import { requirePermission } from "@/modules/core/server/auth"; +import { deleteFamilyFeedback, isFamilyMutationFailure, updateFamilyFeedback } from "@/modules/family/server/operations"; +import { validateFamilyFeedbackInput } from "@/modules/family/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("family:manage", { action: "family.feedback.update", targetType: "family_feedback", targetId: id }); + if (!auth.success) { + return auth.response; + } + + const organizationId = auth.context.organization?.id; + if (!organizationId) { + return jsonFailure("请选择机构后维护家属反馈", 400); + } + + const input = validateFamilyFeedbackInput(await readJsonBody(request)); + if (!input.success) { + return jsonFailure(input.reason); + } + + const feedback = await updateFamilyFeedback({ ...input.data, id, organizationId, accountId: auth.context.account.id }); + if (isFamilyMutationFailure(feedback)) { + return jsonFailure(feedback.reason, feedback.status); + } + + await recordAuditLog({ + actor: auth.context.account, + organizationId, + action: "family.feedback.update", + targetType: "family_feedback", + targetId: feedback.id, + result: "success", + reason: `更新家属反馈:${feedback.elderName}`, + }); + + return jsonSuccess("家属反馈已更新", { feedback }); +} + +export async function DELETE(_request: Request, context: RouteContext): Promise { + const { id } = await context.params; + const auth = await requirePermission("family:manage", { action: "family.feedback.delete", targetType: "family_feedback", targetId: id }); + if (!auth.success) { + return auth.response; + } + + const organizationId = auth.context.organization?.id; + if (!organizationId) { + return jsonFailure("请选择机构后维护家属反馈", 400); + } + + const feedback = await deleteFamilyFeedback({ id, organizationId }); + if (isFamilyMutationFailure(feedback)) { + return jsonFailure(feedback.reason, feedback.status); + } + + await recordAuditLog({ + actor: auth.context.account, + organizationId, + action: "family.feedback.delete", + targetType: "family_feedback", + targetId: feedback.id, + result: "success", + reason: `删除家属反馈:${feedback.elderName}`, + }); + + return jsonSuccess("家属反馈已删除", { feedback }); +} diff --git a/app/api/family/feedback/route.ts b/app/api/family/feedback/route.ts new file mode 100644 index 0000000..27f39d3 --- /dev/null +++ b/app/api/family/feedback/route.ts @@ -0,0 +1,39 @@ +import { jsonFailure, jsonSuccess, readJsonBody } from "@/modules/core/server/api"; +import { recordAuditLog } from "@/modules/core/server/audit"; +import { requirePermission } from "@/modules/core/server/auth"; +import { createFamilyFeedback, isFamilyMutationFailure } from "@/modules/family/server/operations"; +import { validateFamilyFeedbackInput } from "@/modules/family/types"; + +export async function POST(request: Request): Promise { + const auth = await requirePermission("family:manage", { action: "family.feedback.create", targetType: "family_feedback" }); + if (!auth.success) { + return auth.response; + } + + const organizationId = auth.context.organization?.id; + if (!organizationId) { + return jsonFailure("请选择机构后维护家属反馈", 400); + } + + const input = validateFamilyFeedbackInput(await readJsonBody(request)); + if (!input.success) { + return jsonFailure(input.reason); + } + + const feedback = await createFamilyFeedback({ ...input.data, organizationId, accountId: auth.context.account.id }); + if (isFamilyMutationFailure(feedback)) { + return jsonFailure(feedback.reason, feedback.status); + } + + await recordAuditLog({ + actor: auth.context.account, + organizationId, + action: "family.feedback.create", + targetType: "family_feedback", + targetId: feedback.id, + result: "success", + reason: `创建家属反馈:${feedback.elderName}`, + }); + + return jsonSuccess("家属反馈已创建", { feedback }, 201); +} diff --git a/app/api/family/visits/[id]/route.ts b/app/api/family/visits/[id]/route.ts new file mode 100644 index 0000000..fd2bb5f --- /dev/null +++ b/app/api/family/visits/[id]/route.ts @@ -0,0 +1,74 @@ +import { jsonFailure, jsonSuccess, readJsonBody } from "@/modules/core/server/api"; +import { recordAuditLog } from "@/modules/core/server/audit"; +import { requirePermission } from "@/modules/core/server/auth"; +import { deleteFamilyVisit, isFamilyMutationFailure, updateFamilyVisit } from "@/modules/family/server/operations"; +import { validateFamilyVisitInput } from "@/modules/family/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("family:manage", { action: "family.visit.update", targetType: "family_visit", targetId: id }); + if (!auth.success) { + return auth.response; + } + + const organizationId = auth.context.organization?.id; + if (!organizationId) { + return jsonFailure("请选择机构后维护探访预约", 400); + } + + const input = validateFamilyVisitInput(await readJsonBody(request)); + if (!input.success) { + return jsonFailure(input.reason); + } + + const visit = await updateFamilyVisit({ ...input.data, id, organizationId, accountId: auth.context.account.id }); + if (isFamilyMutationFailure(visit)) { + return jsonFailure(visit.reason, visit.status); + } + + await recordAuditLog({ + actor: auth.context.account, + organizationId, + action: "family.visit.update", + targetType: "family_visit", + targetId: visit.id, + result: "success", + reason: `更新探访预约:${visit.elderName}`, + }); + + return jsonSuccess("探访预约已更新", { visit }); +} + +export async function DELETE(_request: Request, context: RouteContext): Promise { + const { id } = await context.params; + const auth = await requirePermission("family:manage", { action: "family.visit.delete", targetType: "family_visit", targetId: id }); + if (!auth.success) { + return auth.response; + } + + const organizationId = auth.context.organization?.id; + if (!organizationId) { + return jsonFailure("请选择机构后维护探访预约", 400); + } + + const visit = await deleteFamilyVisit({ id, organizationId }); + if (isFamilyMutationFailure(visit)) { + return jsonFailure(visit.reason, visit.status); + } + + await recordAuditLog({ + actor: auth.context.account, + organizationId, + action: "family.visit.delete", + targetType: "family_visit", + targetId: visit.id, + result: "success", + reason: `删除探访预约:${visit.elderName}`, + }); + + return jsonSuccess("探访预约已删除", { visit }); +} diff --git a/app/api/family/visits/route.ts b/app/api/family/visits/route.ts new file mode 100644 index 0000000..472537d --- /dev/null +++ b/app/api/family/visits/route.ts @@ -0,0 +1,39 @@ +import { jsonFailure, jsonSuccess, readJsonBody } from "@/modules/core/server/api"; +import { recordAuditLog } from "@/modules/core/server/audit"; +import { requirePermission } from "@/modules/core/server/auth"; +import { createFamilyVisit, isFamilyMutationFailure } from "@/modules/family/server/operations"; +import { validateFamilyVisitInput } from "@/modules/family/types"; + +export async function POST(request: Request): Promise { + const auth = await requirePermission("family:manage", { action: "family.visit.create", targetType: "family_visit" }); + if (!auth.success) { + return auth.response; + } + + const organizationId = auth.context.organization?.id; + if (!organizationId) { + return jsonFailure("请选择机构后维护探访预约", 400); + } + + const input = validateFamilyVisitInput(await readJsonBody(request)); + if (!input.success) { + return jsonFailure(input.reason); + } + + const visit = await createFamilyVisit({ ...input.data, organizationId, accountId: auth.context.account.id }); + if (isFamilyMutationFailure(visit)) { + return jsonFailure(visit.reason, visit.status); + } + + await recordAuditLog({ + actor: auth.context.account, + organizationId, + action: "family.visit.create", + targetType: "family_visit", + targetId: visit.id, + result: "success", + reason: `创建探访预约:${visit.elderName}`, + }); + + return jsonSuccess("探访预约已创建", { visit }, 201); +} diff --git a/app/api/notices/[id]/route.ts b/app/api/notices/[id]/route.ts new file mode 100644 index 0000000..4dc6131 --- /dev/null +++ b/app/api/notices/[id]/route.ts @@ -0,0 +1,94 @@ +import { jsonFailure, jsonSuccess, readJsonBody } from "@/modules/core/server/api"; +import { recordAuditLog } from "@/modules/core/server/audit"; +import { requirePermission } from "@/modules/core/server/auth"; +import { deleteNotice, isNoticeMutationFailure, markNoticeRead, updateNotice } from "@/modules/notices/server/operations"; +import { validateNoticeInput } from "@/modules/notices/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("notice:manage", { action: "notice.update", targetType: "notice", targetId: id }); + if (!auth.success) { + return auth.response; + } + + const organizationId = auth.context.organization?.id; + if (!organizationId) { + return jsonFailure("请选择机构后维护公告", 400); + } + + const input = validateNoticeInput(await readJsonBody(request)); + if (!input.success) { + return jsonFailure(input.reason); + } + + const notice = await updateNotice({ ...input.data, id, organizationId, accountId: auth.context.account.id }); + if (isNoticeMutationFailure(notice)) { + return jsonFailure(notice.reason, notice.status); + } + + await recordAuditLog({ + actor: auth.context.account, + organizationId, + action: "notice.update", + targetType: "notice", + targetId: notice.id, + result: "success", + reason: `更新公告:${notice.title}`, + }); + + return jsonSuccess("公告已更新", { notice }); +} + +export async function POST(_request: Request, context: RouteContext): Promise { + const { id } = await context.params; + const auth = await requirePermission("notice:read", { action: "notice.read", targetType: "notice", targetId: id }); + if (!auth.success) { + return auth.response; + } + + const organizationId = auth.context.organization?.id; + if (!organizationId) { + return jsonFailure("请选择机构后查看公告", 400); + } + + const notice = await markNoticeRead({ id, organizationId, accountId: auth.context.account.id }); + if (isNoticeMutationFailure(notice)) { + return jsonFailure(notice.reason, notice.status); + } + + return jsonSuccess("公告已标记已读", { notice }); +} + +export async function DELETE(_request: Request, context: RouteContext): Promise { + const { id } = await context.params; + const auth = await requirePermission("notice:manage", { action: "notice.delete", targetType: "notice", targetId: id }); + if (!auth.success) { + return auth.response; + } + + const organizationId = auth.context.organization?.id; + if (!organizationId) { + return jsonFailure("请选择机构后维护公告", 400); + } + + const notice = await deleteNotice({ id, organizationId }); + if (isNoticeMutationFailure(notice)) { + return jsonFailure(notice.reason, notice.status); + } + + await recordAuditLog({ + actor: auth.context.account, + organizationId, + action: "notice.delete", + targetType: "notice", + targetId: notice.id, + result: "success", + reason: `删除公告:${notice.title}`, + }); + + return jsonSuccess("公告已删除", { notice }); +} diff --git a/app/api/notices/route.ts b/app/api/notices/route.ts new file mode 100644 index 0000000..3ef00aa --- /dev/null +++ b/app/api/notices/route.ts @@ -0,0 +1,54 @@ +import { jsonFailure, jsonSuccess, readJsonBody } from "@/modules/core/server/api"; +import { recordAuditLog } from "@/modules/core/server/audit"; +import { requirePermission } from "@/modules/core/server/auth"; +import { createNotice, isNoticeMutationFailure, listNoticeCenterData } from "@/modules/notices/server/operations"; +import { validateNoticeInput } from "@/modules/notices/types"; + +export async function GET(): Promise { + const auth = await requirePermission("notice:read", { action: "notice.list", targetType: "notice" }); + if (!auth.success) { + return auth.response; + } + + const organizationId = auth.context.organization?.id; + if (!organizationId) { + return jsonFailure("请选择机构后查看公告", 400); + } + + const data = await listNoticeCenterData(organizationId, auth.context.account.id); + return jsonSuccess("公告通知已加载", { data }); +} + +export async function POST(request: Request): Promise { + const auth = await requirePermission("notice:manage", { action: "notice.create", targetType: "notice" }); + if (!auth.success) { + return auth.response; + } + + const organizationId = auth.context.organization?.id; + if (!organizationId) { + return jsonFailure("请选择机构后维护公告", 400); + } + + const input = validateNoticeInput(await readJsonBody(request)); + if (!input.success) { + return jsonFailure(input.reason); + } + + const notice = await createNotice({ ...input.data, organizationId, accountId: auth.context.account.id }); + if (isNoticeMutationFailure(notice)) { + return jsonFailure(notice.reason, notice.status); + } + + await recordAuditLog({ + actor: auth.context.account, + organizationId, + action: "notice.create", + targetType: "notice", + targetId: notice.id, + result: "success", + reason: `创建公告:${notice.title}`, + }); + + return jsonSuccess("公告已创建", { notice }, 201); +} diff --git a/drizzle/0005_quiet_sandman.sql b/drizzle/0005_quiet_sandman.sql new file mode 100644 index 0000000..633f86f --- /dev/null +++ b/drizzle/0005_quiet_sandman.sql @@ -0,0 +1,170 @@ +CREATE TYPE "public"."alert_rule_status" AS ENUM('enabled', 'disabled');--> statement-breakpoint +CREATE TYPE "public"."alert_rule_type" AS ENUM('health', 'care', 'device', 'incident', 'admission', 'other');--> statement-breakpoint +CREATE TYPE "public"."alert_trigger_status" AS ENUM('open', 'acknowledged', 'resolved', 'closed');--> statement-breakpoint +CREATE TYPE "public"."device_asset_status" AS ENUM('active', 'maintenance', 'disabled', 'retired');--> statement-breakpoint +CREATE TYPE "public"."family_contact_status" AS ENUM('active', 'suspended', 'archived');--> statement-breakpoint +CREATE TYPE "public"."family_feedback_status" AS ENUM('open', 'in_progress', 'resolved', 'closed');--> statement-breakpoint +CREATE TYPE "public"."family_feedback_type" AS ENUM('service', 'care', 'meal', 'environment', 'other');--> statement-breakpoint +CREATE TYPE "public"."family_visit_status" AS ENUM('requested', 'approved', 'completed', 'cancelled');--> statement-breakpoint +CREATE TYPE "public"."maintenance_ticket_priority" AS ENUM('low', 'normal', 'high', 'urgent');--> statement-breakpoint +CREATE TYPE "public"."maintenance_ticket_status" AS ENUM('open', 'assigned', 'resolved', 'closed', 'cancelled');--> statement-breakpoint +CREATE TYPE "public"."notice_status" AS ENUM('draft', 'published', 'retracted');--> statement-breakpoint +CREATE TABLE "alert_rules" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "organization_id" uuid NOT NULL, + "name" text NOT NULL, + "rule_type" "alert_rule_type" DEFAULT 'other' NOT NULL, + "severity" "incident_severity" DEFAULT 'warning' NOT NULL, + "status" "alert_rule_status" DEFAULT 'enabled' NOT NULL, + "condition_summary" text DEFAULT '' NOT NULL, + "suggestion" text DEFAULT '' NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "alert_triggers" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "organization_id" uuid NOT NULL, + "rule_id" uuid, + "elder_id" uuid, + "title" text NOT NULL, + "description" text DEFAULT '' NOT NULL, + "status" "alert_trigger_status" DEFAULT 'open' NOT NULL, + "source" text DEFAULT '' NOT NULL, + "handled_by_account_id" uuid, + "handled_at" timestamp with time zone, + "handling_notes" text DEFAULT '' NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "device_assets" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "organization_id" uuid NOT NULL, + "name" text NOT NULL, + "code" text NOT NULL, + "category" text DEFAULT '' NOT NULL, + "location" text DEFAULT '' NOT NULL, + "status" "device_asset_status" DEFAULT 'active' NOT NULL, + "last_inspected_at" timestamp with time zone, + "notes" text DEFAULT '' NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "family_contacts" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "organization_id" uuid NOT NULL, + "elder_id" uuid NOT NULL, + "name" text NOT NULL, + "relationship" text NOT NULL, + "phone" text NOT NULL, + "status" "family_contact_status" DEFAULT 'active' NOT NULL, + "notes" text DEFAULT '' NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "family_feedback" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "organization_id" uuid NOT NULL, + "elder_id" uuid NOT NULL, + "contact_id" uuid, + "feedback_type" "family_feedback_type" DEFAULT 'other' NOT NULL, + "content" text NOT NULL, + "status" "family_feedback_status" DEFAULT 'open' NOT NULL, + "response_notes" text DEFAULT '' NOT NULL, + "handled_by_account_id" uuid, + "handled_at" timestamp with time zone, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "family_visit_appointments" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "organization_id" uuid NOT NULL, + "elder_id" uuid NOT NULL, + "contact_id" uuid, + "scheduled_at" timestamp with time zone NOT NULL, + "status" "family_visit_status" DEFAULT 'requested' NOT NULL, + "notes" text DEFAULT '' NOT NULL, + "handled_by_account_id" uuid, + "handled_at" timestamp with time zone, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "maintenance_tickets" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "organization_id" uuid NOT NULL, + "device_id" uuid, + "title" text NOT NULL, + "description" text DEFAULT '' NOT NULL, + "priority" "maintenance_ticket_priority" DEFAULT 'normal' NOT NULL, + "status" "maintenance_ticket_status" DEFAULT 'open' NOT NULL, + "assignee_label" text DEFAULT '' NOT NULL, + "resolution_notes" text DEFAULT '' NOT NULL, + "resolved_at" timestamp with time zone, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "notice_read_receipts" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "organization_id" uuid NOT NULL, + "notice_id" uuid NOT NULL, + "account_id" uuid NOT NULL, + "read_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "notices" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "organization_id" uuid NOT NULL, + "title" text NOT NULL, + "content" text DEFAULT '' NOT NULL, + "audience" text DEFAULT '全体员工' NOT NULL, + "status" "notice_status" DEFAULT 'draft' NOT NULL, + "published_at" timestamp with time zone, + "created_by_account_id" uuid, + "updated_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 "alert_rules" ADD CONSTRAINT "alert_rules_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "alert_triggers" ADD CONSTRAINT "alert_triggers_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "alert_triggers" ADD CONSTRAINT "alert_triggers_rule_id_alert_rules_id_fk" FOREIGN KEY ("rule_id") REFERENCES "public"."alert_rules"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "alert_triggers" ADD CONSTRAINT "alert_triggers_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 "alert_triggers" ADD CONSTRAINT "alert_triggers_handled_by_account_id_accounts_id_fk" FOREIGN KEY ("handled_by_account_id") REFERENCES "public"."accounts"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "device_assets" ADD CONSTRAINT "device_assets_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "family_contacts" ADD CONSTRAINT "family_contacts_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "family_contacts" ADD CONSTRAINT "family_contacts_elder_id_elders_id_fk" FOREIGN KEY ("elder_id") REFERENCES "public"."elders"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "family_feedback" ADD CONSTRAINT "family_feedback_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "family_feedback" ADD CONSTRAINT "family_feedback_elder_id_elders_id_fk" FOREIGN KEY ("elder_id") REFERENCES "public"."elders"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "family_feedback" ADD CONSTRAINT "family_feedback_contact_id_family_contacts_id_fk" FOREIGN KEY ("contact_id") REFERENCES "public"."family_contacts"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "family_feedback" ADD CONSTRAINT "family_feedback_handled_by_account_id_accounts_id_fk" FOREIGN KEY ("handled_by_account_id") REFERENCES "public"."accounts"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "family_visit_appointments" ADD CONSTRAINT "family_visit_appointments_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "family_visit_appointments" ADD CONSTRAINT "family_visit_appointments_elder_id_elders_id_fk" FOREIGN KEY ("elder_id") REFERENCES "public"."elders"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "family_visit_appointments" ADD CONSTRAINT "family_visit_appointments_contact_id_family_contacts_id_fk" FOREIGN KEY ("contact_id") REFERENCES "public"."family_contacts"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "family_visit_appointments" ADD CONSTRAINT "family_visit_appointments_handled_by_account_id_accounts_id_fk" FOREIGN KEY ("handled_by_account_id") REFERENCES "public"."accounts"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "maintenance_tickets" ADD CONSTRAINT "maintenance_tickets_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "maintenance_tickets" ADD CONSTRAINT "maintenance_tickets_device_id_device_assets_id_fk" FOREIGN KEY ("device_id") REFERENCES "public"."device_assets"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "notice_read_receipts" ADD CONSTRAINT "notice_read_receipts_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "notice_read_receipts" ADD CONSTRAINT "notice_read_receipts_notice_id_notices_id_fk" FOREIGN KEY ("notice_id") REFERENCES "public"."notices"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "notice_read_receipts" ADD CONSTRAINT "notice_read_receipts_account_id_accounts_id_fk" FOREIGN KEY ("account_id") REFERENCES "public"."accounts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "notices" ADD CONSTRAINT "notices_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "notices" ADD CONSTRAINT "notices_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 "notices" ADD CONSTRAINT "notices_updated_by_account_id_accounts_id_fk" FOREIGN KEY ("updated_by_account_id") REFERENCES "public"."accounts"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "alert_rules_status_lookup" ON "alert_rules" USING btree ("organization_id","status","rule_type");--> statement-breakpoint +CREATE INDEX "alert_triggers_queue_lookup" ON "alert_triggers" USING btree ("organization_id","status");--> statement-breakpoint +CREATE INDEX "alert_triggers_rule_lookup" ON "alert_triggers" USING btree ("organization_id","rule_id");--> statement-breakpoint +CREATE UNIQUE INDEX "device_assets_code_organization_unique" ON "device_assets" USING btree ("organization_id","code");--> statement-breakpoint +CREATE INDEX "device_assets_status_lookup" ON "device_assets" USING btree ("organization_id","status");--> statement-breakpoint +CREATE INDEX "family_contacts_elder_lookup" ON "family_contacts" USING btree ("organization_id","elder_id","status");--> statement-breakpoint +CREATE INDEX "family_feedback_queue_lookup" ON "family_feedback" USING btree ("organization_id","status","feedback_type");--> statement-breakpoint +CREATE INDEX "family_visit_appointments_schedule_lookup" ON "family_visit_appointments" USING btree ("organization_id","status","scheduled_at");--> statement-breakpoint +CREATE INDEX "maintenance_tickets_queue_lookup" ON "maintenance_tickets" USING btree ("organization_id","status","priority");--> statement-breakpoint +CREATE INDEX "maintenance_tickets_device_lookup" ON "maintenance_tickets" USING btree ("organization_id","device_id");--> statement-breakpoint +CREATE UNIQUE INDEX "notice_read_receipts_account_notice_unique" ON "notice_read_receipts" USING btree ("notice_id","account_id");--> statement-breakpoint +CREATE INDEX "notice_read_receipts_notice_lookup" ON "notice_read_receipts" USING btree ("organization_id","notice_id");--> statement-breakpoint +CREATE INDEX "notices_status_lookup" ON "notices" USING btree ("organization_id","status"); \ No newline at end of file diff --git a/drizzle/meta/0005_snapshot.json b/drizzle/meta/0005_snapshot.json new file mode 100644 index 0000000..fa4d046 --- /dev/null +++ b/drizzle/meta/0005_snapshot.json @@ -0,0 +1,4496 @@ +{ + "id": "2ef6c9ae-30f3-48bd-95af-9103ba70baf6", + "prevId": "4d819091-9b70-4d7e-b656-4b71cff5d1a4", + "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.alert_rules": { + "name": "alert_rules", + "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 + }, + "rule_type": { + "name": "rule_type", + "type": "alert_rule_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'other'" + }, + "severity": { + "name": "severity", + "type": "incident_severity", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'warning'" + }, + "status": { + "name": "status", + "type": "alert_rule_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'enabled'" + }, + "condition_summary": { + "name": "condition_summary", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "suggestion": { + "name": "suggestion", + "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": { + "alert_rules_status_lookup": { + "name": "alert_rules_status_lookup", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "rule_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "alert_rules_organization_id_organizations_id_fk": { + "name": "alert_rules_organization_id_organizations_id_fk", + "tableFrom": "alert_rules", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_triggers": { + "name": "alert_triggers", + "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 + }, + "rule_id": { + "name": "rule_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "elder_id": { + "name": "elder_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "status": { + "name": "status", + "type": "alert_trigger_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "handled_by_account_id": { + "name": "handled_by_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "handled_at": { + "name": "handled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "handling_notes": { + "name": "handling_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": { + "alert_triggers_queue_lookup": { + "name": "alert_triggers_queue_lookup", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "alert_triggers_rule_lookup": { + "name": "alert_triggers_rule_lookup", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "rule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "alert_triggers_organization_id_organizations_id_fk": { + "name": "alert_triggers_organization_id_organizations_id_fk", + "tableFrom": "alert_triggers", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_triggers_rule_id_alert_rules_id_fk": { + "name": "alert_triggers_rule_id_alert_rules_id_fk", + "tableFrom": "alert_triggers", + "tableTo": "alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "alert_triggers_elder_id_elders_id_fk": { + "name": "alert_triggers_elder_id_elders_id_fk", + "tableFrom": "alert_triggers", + "tableTo": "elders", + "columnsFrom": [ + "elder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "alert_triggers_handled_by_account_id_accounts_id_fk": { + "name": "alert_triggers_handled_by_account_id_accounts_id_fk", + "tableFrom": "alert_triggers", + "tableTo": "accounts", + "columnsFrom": [ + "handled_by_account_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.device_assets": { + "name": "device_assets", + "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 + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "status": { + "name": "status", + "type": "device_asset_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_inspected_at": { + "name": "last_inspected_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": { + "device_assets_code_organization_unique": { + "name": "device_assets_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": {} + }, + "device_assets_status_lookup": { + "name": "device_assets_status_lookup", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "device_assets_organization_id_organizations_id_fk": { + "name": "device_assets_organization_id_organizations_id_fk", + "tableFrom": "device_assets", + "tableTo": "organizations", + "columnsFrom": [ + "organization_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.family_contacts": { + "name": "family_contacts", + "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 + }, + "relationship": { + "name": "relationship", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "family_contact_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "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": { + "family_contacts_elder_lookup": { + "name": "family_contacts_elder_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": { + "family_contacts_organization_id_organizations_id_fk": { + "name": "family_contacts_organization_id_organizations_id_fk", + "tableFrom": "family_contacts", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "family_contacts_elder_id_elders_id_fk": { + "name": "family_contacts_elder_id_elders_id_fk", + "tableFrom": "family_contacts", + "tableTo": "elders", + "columnsFrom": [ + "elder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.family_feedback": { + "name": "family_feedback", + "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 + }, + "contact_id": { + "name": "contact_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "feedback_type": { + "name": "feedback_type", + "type": "family_feedback_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'other'" + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "family_feedback_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "response_notes": { + "name": "response_notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "handled_by_account_id": { + "name": "handled_by_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "handled_at": { + "name": "handled_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": { + "family_feedback_queue_lookup": { + "name": "family_feedback_queue_lookup", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "feedback_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "family_feedback_organization_id_organizations_id_fk": { + "name": "family_feedback_organization_id_organizations_id_fk", + "tableFrom": "family_feedback", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "family_feedback_elder_id_elders_id_fk": { + "name": "family_feedback_elder_id_elders_id_fk", + "tableFrom": "family_feedback", + "tableTo": "elders", + "columnsFrom": [ + "elder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "family_feedback_contact_id_family_contacts_id_fk": { + "name": "family_feedback_contact_id_family_contacts_id_fk", + "tableFrom": "family_feedback", + "tableTo": "family_contacts", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "family_feedback_handled_by_account_id_accounts_id_fk": { + "name": "family_feedback_handled_by_account_id_accounts_id_fk", + "tableFrom": "family_feedback", + "tableTo": "accounts", + "columnsFrom": [ + "handled_by_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.family_visit_appointments": { + "name": "family_visit_appointments", + "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 + }, + "contact_id": { + "name": "contact_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "scheduled_at": { + "name": "scheduled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "family_visit_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'requested'" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "handled_by_account_id": { + "name": "handled_by_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "handled_at": { + "name": "handled_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": { + "family_visit_appointments_schedule_lookup": { + "name": "family_visit_appointments_schedule_lookup", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scheduled_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "family_visit_appointments_organization_id_organizations_id_fk": { + "name": "family_visit_appointments_organization_id_organizations_id_fk", + "tableFrom": "family_visit_appointments", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "family_visit_appointments_elder_id_elders_id_fk": { + "name": "family_visit_appointments_elder_id_elders_id_fk", + "tableFrom": "family_visit_appointments", + "tableTo": "elders", + "columnsFrom": [ + "elder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "family_visit_appointments_contact_id_family_contacts_id_fk": { + "name": "family_visit_appointments_contact_id_family_contacts_id_fk", + "tableFrom": "family_visit_appointments", + "tableTo": "family_contacts", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "family_visit_appointments_handled_by_account_id_accounts_id_fk": { + "name": "family_visit_appointments_handled_by_account_id_accounts_id_fk", + "tableFrom": "family_visit_appointments", + "tableTo": "accounts", + "columnsFrom": [ + "handled_by_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "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.maintenance_tickets": { + "name": "maintenance_tickets", + "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 + }, + "device_id": { + "name": "device_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "priority": { + "name": "priority", + "type": "maintenance_ticket_priority", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'normal'" + }, + "status": { + "name": "status", + "type": "maintenance_ticket_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "assignee_label": { + "name": "assignee_label", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "resolution_notes": { + "name": "resolution_notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "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": { + "maintenance_tickets_queue_lookup": { + "name": "maintenance_tickets_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": {} + }, + "maintenance_tickets_device_lookup": { + "name": "maintenance_tickets_device_lookup", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "device_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "maintenance_tickets_organization_id_organizations_id_fk": { + "name": "maintenance_tickets_organization_id_organizations_id_fk", + "tableFrom": "maintenance_tickets", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "maintenance_tickets_device_id_device_assets_id_fk": { + "name": "maintenance_tickets_device_id_device_assets_id_fk", + "tableFrom": "maintenance_tickets", + "tableTo": "device_assets", + "columnsFrom": [ + "device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "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.notice_read_receipts": { + "name": "notice_read_receipts", + "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 + }, + "notice_id": { + "name": "notice_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "read_at": { + "name": "read_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "notice_read_receipts_account_notice_unique": { + "name": "notice_read_receipts_account_notice_unique", + "columns": [ + { + "expression": "notice_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notice_read_receipts_notice_lookup": { + "name": "notice_read_receipts_notice_lookup", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "notice_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notice_read_receipts_organization_id_organizations_id_fk": { + "name": "notice_read_receipts_organization_id_organizations_id_fk", + "tableFrom": "notice_read_receipts", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notice_read_receipts_notice_id_notices_id_fk": { + "name": "notice_read_receipts_notice_id_notices_id_fk", + "tableFrom": "notice_read_receipts", + "tableTo": "notices", + "columnsFrom": [ + "notice_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notice_read_receipts_account_id_accounts_id_fk": { + "name": "notice_read_receipts_account_id_accounts_id_fk", + "tableFrom": "notice_read_receipts", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notices": { + "name": "notices", + "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 + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'全体员工'" + }, + "status": { + "name": "status", + "type": "notice_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "published_at": { + "name": "published_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 + }, + "updated_by_account_id": { + "name": "updated_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": { + "notices_status_lookup": { + "name": "notices_status_lookup", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notices_organization_id_organizations_id_fk": { + "name": "notices_organization_id_organizations_id_fk", + "tableFrom": "notices", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notices_created_by_account_id_accounts_id_fk": { + "name": "notices_created_by_account_id_accounts_id_fk", + "tableFrom": "notices", + "tableTo": "accounts", + "columnsFrom": [ + "created_by_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "notices_updated_by_account_id_accounts_id_fk": { + "name": "notices_updated_by_account_id_accounts_id_fk", + "tableFrom": "notices", + "tableTo": "accounts", + "columnsFrom": [ + "updated_by_account_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.alert_rule_status": { + "name": "alert_rule_status", + "schema": "public", + "values": [ + "enabled", + "disabled" + ] + }, + "public.alert_rule_type": { + "name": "alert_rule_type", + "schema": "public", + "values": [ + "health", + "care", + "device", + "incident", + "admission", + "other" + ] + }, + "public.alert_trigger_status": { + "name": "alert_trigger_status", + "schema": "public", + "values": [ + "open", + "acknowledged", + "resolved", + "closed" + ] + }, + "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.device_asset_status": { + "name": "device_asset_status", + "schema": "public", + "values": [ + "active", + "maintenance", + "disabled", + "retired" + ] + }, + "public.elder_status": { + "name": "elder_status", + "schema": "public", + "values": [ + "active", + "pending", + "discharged" + ] + }, + "public.family_contact_status": { + "name": "family_contact_status", + "schema": "public", + "values": [ + "active", + "suspended", + "archived" + ] + }, + "public.family_feedback_status": { + "name": "family_feedback_status", + "schema": "public", + "values": [ + "open", + "in_progress", + "resolved", + "closed" + ] + }, + "public.family_feedback_type": { + "name": "family_feedback_type", + "schema": "public", + "values": [ + "service", + "care", + "meal", + "environment", + "other" + ] + }, + "public.family_visit_status": { + "name": "family_visit_status", + "schema": "public", + "values": [ + "requested", + "approved", + "completed", + "cancelled" + ] + }, + "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.maintenance_ticket_priority": { + "name": "maintenance_ticket_priority", + "schema": "public", + "values": [ + "low", + "normal", + "high", + "urgent" + ] + }, + "public.maintenance_ticket_status": { + "name": "maintenance_ticket_status", + "schema": "public", + "values": [ + "open", + "assigned", + "resolved", + "closed", + "cancelled" + ] + }, + "public.membership_status": { + "name": "membership_status", + "schema": "public", + "values": [ + "active", + "disabled", + "pending" + ] + }, + "public.notice_status": { + "name": "notice_status", + "schema": "public", + "values": [ + "draft", + "published", + "retracted" + ] + }, + "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 64f591d..c204394 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -36,6 +36,13 @@ "when": 1783064001398, "tag": "0004_silly_carnage", "breakpoints": true + }, + { + "idx": 5, + "version": "7", + "when": 1783071837853, + "tag": "0005_quiet_sandman", + "breakpoints": true } ] } \ No newline at end of file diff --git a/modules/alerts/components/AlertsWorkspaceClient.tsx b/modules/alerts/components/AlertsWorkspaceClient.tsx new file mode 100644 index 0000000..f135b16 --- /dev/null +++ b/modules/alerts/components/AlertsWorkspaceClient.tsx @@ -0,0 +1,183 @@ +"use client"; + +import { FormEvent, useMemo, useState } from "react"; +import { Radio, Search, ShieldAlert, Siren } from "lucide-react"; + +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, 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 { IncidentSeverity } from "@/modules/core/types"; +import type { ApiResult } from "@/modules/core/server/api"; +import { INCIDENT_SEVERITY_LABELS } from "@/modules/emergency/types"; +import type { AlertCenterData, AlertRule, AlertRuleInput, AlertRuleStatus, AlertRuleType, AlertTrigger, AlertTriggerInput, AlertTriggerStatus } from "@/modules/alerts/types"; +import { ALERT_RULE_STATUS_LABELS, ALERT_RULE_STATUS_VALUES, ALERT_RULE_TYPE_LABELS, ALERT_RULE_TYPE_VALUES, ALERT_TRIGGER_STATUS_LABELS, ALERT_TRIGGER_STATUS_VALUES } from "@/modules/alerts/types"; + +type Props = { canManage: boolean; initialData: AlertCenterData }; +type DialogState = + | { kind: "rule"; mode: "create"; value: AlertRuleInput } + | { kind: "rule"; mode: "edit"; id: string; value: AlertRuleInput } + | { kind: "trigger"; mode: "create"; value: AlertTriggerInput } + | { kind: "trigger"; mode: "edit"; id: string; value: AlertTriggerInput }; + +const severityValues = ["info", "warning", "critical"] as const satisfies readonly IncidentSeverity[]; +const emptyRule: AlertRuleInput = { name: "", ruleType: "health", severity: "warning", status: "enabled", conditionSummary: "", suggestion: "" }; +const emptyTrigger: AlertTriggerInput = { ruleId: undefined, elderId: undefined, title: "", description: "", status: "open", source: "", handlingNotes: "" }; + +function formatDateTime(value: string | undefined): string { + return value ? new Date(value).toLocaleString("zh-CN") : "-"; +} + +function ruleToInput(rule: AlertRule): AlertRuleInput { + return { name: rule.name, ruleType: rule.ruleType, severity: rule.severity, status: rule.status, conditionSummary: rule.conditionSummary, suggestion: rule.suggestion }; +} + +function triggerToInput(trigger: AlertTrigger): AlertTriggerInput { + return { ruleId: trigger.ruleId, elderId: trigger.elderId, title: trigger.title, description: trigger.description, status: trigger.status, source: trigger.source, handlingNotes: trigger.handlingNotes }; +} + +function badgeVariant(value: string): "danger" | "secondary" | "success" | "warning" { + if (value === "critical") return "danger"; + if (value === "enabled" || value === "resolved" || value === "closed") return "success"; + if (value === "open" || value === "acknowledged" || value === "warning") return "warning"; + return "secondary"; +} + +export function AlertsWorkspaceClient({ canManage, initialData }: Props): React.ReactElement { + const [data, setData] = useState(initialData); + const [query, setQuery] = useState(""); + const [triggerStatus, setTriggerStatus] = useState<"all" | AlertTriggerStatus>("all"); + const [dialog, setDialog] = useState(); + const [message, setMessage] = useState(""); + const [isPending, setIsPending] = useState(false); + const normalizedQuery = query.trim().toLowerCase(); + const filteredRules = useMemo(() => data.rules.filter((rule) => !normalizedQuery || [rule.name, rule.conditionSummary, rule.suggestion].join(" ").toLowerCase().includes(normalizedQuery)), [data.rules, normalizedQuery]); + const filteredTriggers = useMemo( + () => + data.triggers.filter((trigger) => { + const matchesQuery = !normalizedQuery || [trigger.title, trigger.description, trigger.ruleName, trigger.elderName, trigger.source].join(" ").toLowerCase().includes(normalizedQuery); + return matchesQuery && (triggerStatus === "all" || trigger.status === triggerStatus); + }), + [data.triggers, normalizedQuery, triggerStatus], + ); + + async function refreshData(): Promise { + const response = await fetch("/api/alerts/rules"); + const result = (await response.json()) as ApiResult<{ data: AlertCenterData }>; + if (result.success) setData(result.data); + } + + async function submitDialog(event: FormEvent): Promise { + event.preventDefault(); + if (!dialog || !canManage) { + setMessage("当前角色无权维护规则预警"); + return; + } + const isCreate = dialog.mode === "create"; + const path = dialog.kind === "rule" ? (isCreate ? "/api/alerts/rules" : `/api/alerts/rules/${dialog.id}`) : isCreate ? "/api/alerts/triggers" : `/api/alerts/triggers/${dialog.id}`; + setIsPending(true); + const response = await fetch(path, { method: isCreate ? "POST" : "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify(dialog.value) }); + const result = (await response.json()) as ApiResult>; + setIsPending(false); + setMessage(result.reason); + if (result.success) { + setDialog(undefined); + await refreshData(); + } + } + + async function remove(kind: "rule" | "trigger", id: string): Promise { + if (!canManage) { + setMessage("当前角色无权删除规则预警记录"); + return; + } + setIsPending(true); + const response = await fetch(kind === "rule" ? `/api/alerts/rules/${id}` : `/api/alerts/triggers/${id}`, { method: "DELETE" }); + const result = (await response.json()) as ApiResult>; + setIsPending(false); + setMessage(result.reason); + if (result.success) await refreshData(); + } + + return ( +
+
+ + + + +
+
+

规则预警

维护规则并处理预警触发记录。

+
+ + update({ name: event.target.value })} required value={value.name} /> update({ severity: severity as IncidentSeverity })} options={severityValues.map((item) => ({ value: item, label: INCIDENT_SEVERITY_LABELS[item] }))} value={value.severity} />