diff --git a/app/(app)/app/settings/audit/page.tsx b/app/(app)/app/settings/audit/page.tsx index 3f62238..f35ccf9 100644 --- a/app/(app)/app/settings/audit/page.tsx +++ b/app/(app)/app/settings/audit/page.tsx @@ -2,9 +2,9 @@ import { redirect } from "next/navigation"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Table } from "@/components/ui/table"; +import { listAuditLogs } from "@/modules/core/server/audit"; import { getCurrentAuthContext } from "@/modules/core/server/auth"; import { hasPermission } from "@/modules/core/server/permissions"; -import { readData } from "@/modules/core/server/store"; import { TablePagination, TableToolbar } from "@/modules/settings/components/TableToolbar"; import { getPage, getPageCount, getSearchParam, paginate, type SearchParams } from "@/modules/settings/lib/pagination"; @@ -25,9 +25,12 @@ export default async function AuditPage({ searchParams }: AuditPageProps): Promi const params = (await searchParams) ?? {}; const query = getSearchParam(params, "q"); const page = getPage(params); - const data = await readData(); + const logs = await listAuditLogs({ + organizationId: context.account.platformRoleId ? undefined : context.organization?.id, + limit: 100, + }); const normalizedQuery = query.trim().toLowerCase(); - const filteredLogs = data.auditLogs.filter((log) => + const filteredLogs = logs.filter((log) => [log.actorEmail ?? "系统", log.action, log.targetType, log.targetId ?? "", log.result, log.reason] .join(" ") .toLowerCase() @@ -38,10 +41,6 @@ export default async function AuditPage({ searchParams }: AuditPageProps): Promi return (
-
-

审计日志

-
- 审计列表 diff --git a/app/(app)/app/settings/layout.tsx b/app/(app)/app/settings/layout.tsx index 9f19125..5f6c6cc 100644 --- a/app/(app)/app/settings/layout.tsx +++ b/app/(app)/app/settings/layout.tsx @@ -3,12 +3,5 @@ type SettingsLayoutProps = { }; export default function SettingsLayout({ children }: SettingsLayoutProps): React.ReactElement { - return ( -
-
-

系统设置

-
- {children} -
- ); + return
{children}
; } diff --git a/app/(app)/app/settings/organizations/[id]/page.tsx b/app/(app)/app/settings/organizations/[id]/page.tsx index d4b5db9..37f141d 100644 --- a/app/(app)/app/settings/organizations/[id]/page.tsx +++ b/app/(app)/app/settings/organizations/[id]/page.tsx @@ -7,7 +7,13 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Table } from "@/components/ui/table"; import { getCurrentAuthContext, toPublicAccount } from "@/modules/core/server/auth"; import { getRoleDefinitions, hasPermission } from "@/modules/core/server/permissions"; -import { readData } from "@/modules/core/server/store"; +import { + listSettingsAccounts, + listSettingsJoinRequests, + listSettingsMemberships, + listSettingsOrganizationInvitations, + listSettingsOrganizations, +} from "@/modules/core/server/settings"; import { OrganizationDetailClient } from "@/modules/settings/components/OrganizationDetailClient"; type OrganizationDetailPageProps = { @@ -29,19 +35,24 @@ export default async function OrganizationDetailPage({ } const { id } = await params; - const data = await readData(); - const organization = data.organizations.find((item) => item.id === id); + const organizations = await listSettingsOrganizations({ organizationId: id }); + const organization = organizations[0]; if (!organization) { notFound(); } - if (context.organization?.id && context.organization.id !== organization.id) { + if (!context.account.platformRoleId && 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 [roles, organizationMemberships, organizationAccounts, invitations, joinRequests] = await Promise.all([ + getRoleDefinitions(organization.id), + listSettingsMemberships({ organizationId: organization.id }), + listSettingsAccounts({ organizationId: organization.id }), + listSettingsOrganizationInvitations({ organizationId: organization.id }), + listSettingsJoinRequests({ organizationId: organization.id }), + ]); + const accountById = new Map(organizationAccounts.map((account) => [account.id, account])); const members = organizationMemberships .map((membership) => { const account = accountById.get(membership.accountId); @@ -55,10 +66,7 @@ export default async function OrganizationDetailPage({ }; }) .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", - ); + const pendingRequests = joinRequests.filter((request) => request.status === "pending"); return (
diff --git a/app/(app)/app/settings/organizations/page.tsx b/app/(app)/app/settings/organizations/page.tsx index d58ba66..fbc69f7 100644 --- a/app/(app)/app/settings/organizations/page.tsx +++ b/app/(app)/app/settings/organizations/page.tsx @@ -5,7 +5,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Table } from "@/components/ui/table"; import { getCurrentAuthContext } from "@/modules/core/server/auth"; import { getRoleDefinitions, hasPermission } from "@/modules/core/server/permissions"; -import { readData } from "@/modules/core/server/store"; +import { listSettingsOrganizations } from "@/modules/core/server/settings"; import { OrganizationManagementClient, OrganizationRowActions, @@ -30,9 +30,10 @@ export default async function OrganizationsPage({ searchParams }: OrganizationsP const params = (await searchParams) ?? {}; const query = getSearchParam(params, "q"); const page = getPage(params); - const data = await readData(); + const organizationScopeId = context.account.platformRoleId ? undefined : context.organization?.id; + const organizations = await listSettingsOrganizations(organizationScopeId ? { organizationId: organizationScopeId } : {}); const normalizedQuery = query.trim().toLowerCase(); - const filteredOrganizations = data.organizations.filter((organization) => + const filteredOrganizations = organizations.filter((organization) => [organization.name, organization.slug, organization.status].join(" ").toLowerCase().includes(normalizedQuery), ); const pageCount = getPageCount(filteredOrganizations.length); @@ -51,10 +52,11 @@ export default async function OrganizationsPage({ searchParams }: OrganizationsP return (
-
-

机构管理

- {canManageOrganizations ? : null} -
+ {canManageOrganizations ? ( +
+ +
+ ) : null} diff --git a/app/(app)/app/settings/roles/page.tsx b/app/(app)/app/settings/roles/page.tsx index 538f685..3034904 100644 --- a/app/(app)/app/settings/roles/page.tsx +++ b/app/(app)/app/settings/roles/page.tsx @@ -36,10 +36,11 @@ export default async function RolesPage({ searchParams }: RolesPageProps): Promi return (
-
-

角色权限

- {canManageRoles ? : null} -
+ {canManageRoles ? ( +
+ +
+ ) : null} diff --git a/app/(app)/app/settings/status/page.tsx b/app/(app)/app/settings/status/page.tsx index 7d95aac..9c904f5 100644 --- a/app/(app)/app/settings/status/page.tsx +++ b/app/(app)/app/settings/status/page.tsx @@ -6,7 +6,12 @@ import { Table } from "@/components/ui/table"; import { checkDatabaseConnection } from "@/modules/core/server/db"; import { getCurrentAuthContext } from "@/modules/core/server/auth"; import { hasPermission } from "@/modules/core/server/permissions"; -import { readData } from "@/modules/core/server/store"; +import { + countSettingsBeds, + listSettingsAccounts, + listSettingsIncidents, + listSettingsOrganizations, +} from "@/modules/core/server/settings"; import { IncidentStatusActions } from "@/modules/settings/components/IncidentStatusActions"; import { TablePagination, TableToolbar } from "@/modules/settings/components/TableToolbar"; import { getPage, getPageCount, getSearchParam, paginate, type SearchParams } from "@/modules/settings/lib/pagination"; @@ -28,10 +33,18 @@ export default async function StatusPage({ searchParams }: StatusPageProps): Pro const params = (await searchParams) ?? {}; const query = getSearchParam(params, "q"); const page = getPage(params); - const [data, databaseHealth] = await Promise.all([readData(), checkDatabaseConnection()]); + const organizationScopeId = context.account.platformRoleId ? undefined : context.organization?.id; + const scopeParams = organizationScopeId ? { organizationId: organizationScopeId } : {}; + const [organizations, accounts, bedCount, incidents, databaseHealth] = await Promise.all([ + listSettingsOrganizations(scopeParams), + listSettingsAccounts(scopeParams), + countSettingsBeds(scopeParams), + listSettingsIncidents(scopeParams), + checkDatabaseConnection(), + ]); const canManageIncidents = hasPermission(context.permissions, "incident:manage"); const normalizedQuery = query.trim().toLowerCase(); - const filteredIncidents = data.incidents.filter((incident) => + const filteredIncidents = incidents.filter((incident) => [incident.title, incident.description, incident.source, incident.severity, incident.status] .join(" ") .toLowerCase() @@ -42,36 +55,33 @@ export default async function StatusPage({ searchParams }: StatusPageProps): Pro return (
-
-
-

运行状态

-

{databaseHealth.reason}

-
+
+

{databaseHealth.reason}

{databaseHealth.ok ? "Healthy" : "Degraded"}
- {data.organizations.length} + {organizations.length} 机构 - {data.accounts.length} + {accounts.length} 账号 - {data.beds.length} + {bedCount} 床位 - {data.incidents.length} + {incidents.length} 故障事件 diff --git a/app/(app)/app/settings/users/page.tsx b/app/(app)/app/settings/users/page.tsx index b3e047f..351c0f5 100644 --- a/app/(app)/app/settings/users/page.tsx +++ b/app/(app)/app/settings/users/page.tsx @@ -5,7 +5,12 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Table } from "@/components/ui/table"; import { getCurrentAuthContext, toPublicAccount } from "@/modules/core/server/auth"; import { getRoleDefinitions, hasPermission } from "@/modules/core/server/permissions"; -import { readData } from "@/modules/core/server/store"; +import { + listSettingsAccounts, + listSettingsJoinRequests, + listSettingsMemberships, + listSettingsOrganizations, +} from "@/modules/core/server/settings"; import { ROLE_LABELS } from "@/modules/core/types"; import type { RoleId } from "@/modules/core/types"; import { TablePagination, TableToolbar } from "@/modules/settings/components/TableToolbar"; @@ -38,16 +43,23 @@ export default async function UsersPage({ searchParams }: UsersPageProps): Promi const params = (await searchParams) ?? {}; const query = getSearchParam(params, "q"); const page = getPage(params); - const data = await readData(); - const roles = await getRoleDefinitions(context.organization?.id); + const canReadAllAccounts = Boolean(context.account.platformRoleId); + const organizationScopeId = canReadAllAccounts ? undefined : context.organization?.id; + const [organizations, accountsData, memberships, joinRequests, roles] = await Promise.all([ + listSettingsOrganizations(organizationScopeId ? { organizationId: organizationScopeId } : {}), + listSettingsAccounts(organizationScopeId ? { organizationId: organizationScopeId } : {}), + listSettingsMemberships(organizationScopeId ? { organizationId: organizationScopeId } : {}), + listSettingsJoinRequests(organizationScopeId ? { organizationId: organizationScopeId } : {}), + getRoleDefinitions(context.organization?.id), + ]); const roleById = new Map(roles.map((role) => [role.id, role])); - const organizationById = new Map(data.organizations.map((organization) => [organization.id, organization])); - const accounts = data.accounts.map((account) => { + const organizationById = new Map(organizations.map((organization) => [organization.id, organization])); + const accounts = accountsData.map((account) => { const membership = context.organization?.id - ? data.memberships.find( + ? memberships.find( (item) => item.accountId === account.id && item.organizationId === context.organization?.id && item.status === "active", ) - : data.memberships.find((item) => item.accountId === account.id && item.status === "active"); + : memberships.find((item) => item.accountId === account.id && item.status === "active"); const organization = membership ? organizationById.get(membership.organizationId) : undefined; const platformRole = account.platformRoleId ? roleById.get(account.platformRoleId) : undefined; @@ -65,15 +77,11 @@ export default async function UsersPage({ searchParams }: UsersPageProps): Promi return (
-
-

用户管理

-
- @@ -125,7 +133,7 @@ export default async function UsersPage({ searchParams }: UsersPageProps): Promi diff --git a/app/api/organizations/[id]/route.ts b/app/api/organizations/[id]/route.ts index ba9cf9b..c1ef062 100644 --- a/app/api/organizations/[id]/route.ts +++ b/app/api/organizations/[id]/route.ts @@ -37,6 +37,10 @@ function readStatus(value: unknown): OrganizationStatus | null { return ORGANIZATION_STATUSES.find((status) => status === value) ?? null; } +function isValidSlug(value: string): boolean { + return /^[a-z0-9][a-z0-9-]{1,47}$/.test(value) && !value.endsWith("-"); +} + export async function PATCH(request: Request, context: RouteContext): Promise { const { id } = await context.params; const auth = await requirePermission("organization:manage", { @@ -55,7 +59,7 @@ export async function PATCH(request: Request, context: RouteContext): Promise, key: string): string { return typeof value === "string" ? value.trim() : ""; } -function createSlug(name: string): string { - const base = name - .trim() - .toLowerCase() - .replace(/[^a-z0-9]+/g, "-") - .replace(/^-|-$/g, ""); - - return base.length > 0 ? base : `org-${Date.now()}`; +function isValidSlug(value: string): boolean { + return /^[a-z0-9][a-z0-9-]{1,47}$/.test(value) && !value.endsWith("-"); } export async function GET(): Promise { @@ -76,31 +70,47 @@ export async function POST(request: Request): Promise { if (!name) { return jsonFailure("机构名称不能为空"); } - - const database = getDatabase(); - const rows = await database - .insert(organizations) - .values({ - name, - slug: readString(body, "slug") || createSlug(name), - status: "active", - }) - .returning(); - const organization = rows[0]; - if (!organization) { - return jsonFailure("机构创建失败", 500); + const slug = readString(body, "slug").toLowerCase(); + if (!slug) { + return jsonFailure("机构标识不能为空"); + } + if (!isValidSlug(slug)) { + return jsonFailure("机构标识仅支持小写字母、数字和连字符,长度 2-48 位,且不能以连字符结尾"); } - await seedOrganizationRoles(organization.id); - await recordAuditLog({ - actor: auth.context.account, - organizationId: organization.id, - action: "organization.create", - targetType: "organization", - targetId: organization.id, - result: "success", - reason: `创建机构:${organization.name}`, - }); + const database = getDatabase(); + try { + const rows = await database + .insert(organizations) + .values({ + name, + slug, + status: "active", + }) + .returning(); + const organization = rows[0]; + if (!organization) { + return jsonFailure("机构创建失败", 500); + } - return jsonSuccess("机构已创建", { organization }, 201); + await seedOrganizationRoles(organization.id); + await recordAuditLog({ + actor: auth.context.account, + organizationId: organization.id, + action: "organization.create", + targetType: "organization", + targetId: organization.id, + result: "success", + reason: `创建机构:${organization.name}`, + }); + + return jsonSuccess("机构已创建", { organization }, 201); + } catch (error) { + const message = error instanceof Error ? error.message : ""; + if (message.includes("organizations_slug_unique")) { + return jsonFailure("机构标识已存在", 409); + } + + return jsonFailure("机构创建失败", 500); + } } diff --git a/app/api/settings/accounts/route.ts b/app/api/settings/accounts/route.ts index 8eb8d3c..9cad11d 100644 --- a/app/api/settings/accounts/route.ts +++ b/app/api/settings/accounts/route.ts @@ -6,7 +6,11 @@ import { recordAuditLog } from "@/modules/core/server/audit"; import { getDatabase } from "@/modules/core/server/db"; import { getRoleDefinitions, hasPermission } from "@/modules/core/server/permissions"; import { accounts, memberships, organizations, roles } from "@/modules/core/server/schema"; -import { readData } from "@/modules/core/server/store"; +import { + listSettingsAccounts, + listSettingsMemberships, + listSettingsOrganizations, +} from "@/modules/core/server/settings"; function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); @@ -27,35 +31,30 @@ export async function GET(): Promise { return auth.response; } - const data = await readData(); - const canReadAllAccounts = hasPermission(auth.context.permissions, "platform:manage"); - const organizationId = auth.context.organization?.id; - const roleDefinitions = await getRoleDefinitions(organizationId); + const canReadAllAccounts = Boolean(auth.context.account.platformRoleId) || hasPermission(auth.context.permissions, "platform:manage"); + const organizationId = canReadAllAccounts ? undefined : auth.context.organization?.id; + const [accountRows, membershipRows, organizationRows, roleDefinitions] = await Promise.all([ + listSettingsAccounts(organizationId ? { organizationId } : {}), + listSettingsMemberships(organizationId ? { organizationId } : {}), + listSettingsOrganizations(organizationId ? { organizationId } : {}), + getRoleDefinitions(auth.context.organization?.id), + ]); const roleById = new Map(roleDefinitions.map((role) => [role.id, role])); - const organizationById = new Map(data.organizations.map((organization) => [organization.id, organization])); - const scopedAccountIds = - organizationId && !canReadAllAccounts - ? new Set( - data.memberships - .filter((membership) => membership.organizationId === organizationId) - .map((membership) => membership.accountId), - ) - : null; + const organizationById = new Map(organizationRows.map((organization) => [organization.id, organization])); return jsonSuccess("账号列表已加载", { - accounts: data.accounts - .filter((account) => !scopedAccountIds || scopedAccountIds.has(account.id)) - .map((account) => { - const membership = organizationId - ? data.memberships.find( - (item) => item.accountId === account.id && item.organizationId === organizationId && item.status === "active", + accounts: accountRows.map((account) => { + const membership = auth.context.organization?.id + ? membershipRows.find( + (item) => + item.accountId === account.id && item.organizationId === auth.context.organization?.id && item.status === "active", ) - : data.memberships.find((item) => item.accountId === account.id && item.status === "active"); - const organization = membership ? organizationById.get(membership.organizationId) : undefined; - const platformRole = account.platformRoleId ? roleById.get(account.platformRoleId) : undefined; + : membershipRows.find((item) => item.accountId === account.id && item.status === "active"); + const organization = membership ? organizationById.get(membership.organizationId) : undefined; + const platformRole = account.platformRoleId ? roleById.get(account.platformRoleId) : undefined; - return toPublicAccount(account, platformRole?.key ?? membership?.roleKey, organization); - }), + return toPublicAccount(account, platformRole?.key ?? membership?.roleKey, organization); + }), }); } diff --git a/app/api/settings/join-requests/route.ts b/app/api/settings/join-requests/route.ts index 3d295fc..6cb0b5f 100644 --- a/app/api/settings/join-requests/route.ts +++ b/app/api/settings/join-requests/route.ts @@ -1,6 +1,6 @@ import { jsonSuccess } from "@/modules/core/server/api"; import { requirePermission } from "@/modules/core/server/auth"; -import { readData } from "@/modules/core/server/store"; +import { listSettingsJoinRequests } from "@/modules/core/server/settings"; export async function GET(): Promise { const auth = await requirePermission("account:read", { @@ -12,12 +12,9 @@ export async function GET(): Promise { return auth.response; } - const data = await readData(); - const organizationId = auth.context.organization?.id; + const organizationId = auth.context.account.platformRoleId ? undefined : auth.context.organization?.id; return jsonSuccess("加入申请已加载", { - joinRequests: organizationId - ? data.joinRequests.filter((request) => request.organizationId === organizationId) - : data.joinRequests, + joinRequests: await listSettingsJoinRequests(organizationId ? { organizationId } : {}), }); } diff --git a/modules/core/server/settings.ts b/modules/core/server/settings.ts new file mode 100644 index 0000000..c177de1 --- /dev/null +++ b/modules/core/server/settings.ts @@ -0,0 +1,228 @@ +import { desc, eq, sql } from "drizzle-orm"; + +import { getDatabase } from "@/modules/core/server/db"; +import { + accounts, + beds, + joinRequests, + memberships, + organizationInvitations, + organizations, + roles, + systemIncidents, +} from "@/modules/core/server/schema"; +import type { + Account, + JoinRequest, + Membership, + Organization, + OrganizationInvitation, + SystemIncident, +} from "@/modules/core/types"; + +function iso(value: Date): string { + return value.toISOString(); +} + +function toOrganization(row: typeof organizations.$inferSelect): Organization { + return { + id: row.id, + 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, + oidcAvatarClaim: row.oidcAvatarClaim, + oidcAutoProvision: row.oidcAutoProvision, + createdAt: iso(row.createdAt), + updatedAt: iso(row.updatedAt), + }; +} + +function toAccount(row: typeof accounts.$inferSelect): Account { + return { + id: row.id, + platformRoleId: row.platformRoleId ?? undefined, + name: row.name, + email: row.email, + avatarUrl: row.avatarUrl, + role: row.platformRoleId ?? "viewer", + passwordHash: row.passwordHash, + passwordSalt: row.passwordSalt, + status: row.status, + createdAt: iso(row.createdAt), + updatedAt: iso(row.updatedAt), + }; +} + +function toMembership(row: typeof memberships.$inferSelect, role: typeof roles.$inferSelect): Membership { + return { + id: row.id, + accountId: row.accountId, + organizationId: row.organizationId, + roleId: row.roleId, + roleKey: role.key, + roleLabel: role.label, + status: row.status, + createdAt: iso(row.createdAt), + updatedAt: iso(row.updatedAt), + }; +} + +function toJoinRequest(row: { + accountEmail: string; + accountName: string; + organizationName: string; + request: typeof joinRequests.$inferSelect; +}): JoinRequest { + return { + id: row.request.id, + accountId: row.request.accountId, + accountName: row.accountName, + accountEmail: row.accountEmail, + organizationId: row.request.organizationId, + organizationName: row.organizationName, + status: row.request.status, + reason: row.request.reason, + reviewedByAccountId: row.request.reviewedByAccountId ?? undefined, + reviewedAt: row.request.reviewedAt ? iso(row.request.reviewedAt) : undefined, + createdAt: iso(row.request.createdAt), + }; +} + +function toOrganizationInvitation(row: { + invitation: typeof organizationInvitations.$inferSelect; + organizationName: string; + roleLabel: string; +}): OrganizationInvitation { + return { + id: row.invitation.id, + organizationId: row.invitation.organizationId, + organizationName: row.organizationName, + roleId: row.invitation.roleId, + roleLabel: row.roleLabel, + email: row.invitation.email, + token: row.invitation.token, + status: row.invitation.status as OrganizationInvitation["status"], + createdByAccountId: row.invitation.createdByAccountId ?? undefined, + acceptedByAccountId: row.invitation.acceptedByAccountId ?? undefined, + acceptedAt: row.invitation.acceptedAt ? iso(row.invitation.acceptedAt) : undefined, + expiresAt: iso(row.invitation.expiresAt), + createdAt: iso(row.invitation.createdAt), + updatedAt: iso(row.invitation.updatedAt), + }; +} + +function toSystemIncident(row: typeof systemIncidents.$inferSelect): SystemIncident { + return { + id: row.id, + organizationId: row.organizationId ?? undefined, + severity: row.severity, + status: row.status, + title: row.title, + description: row.description, + source: row.source, + createdAt: iso(row.createdAt), + updatedAt: iso(row.updatedAt), + }; +} + +export async function listSettingsOrganizations(params: { organizationId?: string } = {}): Promise { + const database = getDatabase(); + const rows = await database + .select() + .from(organizations) + .where(params.organizationId ? eq(organizations.id, params.organizationId) : sql`true`) + .orderBy(desc(organizations.createdAt)); + + return rows.map(toOrganization); +} + +export async function listSettingsAccounts(params: { organizationId?: string } = {}): Promise { + const database = getDatabase(); + const rows = params.organizationId + ? await database + .select({ account: accounts }) + .from(accounts) + .innerJoin(memberships, eq(memberships.accountId, accounts.id)) + .where(eq(memberships.organizationId, params.organizationId)) + .orderBy(desc(accounts.createdAt)) + : await database.select({ account: accounts }).from(accounts).orderBy(desc(accounts.createdAt)); + + return rows.map((row) => toAccount(row.account)); +} + +export async function listSettingsMemberships(params: { organizationId?: string } = {}): Promise { + const database = getDatabase(); + const rows = await database + .select({ membership: memberships, role: roles }) + .from(memberships) + .innerJoin(roles, eq(roles.id, memberships.roleId)) + .where(params.organizationId ? eq(memberships.organizationId, params.organizationId) : sql`true`) + .orderBy(desc(memberships.createdAt)); + + return rows.map((row) => toMembership(row.membership, row.role)); +} + +export async function listSettingsJoinRequests(params: { organizationId?: string } = {}): Promise { + const database = getDatabase(); + const rows = await database + .select({ + request: joinRequests, + accountName: accounts.name, + accountEmail: accounts.email, + organizationName: organizations.name, + }) + .from(joinRequests) + .innerJoin(accounts, eq(accounts.id, joinRequests.accountId)) + .innerJoin(organizations, eq(organizations.id, joinRequests.organizationId)) + .where(params.organizationId ? eq(joinRequests.organizationId, params.organizationId) : sql`true`) + .orderBy(desc(joinRequests.createdAt)); + + return rows.map(toJoinRequest); +} + +export async function listSettingsOrganizationInvitations( + params: { organizationId?: string } = {}, +): Promise { + const database = getDatabase(); + const rows = await database + .select({ + invitation: organizationInvitations, + organizationName: organizations.name, + roleLabel: roles.label, + }) + .from(organizationInvitations) + .innerJoin(organizations, eq(organizations.id, organizationInvitations.organizationId)) + .innerJoin(roles, eq(roles.id, organizationInvitations.roleId)) + .where(params.organizationId ? eq(organizationInvitations.organizationId, params.organizationId) : sql`true`) + .orderBy(desc(organizationInvitations.createdAt)); + + return rows.map(toOrganizationInvitation); +} + +export async function listSettingsIncidents(params: { organizationId?: string } = {}): Promise { + const database = getDatabase(); + const rows = await database + .select() + .from(systemIncidents) + .where(params.organizationId ? eq(systemIncidents.organizationId, params.organizationId) : sql`true`) + .orderBy(desc(systemIncidents.createdAt)); + + return rows.map(toSystemIncident); +} + +export async function countSettingsBeds(params: { organizationId?: string } = {}): Promise { + const database = getDatabase(); + const rows = await database + .select({ count: sql`count(*)::int` }) + .from(beds) + .where(params.organizationId ? eq(beds.organizationId, params.organizationId) : sql`true`); + + return rows[0]?.count ?? 0; +} diff --git a/modules/settings/components/OrganizationManagementClient.tsx b/modules/settings/components/OrganizationManagementClient.tsx index fdc15ea..bd0cb9c 100644 --- a/modules/settings/components/OrganizationManagementClient.tsx +++ b/modules/settings/components/OrganizationManagementClient.tsx @@ -73,7 +73,13 @@ export function OrganizationManagementClient(): React.ReactElement { >
- + {message ? (

{message} @@ -196,7 +202,13 @@ export function OrganizationRowActions({ > - +