Introduced OwnAccounts

Introduced CostCentres Rebuild
Added EntityModalButtons to IncomingInvoice Create
This commit is contained in:
2025-03-21 14:24:05 +01:00
parent bd1916bdaa
commit 76e207cd20
9 changed files with 385 additions and 79 deletions

View File

@@ -560,6 +560,13 @@ const getAvailableQueryStringData = (keys) => {
</UCard> </UCard>
</div> </div>
<div v-else-if="tab.label === 'Auswertung Kostenstelle'">
<UCard class="mt-5">
<costcentre-display :item="props.item"/>
</UCard>
</div>
</div> </div>

View File

@@ -0,0 +1,30 @@
<script setup>
const supabase = useSupabaseClient()
const profileStore = useProfileStore()
const props = defineProps({
item: {
required: true,
type: String
}
})
const incomingInvoices = ref({})
const setupPage = async () => {
incomingInvoices.value = (await supabase.from("incominginvoices").select().eq("tenant", profileStore.currentTenant)).data.filter(i => i.accounts.find(x => x.costCentre === props.item.id))
}
setupPage()
</script>
<template>
{{props.item}}
{{incomingInvoices}}
</template>
<style scoped>
</style>

View File

@@ -222,6 +222,30 @@ export const useRole = () => {
label: "Aufgaben erstellen", label: "Aufgaben erstellen",
parent: "tasks" parent: "tasks"
}, },
costcentres: {
label: "Kostenstellen",
showToAllUsers: false
},
"costcentres-viewAll": {
label: "Alle Kostenstellen einsehen",
parent: "costcentres"
},
"costcentres-create": {
label: "Kostenstellen erstellen",
parent: "costcentres"
},
ownaccounts: {
label: "Buchungskonten",
showToAllUsers: false
},
"ownaccounts-viewAll": {
label: "Alle Buchungskonten einsehen",
parent: "ownaccounts"
},
"ownaccounts-create": {
label: "Buchungskonten erstellen",
parent: "ownaccounts"
},
"inventory": { "inventory": {
label: "Lager", label: "Lager",
}, },

View File

