Scan aus der Dateienseite starten
This commit is contained in:
@@ -44,6 +44,20 @@ const pickFileTargets = (target: unknown) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const readFileFolder = (target: unknown) => {
|
||||||
|
if (!target || typeof target !== "object" || Array.isArray(target)) return null
|
||||||
|
|
||||||
|
const folder = (target as Record<string, any>).folder
|
||||||
|
return typeof folder === "string" && folder.trim() ? folder.trim() : null
|
||||||
|
}
|
||||||
|
|
||||||
|
const readFileType = (target: unknown) => {
|
||||||
|
if (!target || typeof target !== "object" || Array.isArray(target)) return null
|
||||||
|
|
||||||
|
const type = (target as Record<string, any>).type
|
||||||
|
return typeof type === "string" && type.trim() ? type.trim() : null
|
||||||
|
}
|
||||||
|
|
||||||
export default async function instanceAgentGatewayRoutes(server: FastifyInstance) {
|
export default async function instanceAgentGatewayRoutes(server: FastifyInstance) {
|
||||||
await server.register(multipart, {
|
await server.register(multipart, {
|
||||||
limits: { fileSize: 100 * 1024 * 1024 },
|
limits: { fileSize: 100 * 1024 * 1024 },
|
||||||
@@ -206,8 +220,8 @@ export default async function instanceAgentGatewayRoutes(server: FastifyInstance
|
|||||||
content: fileBuffer,
|
content: fileBuffer,
|
||||||
contentType: data.mimetype || "application/pdf",
|
contentType: data.mimetype || "application/pdf",
|
||||||
},
|
},
|
||||||
null,
|
readFileFolder(job.target),
|
||||||
null,
|
readFileType(job.target),
|
||||||
{
|
{
|
||||||
...pickFileTargets(job.target),
|
...pickFileTargets(job.target),
|
||||||
createdBy: job.requestedBy,
|
createdBy: job.requestedBy,
|
||||||
|
|||||||
297
frontend/components/FileScanModal.vue
Normal file
297
frontend/components/FileScanModal.vue
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
scanData: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({
|
||||||
|
folder: null,
|
||||||
|
type: null,
|
||||||
|
typeEnabled: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(["scanFinished"])
|
||||||
|
|
||||||
|
const modal = useModal()
|
||||||
|
const toast = useToast()
|
||||||
|
const { $api } = useNuxtApp()
|
||||||
|
|
||||||
|
const loadingAgents = ref(true)
|
||||||
|
const scanInProgress = ref(false)
|
||||||
|
const agents = ref([])
|
||||||
|
const selectedAgentId = ref(null)
|
||||||
|
const currentJob = ref(null)
|
||||||
|
const statusMessage = ref("")
|
||||||
|
|
||||||
|
const scanForm = reactive({
|
||||||
|
filename: `scan-${new Date().toISOString().slice(0, 10)}.pdf`,
|
||||||
|
scannerName: "",
|
||||||
|
format: "pdf",
|
||||||
|
resolution: 300,
|
||||||
|
mode: "Color",
|
||||||
|
source: "ADF Duplex"
|
||||||
|
})
|
||||||
|
|
||||||
|
const activeAgents = computed(() =>
|
||||||
|
agents.value.filter((agent) => agent.active && agent.capabilities?.scan)
|
||||||
|
)
|
||||||
|
|
||||||
|
const selectedAgent = computed(() =>
|
||||||
|
agents.value.find((agent) => agent.id === selectedAgentId.value) || null
|
||||||
|
)
|
||||||
|
|
||||||
|
const scannerOptions = computed(() =>
|
||||||
|
(selectedAgent.value?.scannerNames || []).map((scanner) => ({
|
||||||
|
label: scanner,
|
||||||
|
value: scanner
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
const isOnline = (agent) => {
|
||||||
|
if (!agent.lastSeenAt) return false
|
||||||
|
return Date.now() - new Date(agent.lastSeenAt).getTime() < 2 * 60 * 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyAgentDefaults = (agent) => {
|
||||||
|
if (!agent) return
|
||||||
|
|
||||||
|
scanForm.scannerName = agent.preferredScannerName || agent.scannerNames?.[0] || ""
|
||||||
|
scanForm.format = agent.scanDefaults?.format || "pdf"
|
||||||
|
scanForm.resolution = Number(agent.scanDefaults?.resolution || 300)
|
||||||
|
scanForm.mode = agent.scanDefaults?.mode || "Color"
|
||||||
|
scanForm.source = agent.scanDefaults?.source || "ADF Duplex"
|
||||||
|
|
||||||
|
if (!scanForm.filename || scanForm.filename.startsWith("scan-")) {
|
||||||
|
scanForm.filename = `scan-${new Date().toISOString().slice(0, 10)}.${scanForm.format || "pdf"}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(selectedAgent, (agent) => applyAgentDefaults(agent))
|
||||||
|
|
||||||
|
const loadAgents = async () => {
|
||||||
|
loadingAgents.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await $api("/api/instance-agents")
|
||||||
|
agents.value = response?.agents || []
|
||||||
|
|
||||||
|
const firstOnlineAgent = activeAgents.value.find(isOnline)
|
||||||
|
selectedAgentId.value = firstOnlineAgent?.id || activeAgents.value[0]?.id || null
|
||||||
|
applyAgentDefaults(selectedAgent.value)
|
||||||
|
} catch (err) {
|
||||||
|
toast.add({
|
||||||
|
title: "Scanner konnten nicht geladen werden",
|
||||||
|
description: err?.data?.error || err?.message || "Unbekannter Fehler",
|
||||||
|
color: "error"
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
loadingAgents.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const waitForJob = async (jobId) => {
|
||||||
|
for (let i = 0; i < 90; i++) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||||||
|
|
||||||
|
const response = await $api(`/api/scan-jobs/${jobId}`)
|
||||||
|
const job = response?.job
|
||||||
|
currentJob.value = job
|
||||||
|
|
||||||
|
if (job?.status === "completed") return job
|
||||||
|
if (job?.status === "failed" || job?.status === "canceled") {
|
||||||
|
throw new Error(job.agentMessage || "Scan-Auftrag wurde abgebrochen.")
|
||||||
|
}
|
||||||
|
|
||||||
|
statusMessage.value = job?.status === "running"
|
||||||
|
? "Scanner arbeitet..."
|
||||||
|
: "Warte auf den Agenten..."
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Der Scan-Auftrag läuft länger als erwartet.")
|
||||||
|
}
|
||||||
|
|
||||||
|
const startScan = async () => {
|
||||||
|
if (!selectedAgentId.value) return
|
||||||
|
|
||||||
|
scanInProgress.value = true
|
||||||
|
statusMessage.value = "Scan-Auftrag wird erstellt..."
|
||||||
|
currentJob.value = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await $api("/api/scan-jobs", {
|
||||||
|
method: "POST",
|
||||||
|
body: {
|
||||||
|
agentId: selectedAgentId.value,
|
||||||
|
scannerName: scanForm.scannerName || null,
|
||||||
|
requestedFilename: scanForm.filename || null,
|
||||||
|
settings: {
|
||||||
|
format: scanForm.format || "pdf",
|
||||||
|
resolution: Number(scanForm.resolution || 300),
|
||||||
|
mode: scanForm.mode || "Color",
|
||||||
|
source: scanForm.source || null
|
||||||
|
},
|
||||||
|
target: {
|
||||||
|
folder: props.scanData.folder || null,
|
||||||
|
type: props.scanData.type || null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
currentJob.value = response?.job
|
||||||
|
if (!response?.job?.id) {
|
||||||
|
throw new Error("FEDEO hat keinen Scan-Auftrag zurückgegeben.")
|
||||||
|
}
|
||||||
|
|
||||||
|
statusMessage.value = "Warte auf den Agenten..."
|
||||||
|
|
||||||
|
await waitForJob(response.job.id)
|
||||||
|
|
||||||
|
toast.add({
|
||||||
|
title: "Scan gespeichert",
|
||||||
|
description: props.scanData.folder ? "Die Datei wurde im aktuellen Ordner abgelegt." : "Die Datei wurde in Dateien abgelegt.",
|
||||||
|
color: "success"
|
||||||
|
})
|
||||||
|
|
||||||
|
emit("scanFinished")
|
||||||
|
modal.close()
|
||||||
|
} catch (err) {
|
||||||
|
toast.add({
|
||||||
|
title: "Scan fehlgeschlagen",
|
||||||
|
description: err?.data?.error || err?.message || "Unbekannter Fehler",
|
||||||
|
color: "error"
|
||||||
|
})
|
||||||
|
statusMessage.value = err?.message || "Scan fehlgeschlagen"
|
||||||
|
} finally {
|
||||||
|
scanInProgress.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadAgents()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UModal>
|
||||||
|
<template #content>
|
||||||
|
<UCard class="shadow-xl ring-1 ring-black/5">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between gap-4">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10 text-primary ring-1 ring-primary/15">
|
||||||
|
<UIcon name="i-heroicons-document-magnifying-glass" class="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-base font-semibold text-highlighted">Dokument scannen</h3>
|
||||||
|
<p class="text-xs text-muted">Der Scan wird im aktuellen Ordner gespeichert.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<UButton
|
||||||
|
color="neutral"
|
||||||
|
variant="ghost"
|
||||||
|
icon="i-heroicons-x-mark-20-solid"
|
||||||
|
:disabled="scanInProgress"
|
||||||
|
@click="modal.close()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<UAlert
|
||||||
|
v-if="!loadingAgents && activeAgents.length === 0"
|
||||||
|
color="warning"
|
||||||
|
variant="soft"
|
||||||
|
icon="i-heroicons-exclamation-triangle"
|
||||||
|
title="Kein aktiver Scan-Agent"
|
||||||
|
description="Lege in der Administration einen Scanner-Agenten an und starte den lokalen Agenten."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UFormField label="Scanner-Agent">
|
||||||
|
<USelectMenu
|
||||||
|
v-model="selectedAgentId"
|
||||||
|
:items="activeAgents.map((agent) => ({ label: agent.name, value: agent.id, suffix: isOnline(agent) ? 'Online' : 'Offline' }))"
|
||||||
|
value-key="value"
|
||||||
|
label-key="label"
|
||||||
|
placeholder="Agent wählen"
|
||||||
|
:loading="loadingAgents"
|
||||||
|
class="w-full"
|
||||||
|
:disabled="scanInProgress"
|
||||||
|
/>
|
||||||
|
</UFormField>
|
||||||
|
|
||||||
|
<UFormField label="Scanner">
|
||||||
|
<USelectMenu
|
||||||
|
v-if="scannerOptions.length"
|
||||||
|
v-model="scanForm.scannerName"
|
||||||
|
:items="scannerOptions"
|
||||||
|
value-key="value"
|
||||||
|
label-key="label"
|
||||||
|
placeholder="Standard-Scanner"
|
||||||
|
class="w-full"
|
||||||
|
:disabled="scanInProgress"
|
||||||
|
/>
|
||||||
|
<UInput
|
||||||
|
v-else
|
||||||
|
v-model="scanForm.scannerName"
|
||||||
|
placeholder="Standard-Scanner"
|
||||||
|
:disabled="scanInProgress"
|
||||||
|
/>
|
||||||
|
</UFormField>
|
||||||
|
|
||||||
|
<UFormField label="Dateiname">
|
||||||
|
<UInput v-model="scanForm.filename" :disabled="scanInProgress" />
|
||||||
|
</UFormField>
|
||||||
|
|
||||||
|
<div class="grid gap-3 sm:grid-cols-2">
|
||||||
|
<UFormField label="Quelle">
|
||||||
|
<UInput v-model="scanForm.source" placeholder="ADF Duplex" :disabled="scanInProgress" />
|
||||||
|
</UFormField>
|
||||||
|
<UFormField label="Auflösung">
|
||||||
|
<UInput v-model.number="scanForm.resolution" type="number" min="75" step="25" :disabled="scanInProgress" />
|
||||||
|
</UFormField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-3 sm:grid-cols-2">
|
||||||
|
<UFormField label="Modus">
|
||||||
|
<UInput v-model="scanForm.mode" placeholder="Color" :disabled="scanInProgress" />
|
||||||
|
</UFormField>
|
||||||
|
<UFormField label="Format">
|
||||||
|
<USelectMenu
|
||||||
|
v-model="scanForm.format"
|
||||||
|
:items="[
|
||||||
|
{ label: 'PDF', value: 'pdf' },
|
||||||
|
{ label: 'PNG', value: 'png' },
|
||||||
|
{ label: 'TIFF', value: 'tiff' }
|
||||||
|
]"
|
||||||
|
value-key="value"
|
||||||
|
label-key="label"
|
||||||
|
:disabled="scanInProgress"
|
||||||
|
/>
|
||||||
|
</UFormField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UAlert
|
||||||
|
v-if="statusMessage"
|
||||||
|
color="info"
|
||||||
|
variant="soft"
|
||||||
|
icon="i-heroicons-clock"
|
||||||
|
:title="statusMessage"
|
||||||
|
:description="currentJob?.status ? `Status: ${currentJob.status}` : undefined"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<UButton color="neutral" variant="ghost" :disabled="scanInProgress" @click="modal.close()">Abbrechen</UButton>
|
||||||
|
<UButton
|
||||||
|
icon="i-heroicons-document-magnifying-glass"
|
||||||
|
:loading="scanInProgress"
|
||||||
|
:disabled="!selectedAgentId || loadingAgents"
|
||||||
|
@click="startScan"
|
||||||
|
>
|
||||||
|
Scannen
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UCard>
|
||||||
|
</template>
|
||||||
|
</UModal>
|
||||||
|
</template>
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
import {ref, computed, watch, onMounted, onUnmounted} from 'vue';
|
import {ref, computed, watch, onMounted, onUnmounted} from 'vue';
|
||||||
import DocumentDisplayModal from "~/components/DocumentDisplayModal.vue";
|
import DocumentDisplayModal from "~/components/DocumentDisplayModal.vue";
|
||||||
import DocumentUploadModal from "~/components/DocumentUploadModal.vue";
|
import DocumentUploadModal from "~/components/DocumentUploadModal.vue";
|
||||||
|
import FileScanModal from "~/components/FileScanModal.vue";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
// --- Services & Stores ---
|
// --- Services & Stores ---
|
||||||
@@ -329,6 +330,17 @@ const showFile = (fileId) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openScanModal = () => {
|
||||||
|
modal.open(FileScanModal, {
|
||||||
|
scanData: {
|
||||||
|
folder: currentFolder.value?.id,
|
||||||
|
type: currentFolder.value?.standardFiletype,
|
||||||
|
typeEnabled: currentFolder.value?.standardFiletype ? currentFolder.value?.standardFiletypeIsOptional : true
|
||||||
|
},
|
||||||
|
onScanFinished: setupPage
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const isDialogOpen = computed(() => createFolderModalOpen.value || renameModalOpen.value)
|
const isDialogOpen = computed(() => createFolderModalOpen.value || renameModalOpen.value)
|
||||||
|
|
||||||
defineShortcuts({
|
defineShortcuts({
|
||||||
@@ -412,6 +424,9 @@ const syncdokubox = async () => {
|
|||||||
@click="modal.open(DocumentUploadModal, { fileData: { folder: currentFolder?.id,type: currentFolder?.standardFiletype, typeEnabled: currentFolder?.standardFiletype ? currentFolder?.standardFiletypeIsOptional : true }, onUploadFinished: setupPage })">
|
@click="modal.open(DocumentUploadModal, { fileData: { folder: currentFolder?.id,type: currentFolder?.standardFiletype, typeEnabled: currentFolder?.standardFiletype ? currentFolder?.standardFiletypeIsOptional : true }, onUploadFinished: setupPage })">
|
||||||
Datei
|
Datei
|
||||||
</UButton>
|
</UButton>
|
||||||
|
<UButton icon="i-heroicons-document-magnifying-glass" color="neutral" @click="openScanModal">
|
||||||
|
Scan
|
||||||
|
</UButton>
|
||||||
<UButton icon="i-heroicons-folder-plus" color="white" @click="createFolderModalOpen = true">Ordner</UButton>
|
<UButton icon="i-heroicons-folder-plus" color="white" @click="createFolderModalOpen = true">Ordner</UButton>
|
||||||
</UButtonGroup>
|
</UButtonGroup>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user