fix(web): keep admin selections current

This commit is contained in:
root
2026-07-13 12:51:12 +02:00
parent e83d0b25be
commit 49e8a63230
3 changed files with 21 additions and 6 deletions
@@ -32,6 +32,7 @@ const createOpen = ref(false);
const createdCode = ref<string | null>(null);
const filters = reactive({ status: typeof route.query.filter === "string" ? route.query.filter : "", userId: typeof route.query.userId === "string" ? route.query.userId : "", codeId: typeof route.query.redemptionCodeId === "string" ? route.query.redemptionCodeId : "" });
const form = reactive({ displayName: "", creditAmount: 10, reason: "redeem", maxRedemptions: 1, perUserLimit: 1, expiresAt: "" });
let refreshSequence = 0;
const activeTab = computed({ get: () => viewState.tab.value === "redemptions" ? "redemptions" : "codes", set: (value: string) => void viewState.update({ tab: value === "codes" ? undefined : "redemptions" }, false) });
const viewMode = computed({ get: () => viewState.view.value, set: (value: "summary" | "detail") => void viewState.update({ view: value === "summary" ? undefined : value }, false) });
const selectedCode = computed(() => selectedCodeId.value ? codes.value.find((item) => codeKey(item) === selectedCodeId.value) ?? null : null);
@@ -41,8 +42,9 @@ const codeColumns: DataGridColumn[] = [{ key: "code", label: "Code" }, { key: "s
const redemptionColumns: DataGridColumn[] = [{ key: "time", label: "Time" }, { key: "user", label: "User" }, { key: "code", label: "Code" }, { key: "credits", label: "Credits", align: "right" }, { key: "status", label: "Status" }, { key: "ledger", label: "Ledger" }];
onMounted(refreshAll);
watch(() => [route.query.filter, route.query.userId, route.query.redemptionCodeId, route.query.codeId].join("\0"), () => { filters.status = typeof route.query.filter === "string" ? route.query.filter : ""; filters.userId = typeof route.query.userId === "string" ? route.query.userId : ""; filters.codeId = typeof route.query.redemptionCodeId === "string" ? route.query.redemptionCodeId : ""; selectedCodeId.value = typeof route.query.codeId === "string" ? route.query.codeId : null; void refreshAll(); });
async function refreshAll(): Promise<void> { loading.value = true; error.value = null; const [codeResult, redemptionResult] = await Promise.all([billingAPI.adminRedeemCodes({ status: filters.status || undefined, limit: 100 }), billingAPI.adminRedeemRedemptions({ userId: filters.userId || undefined, codeId: filters.codeId || undefined, limit: 100 })]); const errors: string[] = []; if (codeResult.ok) codes.value = codeResult.data?.codes ?? []; else errors.push(`Codes: ${codeResult.error ?? `HTTP ${codeResult.status}`}`); if (redemptionResult.ok) redemptions.value = redemptionResult.data?.redemptions ?? []; else errors.push(`Redemptions: ${redemptionResult.error ?? `HTTP ${redemptionResult.status}`}`); error.value = errors.join(" · ") || null; loading.value = false; loaded.value = true; }
watch(() => [route.query.filter, route.query.userId, route.query.redemptionCodeId].join("\0"), () => { filters.status = typeof route.query.filter === "string" ? route.query.filter : ""; filters.userId = typeof route.query.userId === "string" ? route.query.userId : ""; filters.codeId = typeof route.query.redemptionCodeId === "string" ? route.query.redemptionCodeId : ""; void refreshAll(); });
watch(() => route.query.codeId, (value) => { selectedCodeId.value = typeof value === "string" ? value : null; });
async function refreshAll(): Promise<void> { const requestSequence = ++refreshSequence; loading.value = true; error.value = null; const [codeResult, redemptionResult] = await Promise.all([billingAPI.adminRedeemCodes({ status: filters.status || undefined, limit: 100 }), billingAPI.adminRedeemRedemptions({ userId: filters.userId || undefined, codeId: filters.codeId || undefined, limit: 100 })]); if (requestSequence !== refreshSequence) return; const errors: string[] = []; if (codeResult.ok) codes.value = codeResult.data?.codes ?? []; else errors.push(`Codes: ${codeResult.error ?? `HTTP ${codeResult.status}`}`); if (redemptionResult.ok) redemptions.value = redemptionResult.data?.redemptions ?? []; else errors.push(`Redemptions: ${redemptionResult.error ?? `HTTP ${redemptionResult.status}`}`); error.value = errors.join(" · ") || null; loading.value = false; loaded.value = true; }
async function persistFilters(): Promise<void> { await router.push({ query: { ...route.query, filter: filters.status || undefined, userId: filters.userId.trim() || undefined, redemptionCodeId: filters.codeId.trim() || undefined } }); }
async function selectCode(code: RedeemCodeSummary): Promise<void> { const codeId = codeKey(code); selectedCodeId.value = codeId; await router.push({ query: { ...route.query, codeId } }); }
async function closeInspector(): Promise<void> { selectedCodeId.value = null; await router.push({ query: { ...route.query, codeId: undefined } }); }
@@ -28,6 +28,7 @@ const rows = ref<AdminSecretsRow[]>([]);
const planPanelOpen = ref(false);
const route = useRoute();
const router = useRouter();
let reloadSequence = 0;
const viewState = useConsoleViewState<"flow" | "list">({ view: "flow", viewValues: ["flow", "list"], filter: "all" });
const canPlan = computed(() => summary.value?.permissions.plan === true);
const query = computed({ get: () => viewState.q.value, set: (value: string) => void viewState.update({ q: value.trim() }) });
@@ -59,9 +60,11 @@ const factSteps = computed(() => selected.value ? [
onMounted(() => void reload());
async function reload(): Promise<void> {
const requestSequence = ++reloadSequence;
refreshing.value = true;
error.value = null; apiError.value = null; diagnostic.value = null;
const [summaryResult, listResult] = await Promise.all([adminSecretsAPI.summary(), adminSecretsAPI.externalSecrets()]);
if (requestSequence !== reloadSequence) return;
if (!summaryResult.ok) { error.value = summaryResult.error ?? `HTTP ${summaryResult.status}`; apiError.value = summaryResult.apiError ?? null; diagnostic.value = summaryResult.diagnostic ?? null; }
else {
summary.value = normalizeAdminSecretsSummary(summaryResult.data);
@@ -51,6 +51,9 @@ const auditFilters = reactive({ search: textQuery("auditSearch"), userId: textQu
const createForm = reactive({ email: "", username: "", displayName: "", password: "", role: "user", status: "active", initialCredits: 0 });
const editForm = reactive({ email: "", username: "", displayName: "", password: "", role: "user", status: "active", planId: "default" });
const creditForm = reactive({ deltaCredits: 0, reason: "admin_adjust", idempotencyKey: "" });
let usersRequestSequence = 0;
let detailRequestSequence = 0;
let auditRequestSequence = 0;
const selectedSummary = computed(() => detail.value?.summary ?? users.value.find((item) => item.user?.id === selectedUserId.value) ?? null);
const selectedUser = computed(() => selectedSummary.value?.user ?? null);
@@ -74,7 +77,7 @@ onMounted(async () => {
watch(() => route.params.userId, (value) => {
const userId = typeof value === "string" ? value : "";
if (userId && userId !== selectedUserId.value) void selectUser(userId, false);
if (!userId) { selectedUserId.value = null; detail.value = null; }
if (!userId) { detailRequestSequence += 1; selectedUserId.value = null; detail.value = null; detailLoading.value = false; }
});
watch(() => route.query, () => {
@@ -91,9 +94,12 @@ const auditQueryMap = { search: "auditSearch", userId: "auditUser", serviceId: "
function textQuery(key: string): string { return typeof route.query[key] === "string" ? String(route.query[key]) : ""; }
async function loadPlans(): Promise<void> { const response = await usageAPI.adminPlans(); if (response.ok) plans.value = response.data?.plans ?? []; }
async function loadUsers(updateRoute = true): Promise<void> {
const requestSequence = ++usersRequestSequence;
if (updateRoute) await syncUserQuery();
if (requestSequence !== usersRequestSequence) return;
loading.value = true;
const response = await usageAPI.adminUsers({ page: page.value, pageSize, search: filters.search.trim(), status: filters.status, role: filters.role });
if (requestSequence !== usersRequestSequence) return;
if (!response.ok) error.value = response.error ?? `HTTP ${response.status}`;
else { users.value = response.data?.users ?? []; total.value = Number(response.data?.total ?? response.data?.count ?? users.value.length); stateAuthority.value = String(response.data?.state?.stateAuthority ?? "pk01-postgres"); error.value = null; }
loading.value = false; loaded.value = true;
@@ -103,21 +109,25 @@ async function setPage(value: number): Promise<void> { page.value = value; await
async function syncUserQuery(): Promise<void> { const query = { ...route.query, q: filters.search.trim() || undefined, filter: filters.status || undefined, role: filters.role || undefined, cursor: page.value > 1 ? String(page.value) : undefined }; await router.push({ query }); }
async function selectUser(userId: string | undefined, updateRoute = true): Promise<void> {
if (!userId) return;
const requestSequence = ++detailRequestSequence;
selectedUserId.value = userId;
if (updateRoute && route.params.userId !== userId) await router.push({ name: "UserDetail", params: { userId }, query: route.query });
selectedUserId.value = userId; detailLoading.value = true; mutationError.value = null;
if (requestSequence !== detailRequestSequence || selectedUserId.value !== userId) return;
detailLoading.value = true; mutationError.value = null;
const response = await usageAPI.adminUser(userId);
if (requestSequence !== detailRequestSequence || selectedUserId.value !== userId) return;
if (!response.ok) { mutationError.value = response.error ?? `HTTP ${response.status}`; detail.value = null; }
else { detail.value = response.data?.detail ?? null; fillEditForm(detail.value?.summary?.user ?? users.value.find((item) => item.user?.id === userId)?.user); }
detailLoading.value = false;
}
function closeInspector(): void { selectedUserId.value = null; detail.value = null; void router.push({ name: "Users", query: route.query }); }
function closeInspector(): void { detailRequestSequence += 1; selectedUserId.value = null; detail.value = null; detailLoading.value = false; void router.push({ name: "Users", query: route.query }); }
function fillEditForm(user: (AuthUser & { email?: string }) | null | undefined): void { editForm.email = user?.email ?? ""; editForm.username = user?.username ?? ""; editForm.displayName = user?.displayName ?? ""; editForm.password = ""; editForm.role = user?.role === "admin" ? "admin" : "user"; editForm.status = String(user?.status ?? "active"); editForm.planId = selectedSummary.value?.planId ?? "default"; }
async function createUser(): Promise<void> { pending.value = "create"; mutationError.value = null; const response = await usageAPI.createAdminUser({ ...createForm, initialCredits: Number(createForm.initialCredits || 0) }); pending.value = null; if (!response.ok) { mutationError.value = response.error ?? `HTTP ${response.status}`; return; } Object.assign(createForm, { email: "", username: "", displayName: "", password: "", role: "user", status: "active", initialCredits: 0 }); createOpen.value = false; await loadUsers(false); if (response.data?.user?.id) await selectUser(response.data.user.id); }
async function saveUser(): Promise<void> { const userId = selectedUser.value?.id; if (!userId) return; pending.value = `save:${userId}`; mutationError.value = null; const body: Record<string, unknown> = { email: editForm.email.trim(), username: editForm.username.trim(), displayName: editForm.displayName.trim(), role: editForm.role, status: editForm.status, planId: editForm.planId || "default" }; if (editForm.password) body.password = editForm.password; const response = await usageAPI.updateAdminUser(userId, body); pending.value = null; if (!response.ok) mutationError.value = response.error ?? `HTTP ${response.status}`; else { await loadUsers(false); await selectUser(userId, false); } }
async function setUserStatus(item: AdminBillingUserSummary): Promise<void> { const userId = item.user?.id; if (!userId) return; const status: AccessUserStatus = item.user?.status === "disabled" ? "active" : "disabled"; pending.value = `status:${userId}`; mutationError.value = null; const response = await usageAPI.updateAdminUserStatus(userId, status); pending.value = null; if (!response.ok) mutationError.value = response.error ?? `HTTP ${response.status}`; else { await loadUsers(false); if (selectedUserId.value === userId) await selectUser(userId, false); } }
async function adjustCredits(): Promise<void> { const userId = selectedUser.value?.id; if (!userId) return; pending.value = `credits:${userId}`; mutationError.value = null; const response = await usageAPI.adjustAdminCredits(userId, { deltaCredits: Number(creditForm.deltaCredits), reason: creditForm.reason.trim() || "admin_adjust", idempotencyKey: creditForm.idempotencyKey.trim() || undefined }); pending.value = null; if (!response.ok) mutationError.value = response.error ?? `HTTP ${response.status}`; else { creditForm.deltaCredits = 0; creditForm.idempotencyKey = ""; await Promise.all([loadUsers(false), selectUser(userId, false), loadAudit(false)]); } }
function auditParams(extra: Record<string, unknown> = {}): Record<string, unknown> { const params: Record<string, unknown> = { page: auditPage.value, pageSize: auditPageSize, ...extra }; for (const [key, value] of Object.entries(auditFilters)) if (String(value).trim()) params[key] = String(value).trim(); if (auditTab.value === "usage") { delete params.kind; delete params.source; } else { delete params.serviceId; delete params.resourceType; delete params.apiKeyId; } return params; }
async function loadAudit(updateRoute = true): Promise<void> { if (updateRoute) await syncAuditQuery(); auditLoading.value = true; auditError.value = null; const response = auditTab.value === "usage" ? await usageAPI.adminUsage(auditParams()) : await usageAPI.adminLedger(auditParams()); auditLoading.value = false; if (!response.ok) { auditError.value = response.error ?? `HTTP ${response.status}`; return; } auditTotal.value = Number(response.data?.total ?? response.data?.count ?? 0); if (auditTab.value === "usage") auditUsageRows.value = response.data?.rows ?? []; else auditLedgerRows.value = response.data?.rows ?? []; }
async function loadAudit(updateRoute = true): Promise<void> { const requestSequence = ++auditRequestSequence; if (updateRoute) await syncAuditQuery(); if (requestSequence !== auditRequestSequence) return; auditLoading.value = true; auditError.value = null; const response = auditTab.value === "usage" ? await usageAPI.adminUsage(auditParams()) : await usageAPI.adminLedger(auditParams()); if (requestSequence !== auditRequestSequence) return; auditLoading.value = false; if (!response.ok) { auditError.value = response.error ?? `HTTP ${response.status}`; return; } auditTotal.value = Number(response.data?.total ?? response.data?.count ?? 0); if (auditTab.value === "usage") auditUsageRows.value = response.data?.rows ?? []; else auditLedgerRows.value = response.data?.rows ?? []; }
async function syncAuditQuery(): Promise<void> { const query: LocationQueryRaw = { ...route.query, audit: auditTab.value === "ledger" ? "ledger" : undefined, auditPage: auditPage.value > 1 ? String(auditPage.value) : undefined }; for (const [key, queryKey] of Object.entries(auditQueryMap)) query[queryKey] = auditFilters[key as keyof typeof auditFilters].trim() || undefined; await router.push({ query }); }
async function setAuditTab(value: "usage" | "ledger"): Promise<void> { auditTab.value = value; auditPage.value = 1; await loadAudit(); }
async function setAuditPage(value: number): Promise<void> { auditPage.value = value; await loadAudit(); }