This commit is contained in:
2026-08-17 00:39:25 +02:00
parent 92f046b542
commit 93dc70123c
54 changed files with 25528 additions and 20666 deletions
+81 -4
View File
@@ -66,6 +66,7 @@
let busyUserId = $state<string | null>(null);
let error = $state("");
let success = $state("");
let inviteAcceptLink = $state<string | null>(null);
let tab = $state<TabKey>("users");
let search = $state("");
let staffOnly = $state(false);
@@ -99,6 +100,7 @@
let syncOpen = $state(false);
let syncCompany = $state<AdminOrgCompany | null>(null);
let syncWpCategoriesFile = $state<File | null>(null);
const cloneDestLabel = $derived.by(() => {
const selected = cloneDestOptions.find((c) => c.id === cloneDestId);
@@ -407,6 +409,7 @@
function openSyncDialog(company: AdminOrgCompany) {
syncCompany = company;
syncWpCategoriesFile = null;
syncOpen = true;
error = "";
success = "";
@@ -468,7 +471,8 @@
const targetName = syncCompany.name;
try {
const res = await syncAdminCompanyA1(syncCompany.id, {
reprocessSampleLimit: 25
reprocessSampleLimit: 25,
wpProductCategoriesFile: syncWpCategoriesFile
});
const r = res.result;
const mappedWith = Number(r.mapped_with_category ?? 0);
@@ -489,6 +493,7 @@
});
syncOpen = false;
syncCompany = null;
syncWpCategoriesFile = null;
} catch (err) {
error = failureMessage(err, i18n.t("flash.admin.syncA1Error", { name: targetName }));
} finally {
@@ -500,6 +505,7 @@
busy = true;
error = "";
success = "";
inviteAcceptLink = null;
try {
const body = userId ? { user_id: userId } : {};
const res = await api<{
@@ -510,6 +516,7 @@
skipped_rate_limited?: number;
smtp_enabled: boolean;
token?: string;
accept_url?: string;
}>("/api/admin/emails/set-password", { method: "POST", body });
const parts = [
`sent ${res.sent}`,
@@ -518,9 +525,18 @@
];
if (res.skipped_synthetic) parts.push(`skipped test accounts ${res.skipped_synthetic}`);
if (res.skipped_rate_limited) parts.push(`rate-limited ${res.skipped_rate_limited}`);
success = i18n.t("flash.admin.invitesSummary", { parts: parts.join(", "), smtp: res.smtp_enabled ? i18n.t("flash.admin.smtpOn") : i18n.t("flash.admin.smtpOff") });
if (res.token) {
success += " Invite link available — copy and share it securely (email delivery is off).";
success = i18n.t("flash.admin.invitesSummary", {
parts: parts.join(", "),
smtp: res.smtp_enabled ? i18n.t("flash.admin.smtpOn") : i18n.t("flash.admin.smtpOff")
});
const link =
res.accept_url?.trim() ||
(res.token
? `${window.location.origin}/accept-invite?token=${encodeURIComponent(res.token)}`
: "");
if (link) {
inviteAcceptLink = link;
success += " " + i18n.t("admin.users.inviteLinkReady");
}
} catch (err) {
error = failureMessage(err, "Send failed");
@@ -529,6 +545,17 @@
}
}
async function copyInviteLink() {
if (!inviteAcceptLink) return;
try {
await navigator.clipboard.writeText(inviteAcceptLink);
success = i18n.t("admin.users.inviteLinkCopied");
error = "";
} catch {
error = i18n.t("flash.settings.copyFailed");
}
}
function openPasswordDialog(user: AdminOrgUser) {
passwordUser = user;
passwordValue = "";
@@ -613,6 +640,22 @@
{:else}
<Alert message={error} />
<Alert tone="success" message={success} />
{#if inviteAcceptLink}
<div class="mb-4 flex flex-col gap-2 rounded-md border border-border bg-muted/30 p-3 sm:flex-row sm:items-center">
<Input
value={inviteAcceptLink}
readonly
autocomplete="off"
spellcheck={false}
class="font-mono text-xs"
aria-label={i18n.t("admin.users.inviteLinkLabel")}
/>
<Button type="button" variant="outline" size="sm" onclick={() => copyInviteLink()}>
<Copy class="mr-2 h-4 w-4" />
{i18n.t("admin.users.copyInviteLink")}
</Button>
</div>
{/if}
{#if !staffRoleApiOk}
<Alert
tone="info"
@@ -1087,6 +1130,39 @@
{i18n.t("admin.users.syncA1Company", { name: syncCompany.name })}
</p>
{/if}
<div class="space-y-2">
<label class="block text-sm font-medium text-foreground" for="sync-a1-wp-categories">
{i18n.t("admin.users.syncA1WpUpload")}
</label>
<input
id="sync-a1-wp-categories"
type="file"
accept=".sql,text/plain,application/sql"
class="block w-full text-sm text-foreground file:mr-3 file:rounded-md file:border-0 file:bg-muted file:px-3 file:py-1.5 file:text-sm file:font-medium"
disabled={busy}
onchange={(e) => {
const input = e.currentTarget;
syncWpCategoriesFile = input.files?.[0] ?? null;
}}
/>
<p class="text-xs text-muted-foreground">{i18n.t("admin.users.syncA1WpUploadHint")}</p>
{#if syncWpCategoriesFile}
<div class="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
<span>{syncWpCategoriesFile.name}</span>
<Button
type="button"
variant="outline"
size="sm"
disabled={busy}
onclick={() => {
syncWpCategoriesFile = null;
}}
>
{i18n.t("admin.users.syncA1WpUploadClear")}
</Button>
</div>
{/if}
</div>
<p class="text-sm text-muted-foreground">
{i18n.t("admin.users.syncA1Warning")}
</p>
@@ -1098,6 +1174,7 @@
onclick={() => {
syncOpen = false;
syncCompany = null;
syncWpCategoriesFile = null;
}}
>
{i18n.t("admin.users.syncA1Cancel")}