This commit is contained in:
2026-08-16 18:49:02 +02:00
parent a94a5aaad7
commit b617342601
12 changed files with 168 additions and 84 deletions
+113 -63
View File
@@ -8,6 +8,7 @@
import { formatCredits, formatDate } from "$lib/utils";
import {
PAGE_SIZE,
PLATFORM_DEMO_COMPANY_NAME,
STAFF_ROLE_OPTIONS,
assignAdminPlan,
cloneAdminCompanyCatalog,
@@ -93,23 +94,23 @@
let cloneOpen = $state(false);
let cloneCompany = $state<AdminOrgCompany | null>(null);
let cloneDestId = $state("");
let cloneDestOptions = $state<AdminOrgCompany[]>([]);
let syncOpen = $state(false);
let syncCompany = $state<AdminOrgCompany | null>(null);
const cloneDestLabel = $derived.by(() => {
const selected = cloneDestOptions.find((c) => c.id === cloneDestId);
if (selected?.name) return selected.name;
return i18n.t("admin.users.cloneDestFallback");
});
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) {
@@ -359,11 +360,49 @@
}
}
function openCloneDialog(company: AdminOrgCompany) {
async function openCloneDialog(company: AdminOrgCompany) {
cloneCompany = company;
cloneOpen = true;
error = "";
success = "";
cloneDestId = "";
const byId = new Map<string, AdminOrgCompany>();
for (const c of companies) {
if (c.id !== company.id) byId.set(c.id, c);
}
try {
const demoRes = await listAdminCompanies({
limit: PAGE_SIZE,
offset: 0,
q: PLATFORM_DEMO_COMPANY_NAME
});
for (const c of demoRes.companies) {
if (c.id !== company.id) byId.set(c.id, c);
}
} catch {
/* keep page companies as fallback */
}
const me = authSession.me;
const home = me?.staff_home_company;
if (home?.id && home.id !== company.id && home.name) {
byId.set(home.id, { id: home.id, name: home.name });
}
for (const c of me?.companies ?? []) {
if (c.id && c.id !== company.id && c.name) {
byId.set(c.id, { id: c.id, name: c.name });
}
}
const opts = [...byId.values()].sort((a, b) => a.name.localeCompare(b.name));
cloneDestOptions = opts;
const demo =
opts.find((c) => c.name.trim().toLowerCase() === PLATFORM_DEMO_COMPANY_NAME.toLowerCase()) ??
null;
const homeId = (me?.staff_home_company_id || me?.staff_home_company?.id || "").trim();
cloneDestId =
demo?.id ||
(homeId && homeId !== company.id ? homeId : "") ||
opts[0]?.id ||
"";
}
function openSyncDialog(company: AdminOrgCompany) {
@@ -373,6 +412,54 @@
success = "";
}
async function confirmCloneCatalog() {
if (!cloneCompany) return;
const destId = cloneDestId.trim();
if (!destId || destId === cloneCompany.id) {
error = i18n.t("admin.users.cloneDestRequired");
return;
}
busy = true;
error = "";
success = "";
try {
const res = await cloneAdminCompanyCatalog(cloneCompany.id, {
destCompanyId: destId
});
const products = Number(res.counts?.raw_products ?? 0);
const categories = Number(res.counts?.categories ?? 0);
const mappedWith = Number(res.counts?.mapped_with_category ?? 0);
const activeDest = (res.active_company_id || res.dest_company_id || "").trim();
success = i18n.t("flash.admin.catalogCloned", {
source: cloneCompany.name,
dest: cloneDestLabel,
products: String(products),
categories: String(categories),
mapped_with: String(mappedWith)
});
cloneOpen = false;
cloneCompany = null;
cloneDestId = "";
cloneDestOptions = [];
if (activeDest) {
try {
await api("/api/auth/select-company", {
method: "POST",
body: { company_id: activeDest }
});
} catch {
/* clone handler may already have switched the session */
}
window.location.assign("/products");
return;
}
} catch (err) {
error = failureMessage(err, "Clone catalog failed");
} finally {
busy = false;
}
}
async function confirmSyncA1() {
if (!syncCompany) return;
busy = true;
@@ -409,56 +496,6 @@
}
}
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;
if (!destId && me?.company?.id && me.company.id === cloneCompany.id) {
error = i18n.t("flash.admin.cloneDestMissing");
return;
}
const res = await cloneAdminCompanyCatalog(cloneCompany.id, {
destCompanyId: destId
});
const products = Number(res.counts?.raw_products ?? 0);
const categories = Number(res.counts?.categories ?? 0);
const mappedWith = Number(res.counts?.mapped_with_category ?? 0);
const activeDest = (res.active_company_id || res.dest_company_id || "").trim();
success = i18n.t("flash.admin.catalogCloned", {
source: cloneCompany.name,
dest: cloneDestLabel,
products: String(products),
categories: String(categories),
mapped_with: String(mappedWith)
});
cloneOpen = false;
cloneCompany = null;
if (activeDest) {
try {
await api("/api/auth/select-company", {
method: "POST",
body: { company_id: activeDest }
});
} catch {
/* clone handler may already have switched the session */
}
window.location.assign("/products");
return;
}
} catch (err) {
error = failureMessage(err, "Clone catalog failed");
} finally {
busy = false;
}
}
async function sendSetPasswordEmails(userId?: string) {
busy = true;
error = "";
@@ -998,9 +1035,15 @@
{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>
<div class="space-y-2">
<Label for="clone-dest">{i18n.t("admin.users.cloneDestLabel")}</Label>
<Select id="clone-dest" bind:value={cloneDestId} data-testid="admin-clone-dest">
<option value="">{i18n.t("admin.users.cloneDestSelect")}</option>
{#each cloneDestOptions as dest}
<option value={dest.id}>{dest.name}</option>
{/each}
</Select>
</div>
<p class="text-xs text-muted-foreground">
{i18n.t("admin.users.cloneCatalogWarning")}
</p>
@@ -1012,11 +1055,18 @@
onclick={() => {
cloneOpen = false;
cloneCompany = null;
cloneDestId = "";
cloneDestOptions = [];
}}
>
{i18n.t("admin.users.cloneCatalogCancel")}
</Button>
<Button type="button" loading={busy} onclick={() => confirmCloneCatalog()}>
<Button
type="button"
loading={busy}
disabled={!cloneDestId || cloneDestId === cloneCompany?.id}
onclick={() => confirmCloneCatalog()}
>
{i18n.t("admin.users.cloneCatalogConfirm")}
</Button>
</div>