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
+4 -1
View File
@@ -27,6 +27,9 @@ export const ADMIN_SYNC_A1_PATH = (companyId: string) =>
export const ADMIN_STAFF_ROLE_PATH = (userId: string) => export const ADMIN_STAFF_ROLE_PATH = (userId: string) =>
`/api/admin/users/${encodeURIComponent(userId)}/staff-role`; `/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 const PAGE_SIZE = 25;
export type PlatformStaffRole = "admin" | "developer" | "support_staff"; export type PlatformStaffRole = "admin" | "developer" | "support_staff";
@@ -217,7 +220,7 @@ export type CloneCatalogResult = {
counts: Record<string, number>; counts: Record<string, number>;
}; };
/** 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( export async function cloneAdminCompanyCatalog(
sourceCompanyId: string, sourceCompanyId: string,
opts?: { destCompanyId?: string } opts?: { destCompanyId?: string }
+4 -2
View File
@@ -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. */ /** German (de) UI pack — keys must stay in sync with en.ts. */
export const de: MessageDict = { export const de: MessageDict = {
@@ -812,6 +812,9 @@ export const de: MessageDict = {
"admin.users.cloneCatalogCancel": "Abbrechen", "admin.users.cloneCatalogCancel": "Abbrechen",
"admin.users.cloneCatalogConfirm": "Katalog kopieren", "admin.users.cloneCatalogConfirm": "Katalog kopieren",
"admin.users.cloneDestFallback": "Ihr Sandbox-Unternehmen", "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.syncA1": "A1 synchronisieren",
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}", "admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
"admin.users.syncA1Title": "A1-Katalog synchronisieren", "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.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.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.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.planAssignedShort": "Plan zugewiesen.",
"flash.admin.creditsUpdated": "Credits aktualisiert.", "flash.admin.creditsUpdated": "Credits aktualisiert.",
"flash.admin.cyclesProcessed": "Abrechnungszyklen verarbeitet: {count}", "flash.admin.cyclesProcessed": "Abrechnungszyklen verarbeitet: {count}",
+4 -2
View File
@@ -839,6 +839,9 @@ export const en: MessageDict = {
"admin.users.cloneCatalogCancel": "Cancel", "admin.users.cloneCatalogCancel": "Cancel",
"admin.users.cloneCatalogConfirm": "Copy catalog", "admin.users.cloneCatalogConfirm": "Copy catalog",
"admin.users.cloneDestFallback": "your sandbox company", "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.syncA1": "Sync A1",
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}", "admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
"admin.users.syncA1Title": "Sync A1 catalog", "admin.users.syncA1Title": "Sync A1 catalog",
@@ -2333,10 +2336,9 @@ export const en: MessageDict = {
"flash.admin.staffRoleUpdated": "Staff role updated for {email}.", "flash.admin.staffRoleUpdated": "Staff role updated for {email}.",
"flash.admin.staffRoleUnavailable": "Staff role updates are not available on this server yet.", "flash.admin.staffRoleUnavailable": "Staff role updates are not available on this server yet.",
"flash.admin.planAssigned": "Plan assigned to {name}.", "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.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.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.planAssignedShort": "Plan assigned.",
"flash.admin.creditsUpdated": "Credits updated.", "flash.admin.creditsUpdated": "Credits updated.",
"flash.admin.cyclesProcessed": "Billing cycles processed: {count}", "flash.admin.cyclesProcessed": "Billing cycles processed: {count}",
+4 -2
View File
@@ -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. */ /** Spanish (es) UI pack — keys must stay in sync with en.ts. */
export const es: MessageDict = { export const es: MessageDict = {
@@ -812,6 +812,9 @@ export const es: MessageDict = {
"admin.users.cloneCatalogCancel": "Cancelar", "admin.users.cloneCatalogCancel": "Cancelar",
"admin.users.cloneCatalogConfirm": "Copiar catálogo", "admin.users.cloneCatalogConfirm": "Copiar catálogo",
"admin.users.cloneDestFallback": "tu empresa sandbox", "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.syncA1": "Sincronizar A1",
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}", "admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
"admin.users.syncA1Title": "Sincronizar catálogo A1", "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.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.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.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.planAssigned": "Plan asignado a {name}.",
"flash.admin.planAssignedShort": "Plan asignado.", "flash.admin.planAssignedShort": "Plan asignado.",
"flash.admin.creditsUpdated": "Créditos actualizados.", "flash.admin.creditsUpdated": "Créditos actualizados.",
+4 -2
View File
@@ -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. */ /** French (fr) UI pack — keys must stay in sync with en.ts. */
export const fr: MessageDict = { export const fr: MessageDict = {
@@ -812,6 +812,9 @@ export const fr: MessageDict = {
"admin.users.cloneCatalogCancel": "Annuler", "admin.users.cloneCatalogCancel": "Annuler",
"admin.users.cloneCatalogConfirm": "Copier le catalogue", "admin.users.cloneCatalogConfirm": "Copier le catalogue",
"admin.users.cloneDestFallback": "votre entreprise sandbox", "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.syncA1": "Synchroniser A1",
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}", "admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
"admin.users.syncA1Title": "Synchroniser le catalogue A1", "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.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.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.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.planAssigned": "Offre assignée à {name}.",
"flash.admin.planAssignedShort": "Offre assignée.", "flash.admin.planAssignedShort": "Offre assignée.",
"flash.admin.creditsUpdated": "Crédits mis à jour.", "flash.admin.creditsUpdated": "Crédits mis à jour.",
+4 -2
View File
@@ -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. */ /** Italian (it) UI pack — keys must stay in sync with en.ts. */
export const it: MessageDict = { export const it: MessageDict = {
@@ -812,6 +812,9 @@ export const it: MessageDict = {
"admin.users.cloneCatalogCancel": "Annulla", "admin.users.cloneCatalogCancel": "Annulla",
"admin.users.cloneCatalogConfirm": "Copia catalogo", "admin.users.cloneCatalogConfirm": "Copia catalogo",
"admin.users.cloneDestFallback": "la tua azienda sandbox", "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.syncA1": "Sincronizza A1",
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}", "admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
"admin.users.syncA1Title": "Sincronizza catalogo A1", "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.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.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.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.planAssigned": "Piano assegnato a {name}.",
"flash.admin.planAssignedShort": "Piano assegnato.", "flash.admin.planAssignedShort": "Piano assegnato.",
"flash.admin.creditsUpdated": "Crediti aggiornati.", "flash.admin.creditsUpdated": "Crediti aggiornati.",
+4 -2
View File
@@ -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. */ /** Japanese (ja) UI pack — keys must stay in sync with en.ts. */
export const ja: MessageDict = { export const ja: MessageDict = {
@@ -812,6 +812,9 @@ export const ja: MessageDict = {
"admin.users.cloneCatalogCancel": "キャンセル", "admin.users.cloneCatalogCancel": "キャンセル",
"admin.users.cloneCatalogConfirm": "カタログをコピー", "admin.users.cloneCatalogConfirm": "カタログをコピー",
"admin.users.cloneDestFallback": "サンドボックス会社", "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.syncA1": "A1を同期",
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}", "admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
"admin.users.syncA1Title": "A1カタログを同期", "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.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.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.syncA1Error": "{name} のA1同期に失敗しました。",
"flash.admin.cloneDestMissing": "No sandbox destination. Switch to Platform Demo (staff home), then clone again.",
"flash.admin.planAssigned": "{name} にプランを割り当てました。", "flash.admin.planAssigned": "{name} にプランを割り当てました。",
"flash.admin.planAssignedShort": "プランを割り当てました。", "flash.admin.planAssignedShort": "プランを割り当てました。",
"flash.admin.creditsUpdated": "クレジットを更新しました。", "flash.admin.creditsUpdated": "クレジットを更新しました。",
+4 -2
View File
@@ -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. */ /** Dutch (nl) UI pack — keys must stay in sync with en.ts. */
export const nl: MessageDict = { export const nl: MessageDict = {
@@ -812,6 +812,9 @@ export const nl: MessageDict = {
"admin.users.cloneCatalogCancel": "Annuleren", "admin.users.cloneCatalogCancel": "Annuleren",
"admin.users.cloneCatalogConfirm": "Catalogus kopiëren", "admin.users.cloneCatalogConfirm": "Catalogus kopiëren",
"admin.users.cloneDestFallback": "je sandboxbedrijf", "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.syncA1": "A1 synchroniseren",
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}", "admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
"admin.users.syncA1Title": "A1-catalogus synchroniseren", "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.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.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.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.planAssigned": "Plan toegewezen aan {name}.",
"flash.admin.planAssignedShort": "Plan toegewezen.", "flash.admin.planAssignedShort": "Plan toegewezen.",
"flash.admin.creditsUpdated": "Credits bijgewerkt.", "flash.admin.creditsUpdated": "Credits bijgewerkt.",
+4 -2
View File
@@ -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. */ /** Polish (pl) UI pack — keys must stay in sync with en.ts. */
export const pl: MessageDict = { export const pl: MessageDict = {
@@ -812,6 +812,9 @@ export const pl: MessageDict = {
"admin.users.cloneCatalogCancel": "Anuluj", "admin.users.cloneCatalogCancel": "Anuluj",
"admin.users.cloneCatalogConfirm": "Kopiuj katalog", "admin.users.cloneCatalogConfirm": "Kopiuj katalog",
"admin.users.cloneDestFallback": "twoja firma sandbox", "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.syncA1": "Synchronizuj A1",
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}", "admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
"admin.users.syncA1Title": "Synchronizuj katalog A1", "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.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.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.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.planAssigned": "Przypisano plan do {name}.",
"flash.admin.planAssignedShort": "Przypisano plan.", "flash.admin.planAssignedShort": "Przypisano plan.",
"flash.admin.creditsUpdated": "Zaktualizowano kredyty.", "flash.admin.creditsUpdated": "Zaktualizowano kredyty.",
+4 -2
View File
@@ -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. */ /** Portuguese (pt) UI pack — keys must stay in sync with en.ts. */
export const pt: MessageDict = { export const pt: MessageDict = {
@@ -812,6 +812,9 @@ export const pt: MessageDict = {
"admin.users.cloneCatalogCancel": "Cancelar", "admin.users.cloneCatalogCancel": "Cancelar",
"admin.users.cloneCatalogConfirm": "Copiar catálogo", "admin.users.cloneCatalogConfirm": "Copiar catálogo",
"admin.users.cloneDestFallback": "a sua empresa sandbox", "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.syncA1": "Sincronizar A1",
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}", "admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
"admin.users.syncA1Title": "Sincronizar catálogo A1", "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.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.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.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.planAssigned": "Plano atribuído a {name}.",
"flash.admin.planAssignedShort": "Plano atribuído.", "flash.admin.planAssignedShort": "Plano atribuído.",
"flash.admin.creditsUpdated": "Créditos atualizados.", "flash.admin.creditsUpdated": "Créditos atualizados.",
+15 -2
View File
@@ -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). */ /** Slovenian (sl) UI strings for admin Fix A1 — ready pack. Not registered in UI_LOCALES yet (avoid inventing locale switcher). */
export const sl: MessageDict = { 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.syncA1": "Sinhroniziraj A1",
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}", "admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
"admin.users.syncA1Title": "Sinhroniziraj katalog A1", "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.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.syncA1Cancel": "Prekliči",
"admin.users.syncA1Confirm": "Sinhroniziraj A1", "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.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.syncA1Error": "Sinhronizacija A1 za {name} ni uspela.",
"flash.admin.cloneDestMissing": "Ni ciljnega sandbox podjetja. Preklopite na Platform Demo, nato klonirajte.",
}; };
+113 -63
View File
@@ -8,6 +8,7 @@
import { formatCredits, formatDate } from "$lib/utils"; import { formatCredits, formatDate } from "$lib/utils";
import { import {
PAGE_SIZE, PAGE_SIZE,
PLATFORM_DEMO_COMPANY_NAME,
STAFF_ROLE_OPTIONS, STAFF_ROLE_OPTIONS,
assignAdminPlan, assignAdminPlan,
cloneAdminCompanyCatalog, cloneAdminCompanyCatalog,
@@ -93,23 +94,23 @@
let cloneOpen = $state(false); let cloneOpen = $state(false);
let cloneCompany = $state<AdminOrgCompany | null>(null); let cloneCompany = $state<AdminOrgCompany | null>(null);
let cloneDestId = $state("");
let cloneDestOptions = $state<AdminOrgCompany[]>([]);
let syncOpen = $state(false); let syncOpen = $state(false);
let syncCompany = $state<AdminOrgCompany | null>(null); 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 usersPage = $derived(Math.floor(usersOffset / PAGE_SIZE) + 1);
const usersPages = $derived(Math.max(1, Math.ceil(usersTotal / PAGE_SIZE))); const usersPages = $derived(Math.max(1, Math.ceil(usersTotal / PAGE_SIZE)));
const companiesPage = $derived(Math.floor(companiesOffset / PAGE_SIZE) + 1); const companiesPage = $derived(Math.floor(companiesOffset / PAGE_SIZE) + 1);
const companiesPages = $derived(Math.max(1, Math.ceil(companiesTotal / PAGE_SIZE))); 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 () => { onMount(async () => {
const gate = await requirePlatformAdmin(); const gate = await requirePlatformAdmin();
if (!gate.ok) { if (!gate.ok) {
@@ -359,11 +360,49 @@
} }
} }
function openCloneDialog(company: AdminOrgCompany) { async function openCloneDialog(company: AdminOrgCompany) {
cloneCompany = company; cloneCompany = company;
cloneOpen = true; cloneOpen = true;
error = ""; error = "";
success = ""; 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) { function openSyncDialog(company: AdminOrgCompany) {
@@ -373,6 +412,54 @@
success = ""; 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() { async function confirmSyncA1() {
if (!syncCompany) return; if (!syncCompany) return;
busy = true; 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) { async function sendSetPasswordEmails(userId?: string) {
busy = true; busy = true;
error = ""; error = "";
@@ -998,9 +1035,15 @@
{i18n.t("admin.users.cloneCatalogFrom", { name: cloneCompany.name })} {i18n.t("admin.users.cloneCatalogFrom", { name: cloneCompany.name })}
</p> </p>
{/if} {/if}
<p class="text-sm text-muted-foreground"> <div class="space-y-2">
{i18n.t("admin.users.cloneCatalogInto", { name: cloneDestLabel })} <Label for="clone-dest">{i18n.t("admin.users.cloneDestLabel")}</Label>
</p> <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"> <p class="text-xs text-muted-foreground">
{i18n.t("admin.users.cloneCatalogWarning")} {i18n.t("admin.users.cloneCatalogWarning")}
</p> </p>
@@ -1012,11 +1055,18 @@
onclick={() => { onclick={() => {
cloneOpen = false; cloneOpen = false;
cloneCompany = null; cloneCompany = null;
cloneDestId = "";
cloneDestOptions = [];
}} }}
> >
{i18n.t("admin.users.cloneCatalogCancel")} {i18n.t("admin.users.cloneCatalogCancel")}
</Button> </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")} {i18n.t("admin.users.cloneCatalogConfirm")}
</Button> </Button>
</div> </div>