init
This commit is contained in:
687
frontend/pages/admin.vue
Normal file
687
frontend/pages/admin.vue
Normal file
@@ -0,0 +1,687 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user