688 lines
26 KiB
Vue
688 lines
26 KiB
Vue
<script setup lang="ts">
|
||
import type { Bot, PaginatedResponse, Signal, SubscriptionStatus, User, UserBotAccess } from "~/types";
|
||
|
||
definePageMeta({
|
||
middleware: "admin"
|
||
});
|
||
|
||
type AdminUser = User & {
|
||
pushSubscriptions: { id: string; endpoint: string; active: boolean }[];
|
||
};
|
||
|
||
type AdminPushDashboard = {
|
||
items: Array<{
|
||
id: string;
|
||
status: "ok" | "ready" | "error" | "inactive";
|
||
endpointHost: string;
|
||
user: {
|
||
id: string;
|
||
email: string;
|
||
};
|
||
latestEvent: {
|
||
statusCode: number | null;
|
||
reason: string | null;
|
||
} | null;
|
||
}>;
|
||
};
|
||
|
||
type SubscriptionDraft = {
|
||
status: SubscriptionStatus;
|
||
startsAt: string;
|
||
expiresAt: string;
|
||
};
|
||
|
||
const { formatDateTime } = useBrowserDateTime();
|
||
|
||
const initialForm = () => ({
|
||
eventId: "",
|
||
sportType: "football",
|
||
leagueName: "",
|
||
homeTeam: "",
|
||
awayTeam: "",
|
||
eventStartTime: "",
|
||
marketType: "1X2",
|
||
selection: "",
|
||
lineValue: "",
|
||
odds: "1.90",
|
||
signalTime: "",
|
||
comment: ""
|
||
});
|
||
|
||
const fieldLabels: Record<keyof ReturnType<typeof initialForm>, string> = {
|
||
eventId: "ID события",
|
||
sportType: "Вид спорта",
|
||
leagueName: "Лига",
|
||
homeTeam: "Домашняя команда",
|
||
awayTeam: "Гостевая команда",
|
||
eventStartTime: "Время начала события",
|
||
marketType: "Тип рынка",
|
||
selection: "Выбор",
|
||
lineValue: "Значение линии",
|
||
odds: "Коэффициент",
|
||
signalTime: "Время сигнала",
|
||
comment: "Комментарий"
|
||
};
|
||
|
||
const getFieldLabel = (key: string) => fieldLabels[key as keyof typeof fieldLabels];
|
||
const getFormValue = (key: string) => form.value[key as keyof typeof form.value];
|
||
const handleFormInput = (event: Event, key: string) => {
|
||
const target = event.target as HTMLInputElement | null;
|
||
form.value[key as keyof typeof form.value] = target?.value ?? "";
|
||
};
|
||
|
||
const form = ref(initialForm());
|
||
const signals = ref<Signal[]>([]);
|
||
const users = ref<AdminUser[]>([]);
|
||
const bots = ref<Bot[]>([]);
|
||
const pushDashboard = ref<AdminPushDashboard | null>(null);
|
||
const pushDashboardLoading = ref(false);
|
||
const pushDashboardError = ref("");
|
||
const broadcastTitle = ref("");
|
||
const broadcastBody = ref("");
|
||
const userSearch = ref("");
|
||
const usersLoading = ref(false);
|
||
const userActionMessage = ref("");
|
||
const createSignalOpen = ref(false);
|
||
const subscriptionDrafts = ref<Record<string, SubscriptionDraft>>({});
|
||
const savingSubscriptionKey = ref<string | null>(null);
|
||
const togglingUserId = ref<string | null>(null);
|
||
|
||
const subscriptionStatusOptions: Array<{ value: SubscriptionStatus; label: string }> = [
|
||
{ value: "active", label: "Активна" },
|
||
{ value: "expired", label: "Истекла" },
|
||
{ value: "canceled", label: "Отменена" }
|
||
];
|
||
|
||
const statusLabel: Record<"ok" | "ready" | "error" | "inactive", string> = {
|
||
ok: "OK",
|
||
ready: "Готово",
|
||
error: "Ошибка",
|
||
inactive: "Неактивно"
|
||
};
|
||
|
||
const subscriptionStatusLabel: Record<SubscriptionStatus, string> = {
|
||
active: "Активна",
|
||
expired: "Истекла",
|
||
canceled: "Отменена"
|
||
};
|
||
|
||
const userRoleLabel = (role: User["role"]) => (role === "admin" ? "Администратор" : "Пользователь");
|
||
const userActiveLabel = (active?: boolean) => (active ? "активен" : "отключен");
|
||
|
||
const statusBadgeClass = (status: "ok" | "ready" | "error" | "inactive") => {
|
||
if (status === "error") return "signal-row__badge--manual_review";
|
||
if (status === "inactive") return "signal-row__badge--inactive";
|
||
return "signal-row__badge--win";
|
||
};
|
||
|
||
const subscriptionBadgeClass = (status: SubscriptionStatus, isActiveNow?: boolean) => {
|
||
if (status === "canceled") return "signal-row__badge--inactive";
|
||
if (status === "expired" || !isActiveNow) return "signal-row__badge--manual_review";
|
||
return "signal-row__badge--win";
|
||
};
|
||
|
||
const subscriptionSelectClass = (status: SubscriptionStatus) => {
|
||
if (status === "canceled") return "admin-subscription-select admin-subscription-select--canceled";
|
||
if (status === "expired") return "admin-subscription-select admin-subscription-select--expired";
|
||
return "admin-subscription-select admin-subscription-select--active";
|
||
};
|
||
|
||
const subscriptionsByUserId = computed(() => {
|
||
const grouped = new Map<string, AdminPushDashboard["items"]>();
|
||
|
||
for (const item of pushDashboard.value?.items ?? []) {
|
||
const userItems = grouped.get(item.user.id) ?? [];
|
||
userItems.push(item);
|
||
grouped.set(item.user.id, userItems);
|
||
}
|
||
|
||
return grouped;
|
||
});
|
||
|
||
const toDateTimeLocal = (value?: string | null) => {
|
||
if (!value) return "";
|
||
const date = new Date(value);
|
||
if (Number.isNaN(date.getTime())) return "";
|
||
|
||
const pad = (part: number) => String(part).padStart(2, "0");
|
||
const year = date.getFullYear();
|
||
const month = pad(date.getMonth() + 1);
|
||
const day = pad(date.getDate());
|
||
const hours = pad(date.getHours());
|
||
const minutes = pad(date.getMinutes());
|
||
|
||
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
||
};
|
||
|
||
const subscriptionDraftKey = (userId: string, botId: string) => `${userId}:${botId}`;
|
||
|
||
const createSubscriptionDraft = (access?: UserBotAccess): SubscriptionDraft => ({
|
||
status: access?.status ?? "canceled",
|
||
startsAt: toDateTimeLocal(access?.startsAt ?? new Date().toISOString()),
|
||
expiresAt: toDateTimeLocal(access?.expiresAt ?? null)
|
||
});
|
||
|
||
const assignDrafts = (items: AdminUser[]) => {
|
||
const nextDrafts: Record<string, SubscriptionDraft> = {};
|
||
|
||
for (const user of items) {
|
||
for (const bot of bots.value) {
|
||
const access = (user.botAccesses ?? []).find((entry: UserBotAccess) => entry.bot.id === bot.id);
|
||
nextDrafts[subscriptionDraftKey(user.id, bot.id)] = createSubscriptionDraft(access);
|
||
}
|
||
}
|
||
|
||
subscriptionDrafts.value = nextDrafts;
|
||
};
|
||
|
||
const loadUsers = async () => {
|
||
usersLoading.value = true;
|
||
|
||
try {
|
||
const query = userSearch.value.trim();
|
||
const suffix = query ? `?q=${encodeURIComponent(query)}` : "";
|
||
const usersData = await useApi<AdminUser[]>(`/admin/users${suffix}`);
|
||
users.value = usersData;
|
||
assignDrafts(usersData);
|
||
userActionMessage.value = "";
|
||
} finally {
|
||
usersLoading.value = false;
|
||
}
|
||
};
|
||
|
||
const loadCore = async () => {
|
||
const [signalsData, botsData] = await Promise.all([
|
||
useApi<PaginatedResponse<Signal>>("/signals?page=1&perPage=200&published=true"),
|
||
useApi<Bot[]>("/admin/bots")
|
||
]);
|
||
|
||
signals.value = signalsData.items;
|
||
bots.value = botsData;
|
||
await loadUsers();
|
||
};
|
||
|
||
const loadPushDashboard = async () => {
|
||
pushDashboardLoading.value = true;
|
||
|
||
try {
|
||
pushDashboard.value = await useApi<AdminPushDashboard>("/admin/push-subscriptions");
|
||
pushDashboardError.value = "";
|
||
} catch (error) {
|
||
pushDashboard.value = null;
|
||
pushDashboardError.value = error instanceof Error ? error.message : "Не удалось загрузить статусы подписок";
|
||
} finally {
|
||
pushDashboardLoading.value = false;
|
||
}
|
||
};
|
||
|
||
const load = async () => {
|
||
await Promise.all([loadCore(), loadPushDashboard()]);
|
||
};
|
||
|
||
onMounted(async () => {
|
||
await load();
|
||
});
|
||
|
||
const createSignal = async () => {
|
||
await useApi("/admin/signals", {
|
||
method: "POST",
|
||
body: {
|
||
...form.value,
|
||
eventStartTime: new Date(form.value.eventStartTime).toISOString(),
|
||
signalTime: new Date(form.value.signalTime || form.value.eventStartTime).toISOString(),
|
||
lineValue: form.value.lineValue ? Number(form.value.lineValue) : null,
|
||
odds: Number(form.value.odds),
|
||
sourceType: "manual",
|
||
published: true
|
||
}
|
||
});
|
||
|
||
form.value = initialForm();
|
||
createSignalOpen.value = false;
|
||
await loadCore();
|
||
};
|
||
|
||
const setStatus = async (signalId: string, status: Signal["status"]) => {
|
||
await useApi(`/admin/signals/${signalId}/set-status`, {
|
||
method: "POST",
|
||
body: {
|
||
status,
|
||
explanation: `Статус был установлен вручную: ${status}`
|
||
}
|
||
});
|
||
|
||
await loadCore();
|
||
};
|
||
|
||
const sendPush = async (signalId: string) => {
|
||
await useApi(`/admin/signals/${signalId}/send-push`, {
|
||
method: "POST",
|
||
body: {}
|
||
});
|
||
};
|
||
|
||
const sendBroadcast = async () => {
|
||
await useApi("/admin/broadcast", {
|
||
method: "POST",
|
||
body: {
|
||
title: broadcastTitle.value,
|
||
body: broadcastBody.value
|
||
}
|
||
});
|
||
|
||
broadcastTitle.value = "";
|
||
broadcastBody.value = "";
|
||
};
|
||
|
||
const updateSubscriptionDraft = (userId: string, botId: string, patch: Partial<SubscriptionDraft>) => {
|
||
const key = subscriptionDraftKey(userId, botId);
|
||
subscriptionDrafts.value = {
|
||
...subscriptionDrafts.value,
|
||
[key]: {
|
||
...createSubscriptionDraft(),
|
||
...(subscriptionDrafts.value[key] ?? {}),
|
||
...patch
|
||
}
|
||
};
|
||
};
|
||
|
||
const handleSubscriptionStatusChange = (event: Event, userId: string, botId: string) => {
|
||
const target = event.target as HTMLSelectElement | null;
|
||
updateSubscriptionDraft(userId, botId, { status: (target?.value as SubscriptionStatus | undefined) ?? "canceled" });
|
||
};
|
||
|
||
const handleSubscriptionStartsAtChange = (event: Event, userId: string, botId: string) => {
|
||
const target = event.target as HTMLInputElement | null;
|
||
updateSubscriptionDraft(userId, botId, { startsAt: target?.value ?? "" });
|
||
};
|
||
|
||
const handleSubscriptionExpiresAtChange = (event: Event, userId: string, botId: string) => {
|
||
const target = event.target as HTMLInputElement | null;
|
||
updateSubscriptionDraft(userId, botId, { expiresAt: target?.value ?? "" });
|
||
};
|
||
|
||
const findUserSubscription = (member: AdminUser, botId: string) =>
|
||
(member.botAccesses ?? []).find((entry) => entry.bot.id === botId);
|
||
|
||
const addDays = (date: Date, days: number) => {
|
||
const next = new Date(date);
|
||
next.setDate(next.getDate() + days);
|
||
return next;
|
||
};
|
||
|
||
const saveSubscription = async (userId: string, botId: string) => {
|
||
const key = subscriptionDraftKey(userId, botId);
|
||
const draft = subscriptionDrafts.value[key];
|
||
if (!draft) return;
|
||
|
||
savingSubscriptionKey.value = key;
|
||
|
||
try {
|
||
const savedSubscription = await useApi<UserBotAccess>(`/admin/users/${userId}/subscriptions/${botId}`, {
|
||
method: "PATCH",
|
||
body: {
|
||
status: draft.status,
|
||
startsAt: draft.startsAt ? new Date(draft.startsAt).toISOString() : new Date().toISOString(),
|
||
expiresAt: draft.expiresAt ? new Date(draft.expiresAt).toISOString() : null
|
||
}
|
||
});
|
||
|
||
users.value = users.value.map((user) => {
|
||
if (user.id !== userId) return user;
|
||
|
||
const existing = (user.botAccesses ?? []).filter((entry: UserBotAccess) => entry.bot.id !== botId);
|
||
return {
|
||
...user,
|
||
botAccesses: [...existing, savedSubscription].sort((left, right) => left.bot.name.localeCompare(right.bot.name, "ru"))
|
||
};
|
||
});
|
||
|
||
updateSubscriptionDraft(userId, botId, createSubscriptionDraft(savedSubscription));
|
||
|
||
const userEmail = users.value.find((entry) => entry.id === userId)?.email ?? "пользователя";
|
||
const botName = bots.value.find((entry) => entry.id === botId)?.name ?? "бота";
|
||
userActionMessage.value = `Подписка ${userEmail} на ${botName} обновлена`;
|
||
} finally {
|
||
savingSubscriptionKey.value = null;
|
||
}
|
||
};
|
||
|
||
const activateMonthlySubscription = async (userId: string, botId: string) => {
|
||
const now = new Date();
|
||
const expiresAt = addDays(now, 30);
|
||
|
||
updateSubscriptionDraft(userId, botId, {
|
||
status: "active",
|
||
startsAt: toDateTimeLocal(now.toISOString()),
|
||
expiresAt: toDateTimeLocal(expiresAt.toISOString())
|
||
});
|
||
|
||
await saveSubscription(userId, botId);
|
||
};
|
||
|
||
const toggleUserActive = async (user: AdminUser) => {
|
||
togglingUserId.value = user.id;
|
||
|
||
try {
|
||
const updated = await useApi<AdminUser>(`/admin/users/${user.id}/active`, {
|
||
method: "PATCH",
|
||
body: {
|
||
active: !user.active
|
||
}
|
||
});
|
||
|
||
users.value = users.value.map((entry) => (entry.id === user.id ? { ...entry, active: updated.active } : entry));
|
||
userActionMessage.value = `${updated.email}: ${updated.active ? "доступ включен" : "доступ отключен"}`;
|
||
} finally {
|
||
togglingUserId.value = null;
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<template>
|
||
<section class="page admin-grid">
|
||
<!-- <div class="panel">
|
||
<div class="page-header page-header--admin-section">
|
||
<div>
|
||
<h1>Создание сигнала</h1>
|
||
<p class="muted">Разверните блок, чтобы добавить сигнал вручную.</p>
|
||
</div>
|
||
<button class="secondary" type="button" @click="createSignalOpen = !createSignalOpen">
|
||
{{ createSignalOpen ? "Свернуть" : "Развернуть" }}
|
||
</button>
|
||
</div>
|
||
|
||
<form v-if="createSignalOpen" class="admin-list" @submit.prevent="createSignal">
|
||
<label v-for="(value, key) in form" :key="key">
|
||
{{ getFieldLabel(String(key)) }}
|
||
<input
|
||
:value="getFormValue(String(key))"
|
||
:type="String(key).includes('Time') ? 'datetime-local' : 'text'"
|
||
@input="handleFormInput($event, String(key))"
|
||
/>
|
||
</label>
|
||
<button type="submit">Сохранить сигнал</button>
|
||
</form>
|
||
</div> -->
|
||
|
||
<!-- <div class="panel">
|
||
<h2>Действия с сигналами</h2>
|
||
<div class="admin-list">
|
||
<div v-for="signal in signals" :key="signal.id" class="admin-row">
|
||
<div>
|
||
<strong>{{ signal.homeTeam }} - {{ signal.awayTeam }}</strong>
|
||
<p>{{ signal.selection }} @ {{ signal.odds }}</p>
|
||
<p v-if="signal.forecast" class="muted">{{ signal.forecast }}</p>
|
||
<p v-if="signal.rawPayload?.botName" class="muted">{{ signal.rawPayload.botName }}</p>
|
||
</div>
|
||
<div class="button-row">
|
||
<button class="secondary" @click="sendPush(signal.id)">Отправить push</button>
|
||
<button class="secondary" @click="setStatus(signal.id, 'win')">Выигрыш</button>
|
||
<button class="secondary" @click="setStatus(signal.id, 'lose')">Проигрыш</button>
|
||
<button class="secondary" @click="setStatus(signal.id, 'void')">Возврат</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div> -->
|
||
|
||
<div class="panel">
|
||
<h2>Отправить уведомление со своим текстом</h2>
|
||
<label>
|
||
Заголовок
|
||
<input v-model="broadcastTitle" />
|
||
</label>
|
||
<label>
|
||
Текст
|
||
<textarea v-model="broadcastBody" rows="4" />
|
||
</label>
|
||
<button @click="sendBroadcast">Отправить рассылку</button>
|
||
</div>
|
||
<!--
|
||
<div class="panel">
|
||
<div class="page-header page-header--admin-section">
|
||
<h2>Push-подписки</h2>
|
||
<NuxtLink class="topbar__link" to="/admin/pushes">Открыть монитор</NuxtLink>
|
||
</div>
|
||
<p class="muted">Отдельная страница со всеми подписками, статусами доставки и автообновлением.</p>
|
||
<p v-if="pushDashboardLoading" class="muted">Загрузка статусов подписок...</p>
|
||
<p v-else-if="pushDashboardError" class="error">Статусы подписок недоступны: {{ pushDashboardError }}</p>
|
||
<p v-else-if="pushDashboard" class="muted">
|
||
{{ pushDashboard.items.length }} подписок, {{ pushDashboard.items.filter((item) => item.status === "ok").length }} OK,
|
||
{{ pushDashboard.items.filter((item) => item.status === "ready").length }} готово,
|
||
{{ pushDashboard.items.filter((item) => item.status === "error").length }} ошибок.
|
||
</p>
|
||
</div> -->
|
||
|
||
<div class="panel">
|
||
<div class="page-header page-header--admin-section">
|
||
<div>
|
||
<h2>Пользователи и подписки</h2>
|
||
<p class="muted">Ищите пользователя по email и управляйте подпиской на каждого бота отдельно.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="admin-users-toolbar">
|
||
<input
|
||
v-model="userSearch"
|
||
class="admin-users-toolbar__search"
|
||
placeholder="Поиск по email"
|
||
@keyup.enter="loadUsers"
|
||
/>
|
||
<button class="secondary" :disabled="usersLoading" @click="loadUsers">
|
||
{{ usersLoading ? "Поиск..." : "Найти пользователя" }}
|
||
</button>
|
||
</div>
|
||
|
||
<p v-if="userActionMessage" class="success">{{ userActionMessage }}</p>
|
||
|
||
<div class="admin-list">
|
||
<article v-for="member in users" :key="member.id" class="admin-user-card">
|
||
<div class="admin-user-card__header">
|
||
<div>
|
||
<strong>{{ member.email }}</strong>
|
||
<p class="muted">
|
||
{{ userRoleLabel(member.role) }} · {{ userActiveLabel(member.active) }} · создан
|
||
{{ formatDateTime(member.createdAt ?? "") }}
|
||
</p>
|
||
</div>
|
||
|
||
<button
|
||
class="secondary"
|
||
:disabled="togglingUserId === member.id"
|
||
@click="toggleUserActive(member)"
|
||
>
|
||
{{ togglingUserId === member.id ? "Сохранение..." : member.active ? "Отключить пользователя" : "Включить пользователя" }}
|
||
</button>
|
||
</div>
|
||
|
||
<div class="admin-user-card__body">
|
||
<div class="p-4 mt-2 border rounded-2xl border-(--border)">
|
||
<p class="muted">Текущие подписки</p>
|
||
<div class="admin-bot-chip-list">
|
||
<span
|
||
v-for="access in member.botAccesses ?? []"
|
||
:key="access.id"
|
||
class="signal-row__badge"
|
||
:class="subscriptionBadgeClass(access.status, access.isActiveNow)"
|
||
>
|
||
{{ access.bot.name }} · {{ subscriptionStatusLabel[access.status] }}
|
||
</span>
|
||
<span v-if="!(member.botAccesses ?? []).length" class="muted">Подписок пока нет</span>
|
||
</div>
|
||
|
||
<div v-if="subscriptionsByUserId.get(member.id)?.length" class="admin-user-subscriptions">
|
||
<div
|
||
v-for="subscription in subscriptionsByUserId.get(member.id)"
|
||
:key="subscription.id"
|
||
class="admin-user-subscription"
|
||
>
|
||
<span class="signal-row__badge" :class="statusBadgeClass(subscription.status)">
|
||
{{ statusLabel[subscription.status] }}
|
||
</span>
|
||
<span class="muted">{{ subscription.endpointHost }}</span>
|
||
<span v-if="subscription.latestEvent?.statusCode" class="muted">
|
||
{{ subscription.latestEvent.statusCode }}
|
||
</span>
|
||
<span v-if="subscription.latestEvent?.reason" class="muted">
|
||
{{ subscription.latestEvent.reason }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<details class="admin-subscription-manager">
|
||
<summary class="admin-subscription-manager__toggle">
|
||
<span>Управление подписками по ботам</span>
|
||
<span class="admin-subscription-manager__toggle-icon" aria-hidden="true">+</span>
|
||
</summary>
|
||
<div class="admin-subscription-grid admin-subscription-grid--mobile">
|
||
<article v-for="bot in bots" :key="bot.id" class="admin-subscription-card">
|
||
<div class="admin-subscription-card__header">
|
||
<div>
|
||
<strong>{{ bot.name }}</strong>
|
||
<small>{{ bot.key }}</small>
|
||
</div>
|
||
</div>
|
||
|
||
<label>
|
||
Статус
|
||
<select
|
||
:class="subscriptionSelectClass(
|
||
subscriptionDrafts[subscriptionDraftKey(member.id, bot.id)]?.status ?? 'canceled'
|
||
)"
|
||
:value="subscriptionDrafts[subscriptionDraftKey(member.id, bot.id)]?.status ?? 'canceled'"
|
||
@change="handleSubscriptionStatusChange($event, member.id, bot.id)"
|
||
>
|
||
<option
|
||
v-for="option in subscriptionStatusOptions"
|
||
:key="option.value"
|
||
:value="option.value"
|
||
>
|
||
{{ option.label }}
|
||
</option>
|
||
</select>
|
||
</label>
|
||
|
||
<label>
|
||
Начало
|
||
<input
|
||
type="datetime-local"
|
||
:value="subscriptionDrafts[subscriptionDraftKey(member.id, bot.id)]?.startsAt ?? ''"
|
||
@input="handleSubscriptionStartsAtChange($event, member.id, bot.id)"
|
||
/>
|
||
</label>
|
||
|
||
<label>
|
||
Окончание
|
||
<input
|
||
type="datetime-local"
|
||
:value="subscriptionDrafts[subscriptionDraftKey(member.id, bot.id)]?.expiresAt ?? ''"
|
||
@input="handleSubscriptionExpiresAtChange($event, member.id, bot.id)"
|
||
/>
|
||
</label>
|
||
|
||
<div class="button-row">
|
||
<button
|
||
class="secondary"
|
||
:disabled="savingSubscriptionKey === subscriptionDraftKey(member.id, bot.id)"
|
||
@click="activateMonthlySubscription(member.id, bot.id)"
|
||
>
|
||
Активировать на месяц
|
||
</button>
|
||
<button
|
||
:disabled="savingSubscriptionKey === subscriptionDraftKey(member.id, bot.id)"
|
||
@click="saveSubscription(member.id, bot.id)"
|
||
>
|
||
{{
|
||
savingSubscriptionKey === subscriptionDraftKey(member.id, bot.id)
|
||
? "Сохранение..."
|
||
: "Сохранить подписку"
|
||
}}
|
||
</button>
|
||
</div>
|
||
</article>
|
||
</div>
|
||
|
||
<div class="admin-subscription-table-wrap">
|
||
<table class="admin-subscription-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Бот</th>
|
||
<th>Статус</th>
|
||
<th>Начало</th>
|
||
<th>Окончание</th>
|
||
<th>Действия</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="bot in bots" :key="`table-${bot.id}`">
|
||
<td class="admin-subscription-table__bot">
|
||
<strong>{{ bot.name }}</strong>
|
||
<small>{{ bot.key }}</small>
|
||
</td>
|
||
<td>
|
||
<div class="admin-subscription-table__field">
|
||
<select
|
||
:class="subscriptionSelectClass(
|
||
subscriptionDrafts[subscriptionDraftKey(member.id, bot.id)]?.status ?? 'canceled'
|
||
)"
|
||
:value="subscriptionDrafts[subscriptionDraftKey(member.id, bot.id)]?.status ?? 'canceled'"
|
||
@change="handleSubscriptionStatusChange($event, member.id, bot.id)"
|
||
>
|
||
<option
|
||
v-for="option in subscriptionStatusOptions"
|
||
:key="option.value"
|
||
:value="option.value"
|
||
>
|
||
{{ option.label }}
|
||
</option>
|
||
</select>
|
||
</div>
|
||
</td>
|
||
<td>
|
||
<input
|
||
type="datetime-local"
|
||
:value="subscriptionDrafts[subscriptionDraftKey(member.id, bot.id)]?.startsAt ?? ''"
|
||
@input="handleSubscriptionStartsAtChange($event, member.id, bot.id)"
|
||
/>
|
||
</td>
|
||
<td>
|
||
<input
|
||
type="datetime-local"
|
||
:value="subscriptionDrafts[subscriptionDraftKey(member.id, bot.id)]?.expiresAt ?? ''"
|
||
@input="handleSubscriptionExpiresAtChange($event, member.id, bot.id)"
|
||
/>
|
||
</td>
|
||
<td>
|
||
<div class="admin-subscription-table__actions">
|
||
<button
|
||
class="secondary"
|
||
:disabled="savingSubscriptionKey === subscriptionDraftKey(member.id, bot.id)"
|
||
@click="activateMonthlySubscription(member.id, bot.id)"
|
||
>
|
||
Добавить еще месяц
|
||
</button>
|
||
<button
|
||
:disabled="savingSubscriptionKey === subscriptionDraftKey(member.id, bot.id)"
|
||
@click="saveSubscription(member.id, bot.id)"
|
||
>
|
||
{{
|
||
savingSubscriptionKey === subscriptionDraftKey(member.id, bot.id)
|
||
? "Сохранение..."
|
||
: "Сохранить"
|
||
}}
|
||
</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</details>
|
||
</div>
|
||
</article>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</template>
|