diff --git a/components/LabelPrintModal.vue b/components/LabelPrintModal.vue
new file mode 100644
index 0000000..e660a6d
--- /dev/null
+++ b/components/LabelPrintModal.vue
@@ -0,0 +1,130 @@
+
+
+
+
+
+
+
+
+
Label drucken
+
+
+
+
+
+
![Label Preview]()
+
+
+ Kein Drucker verbunden
+
+
+
+
+
+
+
+
+
+
+ Abbrechen
+
+
+ Drucken
+ ⌘
+ P
+
+
+
+
+
+
+
diff --git a/components/LabelPrinterButton.vue b/components/LabelPrinterButton.vue
new file mode 100644
index 0000000..2a29fee
--- /dev/null
+++ b/components/LabelPrinterButton.vue
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
Drucker Informationen
+
+
+
+ Seriennummer: {{labelPrinter.info.serial}}
+ MAC: {{labelPrinter.info.mac}}
+ Modell: {{labelPrinter.info.modelId}}
+ Charge: {{labelPrinter.info.charge}}
+ Hardware Version: {{labelPrinter.info.hardwareVersion}}
+ Software Version: {{labelPrinter.info.softwareVersion}}
+
+
+
+
+ Drucker verbunden
+ Drucker verbinden
+
+
+
+
\ No newline at end of file
diff --git a/components/MainNav.vue b/components/MainNav.vue
index dc00730..0307b41 100644
--- a/components/MainNav.vue
+++ b/components/MainNav.vue
@@ -339,8 +339,7 @@ const links = computed(() => {
},{
label: "Export",
to: "/export",
- icon: "i-heroicons-clipboard-document-list",
- disabled: true
+ icon: "i-heroicons-clipboard-document-list"
}
]
}
diff --git a/components/PageLeaveGuard.vue b/components/PageLeaveGuard.vue
new file mode 100644
index 0000000..af9a563
--- /dev/null
+++ b/components/PageLeaveGuard.vue
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
+
+
+
+
+ Du hast ungespeicherte Änderungen. Diese gehen verloren, wenn du die Seite verlässt.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/components/PublicDynamicForm.vue b/components/PublicDynamicForm.vue
new file mode 100644
index 0000000..a3cdd07
--- /dev/null
+++ b/components/PublicDynamicForm.vue
@@ -0,0 +1,201 @@
+
+
+
+
+
+
+
{{ config?.ui?.title || 'Erfassung' }}
+
{{ config?.ui?.description }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ form.startDate = new Date(e.target.value)"
+ />
+
+
+ form.endDate = new Date(e.target.value)"
+ />
+
+
+
+
+
+
+ Liter
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/components/displayBankaccounts.vue b/components/displayBankaccounts.vue
index cb4ac08..07978a7 100644
--- a/components/displayBankaccounts.vue
+++ b/components/displayBankaccounts.vue
@@ -37,7 +37,7 @@ const calculateOpenSum = (statement) => {
| {{ account.name }}: |
- {{dayjs(account.synced_at).format("DD.MM.YY HH:mm")}}
+ {{dayjs(account.syncedAt).format("DD.MM.YY HH:mm")}}
|
{{useCurrency(account.balance)}}
diff --git a/components/nimbot.vue b/components/nimbot.vue
new file mode 100644
index 0000000..7ceb36a
--- /dev/null
+++ b/components/nimbot.vue
@@ -0,0 +1,60 @@
+
+
+
+
+
+ Connect S
+ Connect B
+ Disconnect
+ Print
+
+
+ {{labelPrinter.info}}
+
+ {{labelPrinter.printProgress}}
+
+
+ mm
+
+
+ mm
+
+
+
+
+
+
diff --git a/layouts/default.vue b/layouts/default.vue
index e994ab7..4993db6 100644
--- a/layouts/default.vue
+++ b/layouts/default.vue
@@ -6,6 +6,7 @@ import dayjs from "dayjs";
import {useCapacitor} from "../composables/useCapacitor.js";
import GlobalMessages from "~/components/GlobalMessages.vue";
import TenantDropdown from "~/components/TenantDropdown.vue";
+import LabelPrinterButton from "~/components/LabelPrinterButton.vue";
const dataStore = useDataStore()
const colorMode = useColorMode()
@@ -14,6 +15,7 @@ const router = useRouter()
const route = useRoute()
const auth = useAuthStore()
+const labelPrinter = useLabelPrinterStore()
const month = dayjs().format("MM")
@@ -240,13 +242,16 @@ const footerLinks = [
-
+
+
+
+
+
-
diff --git a/pages/accounts/index.vue b/pages/accounts/index.vue
index 3472214..1593e1f 100644
--- a/pages/accounts/index.vue
+++ b/pages/accounts/index.vue
@@ -26,13 +26,13 @@ const setupPage = async () => {
incominginvoices.value = (await useEntities("incominginvoices").select("*, vendor(*)"))
items.value = await Promise.all(items.value.map(async (i) => {
- let renderedAllocationsTemp = await renderedAllocations(i.id)
- let saldo = getSaldo(renderedAllocationsTemp)
+ // let renderedAllocationsTemp = await renderedAllocations(i.id)
+ // let saldo = getSaldo(renderedAllocationsTemp)
return {
...i,
- saldo: saldo,
- allocations: renderedAllocationsTemp.length,
+ // saldo: saldo,
+ // allocations: renderedAllocationsTemp.length,
}
}))
@@ -103,13 +103,13 @@ const templateColumns = [
},{
key: "label",
label: "Name"
- },{
+ },/*{
key: "allocations",
label: "Buchungen"
},{
key: "saldo",
label: "Saldo"
- }, {
+ },*/ {
key: "description",
label: "Beschreibung"
},
@@ -166,7 +166,7 @@ setupPage()
-
Spalten
-
+ -->
-
import dayjs from "dayjs";
+// Zugriff auf $api und Toast Notification
+const { $api } = useNuxtApp()
+const toast = useToast()
+
defineShortcuts({
'/': () => {
- //console.log(searchinput)
- //searchinput.value.focus()
document.getElementById("searchinput").focus()
}
})
@@ -16,10 +17,43 @@ const route = useRoute()
const bankstatements = ref([])
const bankaccounts = ref([])
+const filterAccount = ref([])
+
+// Status für den Lade-Button
+const isSyncing = ref(false)
const setupPage = async () => {
bankstatements.value = (await useEntities("bankstatements").select("*, statementallocations(*)", "date", false))
bankaccounts.value = await useEntities("bankaccounts").select()
+ if(bankaccounts.value.length > 0) filterAccount.value = bankaccounts.value
+}
+
+// Funktion für den Bankabruf
+const syncBankStatements = async () => {
+ isSyncing.value = true
+ try {
+ await $api('/api/functions/services/bankstatementsync', { method: 'POST' })
+
+ toast.add({
+ title: 'Erfolg',
+ description: 'Bankdaten wurden erfolgreich synchronisiert.',
+ icon: 'i-heroicons-check-circle',
+ color: 'green'
+ })
+
+ // Wichtig: Daten neu laden, damit die neuen Buchungen direkt sichtbar sind
+ await setupPage()
+ } catch (error) {
+ console.error(error)
+ toast.add({
+ title: 'Fehler',
+ description: 'Beim Abrufen der Bankdaten ist ein Fehler aufgetreten.',
+ icon: 'i-heroicons-exclamation-circle',
+ color: 'red'
+ })
+ } finally {
+ isSyncing.value = false
+ }
}
const templateColumns = [
@@ -58,7 +92,6 @@ const clearSearchString = () => {
searchString.value = ''
}
-const filterAccount = ref(bankaccounts.value || [])
const displayCurrency = (value, currency = "€") => {
return `${Number(value).toFixed(2).replace(".",",")} ${currency}`
@@ -98,9 +131,6 @@ const filteredRows = computed(() => {
}
}
-
-
-
return useSearch(searchString.value, temp.filter(i => filterAccount.value.find(x => x.id === i.account)))
})
@@ -110,6 +140,17 @@ setupPage()
+
+
+
Konto
@@ -150,19 +191,6 @@ setupPage()
-
{{String(row.amount.toFixed(2)).replace(".",",")}} €
{{String(row.amount.toFixed(2)).replace(".",",")}} €
@@ -208,23 +236,20 @@ setupPage()
{{row.credName}}
{{row.debName}}
-
-
+
-
-
\ No newline at end of file
diff --git a/pages/createDocument/edit/[[id]].vue b/pages/createDocument/edit/[[id]].vue
index 23aa8d1..5312576 100644
--- a/pages/createDocument/edit/[[id]].vue
+++ b/pages/createDocument/edit/[[id]].vue
@@ -632,7 +632,7 @@ const findDocumentErrors = computed(() => {
if (itemInfo.value.customer === null) errors.push({message: "Es ist kein Kunde ausgewählt", type: "breaking"})
if (itemInfo.value.contact === null) errors.push({message: "Es ist kein Kontakt ausgewählt", type: "info"})
if (itemInfo.value.letterhead === null) errors.push({message: "Es ist kein Briefpapier ausgewählt", type: "breaking"})
- if (itemInfo.value.created_by === null || !itemInfo.value.created_by) errors.push({message: "Es ist kein Ansprechpartner im Unternehmen ausgewählt", type: "breaking"})
+ if (itemInfo.value.created_by === null || !itemInfo.value.created_by) errors.push({message: "Es ist kein Mitarbeiter ausgewählt", type: "breaking"})
if (itemInfo.value.address.street === null) errors.push({
message: "Es ist keine Straße im Adressat angegeben",
type: "breaking"
@@ -697,11 +697,11 @@ const findDocumentErrors = computed(() => {
if (["normal", "service", "free"].includes(row.mode)) {
- if (!row.taxPercent && typeof row.taxPercent !== "number") errors.push({
+ if (!row.taxPercent && typeof row.taxPercent !== "number" && itemInfo.value.type !== "deliveryNotes") errors.push({
message: `In Position ${row.pos} ist kein Steuersatz hinterlegt`,
type: "breaking"
})
- if (!row.price && typeof row.price !== "number") errors.push({
+ if (!row.price && typeof row.price !== "number" && itemInfo.value.type !== "deliveryNotes") errors.push({
message: `In Position ${row.pos} ist kein Preis hinterlegt`,
type: "breaking"
})
@@ -1281,6 +1281,7 @@ const saveSerialInvoice = async () => {
payment_type: itemInfo.value.payment_type,
deliveryDateType: "Leistungszeitraum",
createdBy: itemInfo.value.createdBy,
+ created_by: itemInfo.value.created_by,
title: itemInfo.value.title,
description: itemInfo.value.description,
startText: itemInfo.value.startText,
@@ -1289,6 +1290,7 @@ const saveSerialInvoice = async () => {
contactPerson: itemInfo.value.contactPerson,
serialConfig: itemInfo.value.serialConfig,
letterhead: itemInfo.value.letterhead,
+ taxType:itemInfo.value.taxType
}
let data = null
@@ -1692,7 +1694,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
+
diff --git a/pages/createDocument/index.vue b/pages/createDocument/index.vue
index b62a84c..10378ab 100644
--- a/pages/createDocument/index.vue
+++ b/pages/createDocument/index.vue
@@ -68,78 +68,69 @@
class="ml-2"
variant="outline"
>
- {{ filteredRows.filter(i => item.key === 'invoices' ? ['invoices', 'advanceInvoices', 'cancellationInvoices'].includes(i.type) : item.key === i.type).length }}
+ {{ getRowsForTab(item.key).length }}
- {{ dataStore.documentTypesForCreation[row.type].labelSingle }}
-
+ {{
+ dataStore.documentTypesForCreation[row.type].labelSingle
+ }} für {{ filteredRows.find(i => row.createddocument?.id === i.id)?.documentNumber }}
+ {{ dataStore.documentTypesForCreation[row.type].labelSingle }}
+
-
- {{ row.state }}
-
-
+ {{ row.state }}
- {{ row.state }}
-
+ {{ row.state }}
+
- Storniert mit {{ items.find(i => i.createddocument && i.createddocument.id === row.id).documentNumber }}
-
-
- {{ row.state }}
-
+ Storniert mit {{ items.find(i => i.createddocument && i.createddocument.id === row.id).documentNumber }}
+
+ {{ row.state }}
+
- {{ row.customer ? row.customer.name : "" }}
+ {{ row.customer ? row.customer.name : "" }}
{{ row.customer.name.substring(0, 20) }}...
+
- {{ row.documentNumber }}
+ {{ row.documentNumber }}
{{ row.documentNumber }}
+
{{ row.date ? dayjs(row.date).format("DD.MM.YY") : '' }}
- {{ row.documentDate ? dayjs(row.documentDate).format("DD.MM.YY") : '' }}
+ {{ row.documentDate ? dayjs(row.documentDate).format("DD.MM.YY") : '' }}
+
- {{ row.documentDate ? dayjs(row.documentDate).add(row.paymentDays, 'day').format("DD.MM.YY") : '' }}
+
+ {{ row.documentDate ? dayjs(row.documentDate).add(row.paymentDays, 'day').format("DD.MM.YY") : '' }}
+
+
@@ -147,20 +138,21 @@
Offen
+
- {{ displayCurrency(useSum().getCreatedDocumentSum(row, items)) }}
+ {{ displayCurrency(useSum().getCreatedDocumentSum(row, items)) }}
+
{{ displayCurrency(useSum().getCreatedDocumentSum(row, items) - row.statementallocations.reduce((n, {amount}) => n + amount, 0)) }}
+ v-if="!['deliveryNotes','cancellationInvoices','quotes','confirmationOrders'].includes(row.type) && row.state !== 'Entwurf' && !useSum().getIsPaid(row,items) && !items.find(i => i.linkedDocument && i.linkedDocument.id === row.id) ">
+ {{ displayCurrency(useSum().getCreatedDocumentSum(row, items) - row.statementallocations.reduce((n, {amount}) => n + amount, 0)) }}
+
-
-
\ No newline at end of file
diff --git a/pages/createDocument/serialInvoice.vue b/pages/createDocument/serialInvoice.vue
index 8900d36..b84c471 100644
--- a/pages/createDocument/serialInvoice.vue
+++ b/pages/createDocument/serialInvoice.vue
@@ -14,6 +14,23 @@
+
+
+
+
+
@@ -21,20 +38,9 @@
+
-
+
+
+
+
+ Laufende Ausführungen
+
+
+
+
+
+ Start: {{ dayjs(exec.createdAt).format('DD.MM.YYYY HH:mm') }}
+
+ Läuft
+
+
+
+ {{exec.summary}}
+
+
+
+
+
+
+
+
+
router.push(`/createDocument/edit/${row.id}`)"
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Belege anzuzeigen' }"
>
+
+
+
+
+
+
+
+
{{dataStore.documentTypesForCreation[row.type].labelSingle}}
@@ -67,23 +119,6 @@
{{row.customer ? row.customer.name : ""}}
-
{{displayCurrency(calculateDocSum(row))}}
@@ -98,27 +133,248 @@
Monatlich
Quartalsweise
+
+ Überweisung
+ SEPA - Einzug
+
+
+
+
+
+
+ Serienrechnungen manuell ausführen
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ filteredExecutionList.length }} sichtbar
+
+
+
+
+
+
+
+
+
+ {{row.customer ? row.customer.name : "-"}}
+
+
+ {{displayCurrency(calculateDocSum(row))}}
+
+
+ {{ row.serialConfig?.intervall }}
+
+
+ {{row.contract?.contractNumber}} - {{row.contract?.name}}
+
+
+
+
+
+ {{ selectedExecutionRows.length }} Vorlage(n) ausgewählt.
+
+
+
+
+
+ Abbrechen
+
+ Jetzt ausführen
+
+
+
+
+
+
+
+
+
+
+
+ Alle Ausführungen
+
+
+
+
+
+
+
+
+
+
+
+ Keine abgeschlossenen Ausführungen gefunden.
+
+
+
+
+ {{ dayjs(exec.createdAt).format('DD.MM.YYYY HH:mm') }}
+
+ {{ getStatusLabel(exec.status) }}
+
+
+
+
+
+ {{exec.summary}}
+
+
+
+
+
+
+
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/pages/export/create/sepa.vue b/pages/export/create/sepa.vue
new file mode 100644
index 0000000..9e17a7a
--- /dev/null
+++ b/pages/export/create/sepa.vue
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+ Erstellen
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pages/incomingInvoices/[mode]/[id].vue b/pages/incomingInvoices/[mode]/[id].vue
index 319fc26..ed07dbf 100644
--- a/pages/incomingInvoices/[mode]/[id].vue
+++ b/pages/incomingInvoices/[mode]/[id].vue
@@ -43,6 +43,10 @@ const setup = async () => {
accounts.value = await useEntities("accounts").selectSpecial()
itemInfo.value = await useEntities("incominginvoices").selectSingle(route.params.id, "*, files(*)")
+
+ //TODO: Dirty Fix
+ itemInfo.value.vendor = itemInfo.value.vendor?.id
+
await loadFile(itemInfo.value.files[itemInfo.value.files.length-1].id)
if(itemInfo.value.date && !itemInfo.value.dueDate) itemInfo.value.dueDate = itemInfo.value.date
@@ -412,7 +416,7 @@ const findIncomingInvoiceErrors = computed(() => {
value-attribute="id"
searchable
:disabled="mode === 'show'"
- :search-attributes="['label']"
+ :search-attributes="['label','number']"
searchable-placeholder="Suche..."
v-model="item.account"
:color="(item.account && accounts.find(i => i.id === item.account)) ? 'primary' : 'rose'"
@@ -420,6 +424,9 @@ const findIncomingInvoiceErrors = computed(() => {
{{accounts.find(account => account.id === item.account) ? accounts.find(account => account.id === item.account).label : "Keine Kategorie ausgewählt" }}
+
+ {{option.number}} - {{option.label}}
+
@@ -575,6 +582,7 @@ const findIncomingInvoiceErrors = computed(() => {
+
diff --git a/pages/incomingInvoices/index.vue b/pages/incomingInvoices/index.vue
index 8d12d6e..52e884f 100644
--- a/pages/incomingInvoices/index.vue
+++ b/pages/incomingInvoices/index.vue
@@ -2,6 +2,9 @@
import dayjs from "dayjs"
import {useSum} from "~/composables/useSum.js";
+// Zugriff auf API und Toast
+const { $api } = useNuxtApp()
+const toast = useToast()
defineShortcuts({
'/': () => {
@@ -47,6 +50,9 @@ const sort = ref({
direction: 'desc'
})
+// Status für den Button
+const isPreparing = ref(false)
+
const type = "incominginvoices"
const dataType = dataStore.dataTypes[type]
@@ -54,6 +60,34 @@ const setupPage = async () => {
items.value = await useEntities(type).select("*, vendor(id,name), statementallocations(id,amount)",sort.value.column,sort.value.direction === "asc")
}
+// Funktion zum Vorbereiten der Belege
+const prepareInvoices = async () => {
+ isPreparing.value = true
+ try {
+ await $api('/api/functions/services/prepareincominginvoices', { method: 'POST' })
+
+ toast.add({
+ title: 'Erfolg',
+ description: 'Eingangsbelege wurden vorbereitet.',
+ icon: 'i-heroicons-check-circle',
+ color: 'green'
+ })
+
+ // Liste neu laden
+ await setupPage()
+ } catch (error) {
+ console.error(error)
+ toast.add({
+ title: 'Fehler',
+ description: 'Beim Vorbereiten der Belege ist ein Fehler aufgetreten.',
+ icon: 'i-heroicons-exclamation-circle',
+ color: 'red'
+ })
+ } finally {
+ isPreparing.value = false
+ }
+}
+
setupPage()
const selectedColumns = ref(tempStore.columns[type] ? tempStore.columns[type] : dataType.templateColumns.filter(i => !i.disabledInTable))
@@ -128,6 +162,17 @@ const selectIncomingInvoice = (invoice) => {
+
+
+
{
v-if="searchString.length > 0"
/>
-
@@ -193,8 +237,8 @@ const selectIncomingInvoice = (invoice) => {
{{item.label}}
{{filteredRows.filter(i => item.label === 'Gebucht' ? i.state === 'Gebucht' : i.state !== 'Gebucht' ).length}}
diff --git a/pages/index.vue b/pages/index.vue
index 03bb084..41cb992 100644
--- a/pages/index.vue
+++ b/pages/index.vue
@@ -56,16 +56,36 @@
>
+
+
+ Label Drucken
+
+
diff --git a/pages/workflows/[token].vue b/pages/workflows/[token].vue
new file mode 100644
index 0000000..80b1697
--- /dev/null
+++ b/pages/workflows/[token].vue
@@ -0,0 +1,121 @@
+
+
+
+
+
+
+
+
+
+
+ Fehler
+ {{ errorMsg }}
+
+
+
+
+
+
+
+
+ Geschützter Bereich
+ Bitte PIN eingeben
+
+
+
+
+
+
+
+
+
+ Gespeichert!
+ Die Daten wurden erfolgreich übertragen.
+ window.location.reload()">Neuen Eintrag erfassen
+
+
+
+
+
+
\ No newline at end of file
diff --git a/stores/data.js b/stores/data.js
index dd805a7..a28952b 100644
--- a/stores/data.js
+++ b/stores/data.js
@@ -2608,7 +2608,7 @@ export const useDataStore = defineStore('data', () => {
},
cancellationInvoices: {
label: "Stornorechnungen",
- labelSingle: "Stornorechnung"
+ labelSingle: "Storno"
},
quotes: {
label: "Angebote",
diff --git a/stores/labelPrinter.ts b/stores/labelPrinter.ts
new file mode 100644
index 0000000..60a96a9
--- /dev/null
+++ b/stores/labelPrinter.ts
@@ -0,0 +1,190 @@
+import { defineStore } from "pinia"
+import {
+ Utils,
+ RequestCommandId,
+ ResponseCommandId,
+ NiimbotBluetoothClient,
+ NiimbotSerialClient
+} from "@mmote/niimbluelib"
+import { useToast } from "#imports"
+
+export const useLabelPrinterStore = defineStore("labelPrinter", {
+ state: () => ({
+ client: null as NiimbotBluetoothClient | NiimbotSerialClient | null,
+ connected: false,
+ connectLoading: false,
+ transportLastUsed: "",
+ printProgress: 0,
+ info: {} as any
+ }),
+
+ actions: {
+
+ /** Logging Helper */
+ logger(...args: any[]) {
+ console.debug("[Printer]", ...args)
+ },
+
+ /** --- Client erzeugen --- */
+ newClient(transport: "ble" | "serial" = "serial") {
+ const toast = useToast()
+
+ // alten Client trennen
+ if (this.client) {
+ try { this.client.disconnect() } catch {}
+ }
+
+ // neuen Client erzeugen
+ this.client =
+ transport === "ble"
+ ? new NiimbotBluetoothClient()
+ : new NiimbotSerialClient()
+
+ /** Events registrieren */
+
+ this.client.on("printerinfofetched", (e) => {
+ console.log("printerInfoFetched")
+ console.log(e.info)
+ this.info = e.info
+ })
+
+ this.client.on("connect", () => {
+ this.connected = true
+ toast.add({ title: "Drucker verbunden" })
+ this.logger("connected")
+ })
+
+ this.client.on("disconnect", () => {
+ this.connected = false
+ toast.add({ title: "Drucker getrennt" })
+ this.logger("disconnected")
+ })
+
+ this.client.on("printprogress", (e) => {
+ if (e.pagePrintProgress) this.printProgress = e.pagePrintProgress
+ this.logger(
+ `Page ${e.page}/${e.pagesTotal}, Page print ${e.pagePrintProgress}%, Page feed ${e.pageFeedProgress}%`
+ )
+ })
+
+ return this.client
+ },
+
+ /** --- Verbinden --- */
+ async connect(transport: "ble" | "serial" = "serial") {
+ const toast = useToast()
+
+ this.connectLoading = true
+
+ this.newClient(transport)
+
+ try {
+ await this.client!.connect()
+ this.transportLastUsed = transport
+ this.connectLoading = false
+ } catch (err) {
+ console.error("[Printer] Connect failed:", err)
+ toast.add({ title: "Verbindung fehlgeschlagen", color: "red" })
+ this.connectLoading = false
+ }
+ },
+
+ /** --- Trennen --- */
+ async disconnect({ forget = false } = {}) {
+ const toast = useToast()
+ this.logger("Disconnect requested…")
+
+ if (!this.client) return
+
+ try {
+ // Timer stoppen
+ try {
+ if (this.client.heartbeatTimer) {
+ clearInterval(this.client.heartbeatTimer)
+ this.client.heartbeatTimer = null
+ }
+ if (this.client.abstraction?.statusPollTimer) {
+ clearInterval(this.client.abstraction.statusPollTimer)
+ this.client.abstraction.statusPollTimer = null
+ }
+ } catch {}
+
+ await this.client.disconnect?.()
+
+ // Serial-Port schließen
+ const port = (this.client as any).port
+ if (port) {
+ try {
+ if (port.readable) port.readable.cancel?.()
+ if (port.writable) await port.writable.abort?.()
+ await port.close?.()
+
+ if (forget && navigator.serial?.forgetPort) {
+ await navigator.serial.forgetPort(port)
+ }
+ } catch (err) {
+ this.logger("Error closing port:", err)
+ }
+ }
+
+ // BLE GATT
+ if (
+ this.client instanceof NiimbotBluetoothClient &&
+ this.client.device?.gatt?.connected
+ ) {
+ try {
+ this.client.device.gatt.disconnect()
+ } catch {}
+ }
+
+ this.connected = false
+ this.client = null
+ toast.add({ title: "Drucker getrennt" })
+ } catch (err) {
+ console.error("[Printer] Disconnect error", err)
+ toast.add({ title: "Fehler beim Trennen", color: "red" })
+ }
+ },
+
+ /** Hilfsfunktion: EncodedImage reparieren */
+ reviveEncodedImage(encoded: any) {
+ if (!encoded?.rowsData) return encoded
+ for (const row of encoded.rowsData) {
+ if (row.rowData && !(row.rowData instanceof Uint8Array)) {
+ row.rowData = new Uint8Array(Object.values(row.rowData))
+ }
+ }
+ return encoded
+ },
+
+ /** --- Drucken --- */
+ async print(encoded: any, options?: { density?: number; pages?: number }) {
+ const toast = useToast()
+
+ if (!this.client) throw new Error("Kein Drucker verbunden")
+
+ const fixed = this.reviveEncodedImage(encoded)
+ const taskName = this.client.getPrintTaskType() ?? "B1"
+
+ const task = this.client.abstraction.newPrintTask(taskName, {
+ totalPages: options?.pages ?? 1,
+ statusPollIntervalMs: 100,
+ statusTimeoutMs: 8000,
+ density: options?.density ?? 5
+ })
+
+ try {
+ this.printProgress = 0
+ await task.printInit()
+ await task.printPage(fixed, options?.pages ?? 1)
+ await task.waitForFinished()
+ toast.add({ title: "Druck abgeschlossen" })
+ } catch (e) {
+ console.error("[Printer] print error", e)
+ toast.add({ title: "Druckfehler", color: "red" })
+ } finally {
+ await task.printEnd()
+ }
+ }
+ }
+})
|