diff --git a/apps/web/src/lib/admin-orgs.ts b/apps/web/src/lib/admin-orgs.ts
index 3423350..9d4fc3c 100644
--- a/apps/web/src/lib/admin-orgs.ts
+++ b/apps/web/src/lib/admin-orgs.ts
@@ -27,6 +27,9 @@ export const ADMIN_SYNC_A1_PATH = (companyId: string) =>
export const ADMIN_STAFF_ROLE_PATH = (userId: string) =>
`/api/admin/users/${encodeURIComponent(userId)}/staff-role`;
+/** Matches API `platformDemoCompanyName` (catalog/prompt_repair.go). */
+export const PLATFORM_DEMO_COMPANY_NAME = "Platform Demo";
+
export const PAGE_SIZE = 25;
export type PlatformStaffRole = "admin" | "developer" | "support_staff";
@@ -217,7 +220,7 @@ export type CloneCatalogResult = {
counts: Record;
};
-/** Copy source company catalog into the admin sandbox (home / dest). Source is unchanged. */
+/** Copy source company catalog into dest (explicit id preferred). Source is unchanged. */
export async function cloneAdminCompanyCatalog(
sourceCompanyId: string,
opts?: { destCompanyId?: string }
diff --git a/apps/web/src/lib/i18n/messages/de.ts b/apps/web/src/lib/i18n/messages/de.ts
index 1b2a451..b5233e5 100644
--- a/apps/web/src/lib/i18n/messages/de.ts
+++ b/apps/web/src/lib/i18n/messages/de.ts
@@ -1,4 +1,4 @@
-import type { MessageDict } from "./types";
+import type { MessageDict } from "./types";
/** German (de) UI pack — keys must stay in sync with en.ts. */
export const de: MessageDict = {
@@ -812,6 +812,9 @@ export const de: MessageDict = {
"admin.users.cloneCatalogCancel": "Abbrechen",
"admin.users.cloneCatalogConfirm": "Katalog kopieren",
"admin.users.cloneDestFallback": "Ihr Sandbox-Unternehmen",
+ "admin.users.cloneDestLabel": "Destination company",
+ "admin.users.cloneDestSelect": "Select destination",
+ "admin.users.cloneDestRequired": "Choose a destination company different from the source.",
"admin.users.syncA1": "A1 synchronisieren",
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
"admin.users.syncA1Title": "A1-Katalog synchronisieren",
@@ -2307,7 +2310,6 @@ export const de: MessageDict = {
"flash.admin.catalogCloned": "Copied {source} into {dest}: {products} products, {categories} taxonomy categories, {mapped_with} with mapped category.",
"flash.admin.syncA1Success": "Synced A1 for {name}: dump_mapped {dump_mapped} ({dump_status}), prompts {prompts}, hashes {hashes}, processed←mapped {categories}, mapped←processed {mapped_backfilled}. Coverage: {mapped_with}/{mapped_total} ({taxonomy} taxonomy).",
"flash.admin.syncA1Error": "A1-Sync für {name} fehlgeschlagen.",
- "flash.admin.cloneDestMissing": "No sandbox destination. Switch to Platform Demo (staff home), then clone again.",
"flash.admin.planAssignedShort": "Plan zugewiesen.",
"flash.admin.creditsUpdated": "Credits aktualisiert.",
"flash.admin.cyclesProcessed": "Abrechnungszyklen verarbeitet: {count}",
diff --git a/apps/web/src/lib/i18n/messages/en.ts b/apps/web/src/lib/i18n/messages/en.ts
index 610602d..6125ba9 100644
--- a/apps/web/src/lib/i18n/messages/en.ts
+++ b/apps/web/src/lib/i18n/messages/en.ts
@@ -839,6 +839,9 @@ export const en: MessageDict = {
"admin.users.cloneCatalogCancel": "Cancel",
"admin.users.cloneCatalogConfirm": "Copy catalog",
"admin.users.cloneDestFallback": "your sandbox company",
+ "admin.users.cloneDestLabel": "Destination company",
+ "admin.users.cloneDestSelect": "Select destination",
+ "admin.users.cloneDestRequired": "Choose a destination company different from the source.",
"admin.users.syncA1": "Sync A1",
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
"admin.users.syncA1Title": "Sync A1 catalog",
@@ -2333,10 +2336,9 @@ export const en: MessageDict = {
"flash.admin.staffRoleUpdated": "Staff role updated for {email}.",
"flash.admin.staffRoleUnavailable": "Staff role updates are not available on this server yet.",
"flash.admin.planAssigned": "Plan assigned to {name}.",
- "flash.admin.catalogCloned": "Copied {source} into {dest}: {products} products, {categories} taxonomy categories, {mapped_with} products with mapped category. Open Products — uncategorized SKUs need dump backfill or categorize, not another Fix.",
+ "flash.admin.catalogCloned": "Copied {source} into {dest}: {products} products, {categories} taxonomy categories, {mapped_with} products with mapped category. Open Products ΓÇö uncategorized SKUs need dump backfill or categorize, not another Fix.",
"flash.admin.syncA1Success": "Synced A1 for {name}: dump_mapped {dump_mapped} ({dump_status}), prompts {prompts}, hashes {hashes}, processed←mapped {categories}, mapped←processed {mapped_backfilled}. Coverage: {mapped_with}/{mapped_total} ({taxonomy} taxonomy).",
"flash.admin.syncA1Error": "Sync A1 failed for {name}.",
- "flash.admin.cloneDestMissing": "No sandbox destination. Switch active company to Platform Demo (or your staff home), then clone again. Clone refuses to overwrite A1.",
"flash.admin.planAssignedShort": "Plan assigned.",
"flash.admin.creditsUpdated": "Credits updated.",
"flash.admin.cyclesProcessed": "Billing cycles processed: {count}",
diff --git a/apps/web/src/lib/i18n/messages/es.ts b/apps/web/src/lib/i18n/messages/es.ts
index ef2dcc4..d70e91e 100644
--- a/apps/web/src/lib/i18n/messages/es.ts
+++ b/apps/web/src/lib/i18n/messages/es.ts
@@ -1,4 +1,4 @@
-import type { MessageDict } from "./types";
+import type { MessageDict } from "./types";
/** Spanish (es) UI pack — keys must stay in sync with en.ts. */
export const es: MessageDict = {
@@ -812,6 +812,9 @@ export const es: MessageDict = {
"admin.users.cloneCatalogCancel": "Cancelar",
"admin.users.cloneCatalogConfirm": "Copiar catálogo",
"admin.users.cloneDestFallback": "tu empresa sandbox",
+ "admin.users.cloneDestLabel": "Destination company",
+ "admin.users.cloneDestSelect": "Select destination",
+ "admin.users.cloneDestRequired": "Choose a destination company different from the source.",
"admin.users.syncA1": "Sincronizar A1",
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
"admin.users.syncA1Title": "Sincronizar catálogo A1",
@@ -2306,7 +2309,6 @@ export const es: MessageDict = {
"flash.admin.catalogCloned": "Copied {source} into {dest}: {products} products, {categories} taxonomy categories, {mapped_with} with mapped category.",
"flash.admin.syncA1Success": "Synced A1 for {name}: dump_mapped {dump_mapped} ({dump_status}), prompts {prompts}, hashes {hashes}, processed←mapped {categories}, mapped←processed {mapped_backfilled}. Coverage: {mapped_with}/{mapped_total} ({taxonomy} taxonomy).",
"flash.admin.syncA1Error": "Falló la sincronización A1 de {name}.",
- "flash.admin.cloneDestMissing": "No sandbox destination. Switch to Platform Demo (staff home), then clone again.",
"flash.admin.planAssigned": "Plan asignado a {name}.",
"flash.admin.planAssignedShort": "Plan asignado.",
"flash.admin.creditsUpdated": "Créditos actualizados.",
diff --git a/apps/web/src/lib/i18n/messages/fr.ts b/apps/web/src/lib/i18n/messages/fr.ts
index 6dd12af..9c0a10a 100644
--- a/apps/web/src/lib/i18n/messages/fr.ts
+++ b/apps/web/src/lib/i18n/messages/fr.ts
@@ -1,4 +1,4 @@
-import type { MessageDict } from "./types";
+import type { MessageDict } from "./types";
/** French (fr) UI pack — keys must stay in sync with en.ts. */
export const fr: MessageDict = {
@@ -812,6 +812,9 @@ export const fr: MessageDict = {
"admin.users.cloneCatalogCancel": "Annuler",
"admin.users.cloneCatalogConfirm": "Copier le catalogue",
"admin.users.cloneDestFallback": "votre entreprise sandbox",
+ "admin.users.cloneDestLabel": "Destination company",
+ "admin.users.cloneDestSelect": "Select destination",
+ "admin.users.cloneDestRequired": "Choose a destination company different from the source.",
"admin.users.syncA1": "Synchroniser A1",
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
"admin.users.syncA1Title": "Synchroniser le catalogue A1",
@@ -2306,7 +2309,6 @@ export const fr: MessageDict = {
"flash.admin.catalogCloned": "Copied {source} into {dest}: {products} products, {categories} taxonomy categories, {mapped_with} with mapped category.",
"flash.admin.syncA1Success": "Synced A1 for {name}: dump_mapped {dump_mapped} ({dump_status}), prompts {prompts}, hashes {hashes}, processed←mapped {categories}, mapped←processed {mapped_backfilled}. Coverage: {mapped_with}/{mapped_total} ({taxonomy} taxonomy).",
"flash.admin.syncA1Error": "Échec de la synchronisation A1 pour {name}.",
- "flash.admin.cloneDestMissing": "No sandbox destination. Switch to Platform Demo (staff home), then clone again.",
"flash.admin.planAssigned": "Offre assignée à {name}.",
"flash.admin.planAssignedShort": "Offre assignée.",
"flash.admin.creditsUpdated": "Crédits mis à jour.",
diff --git a/apps/web/src/lib/i18n/messages/it.ts b/apps/web/src/lib/i18n/messages/it.ts
index 0f84342..8fed8ce 100644
--- a/apps/web/src/lib/i18n/messages/it.ts
+++ b/apps/web/src/lib/i18n/messages/it.ts
@@ -1,4 +1,4 @@
-import type { MessageDict } from "./types";
+import type { MessageDict } from "./types";
/** Italian (it) UI pack — keys must stay in sync with en.ts. */
export const it: MessageDict = {
@@ -812,6 +812,9 @@ export const it: MessageDict = {
"admin.users.cloneCatalogCancel": "Annulla",
"admin.users.cloneCatalogConfirm": "Copia catalogo",
"admin.users.cloneDestFallback": "la tua azienda sandbox",
+ "admin.users.cloneDestLabel": "Destination company",
+ "admin.users.cloneDestSelect": "Select destination",
+ "admin.users.cloneDestRequired": "Choose a destination company different from the source.",
"admin.users.syncA1": "Sincronizza A1",
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
"admin.users.syncA1Title": "Sincronizza catalogo A1",
@@ -2306,7 +2309,6 @@ export const it: MessageDict = {
"flash.admin.catalogCloned": "Copied {source} into {dest}: {products} products, {categories} taxonomy categories, {mapped_with} with mapped category.",
"flash.admin.syncA1Success": "Synced A1 for {name}: dump_mapped {dump_mapped} ({dump_status}), prompts {prompts}, hashes {hashes}, processed←mapped {categories}, mapped←processed {mapped_backfilled}. Coverage: {mapped_with}/{mapped_total} ({taxonomy} taxonomy).",
"flash.admin.syncA1Error": "Sincronizzazione A1 per {name} non riuscita.",
- "flash.admin.cloneDestMissing": "No sandbox destination. Switch to Platform Demo (staff home), then clone again.",
"flash.admin.planAssigned": "Piano assegnato a {name}.",
"flash.admin.planAssignedShort": "Piano assegnato.",
"flash.admin.creditsUpdated": "Crediti aggiornati.",
diff --git a/apps/web/src/lib/i18n/messages/ja.ts b/apps/web/src/lib/i18n/messages/ja.ts
index cbd7ada..9bb9082 100644
--- a/apps/web/src/lib/i18n/messages/ja.ts
+++ b/apps/web/src/lib/i18n/messages/ja.ts
@@ -1,4 +1,4 @@
-import type { MessageDict } from "./types";
+import type { MessageDict } from "./types";
/** Japanese (ja) UI pack — keys must stay in sync with en.ts. */
export const ja: MessageDict = {
@@ -812,6 +812,9 @@ export const ja: MessageDict = {
"admin.users.cloneCatalogCancel": "ã‚ャンセル",
"admin.users.cloneCatalogConfirm": "ã‚«ã‚¿ãƒã‚°ã‚’コピー",
"admin.users.cloneDestFallback": "サンドボックス会社",
+ "admin.users.cloneDestLabel": "Destination company",
+ "admin.users.cloneDestSelect": "Select destination",
+ "admin.users.cloneDestRequired": "Choose a destination company different from the source.",
"admin.users.syncA1": "A1を同期",
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
"admin.users.syncA1Title": "A1カタログを同期",
@@ -2306,7 +2309,6 @@ export const ja: MessageDict = {
"flash.admin.catalogCloned": "Copied {source} into {dest}: {products} products, {categories} taxonomy categories, {mapped_with} with mapped category.",
"flash.admin.syncA1Success": "Synced A1 for {name}: dump_mapped {dump_mapped} ({dump_status}), prompts {prompts}, hashes {hashes}, processed←mapped {categories}, mapped←processed {mapped_backfilled}. Coverage: {mapped_with}/{mapped_total} ({taxonomy} taxonomy).",
"flash.admin.syncA1Error": "{name} のA1同期に失敗しました。",
- "flash.admin.cloneDestMissing": "No sandbox destination. Switch to Platform Demo (staff home), then clone again.",
"flash.admin.planAssigned": "{name} ã«ãƒ—ランを割り当ã¦ã¾ã—ãŸã€‚",
"flash.admin.planAssignedShort": "プランを割り当ã¦ã¾ã—ãŸã€‚",
"flash.admin.creditsUpdated": "クレジットを更新ã—ã¾ã—ãŸã€‚",
diff --git a/apps/web/src/lib/i18n/messages/nl.ts b/apps/web/src/lib/i18n/messages/nl.ts
index cbbda19..8767aed 100644
--- a/apps/web/src/lib/i18n/messages/nl.ts
+++ b/apps/web/src/lib/i18n/messages/nl.ts
@@ -1,4 +1,4 @@
-import type { MessageDict } from "./types";
+import type { MessageDict } from "./types";
/** Dutch (nl) UI pack — keys must stay in sync with en.ts. */
export const nl: MessageDict = {
@@ -812,6 +812,9 @@ export const nl: MessageDict = {
"admin.users.cloneCatalogCancel": "Annuleren",
"admin.users.cloneCatalogConfirm": "Catalogus kopiëren",
"admin.users.cloneDestFallback": "je sandboxbedrijf",
+ "admin.users.cloneDestLabel": "Destination company",
+ "admin.users.cloneDestSelect": "Select destination",
+ "admin.users.cloneDestRequired": "Choose a destination company different from the source.",
"admin.users.syncA1": "A1 synchroniseren",
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
"admin.users.syncA1Title": "A1-catalogus synchroniseren",
@@ -2306,7 +2309,6 @@ export const nl: MessageDict = {
"flash.admin.catalogCloned": "Copied {source} into {dest}: {products} products, {categories} taxonomy categories, {mapped_with} with mapped category.",
"flash.admin.syncA1Success": "Synced A1 for {name}: dump_mapped {dump_mapped} ({dump_status}), prompts {prompts}, hashes {hashes}, processed←mapped {categories}, mapped←processed {mapped_backfilled}. Coverage: {mapped_with}/{mapped_total} ({taxonomy} taxonomy).",
"flash.admin.syncA1Error": "A1-synchronisatie voor {name} mislukt.",
- "flash.admin.cloneDestMissing": "No sandbox destination. Switch to Platform Demo (staff home), then clone again.",
"flash.admin.planAssigned": "Plan toegewezen aan {name}.",
"flash.admin.planAssignedShort": "Plan toegewezen.",
"flash.admin.creditsUpdated": "Credits bijgewerkt.",
diff --git a/apps/web/src/lib/i18n/messages/pl.ts b/apps/web/src/lib/i18n/messages/pl.ts
index e7cc282..7a22c07 100644
--- a/apps/web/src/lib/i18n/messages/pl.ts
+++ b/apps/web/src/lib/i18n/messages/pl.ts
@@ -1,4 +1,4 @@
-import type { MessageDict } from "./types";
+import type { MessageDict } from "./types";
/** Polish (pl) UI pack — keys must stay in sync with en.ts. */
export const pl: MessageDict = {
@@ -812,6 +812,9 @@ export const pl: MessageDict = {
"admin.users.cloneCatalogCancel": "Anuluj",
"admin.users.cloneCatalogConfirm": "Kopiuj katalog",
"admin.users.cloneDestFallback": "twoja firma sandbox",
+ "admin.users.cloneDestLabel": "Destination company",
+ "admin.users.cloneDestSelect": "Select destination",
+ "admin.users.cloneDestRequired": "Choose a destination company different from the source.",
"admin.users.syncA1": "Synchronizuj A1",
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
"admin.users.syncA1Title": "Synchronizuj katalog A1",
@@ -2306,7 +2309,6 @@ export const pl: MessageDict = {
"flash.admin.catalogCloned": "Copied {source} into {dest}: {products} products, {categories} taxonomy categories, {mapped_with} with mapped category.",
"flash.admin.syncA1Success": "Synced A1 for {name}: dump_mapped {dump_mapped} ({dump_status}), prompts {prompts}, hashes {hashes}, processed←mapped {categories}, mapped←processed {mapped_backfilled}. Coverage: {mapped_with}/{mapped_total} ({taxonomy} taxonomy).",
"flash.admin.syncA1Error": "Synchronizacja A1 dla {name} nie powiodła się.",
- "flash.admin.cloneDestMissing": "No sandbox destination. Switch to Platform Demo (staff home), then clone again.",
"flash.admin.planAssigned": "Przypisano plan do {name}.",
"flash.admin.planAssignedShort": "Przypisano plan.",
"flash.admin.creditsUpdated": "Zaktualizowano kredyty.",
diff --git a/apps/web/src/lib/i18n/messages/pt.ts b/apps/web/src/lib/i18n/messages/pt.ts
index 0249a4f..56f3613 100644
--- a/apps/web/src/lib/i18n/messages/pt.ts
+++ b/apps/web/src/lib/i18n/messages/pt.ts
@@ -1,4 +1,4 @@
-import type { MessageDict } from "./types";
+import type { MessageDict } from "./types";
/** Portuguese (pt) UI pack — keys must stay in sync with en.ts. */
export const pt: MessageDict = {
@@ -812,6 +812,9 @@ export const pt: MessageDict = {
"admin.users.cloneCatalogCancel": "Cancelar",
"admin.users.cloneCatalogConfirm": "Copiar catálogo",
"admin.users.cloneDestFallback": "a sua empresa sandbox",
+ "admin.users.cloneDestLabel": "Destination company",
+ "admin.users.cloneDestSelect": "Select destination",
+ "admin.users.cloneDestRequired": "Choose a destination company different from the source.",
"admin.users.syncA1": "Sincronizar A1",
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
"admin.users.syncA1Title": "Sincronizar catálogo A1",
@@ -2306,7 +2309,6 @@ export const pt: MessageDict = {
"flash.admin.catalogCloned": "Copied {source} into {dest}: {products} products, {categories} taxonomy categories, {mapped_with} with mapped category.",
"flash.admin.syncA1Success": "Synced A1 for {name}: dump_mapped {dump_mapped} ({dump_status}), prompts {prompts}, hashes {hashes}, processed←mapped {categories}, mapped←processed {mapped_backfilled}. Coverage: {mapped_with}/{mapped_total} ({taxonomy} taxonomy).",
"flash.admin.syncA1Error": "Falha na sincronização A1 de {name}.",
- "flash.admin.cloneDestMissing": "No sandbox destination. Switch to Platform Demo (staff home), then clone again.",
"flash.admin.planAssigned": "Plano atribuÃdo a {name}.",
"flash.admin.planAssignedShort": "Plano atribuÃdo.",
"flash.admin.creditsUpdated": "Créditos atualizados.",
diff --git a/apps/web/src/lib/i18n/messages/sl.ts b/apps/web/src/lib/i18n/messages/sl.ts
index 28c6cab..5186b50 100644
--- a/apps/web/src/lib/i18n/messages/sl.ts
+++ b/apps/web/src/lib/i18n/messages/sl.ts
@@ -1,7 +1,20 @@
-import type { MessageDict } from "./types.ts";
+import type { MessageDict } from "./types.ts";
/** Slovenian (sl) UI strings for admin Fix A1 — ready pack. Not registered in UI_LOCALES yet (avoid inventing locale switcher). */
export const sl: MessageDict = {
+ "admin.users.cloneCatalog": "Copy to my company",
+ "admin.users.cloneCatalogAria": "Copy {name} catalog into your sandbox company",
+ "admin.users.cloneCatalogTitle": "Copy company catalog",
+ "admin.users.cloneCatalogDesc": "Stay on your admin account. Source data is not changed.",
+ "admin.users.cloneCatalogFrom": "Source: {name}",
+ "admin.users.cloneCatalogInto": "Destination: {name}",
+ "admin.users.cloneCatalogWarning": "Replaces categories, feeds, products, attributes, mappings, and brand on the destination. Plan, billing, members, API keys, and store connectors are not copied.",
+ "admin.users.cloneCatalogCancel": "Cancel",
+ "admin.users.cloneCatalogConfirm": "Copy catalog",
+ "admin.users.cloneDestFallback": "your sandbox company",
+ "admin.users.cloneDestLabel": "Destination company",
+ "admin.users.cloneDestSelect": "Select destination",
+ "admin.users.cloneDestRequired": "Choose a destination company different from the source.",
"admin.users.syncA1": "Sinhroniziraj A1",
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
"admin.users.syncA1Title": "Sinhroniziraj katalog A1",
@@ -10,7 +23,7 @@ export const sl: MessageDict = {
"admin.users.syncA1Warning": "Requires descrybe_new.sql on the API server (scripts/seed/ or SEED_A1_MYSQL_DUMP) for dump category coverage. Without a dump, only DB hygiene runs. No mass reprocess.",
"admin.users.syncA1Cancel": "Prekliči",
"admin.users.syncA1Confirm": "Sinhroniziraj A1",
+ "flash.admin.catalogCloned": "Copied {source} into {dest}: {products} products, {categories} taxonomy categories, {mapped_with} with mapped category.",
"flash.admin.syncA1Success": "Synced A1 for {name}: dump_mapped {dump_mapped} ({dump_status}), prompts {prompts}, hashes {hashes}, processed←mapped {categories}, mapped←processed {mapped_backfilled}. Coverage: {mapped_with}/{mapped_total} ({taxonomy} taxonomy).",
"flash.admin.syncA1Error": "Sinhronizacija A1 za {name} ni uspela.",
- "flash.admin.cloneDestMissing": "Ni ciljnega sandbox podjetja. Preklopite na Platform Demo, nato klonirajte.",
};
diff --git a/apps/web/src/routes/admin/users/+page.svelte b/apps/web/src/routes/admin/users/+page.svelte
index d2ae85f..66f4de8 100644
--- a/apps/web/src/routes/admin/users/+page.svelte
+++ b/apps/web/src/routes/admin/users/+page.svelte
@@ -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(null);
+ let cloneDestId = $state("");
+ let cloneDestOptions = $state([]);
let syncOpen = $state(false);
let syncCompany = $state(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();
+ 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 })}
{/if}
-
- {i18n.t("admin.users.cloneCatalogInto", { name: cloneDestLabel })}
-
+
+
+
+
{i18n.t("admin.users.cloneCatalogWarning")}
@@ -1012,11 +1055,18 @@
onclick={() => {
cloneOpen = false;
cloneCompany = null;
+ cloneDestId = "";
+ cloneDestOptions = [];
}}
>
{i18n.t("admin.users.cloneCatalogCancel")}
-