Added missing files

This commit is contained in:
2026-03-21 17:57:23 +01:00
parent 6c3c318f86
commit 8038f03406
4 changed files with 526 additions and 0 deletions

View File

@@ -0,0 +1,155 @@
import { createSharedComposable } from '@vueuse/core'
type ChangelogEntry = {
hash: string
shortHash: string
subject: string
authorName: string
committedAt: string
}
type ChangelogSeenState = {
lastOpenedAt: string | null
latestSeenHash: string | null
}
const defaultSeenState = (): ChangelogSeenState => ({
lastOpenedAt: null,
latestSeenHash: null
})
let changelogRequest: Promise<void> | null = null
const _useChangelog = () => {
const auth = useAuthStore()
const entries = useState<ChangelogEntry[]>('changelog:entries', () => [])
const pending = useState<boolean>('changelog:pending', () => false)
const error = useState<string | null>('changelog:error', () => null)
const loadedKey = useState<string | null>('changelog:loaded-key', () => null)
const seenState = useState<ChangelogSeenState>('changelog:seen-state', defaultSeenState)
const scopeKey = computed(() => {
const userId = auth.user?.id
const tenantId = auth.activeTenant
if (!userId || !tenantId) return null
return `${userId}:${tenantId}`
})
const storageKey = computed(() => {
if (!scopeKey.value) return null
return `fedeo:changelog:last-opened:${scopeKey.value}`
})
const latestEntry = computed(() => entries.value[0] || null)
const hasUnread = computed(() => {
if (!latestEntry.value?.hash) return false
return latestEntry.value.hash !== seenState.value.latestSeenHash
})
function loadSeenState() {
if (!process.client || !storageKey.value) {
seenState.value = defaultSeenState()
return
}
try {
const raw = localStorage.getItem(storageKey.value)
if (!raw) {
seenState.value = defaultSeenState()
return
}
const parsed = JSON.parse(raw)
seenState.value = {
lastOpenedAt: parsed?.lastOpenedAt || null,
latestSeenHash: parsed?.latestSeenHash || null
}
} catch (err) {
console.error('Could not parse changelog seen state', err)
seenState.value = defaultSeenState()
}
}
async function refresh(force = false) {
if (!process.client || !scopeKey.value) return
if (!force && loadedKey.value === scopeKey.value && entries.value.length) return
if (changelogRequest) return changelogRequest
changelogRequest = (async () => {
pending.value = true
error.value = null
try {
const response = await useNuxtApp().$api('/api/functions/changelog', {
query: { limit: 20 }
})
entries.value = Array.isArray(response?.entries) ? response.entries : []
loadedKey.value = scopeKey.value
} catch (err: any) {
error.value = err?.data?.error || err?.message || 'Changelog konnte nicht geladen werden.'
} finally {
pending.value = false
}
})()
try {
await changelogRequest
} finally {
changelogRequest = null
}
}
function markAsSeen() {
if (!process.client || !storageKey.value) return
const nextState = {
lastOpenedAt: new Date().toISOString(),
latestSeenHash: latestEntry.value?.hash || null
}
seenState.value = nextState
try {
localStorage.setItem(storageKey.value, JSON.stringify(nextState))
} catch (err) {
console.error('Could not persist changelog seen state', err)
}
}
watch(storageKey, () => {
loadSeenState()
}, { immediate: true })
watch(scopeKey, (nextScopeKey, previousScopeKey) => {
if (!process.client || !nextScopeKey) return
if (nextScopeKey !== previousScopeKey) {
entries.value = []
loadedKey.value = null
}
void refresh(true)
}, { immediate: true })
return {
entries,
pending,
error,
latestEntry,
hasUnread,
seenState,
refresh,
markAsSeen
}
}
export const useChangelog = createSharedComposable(_useChangelog)

View File

