feat(submissions): auto-replace player in scopes, cancel sync + info about the sync
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/vue-query";
|
||||
import dayjs from "dayjs";
|
||||
import {
|
||||
submissionsApiStopSyncScopesMutation,
|
||||
submissionsApiSyncScopesMutation,
|
||||
submissionsApiSyncScopesStatusOptions,
|
||||
submissionsApiSyncScopesStatusQueryKey,
|
||||
} from "@/api/@tanstack/vue-query.gen";
|
||||
|
||||
interface Props {
|
||||
eventPath: string;
|
||||
gamePath: string;
|
||||
gameName: string;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const open = defineModel<boolean>("open", { required: true });
|
||||
|
||||
const { t } = useI18n();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const path = computed(() => ({ event_path: props.eventPath, game_path: props.gamePath }));
|
||||
const statusKey = computed(() => submissionsApiSyncScopesStatusQueryKey({ path: path.value }));
|
||||
|
||||
// Polled while open, so admins see each run land (last/next run, errors).
|
||||
const { data: status, isLoading } = useQuery(
|
||||
computed(() => ({
|
||||
...submissionsApiSyncScopesStatusOptions({ path: path.value }),
|
||||
enabled: open.value,
|
||||
refetchInterval: 5000,
|
||||
})),
|
||||
);
|
||||
|
||||
const apibotToken = ref("");
|
||||
const message = ref<{ type: "success" | "info" | "error"; text: string } | null>(null);
|
||||
|
||||
watch(open, (isOpen) => {
|
||||
if (!isOpen) {
|
||||
apibotToken.value = "";
|
||||
message.value = null;
|
||||
}
|
||||
});
|
||||
|
||||
const refreshStatus = () => queryClient.invalidateQueries({ queryKey: statusKey.value });
|
||||
|
||||
const start = useMutation({
|
||||
...submissionsApiSyncScopesMutation(),
|
||||
onSuccess: (data) => {
|
||||
apibotToken.value = "";
|
||||
message.value = data.started
|
||||
? { type: "success", text: t("scopeSync.started") }
|
||||
: { type: "info", text: t("scopeSync.alreadyRunning") };
|
||||
},
|
||||
onError: () => {
|
||||
message.value = { type: "error", text: t("scopeSync.startFailed") };
|
||||
},
|
||||
onSettled: refreshStatus,
|
||||
});
|
||||
|
||||
const stop = useMutation({
|
||||
...submissionsApiStopSyncScopesMutation(),
|
||||
onSuccess: (data) => {
|
||||
message.value = data.stopped
|
||||
? { type: "success", text: t("scopeSync.stopped") }
|
||||
: { type: "info", text: t("scopeSync.notRunning") };
|
||||
},
|
||||
onError: () => {
|
||||
message.value = { type: "error", text: t("scopeSync.stopFailed") };
|
||||
},
|
||||
onSettled: refreshStatus,
|
||||
});
|
||||
|
||||
const busy = computed(() => start.isPending.value || stop.isPending.value);
|
||||
const hasToken = computed(() => status.value?.has_token ?? false);
|
||||
const tokenTyped = computed(() => apibotToken.value.trim() !== "");
|
||||
|
||||
const submit = () => {
|
||||
start.mutate({
|
||||
path: path.value,
|
||||
body: { apibot_token: tokenTyped.value ? apibotToken.value.trim() : null },
|
||||
});
|
||||
};
|
||||
|
||||
const AANDT_STATUSES: Record<string, string> = {
|
||||
"0prep": "prep",
|
||||
"1publ": "published",
|
||||
"3insc": "registrations",
|
||||
"5inscdone": "registrationsDone",
|
||||
"7inprog": "inProgress",
|
||||
"9done": "done",
|
||||
"11reviewed": "reviewed",
|
||||
};
|
||||
|
||||
const aandtStatusLabel = computed(() => {
|
||||
const value = status.value?.aandt_status;
|
||||
if (!value) return t("scopeSync.unknown");
|
||||
return AANDT_STATUSES[value] ? t(`scopeSync.aandtStatus.${AANDT_STATUSES[value]}`) : value;
|
||||
});
|
||||
|
||||
const formatDate = (value?: string | null) =>
|
||||
value ? dayjs(value).format("DD/MM HH:mm:ss") : t("scopeSync.never");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="['modal', { 'modal-open': open }]">
|
||||
<div class="modal-box w-11/12 max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
<h3 class="font-bold text-xl flex items-center gap-2">
|
||||
<i class="mdi mdi-sync"></i>
|
||||
{{ t("scopeSync.title", { game: gameName }) }}
|
||||
</h3>
|
||||
|
||||
<!-- What this actually does -->
|
||||
<div class="mt-4 space-y-3 text-base-content/80">
|
||||
<p>{{ t("scopeSync.intro") }}</p>
|
||||
<ul class="list-disc pl-6 space-y-1">
|
||||
<li>{{ t("scopeSync.whatScopes") }}</li>
|
||||
<li>{{ t("scopeSync.whatPlayers") }}</li>
|
||||
<li>{{ t("scopeSync.whatClose") }}</li>
|
||||
<li>{{ t("scopeSync.whatLink") }}</li>
|
||||
<li>{{ t("scopeSync.whatLive") }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info mt-4 items-start">
|
||||
<i class="mdi mdi-autorenew text-2xl"></i>
|
||||
<div>
|
||||
<h4 class="font-semibold">{{ t("scopeSync.autoTitle") }}</h4>
|
||||
<p class="text-sm">{{ t("scopeSync.autoInterval") }}</p>
|
||||
<p class="text-sm">{{ t("scopeSync.autoStop") }}</p>
|
||||
<p class="text-sm">{{ t("scopeSync.autoErrors") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Current state -->
|
||||
<div class="mt-6">
|
||||
<h4 class="font-semibold mb-2">{{ t("scopeSync.statusTitle") }}</h4>
|
||||
<div v-if="isLoading" class="flex justify-center py-6">
|
||||
<span class="loading loading-spinner"></span>
|
||||
</div>
|
||||
<div v-else-if="status" class="stats stats-vertical md:stats-horizontal shadow w-full">
|
||||
<div class="stat">
|
||||
<div class="stat-title">{{ t("scopeSync.loop") }}</div>
|
||||
<div class="stat-value text-lg flex items-center gap-2">
|
||||
<span :class="['badge', status.running ? 'badge-success' : 'badge-ghost']">
|
||||
{{ status.running ? t("scopeSync.running") : t("scopeSync.notRunningShort") }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="stat-desc">
|
||||
{{ t("scopeSync.nextRun") }}: {{ status.running ? formatDate(status.next_run_at) : "—" }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-title">{{ t("scopeSync.aandt") }}</div>
|
||||
<div class="stat-value text-lg">{{ aandtStatusLabel }}</div>
|
||||
<div class="stat-desc">{{ t("scopeSync.lastRun") }}: {{ formatDate(status.last_run_at) }}</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-title">{{ t("scopeSync.scopes") }}</div>
|
||||
<div class="stat-value text-lg">{{ status.active_scope_count }} / {{ status.scope_count }}</div>
|
||||
<div class="stat-desc">{{ t("scopeSync.activeOfTotal") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="status?.error" class="alert alert-error mt-3">
|
||||
<i class="mdi mdi-alert-circle-outline"></i>
|
||||
<span>{{ t("scopeSync.lastError") }}: {{ status.error }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Token -->
|
||||
<form class="mt-6" @submit.prevent="submit">
|
||||
<label class="font-semibold block mb-1" for="apibot-token">{{ t("scopeSync.tokenTitle") }}</label>
|
||||
<p class="text-sm text-base-content/70 mb-2">
|
||||
{{ hasToken ? t("scopeSync.tokenReplace") : t("scopeSync.tokenMissing") }}
|
||||
</p>
|
||||
<input id="apibot-token" v-model="apibotToken" type="password" autocomplete="off"
|
||||
:placeholder="hasToken ? t('scopeSync.tokenConfigured') : t('scopeSync.tokenPlaceholder')"
|
||||
class="input input-bordered w-full" />
|
||||
|
||||
<div v-if="message" :class="['alert mt-4', `alert-${message.type}`]">{{ message.text }}</div>
|
||||
|
||||
<div class="modal-action flex-wrap">
|
||||
<button type="button" @click="open = false" class="btn">{{ t("common.close") }}</button>
|
||||
<button v-if="status?.running" type="button" :disabled="busy" class="btn btn-error"
|
||||
@click="stop.mutate({ path })">
|
||||
<span v-if="stop.isPending.value" class="loading loading-spinner loading-xs"></span>
|
||||
<i v-else class="mdi mdi-stop"></i>
|
||||
{{ t("scopeSync.stop") }}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary"
|
||||
:disabled="busy || (!hasToken && !tokenTyped) || (status?.running && !tokenTyped)">
|
||||
<span v-if="start.isPending.value" class="loading loading-spinner loading-xs"></span>
|
||||
<i v-else class="mdi mdi-play"></i>
|
||||
{{ status?.running ? t("scopeSync.saveToken") : t("scopeSync.start") }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-backdrop" @click="open = false"></div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user