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")} -