@@ -0,0 +1,162 @@
import dayjs from "dayjs"
import customParseFormat from "dayjs/plugin/customParseFormat"
dayjs.extend(customParseFormat)
export const TAX_EVALUATION_PERIOD_OPTIONS = [
{ label: "Monatlich", value: "monthly" },
{ label: "Quartalsweise", value: "quarterly" },
{ label: "Jährlich", value: "yearly" },
]
export const normalizeTaxEvaluationPeriod = (value?: string) => {
if (value === "quarterly" || value === "yearly") return value
return "monthly"
}
const ZERO_BREAKDOWN = () => ({
net19: 0,
tax19: 0,
net7: 0,
tax7: 0,
net0: 0,
})
const isTaxFreeDocument = (taxType?: string | null) => {
return ["13b UStG", "19 UStG", "12.3 UStG"].includes(String(taxType || ""))
}
export const getTaxEvaluationPeriodBounds = (
referenceDate: dayjs.ConfigType,
period: string
) => {
const normalized = normalizeTaxEvaluationPeriod(period)
const base = dayjs(referenceDate)
if (normalized === "yearly") {
return {
start: base.startOf("year"),
end: base.endOf("year"),
}
}
if (normalized === "quarterly") {
const quarterStartMonth = Math.floor(base.month() / 3) * 3
const start = base.month(quarterStartMonth).startOf("month")
return {
start,
end: start.add(2, "month").endOf("month"),
}
}
return {
start: base.startOf("month"),
end: base.endOf("month"),
}
}
export const shiftTaxEvaluationPeriodStart = (
periodStart: dayjs.ConfigType,
period: string,
offset: number
) => {
const normalized = normalizeTaxEvaluationPeriod(period)
const base = dayjs(periodStart)
if (normalized === "yearly") return base.add(offset, "year").startOf("year")
if (normalized === "quarterly") return base.add(offset * 3, "month").startOf("month")
return base.add(offset, "month").startOf("month")
}
export const formatTaxEvaluationPeriodLabel = (
periodStart: dayjs.ConfigType,
period: string
) => {
const { start, end } = getTaxEvaluationPeriodBounds(periodStart, period)
const normalized = normalizeTaxEvaluationPeriod(period)
if (normalized === "yearly") {
return start.format("YYYY")
}
if (normalized === "quarterly") {
return `Q${Math.floor(start.month() / 3) + 1} ${start.format("YYYY")}`
}
return start.format("MMMM YYYY")
}
export const formatTaxEvaluationPeriodRange = (
periodStart: dayjs.ConfigType,
period: string
) => {
const { start, end } = getTaxEvaluationPeriodBounds(periodStart, period)
return `${start.format("DD.MM.YYYY")} - ${end.format("DD.MM.YYYY")}`
}
export const getCreatedDocumentTaxBreakdown = (doc: any) => {
const breakdown = ZERO_BREAKDOWN()
if (!doc || isTaxFreeDocument(doc.taxType)) {
return breakdown
}
;(doc.rows || []).forEach((row: any) => {
if (!row || ["pagebreak", "title", "text"].includes(row.mode)) return
const quantity = Number(row.quantity || 0)
const price = Number(row.price || 0)
const discountPercent = Number(row.discountPercent || 0)
const taxPercent = Number(row.taxPercent || 0)
const net = Number((quantity * price * (1 - discountPercent / 100)).toFixed(2))
if (!Number.isFinite(net) || net === 0) return
if (taxPercent === 19) {
breakdown.net19 += net
breakdown.tax19 += Number((net * 0.19).toFixed(2))
} else if (taxPercent === 7) {
breakdown.net7 += net
breakdown.tax7 += Number((net * 0.07).toFixed(2))
} else {
breakdown.net0 += net
}
})
return {
net19: Number(breakdown.net19.toFixed(2)),
tax19: Number(breakdown.tax19.toFixed(2)),
net7: Number(breakdown.net7.toFixed(2)),
tax7: Number(breakdown.tax7.toFixed(2)),
net0: Number(breakdown.net0.toFixed(2)),
}
}
export const getIncomingInvoiceTaxBreakdown = (invoice: any) => {
const breakdown = ZERO_BREAKDOWN()
;(invoice?.accounts || []).forEach((account: any) => {
const taxType = String(account?.taxType || "")
const amountNet = Number(account?.amountNet || 0)
const amountTax = Number(account?.amountTax || 0)
if (taxType === "19") {
breakdown.net19 += amountNet
breakdown.tax19 += amountTax
} else if (taxType === "7") {
breakdown.net7 += amountNet
breakdown.tax7 += amountTax
} else {
breakdown.net0 += amountNet
}
})
return {
net19: Number(breakdown.net19.toFixed(2)),
tax19: Number(breakdown.tax19.toFixed(2)),
net7: Number(breakdown.net7.toFixed(2)),
tax7: Number(breakdown.tax7.toFixed(2)),
net0: Number(breakdown.net0.toFixed(2)),
}
}