fixes
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
PAGE_SIZE,
|
||||
STAFF_ROLE_OPTIONS,
|
||||
assignAdminPlan,
|
||||
cloneAdminCompanyCatalog,
|
||||
companyPlanBadge,
|
||||
isStaffRoleApiUnavailable,
|
||||
listAdminCompanies,
|
||||
@@ -24,6 +25,7 @@
|
||||
type AdminOrgUser,
|
||||
type PlatformStaffRole
|
||||
} from "$lib/admin-orgs";
|
||||
import { authSession } from "$lib/auth-session.svelte";
|
||||
import PageShell from "$lib/components/PageShell.svelte";
|
||||
import Alert from "$lib/components/Alert.svelte";
|
||||
import Spinner from "$lib/components/Spinner.svelte";
|
||||
@@ -52,7 +54,7 @@
|
||||
TabsList,
|
||||
TabsTrigger
|
||||
} from "$lib/components/ui";
|
||||
import { Building2, KeyRound, Search, Shield, UserPlus, Users } from "@lucide/svelte";
|
||||
import { Building2, Copy, KeyRound, Search, Shield, UserPlus, Users } from "@lucide/svelte";
|
||||
|
||||
type TabKey = "users" | "companies";
|
||||
|
||||
@@ -88,11 +90,22 @@
|
||||
let assignCompany = $state<AdminOrgCompany | null>(null);
|
||||
let assignPlanId = $state("");
|
||||
|
||||
let cloneOpen = $state(false);
|
||||
let cloneCompany = $state<AdminOrgCompany | null>(null);
|
||||
|
||||
const usersPage = $derived(Math.floor(usersOffset / PAGE_SIZE) + 1);
|
||||
const usersPages = $derived(Math.max(1, Math.ceil(usersTotal / PAGE_SIZE)));
|
||||
const companiesPage = $derived(Math.floor(companiesOffset / PAGE_SIZE) + 1);
|
||||
const companiesPages = $derived(Math.max(1, Math.ceil(companiesTotal / PAGE_SIZE)));
|
||||
|
||||
const cloneDestLabel = $derived.by(() => {
|
||||
const me = authSession.me;
|
||||
const home = me?.staff_home_company;
|
||||
if (home?.name) return home.name;
|
||||
if (me?.company?.name) return me.company.name;
|
||||
return i18n.t("admin.users.cloneDestFallback");
|
||||
});
|
||||
|
||||
onMount(async () => {
|
||||
const gate = await requirePlatformAdmin();
|
||||
if (!gate.ok) {
|
||||
@@ -342,6 +355,44 @@
|
||||
}
|
||||
}
|
||||
|
||||
function openCloneDialog(company: AdminOrgCompany) {
|
||||
cloneCompany = company;
|
||||
cloneOpen = true;
|
||||
error = "";
|
||||
success = "";
|
||||
}
|
||||
|
||||
async function confirmCloneCatalog() {
|
||||
if (!cloneCompany) return;
|
||||
busy = true;
|
||||
error = "";
|
||||
success = "";
|
||||
try {
|
||||
const me = authSession.me;
|
||||
const destId =
|
||||
me?.staff_home_company_id?.trim() ||
|
||||
me?.staff_home_company?.id?.trim() ||
|
||||
undefined;
|
||||
const res = await cloneAdminCompanyCatalog(cloneCompany.id, {
|
||||
destCompanyId: destId
|
||||
});
|
||||
const products = Number(res.counts?.raw_products ?? 0);
|
||||
const categories = Number(res.counts?.categories ?? 0);
|
||||
success = i18n.t("flash.admin.catalogCloned", {
|
||||
source: cloneCompany.name,
|
||||
dest: cloneDestLabel,
|
||||
products: String(products),
|
||||
categories: String(categories)
|
||||
});
|
||||
cloneOpen = false;
|
||||
cloneCompany = null;
|
||||
} catch (err) {
|
||||
error = failureMessage(err, "Clone catalog failed");
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function sendSetPasswordEmails(userId?: string) {
|
||||
busy = true;
|
||||
error = "";
|
||||
@@ -712,14 +763,28 @@
|
||||
</TableCell>
|
||||
<TableCell class="hidden text-muted-foreground lg:table-cell">{company.language || "—"}</TableCell>
|
||||
<TableCell stickyRight>
|
||||
<Button
|
||||
size="sm"
|
||||
onclick={() => openAssignDialog(company)}
|
||||
aria-label={`Assign plan to ${company.name}`}
|
||||
>
|
||||
<UserPlus class="h-3.5 w-3.5 lg:mr-1" aria-hidden="true" />
|
||||
<span class="hidden lg:inline">{i18n.t("admin.users.assignPlan")}</span>
|
||||
</Button>
|
||||
<div class="flex flex-wrap items-center justify-end gap-1">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onclick={() => openCloneDialog(company)}
|
||||
aria-label={i18n.t("admin.users.cloneCatalogAria", {
|
||||
name: company.name
|
||||
})}
|
||||
data-testid="admin-clone-catalog"
|
||||
>
|
||||
<Copy class="h-3.5 w-3.5 lg:mr-1" aria-hidden="true" />
|
||||
<span class="hidden lg:inline">{i18n.t("admin.users.cloneCatalog")}</span>
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onclick={() => openAssignDialog(company)}
|
||||
aria-label={`Assign plan to ${company.name}`}
|
||||
>
|
||||
<UserPlus class="h-3.5 w-3.5 lg:mr-1" aria-hidden="true" />
|
||||
<span class="hidden lg:inline">{i18n.t("admin.users.assignPlan")}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
@@ -758,8 +823,8 @@
|
||||
|
||||
<Dialog
|
||||
bind:open={passwordOpen}
|
||||
title="Set password"
|
||||
description="Force-set a login password for this user (works for fake/legacy emails that cannot receive invites)."
|
||||
title={i18n.t("admin.users.setPasswordTitle")}
|
||||
description={i18n.t("admin.users.setPasswordDesc")}
|
||||
>
|
||||
<form class="space-y-4" onsubmit={saveForcedPassword}>
|
||||
{#if error}
|
||||
@@ -771,7 +836,7 @@
|
||||
</p>
|
||||
{/if}
|
||||
<div class="space-y-2">
|
||||
<Label for="forced-password">New password</Label>
|
||||
<Label for="forced-password">{i18n.t("admin.users.newPassword")}</Label>
|
||||
<Input
|
||||
id="forced-password"
|
||||
type="password"
|
||||
@@ -781,7 +846,9 @@
|
||||
bind:value={passwordValue}
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" loading={busyUserId === passwordUser?.id}>Set password</Button>
|
||||
<Button type="submit" loading={busyUserId === passwordUser?.id}
|
||||
>{i18n.t("admin.users.setPasswordSubmit")}</Button
|
||||
>
|
||||
</form>
|
||||
</Dialog>
|
||||
|
||||
@@ -838,3 +905,42 @@
|
||||
<Button type="submit" loading={busy} disabled={!assignPlanId}>{i18n.t("admin.users.assign")}</Button>
|
||||
</form>
|
||||
</Dialog>
|
||||
|
||||
<Dialog
|
||||
bind:open={cloneOpen}
|
||||
title={i18n.t("admin.users.cloneCatalogTitle")}
|
||||
description={i18n.t("admin.users.cloneCatalogDesc")}
|
||||
>
|
||||
<div class="space-y-4">
|
||||
{#if error}
|
||||
<p class="text-sm text-destructive" role="alert">{error}</p>
|
||||
{/if}
|
||||
{#if cloneCompany}
|
||||
<p class="text-sm text-foreground">
|
||||
{i18n.t("admin.users.cloneCatalogFrom", { name: cloneCompany.name })}
|
||||
</p>
|
||||
{/if}
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{i18n.t("admin.users.cloneCatalogInto", { name: cloneDestLabel })}
|
||||
</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{i18n.t("admin.users.cloneCatalogWarning")}
|
||||
</p>
|
||||
<div class="flex flex-wrap justify-end gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
disabled={busy}
|
||||
onclick={() => {
|
||||
cloneOpen = false;
|
||||
cloneCompany = null;
|
||||
}}
|
||||
>
|
||||
{i18n.t("admin.users.cloneCatalogCancel")}
|
||||
</Button>
|
||||
<Button type="button" loading={busy} onclick={() => confirmCloneCatalog()}>
|
||||
{i18n.t("admin.users.cloneCatalogConfirm")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
Reference in New Issue
Block a user