Merge branch 'refs/heads/beta'

This commit is contained in:
2025-07-31 21:02:08 +02:00
27 changed files with 1201 additions and 265 deletions

View File

@@ -18,10 +18,13 @@ const setup = async () => {
if(await useCapacitor().getIsPhone()) {
platform.value = "mobile"
}
const dev = process.dev
console.log(dev)
}
setup()
const dev = process.dev
Sentry.init({
dsn: "https://62e62ff08e1a438591fe5eb4dd9de244@glitchtip.federspiel.software/3",

View File

@@ -2,6 +2,7 @@
import {useTempStore} from "~/stores/temp.js";
import FloatingActionButton from "~/components/mobile/FloatingActionButton.vue";
import EntityTable from "~/components/EntityTable.vue";
import EntityListMobile from "~/components/EntityListMobile.vue";
const props = defineProps({
type: {
@@ -181,7 +182,14 @@ const filteredRows = computed(() => {
</USelectMenu>
</template>
</UDashboardToolbar>
<EntityListMobile
v-if="platform === 'mobile'"
:type="props.type"
:columns="columns"
:rows="filteredRows"
/>
<EntityTable
v-else
:type="props.type"
:columns="columns"
:rows="filteredRows"

View File

@@ -0,0 +1,129 @@
<script setup>
/*defineShortcuts({
/!*'/': () => {
//console.log(searchinput)
//searchinput.value.focus()
document.getElementById("searchinput").focus()
},*!/
'Enter': {
usingInput: true,
handler: () => {
router.push(`/standardEntity/${props.type}/show/${props.rows.value[selectedItem.value].id}`)
}
},
'arrowdown': () => {
if(selectedItem.value < props.rows.length - 1) {
selectedItem.value += 1
} else {
selectedItem.value = 0
}
},
'arrowup': () => {
if(selectedItem.value === 0) {
selectedItem.value = props.rows.length - 1
} else {
selectedItem.value -= 1
}
}
})*/
const props = defineProps({
rows: {
type: Array,
required: true,
default: []
},
columns: {
type: Array,
required: true,
},
type: {
type: String,
required: true,
}
})
const dataStore = useDataStore()
const router = useRouter()
const dataType = dataStore.dataTypes[props.type]
const selectedItem = ref(0)
</script>
<template>
<UDashboardPanelContent class="w-full">
<a
v-for="item in props.rows"
class="my-1"
@click="router.push(`/standardEntity/${type}/show/${item.id}`)"
>
<p class="truncate text-left text-primary text-xl">{{dataType.templateColumns.find(i => i.title).key ? item[dataType.templateColumns.find(i => i.title).key] : null}}</p>
<p class="text-sm">
{{ dataType.numberRangeHolder ? item[dataType.numberRangeHolder] : null}}
<span v-for="secondInfo in dataType.templateColumns.filter(i => i.secondInfo)">{{(secondInfo.secondInfoKey && item[secondInfo.key]) ? item[secondInfo.key][secondInfo.secondInfoKey] : item[secondInfo.key]}}</span>
</p>
</a>
</UDashboardPanelContent>
<!-- <UTable
v-if="dataType && columns"
:rows="props.rows"
:columns="props.columns"
class="w-full"
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
@select="(i) => router.push(`/standardEntity/${type}/show/${i.id}`) "
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: `Keine ${dataType.label} anzuzeigen` }"
>
<template
v-for="column in dataType.templateColumns.filter(i => !i.disabledInTable)"
v-slot:[`${column.key}-header`]="{row}">
<span class="text-nowrap">{{column.label}}</span>
</template>
<template #name-data="{row}">
<span
v-if="row.id === props.rows[selectedItem].id"
class="text-primary-500 font-bold">{{row.name}}
</span>
<span v-else>
{{row.name}}
</span>
</template>
<template #fullName-data="{row}">
<span
v-if="row.id === props.rows[selectedItem].id"
class="text-primary-500 font-bold">{{row.fullName}}
</span>
<span v-else>
{{row.fullName}}
</span>
</template>
<template #licensePlate-data="{row}">
<span
v-if="row.id === props.rows[selectedItem].id"
class="text-primary-500 font-bold">{{row.licensePlate}}
</span>
<span v-else>
{{row.licensePlate}}
</span>
</template>
<template
v-for="column in dataType.templateColumns.filter(i => i.key !== 'name' && i.key !== 'fullName' && i.key !== 'licensePlate' && !i.disabledInTable)"
v-slot:[`${column.key}-data`]="{row}">
<component v-if="column.component" :is="column.component" :row="row"></component>
<span v-else>{{row[column.key] ? `${row[column.key]} ${column.unit ? column.unit : ''}`: ''}}</span>
</template>
</UTable>-->
</template>
<style scoped>
</style>

View File

@@ -115,6 +115,7 @@ setup()
:type="type"
:columns="columns"
:rows="props.item[type]"
style
/>
</UCard>

View File

@@ -68,27 +68,30 @@ setup()
const templateColumns = [
{
key: "reference",
label: "Referenz",
sortable: true
label: "Referenz"
},
{
key: 'type',
label: "Typ",
sortable: true
label: "Typ"
},{
key: 'state',
label: "Status",
sortable: true
label: "Status"
},{
key: 'paid',
label: "Bezahlt"
},{
key: 'amount',
label: "Betrag"
},
{
key: "date",
label: "Datum",
sortable: true
label: "Datum"
},
{
key: "dueDate",
label: "Fällig",
sortable: true
label: "Fällig"
}
]
const selectedColumns = ref(tempStore.columns["createddocuments"] ? tempStore.columns["createddocuments"] : templateColumns)
@@ -119,7 +122,14 @@ const getAvailableQueryStringData = (keys) => {
}
const invoiceDeliveryNotes = () => {
router.push(`/createDocument/edit?type=invoices&linkedDocuments=[${props.item.createddocuments.filter(i => i.type === "deliveryNotes").map(i => i.id)}]`)
router.push(`/createDocument/edit?type=invoices&loadMode=deliveryNotes&linkedDocuments=[${props.item.createddocuments.filter(i => i.type === "deliveryNotes").map(i => i.id)}]`)
}
const showFinalInvoiceConfig = ref(false)
const referenceDocument = ref(null)
const advanceInvoicesToAdd = ref([])
const invoiceAdvanceInvoices = () => {
router.push(`/createDocument/edit?type=invoices&loadMode=finalInvoice&linkedDocuments=[${[referenceDocument.value, ... advanceInvoicesToAdd.value]}]`)
}
const selectItem = (item) => {
@@ -166,6 +176,59 @@ const selectItem = (item) => {
>
+ Abschlagsrechnung
</UButton>
<UButton
@click="showFinalInvoiceConfig = true"
v-if="props.topLevelType === 'projects'"
:disabled="!props.item.createddocuments.filter(i => !i.archived && i.type === 'advanceInvoices').length > 0"
>
+ Schlussrechnung
</UButton>
<UModal
prevent-close
v-model="showFinalInvoiceConfig"
>
<UCard>
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
Schlussrechnung konfigurieren
</h3>
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="isOpen = false" />
</div>
</template>
<UFormGroup
label="Rechnungsvorlage"
>
<USelectMenu
:options="props.item.createddocuments.filter(i => ['confirmationOrders','quotes'].includes(i.type))"
value-attribute="id"
option-attribute="documentNumber"
v-model="referenceDocument"
/>
</UFormGroup>
<UFormGroup
label="Abschlagsrechnungen"
>
<USelectMenu
:options="props.item.createddocuments.filter(i => ['advanceInvoices'].includes(i.type))"
multiple
value-attribute="id"
option-attribute="documentNumber"
v-model="advanceInvoicesToAdd"
/>
</UFormGroup>
<template #footer>
<UButton
@click="invoiceAdvanceInvoices"
>
Weiter
</UButton>
</template>
</UCard>
</UModal>
<UButton
@click="router.push(`/createDocument/edit/?${getAvailableQueryStringData({type: 'invoices'})}`)"
>
@@ -195,6 +258,7 @@ const selectItem = (item) => {
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
@select="selectItem"
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Belege anzuzeigen' }"
style="height: 70vh"
>
<template #type-data="{row}">
{{dataStore.documentTypesForCreation[row.type].labelSingle}}
@@ -219,6 +283,12 @@ const selectItem = (item) => {
{{row.state}}
</span>
</template>
<template #paid-data="{row}">
<div v-if="(row.type === 'invoices' ||row.type === 'advanceInvoices') && row.state === 'Gebucht'">
<span v-if="useSum().getIsPaid(row,createddocuments)" class="text-primary-500">Bezahlt</span>
<span v-else class="text-rose-600">Offen</span>
</div>
</template>
<template #reference-data="{row}">
<span v-if="row === props.item.createddocuments[selectedItem]" class="text-primary-500 font-bold">{{row.documentNumber}}</span>
<span v-else>{{row.documentNumber}}</span>
@@ -230,11 +300,12 @@ const selectItem = (item) => {
<template #dueDate-data="{row}">
<span v-if="row.paymentDays && ['invoices','advanceInvoices'].includes(row.type)" >{{row.documentDate ? dayjs(row.documentDate).add(row.paymentDays,'day').format("DD.MM.YY") : ''}}</span>
</template>
<!-- <template #amount-data="{row}">
<template #amount-data="{row}">
<span v-if="row.type !== 'deliveryNotes'">{{useCurrency(useSum().getCreatedDocumentSum(row, createddocuments))}}</span>
</template>-->
</template>
</UTable>
</UCard>
</template>

View File

@@ -11,7 +11,16 @@ const setup = async () => {
data = data.filter((message) => message.profiles.length === 0)
globalMessages.value = data
if(data.length > 0) {
messageToShow.value = data[0]
showMessageModal.value = true
}
}
const showMessageModal = ref(false)
const messageToShow = ref(null)
@@ -24,15 +33,32 @@ const markMessageAsRead = async () => {
profile: profileStore.activeProfile.id,
message: messageToShow.value.id,
})
setup()
showMessageModal.value = false
setup()
}
setup()
</script>
<template>
<UCard
<UModal v-model="showMessageModal" prevent-close>
<UCard>
<template #header>
<span class="font-bold">{{messageToShow.title}}</span>
</template>
<p class=" my-2" v-html="messageToShow.description"></p>
<UButton
variant="outline"
@click="markMessageAsRead"
>Gelesen</UButton>
</UCard>
</UModal>
<!-- <UCard
v-if="globalMessages.length >0"
class="mt-3"
style="border: .75px solid #69c350"
@@ -55,7 +81,7 @@ setup()
>Gelesen</UButton>
</UCard>
</UModal>
</UCard>
</UCard>-->
</template>
<style scoped>

View File

@@ -125,7 +125,7 @@ const links = computed(() => {
icon: "i-heroicons-user-group",
children: [
... profileStore.ownTenant.features.timeTracking ? [{
label: "Zeiterfassung",
label: "Projektzeiten",
to: "/times",
icon: "i-heroicons-clock"
}] : [],

View File

@@ -12,10 +12,42 @@ let incomeData = ref({})
let expenseData = ref({})
const setup = async () => {
let incomeRawData = (await supabase.from("createddocuments").select().eq("tenant",profileStore.currentTenant).in('type',['invoices','cancellationInvoices'])).data
let incomeRawData = (await supabase.from("createddocuments").select().eq("tenant",profileStore.currentTenant).eq("state","Gebucht").in('type',['invoices','advanceInvoices','cancellationInvoices'])).data
console.log(incomeRawData)
let incomeRawFilteredData = incomeRawData.filter(x => x.state === 'Gebucht' && incomeRawData.find(i => i.linkedDocument && i.linkedDocument.id === x.id && i.type === 'cancellationInvoices') && ['invoices','advanceInvoices'].includes(row.type))
let expenseRawData =(await supabase.from("incominginvoices").select().eq("tenant",profileStore.currentTenant)).data
let withoutInvoiceRawData = (await supabase.from("statementallocations").select().eq("tenant",profileStore.currentTenant).not("account","is",null)).data
let withoutInvoiceRawDataExpenses = []
let withoutInvoiceRawDataIncomes = []
withoutInvoiceRawData.forEach(i => {
if(i.amount > 0) {
withoutInvoiceRawDataIncomes.push({
id: i.id,
date: dayjs(i.created_at).format("DD-MM-YY"),
amount: Math.abs(i.amount),
bs_id: i.bs_id
})
} else if(i.amount < 0) {
withoutInvoiceRawDataExpenses.push({
id: i.id,
date: dayjs(i.created_at).format("DD-MM-YY"),
amount: Math.abs(i.amount),
bs_id: i.bs_id
})
}
})
/*withoutInvoiceRawDataExpenses.forEach(i => {
expenseData.value[i.date] ? expenseData.value[i.date] = Number((expenseData.value[i.date] + i.amount).toFixed(2)) : expenseData.value[i.date] = i.amount
})
withoutInvoiceRawDataIncomes.forEach(i => {
incomeData.value[i.date] ? incomeData.value[i.date] = Number((incomeData.value[i.date] + i.amount).toFixed(2)) : incomeData.value[i.date] = i.amount
})*/
expenseRawData = expenseRawData.filter(i => i.date).map(i => {
let amount = 0
@@ -55,6 +87,8 @@ const setup = async () => {
}
Object.keys(expenseMonths).forEach(month => {
let dates = Object.keys(expenseData.value).filter(i => i.split("-")[1] === month && i.split("-")[2] === dayjs().format("YY"))
console.log(dates)
dates.forEach(date => {
if(expenseMonths[month]){

View File

@@ -12,23 +12,29 @@ let unpaidOverdueInvoicesCount = ref(0)
let draftInvoicesSum = ref(0)
let draftInvoicesCount = ref(0)
let countUnfinishedOpenIncomingInvoices = ref(0)
let countPreparedOpenIncomingInvoices = ref(0)
const setupPage = async () => {
let documents = (await useSupabaseSelect("createddocuments","*, statementallocations(*), customer(id,name)")).filter(i => i.type === "invoices" ||i.type === "advanceInvoices"||i.type === "cancellationInvoices").filter(i => !i.archived)
let items = (await useSupabaseSelect("createddocuments","*, statementallocations(*), customer(id,name), linkedDocument(*)")).filter(i => !i.archived)
let documents = items.filter(i => i.type === "invoices" ||i.type === "advanceInvoices")
let draftDocuments = documents.filter(i => i.state === "Entwurf")
let finalizedDocuments = documents.filter(i => i.state === "Gebucht")
finalizedDocuments = finalizedDocuments.filter(i => i.statementallocations.reduce((n,{amount}) => n + amount, 0).toFixed(2) !== useSum().getCreatedDocumentSum(i, documents).toFixed(2))
finalizedDocuments = finalizedDocuments.filter(x => (x.type === 'invoices' || x.type === 'advanceInvoices') && x.state === 'Gebucht' && !items.find(i => i.linkedDocument && i.linkedDocument.id === x.id))
console.log(finalizedDocuments)
finalizedDocuments.forEach(i => {
console.log(i)
//if(process.dev) console.log(i)
//if(process.dev) console.log(useSum().getCreatedDocumentSum(i, documents) - i.statementallocations.reduce((n,{amount}) => n + amount, 0))
if(dayjs().subtract(i.paymentDays,"days").isAfter(i.documentDate)) {
unpaidOverdueInvoicesSum.value += useSum().getCreatedDocumentSum(i, documents) - i.statementallocations.reduce((n,{amount}) => n + amount, 0)
unpaidOverdueInvoicesCount.value += 1
} else {
unpaidInvoicesSum.value += useSum().getCreatedDocumentSum(i, documents) - i.statementallocations.reduce((n,{amount}) => n + amount, 0)
unpaidInvoicesSum.value += useSum().getCreatedDocumentSum(i, items) - i.statementallocations.reduce((n,{amount}) => n + amount, 0)
unpaidInvoicesCount.value += 1
}
})
@@ -39,8 +45,7 @@ const setupPage = async () => {
})
draftInvoicesCount.value = draftDocuments.length
let filetype = (await supabase.from("filetags").select().eq("tenant",profileStore.currentTenant).eq("incomingDocumentType","invoices").single()).data.id
countUnfinishedOpenIncomingInvoices.value = (await supabase.from("files").select("id").eq("tenant",profileStore.currentTenant).eq("type", filetype).is("incominginvoice",null)).data.length
countPreparedOpenIncomingInvoices.value = (await supabase.from("incominginvoices").select("id").eq("tenant",profileStore.currentTenant).eq("state", "Vorbereitet")).data.length
}
@@ -62,7 +67,7 @@ setupPage()
<tr>
<td class="break-all">Überfällige Rechnungen:</td>
<td
v-if="unpaidOverdueInvoicesSum > 0"
v-if="unpaidOverdueInvoicesSum !== 0"
class="text-rose-600 font-bold text-nowrap"
>{{unpaidOverdueInvoicesCount}} Stk /<br> {{useCurrency(unpaidOverdueInvoicesSum)}}</td>
<td v-else class="text-primary-500 font-bold text-no-wrap">0 Stk / 0,00</td>
@@ -78,9 +83,9 @@ setupPage()
<tr>
<td class="break-all">ToDo Eingangsrechnungsrechnungen:</td>
<td
v-if="countUnfinishedOpenIncomingInvoices > 0"
v-if="countPreparedOpenIncomingInvoices > 0"
class="text-orange-500 font-bold text-nowrap"
>{{countUnfinishedOpenIncomingInvoices}} Stk </td>
>{{countPreparedOpenIncomingInvoices}} Stk </td>
<td v-else class="text-primary-500 font-bold text-no-wrap">0 Stk</td>
</tr>
</table>

View File

@@ -43,9 +43,9 @@ const startTime = async () => {
.select()
if(error) {
console.log(error)
toast.add({title: "Fehler beim starten der Zeit",color:"rose"})
toast.add({title: "Fehler beim starten der Projektzeit",color:"rose"})
} else if(data) {
toast.add({title: "Zeit erfolgreich gestartet"})
toast.add({title: "Projektzeit erfolgreich gestartet"})
runningTimeInfo.value = data[0]
//console.log(runningTimeInfo.value)
}
@@ -63,11 +63,11 @@ const stopStartedTime = async () => {
if(error) {
console.log(error)
let errorId = await useError().logError(`${status} - ${JSON.stringify(error)}`)
toast.add({title: errorId ? `Fehler beim stoppen der Zeit (Fehler ID: ${errorId})` : `Fehler beim stoppen der Zeit`,color:"rose"})
toast.add({title: errorId ? `Fehler beim stoppen der Projektzeit (Fehler ID: ${errorId})` : `Fehler beim stoppen der Projektzeit`,color:"rose"})
} else {
toast.add({title: "Zeit erfolgreich gestoppt"})
toast.add({title: "Projektzeit erfolgreich gestoppt"})
runningTimeInfo.value = {}
}
}
@@ -111,7 +111,7 @@ const stopStartedTime = async () => {
</UButton>
</div>
<div v-else>
<p>Keine Zeit gestartet</p>
<p>Keine Projektzeit gestartet</p>
<UButton
class="mt-3"
@click="startTime"

View File

@@ -123,8 +123,6 @@ export const useFiles = () => {
})
}
//console.log(data)
return data
}

View File

@@ -1,7 +1,7 @@
import axios from "axios";
import dayjs from "dayjs";
const baseURL = /*"http://localhost:3333"*/ "https://functions.fedeo.io"
const baseURL = /*"http://192.168.1.129:3333"*/ /*"http://localhost:3333"*/ "https://functions.fedeo.io"
export const useFunctions = () => {
const supabase = useSupabaseClient()
@@ -101,6 +101,26 @@ export const useFunctions = () => {
}
const useGetInvoiceData = async (file) => {
const {data:{session:{access_token}}} = await supabase.auth.getSession()
const {data} = await axios({
method: "POST",
url: `${baseURL}/functions/getinvoicedatafromgpt`,
data: {
file
},
headers: {
Authorization: `Bearer ${access_token}`
}
})
console.log(data)
return data
}
const useSendTelegramNotification = async (message) => {
const {data:{session:{access_token}}} = await supabase.auth.getSession()
@@ -152,5 +172,5 @@ export const useFunctions = () => {
}
return {getWorkingTimesEvaluationData, useNextNumber, useCreateTicket, useBankingGenerateLink, useBankingCheckInstitutions, useBankingListRequisitions, useCreatePDF, useSendTelegramNotification}
return {getWorkingTimesEvaluationData, useNextNumber, useCreateTicket, useBankingGenerateLink, useBankingCheckInstitutions, useBankingListRequisitions, useCreatePDF,useGetInvoiceData, useSendTelegramNotification}
}

View File

@@ -139,6 +139,13 @@ export const useSum = () => {
}
}
return {getIncomingInvoiceSum, getCreatedDocumentSum, getCreatedDocumentSumDetailed}
const getIsPaid = (createddocument,createddocuments) => {
let amountPaid = 0
createddocument.statementallocations.forEach(allocation => amountPaid += allocation.amount)
return Number(amountPaid.toFixed(2)) === getCreatedDocumentSum(createddocument,createddocuments)
}
return {getIncomingInvoiceSum, getCreatedDocumentSum, getCreatedDocumentSumDetailed, getIsPaid}
}

View File

@@ -167,12 +167,12 @@ const footerLinks = [/*{
variant="ghost"
:color="route.fullPath === '/standardEntity/projects' ? 'primary' : 'gray'"
/>
<UButton
<!-- <UButton
icon="i-heroicons-clock"
to="/workingtimes"
variant="ghost"
:color="route.fullPath === '/workingtimes' ? 'primary' : 'gray'"
/>
/>-->
<UButton
icon="i-heroicons-bars-4"
to="/mobile/menu"

View File

@@ -17,9 +17,6 @@ definePageMeta({
middleware: "auth"
})
const showProductSelectionModal = ref(false)
const showServiceSelectionModal = ref(false)
const itemInfo = ref({
type: "invoices",
@@ -107,7 +104,7 @@ const setupPage = async () => {
checkCompatibilityWithInputPrice()
}
if(itemInfo.value.project) checkForOpenAdvanceInvoices()
if(itemInfo.value.project) await checkForOpenAdvanceInvoices()
if(!itemInfo.value.deliveryDateType) itemInfo.value.deliveryDateType = "Lieferdatum"
@@ -132,74 +129,150 @@ const setupPage = async () => {
if(route.query.linkedDocuments) {
let linkedDocuments = (await supabase.from("createddocuments").select().in("id",JSON.parse(route.query.linkedDocuments))).data
console.log(route.query.loadMode)
//TODO: Implement Checking for Same Customer, Contact and Project
if(route.query.loadMode === "deliveryNotes") {
let linkedDocuments = (await supabase.from("createddocuments").select().in("id",JSON.parse(route.query.linkedDocuments))).data
itemInfo.value.customer = linkedDocuments[0].customer
itemInfo.value.project = linkedDocuments[0].project
itemInfo.value.contact = linkedDocuments[0].contact
//TODO: Implement Checking for Same Customer, Contact and Project
setCustomerData()
itemInfo.value.customer = linkedDocuments[0].customer
itemInfo.value.project = linkedDocuments[0].project
itemInfo.value.contact = linkedDocuments[0].contact
let firstDate = null
let lastDate = null
setCustomerData()
let firstDate = null
let lastDate = null
linkedDocuments.forEach(doc => {
let lastId = 0
itemInfo.value.rows.forEach(row => {
if(row.id > lastId) lastId = row.id
})
if(dayjs(doc.documentDate).isBefore(firstDate) || !firstDate) firstDate = doc.documentDate
if(dayjs(doc.documentDate).isAfter(lastDate) || !lastDate) lastDate = doc.documentDate
itemInfo.value.rows.push(...[
{
id:uuidv4(),
mode: "title",
text: `${doc.title} vom ${dayjs(doc.documentDate).format("DD.MM.YYYY")}`
},
...doc.rows
])
linkedDocuments.forEach(doc => {
let lastId = 0
itemInfo.value.rows.forEach(row => {
if(row.id > lastId) lastId = row.id
})
if(dayjs(doc.documentDate).isBefore(firstDate) || !firstDate) firstDate = doc.documentDate
if(dayjs(doc.documentDate).isAfter(lastDate) || !lastDate) lastDate = doc.documentDate
itemInfo.value.deliveryDateType = "Leistungszeitraum"
itemInfo.value.deliveryDate = firstDate
itemInfo.value.deliveryDateEnd = lastDate
itemInfo.value.rows.push(...[
{
itemInfo.value.rows.forEach(row => {
row.discountPercent = 0
setRowData(row)
})
setPosNumbers()
console.log(linkedDocuments)
if(linkedDocuments.find(i => i.rows.find( x => x.agriculture.dieselUsage))){
console.log("has diesel")
//Remove Existing Total Diesel Pos
itemInfo.value.rows = itemInfo.value.rows.filter(i => i.key !== "dieselPos")
//Remove Existing Total Ad Blue Pos
itemInfo.value.rows = itemInfo.value.rows.filter(i => i.key !== "adbluePos")
//Add Total Title
itemInfo.value.rows.push({
id:uuidv4(),
mode: "title",
text: `${doc.title} vom ${dayjs(doc.documentDate).format("DD.MM.YYYY")}`
},
...doc.rows
])
text: "Allgemein"
})
})
processDieselPosition()
}
itemInfo.value.deliveryDateType = "Leistungszeitraum"
itemInfo.value.deliveryDate = firstDate
itemInfo.value.deliveryDateEnd = lastDate
} else if(route.query.loadMode === "finalInvoice") {
let linkedDocuments = (await supabase.from("createddocuments").select().in("id",JSON.parse(route.query.linkedDocuments))).data
itemInfo.value.rows.forEach(row => {
//TODO: Implement Checking for Same Customer, Contact and Project
row.discountPercent = 0
console.log(linkedDocuments)
setRowData(row)
itemInfo.value.customer = linkedDocuments[0].customer
itemInfo.value.project = linkedDocuments[0].project
itemInfo.value.contact = linkedDocuments[0].contact
})
setCustomerData()
setPosNumbers()
for await (const doc of linkedDocuments.filter(i => i.type === "confirmationOrders")) {
let linkedDocument = await useSupabaseSelectSingle("createddocuments",doc.id)
console.log(linkedDocuments)
itemInfo.value.rows.push({
mode: "title",
text: linkedDocument.title,
})
if(linkedDocuments.find(i => i.rows.find( x => x.agriculture.dieselUsage))){
console.log("has diesel")
itemInfo.value.rows.push(...linkedDocument.rows)
}
//Remove Existing Total Diesel Pos
itemInfo.value.rows = itemInfo.value.rows.filter(i => i.key !== "dieselPos")
//Remove Existing Total Ad Blue Pos
itemInfo.value.rows = itemInfo.value.rows.filter(i => i.key !== "adbluePos")
for await (const doc of linkedDocuments.filter(i => i.type === "quotes")) {
let linkedDocument = await useSupabaseSelectSingle("createddocuments",doc.id)
itemInfo.value.rows.push({
mode: "title",
text: linkedDocument.title,
})
itemInfo.value.rows.push(...linkedDocument.rows)
}
//Add Total Title
itemInfo.value.rows.push({
id:uuidv4(),
mode: "title",
text: "Allgemein"
text: "Abschlagsrechnungen",
})
processDieselPosition()
for await (const doc of linkedDocuments.filter(i => i.type === "advanceInvoices")) {
itemInfo.value.rows.push({
mode: "free",
text: `Abschlagsrechnung ${doc.documentNumber}`,
quantity: 1,
taxPercent: 19, // TODO TAX PERCENTAGE
discountPercent: 0,
unit: 10,
inputPrice: useSum().getCreatedDocumentSumDetailed(doc).totalNet *-1,
linkedEntitys: [
{
type: "createddocuments",
subtype: "advanceInvoices",
id: doc.id
}
],
editDisabled: true
})
updateCustomSurcharge()
}
setPosNumbers()
}
}
if(route.query.linkedDocument) {
@@ -236,8 +309,11 @@ const setupPage = async () => {
} else {
// Import all
if(process.dev) console.log(linkedDocument)
itemInfo.value.taxType = linkedDocument.taxType
itemInfo.value.customer = linkedDocument.customer
await setCustomerData(null,true)
itemInfo.value.letterhead = linkedDocument.letterhead
itemInfo.value.contact = linkedDocument.contact
itemInfo.value.deliveryDateType = linkedDocument.deliveryDateType
@@ -256,7 +332,7 @@ const setupPage = async () => {
itemInfo.value.endText = linkedDocument.endText
}
setCustomerData(null,true)
checkCompatibilityWithInputPrice()
@@ -265,6 +341,12 @@ const setupPage = async () => {
row.price = row.price * -1
})
itemInfo.value.documentDate = dayjs()
itemInfo.value.usedAdvanceInvoices = linkedDocument.usedAdvanceInvoices
checkForOpenAdvanceInvoices()
itemInfo.value.description = `Stornorechnung zu Rechnung ${linkedDocument.documentNumber} vom ${dayjs(linkedDocument.documentDate).format('DD.MM.YYYY')}`
itemInfo.value.type = "cancellationInvoices"
@@ -310,11 +392,12 @@ setupPage()
const openAdvanceInvoices = ref([])
const checkForOpenAdvanceInvoices = async () => {
console.log("Check for Open Advance Invoices")
const {data,error} = await supabase.from("createddocuments").select().eq("project", itemInfo.value.project).eq("advanceInvoiceResolved", false).eq("type","advanceInvoices")
const {data} = await supabase.from("createddocuments").select().eq("project", itemInfo.value.project).eq("advanceInvoiceResolved", false).eq("type","advanceInvoices")
const {data: usedAdvanceInvoices} = await supabase.from("createddocuments").select().in("id", itemInfo.value.usedAdvanceInvoices)
console.log(data)
openAdvanceInvoices.value = data
openAdvanceInvoices.value = [...data, ...usedAdvanceInvoices.filter(i => !data.find(x => x.id === i.id))]
}
@@ -487,7 +570,8 @@ const addPosition = (mode) => {
inputPrice: 0,
price: 0,
taxPercent: taxPercentage,
discountPercent: 0
discountPercent: 0,
linkedEntitys: []
}
itemInfo.value.rows.push({...rowData, ...profileStore.ownTenant.extraModules.includes("agriculture") ? {agriculture: {}}: {}})
@@ -501,7 +585,8 @@ const addPosition = (mode) => {
price: 0,
taxPercent: taxPercentage,
discountPercent: 0,
unit: 1
unit: 1,
linkedEntitys: []
})
} else if(mode === 'service'){
let rowData = {
@@ -512,7 +597,8 @@ const addPosition = (mode) => {
price: 0,
taxPercent: taxPercentage,
discountPercent: 0,
unit: 1
unit: 1,
linkedEntitys: []
}
//Push Agriculture Holder only if Module is activated
@@ -521,16 +607,19 @@ const addPosition = (mode) => {
itemInfo.value.rows.push({
id: uuidv4(),
mode: "pagebreak",
linkedEntitys: []
})
} else if(mode === "title") {
itemInfo.value.rows.push({
id: uuidv4(),
mode: "title",
linkedEntitys: []
})
} else if(mode === "text") {
itemInfo.value.rows.push({
id: uuidv4(),
mode: "text",
linkedEntitys: []
})
}
@@ -546,15 +635,6 @@ const removePosition = (id) => {
}
const getRowMargin = (row) => {
if(row.mode === "normal" && row.product) {
let purchasePrice = products.value.find(i => i.id === row.product).purchasePrice || 0
return row.price - purchasePrice
} else {
return 0
}
}
const findDocumentErrors = computed(() => {
let errors = []
@@ -621,7 +701,7 @@ const findDocumentErrors = computed(() => {
}
return errors.sort((a,b) => (a.type === "breaking") ? -1 : 1)
return errors.sort((a) => (a.type === "breaking") ? -1 : 1)
})
const tabItems = computed(() => {
@@ -637,13 +717,18 @@ const tabItems = computed(() => {
})
const renderCurrency = (value, currency = "€") => {
return Number(value).toFixed(2).replace(".",",") + " €"
//return Number(value).toFixed(2).replace(".",",") + " " + currency
return useCurrency(value, currency)
}
const documentTotal = computed(() => {
let totalNet = 0
let total19 = 0
let totalNet19 = 0
let total7 = 0
let totalNet7 = 0
let total0 = 0
let totalNet0 = 0
itemInfo.value.rows.filter(i => !i.optional && !i.alternative).forEach(row => {
if(!['pagebreak','title','text'].includes(row.mode)){
@@ -652,8 +737,12 @@ const documentTotal = computed(() => {
if(row.taxPercent === 19) {
total19 = total19 + Number(rowPrice * 0.19)
totalNet19 += Number(rowPrice)
} else if(row.taxPercent === 7) {
total7 = total7 + Number(rowPrice * 0.07)
totalNet7 += Number(rowPrice)
} else if(row.taxPercent === 0) {
totalNet0 += Number(rowPrice)
}
}
})
@@ -661,17 +750,29 @@ const documentTotal = computed(() => {
//Title Sum
let titleSums = {}
let titleSumsTransfer = {}
let lastTitle = ""
let transferCounter = 0
itemInfo.value.rows.forEach(row => {
if(row.mode === 'title'){
titleSums[`${row.pos} - ${row.text}`] = 0
lastTitle = `${row.pos} - ${row.text}`
let title = `${row.pos} - ${row.text}`
titleSums[title] = 0
lastTitle = title
//Übertrag berechnen
titleSumsTransfer[Object.keys(titleSums)[row.pos-2]] = transferCounter
} else if(!['pagebreak','text'].includes(row.mode) && lastTitle !== "" && !row.optional && !row.alternative){
titleSums[lastTitle] = Number(titleSums[lastTitle]) + Number(Number(row.quantity) * Number(row.price) * (1 - Number(row.discountPercent) /100) )
transferCounter += Number(Number(row.quantity) * Number(row.price) * (1 - Number(row.discountPercent) /100) )
console.log(transferCounter)
}
})
console.log(titleSumsTransfer)
@@ -691,13 +792,33 @@ const documentTotal = computed(() => {
console.log(totalGrossAlreadyPaid)
let sumToPay = totalGross - totalGrossAlreadyPaid
let sumToPay = 0
if(itemInfo.value.type === "invoices") {
sumToPay = totalGross - totalGrossAlreadyPaid
} else if(itemInfo.value.type === "cancellationInvoices") {
sumToPay = totalGross + totalGrossAlreadyPaid
}
return {
titleSums: titleSums,
titleSumsTransfer: titleSumsTransfer,
totalNet: totalNet,
total19: total19,
totalNet19: totalNet19,
total7: total7,
totalNet7: totalNet7,
total0: total0,
totalNet0: totalNet0,
totalGross: totalGross,
totalGrossAlreadyPaid: totalGrossAlreadyPaid,
totalSumToPay: sumToPay
@@ -719,18 +840,15 @@ const documentReport = computed(() => {
itemInfo.value.rows.filter(i => !i.optional && !i.alternative).forEach(row => {
if(row.product) {
let product = products.value.find(i => i.id === row.product)
console.log(product.purchasePrice)
totalProductsPurchasePrice += product.purchasePrice * row.quantity
} else if(row.service) {
let service = services.value.find(i => i.id === row.service)
console.log(service)
if(service.materialComposition) {
service.materialComposition.forEach(entry => {
let productData = products.value.find(i => i.id === entry.product)
console.log(productData)
totalProductsFromServicesPurchasePrice += productData.purchasePrice * entry.quantity * row.quantity
})
@@ -930,7 +1048,15 @@ const getDocumentData = () => {
})
}
let returnTitleSumsTransfer = {}
if(Object.keys(documentTotal.value.titleSumsTransfer).length > 0) {
Object.keys(documentTotal.value.titleSumsTransfer).forEach(key => {
returnTitleSumsTransfer[key] = renderCurrency(documentTotal.value.titleSumsTransfer[key])
})
}
console.log(returnTitleSums)
console.log(returnTitleSumsTransfer)
const returnData = {
@@ -1013,13 +1139,37 @@ const getDocumentData = () => {
endText: templateEndText(generateContext(itemInfo.value, contactData)),
startText: templateStartText(generateContext(itemInfo.value, contactData)),
rows: rows,
totalArray: [
{
label: "Nettobetrag",
content: renderCurrency(documentTotal.value.totalNet),
},
... rows.find(i => i.taxPercent === 19) ? [{
label: `zzgl. 19% USt auf ${renderCurrency(documentTotal.value.totalNet19)}`,
content: renderCurrency(documentTotal.value.total19),
}] : [],
... rows.find(i => i.taxPercent === 7) ? [{
label: `zzgl. 7% USt auf ${renderCurrency(documentTotal.value.totalNet7)}`,
content: renderCurrency(documentTotal.value.total7),
}]: [],
...rows.find(i => i.taxPercent === 0) ? [{
label: `zzgl. 0% USt auf ${renderCurrency(documentTotal.value.totalNet0)}`,
content: renderCurrency(documentTotal.value.total0),
}] : [],
{
label: "Gesamtbetrag",
content: renderCurrency(documentTotal.value.totalGross),
},
],
total: {
totalNet: renderCurrency(documentTotal.value.totalNet),
total19: renderCurrency(documentTotal.value.total19),
total0: renderCurrency(documentTotal.value.total0),
totalGross: renderCurrency(documentTotal.value.totalGross),
totalGrossAlreadyPaid: renderCurrency(documentTotal.value.totalGrossAlreadyPaid),
totalSumToPay: renderCurrency(documentTotal.value.totalSumToPay),
titleSums: returnTitleSums
titleSums: returnTitleSums,
titleSumsTransfer: returnTitleSumsTransfer
},
agriculture: itemInfo.value.agriculture,
usedAdvanceInvoices: itemInfo.value.usedAdvanceInvoices.map(i => {
@@ -1035,7 +1185,6 @@ const getDocumentData = () => {
const showDocument = ref(false)
const uri = ref("")
const generateDocument = async () => {
const ownTenant = profileStore.ownTenant
const path = letterheads.value.find(i => i.id === itemInfo.value.letterhead).path
/*const {data,error} = await supabase.functions.invoke('create_pdf',{
@@ -1102,7 +1251,7 @@ const saveSerialInvoice = async () => {
address: itemInfo.value.address,
project: itemInfo.value.project,
paymentDays: itemInfo.value.paymentDays,
deliveryDateType: itemInfo.value.deliveryDateType,
deliveryDateType: "Leistungszeitraum",
createdBy: itemInfo.value.createdBy,
title: itemInfo.value.title,
description: itemInfo.value.description,
@@ -1179,7 +1328,7 @@ const saveDocument = async (state,resetup = false) => {
let createData = {
type: itemInfo.value.type,
taxType: (itemInfo.value.type === "invoices" || itemInfo.value.type === "quotes") ? itemInfo.value.taxType : null,
taxType: ['invoices','cancellationInvoices','advanceInvoices','qoutes','confirmationOrders'].includes(itemInfo.value.type) ? itemInfo.value.taxType : null,
state: itemInfo.value.state || "Entwurf",
customer: itemInfo.value.customer,
contact: itemInfo.value.contact,
@@ -1281,7 +1430,6 @@ const getTextTemplateByType = (type, pos) => {
const checkCompatibilityWithInputPrice = () => {
itemInfo.value.rows.forEach(row => {
console.log(row)
if(!row.inputPrice) {
row.inputPrice = row.price
}
@@ -1290,13 +1438,15 @@ const checkCompatibilityWithInputPrice = () => {
const updateCustomSurcharge = () => {
itemInfo.value.rows.forEach(row => {
if(!["pagebreak","title","text"].includes(row.mode)) {
if(!["pagebreak","title","text"].includes(row.mode) /*&& !row.linkedEntitys.find(i => i.type === "createddocuments" && i.subtype === "advanceInvoices")*/) {
//setRowData(row)
row.price = Number((row.inputPrice * (1 + itemInfo.value.customSurchargePercentage /100)).toFixed(2))
}
}/* else if(!["pagebreak","title","text"].includes(row.mode) /!*&& row.linkedEntitys.find(i => i.type === "createddocuments" && i.subtype === "advanceInvoices")*!/) {
row.price = row.inputPrice
}*/
})
}
@@ -1493,10 +1643,10 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
<UFormGroup
label="Steuertyp:"
v-if="['invoices','quotes','confirmationOrders'].includes(itemInfo.type)"
v-if="['invoices','advanceInvoices','quotes','confirmationOrders'].includes(itemInfo.type)"
>
<USelectMenu
:options="[{key:'Standard', label: 'Standard'},{key:'13b UStG', label: '13b UStG'},{key:'19 UStG', label: '19 UStG Kleinunternehmer'},{key:'12.3 UStG', label: 'PV 0% USt'}]"
:options="[{key:'Standard', label: 'Standard'},{key:'13b UStG', label: '13b UStG'},{key:'19 UStG', label: '19 UStG Kleinunternehmer'}/*,{key:'12.3 UStG', label: 'PV 0% USt'}*/]"
v-model="itemInfo.taxType"
value-attribute="key"
option-attribute="label"
@@ -1618,7 +1768,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
</UFormGroup>
<UFormGroup
label="Ansprechpartner:"
v-if="itemInfo.customer ? dataStore.getCustomerById(itemInfo.customer).isCompany : false "
v-if="itemInfo.customer ? customers.find(i => i.id === itemInfo.customer).isCompany : false "
>
<InputGroup>
<USelectMenu
@@ -1637,13 +1787,13 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
class="w-full"
:disabled="!itemInfo.customer"
>
<span class="truncate">{{dataStore.getContactById(itemInfo.contact) ? dataStore.getContactById(itemInfo.contact).fullName : "Kein Kontakt ausgewählt"}}</span>
<span class="truncate">{{itemInfo.contact ? contacts.find(i => i.id === itemInfo.contact).fullName : "Kein Kontakt ausgewählt"}}</span>
<UIcon name="i-heroicons-chevron-right-20-solid" class="w-5 h-5 transition-transform text-gray-400 dark:text-gray-500" :class="['transform rotate-90']" />
</UButton>
<template #label>
{{dataStore.getContactById(itemInfo.contact) ? dataStore.getContactById(itemInfo.contact).fullName : "Kein Kontakt ausgewählt"}}
{{ itemInfo.contact ? contacts.find(i => i.id === itemInfo.contact).fullName : "Kein Kontakt ausgewählt"}}
</template>
</USelectMenu>
<!-- <UButton
@@ -1655,7 +1805,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
<EntityModalButtons
type="contacts"
:id="itemInfo.contact"
:create-query="{customerId: itemInfo.customer}"
:create-query="{customer: itemInfo.customer}"
@return-data="(data) => itemInfo.contact = data.id"
/>
</InputGroup>
@@ -1667,25 +1817,25 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
>
<UInput
v-model="itemInfo.address.street"
:placeholder="dataStore.getCustomerById(itemInfo.customer) ? dataStore.getCustomerById(itemInfo.customer).infoData.street : 'Straße + Hausnummer'"
:placeholder="itemInfo.customer ? customers.find(i => i.id === itemInfo.customer).infoData.street : 'Straße + Hausnummer'"
:color="itemInfo.address.street ? 'primary' : 'rose'"
/>
<UInput
v-model="itemInfo.address.special"
class="mt-3"
:placeholder="dataStore.getCustomerById(itemInfo.customer) ? dataStore.getCustomerById(itemInfo.customer).infoData.special : 'Adresszusatz'"
:placeholder="itemInfo.customer ? customers.find(i => i.id === itemInfo.customer).infoData.special : 'Adresszusatz'"
/>
<InputGroup class="mt-3">
<UInput
class="flex-auto"
v-model="itemInfo.address.zip"
:placeholder="dataStore.getCustomerById(itemInfo.customer) ? dataStore.getCustomerById(itemInfo.customer).infoData.zip : 'PLZ'"
:placeholder="itemInfo.customer ? customers.find(i => i.id === itemInfo.customer).infoData.zip : 'PLZ'"
:color="itemInfo.address.zip ? 'primary' : 'rose'"
/>
<UInput
class="flex-auto"
v-model="itemInfo.address.city"
:placeholder="dataStore.getCustomerById(itemInfo.customer) ? dataStore.getCustomerById(itemInfo.customer).infoData.city : 'Ort'"
:placeholder="itemInfo.customer ? customers.find(i => i.id === itemInfo.customer).infoData.city : 'Ort'"
:color="itemInfo.address.city ? 'primary' : 'rose'"
/>
</InputGroup>
@@ -1880,7 +2030,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
@change="checkForOpenAdvanceInvoices"
>
<template #label>
{{dataStore.getProjectById(itemInfo.project) ? dataStore.getProjectById(itemInfo.project).name : "Kein Projekt ausgewählt"}}
{{itemInfo.project ? projects.find(i => i.id === itemInfo.project).name : "Kein Projekt ausgewählt"}}
</template>
<template #option="{option: project}">
{{dataStore.getCustomerById(project.customer).name}} - {{project.name}}
@@ -2060,11 +2210,12 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
<UIcon
class="handle"
name="i-mdi-menu"
v-if="itemInfo.type !== 'cancellationInvoices'"
/>
</td>
<td
v-if="row.mode === 'pagebreak'"
colspan="9"
colspan="7"
>
<UDivider/>
</td>
@@ -2090,6 +2241,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
v-if="row.mode === 'free'"
>
<UInput
:disabled="itemInfo.type === 'cancellationInvoices' || row.editDisabled"
v-model="row.text"
placeholder="Name"
class="min-w-40"
@@ -2101,6 +2253,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
>
<InputGroup class="w-full">
<USelectMenu
:disabled="itemInfo.type === 'cancellationInvoices'"
class="w-60"
:options="products"
:color="row.product ? 'primary' : 'rose'"
@@ -2170,6 +2323,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
>
<InputGroup class="w-full">
<USelectMenu
:disabled="itemInfo.type === 'cancellationInvoices'"
class="w-60"
:options="services"
:color="row.service ? 'primary' : 'rose'"
@@ -2236,6 +2390,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
>
<UInput
v-model="row.quantity"
:disabled="itemInfo.type === 'cancellationInvoices'"
type="number"
:step="dataStore.units.find(i => i.id === row.unit) ? dataStore.units.find(i => i.id === row.unit).step : '1' "
@@ -2247,6 +2402,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
>
<USelectMenu
v-model="row.unit"
:disabled="itemInfo.type === 'cancellationInvoices'"
:options="dataStore.units"
option-attribute="name"
value-attribute="id"
@@ -2262,6 +2418,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
>
<UInput
v-model="row.inputPrice"
:disabled="itemInfo.type === 'cancellationInvoices'"
type="number"
step="0.001"
@change="updateCustomSurcharge"
@@ -2325,10 +2482,12 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
v-if="!['pagebreak','title','text'].includes(row.mode)"
>
<UButton
:disabled="itemInfo.type === 'cancellationInvoices'"
icon="i-heroicons-pencil-square-16-solid"
@click="row.showEdit = true"
/>
<UButton
:disabled="itemInfo.type === 'cancellationInvoices'"
icon="i-mdi-water-drop-outline"
class="ml-3"
v-if="row.agriculture"
@@ -2347,6 +2506,36 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="row.showEdit = false" />
</div>
</template>
<InputGroup>
<UFormGroup
label="Anzahl:"
class="flex-auto"
>
<UInput
v-model="row.quantity"
type="number"
:step="dataStore.units.find(i => i.id === row.unit) ? dataStore.units.find(i => i.id === row.unit).step : '1' "
/>
</UFormGroup>
<UFormGroup
label="Einheit:"
class="flex-auto"
>
<USelectMenu
v-model="row.unit"
:options="dataStore.units"
option-attribute="name"
value-attribute="id"
>
<template #label>
{{dataStore.units.find(i => i.id === row.unit) ? dataStore.units.find(i => i.id === row.unit).name : "Keine Einheit gewählt"}}
</template>
</USelectMenu>
</UFormGroup>
</InputGroup>
<UFormGroup
label="Einzelpreis:"
v-if="itemInfo.type !== 'deliveryNotes'"
@@ -2432,6 +2621,26 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
</UFormGroup>
</InputGroup>
<UAlert
v-if="row.linkedEntitys && row.linkedEntitys.find(i => i.type === 'createddocuments' && i.subtype === 'advanceInvoices')"
title="Berechnung des Individuellen Aufschlags für die Zeile gesperrt"
color="orange"
variant="subtle"
class="my-3"
/>
Verknüpfungen:
<ul>
<li
v-for="linkedEntity in row.linkedEntitys"
>
- <a
v-if="linkedEntity.subtype === 'advanceInvoices'"
:to="`/createddocuments/show/${linkedEntity.id}`"
>Abschlagsrechnung</a>
</li>
</ul>
<UFormGroup
label="Beschreibung:"
class="mt-3"
@@ -2546,12 +2755,14 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
colspan="6"
>
<UInput
:disabled="itemInfo.type === 'cancellationInvoices'"
v-model="row.text"
placeholder="Titel"
/>
</td>
<td>
<UButton
:disabled="itemInfo.type === 'cancellationInvoices'"
variant="ghost"
color="rose"
icon="i-heroicons-x-mark-16-solid"
@@ -2574,47 +2785,50 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
<UButton
@click="addPosition('service')"
class="mt-3"
:disabled="itemInfo.type === 'advanceInvoices'"
:disabled="['advanceInvoices','cancellationInvoices'].includes(itemInfo.type)"
>
+ Leistung
</UButton>
<UButton
@click="addPosition('normal')"
class="mt-3"
:disabled="itemInfo.type === 'advanceInvoices'"
:disabled="['advanceInvoices','cancellationInvoices'].includes(itemInfo.type)"
>
+ Artikel
</UButton>
<UButton
@click="addPosition('free')"
class="mt-3"
:disabled="itemInfo.type === 'advanceInvoices'"
:disabled="['advanceInvoices','cancellationInvoices'].includes(itemInfo.type)"
>
+ Freie Position
</UButton>
<UButton
@click="addPosition('pagebreak')"
class="mt-3"
:disabled="['advanceInvoices','cancellationInvoices'].includes(itemInfo.type)"
>
+ Seitenumbruch
</UButton>
<UButton
@click="addPosition('title')"
class="mt-3"
:disabled="['advanceInvoices','cancellationInvoices'].includes(itemInfo.type)"
>
+ Titel
</UButton>
<UButton
@click="addPosition('text')"
class="mt-3"
:disabled="['advanceInvoices','cancellationInvoices'].includes(itemInfo.type)"
>
+ Text
</UButton>
</InputGroup>
<UDivider
<!-- <UDivider
class="mt-5 mb-3"
v-if="openAdvanceInvoices.length > 0"
v-if="openAdvanceInvoices.length > 0 || itemInfo.usedAdvanceInvoices.length > 0"
>
Noch nicht abgerechnete Abschlagsrechnungen
</UDivider>
@@ -2643,7 +2857,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
X
</UButton>
</InputGroup>
</div>
</div>-->
<UDivider class="my-3" v-if="Object.keys(documentTotal.titleSums).length > 0">Überschriften</UDivider>
@@ -2693,10 +2907,18 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
<td class="font-bold">Netto:</td>
<td class="text-right">{{renderCurrency(documentTotal.totalNet)}}</td>
</tr>
<tr v-if="itemInfo.taxType === 'Standard'">
<td class="font-bold">zzgl. 19 % USt:</td>
<tr v-if="itemInfo.taxType === 'Standard' && itemInfo.rows.find(i => i.taxPercent === 19)">
<td class="font-bold">zzgl. 19 % USt auf {{renderCurrency(documentTotal.totalNet19)}}:</td>
<td class="text-right">{{renderCurrency(documentTotal.total19)}}</td>
</tr>
<tr v-if="itemInfo.taxType === 'Standard' && itemInfo.rows.find(i => i.taxPercent === 7)">
<td class="font-bold">zzgl. 7 % USt auf {{renderCurrency(documentTotal.totalNet7)}}:</td>
<td class="text-right">{{renderCurrency(documentTotal.total7)}}</td>
</tr>
<tr v-if="itemInfo.taxType === 'Standard' && itemInfo.rows.find(i => i.taxPercent === 0)">
<td class="font-bold">zzgl. 0 % USt auf {{renderCurrency(documentTotal.totalNet0)}}:</td>
<td class="text-right">{{renderCurrency(documentTotal.total0)}}</td>
</tr>
<tr>
<td class="font-bold">Brutto:</td>
<td class="text-right">{{renderCurrency(documentTotal.totalGross)}}</td>

View File

@@ -9,11 +9,19 @@
placeholder="Suche..."
class="hidden lg:block"
@keydown.esc="$event.target.blur()"
@change="tempStore.modifySearchString('createddocuments',searchString)"
>
<template #trailing>
<UKbd value="/" />
</template>
</UInput>
<UButton
icon="i-heroicons-x-mark"
variant="outline"
color="rose"
@click="clearSearchString()"
v-if="searchString.length > 0"
/>
<UButton
@click="router.push(`/createDocument/edit`)"
>
@@ -72,62 +80,85 @@
</template>
</UDashboardToolbar>
<UTable
:rows="filteredRows"
:columns="columns"
class="w-full"
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
@select="selectItem"
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Belege anzuzeigen' }"
>
<template #type-data="{row}">
{{dataStore.documentTypesForCreation[row.type].labelSingle}}
</template>
<template #state-data="{row}">
<span
v-if="row.state === 'Entwurf'"
class="text-rose-500"
<UTabs :items="selectedTypes" class="m-3">
<template #item="{item}">
<div style="height: 80vh; overflow-y: scroll">
<UTable
:rows="filteredRows.filter(i => item.key === 'invoices' ? ['invoices','advanceInvoices','cancellationInvoices'].includes(i.type) : item.key === i.type)"
:columns="columns"
class="w-full"
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
@select="selectItem"
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Belege anzuzeigen' }"
>
{{row.state}}
</span>
<template #type-data="{row}">
{{dataStore.documentTypesForCreation[row.type].labelSingle}}
<span v-if="row.type === 'cancellationInvoices'"> zu {{row.linkedDocument.documentNumber}}</span>
</template>
<template #state-data="{row}">
<span
v-if="row.state === 'Gebucht'"
class="text-cyan-500"
v-if="row.state === 'Entwurf'"
class="text-rose-500"
>
{{row.state}}
</span>
<!-- <span
v-if="row.state === 'Gebucht'"
class="text-cyan-500"
>
{{row.state}}
</span>-->
<span
v-if="row.state === 'Gebucht' && !items.find(i => i.linkedDocument && i.linkedDocument.id === row.id)"
class="text-primary-500"
>
{{row.state}}
</span>
<span
v-if="row.state === 'Abgeschlossen'"
class="text-primary-500"
>
</span>
<span
v-else-if="row.state === 'Gebucht' && items.find(i => i.linkedDocument && i.linkedDocument.id === row.id && i.type === 'cancellationInvoices') && ['invoices','advanceInvoices'].includes(row.type)"
class="text-cyan-500"
>
Storniert mit {{items.find(i => i.linkedDocument && i.linkedDocument.id === row.id).documentNumber}}
</span>
<span
v-else-if="row.state === 'Gebucht'"
class="text-primary-500"
>
{{row.state}}
</span>
</template>
<template #partner-data="{row}">
<span v-if="row.customer">{{row.customer ? row.customer.name : ""}}</span>
</span>
</template>
<template #partner-data="{row}">
<span v-if="row.customer">{{row.customer ? row.customer.name : ""}}</span>
</template>
<template #reference-data="{row}">
<span v-if="row === filteredRows[selectedItem]" class="text-primary-500 font-bold">{{row.documentNumber}}</span>
<span v-else>{{row.documentNumber}}</span>
</template>
<template #date-data="{row}">
<span v-if="row.date">{{row.date ? dayjs(row.date).format("DD.MM.YY") : ''}}</span>
<span v-if="row.documentDate">{{row.documentDate ? dayjs(row.documentDate).format("DD.MM.YY") : ''}}</span>
</template>
<template #dueDate-data="{row}">
<span v-if="row.state === 'Gebucht' && row.paymentDays && ['invoices','advanceInvoices'].includes(row.type) && !items.find(i => i.linkedDocument && i.linkedDocument.id === row.id)" :class="dayjs(row.documentDate).add(row.paymentDays,'day').diff(dayjs()) <= 0 && !isPaid(row) ? ['text-rose-500'] : '' ">{{row.documentDate ? dayjs(row.documentDate).add(row.paymentDays,'day').format("DD.MM.YY") : ''}}</span>
</template>
<template #paid-data="{row}">
<div v-if="(row.type === 'invoices' ||row.type === 'advanceInvoices') && row.state === 'Gebucht' && !items.find(i => i.linkedDocument && i.linkedDocument.id === row.id)">
<span v-if="useSum().getIsPaid(row,items)" class="text-primary-500">Bezahlt</span>
<span v-else class="text-rose-600">Offen</span>
</div>
</template>
<template #amount-data="{row}">
<span v-if="row.type !== 'deliveryNotes'">{{displayCurrency(useSum().getCreatedDocumentSum(row,items))}}</span>
</template>
<template #amountOpen-data="{row}">
<span 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))}}</span>
</template>
</UTable>
</div>
</template>
<template #reference-data="{row}">
<span v-if="row === filteredRows[selectedItem]" class="text-primary-500 font-bold">{{row.documentNumber}}</span>
<span v-else>{{row.documentNumber}}</span>
</template>
<template #date-data="{row}">
<span v-if="row.date">{{row.date ? dayjs(row.date).format("DD.MM.YY") : ''}}</span>
<span v-if="row.documentDate">{{row.documentDate ? dayjs(row.documentDate).format("DD.MM.YY") : ''}}</span>
</template>
<template #dueDate-data="{row}">
<span v-if="row.state === 'Gebucht' && row.paymentDays && ['invoices','advanceInvoices'].includes(row.type)" :class="dayjs(row.documentDate).add(row.paymentDays,'day').diff(dayjs()) <= 0 && !isPaid(row) ? ['text-rose-500'] : '' ">{{row.documentDate ? dayjs(row.documentDate).add(row.paymentDays,'day').format("DD.MM.YY") : ''}}</span>
</template>
<template #paid-data="{row}">
<div v-if="(row.type === 'invoices' ||row.type === 'advanceInvoices') && row.state === 'Gebucht'">
<span v-if="isPaid(row)" class="text-primary-500">Bezahlt</span>
<span v-else class="text-rose-600">Offen</span>
</div>
</template>
<template #amount-data="{row}">
<span v-if="row.type !== 'deliveryNotes'">{{displayCurrency(useSum().getCreatedDocumentSum(row,items))}}</span>
</template>
</UTable>
</UTabs>
</template>
@@ -177,7 +208,7 @@ const items = ref([])
const selectedItem = ref(0)
const setupPage = async () => {
items.value = (await useSupabaseSelect("createddocuments","*, customer(id,name), statementallocations(id,amount)","documentNumber")).filter(i => !i.archived)
items.value = (await useSupabaseSelect("createddocuments","*, customer(id,name), statementallocations(id,amount),linkedDocument(*)","documentNumber")).filter(i => !i.archived)
}
setupPage()
@@ -212,6 +243,11 @@ const templateColumns = [
label: "Datum",
sortable: true
},
{
key: "amountOpen",
label: "Offener Betrag",
sortable: true
},
{
key: "paid",
label: "Bezahlt",
@@ -230,13 +266,13 @@ const templateTypes = [
{
key: "invoices",
label: "Rechnungen"
},{
}/*,{
key: "cancellationInvoices",
label: "Stornorechnungen"
},{
key: "advanceInvoices",
label: "Abschlagsrechnungen"
}, {
}*/, {
key: "quotes",
label: "Angebote"
}, {
@@ -267,11 +303,16 @@ const displayCurrency = (value, currency = "€") => {
}
const searchString = ref('')
const searchString = ref(tempStore.searchStrings['createddocuments'] ||'')
const clearSearchString = () => {
tempStore.clearSearchString('createddocuments')
searchString.value = ''
}
const selectedFilters = ref([])
const filteredRows = computed(() => {
let temp = items.value.filter(i => types.value.find(x => x.key === i.type))
let temp = items.value.filter(i => types.value.find(x => x.key === 'invoices' ? ['invoices','advanceInvoices','cancellationInvoices'].includes(i.type) : x.key === i.type))
temp = temp.filter(i => i.type !== "serialInvoices")
/*if(showDrafts.value === true) {
@@ -289,17 +330,6 @@ const filteredRows = computed(() => {
})
const calculateDocSum = (row) => {
let sum = 0
row.rows.forEach(row => {
if(row.mode === "normal" || row.mode === "service" || row.mode === "free") {
sum += row.quantity * row.price * (1 - row.discountPercent / 100) * (1 + (row.taxPercent || 0) / 100)
}
})
return sum.toFixed(2)
}
const isPaid = (item) => {
let amountPaid = 0

View File

@@ -3,6 +3,19 @@
<template>
<UDashboardNavbar title="Serienrechnungen" :badge="filteredRows.length">
<template #right>
<UInput
id="searchinput"
v-model="searchString"
icon="i-heroicons-funnel"
autocomplete="off"
placeholder="Suche..."
class="hidden lg:block"
@keydown.esc="$event.target.blur()"
>
<template #trailing>
<UKbd value="/" />
</template>
</UInput>
<UButton
@click="router.push(`/createDocument/edit?type=serialInvoices`)"
>

View File

@@ -189,7 +189,11 @@ const sendEmail = async () => {
</template>
</UDashboardNavbar>
<UDashboardToolbar>
<div class="scrollContainer mt-3">
<div class="flex-col flex w-full">
<UFormGroup
label="Absender"
@@ -203,7 +207,7 @@ const sendEmail = async () => {
</UFormGroup>
<UDivider class="my-3"/>
<UFormGroup
label="Empfänger"
label="Empfänger"
>
<UInput
class="w-full my-1"
@@ -211,7 +215,7 @@ const sendEmail = async () => {
/>
</UFormGroup>
<UFormGroup
label="Kopie"
label="Kopie"
>
<UInput
class="w-full my-1"
@@ -219,7 +223,7 @@ const sendEmail = async () => {
/>
</UFormGroup>
<UFormGroup
label="Blindkopie"
label="Blindkopie"
>
<UInput
class="w-full my-1"
@@ -228,7 +232,7 @@ const sendEmail = async () => {
/>
</UFormGroup>
<UFormGroup
label="Betreff"
label="Betreff"
>
<UInput
class="w-full my-1"
@@ -236,11 +240,8 @@ const sendEmail = async () => {
/>
</UFormGroup>
</div>
</UDashboardToolbar>
<UDashboardPanelContent>
<div id="parentAttachments" class="flex flex-col justify-center">
<UDivider class="my-3"/>
<div id="parentAttachments" class="flex flex-col justify-center mt-3">
<span class="font-medium mb-2 text-xl">Anhänge</span>
<!-- <UIcon
name="i-heroicons-paper-clip"
@@ -273,7 +274,7 @@ const sendEmail = async () => {
@updateContent="contentChanged"
:preloadedContent="preloadedContent"
/>
</UDashboardPanelContent>
</div>
</div>
</div>
@@ -320,4 +321,13 @@ const sendEmail = async () => {
padding: .5rem;
}
.scrollContainer {
overflow-y: scroll;
padding-left: 1em;
padding-right: 1em;
height: 90vh;
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
</style>

View File

@@ -132,12 +132,12 @@ const totalCalculated = computed(() => {
})
const createIncomingInvoice = async () => {
const data = await dataStore.createNewItem('incominginvoices',itemInfo.value)
console.log(data)
const data = await dataStore.createNewItem('incominginvoices',itemInfo.value,true)
const {error} = await supabase.from("files").update({incominginvoice: data.id}).eq("id",loadedFile.value.id)
router.push(`/incominginvoices/show/${data.id}`)
}
const setCostCentre = async (item,data) => {
@@ -145,6 +145,127 @@ const setCostCentre = async (item,data) => {
item.costCentre = data.id
}
const gptLoading = ref(false)
const getInvoiceData = async () => {
gptLoading.value = true
console.log(loadedFile.value)
//loadedFile.value.url
/*let data = {
"invoice_number": "3423478673",
"invoice_date": "2025-05-30",
"invoice_type": "incoming",
"delivery_type": "null",
"delivery_note_number": "null",
"reference": "null",
"issuer": {
"name": "Boels Rental Germany GmbH",
"address": "Emeranstraße 49-51, 85622 Feldkirchen, Deutschland",
"phone": "+49-(0)1801663225",
"email": "fakturierung@boels.de",
"bank": "ABN AMRO Bank N.V.",
"bic": "ABNANL2A",
"iban": "NL09 ABNA 0520 5585 61"
},
"recipient": {
"name": "Federspiel Technology UG",
"address": "Am Schwarzen Brack 14, 26452 Sande, Deutschland",
"phone": "null",
"email": "null"
},
"invoice_items": [
{
"description": "Bautrockner 50 ltr.",
"unit": "piece",
"quantity": 1,
"total": 395.22
},
{
"description": "Servicepauschale Kat. A",
"unit": "piece",
"quantity": 1,
"total": 32.1
},
{
"description": "Haftungsbegrenzung A: (Schäden, exkl. Feuer/Diebstahl/Einbruch)",
"unit": "piece",
"quantity": 1,
"total": 3.2
},
{
"description": "Haftungsbegrenzung B: (Feuer/Diebstahl/Einbruch)",
"unit": "piece",
"quantity": 1,
"total": 16.93
}
],
"subtotal": 89.1,
"tax_rate": 19,
"tax": 16.93,
"total": 106.03,
"terms": "Dieser Betrag wird automatisch mittels Lastschrift von ihrem Konto eingezogen"
}
console.log(data)
console.log(data.subtotal)*/
let data = await useFunctions().useGetInvoiceData(loadedFile.value)
if(data.invoice_number) itemInfo.value.reference = data.invoice_number
if(data.invoice_date) itemInfo.value.date = dayjs(data.invoice_date)
if(data.issuer.id) itemInfo.value.vendor = data.issuer.id
if(data.invoice_duedate) itemInfo.value.dueDate = dayjs(data.invoice_duedate)
if(data.terms) itemInfo.value.paymentType = data.terms
if(data.subtotal) {
itemInfo.value.accounts = [
{
account: null,
amountNet: data.subtotal,
amountTax: data.tax,
taxType: String(data.tax_rate),
costCentre: null,
amountGross: Number(data.subtotal) + Number(data.tax)
}
]
}
if(data.terms === "Direct Debit") {
itemInfo.value.paymentType = "Einzug"
} else if(data.terms === "Transfer") {
itemInfo.value.paymentType = "Überweisung"
} else if(data.terms === "Credit Card") {
itemInfo.value.paymentType = "Kreditkarte"
} else if(data.terms === "Other") {
itemInfo.value.paymentType = "Sonstiges"
}
let description = ""
if(data.delivery_note_number) description += `Lieferschein: ${data.delivery_note_number} \n`
if(data.reference) description += `Referenz: ${data.reference} \n`
if(data.invoice_items) {
data.invoice_items.forEach(item => {
description += `${item.description} - ${item.quantity} ${item.unit} - ${item.total}\n`
})
}
itemInfo.value.description = description
gptLoading.value = false
}
</script>
@@ -204,6 +325,17 @@ const setCostCentre = async (item,data) => {
</div>
<div v-else class=" scrollContainer">
<UButton
icon="i-heroicons-sparkles"
class="my-3"
variant="outline"
@click="getInvoiceData"
:disabled="gptLoading"
>
KI - Vorschlag
<UProgress v-if="gptLoading" animation="carousel"/>
</UButton>
<InputGroup class="mb-3">
<UButton
:variant="itemInfo.expense ? 'solid' : 'outline'"
@@ -431,7 +563,7 @@ const setCostCentre = async (item,data) => {
:color="!item.amountNet ? 'rose' : 'primary'"
:disabled="item.taxType === null"
@keyup="item.amountTax = Number((item.amountNet * (Number(taxOptions.find(i => i.key === item.taxType).percentage)/100)).toFixed(2)),
item.amountGross = Number(item.amountNet) + NUmber(item.amountTax)"
item.amountGross = Number(item.amountNet) + Number(item.amountTax)"
>
<template #trailing>
<span class="text-gray-500 dark:text-gray-400 text-xs">EUR</span>

View File

@@ -125,14 +125,47 @@ const totalCalculated = computed(() => {
})
const updateIncomingInvoice = async () => {
const updateIncomingInvoice = async (setBooked = false) => {
let item = itemInfo.value
delete item.files
if(item.state === "Vorbereitet" && !setBooked) {
item.state = "Entwurf"
} else if(item.state === "Vorbereitet" && setBooked) {
item.state = "Gebucht"
} else if(item.state === "Entwurf" && setBooked) {
item.state = "Gebucht"
} else {
item.state = "Entwurf"
}
const data = await dataStore.updateItem('incominginvoices',item)
}
const findIncomingInvoiceErrors = computed(() => {
let errors = []
if(itemInfo.value.vendor === null) errors.push({message: "Es ist kein Lieferant ausgewählt", type: "breaking"})
if(itemInfo.value.reference === null) errors.push({message: "Es ist keine Referenz angegeben", type: "breaking"})
if(itemInfo.value.date === null) errors.push({message: "Es ist kein Datum ausgewählt", type: "breaking"})
if(itemInfo.value.dueDate === null) errors.push({message: "Es ist kein Fälligkeitsdatum ausgewählt", type: "breaking"})
if(itemInfo.value.paymentType === null) errors.push({message: "Es ist keine Zahlart ausgewählt", type: "breaking"})
if(itemInfo.value.description === null) errors.push({message: "Es ist keine Beschreibung angegeben", type: "info"})
itemInfo.value.accounts.forEach(account => {
if(account.account === null) errors.push({message: "Es ist keine Kategorie ausgewählt", type: "breaking"})
if(account.amountNet === null) errors.push({message: "Es ist kein Nettobetrag angegeben", type: "breaking"})
if(account.taxType === null) errors.push({message: "Es ist kein Steuertyp ausgewählt", type: "breaking"})
if(account.costCentre === null) errors.push({message: "Es ist keine Kostenstelle ausgewählt", type: "info"})
if(account.taxType === null || account.taxType === "0") errors.push({message: "Es ist keine Steuerart ausgewählt", type: "breaking"})
})
return errors.sort((a,b) => (a.type === "breaking") ? -1 : 1)
})
</script>
@@ -140,10 +173,16 @@ const updateIncomingInvoice = async () => {
<UDashboardNavbar :title="'Eingangsbeleg erstellen'">
<template #right>
<UButton
@click="updateIncomingInvoice"
@click="updateIncomingInvoice(false)"
>
Speichern
</UButton>
<UButton
@click="updateIncomingInvoice(true)"
:disabled="findIncomingInvoiceErrors.filter(i => i.type === 'breaking').length > 0"
>
Speichern & Buchen
</UButton>
</template>
</UDashboardNavbar>
<UDashboardPanelContent>
@@ -158,7 +197,22 @@ const updateIncomingInvoice = async () => {
/>
<div class="w-3/5 mx-5">
<UAlert
class="mb-5"
title="Vorhandene Probleme und Informationen:"
:color="findIncomingInvoiceErrors.filter(i => i.type === 'breaking').length > 0 ? 'rose' : 'white'"
variant="outline"
v-if="findIncomingInvoiceErrors.length > 0"
>
<template #description>
<ul class="list-disc ml-5">
<li v-for="error in findIncomingInvoiceErrors" :class="[...error.type === 'breaking' ? ['text-rose-600'] : ['dark:text-white','text-black']]">
{{error.message}}
</li>
</ul>
</template>
</UAlert>
<div class=" scrollContainer">
<InputGroup class="mb-3">
@@ -196,11 +250,17 @@ const updateIncomingInvoice = async () => {
{{dataStore.vendors.find(vendor => vendor.id === itemInfo.vendor) ? dataStore.vendors.find(vendor => vendor.id === itemInfo.vendor).name : 'Lieferant auswählen'}}
</template>
</USelectMenu>
<EntityModalButtons
type="vendors"
:id="itemInfo.vendor"
@return-data="(data) => itemInfo.vendor = data.id"
/>
<UButton
@click="router.push('/standardEntity/vendors/create')"
>
+ Lieferant
</UButton>
icon="i-heroicons-x-mark"
variant="outline"
color="rose"
@click="itemInfo.vendor = null"
/>
</InputGroup>
@@ -304,7 +364,6 @@ const updateIncomingInvoice = async () => {
class="my-3"
v-for="(item,index) in itemInfo.accounts"
>
<UFormGroup
label="Kategorie"
class="mb-3"
@@ -360,6 +419,24 @@ const updateIncomingInvoice = async () => {
/>
</InputGroup>
</UFormGroup>
<UFormGroup
label="Beschreibung"
class="w-full mb-3"
>
<InputGroup class="w-full">
<UInput
v-model="item.description"
class="flex-auto"
></UInput>
<UButton
variant="outline"
color="rose"
v-if="item.description"
icon="i-heroicons-x-mark"
@click="item.description = null"
/>
</InputGroup>
</UFormGroup>
@@ -470,7 +547,7 @@ const updateIncomingInvoice = async () => {
overflow-y: scroll;
padding-left: 1em;
padding-right: 1em;
height: 75vh;
height: 70vh;
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}

View File

@@ -37,6 +37,7 @@ defineShortcuts({
})
const dataStore = useDataStore()
const tempStore = useTempStore()
const router = useRouter()
const sum = useSum()
@@ -45,7 +46,7 @@ const items = ref([])
const selectedItem = ref(0)
const setupPage = async () => {
items.value = await useSupabaseSelect("incominginvoices","*, vendor(id,name), statementallocations(id,amount)")
items.value = await useSupabaseSelect("incominginvoices","*, vendor(id,name), statementallocations(id,amount)","created_at",false)
}
setupPage()
@@ -91,9 +92,17 @@ const selectedColumns = ref(templateColumns)
const columns = computed(() => templateColumns.filter((column) => selectedColumns.value.includes(column)))
const searchString = ref('')
const searchString = ref(tempStore.searchStrings['incominginvoices'] ||'')
const clearSearchString = () => {
tempStore.clearSearchString('incominginvoices')
searchString.value = ''
}
const filteredRows = computed(() => {
return useSearch(searchString.value, items.value)
let filteredItems = useSearch(searchString.value, items.value)
return [...filteredItems.filter(i => i.state === "Vorbereitet"), ...filteredItems.filter(i => i.state !== "Vorbereitet")]
})
@@ -115,6 +124,16 @@ const isPaid = (item) => {
return Math.abs(amountPaid) === Math.abs(Number(getInvoiceSum(item)))
}
const selectIncomingInvoice = (invoice) => {
if(invoice.state === "Vorbereitet") {
router.push(`/incomingInvoices/edit/${invoice.id}`)
} else {
router.push(`/incomingInvoices/show/${invoice.id}`)
}
}
</script>
@@ -130,13 +149,21 @@ const isPaid = (item) => {
placeholder="Suche..."
class="hidden lg:block"
@keydown.esc="$event.target.blur()"
@change="tempStore.modifySearchString('incominginvoices',searchString)"
>
<template #trailing>
<UKbd value="/" />
</template>
</UInput>
<UButton
icon="i-heroicons-x-mark"
variant="outline"
color="rose"
@click="clearSearchString()"
v-if="searchString.length > 0"
/>
<UButton @click="router.push(`/incomingInvoices/create`)">+ Beleg</UButton>
<!-- <UButton @click="router.push(`/incomingInvoices/create`)">+ Beleg</UButton>-->
</template>
</UDashboardNavbar>
<UDashboardToolbar>
@@ -162,13 +189,18 @@ const isPaid = (item) => {
:columns="columns"
class="w-full"
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
@select="(i) => router.push(`/incomingInvoices/show/${i.id}`) "
@select="(i) => selectIncomingInvoice(i) "
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Belege anzuzeigen' }"
>
<template #reference-data="{row}">
<span v-if="row === filteredRows[selectedItem]" class="text-primary-500 font-bold">{{row.reference}}</span>
<span v-else>{{row.reference}}</span>
</template>
<template #state-data="{row}">
<span v-if="row.state === 'Vorbereitet'" class="text-cyan-500">{{row.state}}</span>
<span v-else-if="row.state === 'Entwurf'" class="text-red-500">{{row.state}}</span>
<span v-else-if="row.state === 'Gebucht'" class="text-primary-500">{{row.state}}</span>
</template>
<template #date-data="{row}">
{{dayjs(row.date).format("DD.MM.YYYY")}}
</template>
@@ -179,7 +211,7 @@ const isPaid = (item) => {
{{displayCurrency(sum.getIncomingInvoiceSum(row))}}
</template>
<template #dueDate-data="{row}">
{{dayjs(row.dueDate).format("DD.MM.YYYY")}}
<span v-if="row.dueDate">{{dayjs(row.dueDate).format("DD.MM.YYYY")}}</span>
</template>
<template #paid-data="{row}">
<span v-if="isPaid(row)" class="text-primary-500">Bezahlt</span>

View File

@@ -46,6 +46,7 @@ const loading = ref(true)
const setupPage = async () => {
if((mode.value === "show") && route.params.id){
itemInfo.value = await useSupabaseSelectSingle("incominginvoices",route.params.id,"*, files(*), vendor(*)")
if(process.dev) console.log(itemInfo.value)
currentDocument.value = await useFiles().selectDocument(itemInfo.value.files[0].id)
}
loading.value = false
@@ -70,6 +71,15 @@ setupPage()
<template>
<UDashboardNavbar :title="'Eingangsbeleg anzeigen'">
<template #left>
<UButton
to="/incominginvoices"
icon="i-heroicons-chevron-left"
variant="outline"
>
Übersicht
</UButton>
</template>
<template #right>
<UButton
@click="router.push(`/incomingInvoices/edit/${itemInfo.id}`)"

View File

@@ -39,15 +39,17 @@
<display-projects-in-phases/>
</UDashboardCard>
<UDashboardCard
title="Anwesenheiten"
title="Anwesende"
>
<display-present-profiles/>
</UDashboardCard>
<UDashboardCard
title="Projektzeiten"
>
<display-running-time/>
</UDashboardCard>
<UDashboardCard
title="Anwesenheiten"
>
<display-running-working-time/>
</UDashboardCard>

View File

@@ -10,32 +10,58 @@ const profileStore = useProfileStore()
<template>
<UDashboardPanelContent>
<UDivider class="mb-3">Weiteres</UDivider>
<UButton
class="w-full my-1"
to="/times"
icon="i-heroicons-clock"
>
Zeiten
</UButton>
<UButton
class="w-full my-1"
to="/standardEntity/absencerequests"
icon="i-heroicons-document-text"
>
Abwesenheiten
</UButton>
<UButton
class="w-full my-1"
to="/workingtimes"
icon="i-heroicons-clock"
>
Anwesenheiten
</UButton>
<!-- <UButton
class="w-full my-1">
Kalender
</UButton>
</UButton>-->
<UButton
class="w-full my-1"
to="/standardEntity/customers"
icon="i-heroicons-user-group"
>
Kunden
</UButton>
<UButton
class="w-full my-1"
to="/standardEntity/vendors"
icon="i-heroicons-truck"
>
Lieferanten
</UButton>
<UButton
class="w-full my-1"
to="/standardEntity/contacts"
icon="i-heroicons-user-group"
>
Ansprechpartner
</UButton>
<UButton
class="w-full my-1"
to="/standardEntity/plants"
icon="i-heroicons-clipboard-document"
>
Objekte
</UButton>

View File

@@ -59,6 +59,18 @@
<td>zahlungsziel_in_tagen</td>
<td>Zahlungsziel in Tagen</td>
</tr>
<tr>
<td>lohnkosten</td>
<td>Lohnkosten Verkauf</td>
</tr>
<tr>
<td>titel</td>
<td>Titel</td>
</tr>
<tr>
<td>anrede</td>
<td>Anrede</td>
</tr>
</table>
</UCard>

View File

@@ -2,6 +2,7 @@
import dayjs from "dayjs";
import VueDatePicker from '@vuepic/vue-datepicker'
import '@vuepic/vue-datepicker/dist/main.css'
import {setPageLayout} from "#app";
definePageMeta({
@@ -32,8 +33,18 @@ const runningTimeInfo = ref({})
const showConfigTimeModal = ref(false)
const configTimeMode = ref("create")
const platform = ref("default")
const projects = ref([])
const setup = async () => {
times.value = await useSupabaseSelect("times","*, profile(*), project(id, name)")
projects.value = await useSupabaseSelect("projects","*")
if(await useCapacitor().getIsPhone()) {
platform.value = "mobile"
setPageLayout("mobile")
}
runningTimeInfo.value = (await supabase
.from("times")
@@ -69,8 +80,6 @@ const filteredRows = computed(() => {
const itemInfo = ref({
user: "",
start: new Date(),
end: "",
notes: null,
project: null,
type: null,
@@ -166,15 +175,31 @@ const createTime = async () => {
itemInfo.value = {}
toast.add({title: "Zeit erfolgreich erstellt"})
showConfigTimeModal.value = false
await dataStore.fetchTimes()
setup()
}
}
const openTime = (row) => {
itemInfo.value = row
itemInfo.value.project = itemInfo.value.project.id
itemInfo.value.profile = itemInfo.value.profile.id
showConfigTimeModal.value = true
configTimeMode.value = "edit"
}
const updateTime = async () => {
let data = itemInfo.value
data.profile = data.profile.id
data.project = data.project.id
const {error} = await supabase
.from("times")
.update(itemInfo.value)
.update(data)
.eq('id',itemInfo.value.id)
if(error) {
@@ -183,7 +208,7 @@ const updateTime = async () => {
toast.add({title: "Zeit erfolgreich gespeichert"})
showConfigTimeModal.value = false
await dataStore.fetchTimes()
setup()
}
const format = (date) => {
@@ -192,7 +217,7 @@ const format = (date) => {
return `${dateFormat}`;
}
const getDuration = (time) => {
/*const getDuration = (time) => {
const dez = dayjs(time.end).diff(time.start,'hour',true).toFixed(2)
const hours = Math.floor(dez)
const minutes = Math.floor((dez - hours) * 60)
@@ -202,18 +227,30 @@ const getDuration = (time) => {
minutes: minutes,
composed: `${hours}:${minutes}`
}
}
}*/
const setState = async (newState) => {
itemInfo.value.state = newState
await updateTime()
}
const getSecondInfo = (item) => {
let returnArray = []
if(item.type) returnArray.push(item.type)
if(item.project) returnArray.push(item.project.name)
if(item.notes) returnArray.push(item.notes)
return returnArray
}
</script>
<template>
<UDashboardNavbar title="Zeiterfassung">
<UDashboardNavbar title="Projektzeiten">
<template #toggle>
<div v-if="platform === 'mobile'"></div>
</template>
</UDashboardNavbar>
<UDashboardToolbar>
<template #left>
@@ -230,11 +267,13 @@ const setState = async (newState) => {
Stop
</UButton>
<UButton
@click="configTimeMode = 'create'; itemInfo = {start: new Date(), end: new Date(), user: user.id}; showConfigTimeModal = true"
v-if="platform !== 'mobile'"
@click="configTimeMode = 'create'; itemInfo = {startDate: new Date(), endDate: new Date()}; showConfigTimeModal = true"
>
Erstellen
</UButton>
<USelectMenu
v-if="platform !== 'mobile'"
:options="profileStore.profiles"
option-attribute="fullName"
value-attribute="id"
@@ -295,7 +334,7 @@ const setState = async (newState) => {
>
<UCard>
<template #header>
Zeiteintrag {{configTimeMode === 'create' ? "erstellen" : "bearbeiten"}}
Projektzeit {{configTimeMode === 'create' ? "erstellen" : "bearbeiten"}}
</template>
<UFormGroup
label="Start:"
@@ -303,7 +342,7 @@ const setState = async (newState) => {
<UPopover :popper="{ placement: 'bottom-start' }">
<UButton
icon="i-heroicons-calendar-days-20-solid"
:label="itemInfo.startDate ? dayjs(itemInfo.startDate).format('DD.MM.YYYY') : 'Datum auswählen'"
:label="itemInfo.startDate ? dayjs(itemInfo.startDate).format('DD.MM.YYYY HH:mm') : 'Datum auswählen'"
variant="outline"
/>
@@ -318,7 +357,7 @@ const setState = async (newState) => {
<UPopover :popper="{ placement: 'bottom-start' }">
<UButton
icon="i-heroicons-calendar-days-20-solid"
:label="itemInfo.endDate ? dayjs(itemInfo.endDate).format('DD.MM.YYYY') : 'Datum auswählen'"
:label="itemInfo.endDate ? dayjs(itemInfo.endDate).format('DD.MM.YYYY HH:mm') : 'Datum auswählen'"
variant="outline"
/>
@@ -332,13 +371,12 @@ const setState = async (newState) => {
>
<USelectMenu
:options="profileStore.profiles"
v-model="itemInfo.user"
v-model="itemInfo.profile"
option-attribute="fullName"
value-attribute="id"
:disabled="(configTimeMode === 'create' ? false : itemInfo.state !== 'Entwurf') || (!dataStore.hasRight('createTime') || !useRole().hasRight('createOwnTime'))"
>
<template #label>
{{profileStore.profiles.find(profile => profile.id === itemInfo.user) ? profileStore.profiles.find(profile => profile.id === itemInfo.user).fullName : "Benutzer auswählen"}}
{{profileStore.profiles.find(profile => profile.id === itemInfo.profile) ? profileStore.profiles.find(profile => profile.id === itemInfo.profile).fullName : "Benutzer auswählen"}}
</template>
</USelectMenu>
</UFormGroup>
@@ -346,14 +384,13 @@ const setState = async (newState) => {
label="Projekt:"
>
<USelectMenu
:options="dataStore.projects"
:options="projects"
v-model="itemInfo.project"
option-attribute="name"
value-attribute="id"
searchable
searchable-placeholder="Suche..."
:search-attributes="['name']"
:disabled="configTimeMode === 'create' ? false : itemInfo.state !== 'Entwurf'"
>
<template #label>
{{dataStore.projects.find(project => project.id === itemInfo.project) ? dataStore.projects.find(project => project.id === itemInfo.project).name : "Projekt auswählen"}}
@@ -368,7 +405,6 @@ const setState = async (newState) => {
:options="timeTypes"
option-attribute="label"
value-attribute="label"
:disabled="configTimeMode === 'create' ? false : itemInfo.state !== 'Entwurf'"
>
<template #label>
{{itemInfo.type ? itemInfo.type : "Kategorie auswählen"}}
@@ -380,12 +416,11 @@ const setState = async (newState) => {
>
<UTextarea
v-model="itemInfo.notes"
:disabled="configTimeMode === 'create' ? false : itemInfo.state !== 'Entwurf'"
/>
</UFormGroup>
<template #footer v-if="configTimeMode === 'create' || itemInfo.state === 'Entwurf'">
<template #footer>
<InputGroup>
<UButton
@click="createTime"
@@ -396,7 +431,6 @@ const setState = async (newState) => {
<UButton
@click="updateTime"
v-else-if="configTimeMode === 'edit'"
v-if="itemInfo.state === 'Entwurf'"
>
Speichern
</UButton>
@@ -412,14 +446,29 @@ const setState = async (newState) => {
</UCard>
</UModal>
<UDashboardPanelContent class="w-full" v-if="platform === 'mobile'">
<a
v-for="item in filteredRows"
class="my-1"
>
<p class="truncate text-left text-primary text-xl">{{dayjs(item.startDate).format("DD.MM.YYYY HH:mm")}} - {{dayjs(item.endDate).format("HH:mm")}}</p>
<p class="text-sm">
<span v-for="(i,index) in getSecondInfo(item)">
{{i}}{{index < getSecondInfo(item).length - 1 ? " - " : ""}}
</span>
</p>
</a>
</UDashboardPanelContent>
<UTable
v-else
class="mt-3"
:columns="columns"
:rows="filteredRows"
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Noch keine Einträge' }"
@select="(row) => {configTimeMode = 'edit';
itemInfo = row;
showConfigTimeModal = true}"
@select="(row) => openTime(row)"
>
<template #state-data="{row}">
<span

View File

@@ -169,7 +169,7 @@ export const useDataStore = defineStore('data', () => {
numberRangeHolder: "customerNumber",
historyItemHolder: "customer",
supabaseSortColumn: "customerNumber",
supabaseSelectWithInformation: "*, projects(*), plants(*), contracts(*), contacts(*), createddocuments(*), files(*), events(*)",
supabaseSelectWithInformation: "*, projects(*), plants(*), contracts(*), contacts(*), createddocuments(*, statementallocations(*)), files(*), events(*)",
filters: [{
name: "Archivierte ausblenden",
default: true,
@@ -471,10 +471,16 @@ export const useDataStore = defineStore('data', () => {
key: "salutation",
label: "Anrede",
inputType: "text",
inputChangeFunction: function (row) {
row.fullName = `${row.firstName} ${row.lastName}`
}
},{
key: "title",
label: "Titel",
inputType: "text",
inputChangeFunction: function (row) {
row.fullName = `${row.firstName} ${row.lastName}`
}
},{
key: "firstName",
label: "Vorname",
@@ -505,6 +511,8 @@ export const useDataStore = defineStore('data', () => {
selectDataType: "customers",
selectOptionAttribute: "name",
selectSearchAttributes: ['name'],
secondInfo: true,
secondInfoKey: "name"
},
{
key: "vendor",
@@ -514,6 +522,8 @@ export const useDataStore = defineStore('data', () => {
selectDataType: "vendors",
selectOptionAttribute: "name",
selectSearchAttributes: ['name'],
secondInfo: true,
secondInfoKey: "name"
},
{
key: "role",
@@ -906,9 +916,9 @@ export const useDataStore = defineStore('data', () => {
inputTrailing: "EUR",
inputChangeFunction: function (row) {
if(row.markupPercentage) {
row.sellingPrice = row.purchasePrice * (1+row.markupPercentage/100);
row.sellingPrice = (row.purchasePrice * (1+row.markupPercentage/100)).toFixed(4)
} else {
row.sellingPrice = row.purchasePrice;
row.sellingPrice = row.purchasePrice.toFixed(4)
}
}
},{
@@ -917,8 +927,12 @@ export const useDataStore = defineStore('data', () => {
inputType: "number",
inputTrailing: "%",
inputChangeFunction: function (row) {
if(row.sellingPrice) {
row.sellingPrice = row.purchasePrice * (1+row.markupPercentage/100);
if(row.purchasePrice && ! row.sellingPrice) {
row.sellingPrice = (row.purchasePrice * (1+row.markupPercentage/100)).toFixed(4)
} else if(row.sellingPrice && !row.purchasePrice) {
row.purchasePrice = (row.sellingPrice / (1+row.markupPercentage/100)).toFixed(4)
} else {
row.sellingPrice = (row.purchasePrice * (1+row.markupPercentage/100)).toFixed(4)
}
}
},{
@@ -929,8 +943,10 @@ export const useDataStore = defineStore('data', () => {
inputType: "number",
inputTrailing: "EUR",
inputChangeFunction: function (row) {
if(row.markupPercentage) {
row.purchasePrice = (row.sellingPrice / (1+row.markupPercentage/100)).toFixed(4);
if(row.purchasePrice ) {
row.markupPercentage = ((row.sellingPrice / row.purchasePrice - 1) * 100 ).toFixed(2)
} else{
row.purchasePrice = (row.sellingPrice / (1+row.markupPercentage/100)).toFixed(4)
}
}
},{
@@ -1045,6 +1061,8 @@ export const useDataStore = defineStore('data', () => {
selectDataType: "customers",
selectOptionAttribute: "name",
selectSearchAttributes: ['name'],
secondInfo: true,
secondInfoKey: "name",
},{
key: "plant",
label: "Objekt",
@@ -1485,7 +1503,8 @@ export const useDataStore = defineStore('data', () => {
createddocuments: {
isArchivable: true,
label: "Dokumente",
labelSingle: "Dokument"
labelSingle: "Dokument",
supabaseSelectWithInformation: "*, files(*), statementallocations(*)",
},
incominginvoices: {
label: "Eingangsrechnungen",