@@ -31,6 +31,7 @@ const allocatedIncomingInvoices = ref([])
const createddocuments = ref([]) const createddocuments = ref([])
const accounts = ref([]) const accounts = ref([])
const ownaccounts = ref([])
const loading = ref(true) const loading = ref(true)
const setup = async () => { const setup = async () => {
@@ -46,6 +47,7 @@ const setup = async () => {
const incominginvoices = (await useSupabaseSelect("incominginvoices","*, statementallocations(*), vendor(id,name)")).filter(i => i.state === "Gebucht") const incominginvoices = (await useSupabaseSelect("incominginvoices","*, statementallocations(*), vendor(id,name)")).filter(i => i.state === "Gebucht")
accounts.value = (await supabase.from("accounts").select()).data accounts.value = (await supabase.from("accounts").select()).data
ownaccounts.value = (await supabase.from("ownaccounts").select()).data
openDocuments.value = documents.filter(i => i.statementallocations.reduce((n,{amount}) => n + amount, 0).toFixed(2) !== useSum().getCreatedDocumentSum(i,createddocuments.value).toFixed(2)) openDocuments.value = documents.filter(i => i.statementallocations.reduce((n,{amount}) => n + amount, 0).toFixed(2) !== useSum().getCreatedDocumentSum(i,createddocuments.value).toFixed(2))
openDocuments.value = openDocuments.value.map(i => { openDocuments.value = openDocuments.value.map(i => {
@@ -118,6 +120,7 @@ const saveAllocations = async () => {
const showAccountSelection = ref(false) const showAccountSelection = ref(false)
const accountToSave = ref("") const accountToSave = ref("")
const ownAccountToSave = ref("")
const selectAccount = (id) => { const selectAccount = (id) => {
accountToSave.value = id accountToSave.value = id
@@ -376,6 +379,24 @@ setup()
@click="removeAllocation(item.id)" @click="removeAllocation(item.id)"
/> />
</UCard> </UCard>
<UCard
class="mt-5"
v-for="item in itemInfo.statementallocations.filter(i => i.ownaccount)"
>
<template #header>
<div class="flex flex-row justify-between">
<span>{{ownaccounts.find(i => i.id === item.ownaccount).number}} - {{ownaccounts.find(i => i.id === item.ownaccount).name}}</span>
<span class="font-semibold text-nowrap">{{displayCurrency(item.amount)}}</span>
</div>
</template>
<UButton
icon="i-heroicons-x-mark"
variant="outline"
color="rose"
class="mr-3"
@click="removeAllocation(item.id)"
/>
</UCard>
<UCard <UCard
class="mt-5" class="mt-5"
:ui="{ring: itemInfo.statementallocations.find(i => i.cd_id === item.cd_id) ? 'ring-primary-500' : 'ring-gray-200 dark:ring-gray-800'}" :ui="{ring: itemInfo.statementallocations.find(i => i.cd_id === item.cd_id) ? 'ring-primary-500' : 'ring-gray-200 dark:ring-gray-800'}"
@@ -452,77 +473,124 @@ setup()
<div class="w-2/5 mx-auto"> <div class="w-2/5 mx-auto">
<div class="px-2"> <div class="px-2">
<UDivider class="my-3">Ohne Beleg buchen</UDivider> <UDivider class="my-3">Buchungssumme</UDivider>
<InputGroup class="mt-3 w-full"> <UInput
<USelectMenu
class="w-full"
:options="accounts"
value-attribute="id"
option-attribute="label"
:ui-menu="{ width: 'min-w-max' }"
v-model="accountToSave"
searchable
:search-attributes="['number','label']"
>
<template #label>
<span v-if="accountToSave">{{accounts.find(i => i.id === accountToSave).number}} - {{accounts.find(i => i.id === accountToSave).label}}</span>
<span v-else>Kein Konto ausgewählt</span>
</template>
<template #option="{option}">
{{option.number}} - {{option.label}}
</template>
</USelectMenu>
<UInput
v-model="manualAllocationSum" v-model="manualAllocationSum"
type="number" type="number"
step="0.01" step="0.01"
>
<template #trailing>
<span class="text-gray-500 dark:text-gray-400 text-xs">EUR</span>
</template>
</UInput>
<UButton
@click="showAccountSelection = true"
icon="i-heroicons-magnifying-glass"
variant="outline"
/>
<UButton
variant="outline"
icon="i-heroicons-check"
:disabled="!accountToSave"
@click="saveAllocation({bs_id: itemInfo.id, amount: manualAllocationSum, account: accountToSave })"
/>
<UButton
@click="accountToSave = ''"
icon="i-heroicons-x-mark"
variant="outline"
color="rose"
/>
</InputGroup>
<UModal
v-model="showAccountSelection"
> >
<UCard> <template #trailing>
<template #header> <span class="text-gray-500 dark:text-gray-400 text-xs">EUR</span>
<div class="flex items-center justify-between"> </template>
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white"> </UInput>
Konto auswählen
</h3> <UDivider class="my-3">Ohne Beleg buchen</UDivider>
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="showAccountSelection = false" />
</div> <UFormGroup
</template> label="Buchungskonten"
<UButton class="mt-3"
v-for="selectableAccount in accounts" >
variant="outline" <InputGroup class="mt-3 w-full">
class="m-3" <USelectMenu
@click="selectAccount(selectableAccount.id)" class="w-full"
> :options="accounts"
{{selectableAccount.label}} value-attribute="id"
</UButton> option-attribute="label"
</UCard> :ui-menu="{ width: 'min-w-max' }"
v-model="accountToSave"
searchable
:search-attributes="['number','label']"
>
<template #label>
<span v-if="accountToSave">{{accounts.find(i => i.id === accountToSave).number}} - {{accounts.find(i => i.id === accountToSave).label}}</span>
<span v-else>Kein Konto ausgewählt</span>
</template>
<template #option="{option}">
{{option.number}} - {{option.label}}
</template>
</USelectMenu>
<UButton
@click="showAccountSelection = true"
icon="i-heroicons-magnifying-glass"
variant="outline"
/>
<UButton
variant="outline"
icon="i-heroicons-check"
:disabled="!accountToSave"
@click="saveAllocation({bs_id: itemInfo.id, amount: manualAllocationSum, account: accountToSave })"
/>
<UButton
@click="accountToSave = ''"
icon="i-heroicons-x-mark"
variant="outline"
color="rose"
/>
</InputGroup>
<UModal
v-model="showAccountSelection"
>
<UCard>
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
Konto auswählen
</h3>
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="showAccountSelection = false" />
</div>
</template>
<UButton
v-for="selectableAccount in accounts"
variant="outline"
class="m-3"
@click="selectAccount(selectableAccount.id)"
>
{{selectableAccount.label}}
</UButton>
</UCard>
</UModal>
</UFormGroup>
<UFormGroup
label="zusätzliche Buchungskonten"
class="mt-3"
>
<InputGroup class="w-full">
<USelectMenu
class="w-full"
:options="ownaccounts"
value-attribute="id"
option-attribute="label"
:ui-menu="{ width: 'min-w-max' }"
v-model="ownAccountToSave"
searchable
:search-attributes="['number','label']"
>
<template #label>
<span v-if="ownAccountToSave">{{ownaccounts.find(i => i.id === ownAccountToSave).number}} - {{ownaccounts.find(i => i.id === ownAccountToSave).name}}</span>
<span v-else>Kein Konto ausgewählt</span>
</template>
<template #option="{option}">
{{option.number}} - {{option.name}}
</template>
</USelectMenu>
<UButton
variant="outline"
icon="i-heroicons-check"
:disabled="!ownAccountToSave"
@click="saveAllocation({bs_id: itemInfo.id, amount: manualAllocationSum, ownaccount: ownAccountToSave })"
/>
<UButton
@click="accountToSave = ''"
icon="i-heroicons-x-mark"
variant="outline"
color="rose"
/>
</InputGroup>
</UFormGroup>
</UModal>
<UDivider <UDivider
class="my-3" class="my-3"

View File

@@ -82,6 +82,7 @@ const customers = ref([])
const contacts = ref([]) const contacts = ref([])
const texttemplates = ref([]) const texttemplates = ref([])
const loaded = ref(false) const loaded = ref(false)
const setupPage = async () => { const setupPage = async () => {

View File

@@ -2,6 +2,7 @@
import InputGroup from "~/components/InputGroup.vue"; import InputGroup from "~/components/InputGroup.vue";
import dayjs from "dayjs"; import dayjs from "dayjs";
import HistoryDisplay from "~/components/HistoryDisplay.vue"; import HistoryDisplay from "~/components/HistoryDisplay.vue";
import {useSupabaseSelect} from "~/composables/useSupabase.js";
definePageMeta({ definePageMeta({
middleware: "auth" middleware: "auth"
@@ -35,16 +36,24 @@ const itemInfo = ref({
}) })
const availableDocuments = ref([]) const availableDocuments = ref([])
const costcentres = ref([])
const setup = async () => { const setup = async () => {
let filetype = (await supabase.from("filetags").select().eq("tenant",profileStore.currentTenant).eq("incomingDocumentType","invoices").single()).data.id let filetype = (await supabase.from("filetags").select().eq("tenant",profileStore.currentTenant).eq("incomingDocumentType","invoices").single()).data.id
console.log(filetype) console.log(filetype)
let ids = (await supabase.from("files").select("id").eq("tenant",profileStore.currentTenant).eq("type", filetype).is("incominginvoice",null)).data.map(i => i.id) let ids = (await supabase.from("files").select("id").eq("tenant",profileStore.currentTenant).eq("type", filetype).is("incominginvoice",null)).data.map(i => i.id)
availableDocuments.value = await useFiles().selectSomeDocuments(ids) availableDocuments.value = await useFiles().selectSomeDocuments(ids)
} }
setup() setup()
const loadCostCentres = async () => {
costcentres.value = await useSupabaseSelect("costcentres")
}
loadCostCentres()
const useNetMode = ref(false) const useNetMode = ref(false)
const loadedFile = ref(null) const loadedFile = ref(null)
@@ -129,6 +138,11 @@ const createIncomingInvoice = async () => {
} }
const setCostCentre = async (item,data) => {
await loadCostCentres()
item.costCentre = data.id
}
</script> </script>
@@ -145,10 +159,14 @@ const createIncomingInvoice = async () => {
<UDashboardPanelContent> <UDashboardPanelContent>
<div v-if="!loadedFile"> <div v-if="!loadedFile">
<DocumentList <DocumentList
v-if="availableDocuments.length > 0"
:documents="availableDocuments" :documents="availableDocuments"
:return-document-id="true" :return-document-id="true"
@selectDocument="(documentId) => loadFile(documentId)" @selectDocument="(documentId) => loadFile(documentId)"
/> />
<div v-else class="w-full text-center">
<span class="text-xl font-medium mt-10">Keine Dateien zum zuweisen verfügbar</span>
</div>
</div> </div>
<div <div
v-else v-else
@@ -219,11 +237,11 @@ const createIncomingInvoice = async () => {
{{dataStore.vendors.find(vendor => vendor.id === itemInfo.vendor) ? dataStore.vendors.find(vendor => vendor.id === itemInfo.vendor).name : 'Lieferant auswählen'}} {{dataStore.vendors.find(vendor => vendor.id === itemInfo.vendor) ? dataStore.vendors.find(vendor => vendor.id === itemInfo.vendor).name : 'Lieferant auswählen'}}
</template> </template>
</USelectMenu> </USelectMenu>
<UButton <EntityModalButtons
@click="router.push('/standardEntity/vendors/create')" type="vendors"
> :id="itemInfo.vendor"
+ Lieferant @return-data="(data) => itemInfo.vendor = data.id"
</UButton> />
</InputGroup> </InputGroup>
@@ -345,6 +363,9 @@ const createIncomingInvoice = async () => {
<template #label> <template #label>
{{dataStore.accounts.find(account => account.id === item.account) ? dataStore.accounts.find(account => account.id === item.account).label : "Keine Kategorie ausgewählt" }} {{dataStore.accounts.find(account => account.id === item.account) ? dataStore.accounts.find(account => account.id === item.account).label : "Keine Kategorie ausgewählt" }}
</template> </template>
<template #option="{option}">
{{option.number}} - {{option.label}}
</template>
</USelectMenu> </USelectMenu>
</UFormGroup> </UFormGroup>
@@ -352,18 +373,25 @@ const createIncomingInvoice = async () => {
label="Kostenstelle" label="Kostenstelle"
class=" mb-3" class=" mb-3"
> >
<InputGroup> <InputGroup class="w-full">
<USelectMenu <USelectMenu
:options="dataStore.getCostCentresComposed" :options="costcentres"
option-attribute="label" option-attribute="name"
value-attribute="id" value-attribute="id"
searchable searchable
:search-attributes="['label']" :search-attributes="['label']"
searchable-placeholder="Suche..." searchable-placeholder="Suche..."
v-model="item.costCentre" v-model="item.costCentre"
class="flex-auto"
> >
<template #label> <template #label>
{{dataStore.getCostCentresComposed.find(account => account.id === item.costCentre) ? dataStore.getCostCentresComposed.find(account => account.id === item.costCentre).label : "Keine Kostenstelle ausgewählt" }} {{costcentres.find(i => i.id === item.costCentre) ? costcentres.find(i => i.id === item.costCentre).name : "Keine Kostenstelle ausgewählt" }}
</template>
<template #option="{option}">
<span v-if="option.vehicle">{{option.number}} - Fahrzeug - {{option.name}}</span>
<span v-else-if="option.project">{{option.number}} - Projekt - {{option.name}}</span>
<span v-else-if="option.inventoryitem">{{option.number}} - Inventarartikel - {{option.name}}</span>
<span v-else>{{option.number}} - {{option.name}}</span>
</template> </template>
</USelectMenu> </USelectMenu>
@@ -374,6 +402,11 @@ const createIncomingInvoice = async () => {
icon="i-heroicons-x-mark" icon="i-heroicons-x-mark"
@click="item.costCentre = null" @click="item.costCentre = null"
/> />
<EntityModalButtons
type="costcentres"
:id="item.costCentre"
@return-data="(data) => setCostCentre(item,data)"
/>
</InputGroup> </InputGroup>
</UFormGroup> </UFormGroup>

View File

@@ -2,6 +2,7 @@
import InputGroup from "~/components/InputGroup.vue"; import InputGroup from "~/components/InputGroup.vue";
import dayjs from "dayjs"; import dayjs from "dayjs";
import HistoryDisplay from "~/components/HistoryDisplay.vue"; import HistoryDisplay from "~/components/HistoryDisplay.vue";
import {useSupabaseSelect} from "~/composables/useSupabase.js";
definePageMeta({ definePageMeta({
middleware: "auth" middleware: "auth"
@@ -34,10 +35,14 @@ const itemInfo = ref({
] ]
}) })
const costcentres = ref([])
const setup = async () => { const setup = async () => {
let filetype = (await supabase.from("filetags").select().eq("tenant",profileStore.currentTenant).eq("incomingDocumentType","invoices").single()).data.id let filetype = (await supabase.from("filetags").select().eq("tenant",profileStore.currentTenant).eq("incomingDocumentType","invoices").single()).data.id
console.log(filetype) console.log(filetype)
costcentres.value = await useSupabaseSelect("costcentres")
itemInfo.value = await useSupabaseSelectSingle("incominginvoices", route.params.id, "*, files(*)") itemInfo.value = await useSupabaseSelectSingle("incominginvoices", route.params.id, "*, files(*)")
await loadFile(itemInfo.value.files[itemInfo.value.files.length-1].id) await loadFile(itemInfo.value.files[itemInfo.value.files.length-1].id)
@@ -322,20 +327,27 @@ const updateIncomingInvoice = async () => {
</UFormGroup> </UFormGroup>
<UFormGroup <UFormGroup
label="Kostenstelle" label="Kostenstelle"
class=" mb-3" class="w-full mb-3"
> >
<InputGroup> <InputGroup class="w-full">
<USelectMenu <USelectMenu
:options="dataStore.getCostCentresComposed" :options="costcentres"
option-attribute="label" option-attribute="name"
value-attribute="id" value-attribute="id"
searchable searchable
:search-attributes="['label']" :search-attributes="['label']"
searchable-placeholder="Suche..." searchable-placeholder="Suche..."
v-model="item.costCentre" v-model="item.costCentre"
class="flex-auto"
> >
<template #label> <template #label>
{{dataStore.getCostCentresComposed.find(account => account.id === item.costCentre) ? dataStore.getCostCentresComposed.find(account => account.id === item.costCentre).label : "Keine Kostenstelle ausgewählt" }} {{costcentres.find(i => i.id === item.costCentre) ? costcentres.find(i => i.id === item.costCentre).name : "Keine Kostenstelle ausgewählt" }}
</template>
<template #option="{option}">
<span v-if="option.vehicle">Fahrzeug - {{option.name}}</span>
<span v-else-if="option.project">Projekt - {{option.name}}</span>
<span v-else-if="option.inventoryitem">Inventarartikel - {{option.name}}</span>
<span v-else>{{option.name}}</span>
</template> </template>
</USelectMenu> </USelectMenu>

View File

@@ -39,6 +39,9 @@ const resources = {
}, },
deliveryNotes: { deliveryNotes: {
label: "Lieferscheine" label: "Lieferscheine"
},
costcentres: {
label: "Kostenstellen"
} }
} }

View File

@@ -1957,6 +1957,134 @@ export const useDataStore = defineStore('data', () => {
} }
] ]
}, },
costcentres: {
isArchivable: true,
label: "Kostenstellen",
labelSingle: "Kostenstelle",
isStandardEntity: true,
redirect:true,
numberRangeHolder: "number",
historyItemHolder: "costcentre",
supabaseSortColumn: "number",
supabaseSelectWithInformation: "*, project(*), vehicle(*), inventoryitem(*)",
filters: [{
name: "Archivierte ausblenden",
default: true,
"filterFunction": function (row) {
if(!row.archived) {
return true
} else {
return false
}
}
}],
templateColumns: [
{
key: 'number',
label: "Nummer",
inputIsNumberRange: true,
inputType: "text"
}, {
key: "name",
label: "Name",
required: true,
title: true,
inputType: "text",
},
{
key: "description",
label: "Beschreibung",
inputType: "textarea"
},
{
key: "vehicle",
label: "Fahrzeug",
component: vehicle,
inputType: "select",
selectDataType: "vehicles",
selectOptionAttribute: "licensePlate",
selectSearchAttributes: ['licensePlate'],
},
{
key: "inventoryitem",
label: "Inventarartikel",
inputType: "select",
selectDataType: "inventoryitems",
selectOptionAttribute: "name",
selectSearchAttributes: ['name'],
},
{
key: "project",
label: "Projekt",
component: project,
inputType: "select",
selectDataType: "projects",
selectOptionAttribute: "name",
selectSearchAttributes: ['name'],
},
{
key: "profiles",
label: "Berechtigte Benutzer",
inputType: "select",
selectDataType: "profiles",
selectOptionAttribute: "fullName",
selectSearchAttributes: ['fullName'],
selectMultiple: true,
component: profiles
},
],
showTabs: [{label: 'Informationen'},{label: 'Auswertung Kostenstelle'}]
},
ownaccounts: {
isArchivable: true,
label: "zusätzliche Buchungskonten",
labelSingle: "zusätzliches Buchungskonto",
isStandardEntity: true,
redirect:true,
historyItemHolder: "ownaccount",
supabaseSortColumn: "number",
supabaseSelectWithInformation: "*",
filters: [{
name: "Archivierte ausblenden",
default: true,
"filterFunction": function (row) {
if(!row.archived) {
return true
} else {
return false
}
}
}],
templateColumns: [
{
key: 'number',
label: "Nummer",
inputType: "text"
}, {
key: "name",
label: "Name",
required: true,
title: true,
inputType: "text",
},
{
key: "description",
label: "Beschreibung",
inputType: "textarea"
},
{
key: "profiles",
label: "Berechtigte Benutzer",
inputType: "select",
selectDataType: "profiles",
selectOptionAttribute: "fullName",
selectSearchAttributes: ['fullName'],
selectMultiple: true,
component: profiles
},
],
showTabs: [{label: 'Informationen'}]
},
} }
const documentTypesForCreation = ref({ const documentTypesForCreation = ref({