New CustomerInventory,
New Mitgliederverwaltung für Vereine New Bank Auto Complete
This commit is contained in:
@@ -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" />
|
||||
|
||||
@@ -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}`)"
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
48
frontend/components/columnRenderings/memberrelation.vue
Normal file
48
frontend/components/columnRenderings/memberrelation.vue
Normal 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>
|
||||
20
frontend/components/columnRenderings/product.vue
Normal file
20
frontend/components/columnRenderings/product.vue
Normal 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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user