206 lines
8.1 KiB
Vue
206 lines
8.1 KiB
Vue
<script setup lang="ts">
|
|
const route = useRoute();
|
|
const pushApi = usePushApi();
|
|
const toast = useToast();
|
|
const id = route.params.id as string;
|
|
const testSubmitting = ref(false);
|
|
const selectedDeviceIds = ref<string[]>([]);
|
|
const testForm = reactive({
|
|
title: "FEDEO Push-Test",
|
|
body: "Diese Testnachricht wurde über das Push-Admin-Dashboard gesendet.",
|
|
});
|
|
|
|
const { data: instance, refresh: refreshInstance } = await useAsyncData(`instance-${id}`, () => pushApi.request<Record<string, any>>(`/admin/instances/${id}`), { immediate: false });
|
|
const { data: devices, refresh: refreshDevices } = await useAsyncData(`devices-${id}`, () => pushApi.request<Record<string, any>[]>(`/admin/instances/${id}/devices`), { default: () => [], immediate: false });
|
|
const { data: jobs, refresh: refreshJobs } = await useAsyncData(`jobs-${id}`, () => pushApi.request<Record<string, any>[]>(`/admin/instances/${id}/jobs`), { default: () => [], immediate: false });
|
|
const activeDevices = computed(() => (devices.value || []).filter((device) => device.status === "active"));
|
|
|
|
onMounted(async () => {
|
|
pushApi.hydrateToken();
|
|
if (pushApi.token.value) {
|
|
await refreshAll();
|
|
}
|
|
});
|
|
|
|
async function refreshAll() {
|
|
await Promise.all([refreshInstance(), refreshDevices(), refreshJobs()]);
|
|
}
|
|
|
|
function toggleDevice(centralDeviceId: string, checked: boolean) {
|
|
if (checked) {
|
|
selectedDeviceIds.value = [...new Set([...selectedDeviceIds.value, centralDeviceId])];
|
|
} else {
|
|
selectedDeviceIds.value = selectedDeviceIds.value.filter((id) => id !== centralDeviceId);
|
|
}
|
|
}
|
|
|
|
function selectAllActiveDevices() {
|
|
selectedDeviceIds.value = activeDevices.value.map((device) => device.centralDeviceId);
|
|
}
|
|
|
|
async function sendTestPush() {
|
|
testSubmitting.value = true;
|
|
try {
|
|
const result = await pushApi.request<Record<string, any>>(`/admin/instances/${id}/test-push`, {
|
|
method: "POST",
|
|
body: {
|
|
title: testForm.title,
|
|
body: testForm.body,
|
|
deviceIds: selectedDeviceIds.value.length ? selectedDeviceIds.value : undefined,
|
|
priority: "high",
|
|
},
|
|
});
|
|
await Promise.all([refreshInstance(), refreshJobs(), refreshDevices()]);
|
|
toast.add({
|
|
title: "Test-Push gesendet",
|
|
description: `${result.sent ?? 0} gesendet, ${result.failed ?? 0} fehlgeschlagen`,
|
|
color: result.failed ? "warning" : "success",
|
|
});
|
|
} catch (error: any) {
|
|
toast.add({
|
|
title: "Test-Push fehlgeschlagen",
|
|
description: error?.data?.message || error?.message || "Die Testnachricht konnte nicht gesendet werden.",
|
|
color: "error",
|
|
});
|
|
} finally {
|
|
testSubmitting.value = false;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<AdminShell>
|
|
<TokenGate>
|
|
<div class="space-y-6">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<UButton to="/instances" icon="i-lucide-arrow-left" variant="ghost" size="sm">Zurück</UButton>
|
|
<h2 class="mt-2 text-2xl font-bold">{{ instance?.name || "Instanz" }}</h2>
|
|
<p class="text-sm text-gray-500">{{ instance?.instanceId }}</p>
|
|
</div>
|
|
<UButton icon="i-lucide-refresh-cw" variant="soft" @click="refreshAll">Aktualisieren</UButton>
|
|
</div>
|
|
|
|
<div class="grid gap-4 md:grid-cols-3">
|
|
<UCard>
|
|
<p class="text-sm text-gray-500">Geräte</p>
|
|
<p class="mt-2 text-3xl font-bold">{{ instance?.deviceCount ?? 0 }}</p>
|
|
</UCard>
|
|
<UCard>
|
|
<p class="text-sm text-gray-500">Zustelljobs</p>
|
|
<p class="mt-2 text-3xl font-bold">{{ instance?.jobCount ?? 0 }}</p>
|
|
</UCard>
|
|
<UCard>
|
|
<p class="text-sm text-gray-500">Status</p>
|
|
<p class="mt-2 text-xl font-bold">{{ instance?.status }}</p>
|
|
</UCard>
|
|
</div>
|
|
|
|
<UCard>
|
|
<template #header>
|
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
|
<h3 class="font-bold">Testnachricht</h3>
|
|
<UBadge color="neutral" variant="soft">{{ selectedDeviceIds.length || activeDevices.length }} Zielgeräte</UBadge>
|
|
</div>
|
|
</template>
|
|
<div class="grid gap-4 lg:grid-cols-[1fr_240px]">
|
|
<div class="space-y-4">
|
|
<UFormField label="Titel">
|
|
<UInput v-model="testForm.title" maxlength="120" />
|
|
</UFormField>
|
|
<UFormField label="Nachricht">
|
|
<UTextarea v-model="testForm.body" :rows="3" maxlength="240" />
|
|
</UFormField>
|
|
<div class="flex flex-wrap gap-2">
|
|
<UButton
|
|
icon="i-lucide-send"
|
|
:loading="testSubmitting"
|
|
:disabled="!activeDevices.length || !testForm.title.trim() || !testForm.body.trim()"
|
|
@click="sendTestPush"
|
|
>
|
|
Test senden
|
|
</UButton>
|
|
<UButton icon="i-lucide-check-check" variant="soft" :disabled="!activeDevices.length" @click="selectAllActiveDevices">
|
|
Alle aktiven auswählen
|
|
</UButton>
|
|
<UButton icon="i-lucide-x" variant="ghost" :disabled="!selectedDeviceIds.length" @click="selectedDeviceIds = []">
|
|
Auswahl leeren
|
|
</UButton>
|
|
</div>
|
|
</div>
|
|
<div class="rounded-lg border border-gray-200 p-3">
|
|
<p class="mb-3 text-sm font-medium text-gray-700">Zielgeräte</p>
|
|
<div class="max-h-48 space-y-2 overflow-auto">
|
|
<label v-for="device in activeDevices" :key="device.id" class="flex cursor-pointer items-center gap-2 text-sm">
|
|
<UCheckbox
|
|
:model-value="selectedDeviceIds.includes(device.centralDeviceId)"
|
|
@update:model-value="toggleDevice(device.centralDeviceId, Boolean($event))"
|
|
/>
|
|
<span class="min-w-0 truncate font-mono">{{ device.centralDeviceId }}</span>
|
|
</label>
|
|
<p v-if="!activeDevices.length" class="text-sm text-gray-500">Keine aktiven Geräte vorhanden.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</UCard>
|
|
|
|
<UCard>
|
|
<template #header>
|
|
<h3 class="font-bold">Geräte</h3>
|
|
</template>
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full text-left text-sm">
|
|
<thead class="text-gray-500">
|
|
<tr>
|
|
<th class="py-2">Central ID</th>
|
|
<th>Plattform</th>
|
|
<th>Status</th>
|
|
<th>Zuletzt gesehen</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="device in devices" :key="device.id" class="border-t border-gray-100">
|
|
<td class="py-2 font-mono">{{ device.centralDeviceId }}</td>
|
|
<td>{{ device.platform }}</td>
|
|
<td>{{ device.status }}</td>
|
|
<td>{{ new Date(device.lastSeenAt).toLocaleString() }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</UCard>
|
|
|
|
<UCard>
|
|
<template #header>
|
|
<h3 class="font-bold">Letzte Zustelljobs</h3>
|
|
</template>
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full text-left text-sm">
|
|
<thead class="text-gray-500">
|
|
<tr>
|
|
<th class="py-2">Job</th>
|
|
<th>Status</th>
|
|
<th>Angenommen</th>
|
|
<th>Gesendet</th>
|
|
<th>Fehler</th>
|
|
<th>Erstellt</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="job in jobs" :key="job.id" class="border-t border-gray-100">
|
|
<td class="py-2 font-mono">{{ job.deliveryJobId }}</td>
|
|
<td>{{ job.status }}</td>
|
|
<td>{{ job.acceptedCount }}</td>
|
|
<td>{{ job.sentCount }}</td>
|
|
<td>{{ job.failedCount }}</td>
|
|
<td>{{ new Date(job.createdAt).toLocaleString() }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</UCard>
|
|
</div>
|
|
</TokenGate>
|
|
</AdminShell>
|
|
</template>
|