Changes in RLS and Tenants

Many Changes in Invoice Editor
Page Loading Rebuilt
Started Rights Management
This commit is contained in:
2024-01-30 21:18:46 +01:00
parent 3167b6a20a
commit fe74e7d91b
13 changed files with 885 additions and 527 deletions

View File

@@ -0,0 +1,887 @@
<script setup>
import dayjs from "dayjs"
const dataStore = useDataStore()
const user = useSupabaseUser()
const route = useRoute()
const tabItems = [
{
label: "Editor"
},
{
label: "Vorschau"
}
]
const itemInfo = ref({
type: "invoices",
customer: null,
contact: null,
address: {
street: null,
special: null,
zip: null,
city: null,
},
project: null,
documentNumber: null,
documentDate: null,
deliveryDate: null,
dateOfPerformance: null,
createdBy: user.value.id,
title: null,
description: null,
startText: "Sehr geehrte Frau Sindern,\n" +
"wir bedanken uns für Ihr entgegengebrachtes Vertrauen und Ihren Auftrag und stellen Ihnen\n" +
"folgende Positionen in Rechnung: ",
endText: "Bitte überweisen Sie den Rechnungsbetrag unter Angabe der Rechnungsnummer im Verwendungszweck innerhalb von 10 Tagen auf das unten angegebene Konto. Wir bedanken uns für das entgegengebrachte Vertrauen und freuen uns auf eine weitere gute Zusammenarbeit.",
rows: [
]
})
const setupPage = () => {
if(route.params) {
if(route.params.id) itemInfo.value = dataStore.getCreatedDocumentById(Number(route.params.id))
}
if(route.query) {
if(route.query.type) itemInfo.value.type = route.query.type
if(route.query.project) itemInfo.value.project = Number(route.query.project)
if(route.query.customer) itemInfo.value.customer = Number(route.query.customer)
}
}
const getRowAmount = (row) => {
return String(Number(Number(row.quantity) * Number(row.price) * (1 - Number(row.discountPercent) /100) ).toFixed(2)).replace('.',',')
}
const addPosition = (mode) => {
let lastId = 0
itemInfo.value.rows.forEach(row => {
if(row.id > lastId) lastId = row.id
})
if(mode === 'free'){
itemInfo.value.rows.push({
id: lastId +1,
mode: "free",
text: "",
quantity: 1,
unit: 1,
price: 0,
taxPercent: 19,
discountPercent: 0
})
} else if(mode === 'normal'){
itemInfo.value.rows.push({
id: lastId +1,
mode: "normal",
quantity: 1,
price: 0,
taxPercent: 19,
discountPercent: 0
})
} else if(mode === "pagebreak") {
itemInfo.value.rows.push({
id: lastId +1,
mode: "pagebreak",
})
}
setPosNumbers()
}
const removePosition = (id) => {
let rows = itemInfo.value.rows.filter(row => row.id !== id)
/*rows = rows.sort((a,b) => a.pos - b.pos)
rows.forEach((row,index) => {
rows[index] = {...row, pos: index + 1}
})*/
itemInfo.value.rows = rows
setPosNumbers()
}
const documentTotal = computed(() => {
let totalNet = 0
let total19 = 0
let totalGross = 0
itemInfo.value.rows.forEach(row => {
if(row.mode === 'free' || row.mode === 'normal'){
let rowPrice = Number(Number(row.quantity) * Number(row.price)).toFixed(2)
totalNet += Number(rowPrice)
if(row.taxPercent === 19) {
total19 += Number(rowPrice * 0.19)
}
}
})
return {
totalNet: `${String(totalNet.toFixed(2)).replace(".",",")}`,
total19: `${String(total19.toFixed(2)).replace(".",",")}`,
totalGross: `${String(Number(totalNet + total19).toFixed(2)).replace(".",",")}`
}
})
const getDocumentData = () => {
let customerData = dataStore.getCustomerById(itemInfo.value.customer)
let contactData = dataStore.getContactById(itemInfo.value.contact)
let userData = dataStore.getProfileById(user.value.id)
let rows = itemInfo.value.rows.map(row => {
let unit = dataStore.units.find(i => i.id === row.unit)
if(row.mode === 'free' || row.mode === 'normal') {
if(row.mode === 'normal') row.text = dataStore.getProductById(row.product).name
return {
...row,
rowAmount: `${getRowAmount(row)}`,
quantity: String(row.quantity).replace(".",","),
unit: unit.short,
pos: String(row.pos),
price: `${String(row.price.toFixed(2)).replace(".",",")}`,
discountText: `(Rabatt: ${row.discountPercent} %)`
}
} else {
return row
}
})
const returnData = {
recipient: {
name: customerData.name,
contact: `${contactData.firstName} ${contactData.lastName}`,
street: customerData.infoData.street,
special: "",
city: customerData.infoData.city,
zip: customerData.infoData.zip
},
info: {
customerNumber: customerData.customerNumber,
documentNumber: itemInfo.value.documentNumber,
documentDate: dayjs(itemInfo.value.documentDate, 'DD.MM.YYYY').format("DD.MM.YYYY"),
deliveryDate: dayjs(itemInfo.value.deliveryDate, 'DD.MM.YYYY').format("DD.MM.YYYY"),
contactPerson: userData.fullName ||"",
contactTel: userData.mobileTel ||"",
contactEMail: userData.email,
project: dataStore.getProjectById(itemInfo.value.project).name
},
title: itemInfo.value.title,
description: itemInfo.value.description,
endText: itemInfo.value.endText,
startText: itemInfo.value.startText,
rows: rows,
total: documentTotal.value
}
console.log(returnData)
return returnData
}
const showDocument = ref(false)
const uri = ref("")
const generateDocument = async () => {
uri.value = await useCreatePdf(getDocumentData())
//alert(uri.value)
showDocument.value = true
}
const onChangeTab = (index) => {
if(index === 1) {
generateDocument()
}
}
const setPosNumbers = () => {
let index = 1
let rows = itemInfo.value.rows.map(row => {
if(row.mode === 'free' ||row.mode === 'normal') {
row.pos = index
index += 1
}
return row
})
}
const saveDocument = async () => {
let createData = {
type: itemInfo.value.type,
customer: itemInfo.value.customer,
contact: itemInfo.value.contact,
address: itemInfo.value.address,
project: itemInfo.value.project,
documentNumber: itemInfo.value.documentNumber,
documentDate: itemInfo.value.documentDate,
state: "Entwurf",
info: {
},
createdBy: itemInfo.value.createdBy,
title: itemInfo.value.title,
description: itemInfo.value.description,
startText: itemInfo.value.startText,
endText: itemInfo.value.endText,
rows: itemInfo.value.rows
}
if(route.params.id) {
await dataStore.updateItem("createdDocuments", {...createData, id: itemInfo.value.id})
} else {
await dataStore.createNewItem("createdDocuments", createData)
}
}
const closeDocument = () => {
generateDocument()
console.log(uri)
}
setupPage()
</script>
<template>
<UCard class="h-fit">
<InputGroup class="mb-3">
<UButton
icon="i-mdi-content-save"
@click="saveDocument"
>
Entwurf
</UButton>
<UButton
@click="closeDocument"
>
Fertigstellen
</UButton>
</InputGroup>
<UTabs :items="tabItems" @change="onChangeTab">
<template #item="{item}">
<div v-if="item.label === 'Editor'">
<InputGroup>
<div class="flex-auto mr-5">
<UFormGroup
label="Dokumenttyp:"
>
<USelectMenu
:options="Object.keys(dataStore.documentTypesForCreation).map(i => {return {label: dataStore.documentTypesForCreation[i].labelSingle, type: i }})"
v-model="itemInfo.type"
value-attribute="type"
option-attribute="label"
>
<template #label>
{{dataStore.documentTypesForCreation[itemInfo.type].labelSingle}}
</template>
</USelectMenu>
</UFormGroup>
<UFormGroup
label="Kunde:"
>
<USelectMenu
:options="dataStore.customers"
option-attribute="name"
value-attribute="id"
:search-attributes="['name']"
searchable
searchable-placeholder="Suche..."
v-model="itemInfo.customer"
>
<template #label>
{{dataStore.getCustomerById(itemInfo.customer) ? dataStore.getCustomerById(itemInfo.customer).name : "Kein Kunde ausgewählt"}}
</template>
</USelectMenu>
</UFormGroup>
<UFormGroup
label="Ansprechpartner:"
>
<USelectMenu
:options="dataStore.getContactsByCustomerId(itemInfo.customer)"
option-attribute="fullName"
value-attribute="id"
:search-attributes="['name']"
searchable
searchable-placeholder="Suche..."
v-model="itemInfo.contact"
>
<template #label>
{{dataStore.getContactById(itemInfo.contact) ? dataStore.getContactById(itemInfo.contact).fullName : "Kein Kontakt ausgewählt"}}
</template>
</USelectMenu>
</UFormGroup>
<UFormGroup
label="Adresse:"
>
<UInput
v-model="itemInfo.address.street"
:placeholder="dataStore.getCustomerById(itemInfo.customer) ? dataStore.getCustomerById(itemInfo.customer).infoData.street : 'Straße + Hausnummer'"
/>
<UInput
v-model="itemInfo.address.special"
class="mt-3"
:placeholder="dataStore.getCustomerById(itemInfo.customer) ? dataStore.getCustomerById(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'"
/>
<UInput
class="flex-auto"
v-model="itemInfo.address.city"
:placeholder="dataStore.getCustomerById(itemInfo.customer) ? dataStore.getCustomerById(itemInfo.customer).infoData.city : 'Ort'"
/>
</InputGroup>
</UFormGroup>
</div>
<div class="flex-auto">
<UFormGroup
label="Rechnungsnummer:"
>
<UInput
v-model="itemInfo.documentNumber"
placeholder="Leer lassen für automatisch generierte Nummer"
/>
</UFormGroup>
<UFormGroup
label="Datum:"
>
<UInput
v-model="itemInfo.documentDate"
/>
</UFormGroup>
<UFormGroup
label="Lieferdatum:"
>
<UInput
v-model="itemInfo.deliveryDate"
/>
</UFormGroup>
<UFormGroup
label="Ansprechpartner:"
>
<UInput
/>
</UFormGroup>
<UFormGroup
label="Kontakt Telefon:"
>
<UInput
/>
</UFormGroup>
<UFormGroup
label="Kontakt E-Mail:"
>
<UInput
/>
</UFormGroup><UFormGroup
label="Kontakt E-Mail:"
>
<UInput
/>
</UFormGroup>
<UFormGroup
label="Projekt:"
>
<USelectMenu
:options="dataStore.projects"
v-model="itemInfo.project"
value-attribute="id"
option-attribute="name"
searchable
searchable-placeholder="Suche..."
:search-attributes="['name']"
>
<template #label>
{{dataStore.getProjectById(itemInfo.project) ? dataStore.getProjectById(itemInfo.project).name : "Kein Projekt ausgewählt"}}
</template>
<template #option="{option: project}">
{{dataStore.getCustomerById(project.customer).name}} - {{project.name}}
</template>
</USelectMenu>
</UFormGroup>
</div>
</InputGroup>
<UDivider
class="my-3"
/>
<UFormGroup
label="Titel:"
>
<UInput v-model="itemInfo.title"/>
</UFormGroup>
<UFormGroup
label="Beschreibung:"
class="mt-3"
>
<UInput v-model="itemInfo.description"/>
</UFormGroup>
<UDivider
class="my-3"
/>
<UFormGroup
label="Einleitung:"
>
<UTextarea
v-model="itemInfo.startText"
/>
</UFormGroup>
<UDivider
class="my-3"
/>
<table class="w-full">
<thead>
<tr>
<th></th>
<th>Pos.</th>
<th>Produkt / Leistung</th>
<th>Menge</th>
<th>Einheit</th>
<th>Preis</th>
<th>Steuer</th>
<th>Rabatt</th>
<th>Gesamt</th>
</tr>
</thead>
<draggable
v-model="itemInfo.rows"
handle=".handle"
tag="tbody"
itemKey="pos"
@end="setPosNumbers"
>
<template #item="{element: row}">
<tr
>
<td>
<UIcon
class="handle"
name="i-mdi-menu"
/>
</td>
<td
v-if="row.mode === 'pagebreak'"
colspan="8"
>
<UDivider/>
</td>
<td
v-if="row.mode === 'free' || row.mode === 'normal'"
>{{row.pos}}</td>
<td
class="w-120"
v-if="row.mode === 'free'"
>
<UInput
v-model="row.text"
maxlength="40"
placeholder="Name"
/>
</td>
<td
class="w-120"
v-else-if="row.mode === 'normal'"
>
<USelectMenu
:options="dataStore.products"
option-attribute="name"
value-attribute="id"
searchable
searchable-placeholder="Suche ..."
:search-attributes="['name']"
v-model="row.product"
@change="row.unit = dataStore.getProductById(row.product).unit,
row.price = dataStore.getProductById(row.product).sellingPrice || 0"
>
<template #label>
{{dataStore.getProductById(row.product) ?dataStore.getProductById(row.product).name : "Kein Produkt ausgewählt" }}
</template>
</USelectMenu>
</td>
<td
class="w-20"
v-if="row.mode === 'free' || row.mode === 'normal'"
>
<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' "
/>
</td>
<td
class="w-40"
v-if="row.mode === 'free' || row.mode === 'normal'"
>
<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>
</td>
<td
v-if="row.mode === 'free' || row.mode === 'normal'"
>
<UInput
v-model="row.price"
type="number"
step="0.01"
>
<template #trailing>
<span class="text-gray-500 dark:text-gray-400 text-xs">EUR</span>
</template>
</UInput>
</td>
<td
class="w-40"
v-if="row.mode === 'free' || row.mode === 'normal'"
>
<USelectMenu
:options="['19','7','0']"
v-model="row.taxPercent"
>
<template #option="{option}">
{{option}} %
</template>
<template #label>
{{row.taxPercent}} %
</template>
</USelectMenu>
</td>
<td
class="w-40"
v-if="row.mode === 'free' || row.mode === 'normal'"
>
<UInput
v-model="row.discountPercent"
type="number"
step="0.01"
placeholder="0"
>
<template #trailing>
<span class="text-gray-500 dark:text-gray-400 text-xs">%</span>
</template>
</UInput>
</td>
<td
v-if="row.mode === 'free' || row.mode === 'normal'"
>
<div class="text-right font-bold">{{getRowAmount(row)}} </div>
</td>
<td>
<UButton
variant="ghost"
color="rose"
icon="i-heroicons-x-mark-16-solid"
@click="removePosition(row.id)"
/>
</td>
</tr>
</template>
</draggable>
<!-- <template v-for="row in itemInfo.rows">
<tr
>
<td
v-if="row.mode === 'pagebreak'"
colspan="8"
>
<UDivider/>
</td>
<td
rowspan="2"
v-if="row.mode === 'free' || row.mode === 'normal'"
>{{row.pos}}</td>
<td
class="w-120"
v-if="row.mode === 'free'"
>
<UInput
v-model="row.text"
maxlength="40"
placeholder="Name"
/>
</td>
<td
class="w-120"
v-else-if="row.mode === 'normal'"
>
<USelectMenu
:options="dataStore.products"
option-attribute="name"
value-attribute="id"
searchable
searchable-placeholder="Suche ..."
:search-attributes="['name']"
v-model="row.product"
@change="row.unit = dataStore.getProductById(row.product).unit,
row.price = dataStore.getProductById(row.product).sellingPrice || 0"
>
<template #label>
{{dataStore.getProductById(row.product) ?dataStore.getProductById(row.product).name : "Kein Produkt ausgewählt" }}
</template>
</USelectMenu>
</td>
<td
class="w-20"
v-if="row.mode === 'free' || row.mode === 'normal'"
>
<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' "
/>
</td>
<td
class="w-40"
v-if="row.mode === 'free' || row.mode === 'normal'"
>
<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>
</td>
<td
v-if="row.mode === 'free' || row.mode === 'normal'"
>
<UInput
v-model="row.price"
type="number"
step="0.01"
>
<template #trailing>
<span class="text-gray-500 dark:text-gray-400 text-xs">EUR</span>
</template>
</UInput>
</td>
<td
class="w-40"
v-if="row.mode === 'free' || row.mode === 'normal'"
>
<USelectMenu
:options="['19','7','0']"
v-model="row.taxPercent"
>
<template #option="{option}">
{{option}} %
</template>
<template #label>
{{row.taxPercent}} %
</template>
</USelectMenu>
&lt;!&ndash; <UInput
v-model="row.taxPercent"
>
<template #trailing>
<span class="text-gray-500 dark:text-gray-400 text-xs">%</span>
</template>
</UInput>&ndash;&gt;
</td>
<td
class="w-40"
v-if="row.mode === 'free' || row.mode === 'normal'"
>
<UInput
v-model="row.discountPercent"
type="number"
step="0.01"
placeholder="0"
>
<template #trailing>
<span class="text-gray-500 dark:text-gray-400 text-xs">%</span>
</template>
</UInput>
</td>
<td
v-if="row.mode === 'free' || row.mode === 'normal'"
rowspan="2"
>
<div class="text-right font-bold">{{getRowAmount(row)}} </div>
</td>
<td rowspan="2">
<UButton
variant="ghost"
color="rose"
icon="i-heroicons-x-mark-16-solid"
@click="removePosition(row.pos)"
/>
</td>
</tr>
<tr>
<td colspan="6">
<UTextarea
v-model="row.description"
placeholder="Beschreibung"
/>
</td>
</tr>
</template>-->
</table>
<InputGroup>
<UButton
@click="addPosition('normal')"
class="mt-3"
>
+ Artikelposition
</UButton>
<UButton
@click="addPosition('free')"
class="mt-3"
>
+ Freitextposition
</UButton>
<UButton
@click="addPosition('pagebreak')"
class="mt-3"
>
+ Seitenumbruch
</UButton>
</InputGroup>
<table>
<tr>
<td class="font-bold">Netto:</td>
<td>{{documentTotal.totalNet}}</td>
</tr>
<tr>
<td class="font-bold">zzgl. 19 % USt:</td>
<td>{{documentTotal.total19}}</td>
</tr>
<tr>
<td class="font-bold">Brutto:</td>
<td>{{documentTotal.totalGross}}</td>
</tr>
</table>
<UDivider
class="my-3"
/>
<UFormGroup
label="Nachbemerkung:"
>
<UTextarea
v-model="itemInfo.endText"
/>
</UFormGroup>
</div>
<div v-else-if="item.label === 'Vorschau'">
<!-- <UButton
@click="generateDocument"
>
Show
</UButton>-->
<object
:data="uri"
v-if="showDocument"
type="application/pdf"
class="w-full previewDocument"
/>
</div>
</template>
</UTabs>
</UCard>
</template>
<style scoped>
th {
text-align: left;
}
td {
padding: .4em;
}
/*tr:hover {
border: 1px solid #69c350;
}*/
.previewDocument {
height: 80vh;
}
</style>