diff --git a/app/(app)/app/settings/organizations/page.tsx b/app/(app)/app/settings/organizations/page.tsx
index eef208d..db0e6c2 100644
--- a/app/(app)/app/settings/organizations/page.tsx
+++ b/app/(app)/app/settings/organizations/page.tsx
@@ -4,7 +4,7 @@ import Link from "next/link";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { getCurrentAuthContext } from "@/modules/core/server/auth";
-import { hasPermission } from "@/modules/core/server/permissions";
+import { getRoleDefinitions, hasPermission } from "@/modules/core/server/permissions";
import { readData } from "@/modules/core/server/store";
import {
OrganizationManagementClient,
@@ -37,17 +37,29 @@ export default async function OrganizationsPage({ searchParams }: OrganizationsP
);
const pageCount = getPageCount(filteredOrganizations.length);
const visibleOrganizations = paginate(filteredOrganizations, page);
+ const canManageOrganizations = hasPermission(context.permissions, "organization:manage");
+ const canInviteMembers = hasPermission(context.permissions, "account:manage");
+ const organizationRoleGroups = await Promise.all(
+ visibleOrganizations.map(async (organization) => ({
+ organizationId: organization.id,
+ roles: await getRoleDefinitions(organization.id),
+ })),
+ );
+ const rolesByOrganizationId = new Map(
+ organizationRoleGroups.map((group) => [group.organizationId, group.roles]),
+ );
return (
-
- Organizations
- 机构管理
- 多机构租户、启停状态和机构角色初始化。
+
+
+
Organizations
+
机构管理
+
多机构租户、启停状态、成员邀请和机构角色初始化。
+
+ {canManageOrganizations ? : null}
- {hasPermission(context.permissions, "organization:manage") ? : null}
-
机构列表
@@ -89,7 +101,11 @@ export default async function OrganizationsPage({ searchParams }: OrganizationsP
{new Date(organization.createdAt).toLocaleString("zh-CN")}
-
+
|
))}
diff --git a/app/(app)/app/settings/roles/page.tsx b/app/(app)/app/settings/roles/page.tsx
index 635caf5..c4cdbe2 100644
--- a/app/(app)/app/settings/roles/page.tsx
+++ b/app/(app)/app/settings/roles/page.tsx
@@ -32,17 +32,19 @@ export default async function RolesPage({ searchParams }: RolesPageProps): Promi
);
const pageCount = getPageCount(filteredRoles.length);
const visibleRoles = paginate(filteredRoles, page);
+ const canManageRoles = hasPermission(context.permissions, "role:manage");
return (
-
- RBAC
- 角色权限
- 系统权限点、内置角色和机构自定义角色。
+
+
+
RBAC
+
角色权限
+
系统权限点、内置角色和机构自定义角色。
+
+ {canManageRoles ? : null}
- {hasPermission(context.permissions, "role:manage") ? : null}
-
角色列表
diff --git a/app/(app)/app/settings/users/page.tsx b/app/(app)/app/settings/users/page.tsx
index d10602b..581b53d 100644
--- a/app/(app)/app/settings/users/page.tsx
+++ b/app/(app)/app/settings/users/page.tsx
@@ -69,7 +69,12 @@ export default async function UsersPage({ searchParams }: UsersPageProps): Promi
账号创建、机构加入审批、角色分配和账号列表。
-
+
diff --git a/modules/settings/components/OrganizationDetailClient.tsx b/modules/settings/components/OrganizationDetailClient.tsx
index e57f6b0..44887e8 100644
--- a/modules/settings/components/OrganizationDetailClient.tsx
+++ b/modules/settings/components/OrganizationDetailClient.tsx
@@ -10,6 +10,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
import { Input } from "@/components/ui/input";
import type { ApiResult } from "@/modules/core/server/api";
import type { Organization, OrganizationInvitation, RoleDefinition } from "@/modules/core/types";
+import { getInviteHref, OrganizationInviteDialog } from "@/modules/settings/components/OrganizationInviteDialog";
type OrganizationDetailClientProps = {
invitations: OrganizationInvitation[];
@@ -17,21 +18,15 @@ type OrganizationDetailClientProps = {
roles: RoleDefinition[];
};
-function getInviteHref(token: string): string {
- return `/register?invite=${encodeURIComponent(token)}`;
-}
-
export function OrganizationDetailClient({
invitations,
organization,
roles,
}: OrganizationDetailClientProps): React.ReactElement {
const router = useRouter();
+ const [isInviteOpen, setIsInviteOpen] = useState(false);
const [settingsMessage, setSettingsMessage] = useState("");
- const [inviteMessage, setInviteMessage] = useState("");
const [isPending, setIsPending] = useState(false);
- const organizationRoles = roles.filter((role) => role.scope === "organization" && role.organizationId === organization.id && role.isEnabled);
- const defaultRoleId = organizationRoles.find((role) => role.key === "caregiver")?.id ?? organizationRoles[0]?.id ?? "";
async function handleSettingsSubmit(event: FormEvent): Promise {
event.preventDefault();
@@ -64,32 +59,6 @@ export function OrganizationDetailClient({
router.refresh();
}
- async function handleInviteSubmit(event: FormEvent): Promise {
- event.preventDefault();
- setInviteMessage("");
- setIsPending(true);
- const formData = new FormData(event.currentTarget);
- const response = await fetch(`/api/organizations/${organization.id}/invitations`, {
- method: "POST",
- headers: { "content-type": "application/json" },
- body: JSON.stringify({
- email: String(formData.get("email") ?? ""),
- roleId: String(formData.get("roleId") ?? ""),
- }),
- });
- const result = (await response.json()) as ApiResult<{ invitation: OrganizationInvitation }>;
- setIsPending(false);
-
- if (!result.success) {
- setInviteMessage(result.reason);
- return;
- }
-
- setInviteMessage(`${result.reason}:${getInviteHref(result.invitation.token)}`);
- event.currentTarget.reset();
- router.refresh();
- }
-
return (
@@ -169,36 +138,17 @@ export function OrganizationDetailClient({
-
- 机构邀请
- 生成邀请链接后发送给成员;可限定邮箱,也可生成通用邀请。
+
+
+ 机构邀请
+ 生成邀请链接后发送给成员;可限定邮箱,也可生成通用邀请。
+
+
-
- {inviteMessage ? (
-
- {inviteMessage}
-
- ) : null}
-
@@ -250,6 +200,12 @@ export function OrganizationDetailClient({
+ setIsInviteOpen(false)}
+ open={isInviteOpen}
+ organization={organization}
+ roles={roles}
+ />
);
}
diff --git a/modules/settings/components/OrganizationInviteDialog.tsx b/modules/settings/components/OrganizationInviteDialog.tsx
new file mode 100644
index 0000000..7ea9eb7
--- /dev/null
+++ b/modules/settings/components/OrganizationInviteDialog.tsx
@@ -0,0 +1,140 @@
+"use client";
+
+import { FormEvent, useMemo, useState } from "react";
+import { Copy, Send } from "lucide-react";
+import { useRouter } from "next/navigation";
+
+import { Button } from "@/components/ui/button";
+import { Dialog } from "@/components/ui/dialog";
+import { Input } from "@/components/ui/input";
+import type { ApiResult } from "@/modules/core/server/api";
+import type { Organization, OrganizationInvitation, RoleDefinition } from "@/modules/core/types";
+
+type OrganizationInviteDialogProps = {
+ open: boolean;
+ organization: Organization;
+ roles: RoleDefinition[];
+ onClose: () => void;
+};
+
+export function getInviteHref(token: string): string {
+ return `/register?invite=${encodeURIComponent(token)}`;
+}
+
+export function OrganizationInviteDialog({
+ open,
+ organization,
+ roles,
+ onClose,
+}: OrganizationInviteDialogProps): React.ReactElement {
+ const router = useRouter();
+ const [message, setMessage] = useState("");
+ const [lastInviteHref, setLastInviteHref] = useState("");
+ const [isPending, setIsPending] = useState(false);
+ const organizationRoles = useMemo(
+ () => roles.filter((role) => role.scope === "organization" && role.organizationId === organization.id && role.isEnabled),
+ [organization.id, roles],
+ );
+ const defaultRoleId = organizationRoles.find((role) => role.key === "caregiver")?.id ?? organizationRoles[0]?.id ?? "";
+
+ function closeDialog(): void {
+ if (isPending) {
+ return;
+ }
+
+ setMessage("");
+ setLastInviteHref("");
+ onClose();
+ }
+
+ async function handleInviteSubmit(event: FormEvent): Promise {
+ event.preventDefault();
+ setMessage("");
+ setLastInviteHref("");
+ setIsPending(true);
+ const formData = new FormData(event.currentTarget);
+ const response = await fetch(`/api/organizations/${organization.id}/invitations`, {
+ method: "POST",
+ headers: { "content-type": "application/json" },
+ body: JSON.stringify({
+ email: String(formData.get("email") ?? ""),
+ roleId: String(formData.get("roleId") ?? ""),
+ }),
+ });
+ const result = (await response.json()) as ApiResult<{ invitation: OrganizationInvitation }>;
+ setIsPending(false);
+
+ if (!result.success) {
+ setMessage(result.reason);
+ return;
+ }
+
+ const inviteHref = getInviteHref(result.invitation.token);
+ setMessage(result.reason);
+ setLastInviteHref(inviteHref);
+ event.currentTarget.reset();
+ router.refresh();
+ }
+
+ return (
+
+ );
+}
diff --git a/modules/settings/components/OrganizationManagementClient.tsx b/modules/settings/components/OrganizationManagementClient.tsx
index a81d658..5bd455c 100644
--- a/modules/settings/components/OrganizationManagementClient.tsx
+++ b/modules/settings/components/OrganizationManagementClient.tsx
@@ -1,21 +1,35 @@
"use client";
import { FormEvent, useState } from "react";
-import { Pencil, Plus, Power } from "lucide-react";
+import { Pencil, Plus, Power, Send } from "lucide-react";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Dialog } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import type { ApiResult } from "@/modules/core/server/api";
-import type { Organization } from "@/modules/core/types";
+import type { Organization, RoleDefinition } from "@/modules/core/types";
+import { OrganizationInviteDialog } from "@/modules/settings/components/OrganizationInviteDialog";
export function OrganizationManagementClient(): React.ReactElement {
const router = useRouter();
+ const [isCreateOpen, setIsCreateOpen] = useState(false);
const [message, setMessage] = useState("");
const [isPending, setIsPending] = useState(false);
+ function openCreateDialog(): void {
+ setMessage("");
+ setIsCreateOpen(true);
+ }
+
+ function closeCreateDialog(): void {
+ if (isPending) {
+ return;
+ }
+
+ setIsCreateOpen(false);
+ }
+
async function handleSubmit(event: FormEvent): Promise {
event.preventDefault();
setMessage("");
@@ -38,42 +52,61 @@ export function OrganizationManagementClient(): React.ReactElement {
}
setMessage(result.reason);
+ setIsCreateOpen(false);
event.currentTarget.reset();
router.refresh();
}
return (
-
-
- 新增机构
- 创建后会自动初始化机构内置角色。
-
-
-
-
+
+ >
);
}
type OrganizationRowActionsProps = {
+ canInvite: boolean;
organization: Organization;
+ roles: RoleDefinition[];
};
-export function OrganizationRowActions({ organization }: OrganizationRowActionsProps): React.ReactElement {
+export function OrganizationRowActions({
+ canInvite,
+ organization,
+ roles,
+}: OrganizationRowActionsProps): React.ReactElement {
const router = useRouter();
const [isEditOpen, setIsEditOpen] = useState(false);
+ const [isInviteOpen, setIsInviteOpen] = useState(false);
const [message, setMessage] = useState("");
const [isPending, setIsPending] = useState(false);
@@ -131,6 +164,12 @@ export function OrganizationRowActions({ organization }: OrganizationRowActionsP
编辑
+ {canInvite ? (
+
+ ) : null}
{organization.status === "disabled" ? (