Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* Admin orgs UI client — users + companies directory, staff roles, plan assign.
|
||||
* Contract: docs/admin-roles-support/04-contract.md · Docs: 10-admin-orgs-ui.md
|
||||
*/
|
||||
import { api, ApiError } from "$lib/api";
|
||||
import {
|
||||
assignAdminPlan,
|
||||
classifyAdminPlanVisibility,
|
||||
adminPlanVisibilityBadgeVariant,
|
||||
adminPlanVisibilityLabel,
|
||||
planOptionLabel,
|
||||
type AdminBillingPlan,
|
||||
type AdminPlanVisibility,
|
||||
ADMIN_PLANS_PATH
|
||||
} from "$lib/admin-billing-plans";
|
||||
|
||||
export const ADMIN_USERS_PATH = "/api/admin/users";
|
||||
export const ADMIN_COMPANIES_PATH = "/api/admin/companies";
|
||||
export const ADMIN_STAFF_ROLE_PATH = (userId: string) =>
|
||||
`/api/admin/users/${encodeURIComponent(userId)}/staff-role`;
|
||||
|
||||
export const PAGE_SIZE = 25;
|
||||
|
||||
export type PlatformStaffRole = "admin" | "developer" | "support_staff";
|
||||
|
||||
export const STAFF_ROLE_OPTIONS: { value: "" | PlatformStaffRole; label: string }[] = [
|
||||
{ value: "", label: "No staff role" },
|
||||
{ value: "admin", label: "Admin" },
|
||||
{ value: "developer", label: "Developer" },
|
||||
{ value: "support_staff", label: "Support staff" }
|
||||
];
|
||||
|
||||
export type AdminOrgUser = {
|
||||
id: string;
|
||||
email: string;
|
||||
name?: string | null;
|
||||
must_set_password?: boolean;
|
||||
is_platform_admin?: boolean;
|
||||
staff_role?: string | null;
|
||||
resolved_role?: string;
|
||||
is_active?: boolean;
|
||||
created_at?: string;
|
||||
};
|
||||
|
||||
export type AdminOrgCompany = {
|
||||
id: string;
|
||||
name: string;
|
||||
language?: string;
|
||||
created_at?: string;
|
||||
total_credits?: number;
|
||||
used_credits?: number;
|
||||
has_active_plan?: boolean;
|
||||
plan_id?: number | null;
|
||||
plan_name?: string | null;
|
||||
plan_is_custom?: boolean;
|
||||
plan_is_legacy?: boolean;
|
||||
/** False when the company has no non-revoked api_keys (cutover reissue gap). */
|
||||
has_api_key?: boolean;
|
||||
};
|
||||
|
||||
export type PaginatedUsers = {
|
||||
users: AdminOrgUser[];
|
||||
total: number;
|
||||
limit: number;
|
||||
offset: number;
|
||||
};
|
||||
|
||||
export type PaginatedCompanies = {
|
||||
companies: AdminOrgCompany[];
|
||||
total: number;
|
||||
limit: number;
|
||||
offset: number;
|
||||
without_active_plan?: boolean;
|
||||
without_api_keys?: boolean;
|
||||
};
|
||||
|
||||
export function staffRoleLabel(role: string | null | undefined): string {
|
||||
switch ((role ?? "").trim()) {
|
||||
case "admin":
|
||||
return "Admin";
|
||||
case "developer":
|
||||
return "Developer";
|
||||
case "support_staff":
|
||||
return "Support staff";
|
||||
default:
|
||||
return "User";
|
||||
}
|
||||
}
|
||||
|
||||
export function staffRoleBadgeVariant(
|
||||
role: string | null | undefined
|
||||
): "default" | "secondary" | "outline" | "warning" {
|
||||
switch ((role ?? "").trim()) {
|
||||
case "admin":
|
||||
return "default";
|
||||
case "developer":
|
||||
return "secondary";
|
||||
case "support_staff":
|
||||
return "warning";
|
||||
default:
|
||||
return "outline";
|
||||
}
|
||||
}
|
||||
|
||||
export function companyPlanVisibility(
|
||||
company: AdminOrgCompany
|
||||
): AdminPlanVisibility | "none" {
|
||||
if (!company.has_active_plan || !company.plan_name) return "none";
|
||||
if (company.plan_is_legacy) return "legacy";
|
||||
return classifyAdminPlanVisibility({
|
||||
name: company.plan_name,
|
||||
is_custom: Boolean(company.plan_is_custom)
|
||||
});
|
||||
}
|
||||
|
||||
export function companyPlanBadge(company: AdminOrgCompany): {
|
||||
label: string;
|
||||
variant: "outline" | "warning" | "secondary" | "default";
|
||||
} {
|
||||
if (!company.has_active_plan || !company.plan_name) {
|
||||
return { label: "No plan", variant: "warning" };
|
||||
}
|
||||
if (company.plan_is_legacy) {
|
||||
return { label: `Legacy · ${company.plan_name}`, variant: "warning" };
|
||||
}
|
||||
const kind = classifyAdminPlanVisibility({
|
||||
name: company.plan_name,
|
||||
is_custom: Boolean(company.plan_is_custom)
|
||||
});
|
||||
return {
|
||||
label: `${adminPlanVisibilityLabel(kind)} · ${company.plan_name}`,
|
||||
variant: adminPlanVisibilityBadgeVariant(kind)
|
||||
};
|
||||
}
|
||||
|
||||
export async function listAdminUsers(opts: {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
q?: string;
|
||||
staff_only?: boolean;
|
||||
active_only?: boolean;
|
||||
inactive_only?: boolean;
|
||||
}): Promise<PaginatedUsers> {
|
||||
const params = new URLSearchParams();
|
||||
params.set("limit", String(opts.limit ?? PAGE_SIZE));
|
||||
params.set("offset", String(opts.offset ?? 0));
|
||||
if (opts.q?.trim()) params.set("q", opts.q.trim());
|
||||
if (opts.staff_only) params.set("staff_only", "1");
|
||||
if (opts.active_only) params.set("active_only", "1");
|
||||
if (opts.inactive_only) params.set("inactive_only", "1");
|
||||
const res = await api<PaginatedUsers>(`${ADMIN_USERS_PATH}?${params}`);
|
||||
return {
|
||||
users: res.users ?? [],
|
||||
total: Number(res.total ?? res.users?.length ?? 0),
|
||||
limit: Number(res.limit ?? opts.limit ?? PAGE_SIZE),
|
||||
offset: Number(res.offset ?? opts.offset ?? 0)
|
||||
};
|
||||
}
|
||||
|
||||
export async function listAdminCompanies(opts: {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
q?: string;
|
||||
without_active_plan?: boolean;
|
||||
without_api_keys?: boolean;
|
||||
}): Promise<PaginatedCompanies> {
|
||||
const params = new URLSearchParams();
|
||||
params.set("limit", String(opts.limit ?? PAGE_SIZE));
|
||||
params.set("offset", String(opts.offset ?? 0));
|
||||
if (opts.q?.trim()) params.set("q", opts.q.trim());
|
||||
if (opts.without_active_plan) params.set("without_active_plan", "1");
|
||||
if (opts.without_api_keys) params.set("without_api_keys", "1");
|
||||
const res = await api<PaginatedCompanies>(`${ADMIN_COMPANIES_PATH}?${params}`);
|
||||
return {
|
||||
companies: res.companies ?? [],
|
||||
total: Number(res.total ?? res.companies?.length ?? 0),
|
||||
limit: Number(res.limit ?? opts.limit ?? PAGE_SIZE),
|
||||
offset: Number(res.offset ?? opts.offset ?? 0),
|
||||
without_active_plan: Boolean(res.without_active_plan),
|
||||
without_api_keys: Boolean(res.without_api_keys)
|
||||
};
|
||||
}
|
||||
|
||||
export async function listAdminPlansForAssign(): Promise<AdminBillingPlan[]> {
|
||||
const res = await api<{ plans: AdminBillingPlan[] }>(ADMIN_PLANS_PATH);
|
||||
return res.plans ?? [];
|
||||
}
|
||||
|
||||
export async function setAdminStaffRole(
|
||||
userId: string,
|
||||
staffRole: "" | PlatformStaffRole
|
||||
): Promise<AdminOrgUser> {
|
||||
const body =
|
||||
staffRole === ""
|
||||
? { staff_role: null }
|
||||
: { staff_role: staffRole };
|
||||
const res = await api<{ user: AdminOrgUser }>(ADMIN_STAFF_ROLE_PATH(userId), {
|
||||
method: "PATCH",
|
||||
body
|
||||
});
|
||||
return res.user;
|
||||
}
|
||||
|
||||
export function isStaffRoleApiUnavailable(err: unknown): boolean {
|
||||
return err instanceof ApiError && (err.status === 404 || err.status === 501);
|
||||
}
|
||||
|
||||
export { assignAdminPlan, planOptionLabel };
|
||||
export type { AdminBillingPlan };
|
||||
Reference in New Issue
Block a user