Merge remote-tracking branch 'origin/dev' into dev
This commit is contained in:
@@ -91,6 +91,7 @@ const selectedProductcategorie = ref(null)
|
||||
const services = ref([])
|
||||
const servicecategories = ref([])
|
||||
const selectedServicecategorie = ref(null)
|
||||
const collapsedTitleRowIds = ref(new Set())
|
||||
const customers = ref([])
|
||||
const contacts = ref([])
|
||||
const contracts = ref([])
|
||||
@@ -935,6 +936,45 @@ const getPositionAddMenuItems = (row) => [
|
||||
}))
|
||||
]
|
||||
|
||||
const toggleTitleSection = (rowId) => {
|
||||
const nextCollapsedTitleRowIds = new Set(collapsedTitleRowIds.value)
|
||||
|
||||
if (nextCollapsedTitleRowIds.has(rowId)) {
|
||||
nextCollapsedTitleRowIds.delete(rowId)
|
||||
} else {
|
||||
nextCollapsedTitleRowIds.add(rowId)
|
||||
}
|
||||
|
||||
collapsedTitleRowIds.value = nextCollapsedTitleRowIds
|
||||
}
|
||||
|
||||
const hiddenDocumentRowIds = computed(() => {
|
||||
const hiddenRowIds = new Set()
|
||||
const collapsedTitleLevels = []
|
||||
|
||||
itemInfo.value.rows.forEach((row) => {
|
||||
if (row.mode === 'title') {
|
||||
const titleLevel = Math.min(Math.max(Number(row.titleLevel) || 1, 1), 3)
|
||||
|
||||
while (
|
||||
collapsedTitleLevels.length > 0
|
||||
&& collapsedTitleLevels[collapsedTitleLevels.length - 1] >= titleLevel
|
||||
) {
|
||||
collapsedTitleLevels.pop()
|
||||
}
|
||||
|
||||
if (collapsedTitleLevels.length > 0) hiddenRowIds.add(row.id)
|
||||
if (collapsedTitleRowIds.value.has(row.id)) collapsedTitleLevels.push(titleLevel)
|
||||
} else if (collapsedTitleLevels.length > 0) {
|
||||
hiddenRowIds.add(row.id)
|
||||
}
|
||||
})
|
||||
|
||||
return hiddenRowIds
|
||||
})
|
||||
|
||||
const isDocumentRowHidden = (rowId) => hiddenDocumentRowIds.value.has(rowId)
|
||||
|
||||
const getFullWidthPositionColspan = () => deliveryNoteLikeDocumentTypes.includes(itemInfo.value.type) ? 5 : 8
|
||||
|
||||
const getTitlePositionColspan = () => deliveryNoteLikeDocumentTypes.includes(itemInfo.value.type) ? 4 : 7
|
||||
@@ -2897,13 +2937,28 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
|
||||
@end="setPosNumbers"
|
||||
>
|
||||
<template #item="{element: row}">
|
||||
<tr>
|
||||
<tr :class="{ collapse: isDocumentRowHidden(row.id) }">
|
||||
<td>
|
||||
<UIcon
|
||||
class="handle"
|
||||
name="i-mdi-menu"
|
||||
v-if="itemInfo.type !== 'cancellationInvoices'"
|
||||
/>
|
||||
<div class="flex items-center gap-1 whitespace-nowrap">
|
||||
<UIcon
|
||||
class="handle shrink-0"
|
||||
name="i-mdi-menu"
|
||||
v-if="itemInfo.type !== 'cancellationInvoices'"
|
||||
/>
|
||||
<UTooltip
|
||||
v-if="row.mode === 'title'"
|
||||
:text="collapsedTitleRowIds.has(row.id) ? 'Titelbereich ausklappen' : 'Titelbereich einklappen'"
|
||||
>
|
||||
<UButton
|
||||
class="shrink-0"
|
||||
variant="ghost"
|
||||
color="neutral"
|
||||
:icon="collapsedTitleRowIds.has(row.id) ? 'i-heroicons-chevron-right' : 'i-heroicons-chevron-down'"
|
||||
:aria-label="collapsedTitleRowIds.has(row.id) ? 'Titelbereich ausklappen' : 'Titelbereich einklappen'"
|
||||
@click="toggleTitleSection(row.id)"
|
||||
/>
|
||||
</UTooltip>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
v-if="row.mode === 'pagebreak'"
|
||||
|
||||
@@ -149,6 +149,18 @@ const bankBookingDateLabel = computed(() => {
|
||||
return bankBookingDates.value.map(formatDate).join(", ")
|
||||
})
|
||||
const vendorName = computed(() => vendors.value.find((vendor) => vendor.id === itemInfo.value.vendor)?.name || "-")
|
||||
const eInvoiceValidation = computed(() => itemInfo.value.eInvoiceValidation || null)
|
||||
const eInvoiceSourceLabel = computed(() => {
|
||||
if (itemInfo.value.preparationSource !== "e-invoice") return null
|
||||
if (itemInfo.value.eInvoiceSyntax === "cii") {
|
||||
return String(itemInfo.value.eInvoiceProfile || "").toLowerCase().includes("xrechnung")
|
||||
? "XRechnung (CII)"
|
||||
: "ZUGFeRD / Factur-X (CII)"
|
||||
}
|
||||
if (itemInfo.value.eInvoiceSyntax === "ubl-credit-note") return "XRechnung / UBL-Gutschrift"
|
||||
return "XRechnung / UBL"
|
||||
})
|
||||
const eInvoiceProfileLabel = computed(() => itemInfo.value.eInvoiceProfile || "Profil nicht angegeben")
|
||||
const getAccountLabel = (item) => {
|
||||
const account = accounts.value.find((entry) => entry.id === item.account)
|
||||
|
||||
@@ -356,6 +368,35 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
|
||||
Dokument andocken
|
||||
</UButton>
|
||||
|
||||
<UAlert
|
||||
v-if="eInvoiceSourceLabel"
|
||||
:title="eInvoiceSourceLabel"
|
||||
color="primary"
|
||||
variant="soft"
|
||||
icon="i-heroicons-document-check"
|
||||
>
|
||||
<template #description>
|
||||
<div class="mt-1 space-y-2 text-sm">
|
||||
<p class="break-all">{{ eInvoiceProfileLabel }}</p>
|
||||
<div v-if="eInvoiceValidation?.errors?.length">
|
||||
<p class="font-semibold">Validierungsfehler</p>
|
||||
<ul class="list-inside list-disc">
|
||||
<li v-for="error in eInvoiceValidation.errors" :key="error">{{ error }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="eInvoiceValidation?.warnings?.length">
|
||||
<p class="font-semibold">Hinweise</p>
|
||||
<ul class="list-inside list-disc">
|
||||
<li v-for="warning in eInvoiceValidation.warnings" :key="warning">{{ warning }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p v-if="!eInvoiceValidation?.errors?.length && !eInvoiceValidation?.warnings?.length">
|
||||
Pflichtfelder und Rechnungssummen wurden erfolgreich geprüft.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</UAlert>
|
||||
|
||||
<UAlert
|
||||
v-if="mode !== 'show' && findIncomingInvoiceErrors.length > 0"
|
||||
title="Prüfung erforderlich"
|
||||
|
||||
@@ -287,6 +287,18 @@ const selectIncomingInvoice = (invoiceLike) => {
|
||||
<span v-if="row.original.state === 'Vorbereitet'" class="text-cyan-500">{{row.original.state}}</span>
|
||||
<span v-else-if="row.original.state === 'Entwurf'" class="text-red-500">{{row.original.state}}</span>
|
||||
<span v-else-if="row.original.state === 'Gebucht'" class="text-primary-500">{{row.original.state}}</span>
|
||||
<span v-else-if="row.original.state === 'Prüfung erforderlich'" class="text-orange-500">{{row.original.state}}</span>
|
||||
<span v-else>{{row.original.state}}</span>
|
||||
</template>
|
||||
<template #preparationSource-cell="{row}">
|
||||
<UBadge v-if="row.original.preparationSource === 'e-invoice'" color="primary" variant="soft">
|
||||
{{ row.original.eInvoiceSyntax === 'cii'
|
||||
? (String(row.original.eInvoiceProfile || '').toLowerCase().includes('xrechnung') ? 'XRechnung / CII' : 'ZUGFeRD / CII')
|
||||
: 'XRechnung / UBL' }}
|
||||
</UBadge>
|
||||
<UBadge v-else-if="row.original.preparationSource === 'gpt'" color="neutral" variant="soft">
|
||||
PDF / KI
|
||||
</UBadge>
|
||||
</template>
|
||||
<template #date-cell="{row}">
|
||||
{{dayjs(row.original.date).format("DD.MM.YYYY")}}
|
||||
|
||||
@@ -11,7 +11,8 @@ const router = useRouter()
|
||||
|
||||
const state = reactive({
|
||||
email: '',
|
||||
password: ''
|
||||
password: '',
|
||||
rememberMe: true
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
@@ -19,7 +20,7 @@ const loading = ref(false)
|
||||
const doLogin = async (event: FormSubmitEvent<typeof state>) => {
|
||||
loading.value = true
|
||||
try {
|
||||
await auth.login(event.data.email, event.data.password)
|
||||
await auth.login(event.data.email, event.data.password, event.data.rememberMe)
|
||||
toast.add({title:"Einloggen erfolgreich"})
|
||||
await router.push("/")
|
||||
} catch (err: any) {
|
||||
@@ -79,6 +80,12 @@ const doLogin = async (event: FormSubmitEvent<typeof state>) => {
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<UCheckbox
|
||||
v-model="state.rememberMe"
|
||||
name="rememberMe"
|
||||
label="Angemeldet bleiben"
|
||||
/>
|
||||
|
||||
<UButton type="submit" block class="w-full" :loading="loading">
|
||||
Weiter
|
||||
</UButton>
|
||||
|
||||
@@ -27,6 +27,7 @@ export const useAuthStore = defineStore("auth", {
|
||||
sessionWarningTimer: null as ReturnType<typeof setTimeout> | null,
|
||||
sessionLogoutTimer: null as ReturnType<typeof setTimeout> | null,
|
||||
sessionCountdownTimer: null as ReturnType<typeof setInterval> | null,
|
||||
sessionPersistent: false,
|
||||
}),
|
||||
|
||||
actions: {
|
||||
@@ -36,18 +37,18 @@ export const useAuthStore = defineStore("auth", {
|
||||
|
||||
getStoredToken() {
|
||||
const rootToken = this.tokenCookie().value
|
||||
if (rootToken || !process.client) return rootToken
|
||||
if (!process.client) return rootToken
|
||||
|
||||
const tokenCookie = document.cookie
|
||||
.split(";")
|
||||
.map((part) => part.trim())
|
||||
.find((part) => part.startsWith("token="))
|
||||
return sessionStorage.getItem("token")
|
||||
|| localStorage.getItem("token")
|
||||
|| rootToken
|
||||
},
|
||||
|
||||
if (tokenCookie) {
|
||||
return decodeURIComponent(tokenCookie.slice("token=".length))
|
||||
}
|
||||
getStoredRefreshToken() {
|
||||
if (!process.client) return null
|
||||
|
||||
return localStorage.getItem("token")
|
||||
return sessionStorage.getItem("refreshToken")
|
||||
|| localStorage.getItem("refreshToken")
|
||||
},
|
||||
|
||||
clearScopedTokenCookies() {
|
||||
@@ -155,24 +156,34 @@ export const useAuthStore = defineStore("auth", {
|
||||
const msUntilWarning = msUntilLogout - this.sessionWarningLeadMs
|
||||
|
||||
if (msUntilWarning <= 0) {
|
||||
if (this.getStoredRefreshToken()) {
|
||||
void this.refreshSession()
|
||||
return
|
||||
}
|
||||
this.openSessionWarning(expiresAtMs)
|
||||
return
|
||||
}
|
||||
|
||||
this.sessionWarningTimer = setTimeout(() => {
|
||||
if (this.getStoredRefreshToken()) {
|
||||
void this.refreshSession()
|
||||
return
|
||||
}
|
||||
this.openSessionWarning(expiresAtMs)
|
||||
}, msUntilWarning)
|
||||
},
|
||||
|
||||
setToken(token: string | null) {
|
||||
setToken(token: string | null, persistent = this.sessionPersistent) {
|
||||
this.clearScopedTokenCookies()
|
||||
this.tokenCookie().value = token
|
||||
|
||||
if (process.client) {
|
||||
localStorage.removeItem("token")
|
||||
sessionStorage.removeItem("token")
|
||||
|
||||
if (token) {
|
||||
localStorage.setItem("token", token)
|
||||
} else {
|
||||
localStorage.removeItem("token")
|
||||
const storage = persistent ? localStorage : sessionStorage
|
||||
storage.setItem("token", token)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,6 +197,30 @@ export const useAuthStore = defineStore("auth", {
|
||||
this.scheduleSessionTimers(token)
|
||||
},
|
||||
|
||||
setRefreshToken(refreshToken: string | null, persistent = this.sessionPersistent) {
|
||||
if (!process.client) return
|
||||
|
||||
localStorage.removeItem("refreshToken")
|
||||
sessionStorage.removeItem("refreshToken")
|
||||
|
||||
if (refreshToken) {
|
||||
const storage = persistent ? localStorage : sessionStorage
|
||||
storage.setItem("refreshToken", refreshToken)
|
||||
}
|
||||
},
|
||||
|
||||
setSession(token: string, refreshToken: string, persistent: boolean) {
|
||||
this.sessionPersistent = persistent
|
||||
this.setRefreshToken(refreshToken, persistent)
|
||||
this.setToken(token, persistent)
|
||||
},
|
||||
|
||||
clearStoredSession() {
|
||||
this.setRefreshToken(null)
|
||||
this.setToken(null)
|
||||
this.sessionPersistent = false
|
||||
},
|
||||
|
||||
async persist(token: string | null) {
|
||||
|
||||
console.log("On Web")
|
||||
@@ -195,6 +230,7 @@ export const useAuthStore = defineStore("auth", {
|
||||
|
||||
async initStore() {
|
||||
console.log("Auth initStore")
|
||||
this.sessionPersistent = process.client && Boolean(localStorage.getItem("refreshToken"))
|
||||
|
||||
// 1. Check: Haben wir überhaupt ein Token?
|
||||
const token = this.getStoredToken()
|
||||
@@ -207,7 +243,7 @@ export const useAuthStore = defineStore("auth", {
|
||||
}
|
||||
|
||||
// 2. Token existiert -> Versuche User zu laden
|
||||
await this.fetchMe(token)
|
||||
await this.fetchMe(token, true)
|
||||
|
||||
// Wenn fetchMe fertig ist (egal ob Erfolg oder Fehler), ladebalken weg
|
||||
|
||||
@@ -236,18 +272,18 @@ export const useAuthStore = defineStore("auth", {
|
||||
}
|
||||
},
|
||||
|
||||
async login(email: string, password: string) {
|
||||
async login(email: string, password: string, rememberMe = false) {
|
||||
try {
|
||||
console.log("Auth login")
|
||||
const { token } = await useNuxtApp().$api("/auth/login", {
|
||||
const { token, refreshToken } = await useNuxtApp().$api("/auth/login", {
|
||||
method: "POST",
|
||||
body: { email, password }
|
||||
body: { email, password, rememberMe }
|
||||
})
|
||||
|
||||
console.log("Token: " + token)
|
||||
|
||||
// 1. WICHTIG: Token sofort ins Cookie schreiben, damit es persistiert wird
|
||||
this.setToken(token)
|
||||
// Tokens abhängig von „Angemeldet bleiben“ dauerhaft oder nur für diese Sitzung speichern
|
||||
this.setSession(token, refreshToken, rememberMe)
|
||||
this.sessionExpired = false
|
||||
|
||||
// 2. User Daten laden
|
||||
@@ -269,14 +305,18 @@ export const useAuthStore = defineStore("auth", {
|
||||
|
||||
} catch (e) {
|
||||
console.log("login error:" + e)
|
||||
// Hier könnte man noch eine Fehlermeldung im UI anzeigen
|
||||
throw e
|
||||
}
|
||||
},
|
||||
|
||||
async logout() {
|
||||
console.log("Auth logout")
|
||||
const refreshToken = this.getStoredRefreshToken()
|
||||
try {
|
||||
await useNuxtApp().$api("/auth/logout", { method: "POST" })
|
||||
await useNuxtApp().$api("/auth/logout", {
|
||||
method: "POST",
|
||||
body: { refreshToken }
|
||||
})
|
||||
} catch (e) {
|
||||
console.error("Logout API fehlgeschlagen (egal):", e)
|
||||
}
|
||||
@@ -285,7 +325,7 @@ export const useAuthStore = defineStore("auth", {
|
||||
this.resetState()
|
||||
|
||||
// Token löschen
|
||||
this.setToken(null)
|
||||
this.clearStoredSession()
|
||||
|
||||
// Nur beim expliziten Logout navigieren wir
|
||||
navigateTo("/login")
|
||||
@@ -295,7 +335,7 @@ export const useAuthStore = defineStore("auth", {
|
||||
console.log("Auth session expired")
|
||||
this.resetState()
|
||||
this.sessionExpired = true
|
||||
this.setToken(null)
|
||||
this.clearStoredSession()
|
||||
this.loading = false
|
||||
|
||||
if (process.client) {
|
||||
@@ -317,7 +357,7 @@ export const useAuthStore = defineStore("auth", {
|
||||
this.loading = false
|
||||
},
|
||||
|
||||
async fetchMe(jwt= null) {
|
||||
async fetchMe(jwt= null, allowRefresh = true) {
|
||||
console.log("Auth fetchMe")
|
||||
const tempStore = useTempStore()
|
||||
|
||||
@@ -384,6 +424,10 @@ export const useAuthStore = defineStore("auth", {
|
||||
console.log("fetchMe failed (Invalid Token or Network)", err)
|
||||
|
||||
if (err?.response?.status === 401 || err?.status === 401 || err?.statusCode === 401) {
|
||||
if (allowRefresh && this.getStoredRefreshToken()) {
|
||||
await this.refreshSession()
|
||||
return
|
||||
}
|
||||
this.expireSession()
|
||||
return
|
||||
}
|
||||
@@ -395,17 +439,31 @@ export const useAuthStore = defineStore("auth", {
|
||||
},
|
||||
|
||||
async refreshSession() {
|
||||
const refreshToken = this.getStoredRefreshToken()
|
||||
if (!refreshToken) {
|
||||
this.expireSession()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const { token } = await useNuxtApp().$api("/api/auth/refresh", {
|
||||
const session = await useNuxtApp().$api("/auth/refresh", {
|
||||
method: "POST",
|
||||
body: { refreshToken }
|
||||
})
|
||||
|
||||
this.setToken(token)
|
||||
await this.fetchMe(token)
|
||||
this.setSession(session.token, session.refreshToken, this.sessionPersistent)
|
||||
await this.fetchMe(session.token, false)
|
||||
this.sessionWarningVisible = false
|
||||
} catch (err) {
|
||||
console.error("JWT refresh failed", err)
|
||||
await this.logout()
|
||||
const status = err?.response?.status || err?.status || err?.statusCode
|
||||
if (status === 401) {
|
||||
this.expireSession()
|
||||
} else {
|
||||
const token = this.getStoredToken()
|
||||
const expiresAtMs = token ? this.decodeTokenExpiryMs(token) : null
|
||||
if (expiresAtMs) this.openSessionWarning(expiresAtMs)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -414,14 +472,15 @@ export const useAuthStore = defineStore("auth", {
|
||||
this.loading = true
|
||||
const res = await useNuxtApp().$api("/api/tenant/switch", {
|
||||
method: "POST",
|
||||
body: { tenant_id }
|
||||
body: {
|
||||
tenant_id,
|
||||
refreshToken: this.getStoredRefreshToken()
|
||||
}
|
||||
})
|
||||
console.log(res)
|
||||
|
||||
const {token} = res
|
||||
|
||||
|
||||
this.setToken(token)
|
||||
const {token, refreshToken} = res
|
||||
this.setSession(token, refreshToken, this.sessionPersistent)
|
||||
|
||||
|
||||
await this.init(token)
|
||||
|
||||
@@ -2460,6 +2460,9 @@ export const useDataStore = defineStore('data', () => {
|
||||
key: 'reference',
|
||||
label: "Referenz:",
|
||||
sortable: true,
|
||||
}, {
|
||||
key: 'preparationSource',
|
||||
label: "Quelle",
|
||||
}, {
|
||||
key: 'state',
|
||||
label: "Status:"
|
||||
|
||||
Reference in New Issue
Block a user