feat(clients): selective bulk attach + new bulk detach

Inbounds page:
- AttachClientsModal now shows a per-client selection table (email,
  comment, enabled tag) with search and a live "selected of total"
  counter; all clients are pre-selected so the old "attach all"
  workflow stays a single OK click.
- New DetachClientsModal on the inbound row menu lets you pick which
  clients to remove from that inbound (records are kept so they can be
  re-attached later; for full removal use Delete).

Clients page:
- New "Attach (N)" bulk-action button + BulkAttachInboundsModal that
  attaches selected clients to one or more multi-user inbounds.
- New "Detach (N)" bulk-action button + BulkDetachInboundsModal that
  removes selected clients from chosen inbounds; (email, inbound) pairs
  where the client isn't attached are silently skipped.

Backend adds POST /panel/api/clients/bulkDetach, wrapping the existing
Detach service for each email and reporting per-email
detached/skipped/errors. ClientRecord rows are kept on detach to match
the single-client endpoint; bulkDel remains the path for full removal.
This commit is contained in:
MHSanaei
2026-05-28 11:08:52 +02:00
parent a07b68894c
commit 72b68cce22
15 changed files with 809 additions and 13 deletions
+23
View File
@@ -301,6 +301,16 @@
"attachClientsNoTargets": "اینباند سازگار دیگری برای اتصال وجود ندارد.",
"attachClientsResult": "{attached} متصل شد، {skipped} رد شد.",
"attachClientsResultMixed": "{attached} متصل شد، {skipped} رد شد، {errors} خطا.",
"attachClientsSelectLabel": "کلاینت‌های قابل اتصال",
"attachClientsSearchPlaceholder": "جستجوی ایمیل یا توضیح",
"attachClientsStatusDisabled": "غیرفعال",
"attachClientsSelectedCount": "{selected} از {total} انتخاب‌شده",
"detachClients": "جداسازی کلاینت‌ها",
"detachClientsTitle": "جداسازی کلاینت‌های «{remark}»",
"detachClientsDesc": "کلاینت‌های انتخاب‌شده فقط از همین اینباند جدا می‌شوند. خود رکورد کلاینت‌ها حفظ می‌شود (برای حذف کامل از Delete استفاده کنید). این اینباند مجموعاً {count} کلاینت دارد.",
"detachClientsResult": "{detached} جدا شد، {skipped} رد شد.",
"detachClientsResultMixed": "{detached} جدا شد، {skipped} رد شد، {errors} خطا.",
"detachClientsSelectLabel": "کلاینت‌های قابل جداسازی",
"exportLinksTitle": "خروجی لینک‌های اینباند",
"exportSubsTitle": "خروجی لینک‌های ساب",
"exportAllLinksTitle": "خروجی لینک‌های همه اینباندها",
@@ -503,6 +513,19 @@
"deleteConfirmContent": "این کلاینت از تمام اینباندهای متصل حذف و سابقه ترافیک آن پاک می‌شود. این عمل غیرقابل بازگشت است.",
"deleteSelected": "حذف ({count})",
"adjustSelected": "تنظیم ({count})",
"attachSelected": "اتصال ({count})",
"attachToInboundsTitle": "اتصال {count} کلاینت به اینباند(ها)",
"attachToInboundsDesc": "{count} کلاینت انتخاب‌شده (با همان UUID/پسورد و ترافیک مشترک) به اینباند(های) انتخابی متصل می‌شوند. روی اینباندهای فعلی هم باقی می‌مانند.",
"attachToInboundsTargets": "اینباندهای مقصد",
"attachToInboundsNoTargets": "اینباند سازگار برای اتصال وجود ندارد.",
"detachSelected": "جداسازی ({count})",
"detach": "جداسازی",
"detachFromInboundsTitle": "جداسازی {count} کلاینت از اینباند(ها)",
"detachFromInboundsDesc": "{count} کلاینت انتخاب‌شده از اینباند(های) انتخابی جدا می‌شوند. زوج‌هایی که کلاینت در آن‌ها متصل نیست بی‌صدا رد می‌شوند. خود رکورد کلاینت‌ها حفظ می‌شود (برای حذف کامل از Delete استفاده کنید).",
"detachFromInboundsTargets": "اینباندهای مبدأ",
"detachFromInboundsNoTargets": "اینباند سازگار وجود ندارد.",
"detachFromInboundsResult": "{detached} جدا شد، {skipped} رد شد.",
"detachFromInboundsResultMixed": "{detached} جدا شد، {skipped} رد شد، {errors} خطا.",
"bulkDeleteConfirmTitle": "حذف {count} کلاینت؟",
"bulkDeleteConfirmContent": "هر کلاینت انتخاب‌شده از تمام اینباندهای متصل حذف و سابقه ترافیک آن پاک می‌شود. این عمل غیرقابل بازگشت است.",
"bulkAdjustTitle": "تنظیم {count} کلاینت",