fixes
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
/**
|
||||
* Admin orgs UI client — users + companies directory, staff roles, plan assign.
|
||||
* Admin orgs UI client — users + companies directory, staff roles, plan assign,
|
||||
* clone-catalog, and Fix A1 (`POST …/fix-catalog`).
|
||||
* Flash Fix A1: result.prompts / hashes / categories → flash.admin.fixA1Success.
|
||||
* Contract: docs/admin-roles-support/04-contract.md · Docs: 10-admin-orgs-ui.md
|
||||
*/
|
||||
import { api, ApiError } from "$lib/api";
|
||||
@@ -18,6 +20,8 @@ export const ADMIN_USERS_PATH = "/api/admin/users";
|
||||
export const ADMIN_COMPANIES_PATH = "/api/admin/companies";
|
||||
export const ADMIN_CLONE_CATALOG_PATH = (companyId: string) =>
|
||||
`${ADMIN_COMPANIES_PATH}/${encodeURIComponent(companyId)}/clone-catalog`;
|
||||
export const ADMIN_FIX_CATALOG_PATH = (companyId: string) =>
|
||||
`${ADMIN_COMPANIES_PATH}/${encodeURIComponent(companyId)}/fix-catalog`;
|
||||
export const ADMIN_STAFF_ROLE_PATH = (userId: string) =>
|
||||
`/api/admin/users/${encodeURIComponent(userId)}/staff-role`;
|
||||
|
||||
@@ -226,6 +230,56 @@ export async function cloneAdminCompanyCatalog(
|
||||
});
|
||||
}
|
||||
|
||||
export type FixCatalogResult = {
|
||||
company_id: string;
|
||||
company_name: string;
|
||||
a1_cohort?: boolean;
|
||||
category_attribute_orphans_removed?: number;
|
||||
category_attribute_links?: number;
|
||||
/** Alias of category_prompts_updated for flash.admin.fixA1Success {prompts}. */
|
||||
prompts?: number;
|
||||
category_prompts_updated?: number;
|
||||
product_enhance_languages?: number;
|
||||
/** Alias of weak_hashes_cleared for flash {hashes}. */
|
||||
hashes?: number;
|
||||
weak_hashes_cleared?: number;
|
||||
/** Alias of categories_backfilled for flash {categories}. */
|
||||
categories?: number;
|
||||
categories_backfilled?: number;
|
||||
attributes_sanitized?: number;
|
||||
products_scanned?: number;
|
||||
reprocess_needed_count?: number;
|
||||
reprocess_sample_raw_product_ids?: string[];
|
||||
};
|
||||
|
||||
export type FixCatalogResponse = {
|
||||
status: string;
|
||||
result: FixCatalogResult;
|
||||
note?: string;
|
||||
};
|
||||
|
||||
/** In-place Fix A1 / catalog hygiene. Never clears catalog; no mass reprocess. */
|
||||
export async function fixAdminCompanyCatalog(
|
||||
companyId: string,
|
||||
opts?: { backfillCategories?: boolean; reprocessSampleLimit?: number }
|
||||
): Promise<FixCatalogResponse> {
|
||||
const body: {
|
||||
confirm: true;
|
||||
backfill_categories?: boolean;
|
||||
reprocess_sample_limit?: number;
|
||||
} = { confirm: true };
|
||||
if (opts?.backfillCategories === false) {
|
||||
body.backfill_categories = false;
|
||||
}
|
||||
if (typeof opts?.reprocessSampleLimit === "number") {
|
||||
body.reprocess_sample_limit = opts.reprocessSampleLimit;
|
||||
}
|
||||
return api<FixCatalogResponse>(ADMIN_FIX_CATALOG_PATH(companyId), {
|
||||
method: "POST",
|
||||
body
|
||||
});
|
||||
}
|
||||
|
||||
export function isStaffRoleApiUnavailable(err: unknown): boolean {
|
||||
return err instanceof ApiError && (err.status === 404 || err.status === 501);
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
productFeedSyncLabel,
|
||||
productHasEprel,
|
||||
productHasSpecs,
|
||||
productImageUrls,
|
||||
resolveOriginalDescription,
|
||||
resolveOriginalName
|
||||
} from "./types";
|
||||
@@ -438,6 +439,7 @@
|
||||
const coverageCount = $derived(
|
||||
enrichment ? Object.values(enrichment).filter((s) => s !== "missing").length : 0
|
||||
);
|
||||
const imageUrls = $derived.by(() => productImageUrls(product));
|
||||
|
||||
function attrDisplayValue(key: string, value: string): string {
|
||||
if (key.trim().toLowerCase() !== "category") return value;
|
||||
@@ -571,8 +573,9 @@
|
||||
primary={contentLanguages[0] || DEFAULT_CONTENT_LANGUAGE}
|
||||
allowAdd={false}
|
||||
hasOverride={(code) => {
|
||||
const f = fieldsForLang(code);
|
||||
return Boolean(f.processedName.trim() || f.processedDescription.trim());
|
||||
const primary = contentLanguages[0] || DEFAULT_CONTENT_LANGUAGE;
|
||||
if (code === primary) return false;
|
||||
return Boolean(product?.localized_content?.[code]);
|
||||
}}
|
||||
onChange={(code) => applyContentLang(code)}
|
||||
/>
|
||||
@@ -890,8 +893,36 @@
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</TabsContent>
|
||||
</section>
|
||||
|
||||
{#if imageUrls.length > 0}
|
||||
<section class="space-y-3" data-testid="product-images">
|
||||
<h3 class="text-sm font-medium">{i18n.t("products.edit.images")}</h3>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
{#each imageUrls as src, i}
|
||||
<a
|
||||
href={src}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="block overflow-hidden rounded-md border border-border bg-muted/30"
|
||||
title={src}
|
||||
>
|
||||
<img
|
||||
src={src}
|
||||
alt={i18n.t("products.edit.imageAlt", {
|
||||
index: i + 1,
|
||||
name: product?.processed_name || product?.name || productId
|
||||
})}
|
||||
class="h-28 w-28 object-contain"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
/>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="feed" class="mt-4 space-y-4">
|
||||
<section class="rounded-md border border-border px-3 py-3">
|
||||
@@ -957,14 +988,28 @@
|
||||
>{field.value}</pre>
|
||||
{/if}
|
||||
{:else if /^https?:\/\//i.test(field.value)}
|
||||
<a
|
||||
href={field.value}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="break-all text-primary underline-offset-2 hover:underline"
|
||||
>
|
||||
{field.value}
|
||||
</a>
|
||||
{@const isImage =
|
||||
/image|img|photo|picture|thumbnail/i.test(field.key) ||
|
||||
/\.(jpe?g|png|gif|webp|svg|avif)(\?|$)/i.test(field.value)}
|
||||
<div class="space-y-2">
|
||||
{#if isImage}
|
||||
<img
|
||||
src={field.value}
|
||||
alt={field.key}
|
||||
class="max-h-40 max-w-full rounded border border-border object-contain"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
/>
|
||||
{/if}
|
||||
<a
|
||||
href={field.value}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="break-all text-primary underline-offset-2 hover:underline"
|
||||
>
|
||||
{field.value}
|
||||
</a>
|
||||
</div>
|
||||
{:else}
|
||||
<span class="break-words {field.ok ? '' : 'text-chart-amber'}">{field.value}</span>
|
||||
{/if}
|
||||
|
||||
@@ -847,6 +847,39 @@ function digAttrBag(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Collect http(s) image URLs from mapped_data (main_image + more_images). */
|
||||
export function productImageUrls(product: ProductRow | null | undefined): string[] {
|
||||
const mapped = asRecord(product?.mapped_data);
|
||||
if (!mapped) return [];
|
||||
const out: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
const push = (raw: unknown) => {
|
||||
if (typeof raw === "string") {
|
||||
const url = raw.trim();
|
||||
if (!/^https?:\/\//i.test(url) || seen.has(url)) return;
|
||||
seen.add(url);
|
||||
out.push(url);
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(raw)) {
|
||||
for (const item of raw) push(item);
|
||||
return;
|
||||
}
|
||||
const rec = asRecord(raw);
|
||||
if (!rec) return;
|
||||
for (const key of ["url", "src", "href", "link"]) {
|
||||
if (rec[key] != null) push(rec[key]);
|
||||
}
|
||||
};
|
||||
for (const key of ["main_image", "mainImage", "image", "image_url", "imageUrl"]) {
|
||||
if (mapped[key] != null) push(mapped[key]);
|
||||
}
|
||||
for (const key of ["more_images", "moreImages", "images", "additional_images"]) {
|
||||
if (mapped[key] != null) push(mapped[key]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** True when product carries an EPREL id or enriched energy-label payload. */
|
||||
export function productHasEprel(product: ProductRow | null | undefined): boolean {
|
||||
if (!product) return false;
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Focused check: admin Fix A1 i18n keys exist in every UI_LOCALES pack.
|
||||
*/
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
|
||||
import { UI_LOCALES } from "./locales.ts";
|
||||
import { en } from "./messages/en.ts";
|
||||
import { loadAllMessages, messagesFor } from "./messages/catalog.ts";
|
||||
import { sl } from "./messages/sl.ts";
|
||||
|
||||
const FIX_A1_KEYS = [
|
||||
"admin.users.fixA1",
|
||||
"admin.users.fixA1Aria",
|
||||
"admin.users.fixA1Title",
|
||||
"admin.users.fixA1Desc",
|
||||
"admin.users.fixA1Company",
|
||||
"admin.users.fixA1Warning",
|
||||
"admin.users.fixA1Cancel",
|
||||
"admin.users.fixA1Confirm",
|
||||
"flash.admin.fixA1Success",
|
||||
"flash.admin.fixA1Error"
|
||||
] as const;
|
||||
|
||||
describe("admin Fix A1 i18n keys", () => {
|
||||
it("English defines every Fix A1 key", () => {
|
||||
for (const key of FIX_A1_KEYS) {
|
||||
assert.equal(typeof en[key], "string", key);
|
||||
assert.ok(en[key].trim().length > 0, key);
|
||||
}
|
||||
});
|
||||
|
||||
it("every registered UI locale has Fix A1 keys", async () => {
|
||||
await loadAllMessages();
|
||||
for (const { code } of UI_LOCALES) {
|
||||
const pack = code === "en" ? en : messagesFor(code);
|
||||
for (const key of FIX_A1_KEYS) {
|
||||
const value = pack[key];
|
||||
assert.equal(typeof value, "string", `${code}:${key}`);
|
||||
assert.ok(String(value).trim().length > 0, `${code}:${key}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("Slovenian ready pack has Fix A1 keys (not in UI_LOCALES)", () => {
|
||||
for (const key of FIX_A1_KEYS) {
|
||||
assert.equal(typeof sl[key], "string", key);
|
||||
assert.ok(sl[key].trim().length > 0, key);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -812,6 +812,14 @@ export const de: MessageDict = {
|
||||
"admin.users.cloneCatalogCancel": "Abbrechen",
|
||||
"admin.users.cloneCatalogConfirm": "Katalog kopieren",
|
||||
"admin.users.cloneDestFallback": "Ihr Sandbox-Unternehmen",
|
||||
"admin.users.fixA1": "A1-Katalog reparieren",
|
||||
"admin.users.fixA1Aria": "KI-Prompts, Kategorien und Enhance-Hashes für {name} reparieren",
|
||||
"admin.users.fixA1Title": "Unternehmenskatalog reparieren",
|
||||
"admin.users.fixA1Desc": "Wendet korrigierte KI-Prompts erneut an, löst schwache Enhance-Hashes und füllt Kategorien aus mapped_data nach.",
|
||||
"admin.users.fixA1Company": "Unternehmen: {name}",
|
||||
"admin.users.fixA1Warning": "Bevorzugen Sie Platform Demo oder ein explizites Unternehmen. Löscht keine Feeds, Mappings oder Rohprodukte. Schützt A1-Kohorten-Überschreibregeln.",
|
||||
"admin.users.fixA1Cancel": "Abbrechen",
|
||||
"admin.users.fixA1Confirm": "Katalog reparieren",
|
||||
"admin.users.noUsers": "Keine Benutzer entsprechen diesem Filter.",
|
||||
"admin.users.noCompanies": "Keine Unternehmen entsprechen diesem Filter.",
|
||||
"admin.users.assignRoleTitle": "Mitarbeiterrolle zuweisen",
|
||||
@@ -2297,6 +2305,8 @@ export const de: MessageDict = {
|
||||
"flash.admin.staffRoleUnavailable": "Mitarbeiterrollen-Updates sind auf diesem Server noch nicht verfügbar.",
|
||||
"flash.admin.planAssigned": "Plan {name} zugewiesen.",
|
||||
"flash.admin.catalogCloned": "{source} nach {dest} kopiert: {products} Produkte, {categories} Kategorien.",
|
||||
"flash.admin.fixA1Success": "Katalogreparatur für {name} abgeschlossen: {prompts} Prompts aktualisiert, {hashes} Hashes gelöscht, {categories} Kategorien nachgefüllt.",
|
||||
"flash.admin.fixA1Error": "Katalogreparatur für {name} fehlgeschlagen.",
|
||||
"flash.admin.planAssignedShort": "Plan zugewiesen.",
|
||||
"flash.admin.creditsUpdated": "Credits aktualisiert.",
|
||||
"flash.admin.cyclesProcessed": "Abrechnungszyklen verarbeitet: {count}",
|
||||
|
||||
@@ -355,6 +355,10 @@ export const en: MessageDict = {
|
||||
"dashboard.lowCreditsTitle": "Credits running low",
|
||||
"dashboard.lowCreditsMessage": "{remaining} of {total} credits left. Top up or upgrade before jobs stall.",
|
||||
"dashboard.latestJobs": "Latest processing jobs",
|
||||
"dashboard.jobsRunning": "{count} job running",
|
||||
"dashboard.jobsRunningPlural": "{count} jobs running",
|
||||
"dashboard.activeJobs": "{count} active job",
|
||||
"dashboard.activeJobsPlural": "{count} active jobs",
|
||||
"dashboard.noJobsEmpty": "No jobs yet — import products first.",
|
||||
"dashboard.noJobsReady": "No recent jobs. Start one from Products when you are ready.",
|
||||
"dashboard.startJob": "Start a job",
|
||||
@@ -835,6 +839,14 @@ export const en: MessageDict = {
|
||||
"admin.users.cloneCatalogCancel": "Cancel",
|
||||
"admin.users.cloneCatalogConfirm": "Copy catalog",
|
||||
"admin.users.cloneDestFallback": "your sandbox company",
|
||||
"admin.users.fixA1": "Fix A1 catalog",
|
||||
"admin.users.fixA1Aria": "Repair AI prompts, categories, and enhance hashes for {name}",
|
||||
"admin.users.fixA1Title": "Fix company catalog",
|
||||
"admin.users.fixA1Desc": "In-place repair: category attribute links, category enhance prompts (with attrs), weak enhance hashes, optional category backfill, attribute sanitize. No mass reprocess.",
|
||||
"admin.users.fixA1Company": "Company: {name}",
|
||||
"admin.users.fixA1Warning": "Prefer Platform Demo; A1 may be targeted in place with confirm. Never clears the catalog or uses A1 as a clone destination.",
|
||||
"admin.users.fixA1Cancel": "Cancel",
|
||||
"admin.users.fixA1Confirm": "Fix catalog",
|
||||
"admin.users.noUsers": "No users match this filter.",
|
||||
"admin.users.noCompanies": "No companies match this filter.",
|
||||
"admin.users.assignRoleTitle": "Assign staff role",
|
||||
@@ -2322,6 +2334,8 @@ export const en: MessageDict = {
|
||||
"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} categories. Open Products and process catalog GTINs (EAN-only API calls need a matching feed product).",
|
||||
"flash.admin.fixA1Success": "Catalog repair finished for {name}: {prompts} category prompts updated, {hashes} hashes cleared, {categories} categories backfilled.",
|
||||
"flash.admin.fixA1Error": "Catalog repair failed for {name}.",
|
||||
"flash.admin.planAssignedShort": "Plan assigned.",
|
||||
"flash.admin.creditsUpdated": "Credits updated.",
|
||||
"flash.admin.cyclesProcessed": "Billing cycles processed: {count}",
|
||||
@@ -4238,6 +4252,8 @@ export const en: MessageDict = {
|
||||
"products.edit.updatedRelative": "· updated {when}",
|
||||
"products.edit.viewAllFeedFields": "View all feed fields ({filled}/{total})",
|
||||
"products.edit.productIdentity": "Product identity",
|
||||
"products.edit.images": "Images",
|
||||
"products.edit.imageAlt": "Product image {index}: {name}",
|
||||
"products.edit.aiProcessedName": "AI-processed name",
|
||||
"products.edit.originalName": "Original name",
|
||||
"products.edit.productIdSku": "Product ID / SKU",
|
||||
|
||||
@@ -812,6 +812,14 @@ export const es: MessageDict = {
|
||||
"admin.users.cloneCatalogCancel": "Cancelar",
|
||||
"admin.users.cloneCatalogConfirm": "Copiar catálogo",
|
||||
"admin.users.cloneDestFallback": "tu empresa sandbox",
|
||||
"admin.users.fixA1": "Reparar catálogo A1",
|
||||
"admin.users.fixA1Aria": "Reparar prompts de IA, categorías y hashes de enhance para {name}",
|
||||
"admin.users.fixA1Title": "Reparar catálogo de la empresa",
|
||||
"admin.users.fixA1Desc": "Vuelve a aplicar prompts de IA corregidos, limpia hashes de enhance débiles y completa categorías desde mapped_data.",
|
||||
"admin.users.fixA1Company": "Empresa: {name}",
|
||||
"admin.users.fixA1Warning": "Prefiera Platform Demo o una empresa explícita. No elimina feeds, mapeos ni productos en bruto. Protege las reglas de sobrescritura de la cohorte A1.",
|
||||
"admin.users.fixA1Cancel": "Cancelar",
|
||||
"admin.users.fixA1Confirm": "Reparar catálogo",
|
||||
"admin.users.noUsers": "Ningún usuario coincide con este filtro.",
|
||||
"admin.users.noCompanies": "Ninguna empresa coincide con este filtro.",
|
||||
"admin.users.assignRoleTitle": "Asignar rol de personal",
|
||||
@@ -2296,6 +2304,8 @@ export const es: MessageDict = {
|
||||
"flash.admin.staffRoleUpdated": "Rol de personal actualizado para {email}.",
|
||||
"flash.admin.staffRoleUnavailable": "Las actualizaciones de rol de personal aún no están disponibles en este servidor.",
|
||||
"flash.admin.catalogCloned": "Se copió {source} en {dest}: {products} productos, {categories} categorías.",
|
||||
"flash.admin.fixA1Success": "Reparación del catálogo de {name} finalizada: {prompts} prompts actualizados, {hashes} hashes borrados, {categories} categorías rellenadas.",
|
||||
"flash.admin.fixA1Error": "Falló la reparación del catálogo de {name}.",
|
||||
"flash.admin.planAssigned": "Plan asignado a {name}.",
|
||||
"flash.admin.planAssignedShort": "Plan asignado.",
|
||||
"flash.admin.creditsUpdated": "Créditos actualizados.",
|
||||
|
||||
@@ -812,6 +812,14 @@ export const fr: MessageDict = {
|
||||
"admin.users.cloneCatalogCancel": "Annuler",
|
||||
"admin.users.cloneCatalogConfirm": "Copier le catalogue",
|
||||
"admin.users.cloneDestFallback": "votre entreprise sandbox",
|
||||
"admin.users.fixA1": "Réparer le catalogue A1",
|
||||
"admin.users.fixA1Aria": "Réparer les prompts IA, catégories et hashes enhance pour {name}",
|
||||
"admin.users.fixA1Title": "Réparer le catalogue entreprise",
|
||||
"admin.users.fixA1Desc": "Réapplique les prompts IA corrigés, efface les hashes enhance faibles et complète les catégories depuis mapped_data.",
|
||||
"admin.users.fixA1Company": "Entreprise : {name}",
|
||||
"admin.users.fixA1Warning": "Préférer Platform Demo ou une entreprise explicite. Ne supprime ni feeds, ni mappings, ni produits bruts. Protège les règles d'écrasement de la cohorte A1.",
|
||||
"admin.users.fixA1Cancel": "Annuler",
|
||||
"admin.users.fixA1Confirm": "Réparer le catalogue",
|
||||
"admin.users.noUsers": "Aucun utilisateur ne correspond à ce filtre.",
|
||||
"admin.users.noCompanies": "Aucune entreprise ne correspond à ce filtre.",
|
||||
"admin.users.assignRoleTitle": "Assigner un rôle du personnel",
|
||||
@@ -2296,6 +2304,8 @@ export const fr: MessageDict = {
|
||||
"flash.admin.staffRoleUpdated": "Rôle du personnel mis à jour pour {email}.",
|
||||
"flash.admin.staffRoleUnavailable": "Les mises à jour de rôle personnel ne sont pas encore disponibles sur ce serveur.",
|
||||
"flash.admin.catalogCloned": "{source} copié vers {dest} : {products} produits, {categories} catégories.",
|
||||
"flash.admin.fixA1Success": "Réparation du catalogue pour {name} terminée : {prompts} prompts mis à jour, {hashes} hashes effacés, {categories} catégories complétées.",
|
||||
"flash.admin.fixA1Error": "Échec de la réparation du catalogue pour {name}.",
|
||||
"flash.admin.planAssigned": "Offre assignée à {name}.",
|
||||
"flash.admin.planAssignedShort": "Offre assignée.",
|
||||
"flash.admin.creditsUpdated": "Crédits mis à jour.",
|
||||
|
||||
@@ -812,6 +812,14 @@ export const it: MessageDict = {
|
||||
"admin.users.cloneCatalogCancel": "Annulla",
|
||||
"admin.users.cloneCatalogConfirm": "Copia catalogo",
|
||||
"admin.users.cloneDestFallback": "la tua azienda sandbox",
|
||||
"admin.users.fixA1": "Ripara catalogo A1",
|
||||
"admin.users.fixA1Aria": "Ripara prompt IA, categorie e hash enhance per {name}",
|
||||
"admin.users.fixA1Title": "Ripara catalogo azienda",
|
||||
"admin.users.fixA1Desc": "Riapplica i prompt IA corretti, cancella hash enhance deboli e completa le categorie da mapped_data.",
|
||||
"admin.users.fixA1Company": "Azienda: {name}",
|
||||
"admin.users.fixA1Warning": "Preferisci Platform Demo o un'azienda esplicita. Non elimina feed, mapping o prodotti grezzi. Protegge le regole di sovrascrittura della coorte A1.",
|
||||
"admin.users.fixA1Cancel": "Annulla",
|
||||
"admin.users.fixA1Confirm": "Ripara catalogo",
|
||||
"admin.users.noUsers": "Nessun utente corrisponde a questo filtro.",
|
||||
"admin.users.noCompanies": "Nessuna azienda corrisponde a questo filtro.",
|
||||
"admin.users.assignRoleTitle": "Assegna ruolo staff",
|
||||
@@ -2296,6 +2304,8 @@ export const it: MessageDict = {
|
||||
"flash.admin.staffRoleUpdated": "Ruolo staff aggiornato per {email}.",
|
||||
"flash.admin.staffRoleUnavailable": "Gli aggiornamenti del ruolo staff non sono ancora disponibili su questo server.",
|
||||
"flash.admin.catalogCloned": "Copiato {source} in {dest}: {products} prodotti, {categories} categorie.",
|
||||
"flash.admin.fixA1Success": "Riparazione catalogo per {name} completata: {prompts} prompt aggiornati, {hashes} hash cancellati, {categories} categorie integrate.",
|
||||
"flash.admin.fixA1Error": "Riparazione catalogo per {name} non riuscita.",
|
||||
"flash.admin.planAssigned": "Piano assegnato a {name}.",
|
||||
"flash.admin.planAssignedShort": "Piano assegnato.",
|
||||
"flash.admin.creditsUpdated": "Crediti aggiornati.",
|
||||
|
||||
@@ -812,6 +812,14 @@ export const ja: MessageDict = {
|
||||
"admin.users.cloneCatalogCancel": "キャンセル",
|
||||
"admin.users.cloneCatalogConfirm": "カタログをコピー",
|
||||
"admin.users.cloneDestFallback": "サンドボックス会社",
|
||||
"admin.users.fixA1": "A1カタログを修復",
|
||||
"admin.users.fixA1Aria": "{name} のAIプロンプト、カテゴリ、enhanceハッシュを修復",
|
||||
"admin.users.fixA1Title": "会社カタログを修復",
|
||||
"admin.users.fixA1Desc": "修正済みAIプロンプトを再適用し、弱いenhanceハッシュを消去し、mapped_dataからカテゴリを補完します。",
|
||||
"admin.users.fixA1Company": "会社: {name}",
|
||||
"admin.users.fixA1Warning": "Platform Demoまたは明示的な会社を優先してください。フィード、マッピング、生製品は削除しません。A1コホートの上書きルールを保護します。",
|
||||
"admin.users.fixA1Cancel": "キャンセル",
|
||||
"admin.users.fixA1Confirm": "カタログを修復",
|
||||
"admin.users.noUsers": "このフィルタに一致するユーザーはいません。",
|
||||
"admin.users.noCompanies": "このフィルタに一致する会社はありません。",
|
||||
"admin.users.assignRoleTitle": "スタッフロールを割り当て",
|
||||
@@ -2296,6 +2304,8 @@ export const ja: MessageDict = {
|
||||
"flash.admin.staffRoleUpdated": "{email} のスタッフロールを更新しました。",
|
||||
"flash.admin.staffRoleUnavailable": "このサーバーではスタッフロールの更新はまだ利用できません。",
|
||||
"flash.admin.catalogCloned": "{source} を {dest} にコピーしました: 商品 {products}、カテゴリ {categories}。",
|
||||
"flash.admin.fixA1Success": "{name} のカタログ修復が完了: プロンプト {prompts} 件更新、ハッシュ {hashes} 件消去、カテゴリ {categories} 件補完。",
|
||||
"flash.admin.fixA1Error": "{name} のカタログ修復に失敗しました。",
|
||||
"flash.admin.planAssigned": "{name} にプランを割り当てました。",
|
||||
"flash.admin.planAssignedShort": "プランを割り当てました。",
|
||||
"flash.admin.creditsUpdated": "クレジットを更新しました。",
|
||||
|
||||
@@ -812,6 +812,14 @@ export const nl: MessageDict = {
|
||||
"admin.users.cloneCatalogCancel": "Annuleren",
|
||||
"admin.users.cloneCatalogConfirm": "Catalogus kopiëren",
|
||||
"admin.users.cloneDestFallback": "je sandboxbedrijf",
|
||||
"admin.users.fixA1": "A1-catalogus repareren",
|
||||
"admin.users.fixA1Aria": "AI-prompts, categorieën en enhance-hashes voor {name} repareren",
|
||||
"admin.users.fixA1Title": "Bedrijfscatalogus repareren",
|
||||
"admin.users.fixA1Desc": "Past gecorrigeerde AI-prompts opnieuw toe, wist zwakke enhance-hashes en vult categorieën bij vanuit mapped_data.",
|
||||
"admin.users.fixA1Company": "Bedrijf: {name}",
|
||||
"admin.users.fixA1Warning": "Geef voorkeur aan Platform Demo of een expliciet bedrijf. Verwijdert geen feeds, mappings of ruwe producten. Beschermt A1-cohort-overschrijfregels.",
|
||||
"admin.users.fixA1Cancel": "Annuleren",
|
||||
"admin.users.fixA1Confirm": "Catalogus repareren",
|
||||
"admin.users.noUsers": "Geen gebruikers komen overeen met dit filter.",
|
||||
"admin.users.noCompanies": "Geen bedrijven komen overeen met dit filter.",
|
||||
"admin.users.assignRoleTitle": "Medewerkerrol toewijzen",
|
||||
@@ -2296,6 +2304,8 @@ export const nl: MessageDict = {
|
||||
"flash.admin.staffRoleUpdated": "Personeelsrol bijgewerkt voor {email}.",
|
||||
"flash.admin.staffRoleUnavailable": "Personeelsrolupdates zijn op deze server nog niet beschikbaar.",
|
||||
"flash.admin.catalogCloned": "{source} gekopieerd naar {dest}: {products} producten, {categories} categorieën.",
|
||||
"flash.admin.fixA1Success": "Catalogusreparatie voor {name} voltooid: {prompts} prompts bijgewerkt, {hashes} hashes gewist, {categories} categorieën aangevuld.",
|
||||
"flash.admin.fixA1Error": "Catalogusreparatie voor {name} mislukt.",
|
||||
"flash.admin.planAssigned": "Plan toegewezen aan {name}.",
|
||||
"flash.admin.planAssignedShort": "Plan toegewezen.",
|
||||
"flash.admin.creditsUpdated": "Credits bijgewerkt.",
|
||||
|
||||
@@ -812,6 +812,14 @@ export const pl: MessageDict = {
|
||||
"admin.users.cloneCatalogCancel": "Anuluj",
|
||||
"admin.users.cloneCatalogConfirm": "Kopiuj katalog",
|
||||
"admin.users.cloneDestFallback": "twoja firma sandbox",
|
||||
"admin.users.fixA1": "Napraw katalog A1",
|
||||
"admin.users.fixA1Aria": "Napraw prompty AI, kategorie i hashe enhance dla {name}",
|
||||
"admin.users.fixA1Title": "Napraw katalog firmy",
|
||||
"admin.users.fixA1Desc": "Ponownie stosuje poprawione prompty AI, czyści słabe hashe enhance i uzupełnia kategorie z mapped_data.",
|
||||
"admin.users.fixA1Company": "Firma: {name}",
|
||||
"admin.users.fixA1Warning": "Preferuj Platform Demo lub wskazaną firmę. Nie usuwa feedów, mapowań ani surowych produktów. Chroni reguły nadpisywania kohorty A1.",
|
||||
"admin.users.fixA1Cancel": "Anuluj",
|
||||
"admin.users.fixA1Confirm": "Napraw katalog",
|
||||
"admin.users.noUsers": "Żaden użytkownik nie pasuje do tego filtra.",
|
||||
"admin.users.noCompanies": "Żadna firma nie pasuje do tego filtra.",
|
||||
"admin.users.assignRoleTitle": "Przypisz rolę personelu",
|
||||
@@ -2296,6 +2304,8 @@ export const pl: MessageDict = {
|
||||
"flash.admin.staffRoleUpdated": "Zaktualizowano rolę personelu dla {email}.",
|
||||
"flash.admin.staffRoleUnavailable": "Aktualizacje ról personelu nie są jeszcze dostępne na tym serwerze.",
|
||||
"flash.admin.catalogCloned": "Skopiowano {source} do {dest}: {products} produktów, {categories} kategorii.",
|
||||
"flash.admin.fixA1Success": "Naprawa katalogu dla {name} zakończona: zaktualizowano {prompts} promptów, wyczyszczono {hashes} hashy, uzupełniono {categories} kategorii.",
|
||||
"flash.admin.fixA1Error": "Naprawa katalogu dla {name} nie powiodła się.",
|
||||
"flash.admin.planAssigned": "Przypisano plan do {name}.",
|
||||
"flash.admin.planAssignedShort": "Przypisano plan.",
|
||||
"flash.admin.creditsUpdated": "Zaktualizowano kredyty.",
|
||||
|
||||
@@ -812,6 +812,14 @@ export const pt: MessageDict = {
|
||||
"admin.users.cloneCatalogCancel": "Cancelar",
|
||||
"admin.users.cloneCatalogConfirm": "Copiar catálogo",
|
||||
"admin.users.cloneDestFallback": "a sua empresa sandbox",
|
||||
"admin.users.fixA1": "Reparar catálogo A1",
|
||||
"admin.users.fixA1Aria": "Reparar prompts de IA, categorias e hashes de enhance para {name}",
|
||||
"admin.users.fixA1Title": "Reparar catálogo da empresa",
|
||||
"admin.users.fixA1Desc": "Reaplica prompts de IA corrigidos, limpa hashes de enhance fracos e preenche categorias a partir de mapped_data.",
|
||||
"admin.users.fixA1Company": "Empresa: {name}",
|
||||
"admin.users.fixA1Warning": "Prefira Platform Demo ou uma empresa explícita. Não elimina feeds, mapeamentos ou produtos brutos. Protege as regras de substituição da coorte A1.",
|
||||
"admin.users.fixA1Cancel": "Cancelar",
|
||||
"admin.users.fixA1Confirm": "Reparar catálogo",
|
||||
"admin.users.noUsers": "Nenhum utilizador corresponde a este filtro.",
|
||||
"admin.users.noCompanies": "Nenhuma empresa corresponde a este filtro.",
|
||||
"admin.users.assignRoleTitle": "Atribuir função de equipa",
|
||||
@@ -2296,6 +2304,8 @@ export const pt: MessageDict = {
|
||||
"flash.admin.staffRoleUpdated": "Função de pessoal atualizada para {email}.",
|
||||
"flash.admin.staffRoleUnavailable": "As atualizações de função de pessoal ainda não estão disponíveis neste servidor.",
|
||||
"flash.admin.catalogCloned": "Copiado {source} para {dest}: {products} produtos, {categories} categorias.",
|
||||
"flash.admin.fixA1Success": "Reparação do catálogo de {name} concluída: {prompts} prompts atualizados, {hashes} hashes limpos, {categories} categorias preenchidas.",
|
||||
"flash.admin.fixA1Error": "Falha na reparação do catálogo de {name}.",
|
||||
"flash.admin.planAssigned": "Plano atribuído a {name}.",
|
||||
"flash.admin.planAssignedShort": "Plano atribuído.",
|
||||
"flash.admin.creditsUpdated": "Créditos atualizados.",
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
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.fixA1": "Popravi katalog A1",
|
||||
"admin.users.fixA1Aria": "Popravi AI pozive, kategorije in zgoščevalne vrednosti za {name}",
|
||||
"admin.users.fixA1Title": "Popravi katalog podjetja",
|
||||
"admin.users.fixA1Desc": "Ponovno uporabi popravljene AI pozive, počisti šibke enhance zgoščevalne vrednosti in dopolni kategorije iz mapped podatkov.",
|
||||
"admin.users.fixA1Company": "Podjetje: {name}",
|
||||
"admin.users.fixA1Warning": "Raje Platform Demo ali izrecno podjetje. Ne briše feedov, mapiranj ali surovih izdelkov. Ščiti pravila prepisovanja A1 kohorte.",
|
||||
"admin.users.fixA1Cancel": "Prekliči",
|
||||
"admin.users.fixA1Confirm": "Popravi katalog",
|
||||
"flash.admin.fixA1Success": "Popravilo kataloga za {name} končano: posodobljenih pozivov {prompts}, počiščenih zgoščevalnih vrednosti {hashes}, dopolnjenih kategorij {categories}.",
|
||||
"flash.admin.fixA1Error": "Popravilo kataloga za {name} ni uspelo.",
|
||||
};
|
||||
@@ -11,6 +11,7 @@
|
||||
STAFF_ROLE_OPTIONS,
|
||||
assignAdminPlan,
|
||||
cloneAdminCompanyCatalog,
|
||||
fixAdminCompanyCatalog,
|
||||
companyPlanBadge,
|
||||
isStaffRoleApiUnavailable,
|
||||
listAdminCompanies,
|
||||
@@ -54,7 +55,7 @@
|
||||
TabsList,
|
||||
TabsTrigger
|
||||
} from "$lib/components/ui";
|
||||
import { Building2, Copy, KeyRound, Search, Shield, UserPlus, Users } from "@lucide/svelte";
|
||||
import { Building2, Copy, KeyRound, Search, Shield, UserPlus, Users, Wrench } from "@lucide/svelte";
|
||||
|
||||
type TabKey = "users" | "companies";
|
||||
|
||||
@@ -93,6 +94,9 @@
|
||||
let cloneOpen = $state(false);
|
||||
let cloneCompany = $state<AdminOrgCompany | null>(null);
|
||||
|
||||
let fixOpen = $state(false);
|
||||
let fixCompany = $state<AdminOrgCompany | null>(null);
|
||||
|
||||
const usersPage = $derived(Math.floor(usersOffset / PAGE_SIZE) + 1);
|
||||
const usersPages = $derived(Math.max(1, Math.ceil(usersTotal / PAGE_SIZE)));
|
||||
const companiesPage = $derived(Math.floor(companiesOffset / PAGE_SIZE) + 1);
|
||||
@@ -362,6 +366,39 @@
|
||||
success = "";
|
||||
}
|
||||
|
||||
function openFixDialog(company: AdminOrgCompany) {
|
||||
fixCompany = company;
|
||||
fixOpen = true;
|
||||
error = "";
|
||||
success = "";
|
||||
}
|
||||
|
||||
async function confirmFixCatalog() {
|
||||
if (!fixCompany) return;
|
||||
busy = true;
|
||||
error = "";
|
||||
success = "";
|
||||
const targetName = fixCompany.name;
|
||||
try {
|
||||
const res = await fixAdminCompanyCatalog(fixCompany.id, {
|
||||
reprocessSampleLimit: 25
|
||||
});
|
||||
const r = res.result;
|
||||
success = i18n.t("flash.admin.fixA1Success", {
|
||||
name: targetName,
|
||||
prompts: String(r.prompts ?? r.category_prompts_updated ?? 0),
|
||||
hashes: String(r.hashes ?? r.weak_hashes_cleared ?? 0),
|
||||
categories: String(r.categories ?? r.categories_backfilled ?? 0)
|
||||
});
|
||||
fixOpen = false;
|
||||
fixCompany = null;
|
||||
} catch (err) {
|
||||
error = failureMessage(err, i18n.t("flash.admin.fixA1Error", { name: targetName }));
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmCloneCatalog() {
|
||||
if (!cloneCompany) return;
|
||||
busy = true;
|
||||
@@ -776,28 +813,40 @@
|
||||
</TableCell>
|
||||
<TableCell class="hidden text-muted-foreground lg:table-cell">{company.language || "—"}</TableCell>
|
||||
<TableCell stickyRight>
|
||||
<div class="flex flex-wrap items-center justify-end gap-1">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onclick={() => openCloneDialog(company)}
|
||||
aria-label={i18n.t("admin.users.cloneCatalogAria", {
|
||||
name: company.name
|
||||
})}
|
||||
data-testid="admin-clone-catalog"
|
||||
>
|
||||
<Copy class="h-3.5 w-3.5 lg:mr-1" aria-hidden="true" />
|
||||
<span class="hidden lg:inline">{i18n.t("admin.users.cloneCatalog")}</span>
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onclick={() => openAssignDialog(company)}
|
||||
aria-label={`Assign plan to ${company.name}`}
|
||||
>
|
||||
<UserPlus class="h-3.5 w-3.5 lg:mr-1" aria-hidden="true" />
|
||||
<span class="hidden lg:inline">{i18n.t("admin.users.assignPlan")}</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center justify-end gap-1">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onclick={() => openFixDialog(company)}
|
||||
aria-label={i18n.t("admin.users.fixA1Aria", {
|
||||
name: company.name
|
||||
})}
|
||||
data-testid="admin-fix-catalog"
|
||||
>
|
||||
<Wrench class="h-3.5 w-3.5 lg:mr-1" aria-hidden="true" />
|
||||
<span class="hidden lg:inline">{i18n.t("admin.users.fixA1")}</span>
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onclick={() => openCloneDialog(company)}
|
||||
aria-label={i18n.t("admin.users.cloneCatalogAria", {
|
||||
name: company.name
|
||||
})}
|
||||
data-testid="admin-clone-catalog"
|
||||
>
|
||||
<Copy class="h-3.5 w-3.5 lg:mr-1" aria-hidden="true" />
|
||||
<span class="hidden lg:inline">{i18n.t("admin.users.cloneCatalog")}</span>
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onclick={() => openAssignDialog(company)}
|
||||
aria-label={`Assign plan to ${company.name}`}
|
||||
>
|
||||
<UserPlus class="h-3.5 w-3.5 lg:mr-1" aria-hidden="true" />
|
||||
<span class="hidden lg:inline">{i18n.t("admin.users.assignPlan")}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
@@ -957,3 +1006,39 @@
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
<Dialog
|
||||
bind:open={fixOpen}
|
||||
title={i18n.t("admin.users.fixA1Title")}
|
||||
description={i18n.t("admin.users.fixA1Desc")}
|
||||
>
|
||||
<div class="space-y-4">
|
||||
{#if error}
|
||||
<p class="text-sm text-destructive" role="alert">{error}</p>
|
||||
{/if}
|
||||
{#if fixCompany}
|
||||
<p class="text-sm text-foreground">
|
||||
{i18n.t("admin.users.fixA1Company", { name: fixCompany.name })}
|
||||
</p>
|
||||
{/if}
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{i18n.t("admin.users.fixA1Warning")}
|
||||
</p>
|
||||
<div class="flex flex-wrap justify-end gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
disabled={busy}
|
||||
onclick={() => {
|
||||
fixOpen = false;
|
||||
fixCompany = null;
|
||||
}}
|
||||
>
|
||||
{i18n.t("admin.users.fixA1Cancel")}
|
||||
</Button>
|
||||
<Button type="button" loading={busy} onclick={() => confirmFixCatalog()}>
|
||||
{i18n.t("admin.users.fixA1Confirm")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
Reference in New Issue
Block a user