diff --git a/app/(app)/app/settings/layout.tsx b/app/(app)/app/settings/layout.tsx index 62ad950..9f19125 100644 --- a/app/(app)/app/settings/layout.tsx +++ b/app/(app)/app/settings/layout.tsx @@ -1,14 +1,3 @@ -import Link from "next/link"; -import { Activity, Building2, ListChecks, Settings, Users } from "lucide-react"; - -const settingsNavItems = [ - { href: "/app/settings/users", label: "用户管理", icon: Users }, - { href: "/app/settings/organizations", label: "机构管理", icon: Building2 }, - { href: "/app/settings/roles", label: "角色权限", icon: Settings }, - { href: "/app/settings/audit", label: "审计日志", icon: ListChecks }, - { href: "/app/settings/status", label: "运行状态", icon: Activity }, -]; - type SettingsLayoutProps = { children: React.ReactNode; }; @@ -18,18 +7,6 @@ export default function SettingsLayout({ children }: SettingsLayoutProps): React

系统设置

-
{children}
diff --git a/app/(app)/app/settings/organizations/[id]/page.tsx b/app/(app)/app/settings/organizations/[id]/page.tsx new file mode 100644 index 0000000..12d8840 --- /dev/null +++ b/app/(app)/app/settings/organizations/[id]/page.tsx @@ -0,0 +1,173 @@ +import Link from "next/link"; +import { notFound, redirect } from "next/navigation"; +import { ArrowLeft, Building2, Users } 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 { getCurrentAuthContext, toPublicAccount } from "@/modules/core/server/auth"; +import { getRoleDefinitions, hasPermission } from "@/modules/core/server/permissions"; +import { readData } from "@/modules/core/server/store"; +import { OrganizationDetailClient } from "@/modules/settings/components/OrganizationDetailClient"; + +type OrganizationDetailPageProps = { + params: Promise<{ + id: string; + }>; +}; + +export default async function OrganizationDetailPage({ + params, +}: OrganizationDetailPageProps): Promise { + const context = await getCurrentAuthContext(); + if (!context) { + redirect("/login"); + } + + if (!hasPermission(context.permissions, "organization:read")) { + redirect("/app/dashboard"); + } + + const { id } = await params; + const data = await readData(); + const organization = data.organizations.find((item) => item.id === id); + if (!organization) { + notFound(); + } + + if (context.organization?.id && context.organization.id !== organization.id) { + redirect("/app/settings/organizations"); + } + + const roles = await getRoleDefinitions(organization.id); + const organizationMemberships = data.memberships.filter((membership) => membership.organizationId === organization.id); + const accountById = new Map(data.accounts.map((account) => [account.id, account])); + const members = organizationMemberships + .map((membership) => { + const account = accountById.get(membership.accountId); + if (!account) { + return null; + } + + return { + account: toPublicAccount(account, membership.roleKey, organization), + membership, + }; + }) + .filter((item): item is NonNullable => item !== null); + const invitations = data.organizationInvitations.filter((invitation) => invitation.organizationId === organization.id); + const pendingRequests = data.joinRequests.filter( + (request) => request.organizationId === organization.id && request.status === "pending", + ); + + return ( +
+
+
+ +
+ + +
+

{organization.name}

+

{organization.slug}

+
+
+
+
+ + {organization.status === "active" ? "启用" : "停用"} + + + {organization.registrationEnabled ? "开放注册" : "关闭注册"} + + + {organization.oidcEnabled ? "OIDC 已启用" : "OIDC 未启用"} + +
+
+ +
+ + + {members.length} + + 成员 + + + + {roles.filter((role) => role.scope === "organization").length} + + 机构角色 + + + + {pendingRequests.length} + + 待审批申请 + + + + {invitations.filter((invitation) => invitation.status === "active").length} + + 有效邀请 + +
+ + + + + + + + + +
+ + + + + + + + + + + + {members.map(({ account, membership }) => ( + + + + + + + + ))} + {members.length === 0 ? ( + + + + ) : null} + +
账号角色成员状态账号状态加入时间
+

{account.name}

+

{account.email}

+
{membership.roleLabel}{membership.status}{account.status} + {new Date(membership.createdAt).toLocaleString("zh-CN")} +
+ 暂无成员 +
+
+
+
+
+ ); +} diff --git a/app/(app)/app/settings/organizations/page.tsx b/app/(app)/app/settings/organizations/page.tsx index 987d37c..eef208d 100644 --- a/app/(app)/app/settings/organizations/page.tsx +++ b/app/(app)/app/settings/organizations/page.tsx @@ -1,4 +1,5 @@ import { redirect } from "next/navigation"; +import Link from "next/link"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; @@ -74,7 +75,14 @@ export default async function OrganizationsPage({ searchParams }: OrganizationsP {visibleOrganizations.map((organization) => ( - {organization.name} + + + {organization.name} + + {organization.slug} {organization.status === "active" ? "启用" : "停用"} diff --git a/app/api/auth/register/route.ts b/app/api/auth/register/route.ts index aacf617..2c10621 100644 --- a/app/api/auth/register/route.ts +++ b/app/api/auth/register/route.ts @@ -20,6 +20,7 @@ export async function POST(request: Request): Promise { const email = readString(body, "email"); const password = readString(body, "password"); const organizationId = readString(body, "organizationId"); + const invitationToken = readString(body, "invitationToken"); if (!name || !email || !password) { return jsonFailure("姓名、邮箱和密码不能为空"); @@ -35,6 +36,7 @@ export async function POST(request: Request): Promise { email, password, organizationId: organizationId || undefined, + invitationToken: invitationToken || undefined, }); if (created.session) { await setSessionCookie(created.session.id); diff --git a/app/api/organizations/[id]/invitations/route.ts b/app/api/organizations/[id]/invitations/route.ts new file mode 100644 index 0000000..cb946a7 --- /dev/null +++ b/app/api/organizations/[id]/invitations/route.ts @@ -0,0 +1,103 @@ +import { randomBytes } from "node:crypto"; + +import { and, eq } from "drizzle-orm"; + +import { jsonFailure, jsonSuccess, readJsonBody } from "@/modules/core/server/api"; +import { recordAuditLog } from "@/modules/core/server/audit"; +import { requirePermission } from "@/modules/core/server/auth"; +import { getDatabase } from "@/modules/core/server/db"; +import { organizationInvitations, organizations, roles } from "@/modules/core/server/schema"; + +type RouteContext = { + params: Promise<{ + id: string; + }>; +}; + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function readString(source: Record, key: string): string { + const value = source[key]; + return typeof value === "string" ? value.trim() : ""; +} + +function createInvitationToken(): string { + return randomBytes(24).toString("base64url"); +} + +export async function POST(request: Request, context: RouteContext): Promise { + const { id } = await context.params; + const auth = await requirePermission("account:manage", { + action: "organizationInvitation.create", + targetType: "organization", + targetId: id, + }); + + if (!auth.success) { + return auth.response; + } + + const activeOrganizationId = auth.context.organization?.id; + if (activeOrganizationId && activeOrganizationId !== id) { + return jsonFailure("不能为其他机构创建邀请", 403); + } + + const body = await readJsonBody(request); + if (!isRecord(body)) { + return jsonFailure("请求数据格式无效"); + } + + const email = readString(body, "email"); + const roleId = readString(body, "roleId"); + if (!roleId) { + return jsonFailure("请选择邀请角色"); + } + + const database = getDatabase(); + const organizationRows = await database.select().from(organizations).where(eq(organizations.id, id)).limit(1); + const organization = organizationRows[0]; + if (!organization) { + return jsonFailure("机构不存在", 404); + } + + const roleRows = await database + .select() + .from(roles) + .where(and(eq(roles.id, roleId), eq(roles.organizationId, id), eq(roles.isEnabled, true))) + .limit(1); + const role = roleRows[0]; + if (!role) { + return jsonFailure("角色不存在", 404); + } + + const rows = await database + .insert(organizationInvitations) + .values({ + organizationId: id, + roleId: role.id, + email, + token: createInvitationToken(), + status: "active", + createdByAccountId: auth.context.account.id, + expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), + }) + .returning(); + const invitation = rows[0]; + if (!invitation) { + return jsonFailure("邀请创建失败", 500); + } + + await recordAuditLog({ + actor: auth.context.account, + organizationId: id, + action: "organizationInvitation.create", + targetType: "organizationInvitation", + targetId: invitation.id, + result: "success", + reason: `创建机构邀请:${email || "通用邀请"}`, + }); + + return jsonSuccess("邀请已创建", { invitation }, 201); +} diff --git a/app/api/organizations/[id]/route.ts b/app/api/organizations/[id]/route.ts index bbf734e..ad4df02 100644 --- a/app/api/organizations/[id]/route.ts +++ b/app/api/organizations/[id]/route.ts @@ -24,6 +24,11 @@ function readString(source: Record, key: string): string { return typeof value === "string" ? value.trim() : ""; } +function readBoolean(source: Record, key: string): boolean | null { + const value = source[key]; + return typeof value === "boolean" ? value : null; +} + function readStatus(value: unknown): OrganizationStatus | null { if (typeof value !== "string") { return null; @@ -51,12 +56,41 @@ export async function PATCH(request: Request, context: RouteContext): Promise { const rows = await database.select().from(organizations).where(eq(organizations.status, "active")); return jsonSuccess("机构列表已加载", { - organizations: rows.map((organization) => ({ - id: organization.id, - name: organization.name, - slug: organization.slug, - status: organization.status, - createdAt: organization.createdAt.toISOString(), - updatedAt: organization.updatedAt.toISOString(), - })), + organizations: rows + .filter((organization) => organization.registrationEnabled) + .map((organization) => ({ + id: organization.id, + name: organization.name, + slug: organization.slug, + status: organization.status, + registrationEnabled: organization.registrationEnabled, + oidcEnabled: organization.oidcEnabled, + oidcIssuerUrl: "", + oidcClientId: "", + oidcHasClientSecret: organization.oidcClientSecret.length > 0, + oidcScopes: organization.oidcScopes, + oidcRedirectUri: organization.oidcRedirectUri, + oidcAutoProvision: organization.oidcAutoProvision, + createdAt: organization.createdAt.toISOString(), + updatedAt: organization.updatedAt.toISOString(), + })), }); } diff --git a/app/api/settings/accounts/[id]/route.ts b/app/api/settings/accounts/[id]/route.ts index d06319d..a97ef64 100644 --- a/app/api/settings/accounts/[id]/route.ts +++ b/app/api/settings/accounts/[id]/route.ts @@ -39,6 +39,14 @@ function toOrganization(row: typeof organizations.$inferSelect): Organization { name: row.name, slug: row.slug, status: row.status, + registrationEnabled: row.registrationEnabled, + oidcEnabled: row.oidcEnabled, + oidcIssuerUrl: row.oidcIssuerUrl, + oidcClientId: row.oidcClientId, + oidcHasClientSecret: row.oidcClientSecret.length > 0, + oidcScopes: row.oidcScopes, + oidcRedirectUri: row.oidcRedirectUri, + oidcAutoProvision: row.oidcAutoProvision, createdAt: row.createdAt.toISOString(), updatedAt: row.updatedAt.toISOString(), }; diff --git a/app/api/settings/accounts/route.ts b/app/api/settings/accounts/route.ts index 8b8656e..34022f2 100644 --- a/app/api/settings/accounts/route.ts +++ b/app/api/settings/accounts/route.ts @@ -155,6 +155,14 @@ export async function POST(request: Request): Promise { name: created.organization.name, slug: created.organization.slug, status: created.organization.status, + registrationEnabled: created.organization.registrationEnabled, + oidcEnabled: created.organization.oidcEnabled, + oidcIssuerUrl: created.organization.oidcIssuerUrl, + oidcClientId: created.organization.oidcClientId, + oidcHasClientSecret: created.organization.oidcClientSecret.length > 0, + oidcScopes: created.organization.oidcScopes, + oidcRedirectUri: created.organization.oidcRedirectUri, + oidcAutoProvision: created.organization.oidcAutoProvision, createdAt: created.organization.createdAt.toISOString(), updatedAt: created.organization.updatedAt.toISOString(), }), diff --git a/app/globals.css b/app/globals.css index acc98f4..bcf7c60 100644 --- a/app/globals.css +++ b/app/globals.css @@ -83,11 +83,9 @@ a, @keyframes page-enter { from { opacity: 0; - transform: translateY(6px); } to { opacity: 1; - transform: translateY(0); } } diff --git a/drizzle/0001_tired_queen_noir.sql b/drizzle/0001_tired_queen_noir.sql new file mode 100644 index 0000000..1566035 --- /dev/null +++ b/drizzle/0001_tired_queen_noir.sql @@ -0,0 +1,28 @@ +CREATE TABLE "organization_invitations" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "organization_id" uuid NOT NULL, + "role_id" uuid NOT NULL, + "email" text DEFAULT '' NOT NULL, + "token" text NOT NULL, + "status" text DEFAULT 'active' NOT NULL, + "created_by_account_id" uuid, + "accepted_by_account_id" uuid, + "accepted_at" timestamp with time zone, + "expires_at" timestamp with time zone NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "organizations" ADD COLUMN "registration_enabled" boolean DEFAULT true NOT NULL;--> statement-breakpoint +ALTER TABLE "organizations" ADD COLUMN "oidc_enabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE "organizations" ADD COLUMN "oidc_issuer_url" text DEFAULT '' NOT NULL;--> statement-breakpoint +ALTER TABLE "organizations" ADD COLUMN "oidc_client_id" text DEFAULT '' NOT NULL;--> statement-breakpoint +ALTER TABLE "organizations" ADD COLUMN "oidc_client_secret" text DEFAULT '' NOT NULL;--> statement-breakpoint +ALTER TABLE "organizations" ADD COLUMN "oidc_scopes" text DEFAULT 'openid profile email' NOT NULL;--> statement-breakpoint +ALTER TABLE "organizations" ADD COLUMN "oidc_redirect_uri" text DEFAULT '' NOT NULL;--> statement-breakpoint +ALTER TABLE "organizations" ADD COLUMN "oidc_auto_provision" boolean DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE "organization_invitations" ADD CONSTRAINT "organization_invitations_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "organization_invitations" ADD CONSTRAINT "organization_invitations_role_id_roles_id_fk" FOREIGN KEY ("role_id") REFERENCES "public"."roles"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "organization_invitations" ADD CONSTRAINT "organization_invitations_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 "organization_invitations" ADD CONSTRAINT "organization_invitations_accepted_by_account_id_accounts_id_fk" FOREIGN KEY ("accepted_by_account_id") REFERENCES "public"."accounts"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "organization_invitations_token_unique" ON "organization_invitations" USING btree ("token"); \ No newline at end of file diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json new file mode 100644 index 0000000..7f7c414 --- /dev/null +++ b/drizzle/meta/0001_snapshot.json @@ -0,0 +1,1987 @@ +{ + "id": "f535bccb-22d5-4167-85e8-89fb281c29cb", + "prevId": "eef2df79-ffc1-4c17-b07c-1f58053cfdcc", + "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 + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_salt": { + "name": "password_salt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "account_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "accounts_email_unique": { + "name": "accounts_email_unique", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "accounts_platform_role_id_roles_id_fk": { + "name": "accounts_platform_role_id_roles_id_fk", + "tableFrom": "accounts", + "tableTo": "roles", + "columnsFrom": [ + "platform_role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.admissions": { + "name": "admissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "elder_id": { + "name": "elder_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "bed_id": { + "name": "bed_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "admission_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "admitted_at": { + "name": "admitted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "discharged_at": { + "name": "discharged_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "admissions_organization_id_organizations_id_fk": { + "name": "admissions_organization_id_organizations_id_fk", + "tableFrom": "admissions", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "admissions_elder_id_elders_id_fk": { + "name": "admissions_elder_id_elders_id_fk", + "tableFrom": "admissions", + "tableTo": "elders", + "columnsFrom": [ + "elder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "admissions_bed_id_beds_id_fk": { + "name": "admissions_bed_id_beds_id_fk", + "tableFrom": "admissions", + "tableTo": "beds", + "columnsFrom": [ + "bed_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "actor_account_id": { + "name": "actor_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result": { + "name": "result", + "type": "audit_result", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "audit_logs_organization_id_organizations_id_fk": { + "name": "audit_logs_organization_id_organizations_id_fk", + "tableFrom": "audit_logs", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "audit_logs_actor_account_id_accounts_id_fk": { + "name": "audit_logs_actor_account_id_accounts_id_fk", + "tableFrom": "audit_logs", + "tableTo": "accounts", + "columnsFrom": [ + "actor_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beds": { + "name": "beds", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "bed_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'available'" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "beds_code_room_unique": { + "name": "beds_code_room_unique", + "columns": [ + { + "expression": "room_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "beds_organization_id_organizations_id_fk": { + "name": "beds_organization_id_organizations_id_fk", + "tableFrom": "beds", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "beds_room_id_rooms_id_fk": { + "name": "beds_room_id_rooms_id_fk", + "tableFrom": "beds", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.buildings": { + "name": "buildings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "campus_id": { + "name": "campus_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "buildings_organization_id_organizations_id_fk": { + "name": "buildings_organization_id_organizations_id_fk", + "tableFrom": "buildings", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "buildings_campus_id_campuses_id_fk": { + "name": "buildings_campus_id_campuses_id_fk", + "tableFrom": "buildings", + "tableTo": "campuses", + "columnsFrom": [ + "campus_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.campuses": { + "name": "campuses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "campuses_organization_id_organizations_id_fk": { + "name": "campuses_organization_id_organizations_id_fk", + "tableFrom": "campuses", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.elders": { + "name": "elders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "gender": { + "name": "gender", + "type": "gender", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "age": { + "name": "age", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "care_level": { + "name": "care_level", + "type": "care_level", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "elder_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "primary_contact": { + "name": "primary_contact", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "medical_notes": { + "name": "medical_notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "elders_organization_id_organizations_id_fk": { + "name": "elders_organization_id_organizations_id_fk", + "tableFrom": "elders", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.floors": { + "name": "floors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "building_id": { + "name": "building_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "floors_organization_id_organizations_id_fk": { + "name": "floors_organization_id_organizations_id_fk", + "tableFrom": "floors", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "floors_building_id_buildings_id_fk": { + "name": "floors_building_id_buildings_id_fk", + "tableFrom": "floors", + "tableTo": "buildings", + "columnsFrom": [ + "building_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.join_requests": { + "name": "join_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "join_request_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "reviewed_by_account_id": { + "name": "reviewed_by_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reviewed_at": { + "name": "reviewed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "join_requests_account_id_accounts_id_fk": { + "name": "join_requests_account_id_accounts_id_fk", + "tableFrom": "join_requests", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "join_requests_organization_id_organizations_id_fk": { + "name": "join_requests_organization_id_organizations_id_fk", + "tableFrom": "join_requests", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "join_requests_reviewed_by_account_id_accounts_id_fk": { + "name": "join_requests_reviewed_by_account_id_accounts_id_fk", + "tableFrom": "join_requests", + "tableTo": "accounts", + "columnsFrom": [ + "reviewed_by_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memberships": { + "name": "memberships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "membership_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "memberships_account_organization_unique": { + "name": "memberships_account_organization_unique", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memberships_account_id_accounts_id_fk": { + "name": "memberships_account_id_accounts_id_fk", + "tableFrom": "memberships", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "memberships_organization_id_organizations_id_fk": { + "name": "memberships_organization_id_organizations_id_fk", + "tableFrom": "memberships", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "memberships_role_id_roles_id_fk": { + "name": "memberships_role_id_roles_id_fk", + "tableFrom": "memberships", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_invitations": { + "name": "organization_invitations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "accepted_by_account_id": { + "name": "accepted_by_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organization_invitations_token_unique": { + "name": "organization_invitations_token_unique", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_invitations_organization_id_organizations_id_fk": { + "name": "organization_invitations_organization_id_organizations_id_fk", + "tableFrom": "organization_invitations", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_invitations_role_id_roles_id_fk": { + "name": "organization_invitations_role_id_roles_id_fk", + "tableFrom": "organization_invitations", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "organization_invitations_created_by_account_id_accounts_id_fk": { + "name": "organization_invitations_created_by_account_id_accounts_id_fk", + "tableFrom": "organization_invitations", + "tableTo": "accounts", + "columnsFrom": [ + "created_by_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "organization_invitations_accepted_by_account_id_accounts_id_fk": { + "name": "organization_invitations_accepted_by_account_id_accounts_id_fk", + "tableFrom": "organization_invitations", + "tableTo": "accounts", + "columnsFrom": [ + "accepted_by_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "organization_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "registration_enabled": { + "name": "registration_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "oidc_enabled": { + "name": "oidc_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "oidc_issuer_url": { + "name": "oidc_issuer_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "oidc_client_id": { + "name": "oidc_client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "oidc_client_secret": { + "name": "oidc_client_secret", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "oidc_scopes": { + "name": "oidc_scopes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'openid profile email'" + }, + "oidc_redirect_uri": { + "name": "oidc_redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "oidc_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 + } + }, + "enums": { + "public.account_status": { + "name": "account_status", + "schema": "public", + "values": [ + "active", + "disabled", + "pending" + ] + }, + "public.admission_status": { + "name": "admission_status", + "schema": "public", + "values": [ + "active", + "transferred", + "discharged" + ] + }, + "public.audit_result": { + "name": "audit_result", + "schema": "public", + "values": [ + "success", + "denied", + "failure" + ] + }, + "public.bed_status": { + "name": "bed_status", + "schema": "public", + "values": [ + "available", + "occupied", + "maintenance", + "disabled" + ] + }, + "public.care_level": { + "name": "care_level", + "schema": "public", + "values": [ + "self-care", + "semi-assisted", + "assisted", + "intensive" + ] + }, + "public.elder_status": { + "name": "elder_status", + "schema": "public", + "values": [ + "active", + "pending", + "discharged" + ] + }, + "public.gender": { + "name": "gender", + "schema": "public", + "values": [ + "male", + "female", + "other" + ] + }, + "public.incident_severity": { + "name": "incident_severity", + "schema": "public", + "values": [ + "info", + "warning", + "critical" + ] + }, + "public.incident_status": { + "name": "incident_status", + "schema": "public", + "values": [ + "open", + "acknowledged", + "resolved", + "closed" + ] + }, + "public.join_request_status": { + "name": "join_request_status", + "schema": "public", + "values": [ + "pending", + "approved", + "rejected" + ] + }, + "public.membership_status": { + "name": "membership_status", + "schema": "public", + "values": [ + "active", + "disabled", + "pending" + ] + }, + "public.organization_status": { + "name": "organization_status", + "schema": "public", + "values": [ + "active", + "disabled" + ] + }, + "public.role_scope": { + "name": "role_scope", + "schema": "public", + "values": [ + "platform", + "organization" + ] + } + }, + "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 026b6c7..3603b30 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1782975351852, "tag": "0000_bouncy_micromax", "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1782994149630, + "tag": "0001_tired_queen_noir", + "breakpoints": true } ] } \ No newline at end of file diff --git a/modules/auth/components/AuthPanel.tsx b/modules/auth/components/AuthPanel.tsx index bc3471b..5a06962 100644 --- a/modules/auth/components/AuthPanel.tsx +++ b/modules/auth/components/AuthPanel.tsx @@ -28,6 +28,7 @@ function AuthPanelContent({ mode }: AuthPanelProps): React.ReactElement { const router = useRouter(); const searchParams = useSearchParams(); const redirectTo = searchParams.get("redirectTo") ?? "/app/dashboard"; + const invitationToken = searchParams.get("invite") ?? ""; const [message, setMessage] = useState(""); const [isPending, setIsPending] = useState(false); const [hasAccounts, setHasAccounts] = useState(null); @@ -117,6 +118,7 @@ function AuthPanelContent({ mode }: AuthPanelProps): React.ReactElement { password, organization, organizationId, + invitationToken, }), }); const result = (await response.json()) as ApiResult<{ account: unknown; pendingApproval?: boolean }>; @@ -195,7 +197,13 @@ function AuthPanelContent({ mode }: AuthPanelProps): React.ReactElement { ) : null} - {mode === "register" && organizations.length > 0 ? ( + {mode === "register" && invitationToken ? ( +

+ 正在使用机构邀请注册,完成后将自动加入机构。 +

+ ) : null} + + {mode === "register" && !invitationToken && organizations.length > 0 ? ( + + + +
+ + +
+ + + +
+ + +
+ + + + {settingsMessage ? ( +

+ {settingsMessage} +

+ ) : null} + + + + + + + + + 机构邀请 + 生成邀请链接后发送给成员;可限定邮箱,也可生成通用邀请。 + + +
+ + + +
+ {inviteMessage ? ( +

+ {inviteMessage} +

+ ) : null} + +
+ + + + + + + + + + + + {invitations.map((invitation) => ( + + + + + + + + ))} + {invitations.length === 0 ? ( + + + + ) : null} + +
对象角色状态有效期链接
{invitation.email || "通用邀请"}{invitation.roleLabel} + {invitation.status} + + {new Date(invitation.expiresAt).toLocaleString("zh-CN")} + + +
+ 暂无邀请 +
+
+
+
+
+
+ + ); +} diff --git a/modules/shared/components/AppShell.tsx b/modules/shared/components/AppShell.tsx index 294215b..66650bc 100644 --- a/modules/shared/components/AppShell.tsx +++ b/modules/shared/components/AppShell.tsx @@ -28,84 +28,106 @@ type NavItem = { icon: LucideIcon; }; -const navItems: NavItem[] = [ +type NavGroup = { + label: string; + items: NavItem[]; +}; + +const navGroups: NavGroup[] = [ { - href: "/app/dashboard", - label: "运营看板", - icon: LayoutDashboard, + label: "运营", + items: [ + { + href: "/app/dashboard", + label: "运营看板", + icon: LayoutDashboard, + }, + { + href: "/app/elders", + label: "老人档案", + icon: Users, + }, + { + href: "/app/beds", + label: "床位房间", + icon: BedDouble, + }, + { + href: "/app/health", + label: "健康照护", + icon: HeartPulse, + }, + { + href: "/app/care", + label: "护理服务", + icon: ClipboardCheck, + }, + { + href: "/app/emergency", + label: "安全应急", + icon: ShieldAlert, + }, + ], }, { - href: "/app/elders", - label: "老人档案", - icon: Users, + label: "协同", + items: [ + { + href: "/app/devices", + label: "设备运维", + icon: Wrench, + }, + { + href: "/app/notices", + label: "公告通知", + icon: Bell, + }, + { + href: "/app/alerts", + label: "规则预警", + icon: Radio, + }, + { + href: "/app/family", + label: "家属服务", + icon: TabletSmartphone, + }, + ], }, { - href: "/app/beds", - label: "床位房间", - icon: BedDouble, - }, - { - href: "/app/health", - label: "健康照护", - icon: HeartPulse, - }, - { - href: "/app/care", - label: "护理服务", - icon: ClipboardCheck, - }, - { - href: "/app/emergency", - label: "安全应急", - icon: ShieldAlert, - }, - { - href: "/app/devices", - label: "设备运维", - icon: Wrench, - }, - { - href: "/app/notices", - label: "公告通知", - icon: Bell, - }, - { - href: "/app/alerts", - label: "规则预警", - icon: Radio, - }, - { - href: "/app/family", - label: "家属服务", - icon: TabletSmartphone, - }, - { - href: "/app/settings/organizations", - label: "机构管理", - icon: Building2, - }, - { - href: "/app/settings/users", - label: "用户管理", - icon: Users, - }, - { - href: "/app/settings/roles", - label: "角色权限", - icon: Settings, - }, - { - href: "/app/settings/audit", - label: "审计日志", - icon: ListChecks, - }, - { - href: "/app/settings/status", - label: "运行状态", - icon: Wrench, + label: "系统设置", + items: [ + { + href: "/app/settings/organizations", + label: "机构管理", + icon: Building2, + }, + { + href: "/app/settings/users", + label: "用户管理", + icon: Users, + }, + { + href: "/app/settings/roles", + label: "角色权限", + icon: Settings, + }, + { + href: "/app/settings/audit", + label: "审计日志", + icon: ListChecks, + }, + { + href: "/app/settings/status", + label: "运行状态", + icon: Wrench, + }, + ], }, ]; +const navItems: NavItem[] = navGroups.flatMap((group) => group.items); + type AppShellProps = { children: React.ReactNode; account: PublicAccount; @@ -129,22 +151,29 @@ export function AppShell({ children, account, permissions }: AppShellProps): Rea