419 lines
22 KiB
Vue
419 lines
22 KiB
Vue
<script setup lang="ts">
|
|
import dayjs from "dayjs"
|
|
|
|
type InboxDocument = { id: string; name: string; mimeType: string; size: number; receivedAt: string }
|
|
type ScannerConnection = { username: string; active: boolean; password?: string }
|
|
type FilingFolder = { id: string; name: string; parent: string | null; standardFiletype: string | null; standardFiletypeIsOptional: boolean }
|
|
type FilingType = { id: string; name: string }
|
|
|
|
const { $api } = useNuxtApp()
|
|
const auth = useAuthStore()
|
|
const config = useRuntimeConfig()
|
|
const fileActions = useFiles()
|
|
const toast = useToast()
|
|
const documents = ref<InboxDocument[]>([])
|
|
const total = ref(0)
|
|
const page = ref(1)
|
|
const pageSize = 50
|
|
const search = ref("")
|
|
const loading = ref(false)
|
|
const error = ref("")
|
|
const setupOpen = ref(false)
|
|
const setupLoading = ref(false)
|
|
const setupError = ref("")
|
|
const connection = ref<ScannerConnection | null>(null)
|
|
const password = ref("")
|
|
const confirmReset = ref(false)
|
|
const scannerPath = ref("/scanner-inbox/")
|
|
const origin = ref("")
|
|
const previewOpen = ref(false)
|
|
const previewDocument = ref<InboxDocument | null>(null)
|
|
const filingOpen = ref(false)
|
|
const filingDocument = ref<InboxDocument | null>(null)
|
|
const filingLoading = ref(false)
|
|
const filingSaving = ref(false)
|
|
const filingError = ref("")
|
|
const filingFolders = ref<FilingFolder[]>([])
|
|
const filingTypes = ref<FilingType[]>([])
|
|
const filingFolder = ref("root")
|
|
const filingType = ref("")
|
|
const selectedFolder = computed(() => filingFolders.value.find(folder => folder.id === filingFolder.value))
|
|
const fixedType = computed(() => Boolean(selectedFolder.value?.standardFiletype && !selectedFolder.value.standardFiletypeIsOptional))
|
|
const folderOptions = computed(() => {
|
|
const byId = new Map(filingFolders.value.map(folder => [folder.id, folder]))
|
|
const path = (folder: FilingFolder) => {
|
|
const parts = [folder.name]
|
|
const visited = new Set([folder.id])
|
|
let parent = folder.parent ? byId.get(folder.parent) : undefined
|
|
while (parent && !visited.has(parent.id)) {
|
|
parts.unshift(parent.name)
|
|
visited.add(parent.id)
|
|
parent = parent.parent ? byId.get(parent.parent) : undefined
|
|
}
|
|
return parts.join(" / ")
|
|
}
|
|
return [{ id: "root", name: "Dateien (Hauptebene)" }, ...filingFolders.value
|
|
.map(folder => ({ id: folder.id, name: path(folder) })).sort((a, b) => a.name.localeCompare(b.name, "de"))]
|
|
})
|
|
const canFile = computed(() => Boolean(filingDocument.value && !filingLoading.value && !filingSaving.value
|
|
&& (filingFolder.value === "root" || selectedFolder.value)
|
|
&& filingTypes.value.some(type => type.id === filingType.value)
|
|
&& (!fixedType.value || filingType.value === selectedFolder.value?.standardFiletype)))
|
|
const isAdmin = computed(() => Boolean(auth.user?.is_admin))
|
|
const scannerUrl = computed(() => {
|
|
if (!origin.value) return ""
|
|
const base = String(config.public.apiBase || "").replace(/\/+$/, "")
|
|
return new URL(`${base}${scannerPath.value}`, origin.value).href
|
|
})
|
|
let requestVersion = 0
|
|
let setupVersion = 0
|
|
let filingVersion = 0
|
|
let searchTimer: ReturnType<typeof setTimeout> | undefined
|
|
let pollTimer: ReturnType<typeof setTimeout> | undefined
|
|
let disposed = false
|
|
|
|
const load = async (silent = false) => {
|
|
const version = ++requestVersion
|
|
clearTimeout(pollTimer)
|
|
if (!auth.activeTenant) {
|
|
documents.value = []
|
|
total.value = 0
|
|
loading.value = false
|
|
return
|
|
}
|
|
if (!silent) loading.value = true
|
|
try {
|
|
const result = await $api<{ documents: InboxDocument[]; total: number }>("/api/document-inbox", {
|
|
query: { limit: pageSize, offset: (page.value - 1) * pageSize, search: search.value.trim() },
|
|
})
|
|
if (disposed || version !== requestVersion) return
|
|
if (page.value > 1 && (page.value - 1) * pageSize >= result.total) {
|
|
page.value = Math.max(1, Math.ceil(result.total / pageSize))
|
|
return load()
|
|
}
|
|
documents.value = result.documents
|
|
total.value = result.total
|
|
error.value = ""
|
|
pollTimer = setTimeout(() => load(true), 10000)
|
|
} catch (cause: any) {
|
|
if (disposed || version !== requestVersion) return
|
|
error.value = cause?.data?.error || "Die Dokumentenablage konnte nicht geladen werden."
|
|
} finally {
|
|
if (version === requestVersion) loading.value = false
|
|
}
|
|
}
|
|
|
|
const openPreview = (entry: InboxDocument) => {
|
|
previewDocument.value = entry
|
|
previewOpen.value = true
|
|
}
|
|
|
|
const filePreviewDocument = () => {
|
|
const entry = previewDocument.value
|
|
if (!entry) return
|
|
previewOpen.value = false
|
|
openFiling(entry)
|
|
}
|
|
|
|
const openFiling = async (entry: InboxDocument) => {
|
|
filingOpen.value = true
|
|
filingDocument.value = entry
|
|
filingFolder.value = "root"
|
|
filingType.value = ""
|
|
filingFolders.value = []
|
|
filingTypes.value = []
|
|
filingError.value = ""
|
|
filingLoading.value = true
|
|
filingSaving.value = false
|
|
const version = ++filingVersion
|
|
try {
|
|
const result = await $api<{ folders: FilingFolder[]; types: FilingType[] }>("/api/document-inbox/filing-options")
|
|
if (disposed || version !== filingVersion) return
|
|
filingFolders.value = result.folders
|
|
filingTypes.value = result.types
|
|
} catch (cause: any) {
|
|
if (version === filingVersion) filingError.value = cause?.data?.error || cause?.data?.message || "Ordner und Dokumenttypen konnten nicht geladen werden."
|
|
} finally {
|
|
if (version === filingVersion) filingLoading.value = false
|
|
}
|
|
}
|
|
|
|
const fileDocument = async () => {
|
|
if (!canFile.value) return
|
|
const entry = filingDocument.value!
|
|
const version = filingVersion
|
|
const folder = filingFolder.value === "root" ? null : filingFolder.value
|
|
filingSaving.value = true
|
|
filingError.value = ""
|
|
try {
|
|
await $api(`/api/document-inbox/${entry.id}/file`, { method: "POST", body: { folder, type: filingType.value } })
|
|
if (disposed || version !== filingVersion) return
|
|
// Cancel pending polling responses before removing the successfully filed entry.
|
|
requestVersion++
|
|
clearTimeout(pollTimer)
|
|
documents.value = documents.value.filter(document => document.id !== entry.id)
|
|
total.value = Math.max(0, total.value - 1)
|
|
page.value = Math.min(page.value, Math.max(1, Math.ceil(total.value / pageSize)))
|
|
filingOpen.value = false
|
|
toast.add({ title: "Dokument abgelegt", description: `${entry.name} ist unter Dateien verfügbar.`, color: "success" })
|
|
await load(true)
|
|
} catch (cause: any) {
|
|
if (version === filingVersion) filingError.value = cause?.data?.error || cause?.data?.message || "Das Dokument konnte nicht abgelegt werden."
|
|
} finally {
|
|
if (version === filingVersion) filingSaving.value = false
|
|
}
|
|
}
|
|
|
|
const openSetup = async () => {
|
|
setupOpen.value = true
|
|
setupLoading.value = true
|
|
setupError.value = ""
|
|
connection.value = null
|
|
const version = ++setupVersion
|
|
try {
|
|
const result = await $api<{ connection: ScannerConnection | null; path: string }>("/api/document-inbox/scanner")
|
|
if (disposed || version !== setupVersion) return
|
|
connection.value = result.connection
|
|
scannerPath.value = result.path
|
|
} catch (cause: any) {
|
|
if (version === setupVersion) setupError.value = cause?.data?.error || "Der Scanner-Zugang konnte nicht geladen werden."
|
|
} finally {
|
|
if (version === setupVersion) setupLoading.value = false
|
|
}
|
|
}
|
|
|
|
const createCredentials = async () => {
|
|
setupLoading.value = true
|
|
setupError.value = ""
|
|
const version = ++setupVersion
|
|
try {
|
|
const result = await $api<{ connection: ScannerConnection; path: string }>("/api/document-inbox/scanner/credentials", { method: "POST" })
|
|
if (disposed || version !== setupVersion) return
|
|
connection.value = { username: result.connection.username, active: result.connection.active }
|
|
password.value = result.connection.password || ""
|
|
scannerPath.value = result.path
|
|
confirmReset.value = false
|
|
} catch (cause: any) {
|
|
if (version === setupVersion) setupError.value = cause?.data?.error || "Zugangsdaten konnten nicht erstellt werden."
|
|
} finally {
|
|
if (version === setupVersion) setupLoading.value = false
|
|
}
|
|
}
|
|
|
|
const disableScanner = async () => {
|
|
setupLoading.value = true
|
|
const version = ++setupVersion
|
|
try {
|
|
await $api("/api/document-inbox/scanner", { method: "DELETE" })
|
|
if (disposed || version !== setupVersion) return
|
|
if (connection.value) connection.value.active = false
|
|
password.value = ""
|
|
confirmReset.value = false
|
|
} catch (cause: any) {
|
|
if (version === setupVersion) setupError.value = cause?.data?.error || "Der Scanner-Zugang konnte nicht deaktiviert werden."
|
|
} finally {
|
|
if (version === setupVersion) setupLoading.value = false
|
|
}
|
|
}
|
|
|
|
const copy = async (value: string) => {
|
|
try {
|
|
await navigator.clipboard.writeText(value)
|
|
toast.add({ title: "Kopiert", color: "success" })
|
|
} catch {
|
|
toast.add({ title: "Bitte den Wert markieren und manuell kopieren.", color: "warning" })
|
|
}
|
|
}
|
|
const fileAction = async (entry: InboxDocument, download: boolean) => {
|
|
try {
|
|
if (download) await fileActions.downloadFile(entry.id)
|
|
else await fileActions.openFile(entry.id)
|
|
} catch {
|
|
toast.add({ title: "Das Dokument konnte nicht geöffnet werden.", color: "error" })
|
|
}
|
|
}
|
|
const formatSize = (size: number) => size < 1024 * 1024 ? `${Math.ceil(size / 1024)} KB` : `${(size / 1024 / 1024).toLocaleString("de-DE", { maximumFractionDigits: 1 })} MB`
|
|
const changePage = (next: number) => { page.value = next; load() }
|
|
|
|
watch(search, () => {
|
|
// Prevent an older response from briefly replacing results for a new search.
|
|
requestVersion++
|
|
clearTimeout(searchTimer)
|
|
clearTimeout(pollTimer)
|
|
searchTimer = setTimeout(() => { page.value = 1; load() }, 300)
|
|
})
|
|
watch(setupOpen, open => {
|
|
if (!open) { setupVersion++; password.value = ""; confirmReset.value = false }
|
|
})
|
|
watch(previewOpen, open => {
|
|
if (!open) previewDocument.value = null
|
|
})
|
|
watch(filingFolder, () => {
|
|
const defaultType = selectedFolder.value?.standardFiletype
|
|
if (defaultType) filingType.value = defaultType
|
|
})
|
|
watch(filingOpen, open => {
|
|
if (!open) {
|
|
filingVersion++
|
|
filingDocument.value = null
|
|
filingFolders.value = []
|
|
filingTypes.value = []
|
|
filingError.value = ""
|
|
filingSaving.value = false
|
|
filingLoading.value = false
|
|
}
|
|
})
|
|
watch(() => auth.activeTenant, () => {
|
|
requestVersion++
|
|
setupVersion++
|
|
filingVersion++
|
|
previewOpen.value = false
|
|
previewDocument.value = null
|
|
filingOpen.value = false
|
|
clearTimeout(searchTimer)
|
|
clearTimeout(pollTimer)
|
|
documents.value = []
|
|
total.value = 0
|
|
page.value = 1
|
|
error.value = ""
|
|
setupOpen.value = false
|
|
connection.value = null
|
|
password.value = ""
|
|
load()
|
|
})
|
|
onMounted(() => { origin.value = window.location.origin; load() })
|
|
onBeforeUnmount(() => {
|
|
disposed = true
|
|
requestVersion++
|
|
setupVersion++
|
|
filingVersion++
|
|
clearTimeout(searchTimer)
|
|
clearTimeout(pollTimer)
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<UDashboardNavbar title="Dokumentenablage">
|
|
<template #right>
|
|
<UButton v-if="isAdmin && auth.activeTenant" icon="i-heroicons-cog-6-tooth" color="neutral" variant="outline" @click="openSetup">Scanner einrichten</UButton>
|
|
<UButton icon="i-heroicons-arrow-path" color="neutral" variant="ghost" aria-label="Dokumentenablage aktualisieren" :loading="loading" @click="load()" />
|
|
</template>
|
|
</UDashboardNavbar>
|
|
<UDashboardPanelContent class="space-y-6">
|
|
<div class="flex flex-wrap items-end justify-between gap-4">
|
|
<div>
|
|
<h1 class="text-xl font-semibold text-highlighted">Eingegangene Dokumente</h1>
|
|
<p class="mt-1 text-sm text-muted">Prüfe eingegangene Scans und lege sie mit Ordner und Dokumenttyp unter Dateien ab.</p>
|
|
</div>
|
|
<UInput v-model="search" aria-label="Dokumente nach Dateiname suchen" placeholder="Dateiname suchen …" icon="i-heroicons-magnifying-glass" :maxlength="200" class="w-full sm:w-72" />
|
|
</div>
|
|
<UAlert v-if="!auth.activeTenant" title="Bitte einen Mandanten auswählen." color="neutral" variant="subtle" />
|
|
<UAlert v-else-if="error" :title="error" color="error" variant="subtle" />
|
|
<div v-if="loading && !documents.length" class="space-y-3" aria-label="Dokumente werden geladen">
|
|
<USkeleton v-for="index in 4" :key="index" class="h-16 w-full" />
|
|
</div>
|
|
<div v-else-if="!documents.length && !error" class="rounded-xl border border-dashed border-default px-6 py-16 text-center">
|
|
<UIcon name="i-heroicons-inbox-arrow-down" class="mx-auto h-10 w-10 text-muted" />
|
|
<h2 class="mt-3 font-semibold">{{ search ? 'Keine passenden Dokumente' : 'Keine offenen Dokumente' }}</h2>
|
|
<p class="mt-1 text-sm text-muted">{{ search ? 'Versuche einen anderen Dateinamen.' : 'Sobald dein Scanner eine Datei überträgt, erscheint sie hier automatisch.' }}</p>
|
|
</div>
|
|
<div v-else-if="documents.length" class="overflow-x-auto rounded-xl border border-default">
|
|
<table class="w-full text-left text-sm">
|
|
<thead class="border-b border-default bg-elevated/50 text-muted">
|
|
<tr><th scope="col" class="px-4 py-3">Dokument</th><th scope="col" class="px-4 py-3">Eingegangen</th><th scope="col" class="px-4 py-3">Größe</th><th scope="col" class="px-4 py-3 text-right">Aktionen</th></tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-default">
|
|
<tr v-for="entry in documents" :key="entry.id" class="hover:bg-elevated/30">
|
|
<td class="max-w-sm px-4 py-4"><button class="flex items-center gap-3 text-left font-medium text-highlighted hover:text-primary" :aria-label="`${entry.name} als Vorschau anzeigen`" @click="openPreview(entry)"><UIcon name="i-heroicons-document-text" class="h-5 w-5 shrink-0 text-muted" /><span class="break-all">{{ entry.name }}</span></button></td>
|
|
<td class="whitespace-nowrap px-4 py-4 text-muted">{{ dayjs(entry.receivedAt).format('DD.MM.YYYY HH:mm') }}</td>
|
|
<td class="whitespace-nowrap px-4 py-4 text-muted">{{ formatSize(entry.size) }}</td>
|
|
<td class="px-4 py-4"><div class="flex justify-end gap-1"><UButton icon="i-heroicons-folder-arrow-down" variant="soft" :aria-label="`${entry.name} ablegen`" @click="openFiling(entry)">Ablegen</UButton><UButton icon="i-heroicons-eye" color="neutral" variant="ghost" :aria-label="`${entry.name} als Vorschau anzeigen`" @click="openPreview(entry)">Vorschau</UButton><UButton icon="i-heroicons-arrow-down-tray" color="neutral" variant="ghost" :aria-label="`${entry.name} herunterladen`" @click="fileAction(entry, true)" /></div></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div v-if="total > 0" class="flex items-center justify-between gap-3 text-sm text-muted">
|
|
<span>{{ total }} {{ total === 1 ? 'Dokument' : 'Dokumente' }}</span>
|
|
<div v-if="total > pageSize" class="flex items-center gap-3">
|
|
<UButton color="neutral" variant="outline" :disabled="page <= 1 || loading" @click="changePage(page - 1)">Zurück</UButton>
|
|
<span>Seite {{ page }} von {{ Math.ceil(total / pageSize) }}</span>
|
|
<UButton color="neutral" variant="outline" :disabled="page * pageSize >= total || loading" @click="changePage(page + 1)">Weiter</UButton>
|
|
</div>
|
|
</div>
|
|
</UDashboardPanelContent>
|
|
|
|
<UModal v-model:open="previewOpen" :title="previewDocument?.name || 'Dokumentvorschau'" description="Vorschau des eingegangenen Dokuments" :ui="{ content: 'w-[calc(100vw-2rem)] max-w-6xl h-[90dvh]', body: 'min-h-0 flex-1 overflow-hidden p-0 sm:p-0', title: 'break-all pr-8', footer: 'flex-wrap justify-end' }">
|
|
<template #body>
|
|
<AuthenticatedFilePreview
|
|
v-if="previewOpen && previewDocument"
|
|
:key="previewDocument.id"
|
|
:file-id="previewDocument.id"
|
|
:path="previewDocument.name"
|
|
interactive
|
|
/>
|
|
</template>
|
|
<template #footer>
|
|
<template v-if="previewDocument">
|
|
<UButton icon="i-heroicons-arrow-top-right-on-square" color="neutral" variant="ghost" @click="fileAction(previewDocument, false)">In neuem Tab öffnen</UButton>
|
|
<UButton icon="i-heroicons-arrow-down-tray" color="neutral" variant="outline" @click="fileAction(previewDocument, true)">Herunterladen</UButton>
|
|
<UButton v-if="!filingOpen" icon="i-heroicons-folder-arrow-down" @click="filePreviewDocument">In Dateien ablegen</UButton>
|
|
</template>
|
|
</template>
|
|
</UModal>
|
|
|
|
<UModal v-model:open="filingOpen" title="In Dateien ablegen" description="Wähle den Zielordner und klassifiziere das Dokument." :dismissible="!filingSaving" :close="!filingSaving">
|
|
<template #body>
|
|
<form class="space-y-5" @submit.prevent="fileDocument">
|
|
<div v-if="filingDocument" class="flex items-center justify-between gap-3 rounded-lg bg-elevated/50 p-3">
|
|
<span class="break-all text-sm font-medium">{{ filingDocument.name }}</span>
|
|
<UButton icon="i-heroicons-eye" color="neutral" variant="ghost" aria-label="Dokumentvorschau öffnen" @click="openPreview(filingDocument)" />
|
|
</div>
|
|
<UAlert v-if="filingError" :title="filingError" color="error" variant="subtle" />
|
|
<USkeleton v-if="filingLoading" class="h-32 w-full" />
|
|
<template v-else>
|
|
<UFormField label="Zielordner" required>
|
|
<USelectMenu v-model="filingFolder" :items="folderOptions" value-key="id" label-key="name" :search-input="{ placeholder: 'Ordner suchen …' }" :disabled="filingSaving" class="w-full" />
|
|
</UFormField>
|
|
<UFormField label="Dokumenttyp" required :help="fixedType ? 'Dieser Dokumenttyp wird vom Zielordner vorgegeben.' : 'Die Dokumenttypen entsprechen den Dateitypen in FEDEO.'">
|
|
<USelectMenu v-model="filingType" :items="filingTypes" value-key="id" label-key="name" placeholder="Dokumenttyp wählen …" :search-input="{ placeholder: 'Dokumenttyp suchen …' }" :disabled="filingSaving || fixedType" class="w-full" />
|
|
</UFormField>
|
|
<UAlert v-if="!filingTypes.length && !filingError" title="Noch keine Dokumenttypen vorhanden" description="Lege zuerst unter Stammdaten → Dateitypen einen Typ an." color="warning" variant="subtle" />
|
|
<UAlert v-else-if="fixedType && !filingTypes.some(type => type.id === filingType)" title="Der vorgegebene Dokumenttyp ist nicht verfügbar." description="Bitte die Ordnereinstellungen korrigieren oder einen anderen Zielordner wählen." color="warning" variant="subtle" />
|
|
</template>
|
|
<p class="text-sm text-muted">Nach dem Ablegen findest du das Dokument unter Dateien. Es verschwindet aus diesem Eingang.</p>
|
|
<div class="flex justify-end gap-2">
|
|
<UButton color="neutral" variant="ghost" :disabled="filingSaving" @click="filingOpen = false">Abbrechen</UButton>
|
|
<UButton type="submit" icon="i-heroicons-folder-arrow-down" :loading="filingSaving" :disabled="!canFile">In Dateien ablegen</UButton>
|
|
</div>
|
|
</form>
|
|
</template>
|
|
</UModal>
|
|
|
|
<UModal v-model:open="setupOpen" title="Scanner einrichten" description="Verbinde deinen Epson-Scanner mit der Dokumentenablage dieses Mandanten.">
|
|
<template #body>
|
|
<div class="space-y-5">
|
|
<UAlert v-if="setupError" :title="setupError" color="error" variant="subtle" />
|
|
<p class="text-sm text-muted">Am Scanner „Netzwerkordner/FTP“ und als Kommunikationsmodus „WebDAV (HTTPS)“ wählen. In Web Config heißt das Ziel gegebenenfalls „SharePoint (WebDAV)“.</p>
|
|
<UFormField label="Zieladresse">
|
|
<div class="flex gap-2"><UInput :model-value="scannerUrl" readonly class="flex-1" /><UButton icon="i-heroicons-clipboard" color="neutral" variant="outline" aria-label="Zieladresse kopieren" @click="copy(scannerUrl)" /></div>
|
|
</UFormField>
|
|
<template v-if="connection">
|
|
<UBadge :color="connection.active ? 'success' : 'neutral'" variant="subtle">{{ connection.active ? 'Scanner-Zugang aktiv' : 'Scanner-Zugang deaktiviert' }}</UBadge>
|
|
<UFormField label="Benutzername"><div class="flex gap-2"><UInput :model-value="connection.username" readonly class="flex-1" /><UButton icon="i-heroicons-clipboard" color="neutral" variant="outline" aria-label="Benutzername kopieren" @click="copy(connection.username)" /></div></UFormField>
|
|
<UFormField v-if="password" label="Passwort" help="Wird nur jetzt angezeigt. Bitte vor dem Schließen im Scanner hinterlegen."><div class="flex gap-2"><UInput :model-value="password" readonly class="flex-1 font-mono" autocomplete="off" /><UButton icon="i-heroicons-clipboard" color="neutral" variant="outline" aria-label="Passwort kopieren" @click="copy(password)" /></div></UFormField>
|
|
</template>
|
|
<p class="text-sm text-muted">PDF und Duplex einstellen und das Ziel als „FEDEO“ speichern. Jede Datei bleibt ein Dokument, auch bei mehreren Seiten. Dateinamen mit Datum, Uhrzeit oder laufender Nummer verwenden. Maximal 100 MB pro Datei.</p>
|
|
<div v-if="confirmReset" class="space-y-3 rounded-lg border border-warning/30 bg-warning/10 p-4">
|
|
<p class="text-sm">Das bisherige Passwort wird ungültig. Alle Scanner mit diesem Zugang benötigen anschließend das neue Passwort.</p>
|
|
<div class="flex gap-2"><UButton :loading="setupLoading" @click="createCredentials">Neues Passwort erstellen</UButton><UButton color="neutral" variant="ghost" :disabled="setupLoading" @click="confirmReset = false">Abbrechen</UButton></div>
|
|
</div>
|
|
<div v-else class="flex flex-wrap gap-2">
|
|
<UButton v-if="!connection" :loading="setupLoading" :disabled="Boolean(setupError)" @click="createCredentials">Scanner-Zugang erstellen</UButton>
|
|
<UButton v-else color="neutral" variant="outline" :disabled="setupLoading" @click="confirmReset = true">{{ connection.active ? 'Neues Passwort erstellen' : 'Mit neuem Passwort aktivieren' }}</UButton>
|
|
<UButton v-if="connection?.active" color="error" variant="ghost" :loading="setupLoading" @click="disableScanner">Zugang deaktivieren</UButton>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</UModal>
|
|
</template>
|