Files
FEDEO/frontend/pages/createDocument/index.vue
root 0250cc0dd7
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 22s
Build and Push Docker Images / build-frontend (push) Successful in 1m15s
Build and Push Docker Images / build-central-services-api (push) Successful in 21s
Build and Push Docker Images / build-website (push) Successful in 23s
Build and Push Docker Images / build-central-services-admin (push) Successful in 22s
Build and Push Docker Images / build-docs (push) Successful in 21s
Pluszeichen aus Dokumentbuttons entfernen
2026-08-31 19:07:11 +00:00

460 lines
16 KiB
Vue

<template>
<UDashboardNavbar :badge="filteredRows.length" title="Ausgangsbelege">
<template #right>
<UInput
id="searchinput"
v-model="searchString"
autocomplete="off"
class="hidden lg:block"
icon="i-heroicons-funnel"
placeholder="Suche..."
@keydown.esc="$event.target.blur()"
>
<template #trailing>
<UKbd value="/"/>
</template>
</UInput>
<UButton
v-if="searchString.length > 0"
color="error"
icon="i-heroicons-x-mark"
variant="outline"
@click="clearSearchString()"
/>
<UButton icon="i-heroicons-plus" @click="modal.open(CreateDocumentModal)">
Dokument
</UButton>
<UButton icon="i-heroicons-document-duplicate" variant="outline" @click="modal.open(CreateDocumentFromTemplateModal)">
Dokument aus Vorlage
</UButton>
</template>
</UDashboardNavbar>
<UDashboardToolbar>
<template #right>
<USelectMenu
v-if="selectableFilters.length > 0"
v-model="selectedFilters"
:color="selectedFilters.length > 0 ? 'primary' : 'white'"
:items="selectableFilters"
:content="{ width: 'min-w-max' }"
icon="i-heroicons-adjustments-horizontal-solid"
multiple
@update:model-value="value => tempStore.modifyFilter('createddocumentsList', 'main', value)"
>
<template #label>
Filter
</template>
</USelectMenu>
</template>
</UDashboardToolbar>
<UTabs
v-model="activeTabIndex"
:items="selectedTypes"
class="m-3"
@update:model-value="storeActiveTab"
>
<template #default="{item}">
{{ item.label }}
<UBadge
class="ml-2"
variant="outline"
>
{{ getRowsForTab(item.key).length }}
</UBadge>
</template>
<template #content="{item}">
<div class="flex h-[80vh] flex-col">
<div class="min-h-0 flex-1 overflow-y-auto">
<UTable
:columns="normalizeTableColumns(getColumnsForTab(item.key))"
:data="getVisibleRowsForTab(item.key)"
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
class="w-full"
:watch-options="{ deep: false }"
:on-select="selectItem"
>
<template #type-cell="{row}">
<span v-if="row.original.type === 'cancellationInvoices'" class="text-cyan-500">{{
dataStore.documentTypesForCreation[row.original.type].labelSingle
}} für {{ filteredRows.find(i => row.original.createddocument?.id === i.id)?.documentNumber }}</span>
<span v-else>{{ dataStore.documentTypesForCreation[row.original.type].labelSingle }}</span>
</template>
<template #state-cell="{row}">
<span v-if="row.original.state === 'Entwurf'" class="text-rose-500">{{ row.original.state }}</span>
<span
v-if="row.original.state === 'Gebucht' && !hasCancellationInvoice(row.original)"
class="text-primary-500"
>
{{ row.original.state }}
</span>
<span
v-else-if="row.original.state === 'Gebucht' && hasCancellationInvoice(row.original) && ['invoices','advanceInvoices'].includes(row.original.type)"
class="text-cyan-500"
>
Storniert mit {{ getCancellationInvoice(row.original)?.documentNumber }}
</span>
<span v-else-if="row.original.state === 'Gebucht'" class="text-primary-500">{{ row.original.state }}</span>
</template>
<template #partner-cell="{row}">
<span v-if="row.original.customer && row.original.customer.name.length < 21">{{ row.original.customer ? row.original.customer.name : "" }}</span>
<UTooltip v-else-if="row.original.customer && row.original.customer.name.length > 20" :text="row.original.customer.name">
{{ row.original.customer.name.substring(0, 20) }}...
</UTooltip>
</template>
<template #reference-cell="{row}">
<span v-if="row.original === filteredRows[selectedItem]" class="text-primary-500 font-bold">{{ row.original.documentNumber }}</span>
<span v-else>{{ row.original.documentNumber }}</span>
</template>
<template #date-cell="{row}">
<span v-if="row.original.date">{{ row.original.date ? dayjs(row.original.date).format("DD.MM.YY") : '' }}</span>
<span v-if="row.original.documentDate">{{ row.original.documentDate ? dayjs(row.original.documentDate).format("DD.MM.YY") : '' }}</span>
</template>
<template #dueDate-cell="{row}">
<span
v-if="row.original.state === 'Gebucht' && row.original.paymentDays && ['invoices','advanceInvoices'].includes(row.original.type) && !hasCancellationInvoice(row.original)"
:class="dayjs(row.original.documentDate).add(row.original.paymentDays,'day').diff(dayjs()) <= 0 && !isPaid(row.original) ? ['text-rose-500'] : '' "
>
{{ row.original.documentDate ? dayjs(row.original.documentDate).add(row.original.paymentDays, 'day').format("DD.MM.YY") : '' }}
</span>
</template>
<template #paid-cell="{row}">
<div
v-if="(row.original.type === 'invoices' ||row.original.type === 'advanceInvoices') && row.original.state === 'Gebucht' && !hasCancellationInvoice(row.original)">
<span v-if="isPaid(row.original)" class="text-primary-500">Bezahlt</span>
<span v-else class="text-rose-600">Offen</span>
</div>
</template>
<template #amount-cell="{row}">
<span v-if="!deliveryNoteLikeDocumentTypes.includes(row.original.type)">{{ displayCurrency(getDocumentAmount(row.original)) }}</span>
</template>
<template #amountOpen-cell="{row}">
<span
v-if="!['cancellationInvoices','confirmationOrders', ...quoteLikeDocumentTypes, ...deliveryNoteLikeDocumentTypes].includes(row.original.type) && row.original.state !== 'Entwurf' && !hasCancellationInvoice(row.original) && !isPaid(row.original) ">
{{ displayCurrency(getDocumentOpenAmount(row.original)) }}
</span>
</template>
<template #empty>
<TableEmptyState label="Keine Belege anzuzeigen" />
</template>
</UTable>
</div>
<div
v-if="getRowsForTab(item.key).length > rowsPerPage"
class="flex items-center justify-center border-t border-gray-200 bg-white p-3 dark:border-gray-800 dark:bg-gray-900"
>
<UPagination
v-model:page="tabPages[item.key]"
:items-per-page="rowsPerPage"
:total="getRowsForTab(item.key).length"
:show-edges="true"
@update:page="page => storeTabPage(item.key, page)"
/>
</div>
</div>
</template>
</UTabs>
</template>
<script setup>
import CreateDocumentModal from "~/components/createDocumentModal.vue"
import CreateDocumentFromTemplateModal from "~/components/createDocumentFromTemplateModal.vue"
import dayjs from "dayjs";
import { ref, computed, reactive, watch } from 'vue';
const dataStore = useDataStore()
const tempStore = useTempStore()
const router = useRouter()
const modal = useModal()
const quoteLikeDocumentTypes = ['quotes', 'costEstimates']
const deliveryNoteLikeDocumentTypes = ['deliveryNotes', 'packingSlips']
const type = "createddocuments"
const dataType = dataStore.dataTypes[type]
const items = ref([])
const selectedItem = ref(0)
const rowsPerPage = 100
const storedListNavigation = tempStore.settings?.createddocumentsList || {}
const tabPages = reactive({
drafts: 1,
invoices: 1,
quotes: 1,
costEstimates: 1,
deliveryNotes: 1,
packingSlips: 1,
confirmationOrders: 1,
...(storedListNavigation.tabPages || {})
})
const sum = useSum()
// Debounce-Logik für die Suche
const searchString = ref(tempStore.searchStrings['createddocuments'] || '')
const debouncedSearchString = ref(searchString.value)
let debounceTimeout = null
watch(searchString, (newVal) => {
clearTimeout(debounceTimeout)
debounceTimeout = setTimeout(() => {
debouncedSearchString.value = newVal
tempStore.modifySearchString('createddocuments', newVal)
}, 300) // 300ms warten nach dem letzten Tastendruck
})
defineShortcuts({
'/': () => {
document.getElementById("searchinput").focus()
},
'+': () => {
modal.open(CreateDocumentModal)
},
'Enter': {
usingInput: true,
handler: () => {
if (filteredRows.value[selectedItem.value]) {
router.push(`/createDocument/show/${filteredRows.value[selectedItem.value].id}`)
}
}
},
'arrowdown': () => {
if (selectedItem.value < filteredRows.value.length - 1) {
selectedItem.value += 1
} else {
selectedItem.value = 0
}
},
'arrowup': () => {
if (selectedItem.value === 0) {
selectedItem.value = filteredRows.value.length - 1
} else {
selectedItem.value -= 1
}
}
})
const setupPage = async () => {
items.value = await useNuxtApp().$api('/api/resource/createddocuments/list', {
method: 'GET'
}) || []
}
setupPage()
const templateColumns = [
{key: "reference", label: "Referenz"},
{key: 'type', label: "Typ"},
{key: 'state', label: "Status"},
{key: "amount", label: "Betrag"},
{key: 'partner', label: "Kunde"},
{key: "date", label: "Datum"},
{key: "amountOpen", label: "Offener Betrag"},
{key: "paid", label: "Bezahlt"},
{key: "dueDate", label: "Fällig"}
]
const draftColumns = [
{key: 'type', label: "Typ"},
{key: 'state', label: "Status"},
{key: 'partner', label: "Kunde"},
{key: "date", label: "Erstellt am"},
{key: "amount", label: "Betrag"}
]
const selectedColumns = ref(tempStore.columns["createddocuments"] ? tempStore.columns["createddocuments"] : templateColumns)
const columns = computed(() => templateColumns.filter((column) => selectedColumns.value.find(i => i.key === column.key)))
const getColumnsForTab = (tabKey) => {
if (tabKey === 'drafts') {
return draftColumns
}
return columns.value
}
const templateTypes = [
{ key: "drafts", label: "Entwürfe" },
{ key: "invoices", label: "Rechnungen" },
{ key: "quotes", label: "Angebote" },
{ key: "costEstimates", label: "Kostenschätzungen" },
{ key: "deliveryNotes", label: "Lieferscheine" },
{ key: "packingSlips", label: "Packscheine" },
{ key: "confirmationOrders", label: "Auftragsbestätigungen" }
]
const selectedTypes = ref(tempStore.filters["createddocuments"] ? tempStore.filters["createddocuments"] : templateTypes)
const storedTabIndex = selectedTypes.value.findIndex(type => type.key === storedListNavigation.activeTabKey)
const activeTabIndex = ref(String(storedTabIndex >= 0 ? storedTabIndex : 0))
const types = computed(() => {
return templateTypes.filter((type) => selectedTypes.value.find(i => i.key === type.key))
})
const unwrapSelectedRow = (itemLike) => itemLike?.original || itemLike
const selectItem = (itemLike) => {
const item = unwrapSelectedRow(itemLike)
if (!item?.id) {
return
}
if (item.state === "Entwurf") {
router.push(`/createDocument/edit/${item.id}`)
} else {
router.push(`/createDocument/show/${item.id}`)
}
}
const displayCurrency = (value, currency = "€") => {
return `${Number(value).toFixed(2).replace(".", ",")} ${currency}`
}
const clearSearchString = () => {
tempStore.clearSearchString('createddocuments')
searchString.value = ''
debouncedSearchString.value = ''
}
const cancellationInvoicesByDocument = computed(() => {
const result = new Map()
items.value.forEach((item) => {
const linkedDocumentId = item.createddocument?.id || item.createddocument
if (item.type === 'cancellationInvoices' && item.state !== 'Entwurf' && !item.archived && linkedDocumentId) {
result.set(linkedDocumentId, item)
}
})
return result
})
const documentAmounts = computed(() => {
const result = new Map()
items.value.forEach((item) => {
const amount = sum.getCreatedDocumentSum(item, items.value)
const amountPaid = (item.statementallocations || []).reduce(
(total, allocation) => total + Number(allocation.amount || 0),
0
)
result.set(item.id, {
amount,
openAmount: Number((amount - amountPaid).toFixed(2))
})
})
return result
})
const getCancellationInvoice = (row) => cancellationInvoicesByDocument.value.get(row.id)
const hasCancellationInvoice = (row) => Boolean(getCancellationInvoice(row))
const getDocumentAmount = (row) => documentAmounts.value.get(row.id)?.amount || 0
const getDocumentOpenAmount = (row) => documentAmounts.value.get(row.id)?.openAmount || 0
const isPaid = (row) => getDocumentOpenAmount(row) === 0
const openUnpaidInvoicesFilter = {
name: 'Nur offene Belege',
filterFunction: (row) => {
return row.state === 'Entwurf' || (
['invoices', 'advanceInvoices'].includes(row.type)
&& row.state === 'Gebucht'
&& !hasCancellationInvoice(row)
&& !isPaid(row)
)
}
}
const availableFilters = computed(() => [...dataType.filters, openUnpaidInvoicesFilter])
const selectableFilters = computed(() => availableFilters.value.map(i => i.name))
const selectedFilters = ref(
tempStore.filters?.createddocumentsList?.main
|| dataType.filters.filter(i => i.default).map(i => i.name)
|| []
)
const filteredRows = computed(() => {
let tempItems = items.value.filter(i => types.value.find(x => {
if (x.key === 'drafts') return i.state === 'Entwurf'
if (i.state === 'Entwurf' && x.key !== 'drafts') return false
if (x.key === 'invoices') return ['invoices', 'advanceInvoices', 'cancellationInvoices'].includes(i.type)
return x.key === i.type
}))
tempItems = tempItems.filter(i => i.type !== "serialInvoices")
tempItems = tempItems.map(i => {
return {
...i,
class: i.archived ? 'bg-red-500/50 dark:bg-red-400/50' : null
}
})
if (selectedFilters.value.length > 0) {
selectedFilters.value.forEach(filterName => {
const filter = availableFilters.value.find(i => i.name === filterName)
if (filter?.filterFunction) {
tempItems = tempItems.filter(filter.filterFunction)
}
})
}
// Hier nutzen wir nun den debounced Wert für die lokale Suche
const results = useSearch(debouncedSearchString.value, tempItems.slice().reverse())
return results
})
const rowsByTab = computed(() => {
const result = Object.fromEntries(templateTypes.map(type => [type.key, []]))
filteredRows.value.forEach(row => {
if (row.state === 'Entwurf') {
result.drafts.push(row)
} else if (['invoices', 'advanceInvoices', 'cancellationInvoices'].includes(row.type)) {
result.invoices.push(row)
} else if (result[row.type]) {
result[row.type].push(row)
}
})
return result
})
const getRowsForTab = (tabKey) => rowsByTab.value[tabKey] || []
const storeListNavigation = () => {
const activeTab = selectedTypes.value[Number(activeTabIndex.value)]
tempStore.modifySettings('createddocumentsList', {
activeTabKey: activeTab?.key || selectedTypes.value[0]?.key,
tabPages: {...tabPages}
})
}
const storeActiveTab = () => {
storeListNavigation()
}
const storeTabPage = (tabKey, page) => {
tabPages[tabKey] = page
storeListNavigation()
}
const getVisibleRowsForTab = (tabKey) => {
const rows = getRowsForTab(tabKey)
const start = (tabPages[tabKey] - 1) * rowsPerPage
return rows.slice(start, start + rowsPerPage)
}
watch([debouncedSearchString, selectedFilters, selectedTypes], () => {
Object.keys(tabPages).forEach((key) => {
tabPages[key] = 1
})
storeListNavigation()
}, { deep: true })
</script>