/**
* 成员 —— Keycloak 目录的投影(ADR-012)。
*
* **这里的写是穿透的**:建的人立刻能登录、停的人立刻登不上,没有本地草稿,也没有撤销。
* 所以每个写动作都走 `Acts` 的二次确认带(三必答:谁受影响 / 能不能撤 / 几时生效)。
*
* 检索与「含停用」都交给服务端 —— 那边握着全量,前端只拿要摆的那些。
*/
import { useState } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { createOrgUserRoute, SURFACE_LABEL } from '@contracts';
import type { Surface } from '@contracts';
import { call } from '@web-shared/typed-fetch.ts';
import { Acts, Card, Chip, Clear, Field, NList, NRow, Note, QueryGate, St } from '@web-shared/gov/primitives.tsx';
import { Btn } from '@web-shared/Btn.tsx';
import type { Nav } from '@web-shared/gov/gov-route.ts';
import { ORG_KEYS, useOrgUsers } from '@web-shared/gov/gov-api.ts';
/** 能进的端 —— 摆人读名;一个都没有就直说,别留一片空白让人以为没加载出来 */
export function EndChips({ ends }: { ends: readonly Surface[] }) {
if (ends.length === 0) return 无端可进;
return (
<>
{ends.map((e) => (
{SURFACE_LABEL[e]}
))}
>
);
}
export function MembersTab({ nav }: { nav: Nav }) {
const [q, setQ] = useState('');
const [includeDisabled, setIncludeDisabled] = useState(true);
const [creating, setCreating] = useState(false);
const users = useOrgUsers(q, includeDisabled);
const rows = users.data?.users ?? [];
const active = rows.filter((u) => u.enabled).length;
/*
* 数还没取回来就别摆数 —— `?? []` 会让计数在载入那几秒显示「0 人 · 0 在职」,
* 而那是**一句假话**:不是没有人,是还不知道有几个人(铁规矩 8)。
*/
const counted = users.data !== undefined;
return (
<>
setQ(e.target.value)}
/>
{counted ? `${rows.length} 人 · ${active} 在职` : '数着…'}
setCreating((v) => !v)}>
{creating ? '收起' : '新建成员'}
}
>
真源是 Keycloak · 停用的人立刻登不上;在途任务不受影响(需手动改派)。服务账号不在这份名单里 ——
它是身份不是人。
{creating && setCreating(false)} />}
{rows.length === 0 ? (
{q === '' ? '一个人都没有 —— 去 Keycloak 建人,或用「新建成员」。' : `没有匹配「${q}」的成员。`}
) : (
{rows.map((u) => (
},
]}
right={
{u.enabled ? '在职' : '已停用'}
}
onClick={() => nav.push({ k: 'emp', id: u.sub })}
/>
))}
)}
>
);
}
/**
* 新建成员 —— 口令一律**临时**:本人首登必须改。
* 我们不存它、不回显 —— 它经过这个表单就再也不出现了,所以建完那一句话得把它说清楚。
*/
function CreateMemberForm({ onDone }: { onDone: () => void }) {
const qc = useQueryClient();
const [username, setUsername] = useState('');
const [lastName, setLastName] = useState('');
const [firstName, setFirstName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [err, setErr] = useState(undefined);
const create = useMutation({
mutationFn: () =>
call(createOrgUserRoute, {
body: {
username: username.trim(),
...(lastName.trim() === '' ? {} : { lastName: lastName.trim() }),
...(firstName.trim() === '' ? {} : { firstName: firstName.trim() }),
...(email.trim() === '' ? {} : { email: email.trim() }),
...(password === '' ? {} : { password }),
},
}),
onSuccess: () => {
// 多了一个人 → 名单变了,角色的持有数也跟着变;组没动
void qc.invalidateQueries({ queryKey: ORG_KEYS.users });
void qc.invalidateQueries({ queryKey: ORG_KEYS.roles });
onDone();
},
onError: (e: unknown) => setErr(e instanceof Error ? e.message : String(e)),
});
return (
);
}