Added missing files
This commit is contained in:
183
frontend/components/displayTaxSummary.vue
Normal file
183
frontend/components/displayTaxSummary.vue
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import dayjs from "dayjs"
|
||||||
|
import customParseFormat from "dayjs/plugin/customParseFormat"
|
||||||
|
import {
|
||||||
|
formatTaxEvaluationPeriodLabel,
|
||||||
|
formatTaxEvaluationPeriodRange,
|
||||||
|
getCreatedDocumentTaxBreakdown,
|
||||||
|
getIncomingInvoiceTaxBreakdown,
|
||||||
|
getTaxEvaluationPeriodBounds,
|
||||||
|
normalizeTaxEvaluationPeriod
|
||||||
|
} from "~/composables/useTaxEvaluation"
|
||||||
|
|
||||||
|
dayjs.extend(customParseFormat)
|
||||||
|
|
||||||
|
const auth = useAuthStore()
|
||||||
|
|
||||||
|
const loading = ref(true)
|
||||||
|
const summary = ref({
|
||||||
|
label: "",
|
||||||
|
range: "",
|
||||||
|
outputTax: 0,
|
||||||
|
inputTax: 0,
|
||||||
|
balance: 0,
|
||||||
|
outputCount: 0,
|
||||||
|
inputCount: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
const formatCurrency = (value: number) => {
|
||||||
|
return new Intl.NumberFormat("de-DE", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "EUR"
|
||||||
|
}).format(Number(value || 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadSummary = async () => {
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const periodType = normalizeTaxEvaluationPeriod(auth.activeTenantData?.taxEvaluationPeriod)
|
||||||
|
const bounds = getTaxEvaluationPeriodBounds(dayjs(), periodType)
|
||||||
|
|
||||||
|
const [docs, incoming] = await Promise.all([
|
||||||
|
useEntities("createddocuments").select(),
|
||||||
|
useEntities("incominginvoices").select()
|
||||||
|
])
|
||||||
|
|
||||||
|
const outputDocs = (docs || []).filter((doc: any) => {
|
||||||
|
if (doc?.state !== "Gebucht") return false
|
||||||
|
if (!["invoices", "advanceInvoices", "cancellationInvoices"].includes(doc?.type)) return false
|
||||||
|
|
||||||
|
const date = dayjs(doc.documentDate)
|
||||||
|
return date.isValid() && !date.isBefore(bounds.start, "day") && !date.isAfter(bounds.end, "day")
|
||||||
|
})
|
||||||
|
|
||||||
|
const inputDocs = (incoming || []).filter((invoice: any) => {
|
||||||
|
if (invoice?.state !== "Gebucht" || !invoice?.date) return false
|
||||||
|
|
||||||
|
const date = dayjs(invoice.date)
|
||||||
|
return date.isValid() && !date.isBefore(bounds.start, "day") && !date.isAfter(bounds.end, "day")
|
||||||
|
})
|
||||||
|
|
||||||
|
const outputTax = outputDocs.reduce((sum: number, doc: any) => {
|
||||||
|
const breakdown = getCreatedDocumentTaxBreakdown(doc)
|
||||||
|
return sum + breakdown.tax19 + breakdown.tax7
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
const inputTax = inputDocs.reduce((sum: number, invoice: any) => {
|
||||||
|
const breakdown = getIncomingInvoiceTaxBreakdown(invoice)
|
||||||
|
return sum + breakdown.tax19 + breakdown.tax7
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
summary.value = {
|
||||||
|
label: formatTaxEvaluationPeriodLabel(bounds.start, periodType),
|
||||||
|
range: formatTaxEvaluationPeriodRange(bounds.start, periodType),
|
||||||
|
outputTax: Number(outputTax.toFixed(2)),
|
||||||
|
inputTax: Number(inputTax.toFixed(2)),
|
||||||
|
balance: Number((outputTax - inputTax).toFixed(2)),
|
||||||
|
outputCount: outputDocs.length,
|
||||||
|
inputCount: inputDocs.length,
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(loadSummary)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div class="tax-summary-top">
|
||||||
|
<div>
|
||||||
|
<p class="tax-summary-period">{{ summary.label }}</p>
|
||||||
|
<p class="tax-summary-range">{{ summary.range }}</p>
|
||||||
|
</div>
|
||||||
|
<UButton
|
||||||
|
size="xs"
|
||||||
|
variant="soft"
|
||||||
|
color="gray"
|
||||||
|
icon="i-heroicons-arrow-top-right-on-square"
|
||||||
|
@click="navigateTo('/accounting/tax')"
|
||||||
|
>
|
||||||
|
Details
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tax-summary-row">
|
||||||
|
<span class="tax-summary-label">USt Rechnungen</span>
|
||||||
|
<span class="tax-summary-value text-amber-600 dark:text-amber-400">
|
||||||
|
{{ loading ? "..." : formatCurrency(summary.outputTax) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tax-summary-row">
|
||||||
|
<span class="tax-summary-label">Vorsteuer</span>
|
||||||
|
<span class="tax-summary-value text-sky-600 dark:text-sky-400">
|
||||||
|
{{ loading ? "..." : formatCurrency(summary.inputTax) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tax-summary-row">
|
||||||
|
<span class="tax-summary-label">Ergebnis</span>
|
||||||
|
<span
|
||||||
|
class="tax-summary-value"
|
||||||
|
:class="summary.balance >= 0 ? 'text-rose-600 dark:text-rose-400' : 'text-emerald-600 dark:text-emerald-400'"
|
||||||
|
>
|
||||||
|
{{ loading ? "..." : formatCurrency(summary.balance) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tax-summary-meta">
|
||||||
|
{{ summary.outputCount }} Ausgangsbelege | {{ summary.inputCount }} Eingangsbelege
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.tax-summary-top {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.75rem;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tax-summary-period {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 700;
|
||||||
|
color: rgb(17 24 39);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tax-summary-range,
|
||||||
|
.tax-summary-meta {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: rgb(107 114 128);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tax-summary-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
gap: 0.75rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tax-summary-label {
|
||||||
|
color: rgb(55 65 81);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tax-summary-value {
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.dark) .tax-summary-period {
|
||||||
|
color: rgb(243 244 246);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.dark) .tax-summary-range,
|
||||||
|
:deep(.dark) .tax-summary-meta,
|
||||||
|
:deep(.dark) .tax-summary-label {
|
||||||
|
color: rgb(156 163 175);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
155
frontend/composables/useChangelog.ts
Normal file
155
frontend/composables/useChangelog.ts
Normal 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)
|
||||||
162
frontend/composables/useTaxEvaluation.ts
Normal file
162
frontend/composables/useTaxEvaluation.ts
Normal 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)),
|
||||||
|
}
|
||||||
|
}
|
||||||
26
frontend/utils/handlebars.ts
Normal file
26
frontend/utils/handlebars.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import Handlebars from "handlebars"
|
||||||
|
|
||||||
|
const createDocumentTemplateHandlebars = () => {
|
||||||
|
const instance = Handlebars.create()
|
||||||
|
|
||||||
|
instance.registerHelper("eq", (left, right) => left === right)
|
||||||
|
instance.registerHelper("ne", (left, right) => left !== right)
|
||||||
|
instance.registerHelper("gt", (left, right) => left > right)
|
||||||
|
instance.registerHelper("gte", (left, right) => left >= right)
|
||||||
|
instance.registerHelper("lt", (left, right) => left < right)
|
||||||
|
instance.registerHelper("lte", (left, right) => left <= right)
|
||||||
|
instance.registerHelper("and", (...args) => args.slice(0, -1).every(Boolean))
|
||||||
|
instance.registerHelper("or", (...args) => args.slice(0, -1).some(Boolean))
|
||||||
|
instance.registerHelper("not", (value) => !value)
|
||||||
|
instance.registerHelper("includes", (collection, value) => {
|
||||||
|
if (Array.isArray(collection) || typeof collection === 'string') {
|
||||||
|
return collection.includes(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
return instance
|
||||||
|
}
|
||||||
|
|
||||||
|
export const documentTemplateHandlebars = createDocumentTemplateHandlebars()
|
||||||
Reference in New Issue
Block a user