New CustomerInventory,
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 32s
Build and Push Docker Images / build-frontend (push) Successful in 1m10s

New Mitgliederverwaltung für Vereine
New Bank Auto Complete
This commit is contained in:
2026-02-17 12:38:39 +01:00
parent f26d6bd4f3
commit 6fded3993a
39 changed files with 4837 additions and 158 deletions

View File

@@ -16,6 +16,7 @@ const toast = useToast()
const accounts = ref([])
const ibanSearch = ref("")
const showCreate = ref(false)
const resolvingIban = ref(false)
const createPayload = ref({
iban: "",
@@ -78,6 +79,25 @@ const createAndAssign = async () => {
showCreate.value = false
}
const resolveCreatePayloadFromIban = async () => {
const normalized = normalizeIban(createPayload.value.iban)
if (!normalized) return
resolvingIban.value = true
try {
const data = await useFunctions().useBankingResolveIban(normalized)
if (!data) return
createPayload.value.iban = data.iban || normalized
if (data.bic) createPayload.value.bic = data.bic
if (data.bankName) createPayload.value.bankName = data.bankName
} catch (e) {
// intentionally ignored: user can still enter fields manually
} finally {
resolvingIban.value = false
}
}
loadAccounts()
</script>
@@ -125,7 +145,21 @@ loadAccounts()
<template #header>Neue Bankverbindung erstellen</template>
<div class="space-y-3">
<UFormGroup label="IBAN">
<UInput v-model="createPayload.iban" />
<InputGroup>
<UInput
v-model="createPayload.iban"
@blur="resolveCreatePayloadFromIban"
@keydown.enter.prevent="resolveCreatePayloadFromIban"
/>
<UButton
color="gray"
variant="outline"
:loading="resolvingIban"
@click="resolveCreatePayloadFromIban"
>
Ermitteln
</UButton>
</InputGroup>
</UFormGroup>
<UFormGroup label="BIC">
<UInput v-model="createPayload.bic" />

View File

@@ -2,6 +2,7 @@
import EntityShowSubTimes from "~/components/EntityShowSubTimes.vue";
import WikiEntityWidget from "~/components/wiki/WikiEntityWidget.vue";
import LabelPrintModal from "~/components/LabelPrintModal.vue";
const props = defineProps({
type: {
@@ -136,6 +137,18 @@ const changePinned = async () => {
}
const openCustomerInventoryLabelPrint = () => {
modal.open(LabelPrintModal, {
context: {
id: props.item.id,
customerInventoryId: props.item.customerInventoryId,
name: props.item.name,
customerName: props.item.customer?.name,
serialNumber: props.item.serialNumber
}
})
}
</script>
<template>
@@ -193,6 +206,14 @@ const changePinned = async () => {
color="yellow"
@click="changePinned"
></UButton>
<UButton
v-if="type === 'customerinventoryitems'"
icon="i-heroicons-printer"
variant="outline"
@click="openCustomerInventoryLabelPrint"
>
Label
</UButton>
<UButton
@click="router.push(`/standardEntity/${type}/edit/${item.id}`)"
>
@@ -214,6 +235,14 @@ const changePinned = async () => {
>{{item ? `${dataType.labelSingle}${props.item[dataType.templateColumns.find(i => i.title).key] ? ': ' + props.item[dataType.templateColumns.find(i => i.title).key] : ''}`: '' }}</h1>
</template>
<template #right>
<UButton
v-if="type === 'customerinventoryitems'"
icon="i-heroicons-printer"
variant="outline"
@click="openCustomerInventoryLabelPrint"
>
Label
</UButton>
<UButton
@click="router.push(`/standardEntity/${type}/edit/${item.id}`)"
>

View File

@@ -21,13 +21,20 @@ const props = defineProps({
const dataStore = useDataStore()
const dataType = dataStore.dataTypes[props.topLevelType]
const historyType = computed(() => {
const holder = dataType?.historyItemHolder
if (!holder) return props.topLevelType
const normalized = String(holder).toLowerCase()
return normalized.endsWith("s") ? normalized : `${normalized}s`
})
</script>
<template>
<UCard class="mt-5 scroll" :style="props.platform !== 'mobile' ? 'height: 80vh' : ''">
<HistoryDisplay
:type="props.topLevelType"
:type="historyType"
v-if="props.item.id"
:element-id="props.item.id"
render-headline
@@ -39,4 +46,4 @@ const dataType = dataStore.dataTypes[props.topLevelType]
<style scoped>
</style>
</style>

View File

@@ -1,4 +1,5 @@
<script setup>
import dayjs from "dayjs";
const props = defineProps({
queryStringData: {
@@ -28,6 +29,33 @@ const dataType = dataStore.dataTypes[props.topLevelType]
// const selectedColumns = ref(tempStore.columns[props.topLevelType] ? tempStore.columns[props.topLevelType] : dataType.templateColumns.filter(i => !i.disabledInTable))
// const columns = computed(() => dataType.templateColumns.filter((column) => !column.disabledInTable && selectedColumns.value.find(i => i.key === column.key)))
const getDatapointValue = (datapoint) => {
if (datapoint.key.includes(".")) {
const [parentKey, childKey] = datapoint.key.split(".")
return props.item?.[parentKey]?.[childKey]
}
return props.item?.[datapoint.key]
}
const renderDatapointValue = (datapoint) => {
const value = getDatapointValue(datapoint)
if (value === null || value === undefined || value === "") return "-"
if (datapoint.inputType === "date") {
return dayjs(value).isValid() ? dayjs(value).format("DD.MM.YYYY") : String(value)
}
if (datapoint.inputType === "datetime") {
return dayjs(value).isValid() ? dayjs(value).format("DD.MM.YYYY HH:mm") : String(value)
}
if (datapoint.inputType === "bool" || typeof value === "boolean") {
return value ? "Ja" : "Nein"
}
return `${value}${datapoint.unit ? datapoint.unit : ""}`
}
</script>
<template>
@@ -53,8 +81,7 @@ const dataType = dataStore.dataTypes[props.topLevelType]
<td>
<component v-if="datapoint.component" :is="datapoint.component" :row="props.item" :in-show="true"></component>
<div v-else>
<span v-if="datapoint.key.includes('.')">{{props.item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]]}}{{datapoint.unit}}</span>
<span v-else>{{props.item[datapoint.key]}} {{datapoint.unit}}</span>
<span>{{ renderDatapointValue(datapoint) }}</span>
</div>
</td>
</tr>
@@ -74,4 +101,4 @@ td {
padding-bottom: 0.15em;
padding-top: 0.15em;
}
</style>
</style>

View File

@@ -24,6 +24,7 @@ const emit = defineEmits(["updateNeeded"]);
const router = useRouter()
const profileStore = useProfileStore()
const auth = useAuthStore()
const renderedPhases = computed(() => {
if(props.topLevelType === "projects" && props.item.phases) {
@@ -57,6 +58,7 @@ const renderedPhases = computed(() => {
})
const changeActivePhase = async (key) => {
console.log(props.item)
let item = await useEntities("projects").selectSingle(props.item.id,'*')
let phaseLabel = ""
@@ -67,13 +69,15 @@ const changeActivePhase = async (key) => {
if(p.key === key) {
p.active = true
p.activated_at = dayjs().format()
p.activated_by = profileStore.activeProfile.id
p.activated_by = auth.user.id
phaseLabel = p.label
}
return p
})
console.log(item)
const res = await useEntities("projects").update(item.id, {phases:item.phases,active_phase: item.phases.find(i => i.active).label})
emit("updateNeeded")
@@ -140,7 +144,7 @@ const changeActivePhase = async (key) => {
<div>
<p v-if="item.activated_at" class="dark:text-white text-black">Aktiviert am: {{dayjs(item.activated_at).format("DD.MM.YY HH:mm")}} Uhr</p>
<p v-if="item.activated_by" class="dark:text-white text-black">Aktiviert durch: {{profileStore.getProfileById(item.activated_by).fullName}}</p>
<p v-if="item.activated_by" class="dark:text-white text-black">Aktiviert durch: {{item.activated_by}}</p>
<p v-if="item.description" class="dark:text-white text-black">Beschreibung: {{item.description}}</p>
</div>
</UCard>

View File

@@ -44,7 +44,9 @@ async function loadLabel() {
labelData.value = await $api(`/api/print/label`, {
method: "POST",
body: JSON.stringify({
context: props.context || null
context: props.context || null,
width: 584,
height: 354
})
})
} catch (err) {
@@ -78,11 +80,17 @@ onMounted(() => {
})
watch(() => labelPrinter.connected, (connected) => {
if (connected && !labelData.value) {
loadLabel()
}
})
</script>
<template>
<UModal>
<UCard>
<UModal :ui="{ width: 'sm:max-w-5xl' }">
<UCard class="w-[92vw] max-w-5xl">
<template #header>
<div class="flex items-center justify-between">
@@ -91,11 +99,11 @@ onMounted(() => {
</div>
</template>
<div v-if="!loading && labelPrinter.connected">
<div v-if="!loading && labelPrinter.connected" class="w-full">
<img
:src="`data:image/png;base64,${labelData.base64}`"
alt="Label Preview"
class="max-w-full max-h-64 object-contain"
class="w-full max-h-[70vh] object-contain"
/>
</div>
<div v-else-if="loading && !labelPrinter.connected">

View File

@@ -12,6 +12,9 @@ const tenantExtraModules = computed(() => {
const showMembersNav = computed(() => {
return tenantExtraModules.value.includes("verein") && (has("members") || has("customers"))
})
const showMemberRelationsNav = computed(() => {
return tenantExtraModules.value.includes("verein") && has("members")
})
const links = computed(() => {
return [
@@ -191,6 +194,26 @@ const links = computed(() => {
to: "/standardEntity/spaces",
icon: "i-heroicons-square-3-stack-3d"
}] : [],
...has("inventoryitems") ? [{
label: "Kundenlagerplätze",
to: "/standardEntity/customerspaces",
icon: "i-heroicons-squares-plus"
}] : [],
...has("inventoryitems") ? [{
label: "Kundeninventar",
to: "/standardEntity/customerinventoryitems",
icon: "i-heroicons-qr-code"
}] : [],
...has("inventoryitems") ? [{
label: "Inventar",
to: "/standardEntity/inventoryitems",
icon: "i-heroicons-puzzle-piece"
}] : [],
...has("inventoryitems") ? [{
label: "Inventargruppen",
to: "/standardEntity/inventoryitemgroups",
icon: "i-heroicons-puzzle-piece"
}] : [],
]
}] : [],
{
@@ -218,6 +241,11 @@ const links = computed(() => {
to: "/standardEntity/servicecategories",
icon: "i-heroicons-wrench-screwdriver"
}] : [],
...showMemberRelationsNav.value ? [{
label: "Mitgliedsverhältnisse",
to: "/standardEntity/memberrelations",
icon: "i-heroicons-identification"
}] : [],
{
label: "Mitarbeiter",
to: "/staff/profiles",
@@ -228,21 +256,21 @@ const links = computed(() => {
to: "/standardEntity/hourrates",
icon: "i-heroicons-user-group"
},
{
label: "Projekttypen",
to: "/projecttypes",
icon: "i-heroicons-clipboard-document-list",
},
{
label: "Vertragstypen",
to: "/standardEntity/contracttypes",
icon: "i-heroicons-document-duplicate",
},
...has("vehicles") ? [{
label: "Fahrzeuge",
to: "/standardEntity/vehicles",
icon: "i-heroicons-truck"
}] : [],
...has("inventoryitems") ? [{
label: "Inventar",
to: "/standardEntity/inventoryitems",
icon: "i-heroicons-puzzle-piece"
}] : [],
...has("inventoryitems") ? [{
label: "Inventargruppen",
to: "/standardEntity/inventoryitemgroups",
icon: "i-heroicons-puzzle-piece"
}] : [],
]
},
@@ -286,14 +314,6 @@ const links = computed(() => {
label: "Firmeneinstellungen",
to: "/settings/tenant",
icon: "i-heroicons-building-office",
}, {
label: "Projekttypen",
to: "/projecttypes",
icon: "i-heroicons-clipboard-document-list",
}, {
label: "Vertragstypen",
to: "/standardEntity/contracttypes",
icon: "i-heroicons-document-duplicate",
}, {
label: "Export",
to: "/export",

View File

@@ -1,4 +1,6 @@
<script setup>
import { computed } from "vue"
const props = defineProps({
row: {
type: Object,
@@ -6,13 +8,15 @@ const props = defineProps({
default: {}
}
})
const addressData = computed(() => props.row?.infoData || props.row?.info_data || {})
</script>
<template>
<span v-if="props.row.infoData.streetNumber">{{props.row.infoData.streetNumber}},</span>
<span v-if="props.row.infoData.street">{{props.row.infoData.street}},</span>
<span v-if="props.row.infoData.special">{{props.row.infoData.special}},</span>
<span v-if="props.row.infoData.zip">{{props.row.infoData.zip}},</span>
<span v-if="props.row.infoData.city">{{props.row.infoData.city}}</span>
<span v-if="addressData.streetNumber">{{ addressData.streetNumber }},</span>
<span v-if="addressData.street">{{ addressData.street }},</span>
<span v-if="addressData.special">{{ addressData.special }},</span>
<span v-if="addressData.zip">{{ addressData.zip }},</span>
<span v-if="addressData.city">{{ addressData.city }}</span>
</template>

View File

@@ -0,0 +1,48 @@
<script setup>
const props = defineProps({
row: {
type: Object,
required: true,
default: {}
}
})
const relations = ref([])
const normalizeId = (value) => {
if (value === null || value === undefined || value === "") return null
const parsed = Number(value)
return Number.isNaN(parsed) ? String(value) : parsed
}
const relationLabel = computed(() => {
const id = normalizeId(props.row?.infoData?.memberrelation)
if (!id) return ""
return relations.value.find((i) => normalizeId(i.id) === id)?.type || ""
})
const relationId = computed(() => {
return normalizeId(props.row?.infoData?.memberrelation)
})
const loadRelations = async () => {
try {
relations.value = await useEntities("memberrelations").select()
} catch (e) {
relations.value = []
}
}
loadRelations()
</script>
<template>
<NuxtLink
v-if="relationId && relationLabel"
:to="`/standardEntity/memberrelations/show/${relationId}`"
class="text-primary"
>
{{ relationLabel }}
</NuxtLink>
<span v-else>{{ relationLabel }}</span>
</template>

View File

@@ -0,0 +1,20 @@
<script setup>
const props = defineProps({
row: {
type: Object,
required: true,
default: {}
},
inShow: {
type: Boolean,
default: false
}
})
</script>
<template>
<div v-if="props.row.product">
<nuxt-link v-if="props.inShow" :to="`/standardEntity/products/show/${props.row.product.id}`">{{ props.row.product ? props.row.product.name : '' }}</nuxt-link>
<span v-else>{{ props.row.product ? props.row.product.name : '' }}</span>
</div>
</template>

View File

@@ -0,0 +1,30 @@
<script setup>
const props = defineProps({
row: {
type: Object,
required: true,
default: {}
}
})
const productcategories = ref([])
const setup = async () => {
productcategories.value = await useEntities("productcategories").select()
}
setup()
const renderedCategories = computed(() => {
if (!Array.isArray(props.row?.productcategories)) return ""
return props.row.productcategories
.map((id) => productcategories.value.find((x) => x.id === id)?.name)
.filter(Boolean)
.join(", ")
})
</script>
<template>
<span v-if="renderedCategories">{{ renderedCategories }}</span>
</template>

View File

@@ -86,5 +86,11 @@ export const useFunctions = () => {
}
return {getWorkingTimesEvaluationData, useNextNumber, useBankingGenerateLink, useZipCheck, useBankingCheckInstitutions, useBankingListRequisitions, useCreatePDF}
const useBankingResolveIban = async (iban) => {
const normalized = String(iban || "").replace(/\s+/g, "").toUpperCase()
if (!normalized) return null
return await useNuxtApp().$api(`/api/banking/iban/${encodeURIComponent(normalized)}`)
}
return {getWorkingTimesEvaluationData, useNextNumber, useBankingGenerateLink, useZipCheck, useBankingCheckInstitutions, useBankingListRequisitions, useBankingResolveIban, useCreatePDF}
}

View File

@@ -350,6 +350,14 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
<span class="font-mono text-xs opacity-75 mr-2">{{ option.vendorNumber }}</span> {{ option.name }}
</template>
</USelectMenu>
<UButton
v-if="mode !== 'show'"
icon="i-heroicons-x-mark"
variant="outline"
color="rose"
:disabled="!itemInfo.vendor"
@click="itemInfo.vendor = null"
/>
<EntityModalButtons
v-if="mode !== 'show'"
type="vendors"

View File

@@ -14,6 +14,9 @@ const resources = {
spaces: {
label: "Lagerplätze"
},
customerspaces: {
label: "Kundenlagerplätze"
},
invoices: {
label: "Rechnungen"
},
@@ -23,6 +26,9 @@ const resources = {
inventoryitems: {
label: "Inventarartikel"
},
customerinventoryitems: {
label: "Kundeninventarartikel"
},
projects: {
label: "Projekte"
},
@@ -37,7 +43,17 @@ const resources = {
}
}
const numberRanges = ref(auth.activeTenantData.numberRanges)
const numberRanges = ref(auth.activeTenantData.numberRanges || {})
Object.keys(resources).forEach((key) => {
if (!numberRanges.value[key]) {
numberRanges.value[key] = {
prefix: "",
suffix: "",
nextNumber: 1000
}
}
})
const updateNumberRanges = async (range) => {

View File

@@ -5,10 +5,12 @@ import dayjs from "dayjs"
import projecttype from "~/components/columnRenderings/projecttype.vue"
import contracttype from "~/components/columnRenderings/contracttype.vue"
import memberrelation from "~/components/columnRenderings/memberrelation.vue"
import customer from "~/components/columnRenderings/customer.vue"
import contact from "~/components/columnRenderings/contact.vue"
import plant from "~/components/columnRenderings/plant.vue"
import vendor from "~/components/columnRenderings/vendor.vue"
import product from "~/components/columnRenderings/product.vue"
import active from "~/components/columnRenderings/active.vue"
import sellingPrice from "~/components/columnRenderings/sellingPrice.vue";
import unit from "~/components/columnRenderings/unit.vue";
@@ -32,6 +34,7 @@ import endDate from "~/components/columnRenderings/endDate.vue"
import startDateTime from "~/components/columnRenderings/startDateTime.vue"
import endDateTime from "~/components/columnRenderings/endDateTime.vue"
import serviceCategories from "~/components/columnRenderings/serviceCategories.vue"
import productcategoriesWithLoad from "~/components/columnRenderings/productcategoriesWithLoad.vue"
import phase from "~/components/columnRenderings/phase.vue"
import vehiclesWithLoad from "~/components/columnRenderings/vehiclesWithLoad.vue"
import inventoryitemsWithLoad from "~/components/columnRenderings/inventoryitemsWithLoad.vue"
@@ -167,7 +170,7 @@ export const useDataStore = defineStore('data', () => {
numberRangeHolder: "customerNumber",
historyItemHolder: "customer",
sortColumn: "customerNumber",
selectWithInformation: "*, projects(*), plants(*), contracts(*), contacts(*), createddocuments(*, statementallocations(*)), files(*), events(*)",
selectWithInformation: "*, projects(*), plants(*), contracts(*), contacts(*), createddocuments(*, statementallocations(*)), files(*), events(*), customerinventoryitems(*), customerspaces(*)",
filters: [{
name: "Archivierte ausblenden",
default: true,
@@ -443,7 +446,7 @@ export const useDataStore = defineStore('data', () => {
inputColumn: "Allgemeines"
},*/
],
showTabs: [{label: 'Informationen'},{label: 'Ansprechpartner'},{label: 'Dateien'},{label: 'Ausgangsbelege'},{label: 'Projekte'},{label: 'Objekte'},{label: 'Termine'},{label: 'Verträge'},{label: 'Wiki'}]
showTabs: [{label: 'Informationen'},{label: 'Ansprechpartner'},{label: 'Dateien'},{label: 'Ausgangsbelege'},{label: 'Projekte'},{label: 'Objekte'},{label: 'Termine'},{label: 'Verträge'},{label: 'Kundeninventar', key: 'customerinventoryitems'},{label: 'Kundenlagerplätze', key: 'customerspaces'},{label: 'Wiki'}]
},
members: {
isArchivable: true,
@@ -524,6 +527,24 @@ export const useDataStore = defineStore('data', () => {
inputColumn: "Allgemeines",
sortable: true
},
{
key: "infoData.birthdate",
label: "Geburtsdatum",
inputType: "date",
inputColumn: "Allgemeines",
disabledInTable: true
},
{
key: "infoData.memberrelation",
label: "Mitgliedsverhältnis",
component: memberrelation,
inputType: "select",
selectDataType: "memberrelations",
selectOptionAttribute: "type",
selectSearchAttributes: ['type', 'billingInterval'],
inputColumn: "Allgemeines",
sortable: true
},
{
key: "active",
label: "Aktiv",
@@ -1035,6 +1056,57 @@ export const useDataStore = defineStore('data', () => {
],
showTabs: [{ label: "Informationen" }]
},
memberrelations: {
isArchivable: true,
label: "Mitgliedsverhältnisse",
labelSingle: "Mitgliedsverhältnis",
isStandardEntity: true,
historyItemHolder: "memberrelation",
redirect: true,
sortColumn: "type",
selectWithInformation: "*",
filters: [{
name: "Archivierte ausblenden",
default: true,
"filterFunction": function (row) {
return !row.archived
}
}],
templateColumns: [
{
key: "type",
label: "Typ",
required: true,
title: true,
inputType: "text",
sortable: true
},
{
key: "billingInterval",
label: "Abrechnungsintervall",
required: true,
inputType: "select",
selectValueAttribute: "label",
selectOptionAttribute: "label",
selectManualOptions: [
{ label: "Monatlich" },
{ label: "Quartalsweise" },
{ label: "Halbjährlich" },
{ label: "Jährlich" }
],
sortable: true
},
{
key: "billingAmount",
label: "Abrechnungshöhe",
required: true,
inputType: "number",
inputTrailing: "€",
sortable: true
}
],
showTabs: [{ label: "Informationen" }]
},
absencerequests: {
isArchivable: true,
label: "Abwesenheiten",
@@ -1293,7 +1365,8 @@ export const useDataStore = defineStore('data', () => {
selectDataType: "productcategories",
selectOptionAttribute: "name",
selectSearchAttributes: ['name'],
selectMultiple: true
selectMultiple: true,
component: productcategoriesWithLoad
},
{
key: "description",
@@ -1773,6 +1846,7 @@ export const useDataStore = defineStore('data', () => {
selectValueAttribute: "label",
selectManualOptions: [
{label:"Standort"},
{label:"Raum"},
{label:"Regalplatz"},
{label:"Kiste"},
{label:"Palettenplatz"},
@@ -1856,6 +1930,149 @@ export const useDataStore = defineStore('data', () => {
},{label: 'Inventarartikel'},{label: 'Wiki'}
]
},
customerspaces: {
isArchivable: true,
label: "Kundenlagerplätze",
labelSingle: "Kundenlagerplatz",
isStandardEntity: true,
selectWithInformation: "*, customer(id,name), files(*)",
sortColumn: "space_number",
redirect: true,
numberRangeHolder: "space_number",
historyItemHolder: "customerspace",
filters:[{
name: "Archivierte ausblenden",
default: true,
"filterFunction": function (row) {
return !row.archived
}
}],
inputColumns: [
"Allgemeines",
"Ort"
],
templateColumns: [
{
key: "name",
label: "Name",
inputType: "text",
required: true,
title: true,
inputColumn: "Allgemeines",
sortable: true
},
{
key: "customer",
label: "Kunde",
inputType: "select",
required: true,
selectDataType: "customers",
selectOptionAttribute: "name",
selectSearchAttributes: ["name"],
component: customer,
inputColumn: "Allgemeines",
sortable: true
},
{
key: "space_number",
label: "Kundenlagerplatznr.",
inputType: "text",
inputIsNumberRange: true,
inputColumn: "Allgemeines",
sortable: true
},
{
key: "type",
label: "Typ",
inputType: "select",
required: true,
selectValueAttribute: "label",
selectManualOptions: [
{label:"Standort"},
{label:"Raum"},
{label:"Regalplatz"},
{label:"Kiste"},
{label:"Palettenplatz"},
{label:"Sonstiges"}
],
inputColumn: "Allgemeines",
sortable: true
},
{
key: "parentSpace",
label: "Übergeordneter Kundenlagerplatz",
inputType: "select",
selectDataType: "customerspaces",
selectOptionAttribute: "space_number",
selectValueAttribute: "id",
inputColumn: "Allgemeines"
},
{
key: "info_data.streetNumber",
label: "Straße + Hausnummer",
inputType: "text",
disabledInTable: true,
inputColumn: "Ort"
},
{
key: "info_data.special",
label: "Adresszusatz",
inputType: "text",
disabledInTable: true,
inputColumn: "Ort"
},
{
key: "info_data.zip",
label: "Postleitzahl",
inputType: "text",
disabledInTable: true,
inputColumn: "Ort",
inputChangeFunction: async function (row) {
const zip = String(row.info_data.zip || "").replace(/\D/g, "")
row.info_data.zip = zip
if ([4, 5].includes(zip.length)) {
const zipData = await useFunctions().useZipCheck(zip)
row.info_data.zip = zipData?.zip || row.info_data.zip
row.info_data.city = zipData?.short || row.info_data.city
}
},
},
{
key: "info_data.city",
label: "Stadt",
inputType: "text",
disabledInTable: true,
inputColumn: "Ort"
},
{
key: "info_data.country",
label: "Land",
inputType: "select",
selectDataType: "countrys",
selectOptionAttribute: "name",
selectValueAttribute: "name",
disabledInTable: true,
inputColumn: "Ort"
},
{
key: "address",
label: "Adresse",
component: address
},
{
key: "description",
label: "Beschreibung",
inputType: "textarea",
}
],
showTabs: [
{
label: 'Informationen',
}, {
label: 'Dateien',
},{label: 'Kundeninventar', key: 'customerinventoryitems'},{label: 'Wiki'}
]
},
users: {
label: "Benutzer",
labelSingle: "Benutzer"
@@ -1968,6 +2185,179 @@ export const useDataStore = defineStore('data', () => {
}
]
},
customerinventoryitems: {
isArchivable: true,
label: "Kundeninventar",
labelSingle: "Kundeninventarartikel",
isStandardEntity: true,
selectWithInformation: "*, files(*), customer(id,name), customerspace(id,name,space_number), product(id,name,article_number,description,manufacturer,manufacturer_number,purchase_price,vendorAllocation), vendor(id,name)",
redirect: true,
numberRangeHolder: "customerInventoryId",
historyItemHolder: "customerinventoryitem",
inputColumns: [
"Allgemeines",
"Anschaffung"
],
filters:[{
name: "Archivierte ausblenden",
default: true,
"filterFunction": function (row) {
return !row.archived
}
}],
templateColumns: [
{
key: "name",
label: "Name",
title: true,
required: true,
inputType: "text",
inputColumn: "Allgemeines",
sortable: true
},
{
key: "customer",
label: "Kunde",
inputType: "select",
required: true,
selectDataType: "customers",
selectOptionAttribute: "name",
selectSearchAttributes: ["name"],
component: customer,
inputColumn: "Allgemeines",
sortable: true
},
{
key: "customerInventoryId",
label: "Kundeninventar-ID",
inputType: "text",
inputIsNumberRange: true,
inputColumn: "Allgemeines",
sortable: true
},
{
key: "product",
label: "Ableitung von Artikel",
inputType: "select",
selectDataType: "products",
selectOptionAttribute: "name",
selectSearchAttributes: ["name", "article_number"],
inputColumn: "Allgemeines",
component: product,
inputChangeFunction: function (row, loadedOptions) {
const products = loadedOptions?.products || []
const selected = products.find((p) => p.id === row.product)
if (!selected) return
row.name = selected.name || null
row.description = selected.description || null
row.manufacturer = selected.manufacturer || null
row.manufacturerNumber = selected.manufacturer_number || null
row.purchasePrice = typeof selected.purchase_price === "number" ? selected.purchase_price : row.purchasePrice
row.currentValue = row.currentValue ?? row.purchasePrice
const allocations = Array.isArray(selected.vendor_allocation)
? selected.vendor_allocation
: (Array.isArray(selected.vendorAllocation) ? selected.vendorAllocation : [])
const firstAllocation = allocations[0]
if (typeof firstAllocation === "number") {
row.vendor = firstAllocation
} else if (firstAllocation && typeof firstAllocation === "object") {
row.vendor = firstAllocation.vendor || firstAllocation.vendor_id || firstAllocation.id || row.vendor
}
}
},
{
key: "description",
label: "Beschreibung",
inputType: "textarea",
inputColumn: "Allgemeines",
sortable: true
},
{
key: "customerspace",
label: "Aktueller Kundenlagerplatz",
inputType: "select",
selectDataType: "customerspaces",
selectOptionAttribute: "name",
selectSearchAttributes: ["name", "space_number"],
inputColumn: "Allgemeines",
component: space
},
{
key: "serialNumber",
label: "Seriennummer",
inputType: "text",
inputColumn: "Allgemeines"
},
{
key: "quantity",
label: "Menge",
inputType: "number",
inputColumn: "Allgemeines",
disabledFunction: function (item) {
return item.serialNumber
},
helpComponent: quantity,
sortable: true
},
{
key: "purchaseDate",
label: "Kaufdatum",
inputType: "date",
inputColumn: "Anschaffung",
sortable: true
},
{
key: "vendor",
label: "Lieferant",
inputType: "select",
selectDataType: "vendors",
selectOptionAttribute: "name",
selectSearchAttributes: ["name"],
inputColumn: "Anschaffung",
component: vendor
},
{
key: "purchasePrice",
label: "Kaufpreis",
inputType: "number",
inputStepSize: "0.01",
inputColumn: "Anschaffung",
component: purchasePrice,
sortable: true
},
{
key: "manufacturer",
label: "Hersteller",
inputType: "text",
inputColumn: "Anschaffung"
},
{
key: "manufacturerNumber",
label: "Herstellernummer",
inputType: "text",
inputColumn: "Anschaffung"
},
{
key: "currentValue",
label: "Aktueller Wert",
inputType: "number",
inputStepSize: "0.01",
inputColumn: "Anschaffung",
sortable: true
},
],
showTabs: [
{
label: 'Informationen',
}, {
label: 'Dateien',
}, {
label: 'Wiki',
}
]
},
inventoryitems: {
isArchivable: true,
label: "Inventarartikel",