diff --git a/src/renderer/appDefaults.js b/src/renderer/appDefaults.js new file mode 100644 index 0000000..feab230 --- /dev/null +++ b/src/renderer/appDefaults.js @@ -0,0 +1,118 @@ +export const emptySettings = { + competitorGroups: [""], + ourGroup: "", + minIntervalMinutes: 5, + maxIntervalMinutes: 10, + dailyLimit: 100, + historyLimit: 200, + accountMaxGroups: 10, + accountDailyLimit: 50, + floodCooldownMinutes: 1440, + queueTtlHours: 24 +}; + +export const emptyTaskForm = { + id: null, + name: "", + ourGroup: "", + minIntervalMinutes: 1, + maxIntervalMinutes: 3, + dailyLimit: 15, + historyLimit: 35, + maxInvitesPerCycle: 1, + maxCompetitorBots: 1, + maxOurBots: 1, + randomAccounts: false, + multiAccountsPerRun: false, + retryOnFail: true, + autoJoinCompetitors: true, + autoJoinOurGroup: true, + separateBotRoles: false, + requireSameBotInBoth: true, + parseParticipants: false, + inviteViaAdmins: false, + inviteAdminMasterId: 0, + inviteAdminAllowFlood: false, + inviteAdminAnonymous: true, + separateConfirmRoles: false, + maxConfirmBots: 1, + useWatcherInviteNoUsername: true, + warmupEnabled: true, + warmupStartLimit: 3, + warmupDailyIncrease: 2, + cycleCompetitors: false, + competitorCursor: 0, + inviteLinkOnFail: false, + rolesMode: "manual", + stopOnBlocked: true, + stopBlockedPercent: 25, + notes: "", + enabled: true, + autoAssignAccounts: true, + allowStartWithoutInviteRights: true +}; + +export const normalizeIntervals = (form) => { + const minValue = Number(form.minIntervalMinutes); + const maxValue = Number(form.maxIntervalMinutes); + const min = Number.isFinite(minValue) && minValue > 0 ? minValue : 1; + let max = Number.isFinite(maxValue) && maxValue > 0 ? maxValue : 1; + if (max < min) max = min; + return { ...form, minIntervalMinutes: min, maxIntervalMinutes: max }; +}; + +export const sanitizeTaskForm = (form) => { + let normalized = { ...form }; + normalized = normalizeIntervals(normalized); + if (normalized.requireSameBotInBoth) { + normalized.separateBotRoles = false; + normalized.maxOurBots = normalized.maxCompetitorBots; + } else { + normalized.separateBotRoles = true; + } + if (!normalized.separateBotRoles) { + normalized.separateConfirmRoles = false; + } + return normalized; +}; + +export const normalizeTask = (row) => ({ + id: row.id, + name: row.name || "", + ourGroup: row.our_group || "", + minIntervalMinutes: Number(row.min_interval_minutes || 1), + maxIntervalMinutes: Number(row.max_interval_minutes || 3), + dailyLimit: Number(row.daily_limit || 15), + historyLimit: Number(row.history_limit || 35), + maxInvitesPerCycle: Number(row.max_invites_per_cycle || 1), + maxCompetitorBots: Number(row.max_competitor_bots || 1), + maxOurBots: Number(row.max_our_bots || 1), + randomAccounts: Boolean(row.random_accounts), + multiAccountsPerRun: Boolean(row.multi_accounts_per_run), + retryOnFail: Boolean(row.retry_on_fail), + autoJoinCompetitors: Boolean(row.auto_join_competitors), + autoJoinOurGroup: Boolean(row.auto_join_our_group), + separateBotRoles: Boolean(row.separate_bot_roles), + requireSameBotInBoth: Boolean(row.require_same_bot_in_both), + parseParticipants: Boolean(row.parse_participants), + inviteViaAdmins: Boolean(row.invite_via_admins), + inviteAdminMasterId: Number(row.invite_admin_master_id || 0), + inviteAdminAllowFlood: Boolean(row.invite_admin_allow_flood), + inviteAdminAnonymous: row.invite_admin_anonymous == null ? true : Boolean(row.invite_admin_anonymous), + separateConfirmRoles: Boolean(row.separate_confirm_roles), + maxConfirmBots: Number(row.max_confirm_bots || 1), + useWatcherInviteNoUsername: row.use_watcher_invite_no_username == null ? true : Boolean(row.use_watcher_invite_no_username), + warmupEnabled: row.warmup_enabled == null ? true : Boolean(row.warmup_enabled), + warmupStartLimit: Number(row.warmup_start_limit || 3), + warmupDailyIncrease: Number(row.warmup_daily_increase || 2), + cycleCompetitors: Boolean(row.cycle_competitors), + competitorCursor: Number(row.competitor_cursor || 0), + inviteLinkOnFail: Boolean(row.invite_link_on_fail), + rolesMode: row.role_mode || "manual", + stopOnBlocked: Boolean(row.stop_on_blocked), + stopBlockedPercent: Number(row.stop_blocked_percent || 25), + notes: row.notes || "", + enabled: Boolean(row.enabled), + allowStartWithoutInviteRights: row.allow_start_without_invite_rights == null ? true : Boolean(row.allow_start_without_invite_rights), + autoAssignAccounts: true +}); diff --git a/src/renderer/components/AppMain.jsx b/src/renderer/components/AppMain.jsx new file mode 100644 index 0000000..3177a60 --- /dev/null +++ b/src/renderer/components/AppMain.jsx @@ -0,0 +1,76 @@ +import React from "react"; +import QuickActionsBar from "./QuickActionsBar.jsx"; +import NowStatusCard from "./NowStatusCard.jsx"; +import ChecklistCard from "./ChecklistCard.jsx"; +import MainTabs from "./MainTabs.jsx"; +import TaskSettingsTab from "./TaskSettingsTab.jsx"; +import MainTabContent from "./MainTabContent.jsx"; +import TestRunCard from "./TestRunCard.jsx"; +import useTabProps from "../hooks/useTabProps.js"; + +const AccountsTab = React.lazy(() => import("../tabs/AccountsTab.jsx")); +const LogsTab = React.lazy(() => import("../tabs/LogsTab.jsx")); +const QueueTab = React.lazy(() => import("../tabs/QueueTab.jsx")); +const EventsTab = React.lazy(() => import("../tabs/EventsTab.jsx")); +const SettingsTab = React.lazy(() => import("../tabs/SettingsTab.jsx")); + +export default function AppMain({ + quickActions, + nowStatus, + testRun, + runTestLive, + checklist, + tabs, + taskSettings, + accountsTab, + logsTab, + queueTab, + eventsTab, + settingsTab +}) { + const { + taskSettingsProps, + accountsTabProps, + logsTabProps, + queueTabProps, + eventsTabProps, + settingsTabProps + } = useTabProps(taskSettings, accountsTab, logsTab, queueTab, eventsTab, settingsTab); + + return ( +
+ + + + + + +
+ ); +} diff --git a/src/renderer/components/AppOverlays.jsx b/src/renderer/components/AppOverlays.jsx new file mode 100644 index 0000000..9ec6f14 --- /dev/null +++ b/src/renderer/components/AppOverlays.jsx @@ -0,0 +1,100 @@ +import React from "react"; +import ImportAccountsModal from "./ImportAccountsModal.jsx"; +import NotificationsModal from "./NotificationsModal.jsx"; +import InfoModal from "./InfoModal.jsx"; +import ToastStack from "./ToastStack.jsx"; +import ConfirmModal from "./ConfirmModal.jsx"; + +export default function AppOverlays({ + importModalOpen, + setImportModalOpen, + manualLoginOpen, + setManualLoginOpen, + hasSelectedTask, + loginForm, + setLoginForm, + startLogin, + completeLogin, + loginStatus, + tdataForm, + setTdataForm, + importTdata, + tdataLoading, + tdataResult, + explainTdataError, + notificationsOpen, + setNotificationsOpen, + notificationsModalRef, + notifications, + filteredNotifications, + notificationFilter, + setNotificationFilter, + setNotifications, + infoOpen, + setInfoOpen, + infoTab, + setInfoTab, + liveConfirmOpen, + setLiveConfirmOpen, + liveConfirmContext, + onConfirmLiveInvite, + onCancelLiveInvite, + toasts, + dismissToast +}) { + return ( + <> + setImportModalOpen(false)} + manualLoginOpen={manualLoginOpen} + setManualLoginOpen={setManualLoginOpen} + hasSelectedTask={hasSelectedTask} + loginForm={loginForm} + setLoginForm={setLoginForm} + startLogin={startLogin} + completeLogin={completeLogin} + loginStatus={loginStatus} + tdataForm={tdataForm} + setTdataForm={setTdataForm} + importTdata={importTdata} + tdataLoading={tdataLoading} + tdataResult={tdataResult} + explainTdataError={explainTdataError} + /> + setNotificationsOpen(false)} + notificationsModalRef={notificationsModalRef} + notifications={notifications} + filteredNotifications={filteredNotifications} + notificationFilter={notificationFilter} + setNotificationFilter={setNotificationFilter} + setNotifications={setNotifications} + /> + setInfoOpen(false)} + infoTab={infoTab} + setInfoTab={setInfoTab} + /> + + + + ); +} diff --git a/src/renderer/components/AppSidebar.jsx b/src/renderer/components/AppSidebar.jsx new file mode 100644 index 0000000..ce52bf0 --- /dev/null +++ b/src/renderer/components/AppSidebar.jsx @@ -0,0 +1,154 @@ +import React from "react"; +import SidebarOverview from "./SidebarOverview.jsx"; +import SidebarAccounts from "./SidebarAccounts.jsx"; +import TasksSidebar from "./TasksSidebar.jsx"; + +export function AppSidebarOverview({ + taskSummary, + globalStatus, + selectedTaskName, + competitorGroups, + assignedAccountCount, + taskStatus, + notificationsOpen, + setNotificationsOpen, + infoOpen, + setInfoOpen, + bellRef, + notificationsCount, + onOpenImport +}) { + return ( + <> + + + + ); +} + +export function AppSidebarTasks({ + createTask, + taskSearch, + setTaskSearch, + taskFilter, + setTaskFilter, + taskSort, + setTaskSort, + filteredTasks, + taskStatusMap, + selectedTaskId, + selectTask, + deleteTask, + hasSelectedTask, + formatCountdown, + formatTimestamp, + accountById, + formatAccountLabel +}) { + return ( + + ); +} + +export default function AppSidebar({ + taskSummary, + globalStatus, + selectedTaskName, + competitorGroups, + assignedAccountCount, + taskStatus, + notificationsOpen, + setNotificationsOpen, + infoOpen, + setInfoOpen, + bellRef, + notificationsCount, + onOpenImport, + createTask, + taskSearch, + setTaskSearch, + taskFilter, + setTaskFilter, + taskSort, + setTaskSort, + filteredTasks, + taskStatusMap, + selectedTaskId, + selectTask, + deleteTask, + hasSelectedTask, + formatCountdown, + formatTimestamp, + accountById, + formatAccountLabel +}) { + return ( + + ); +} diff --git a/src/renderer/components/ChecklistCard.jsx b/src/renderer/components/ChecklistCard.jsx new file mode 100644 index 0000000..ed4e85d --- /dev/null +++ b/src/renderer/components/ChecklistCard.jsx @@ -0,0 +1,50 @@ +import React from "react"; + +export default function ChecklistCard({ + checklistStats, + checklistOpen, + setChecklistOpen, + checklistItems, + hasSelectedTask +}) { + return ( +
+
+
+

Чек-лист запуска

+
Готово: {checklistStats.ok}/{checklistStats.total} · Проблемы: {checklistStats.fail}
+
+ +
+ {checklistOpen && ( +
+ {checklistItems.map((item) => { + const status = item.ok ? "ok" : (item.warn ? "warn" : "fail"); + const statusLabel = item.ok ? "Готово" : (item.warn ? "Есть проблемы" : "Нужно внимание"); + return ( +
+
+
{item.label}
+
{item.hint}
+
+
+ {statusLabel} + +
+
+ ); + })} +
+ )} +
+ ); +} diff --git a/src/renderer/components/ConfirmModal.jsx b/src/renderer/components/ConfirmModal.jsx new file mode 100644 index 0000000..38866d1 --- /dev/null +++ b/src/renderer/components/ConfirmModal.jsx @@ -0,0 +1,29 @@ +import React from "react"; + +export default function ConfirmModal({ + open, + title, + message, + confirmLabel = "Подтвердить", + cancelLabel = "Отмена", + onConfirm, + onCancel +}) { + if (!open) return null; + + return ( +
+
event.stopPropagation()}> +
+

{title}

+ +
+
{message}
+
+ + +
+
+
+ ); +} diff --git a/src/renderer/components/ImportAccountsModal.jsx b/src/renderer/components/ImportAccountsModal.jsx new file mode 100644 index 0000000..cb31b83 --- /dev/null +++ b/src/renderer/components/ImportAccountsModal.jsx @@ -0,0 +1,146 @@ +import React from "react"; + +export default function ImportAccountsModal({ + open, + onClose, + manualLoginOpen, + setManualLoginOpen, + hasSelectedTask, + loginForm, + setLoginForm, + startLogin, + completeLogin, + loginStatus, + tdataForm, + setTdataForm, + importTdata, + tdataLoading, + tdataResult, + explainTdataError +}) { + if (!open) return null; + + return ( +
+
event.stopPropagation()}> +
+

Импорт аккаунтов

+ +
+
+
+

Добавить аккаунт по коду

+ +
+ {manualLoginOpen && ( +
+ {!hasSelectedTask && ( +
Выберите задачу, чтобы добавить аккаунт.
+ )} +
+ + +
+ +
+ + +
+
+ + +
+ {loginStatus &&
{loginStatus}
} +
+ )} +
+ +
+

Импорт из tdata

+
+ Можно выбрать сразу несколько папок. Значения по умолчанию — API Telegram Desktop. +
+
+ + +
+ + {tdataLoading &&
Идет импорт, это может занять несколько секунд.
} + {tdataResult && ( +
+
Импортировано: {(tdataResult.imported || []).length}
+
Пропущено: {(tdataResult.skipped || []).length}
+
Ошибок: {(tdataResult.failed || []).length}
+ {(tdataResult.failed || []).length > 0 && ( +
+ {tdataResult.failed.map((item, index) => ( +
+
{item.path}
+
{item.error}
+ {explainTdataError(item.error) && ( +
{explainTdataError(item.error)}
+ )} +
+ ))} +
+ )} +
+ )} +
+
+
+ ); +} diff --git a/src/renderer/components/InfoModal.jsx b/src/renderer/components/InfoModal.jsx new file mode 100644 index 0000000..3e6ee55 --- /dev/null +++ b/src/renderer/components/InfoModal.jsx @@ -0,0 +1,96 @@ +import React from "react"; + +export default function InfoModal({ open, onClose, infoTab, setInfoTab }) { + if (!open) return null; + + return ( +
+
event.stopPropagation()}> +
+

Как пользоваться

+ +
+
+ + + + +
+ {infoTab === "usage" && ( + <> +
    +
  1. Создайте задачу: название, наша группа и группы конкурентов.
  2. +
  3. Импортируйте аккаунты (tdata) и назначьте роли для задачи.
  4. +
  5. Нажмите “Собрать историю”, чтобы добавить авторов из последних сообщений.
  6. +
  7. Нажмите “Запустить”, чтобы отслеживать новые сообщения и приглашать по расписанию.
  8. +
  9. Следите за статусом, логами, событиями и очередью.
  10. +
+

+ “Собрать историю” добавляет в очередь авторов старых сообщений. Без этого учитываются только новые сообщения. +

+ + )} + {infoTab === "features" && ( +
+ Функции и режимы: +
1) Мониторинг: отслеживает новые сообщения в чатах конкурентов и добавляет авторов в очередь.
+
2) Инвайт по расписанию: приглашает с интервалом и дневным лимитом.
+
3) Инвайт через админов: временно выдает право “Приглашать”, затем снимает.
+
4) Инвайт в чаты с флудом: распределяет инвайт через цепочку выдачи прав.
+
5) Циклический обход конкурентов: переключает мониторинг по списку групп.
+
6) Парсинг участников: пытается получить список участников для закрытых чатов.
+
7) Прогрев лимита: плавно увеличивает дневной лимит по дням.
+
8) Fallback‑лист: собирает проблемные инвайты и предлагает маршруты.
+
+ )} + {infoTab === "strategies" && ( +
+ Стратегии инвайта: +
1) access_hash из сообщения.
+
2) Резолв через участников/источник.
+
3) Инвайт по username (если доступен).
+
4) Инвайт через админов (если включен).
+
5) Отправка инвайт‑ссылки (если включено).
+
После успешного инвайта выполняется проверка фактического вступления.
+
+ )} + {infoTab === "limits" && ( +
+ Особенности Telegram и ошибки: +
1) AUTH_KEY_DUPLICATED: tdata уже используется — выйдите из аккаунта на других устройствах и пересоберите tdata.
+
2) CHAT_ADMIN_REQUIRED: аккаунт должен быть админом с правом “добавлять участников”.
+
3) USER_ID_INVALID: скрытые/анонимные авторы, удаленные аккаунты — инвайт возможен только по username.
+
4) USER_NOT_MUTUAL_CONTACT: ограничения Telegram/приватность пользователя — помогает инвайт‑ссылка или другой аккаунт.
+
5) USER_PRIVACY_RESTRICTED: пользователь запретил инвайты в чаты.
+
6) FLOOD/PEER_FLOOD: снизить лимиты, увеличить интервалы, распределить нагрузку.
+
7) CHANNEL_PRIVATE/INVITE_HASH_INVALID: ссылка недействительна или чат приватный.
+
+ )} +
+
+ ); +} diff --git a/src/renderer/components/MainTabContent.jsx b/src/renderer/components/MainTabContent.jsx new file mode 100644 index 0000000..0740542 --- /dev/null +++ b/src/renderer/components/MainTabContent.jsx @@ -0,0 +1,55 @@ +import React, { Suspense } from "react"; + +export default function MainTabContent({ + activeTab, + TaskSettingsTab, + AccountsTab, + LogsTab, + QueueTab, + EventsTab, + SettingsTab, + taskSettingsProps, + accountsTabProps, + logsTabProps, + queueTabProps, + eventsTabProps, + settingsTabProps +}) { + return ( + <> + {activeTab === "task" && ( + + )} + + {activeTab === "accounts" && ( + Загрузка...}> + + + )} + + {activeTab === "logs" && ( + Загрузка...}> + + + )} + + {activeTab === "queue" && ( + Загрузка...}> + + + )} + + {activeTab === "events" && ( + Загрузка...}> + + + )} + + {activeTab === "settings" && ( + Загрузка...}> + + + )} + + ); +} diff --git a/src/renderer/components/MainTabs.jsx b/src/renderer/components/MainTabs.jsx new file mode 100644 index 0000000..37c6ec2 --- /dev/null +++ b/src/renderer/components/MainTabs.jsx @@ -0,0 +1,50 @@ +import React from "react"; + +export default function MainTabs({ activeTab, setActiveTab }) { + return ( +
+ + + + + + +
+ ); +} diff --git a/src/renderer/components/NotificationsModal.jsx b/src/renderer/components/NotificationsModal.jsx new file mode 100644 index 0000000..bddaadb --- /dev/null +++ b/src/renderer/components/NotificationsModal.jsx @@ -0,0 +1,56 @@ +import React from "react"; + +export default function NotificationsModal({ + open, + onClose, + notificationsModalRef, + notifications, + filteredNotifications, + notificationFilter, + setNotificationFilter, + setNotifications +}) { + if (!open) return null; + + return ( +
+
event.stopPropagation()}> +
+

Уведомления

+ +
+
+ + + +
+ {filteredNotifications.length === 0 &&
Пока пусто.
} + {filteredNotifications.map((item) => ( +
+ {item.text}{item.count > 1 ? ` (x${item.count})` : ""} +
+ ))} +
+
+ ); +} diff --git a/src/renderer/components/NowStatusCard.jsx b/src/renderer/components/NowStatusCard.jsx new file mode 100644 index 0000000..57150bc --- /dev/null +++ b/src/renderer/components/NowStatusCard.jsx @@ -0,0 +1,95 @@ +import React from "react"; + +export default function NowStatusCard({ + nowLine, + nowExpanded, + setNowExpanded, + primaryIssue, + openFixTab, + monitorLabels, + inviteLabels, + roleSummary, + taskStatus, + groupVisibility, + lastEvents, + formatTimestamp +}) { + return ( +
+
+
{nowLine}
+ +
+ {primaryIssue && ( +
+ Причина: {primaryIssue} + +
+ )} + {nowExpanded && ( +
+
Мониторит: {monitorLabels.length ? monitorLabels.join(", ") : "—"}
+
Инвайтят: {inviteLabels.length ? inviteLabels.join(", ") : "—"}
+
Схема: мониторинг {roleSummary.monitor.length} · инвайт {roleSummary.invite.length} · подтверждение {roleSummary.confirm.length}
+
Последнее сообщение: {formatTimestamp(taskStatus.monitorInfo ? taskStatus.monitorInfo.lastMessageAt : "")}
+
Источник: {taskStatus.monitorInfo && taskStatus.monitorInfo.lastSource ? taskStatus.monitorInfo.lastSource : "—"}
+ {taskStatus.readiness && ( +
+ Готовность: {taskStatus.readiness.ok ? "Да" : "Нет"} + {!taskStatus.readiness.ok && taskStatus.readiness.reasons && ( +
{taskStatus.readiness.reasons.join("\n")}
+ )} +
+ )} + {taskStatus.lastStopReason && ( +
+ Последняя остановка: {taskStatus.lastStopReason} + {taskStatus.lastStopAt ? ` (${formatTimestamp(taskStatus.lastStopAt)})` : ""} +
+ )} + {taskStatus.warnings && taskStatus.warnings.length > 0 && ( +
+ {taskStatus.warnings.map((warning, index) => ( +
{warning}
+ ))} +
+ )} + {groupVisibility.length > 0 && groupVisibility.some((item) => item.hidden) && ( +
+ В некоторых группах скрыты участники — инвайт возможен только по username. +
+ {groupVisibility + .filter((item) => item.hidden) + .map((item) => ( +
+ {item.title ? `${item.title} (${item.source})` : item.source} +
+ ))} +
+
+ )} +
+ Последние события: + {lastEvents.length === 0 &&
} + {lastEvents.map((event) => { + const firstLine = event.message ? String(event.message).split("\n")[0] : ""; + return ( +
+ {formatTimestamp(event.createdAt)} • {event.eventType}{firstLine ? ` • ${firstLine}` : ""} +
+ ); + })} +
+
+ )} +
+ ); +} diff --git a/src/renderer/components/QuickActionsBar.jsx b/src/renderer/components/QuickActionsBar.jsx new file mode 100644 index 0000000..8a1dda7 --- /dev/null +++ b/src/renderer/components/QuickActionsBar.jsx @@ -0,0 +1,142 @@ +import React from "react"; + +export default function QuickActionsBar({ + selectedTaskName, + autosaveNote, + taskStatus, + hasSelectedTask, + canSaveTask, + taskActionLoading, + saveTask, + parseHistory, + joinGroupsForTask, + checkAll, + startTask, + stopTask, + moreActionsOpen, + setMoreActionsOpen, + moreActionsRef, + clearQueue, + startAllTasks, + stopAllTasks, + clearDatabase, + resetSessions, + pauseReason, + setActiveTab, + tasksLength, + runTestSafe +}) { + return ( +
+
+
+

Быстрые действия

+
+ Задача: {selectedTaskName} + {autosaveNote && {autosaveNote}} +
+
+
+ {taskStatus.running ? "Запущено" : "Остановлено"} +
+
+
+ + + + + + {taskStatus.running ? ( + + ) : ( + + )} +
+
+ { + event.preventDefault(); + setMoreActionsOpen((prev) => !prev); + }} + > + Ещё + +
+ + + + + +
+
+ {!taskStatus.running && pauseReason && ( +
+ Пауза: {pauseReason} + +
+ )} +
+ ); +} diff --git a/src/renderer/components/SidebarAccounts.jsx b/src/renderer/components/SidebarAccounts.jsx new file mode 100644 index 0000000..f12f012 --- /dev/null +++ b/src/renderer/components/SidebarAccounts.jsx @@ -0,0 +1,26 @@ +import React from "react"; + +export default function SidebarAccounts({ onOpenImport }) { + return ( +
+ +
+
+

Аккаунты

+
Общий импорт
+
+ +
+
+
+ ); +} diff --git a/src/renderer/components/SidebarOverview.jsx b/src/renderer/components/SidebarOverview.jsx new file mode 100644 index 0000000..f8e0fc7 --- /dev/null +++ b/src/renderer/components/SidebarOverview.jsx @@ -0,0 +1,75 @@ +import React from "react"; + +export default function SidebarOverview({ + taskSummary, + globalStatus, + selectedTaskName, + competitorGroups, + assignedAccountCount, + taskStatus, + notificationsOpen, + setNotificationsOpen, + infoOpen, + setInfoOpen, + bellRef, + notificationsCount +}) { + return ( +
+ +
+
+

Общий обзор

+
+
+
+ + {notificationsCount > 0 && ( + {notificationsCount} + )} +
+ +
+
+
+
+
+
Всего задач
+
{taskSummary.total}
+
+
+
Сессии
+
+ {globalStatus.connectedSessions}/{globalStatus.totalAccounts} +
+
+
+
+
+ Задача: {selectedTaskName} +
+
+ Конкуренты: {competitorGroups.length} +
+
+ Аккаунты: {assignedAccountCount} +
+
+ Лимит: {taskStatus.dailyUsed}/{taskStatus.dailyLimit} +
+
+
+ ); +} diff --git a/src/renderer/components/TaskSettingsTab.jsx b/src/renderer/components/TaskSettingsTab.jsx new file mode 100644 index 0000000..950a781 --- /dev/null +++ b/src/renderer/components/TaskSettingsTab.jsx @@ -0,0 +1,740 @@ +import React from "react"; + +export default function TaskSettingsTab({ + selectedTaskName, + taskForm, + setTaskForm, + activePreset, + applyTaskPreset, + formatAccountLabel, + accountById, + competitorText, + setCompetitorText, + roleMode, + applyRoleMode, + normalizeIntervals, + taskStatus, + perAccountInviteSum, + hasSelectedTask, + inviteAccessStatus, + inviteAccessCheckedAt, + formatTimestamp, + checkInviteAccess, + accounts, + showNotification, + copyToClipboard, + sanitizeTaskForm, + hasPerAccountInviteLimits, + fileImportResult, + importInviteFile, + fileImportForm, + setFileImportForm, + criticalErrorAccounts +}) { + return ( +
+
+ +
+

Настройки задачи

+
Для: {selectedTaskName}
+
+
+
Основное
+
+
Пресеты:
+ {taskForm.rolesMode === "auto" ? ( + <> + + + + + + + + Мастер-админ: {taskForm.inviteAdminMasterId ? formatAccountLabel(accountById.get(taskForm.inviteAdminMasterId)) : "не выбран"} + + + ) : ( + В ручном режиме пресеты недоступны. + )} +
+
+ + +
+