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 ( + +
+ + + {message ? ( +

+ {message} +

+ ) : null} + {lastInviteHref ? ( +
+ {lastInviteHref} + +
+ ) : null} +
+ + +
+ +
+ ); +} 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 ( - - - 新增机构 - 创建后会自动初始化机构内置角色。 - - -
+ <> + + + - + {message ? ( +

+ {message} +

+ ) : null} +
+ + +
- {message ? ( -

- {message} -

- ) : null} -
-
+ + ); } 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