Merge branch 'beta'

# Conflicts:
#	components/DocumentDisplay.vue
#	components/MainNav.vue
#	composables/useRole.js
#	composables/useSupabase.js
#	pages/createDocument/edit/[[id]].vue
#	pages/files/index.vue
#	pages/inventoryitems/index.vue
#	pages/services/[mode]/[[id]].vue
#	pages/vendors/[mode]/[[id]].vue
#	stores/data.js
This commit is contained in:
2025-01-20 11:05:22 +01:00
135 changed files with 7403 additions and 11455 deletions

View File

@@ -18,6 +18,7 @@ const {color,variant} = props
const showModal = ref(false)
const emitConfirm = () => {
showModal.value = false
emit('confirmed')
}
</script>
@@ -38,19 +39,22 @@ const emitConfirm = () => {
<slot/>
<template #footer>
<div class="text-right">
<UButton
color="rose"
variant="outline"
@click="showModal = false"
>
Abbrechen
</UButton>
<UButton
@click="emitConfirm"
class="ml-2"
>
Bestätigen
</UButton>
<UButtonGroup>
<UButton
color="rose"
variant="outline"
@click="showModal = false"
>
Abbrechen
</UButton>
<UButton
@click="emitConfirm"
class="ml-2"
>
Bestätigen
</UButton>
</UButtonGroup>
</div>
</template>
</UCard>

View File

@@ -1,8 +1,12 @@
<script setup>
import DocumentDisplayModal from "~/components/DocumentDisplayModal.vue";
const toast = useToast()
const supabase = useSupabaseClient()
const dataStore = useDataStore()
const modal = useModal()
const profileStore = useProfileStore()
const router = useRouter()
const props = defineProps({
documentData: {
@@ -19,129 +23,26 @@ const props = defineProps({
})
let {documentData, openShowModal:openShowModalProp, returnEmit } = props;
const tags = dataStore.getDocumentTags
const openShowModal = ref(false)
//Functions
const openDocument = async () => {
//selectedDocument.value = doc
openShowModal.value = true
console.log("open")
}
const updateDocument = async () => {
console.log("Update")
const {url, ...objData} = documentData
delete objData.url
let {documentData, returnEmit } = props;
const {data,error} = await supabase
.from("documents")
.update(objData)
.eq('id',objData.id)
.select()
if(error) {
console.log(error)
} else {
toast.add({title: "Dokument aktualisiert"})
dataStore.fetchDocuments()
//openShowModal.value = false
}
}
const createVendorInvoice = async () => {
const {data:vendorInvoiceData,error:vendorInvoiceError} = await supabase
.from("incominginvoices")
.insert([{
document: documentData.id,
tenant: dataStore.currentTenant
}])
.select()
if(vendorInvoiceError) {
console.log(vendorInvoiceError)
} else if(vendorInvoiceData) {
const {data:documentUpdateData,error:documentError} = await supabase
.from("documents")
.update({
vendorInvoice: vendorInvoiceData[0].id
})
.eq('id',documentData.id)
.select()
if(documentError) {
console.log(documentError)
} else {
toast.add({title: "Dokument aktualisiert"})
dataStore.fetchDocuments()
openShowModal.value = false
}
dataStore.fetchIncomingInvoices()
await router.push("/receipts")
}
const showFile = (file) => {
console.log(file)
modal.open(DocumentDisplayModal,{
documentData: file
})
}
const archiveDocument = () => {
documentData.tags.push("Archiviert")
updateDocument()
}
const resourceOptions = ref([
{label: 'Projekt', value: 'project', optionAttr: "name"},
{label: 'Kunde', value: 'customer', optionAttr: "name"},
{label: 'Lieferant', value: 'vendor', optionAttr: "name"},
{label: 'Fahrzeug', value: 'vehicle', optionAttr: "licensePlate"},
{label: 'Objekt', value: 'plant', optionAttr: "name"},
{label: 'Vertrag', value: 'contract', optionAttr: "name"},
{label: 'Produkt', value: 'product', optionAttr: "name"}
])
const resourceToAssign = ref("project")
const itemOptions = ref([])
const idToAssign = ref(null)
const getItemsBySelectedResource = () => {
if(resourceToAssign.value === "project") {
itemOptions.value = dataStore.projects
} else if(resourceToAssign.value === "customer") {
itemOptions.value = dataStore.customers
} else if(resourceToAssign.value === "vendor") {
itemOptions.value = dataStore.vendors
} else if(resourceToAssign.value === "vehicle") {
itemOptions.value = dataStore.vehicles
} else if(resourceToAssign.value === "product") {
itemOptions.value = dataStore.products
} else if(resourceToAssign.value === "plant") {
itemOptions.value = dataStore.plants
} else if(resourceToAssign.value === "contract") {
itemOptions.value = dataStore.contracts
} else {
itemOptions.value = []
}
}
getItemsBySelectedResource()
const updateDocumentAssignment = async () => {
documentData[resourceToAssign.value] = idToAssign.value
await updateDocument()
}
</script>
<template>
<div class="documentListItem" @click="returnEmit ? $emit('clicked', documentData.id) : openShowModal = true">
<div :id="`docDisplay-${documentData.id}`" class="documentListItem" @click="returnEmit ? $emit('clicked', documentData.id) : showFile(documentData)">
<iframe
:src="`${documentData.url}#toolbar=0&navpanes=0&scrollbar=0`"
class="previewEmbed"
v-if="!documentData.tags.includes('Bild')"
v-if="documentData.path.includes('pdf')"
loading="lazy"
/>
<img
@@ -150,195 +51,17 @@ const updateDocumentAssignment = async () => {
:src="documentData.url"
/>
<!-- TODO: Remove Scrollbar -->
<UTooltip class="w-full" :text="documentData.path.split('_')[documentData.path.split('_').length -1]">
<p class="truncate my-3">{{documentData.path.split("_")[documentData.path.split("_").length -1]}}</p>
<UTooltip class="w-full" :text="documentData.path.split('/')[documentData.path.split('/').length -1]">
<p class="truncate my-3">{{documentData.path.split("/")[documentData.path.split("/").length -1]}}</p>
</UTooltip>
<InputGroup class="mt-3 flex-wrap">
<UBadge
v-for="tag in documentData.tags"
v-for="tag in documentData.filetags"
><span class="text-nowrap">{{ tag }}</span></UBadge>
><span class="text-nowrap">{{ tag.name }}</span></UBadge>
</InputGroup>
<!-- <UButton
@click="openDocument"
class="mt-3"
icon=""
>
<UIcon name="i-heroicons-eye-solid" />
</UButton>-->
<!-- <UToggle
v-model="documentData.selected"
class="ml-2"
/>-->
<!-- <UBadge
v-if="documentData.vendorInvoice"
>{{dataStore.incominginvoices.find(item => item.id === documentData.vendorInvoice) ? dataStore.incominginvoices.find(item => item.id === documentData.vendorInvoice).reference : ''}}</UBadge>
<UBadge
v-if="documentData.inDatev"
>DATEV</UBadge>-->
</div>
<USlideover
v-model="openShowModal"
fullscreen
>
<UCard class="flex flex-col flex-1" :ui="{ body: { base: 'flex-1' }, ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
<template #header>
<div class="flex flex-row justify-between">
<div class="flex items-center gap-2">
<UBadge
v-for="tag in documentData.tags"
>
{{tag}}
</UBadge>
</div>
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="openShowModal = false" />
</div>
</template>
<UContainer class="h-full" :ui="{padding: 'px-1 sm:px-1 lg:px-1'}">
<object
class="bigPreview"
:data="`${documentData.url}#toolbar=0&navpanes=0&scrollbar=0`"
type="application/pdf"
v-if="!documentData.tags.includes('Bild')"
/>
<img
class=" w-full"
:src="documentData.url"
alt=""
v-else
/>
</UContainer>
<template #footer>
<UButtonGroup>
<UButton
@click="archiveDocument"
>
Archivieren
</UButton>
<!-- <UButton
v-if="documentData.tags.includes('Eingangsrechnung')"
@click="createVendorInvoice"
>
Eingangsrechnung erstellen
</UButton>-->
</UButtonGroup>
<br>
<a
:href="documentData.url"
target="_blank"
>In neuen Tab anzeigen</a>
<UFormGroup
label="Tags ändern:"
>
<USelectMenu
:options="tags"
v-model="documentData.tags"
@change="updateDocument"
multiple
>
<template #label>
{{documentData.tags.length}} ausgewählt
</template>
</USelectMenu>
</UFormGroup>
<p>Dokument zuweisen:</p>
<UFormGroup
label="Resource auswählen"
>
<USelectMenu
:options="resourceOptions"
v-model="resourceToAssign"
value-attribute="value"
option-attribute="label"
@change="getItemsBySelectedResource"
>
</USelectMenu>
</UFormGroup>
<UFormGroup
label="Eintrag auswählen:"
>
</UFormGroup>
<USelectMenu
:options="itemOptions"
v-model="idToAssign"
:option-attribute="resourceOptions.find(i => i.value === resourceToAssign)? resourceOptions.find(i => i.value === resourceToAssign).optionAttr : 'name'"
value-attribute="id"
@change="updateDocumentAssignment"
></USelectMenu>
<!-- <UFormGroup
label="Projekt zuweisen:"
>
<USelectMenu
:options="dataStore.projects"
option-attribute="name"
value-attribute="id"
v-model="documentData.project"
@change="updateDocument"
searchable
:search-attributes="['name']"
>
<template #label>
{{dataStore.projects.find(item => item.id === documentData.project) ? dataStore.projects.find(item => item.id === documentData.project).name : "Kein Projekt ausgewählt" }}
</template>
</USelectMenu>
</UFormGroup>
<UFormGroup
label="Kunde zuweisen:"
>
<USelectMenu
:options="dataStore.customers"
option-attribute="name"
value-attribute="id"
v-model="documentData.customer"
@change="updateDocument"
searchable
:search-attributes="['name']"
>
<template #label>
{{dataStore.customers.find(item => item.id === documentData.customer) ? dataStore.customers.find(item => item.id === documentData.customer).name : "Kein Kunde ausgewählt" }}
</template>
</USelectMenu>
</UFormGroup>-->
</template>
</UCard>
<!-- <UCard class="h-full">
</UCard>-->
</USlideover>
</template>
<style scoped>
@@ -370,9 +93,4 @@ const updateDocumentAssignment = async () => {
display: none;
}
.bigPreview {
width: 100%;
aspect-ratio: 1/ 1.414;
}
</style>

View File

@@ -0,0 +1,368 @@
<script setup>
const toast = useToast()
const supabase = useSupabaseClient()
const dataStore = useDataStore()
const modal = useModal()
const props = defineProps({
documentData: {
type: Object,
required: true
},
openShowModal: {
type: Boolean,
required: false,
},
returnEmit: {
type: Boolean
},
})
const emit = defineEmits(["updateNeeded"])
const folders = ref([])
const setup = async () => {
const {data} = await supabase.from("folders").select().eq("tenant",useProfileStore().currentTenant)
data.forEach(folder => {
let name = folder.name
const addParent = (item) => {
name = `${item.name} > ${name}`
if(item.parent){
addParent(data.find(i => i.id === item.parent))
} else {
folders.value.push({
id: folder.id,
name: name,
})
}
}
if(folder.parent) {
addParent(data.find(i => i.id === folder.parent))
} else {
folders.value.push({
id: folder.id,
name: folder.name,
})
}
})
}
setup()
//Functions
const openDocument = async () => {
//selectedDocument.value = doc
openShowModal.value = true
console.log("open")
}
const updateDocument = async () => {
const {url, ...objData} = props.documentData
delete objData.url
delete objData.filetags
if(objData.project) objData.project = objData.project.id
if(objData.customer) objData.customer = objData.customer.id
if(objData.contract) objData.contract = objData.contract.id
if(objData.vendor) objData.vendor = objData.vendor.id
if(objData.plant) objData.plant = objData.plant.id
if(objData.createddocument) objData.createddocument = objData.createddocument.id
if(objData.vehicle) objData.vehicle = objData.vehicle.id
if(objData.product) objData.product = objData.product.id
if(objData.profile) objData.profile = objData.profile.id
if(objData.check) objData.check = objData.check.id
if(objData.inventoryitem) objData.inventoryitem = objData.inventoryitem.id
if(objData.incominginvoice) objData.incominginvoice = objData.incominginvoice.id
const {data,error} = await supabase
.from("files")
.update(objData)
.eq('id',objData.id)
.select()
if(error) {
console.log(error)
} else {
toast.add({title: "Datei aktualisiert"})
modal.close()
emit("updateNeeded")
//openShowModal.value = false
}
}
const archiveDocument = async () => {
props.documentData.archived = true
await updateDocument()
const {data,error} = await supabase.from("historyitems").insert({
createdBy: useProfileStore().activeProfile.id,
tenant: useProfileStore().currentTenant,
text: "Datei archiviert",
file: props.documentData.id
})
modal.close()
emit("update")
}
const resourceOptions = ref([
{label: 'Projekt', value: 'project', optionAttr: "name"},
{label: 'Kunde', value: 'customer', optionAttr: "name"},
{label: 'Lieferant', value: 'vendor', optionAttr: "name"},
{label: 'Fahrzeug', value: 'vehicle', optionAttr: "licensePlate"},
{label: 'Objekt', value: 'plant', optionAttr: "name"},
{label: 'Vertrag', value: 'contract', optionAttr: "name"},
{label: 'Produkt', value: 'product', optionAttr: "name"}
])
const resourceToAssign = ref("project")
const itemOptions = ref([])
const idToAssign = ref(null)
const getItemsBySelectedResource = () => {
if(resourceToAssign.value === "project") {
itemOptions.value = dataStore.projects
} else if(resourceToAssign.value === "customer") {
itemOptions.value = dataStore.customers
} else if(resourceToAssign.value === "vendor") {
itemOptions.value = dataStore.vendors
} else if(resourceToAssign.value === "vehicle") {
itemOptions.value = dataStore.vehicles
} else if(resourceToAssign.value === "product") {
itemOptions.value = dataStore.products
} else if(resourceToAssign.value === "plant") {
itemOptions.value = dataStore.plants
} else if(resourceToAssign.value === "contract") {
itemOptions.value = dataStore.contracts
} else {
itemOptions.value = []
}
}
getItemsBySelectedResource()
const updateDocumentAssignment = async () => {
props.documentData[resourceToAssign.value] = idToAssign.value
await updateDocument()
}
const folderToMoveTo = ref(null)
const moveFile = async () => {
console.log(folderToMoveTo.value)
const {data,error} = await supabase
.from("files")
.update({folder: folderToMoveTo.value})
.eq("id",props.documentData.id)
.select()
if(error) {
console.log(error)
toast.add({title: "Fehler beim verschieben", color:"rose"})
} else {
toast.add({title: "Datei verschoben"})
console.log(data)
}
modal.close()
}
</script>
<template>
<UModal fullscreen >
<UCard :ui="{ body: { base: 'flex-1' }, ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
<template #header>
<div class="flex flex-row justify-between">
<div class="flex items-center gap-2">
<UBadge
v-for="tag in props.documentData.filetags"
>
{{tag.name}}
</UBadge>
</div>
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="modal.close()" />
</div>
</template>
<div class="flex flex-row">
<div class="w-1/3">
<object
class="bigPreview"
:data="`${props.documentData.url}#toolbar=0&navpanes=0&scrollbar=0`"
type="application/pdf"
v-if="props.documentData.path.includes('pdf')"
/>
<img
class=" w-full"
:src="props.documentData.url"
alt=""
v-else
/>
</div>
<div class="w-2/3 p-5">
<UButtonGroup>
<ButtonWithConfirm
color="rose"
variant="outline"
@confirmed="archiveDocument"
>
<template #button>
Archivieren
</template>
<template #header>
<span class="text-md text-black font-bold">Archivieren bestätigen</span>
</template>
Möchten Sie die Datei wirklich archivieren?
</ButtonWithConfirm>
<UButton
:to="props.documentData.url"
variant="outline"
icon="i-heroicons-arrow-top-right-on-square"
target="_blank"
>
Öffnen
</UButton>
</UButtonGroup>
<UDivider>Zuweisungen</UDivider>
<table class="w-full">
<tr v-if="props.documentData.project">
<td>Projekt</td>
<td>
<nuxt-link :to="`/standardEntity/projects/show/${props.documentData.project.id}`">{{props.documentData.project.name}}</nuxt-link>
</td>
</tr>
<tr v-if="props.documentData.customer">
<td>Kunde</td>
<td>
<nuxt-link :to="`/standardEntity/customers/show/${props.documentData.customer.id}`">{{props.documentData.customer.name}}</nuxt-link>
</td>
</tr>
<tr v-if="props.documentData.vendor">
<td>Lieferant</td>
<td>
<nuxt-link :to="`/standardEntity/vendors/show/${props.documentData.vendor.id}`">{{props.documentData.vendor.name}}</nuxt-link>
</td>
</tr>
<tr v-if="props.documentData.createddocument">
<td>Ausgangsbeleg</td>
<td>
<nuxt-link :to="`/createDocument/show/${props.documentData.createddocument.id}`">{{props.documentData.createddocument.documentNumber}}</nuxt-link>
</td>
</tr>
<tr v-if="props.documentData.plant">
<td>Objekt</td>
<td>
<nuxt-link :to="`/standardEntity/plants/show/${props.documentData.plant.id}`">{{props.documentData.plant.name}}</nuxt-link>
</td>
</tr>
<tr v-if="props.documentData.contract">
<td>Vertrag</td>
<td>
<nuxt-link :to="`/standardEntity/contracts/show/${props.documentData.contract.id}`">{{props.documentData.contract.name}}</nuxt-link>
</td>
</tr>
<tr v-if="props.documentData.vehicle">
<td>Fahrzeug</td>
<td>
<nuxt-link :to="`/standardEntity/vehicles/show/${props.documentData.vehicle.id}`">{{props.documentData.vehicle.licensePlate}}</nuxt-link>
</td>
</tr>
<tr v-if="props.documentData.product">
<td>Artikel</td>
<td>
<nuxt-link :to="`/standardEntity/products/show/${props.documentData.product.id}`">{{props.documentData.product.name}}</nuxt-link>
</td>
</tr>
<tr v-if="props.documentData.inventoryitem">
<td>Inventarartikel</td>
<td>
<nuxt-link :to="`/standardEntity/inventoryitem/show/${props.documentData.inventoryitem.id}`">{{props.documentData.inventoryitem.name}}</nuxt-link>
</td>
</tr>
<tr v-if="props.documentData.check">
<td>Überprüfung</td>
<td>
<nuxt-link :to="`/standardEntity/checks/show/${props.documentData.check.id}`">{{props.documentData.check.name}}</nuxt-link>
</td>
</tr>
<tr v-if="props.documentData.profile">
<td>Mitarbeiter</td>
<td>
<nuxt-link :to="`/profiles/show/${props.documentData.profile.id}`">{{props.documentData.profile.fullName}}</nuxt-link>
</td>
</tr>
<tr v-if="props.documentData.incominginvoice">
<td>Eingangsrechnung</td>
<td>
<nuxt-link :to="`/incomingInvoices/show/${props.documentData.incominginvoice.id}`">{{props.documentData.incominginvoice.reference}}</nuxt-link>
</td>
</tr>
</table>
<UDivider class="my-3">Datei zuweisen</UDivider>
<UFormGroup
label="Resource auswählen"
>
<USelectMenu
:options="resourceOptions"
v-model="resourceToAssign"
value-attribute="value"
option-attribute="label"
@change="getItemsBySelectedResource"
>
</USelectMenu>
</UFormGroup>
<UFormGroup
label="Eintrag auswählen:"
>
</UFormGroup>
<USelectMenu
:options="itemOptions"
v-model="idToAssign"
:option-attribute="resourceOptions.find(i => i.value === resourceToAssign)? resourceOptions.find(i => i.value === resourceToAssign).optionAttr : 'name'"
value-attribute="id"
@change="updateDocumentAssignment"
></USelectMenu>
<UDivider class="my-5">Datei verschieben</UDivider>
<InputGroup class="w-full">
<USelectMenu
class="flex-auto"
v-model="folderToMoveTo"
value-attribute="id"
option-attribute="name"
:options="folders"
/>
<UButton
@click="moveFile"
variant="outline"
>Verschieben</UButton>
</InputGroup>
</div>
</div>
</UCard>
</UModal>
</template>
<style scoped>
.bigPreview {
width: 100%;
aspect-ratio: 1/ 1.414;
}
</style>

View File

@@ -10,6 +10,7 @@ const props = defineProps({
})
const dataStore = useDataStore()
const emit = defineEmits(["updateNeeded"])
</script>
@@ -21,6 +22,7 @@ const dataStore = useDataStore()
:key="item.id"
@clicked="(info) => $emit('selectDocument', info)"
:return-emit="returnDocumentId"
@updatedNeeded="emit('updatedNeeded')"
/>
</div>
</template>

View File

@@ -11,16 +11,25 @@ const props = defineProps({
const {type, elementId} = props
const emit = defineEmits(["uploadFinished"])
const dataStore = useDataStore()
const tags = dataStore.getDocumentTags
const profileStore = useProfileStore()
const uploadModalOpen = ref(false)
const uploadInProgress = ref(false)
const fileUploadFormData = ref({
tags: ["Dokument"],
project: null,
tenant: dataStore.currentTenant
tenant: profileStore.currentTenant
})
const availableTags = ref([])
const selectedTags = ref([])
const setup = async () => {
availableTags.value = await useSupabaseSelect("filetags")
}
setup()
const openModal = () => {
uploadModalOpen.value = true
@@ -32,10 +41,11 @@ const uploadFiles = async () => {
let fileData = fileUploadFormData.value
fileData[type] = elementId
await dataStore.uploadFiles(fileData, document.getElementById("fileUploadInput").files,true)
await useFiles().uploadFiles(fileData, document.getElementById("fileUploadInput").files,selectedTags.value,true)
uploadModalOpen.value = false;
uploadInProgress.value = false;
emit("uploadFinished")
}
</script>
@@ -43,7 +53,7 @@ const uploadFiles = async () => {
<USlideover
v-model="uploadModalOpen"
>
<UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
<UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }" class="h-full">
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
@@ -67,6 +77,7 @@ const uploadFiles = async () => {
type="file"
id="fileUploadInput"
multiple
accept="image/jpeg, image/png, image/gif, application/pdf"
/>
</UFormGroup>
<UFormGroup
@@ -75,13 +86,15 @@ const uploadFiles = async () => {
>
<USelectMenu
multiple
option-attribute="name"
value-attribute="id"
searchable
searchable-placeholder="Suchen..."
:options="tags"
v-model="fileUploadFormData.tags"
:options="availableTags"
v-model="selectedTags"
>
<template #label>
<span v-if="fileUploadFormData.tags.length > 0">{{fileUploadFormData.tags.join(", ")}}</span>
<span v-if="selectedTags.length > 0">{{selectedTags.map(i => availableTags.find(x => x.id === i).name).join(", ")}}</span>
<span v-else>Keine Tags ausgewählt</span>
</template>
</USelectMenu>

View File

@@ -0,0 +1,99 @@
<script setup >
const props = defineProps({
fileData: {
type: Object,
default: {
type: null
}
}
})
const emit = defineEmits(["uploadFinished"])
const modal = useModal()
const profileStore = useProfileStore()
const uploadInProgress = ref(false)
const availableFiletypes = ref([])
const setup = async () => {
availableFiletypes.value = await useSupabaseSelect("filetags")
}
setup()
const uploadFiles = async () => {
uploadInProgress.value = true;
await useFiles().uploadFiles(props.fileData, document.getElementById("fileUploadInput").files,[],true)
uploadInProgress.value = false;
emit("uploadFinished")
modal.close()
}
</script>
<template>
<UModal>
<UCard :ui="{ body: { base: 'flex-1' }, ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
Datei hochladen
</h3>
<UButton
color="gray"
variant="ghost"
icon="i-heroicons-x-mark-20-solid"
class="-my-1"
@click="modal.close()"
:disabled="uploadInProgress"
/>
</div>
</template>
<UFormGroup
label="Datei:"
>
<UInput
type="file"
id="fileUploadInput"
multiple
accept="image/jpeg, image/png, image/gif, application/pdf"
/>
</UFormGroup>
<UFormGroup
label="Tags:"
class="mt-3"
>
<USelectMenu
option-attribute="name"
value-attribute="id"
searchable
searchable-placeholder="Suchen..."
:options="availableFiletypes"
v-model="props.fileData.type"
>
<template #label>
<span v-if="props.fileData.type">{{availableFiletypes.find(x => x.id === props.fileData.type).name}}</span>
<span v-else>Keine Typ ausgewählt</span>
</template>
</USelectMenu>
</UFormGroup>
<template #footer>
<UButton
@click="uploadFiles"
:loading="uploadInProgress"
:disabled="uploadInProgress"
>Hochladen</UButton>
</template>
</UCard>
</UModal>
</template>
<style scoped>
</style>

667
components/EntityEdit.vue Normal file
View File

@@ -0,0 +1,667 @@
<script setup>
import dayjs from "dayjs";
import MaterialComposing from "~/components/materialComposing.vue";
const props = defineProps({
type: {
required: true,
type: String
},
item: {
required: true,
type: Object
}
})
const {type} = props
defineShortcuts({
'backspace': () => {
router.push(`/${type}`)
},
'arrowleft': () => {
if(openTab.value > 0){
openTab.value -= 1
}
},
'arrowright': () => {
if(openTab.value < 3) {
openTab.value += 1
}
},
})
const router = useRouter()
const route = useRoute()
const dataStore = useDataStore()
const profileStore = useProfileStore()
const supabase = useSupabaseClient()
const dataType = dataStore.dataTypes[type]
const openTab = ref(0)
const item = ref(JSON.parse(props.item))
console.log(item.value)
const oldItem = ref(null)
const generateOldItemData = () => {
oldItem.value = JSON.parse(props.item)
}
generateOldItemData()
const setupCreate = () => {
dataType.templateColumns.forEach(datapoint => {
if(datapoint.key.includes(".")){
!item.value[datapoint.key.split(".")[0]] ? item.value[datapoint.key.split(".")[0]] = {} : null
}
if(datapoint.inputType === "editor") {
if(datapoint.key.includes(".")){
item.value[datapoint.key.split(".")[0]][datapoint.key.split(".")[1]] = {}
} else {
item.value[datapoint.key] = {}
}
}
})
}
setupCreate()
const setupQuery = () => {
if(route.query) {
Object.keys(route.query).forEach(key => {
if(["customer","contract","plant","contact"].includes(key)){
item.value[key] = Number(route.query[key])
} else {
item.value[key] = route.query[key]
}
})
}
}
setupQuery()
const loadedOptions = ref({})
const loadOptions = async () => {
let optionsToLoad = dataType.templateColumns.filter(i => i.selectDataType).map(i => {
return {
option: i.selectDataType,
key: i.key
}
})
for await(const option of optionsToLoad) {
if(option.option === "countrys") {
loadedOptions.value[option.option] = (await supabase.from("countrys").select()).data
} else if(option.option === "units") {
loadedOptions.value[option.option] = (await supabase.from("units").select()).data
} else {
loadedOptions.value[option.option] = (await useSupabaseSelect(option.option))
if(dataType.templateColumns.find(x => x.key === option.key).selectDataTypeFilter){
loadedOptions.value[option.option] = loadedOptions.value[option.option].filter(i => dataType.templateColumns.find(x => x.key === option.key).selectDataTypeFilter(i, item))
}
}
}
}
loadOptions()
const contentChanged = (content, datapoint) => {
if(datapoint.key.includes(".")){
item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]].html = content.html
item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]].text = content.text
item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]].json = content.json
} else {
item[datapoint.key].html = content.html
item[datapoint.key].text = content.text
item[datapoint.key].json = content.json
}
}
</script>
<template>
<UDashboardNavbar
:ui="{center: 'flex items-stretch gap-1.5 min-w-0'}"
>
<template #left>
<UButton
icon="i-heroicons-chevron-left"
variant="outline"
@click="router.push(`/standardEntity/${type}`)"
>
{{dataType.label}}
</UButton>
</template>
<template #center>
<h1
v-if="item"
:class="['text-xl','font-medium']"
>{{item.id ? `${dataType.labelSingle} bearbeiten` : `${dataType.labelSingle} erstellen` }}</h1>
</template>
<template #right>
<ButtonWithConfirm
color="rose"
variant="outline"
@confirmed="dataStore.updateItem(type,{...item,archived: true}, oldItem)"
>
<template #button>
Archivieren
</template>
<template #header>
<span class="text-md text-black dark:text-white font-bold">Archivieren bestätigen</span>
</template>
Möchten Sie das {{dataType.labelSingle}} {{item[dataType.templateColumns.find(i => i.title).key]}} wirklich archivieren?
</ButtonWithConfirm>
<UButton
v-if="item.id"
@click="dataStore.updateItem(type,item, oldItem)"
>
Speichern
</UButton>
<UButton
v-else
@click="dataStore.createNewItem(type,item)"
>
Erstellen
</UButton>
<UButton
@click="router.push(item.id ? `/standardEntity/${type}/show/${item.id}` : `/standardEntity/${type}`)"
color="red"
class="ml-2"
>
Abbrechen
</UButton>
</template>
</UDashboardNavbar>
<UDashboardPanelContent>
<UForm
class="p-5"
>
<div class="flex flex-row">
<div
v-for="(columnName,index) in dataType.inputColumns"
:class="['w-1/2', ... index < dataType.inputColumns.length -1 ? ['mr-5'] : []]"
>
<UDivider>{{columnName}}</UDivider>
<!--
Die Form Group darf nur in der ersten bearbeitet werden und muss dann runterkopiert werden
-->
<UFormGroup
v-for="datapoint in dataType.templateColumns.filter(i => i.inputType && i.inputColumn === columnName)"
:label="datapoint.label"
>
<template #help>
<component
v-if="datapoint.helpComponent"
:is="datapoint.helpComponent"
:item="item"
/>
</template>
<InputGroup class="w-full" v-if="datapoint.key.includes('.')">
<UInput
class="flex-auto"
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
v-if="['text','number'].includes(datapoint.inputType)"
v-model="item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]]"
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
:type="datapoint.inputType"
:placeholder="datapoint.inputIsNumberRange ? 'Leer lassen für automatisch generierte Nummer' : ''"
>
<template #trailing v-if="datapoint.inputTrailing">
<span class="text-gray-500 dark:text-gray-400 text-xs">{{datapoint.inputTrailing}}</span>
</template>
</UInput>
<UToggle
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
v-else-if="datapoint.inputType === 'bool'"
v-model="item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]]"
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
/>
<USelectMenu
class="flex-auto"
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
v-else-if="datapoint.inputType === 'select'"
v-model="item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]]"
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
:option-attribute="datapoint.selectOptionAttribute"
:value-attribute="datapoint.selectValueAttribute || 'id'"
:options="datapoint.selectManualOptions || loadedOptions[datapoint.selectDataType]"
:searchable="datapoint.selectSearchAttributes"
:search-attributes="datapoint.selectSearchAttributes"
:multiple="datapoint.selectMultiple"
>
<template #empty>
Keine Optionen verfügbar
</template>
</USelectMenu>
<UTextarea
class="flex-auto"
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
v-else-if="datapoint.inputType === 'textarea'"
v-model="item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]]"
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
rows="4"
/>
<UPopover :popper="{ placement: 'bottom-start' }" v-else-if="datapoint.inputType === 'date'">
<UButton
icon="i-heroicons-calendar-days-20-solid"
:label="item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]] ? dayjs(item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]]).format('DD.MM.YYYY') : 'Datum auswählen'"
variant="outline"
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
/>
<template #panel="{ close }">
<LazyDatePicker
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
v-model="item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]]" @close="close"
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
/>
</template>
</UPopover>
<UPopover :popper="{ placement: 'bottom-start' }" v-else-if="datapoint.inputType === 'datetime'">
<UButton
icon="i-heroicons-calendar-days-20-solid"
:label="item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]] ? dayjs(item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]]).format('DD.MM.YY HH:mm') : 'Datum auswählen'"
variant="outline"
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
/>
<template #panel="{ close }">
<LazyDatePicker
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
v-model="item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]]" @close="close"
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
mode="datetime"
/>
</template>
</UPopover>
<!-- TODO: DISABLED FOR TIPTAP -->
<Tiptap
v-else-if="datapoint.inputType === 'editor'"
@updateContent="(i) => contentChanged(i,datapoint)"
:preloadedContent="item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]].html"
/>
<UButton
v-if="['text','number','select','date','datetime','textarea'].includes(datapoint.inputType)"
@click="item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]] = null"
variant="outline"
color="white"
icon="i-heroicons-x-mark"
/>
</InputGroup>
<InputGroup class="w-full" v-else>
<UInput
class="flex-auto"
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
v-if="['text','number'].includes(datapoint.inputType)"
v-model="item[datapoint.key]"
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
:type="datapoint.inputType"
:placeholder="datapoint.inputIsNumberRange ? 'Leer lassen für automatisch generierte Nummer' : ''"
>
<template #trailing v-if="datapoint.inputTrailing">
{{datapoint.inputTrailing}}
</template>
</UInput>
<UToggle
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
v-else-if="datapoint.inputType === 'bool'"
v-model="item[datapoint.key]"
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
/>
<USelectMenu
class="flex-auto"
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
v-else-if="datapoint.inputType === 'select'"
v-model="item[datapoint.key]"
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
:option-attribute="datapoint.selectOptionAttribute"
:value-attribute="datapoint.selectValueAttribute || 'id'"
:options="datapoint.selectManualOptions || loadedOptions[datapoint.selectDataType]"
:searchable="datapoint.selectSearchAttributes"
:search-attributes="datapoint.selectSearchAttributes"
:multiple="datapoint.selectMultiple"
searchable-placeholder="Suche..."
>
<template #empty>
Keine Optionen verfügbar
</template>
</USelectMenu>
<UTextarea
class="flex-auto"
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
v-else-if="datapoint.inputType === 'textarea'"
v-model="item[datapoint.key]"
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
rows="4"
/>
<UPopover :popper="{ placement: 'bottom-start' }" v-else-if="datapoint.inputType === 'date'">
<UButton
icon="i-heroicons-calendar-days-20-solid"
:label="item[datapoint.key] ? dayjs(item[datapoint.key]).format('DD.MM.YYYY') : 'Datum auswählen'"
variant="outline"
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
/>
<template #panel="{ close }">
<LazyDatePicker
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
v-model="item[datapoint.key]" @close="close"
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
/>
</template>
</UPopover>
<UPopover :popper="{ placement: 'bottom-start' }" v-else-if="datapoint.inputType === 'datetime'">
<UButton
icon="i-heroicons-calendar-days-20-solid"
:label="item[datapoint.key] ? dayjs(item[datapoint.key]).format('DD.MM.YY HH:mm') : 'Datum auswählen'"
variant="outline"
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
/>
<template #panel="{ close }">
<LazyDatePicker
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
v-model="item[datapoint.key]"
@close="close"
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
mode="datetime"
/>
</template>
</UPopover>
<Tiptap
v-else-if="datapoint.inputType === 'editor'"
@updateContent="(i) => contentChanged(i,datapoint)"
:preloadedContent="item[datapoint.key].html"
/>
<UButton
v-if="['text','number','select','date','datetime','textarea'].includes(datapoint.inputType)"
@click="item[datapoint.key] = null"
variant="outline"
color="white"
icon="i-heroicons-x-mark"
/>
</InputGroup>
<div
v-if="profileStore.ownTenant.ownFields"
>
<UDivider
class="mt-3"
>Eigene Felder</UDivider>
<UFormGroup
v-for="field in profileStore.ownTenant.ownFields.contracts"
:key="field.key"
:label="field.label"
>
<UInput
v-if="field.type === 'text'"
v-model="item.ownFields[field.key]"
/>
<USelectMenu
v-else-if="field.type === 'select'"
:options="field.options"
v-model="item.ownFields[field.key]"
/>
</UFormGroup>
</div>
</UFormGroup>
</div>
</div>
<UFormGroup
v-for="datapoint in dataType.templateColumns.filter(i => i.inputType && !i.inputColumn)"
:label="datapoint.label"
>
<template #help>
<component
v-if="datapoint.helpComponent"
:is="datapoint.helpComponent"
:item="item"
/>
</template>
<InputGroup class="w-full" v-if="datapoint.key.includes('.')">
<UInput
class="flex-auto"
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
v-if="['text','number'].includes(datapoint.inputType)"
v-model="item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]]"
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
:type="datapoint.inputType"
:placeholder="datapoint.inputIsNumberRange ? 'Leer lassen für automatisch generierte Nummer' : ''"
>
<template #trailing v-if="datapoint.inputTrailing">
<span class="text-gray-500 dark:text-gray-400 text-xs">{{datapoint.inputTrailing}}</span>
</template>
</UInput>
<UToggle
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
v-else-if="datapoint.inputType === 'bool'"
v-model="item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]]"
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
/>
<USelectMenu
class="flex-auto"
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
v-else-if="datapoint.inputType === 'select'"
v-model="item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]]"
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
:option-attribute="datapoint.selectOptionAttribute"
:value-attribute="datapoint.selectValueAttribute || 'id'"
:options="datapoint.selectManualOptions || loadedOptions[datapoint.selectDataType]"
:searchable="datapoint.selectSearchAttributes"
:search-attributes="datapoint.selectSearchAttributes"
:multiple="datapoint.selectMultiple"
>
<template #empty>
Keine Optionen verfügbar
</template>
</USelectMenu>
<UTextarea
class="flex-auto"
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
v-else-if="datapoint.inputType === 'textarea'"
v-model="item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]]"
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
rows="4"
/>
<UPopover :popper="{ placement: 'bottom-start' }" v-else-if="datapoint.inputType === 'date'">
<UButton
icon="i-heroicons-calendar-days-20-solid"
:label="item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]] ? dayjs(item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]]).format('DD.MM.YYYY') : 'Datum auswählen'"
variant="outline"
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
/>
<template #panel="{ close }">
<LazyDatePicker
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
v-model="item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]]" @close="close"
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
/>
</template>
</UPopover>
<UPopover :popper="{ placement: 'bottom-start' }" v-else-if="datapoint.inputType === 'datetime'">
<UButton
icon="i-heroicons-calendar-days-20-solid"
:label="item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]] ? dayjs(item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]]).format('DD.MM.YY HH:mm') : 'Datum auswählen'"
variant="outline"
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
/>
<template #panel="{ close }">
<LazyDatePicker
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
v-model="item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]]" @close="close"
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
mode="datetime"
/>
</template>
</UPopover>
<!-- TODO: DISABLED FOR TIPTAP -->
<Tiptap
v-else-if="datapoint.inputType === 'editor'"
@updateContent="(i) => contentChanged(i,datapoint)"
:preloadedContent="item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]].html"
/>
<UButton
v-if="['text','number','select','date','datetime','textarea'].includes(datapoint.inputType)"
@click="item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]] = null"
variant="outline"
color="white"
icon="i-heroicons-x-mark"
/>
</InputGroup>
<InputGroup class="w-full" v-else>
<UInput
class="flex-auto"
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
v-if="['text','number'].includes(datapoint.inputType)"
v-model="item[datapoint.key]"
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
:type="datapoint.inputType"
:placeholder="datapoint.inputIsNumberRange ? 'Leer lassen für automatisch generierte Nummer' : ''"
>
<template #trailing v-if="datapoint.inputTrailing">
{{datapoint.inputTrailing}}
</template>
</UInput>
<UToggle
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
v-else-if="datapoint.inputType === 'bool'"
v-model="item[datapoint.key]"
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
/>
<USelectMenu
class="flex-auto"
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
v-else-if="datapoint.inputType === 'select'"
v-model="item[datapoint.key]"
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
:option-attribute="datapoint.selectOptionAttribute"
:value-attribute="datapoint.selectValueAttribute || 'id'"
:options="datapoint.selectManualOptions || loadedOptions[datapoint.selectDataType]"
:searchable="datapoint.selectSearchAttributes"
:search-attributes="datapoint.selectSearchAttributes"
:multiple="datapoint.selectMultiple"
searchable-placeholder="Suche..."
>
<template #empty>
Keine Optionen verfügbar
</template>
</USelectMenu>
<UTextarea
class="flex-auto"
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
v-else-if="datapoint.inputType === 'textarea'"
v-model="item[datapoint.key]"
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
rows="4"
/>
<UPopover :popper="{ placement: 'bottom-start' }" v-else-if="datapoint.inputType === 'date'">
<UButton
icon="i-heroicons-calendar-days-20-solid"
:label="item[datapoint.key] ? dayjs(item[datapoint.key]).format('DD.MM.YYYY') : 'Datum auswählen'"
variant="outline"
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
/>
<template #panel="{ close }">
<LazyDatePicker
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
v-model="item[datapoint.key]" @close="close"
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
/>
</template>
</UPopover>
<UPopover :popper="{ placement: 'bottom-start' }" v-else-if="datapoint.inputType === 'datetime'">
<UButton
icon="i-heroicons-calendar-days-20-solid"
:label="item[datapoint.key] ? dayjs(item[datapoint.key]).format('DD.MM.YY HH:mm') : 'Datum auswählen'"
variant="outline"
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
/>
<template #panel="{ close }">
<LazyDatePicker
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
v-model="item[datapoint.key]"
@close="close"
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
mode="datetime"
/>
</template>
</UPopover>
<Tiptap
v-else-if="datapoint.inputType === 'editor'"
@updateContent="(i) => contentChanged(i,datapoint)"
:preloadedContent="item[datapoint.key].html"
/>
<MaterialComposing
v-else-if="datapoint.inputType === 'materialComposing'"
:item="item"
/>
<UButton
v-if="['text','number','select','date','datetime','textarea'].includes(datapoint.inputType)"
@click="item[datapoint.key] = null"
variant="outline"
color="white"
icon="i-heroicons-x-mark"
/>
</InputGroup>
<!-- <div
v-if="profileStore.ownTenant.ownFields"
>
<UDivider
class="mt-3"
>Eigene Felder</UDivider>
<UFormGroup
v-for="field in profileStore.ownTenant.ownFields.contracts"
:key="field.key"
:label="field.label"
>
<UInput
v-if="field.type === 'text'"
v-model="item.ownFields[field.key]"
/>
<USelectMenu
v-else-if="field.type === 'select'"
:options="field.options"
v-model="item.ownFields[field.key]"
/>
</UFormGroup>
</div>-->
</UFormGroup>
</UForm>
</UDashboardPanelContent>
</template>
<style scoped>
td {
border-bottom: 1px solid lightgrey;
vertical-align: top;
padding-bottom: 0.15em;
padding-top: 0.15em;
}
</style>

View File

@@ -19,12 +19,12 @@ defineShortcuts({
document.getElementById("searchinput").focus()
},
'+': () => {
router.push(`/${type}/create`)
router.push(`/standardEntity/${type}/create`)
},
'Enter': {
usingInput: true,
handler: () => {
router.push(`/${type}/show/${filteredRows.value[selectedItem.value].id}`)
router.push(`/standardEntity/${type}/show/${filteredRows.value[selectedItem.value].id}`)
}
},
'arrowdown': () => {
@@ -46,6 +46,7 @@ defineShortcuts({
const router = useRouter()
const dataStore = useDataStore()
const profileStore = useProfileStore()
const dataType = dataStore.dataTypes[type]
@@ -54,15 +55,13 @@ const dataType = dataStore.dataTypes[type]
const selectedItem = ref(0)
const selectedColumns = ref(dataType.templateColumns)
const columns = computed(() => dataType.templateColumns.filter((column) => selectedColumns.value.includes(column)))
const selectedColumns = ref(dataType.templateColumns.filter(i => !i.disabledInTable))
const columns = computed(() => dataType.templateColumns.filter((column) => !column.disabledInTable && selectedColumns.value.includes(column)))
const searchString = ref('')
const selectableFilters = ref(dataType.filters.map(i => i.name))
const selectedFilters = ref(dataType.filters.filter(i => i.default).map(i => i.name) || [])
console.log(selectableFilters)
console.log(selectedFilters)
const filteredRows = computed(() => {
@@ -71,12 +70,26 @@ const filteredRows = computed(() => {
if(selectedFilters.value.length > 0) {
selectedFilters.value.forEach(filterName => {
let filter = dataType.filters.find(i => i.name === filterName)
tempItems = tempItems.filter(filter.filterFunction)
})
}
if(!useRole().generalAvailableRights.value[type].showToAllUsers) {
if(useRole().checkRight(`${type}-viewAll`)){
console.log("Right to Show All")
} else if(useRole().checkRight(type)){
console.log("Only Righty to show Own")
console.log(tempItems)
tempItems = tempItems.filter(item => item.profiles.includes(profileStore.activeProfile.id))
} else {
console.log("No Right to Show")
tempItems = []
}
}
return useSearch(searchString.value, tempItems)
})
@@ -99,7 +112,7 @@ const filteredRows = computed(() => {
</template>
</UInput>
<UButton v-if="useRole().checkRight(`${type}-create`)" @click="router.push(`/${type}/create`)">+ {{dataType.labelSingle}}</UButton>
<UButton v-if="useRole().checkRight(`${type}-create`)" @click="router.push(`/standardEntity/${type}/create`)">+ {{dataType.labelSingle}}</UButton>
</template>
</UDashboardNavbar>
@@ -112,20 +125,25 @@ const filteredRows = computed(() => {
<USelectMenu
v-model="selectedColumns"
icon="i-heroicons-adjustments-horizontal-solid"
:options="dataType.templateColumns"
:options="dataType.templateColumns.filter(i => !i.disabledInTable)"
multiple
class="hidden lg:block"
by="key"
:color="selectedColumns.length !== dataType.templateColumns.filter(i => !i.disabledInTable).length ? 'primary' : 'white'"
:ui-menu="{ width: 'min-w-max' }"
>
<template #label>
Spalten
</template>
</USelectMenu>
<USelectMenu
v-if="selectableFilters.length > 0"
icon="i-heroicons-adjustments-horizontal-solid"
multiple
v-model="selectedFilters"
:options="selectableFilters"
:color="selectedFilters.length > 0 ? 'primary' : 'white'"
:ui-menu="{ width: 'min-w-max' }"
>
<template #label>
Filter
@@ -133,44 +151,52 @@ const filteredRows = computed(() => {
</USelectMenu>
</template>
</UDashboardToolbar>
<UTable
:rows="filteredRows"
:columns="columns"
class="w-full"
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
@select="(i) => router.push(`/projects/show/${i.id}`) "
@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"
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 === filteredRows[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 === filteredRows[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 === filteredRows[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]}}</span>
<span v-else>{{row[column.key] ? `${row[column.key]} ${column.unit ? column.unit : ''}`: ''}}</span>
</template>
<!-- <template #name-data="{row}">
<span class="text-primary-500 font-bold" v-if="row === filteredRows[selectedItem]">{{row.name}}</span>
<span v-else>{{row.name}}</span>
</template>
<template #projecttype-data="{row}">
{{row.projecttype ? row.projecttype.name : ""}}
</template>
<template #phase-data="{row}">
{{getActivePhaseLabel(row)}}
</template>
<template #customer-data="{row}">
{{row.customer ? row.customer.name : ""}}
</template>
<template #plant-data="{row}">
{{row.plant ? row.plant.name : ""}}
</template>
<template #users-data="{row}">
{{row.users.map(i => dataStore.getProfileById(i).fullName).join(", ")}}
</template>-->
</UTable>
</template>

504
components/EntityShow.vue Normal file
View File

@@ -0,0 +1,504 @@
<script setup>
import dayjs from "dayjs";
const props = defineProps({
type: {
required: true,
type: String
},
item: {
required: true,
type: Object
}
})
const {type} = props
defineShortcuts({
'backspace': () => {
router.push(`/standardEntity/${type}`)
},
'arrowleft': () => {
if(openTab.value > 0){
openTab.value -= 1
}
},
'arrowright': () => {
if(openTab.value < dataType.showTabs.length - 1) {
openTab.value += 1
}
},
})
const emit = defineEmits(["updateNeeded"])
const router = useRouter()
const dataStore = useDataStore()
const profileStore = useProfileStore()
const supabase = useSupabaseClient()
const files = useFiles()
const dataType = dataStore.dataTypes[type]
const availableFiles = ref([])
const setup = async () => {
if(props.item.files) {
availableFiles.value = await files.selectSomeDocuments(props.item.files.map(i => i.id)) || []
}
}
setup()
const openTab = ref(0)
const renderedPhases = computed(() => {
console.log(props.item.phases)
if(type === "projects" && props.item.phases) {
return props.item.phases.map((phase,index,array) => {
let isAvailable = false
if(phase.active) {
isAvailable = true
} else if(index > 0 && array[index-1].active ){
isAvailable = true
} else if(index > 1 && array[index-1].optional && array[index-2].active){
isAvailable = true
} else if(array.findIndex(i => i.active) > index) {
isAvailable = true
}
return {
...phase,
label: phase.optional ? `${phase.label}(optional)`: phase.label,
disabled: !isAvailable,
defaultOpen: phase.active ? true : false
}
})
} else {
return []
}
})
const changeActivePhase = async (key) => {
let item = await useSupabaseSelectSingle("projects",props.item.id,'*')
let phaseLabel = ""
item.phases = item.phases.map(p => {
if(p.active) p.active = false
if(p.key === key) {
p.active = true
p.activated_at = dayjs().format()
p.activated_by = profileStore.activeProfile.id
phaseLabel = p.label
}
return p
})
const {error:updateError} = await supabase.from("projects").update({phases: item.phases}).eq("id",item.id)
console.log(updateError)
const {error} = await supabase.from("historyitems").insert({
createdBy: profileStore.activeProfile.id,
tenant: profileStore.currentTenant,
text: `Aktive Phase zu "${phaseLabel}" gewechselt`,
project: item.id
})
emit("updateNeeded")
}
const invoiceDeliveryNotes = () => {
router.push(`/createDocument/edit?type=invoices&linkedDocuments=[${props.item.createddocuments.filter(i => i.type === "deliveryNotes").map(i => i.id)}]`)
}
const getAvailableQueryStringData = () => {
let returnString =""
if(props.item.customer) {
returnString += `&customer=${props.item.customer.id}`
} else if(type === "customers") {
returnString += `&customer=${props.item.id}`
}
if(props.item.project) {
returnString += `&project=${props.item.project.id}`
} else if(type === "projects") {
returnString += `&project=${props.item.id}`
}
return returnString
}
</script>
<template>
<UDashboardNavbar
:ui="{center: 'flex items-stretch gap-1.5 min-w-0'}"
>
<template #left>
<UButton
icon="i-heroicons-chevron-left"
variant="outline"
@click="router.push(`/standardEntity/${type}`)"
>
{{dataType.label}}
</UButton>
</template>
<template #center>
<h1
v-if="item"
:class="['text-xl','font-medium']"
>{{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
@click="router.push(`/standardEntity/${type}/edit/${item.id}`)"
>
Bearbeiten
</UButton>
</template>
</UDashboardNavbar>
<UTabs
:items="dataType.showTabs"
v-if="props.item.id"
class="p-5"
v-model="openTab"
>
<template #item="{item:tab}">
<div class="scroll">
<div v-if="tab.label === 'Informationen'" class="flex flex-row mt-5">
<UCard class="w-1/2 mr-5">
<UAlert
v-if="item.archived"
color="rose"
variant="outline"
:title="`${dataType.labelSingle} archiviert`"
icon="i-heroicons-archive-box"
class="mb-5"
/>
<div class="text-wrap">
<table class="w-full">
<tbody>
<tr
v-for="datapoint in dataType.templateColumns"
>
<td>{{datapoint.label}}:</td>
<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>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</UCard>
<UCard class="w-1/2">
<HistoryDisplay
:type="type.substring(0,type.length-1)"
v-if="props.item.id"
:element-id="props.item.id"
render-headline
/>
</UCard>
</div>
<div v-else-if="tab.label === 'Dateien'">
<UCard class="mt-5">
<Toolbar>
<DocumentUpload
:type="type.substring(0,type.length-1)"
:element-id="item.id"
@uploadFinished="emit('updateNeeded')"
/>
</Toolbar>
<DocumentList
:key="props.item.files.length"
:documents="availableFiles"
v-if="availableFiles.length > 0"
/>
<UAlert
v-else
icon="i-heroicons-x-mark"
title="Keine Dateien verfügbar"
/>
</UCard>
</div>
<div v-else-if="tab.label === 'Projekte'">
<UCard class="mt-5">
<Toolbar>
<UButton
@click="router.push(`/standardEntity/projects/create?${type.substring(0,type.length-1)}=${props.item.id}`)"
>
+ Projekt
</UButton>
</Toolbar>
<UTable
:rows="props.item.projects"
@select="(row) => router.push(`/standardEntity/projects/show/${row.id}`)"
:columns="[{label: 'Name', key: 'name'},{label: 'Phase', key: 'phase'}]"
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine zugehörigen Projekte' }"
>
<template #phase-data="{row}">
{{row.phases ? row.phases.find(i => i.active).label : ""}}
</template>
</UTable>
</UCard>
</div>
<div v-else-if="tab.label === 'Ansprechpartner'">
<UCard class="mt-5">
<Toolbar>
<UButton
@click="router.push(`/standardEntity/contacts/create?${type.substring(0,type.length-1)}=${props.item.id}`)"
>
+ Ansprechpartner
</UButton>
</Toolbar>
<UTable
:rows="props.item.contacts"
@select="(row) => router.push(`/standardEntity/contacts/show/${row.id}`)"
:columns="[{label: 'Name', key: 'fullName'},{label: 'Rolle', key: 'role'}]"
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine zugehörigen Ansprechpartner' }"
>
</UTable>
</UCard>
</div>
<div v-else-if="tab.label === 'Objekte'">
<UCard class="mt-5">
<Toolbar>
<UButton
@click="router.push(`/standardEntity/plants/create?${type.substring(0,type.length-1)}=${props.item.id}`)"
>
+ Objekt
</UButton>
<UButton
v-if="type === 'customers'"
@click="router.push(`/standardEntity/plants/create?${type.substring(0,type.length-1)}=${props.item.id}&name=${encodeURIComponent(`${props.item.infoData.street}, ${props.item.infoData.zip} ${props.item.infoData.city}`)}`)"
>
+ Kundenadresse als Objekt
</UButton>
</Toolbar>
<UTable
:rows="props.item.plants"
@select="(row) => router.push(`/standardEntity/plants/show/${row.id}`)"
:columns="[{label: 'Name', key: 'name'}]"
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine zugehörigen Objekte' }"
>
</UTable>
</UCard>
</div>
<div v-else-if="tab.label === 'Aufgaben'">
<UCard class="mt-5">
<Toolbar>
<UButton
@click="router.push(`/standardEntity/tasks/create?${type.substring(0,type.length-1)}=${props.item.id}`)"
>
+ Aufgabe
</UButton>
</Toolbar>
<UTable
:rows="props.item.tasks"
@select="(row) => router.push(`/standardEntity/tasks/show/${row.id}`)"
:columns="[{label: 'Name', key: 'name'}]"
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine zugehörigen Aufgaben' }"
/>
</UCard>
</div>
<div v-else-if="tab.label === 'Verträge'">
<UCard class="mt-5">
<Toolbar>
<UButton
@click="router.push(`/standardEntity/contracts/create?${type.substring(0,type.length-1)}=${props.item.id}`)"
>
+ Vertrag
</UButton>
</Toolbar>
<UTable
:rows="props.item.contracts"
@select="(row) => router.push(`/standardEntity/contracts/show/${row.id}`)"
:columns="[{label: 'Name', key: 'name'},{label: 'Aktiv', key: 'active'}]"
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine zugehörigen Verträge' }"
></UTable>
</UCard>
</div>
<div v-else-if="tab.label === 'Überprüfungen'">
<UCard class="mt-5">
<UTable
:rows="props.item.checks"
:columns="[{key:'name',label: 'Name'},{key:'rhythm',label: 'Rhythmus'},{key:'description',label: 'Beschreibung'}]"
class="w-full"
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
@select="(i) => router.push(`/checks/show/${i.id}`) "
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Überprüfungen anzuzeigen' }"
>
<template #rhythm-data="{row}">
{{row.distance}}
<span v-if="row.distanceUnit === 'dayjs'">Tage</span>
<span v-if="row.distanceUnit === 'years'">Jahre</span>
</template>
</UTable>
</UCard>
</div>
<div v-else-if="tab.label === 'Phasen'">
<UCard class="mt-5">
<UAccordion
:items="renderedPhases"
>
<template #default="{item,index,open}">
<UButton
variant="ghost"
:color="item.active ? 'primary' : 'white'"
class="mb-1"
:disabled="true"
>
<template #leading>
<div class="w-6 h-6 flex items-center justify-center -my-1">
<UIcon :name="item.icon" class="w-4 h-4 " />
</div>
</template>
<span class="truncate"> {{item.label}}</span>
<template #trailing>
<UIcon
name="i-heroicons-chevron-right-20-solid"
class="w-5 h-5 ms-auto transform transition-transform duration-200"
:class="[open && 'rotate-90']"
/>
</template>
</UButton>
</template>
<template #item="{item, index}">
<UCard class="mx-5">
<template #header>
<span class="dark:text-white text-black">{{item.label}}</span>
</template>
<InputGroup>
<!-- TODO: Reactive Change Phase -->
<UButton
v-if="!item.activated_at && index !== 0 "
@click="changeActivePhase(item.key)"
>
Phase aktivieren
</UButton>
<UButton
v-if="item.active"
v-for="button in item.quickactions"
@click="router.push(`${button.link}&customer=${itemInfo.customer.id}&project=${itemInfo.id}`)"
>
{{button.label}}
</UButton>
</InputGroup>
<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.description" class="dark:text-white text-black">Beschreibung: {{item.description}}</p>
</div>
</UCard>
</template>
</UAccordion>
</UCard>
</div>
<div v-else-if="tab.label === 'Ausgangsbelege'">
<UCard class="mt-5">
<Toolbar>
<UButton
@click="router.push(`/createDocument/edit/?type=quotes${getAvailableQueryStringData()}`)"
>
+ Angebot
</UButton>
<UButton
@click="router.push(`/createDocument/edit/?type=confirmationOrders${getAvailableQueryStringData()}`)"
>
+ Auftragsbestätigung
</UButton>
<UButton
@click="router.push(`/createDocument/edit/?type=deliveryNotes${getAvailableQueryStringData()}`)"
>
+ Lieferschein
</UButton>
<UButton
@click="router.push(`/createDocument/edit/?type=advanceInvoices${getAvailableQueryStringData()}`)"
>
+ Abschlagsrechnung
</UButton>
<UButton
@click="router.push(`/createDocument/edit/?type=invoices${getAvailableQueryStringData()}`)"
>
+ Rechnung
</UButton>
<UButton
@click="invoiceDeliveryNotes"
v-if="type === 'projects'"
>
Lieferscheine abrechnen
</UButton>
</Toolbar>
<UTable
:rows="props.item.createddocuments"
:columns="[{key:'state',label: 'Status'},{key:'type',label: 'Typ'},{key:'documentNumber',label: 'Nummer'},{key:'documentDate',label: 'Datum'}]"
@select="(i) => router.push(i.state === 'Entwurf' ? `/createDocument/edit/${i.id}`:`/createDocument/show/${i.id}`)"
>
<template #documentDate-data="{row}">
{{dayjs(row.documentDate).format("DD.MM.YY HH:mm")}}
</template>
<template #type-data="{row}">
{{dataStore.documentTypesForCreation[row.type].labelSingle}}
</template>
</UTable>
</UCard>
</div>
</div>
</template>
</UTabs>
</template>
<style scoped>
td {
border-bottom: 1px solid lightgrey;
vertical-align: top;
padding-bottom: 0.15em;
padding-top: 0.15em;
}
.scroll {
height: 80vh;
overflow-y: scroll;
padding: 0.3em;
}
</style>

View File

@@ -4,6 +4,7 @@ const { metaSymbol } = useShortcuts()
const shortcuts = ref(false)
const dataStore = useDataStore()
const profileStore = useProfileStore()
const query = ref('')
const supabase = useSupabaseClient()
const toast = useToast()
@@ -101,12 +102,12 @@ const filteredCategories = computed(() => {
const contactRequestData = ref({
source: "helpSlideover",
tenant: dataStore.currentTenant,
tenant: profileStore.currentTenant,
message: "",
title: "",
contactName: dataStore.activeProfile.fullName,
contactTel: dataStore.activeProfile.phoneMobile || dataStore.activeProfile.phoneHome,
contactMail: dataStore.activeProfile.email,
contactName: profileStore.activeProfile.fullName,
contactTel: profileStore.activeProfile.phoneMobile || profileStore.activeProfile.phoneHome,
contactMail: profileStore.activeProfile.email,
contactType: "Hilfe",
currentPath: router.currentRoute
})
@@ -125,12 +126,12 @@ const addContactRequest = async () => {
const resetContactRequest = () => {
contactRequestData.value = {
source: "helpSlideover",
tenant: dataStore.currentTenant,
tenant: profileStore.currentTenant,
message: "",
title: "",
contactName: dataStore.activeProfile.fullName,
contactTel: dataStore.activeProfile.phoneMobile || dataStore.activeProfile.phoneHome,
contactMail: dataStore.activeProfile.email,
contactName: profileStore.activeProfile.fullName,
contactTel: profileStore.activeProfile.phoneMobile || profileStore.activeProfile.phoneHome,
contactMail: profileStore.activeProfile.email,
contactType: "Hilfe"
}
}

View File

@@ -2,11 +2,9 @@
import dayjs from "dayjs"
const props = defineProps({
type: {
required: true,
type: String
},
elementId: {
required: true,
type: String
},
renderHeadline: {
@@ -14,7 +12,7 @@ const props = defineProps({
}
})
const { metaSymbol } = useShortcuts()
const dataStore = useDataStore()
const profileStore = useProfileStore()
const user = useSupabaseUser()
const supabase = useSupabaseClient()
const toast = useToast()
@@ -26,43 +24,12 @@ const items = ref([])
const setup = async () => {
items.value = (await supabase.from("historyitems").select().eq(type,elementId).order("created_at",{ascending: true})).data || []
if(type && elementId){
items.value = (await supabase.from("historyitems").select().eq(type,elementId).order("created_at",{ascending: true})).data || []
} else {
items.value = (await supabase.from("historyitems").select().order("created_at",{ascending: true})).data || []
/*if(type === "customer") {
items.value = (await supabase.from("historyitems").select().eq("customer",elementId)).data || []
} else if(type === "vendor") {
items.value = (await supabase.from("historyitems").select().eq("vendor",elementId)).data || []
} else if(type === "project") {
items.value = (await supabase.from("historyitems").select().eq("project",elementId)).data || []
} else if(type === "plant") {
items.value = (await supabase.from("historyitems").select().eq("plant",elementId)).data || []
} else if(type === "incomingInvoice") {
items.value = (await supabase.from("historyitems").select().eq("incomingInvoice",elementId)).data || []
} else if(type === "document") {
items.value = (await supabase.from("historyitems").select().eq("document",elementId)).data || []
} else if(type === "contact") {
items.value = (await supabase.from("historyitems").select().eq("contact",elementId)).data || []
} else if(type === "contract") {
items.value = (await supabase.from("historyitems").select().eq("contract",elementId)).data || []
} else if(type === "inventoryitem") {
items.value = (await supabase.from("historyitems").select().eq("inventoryitem",elementId)).data || []
} else if(type === "product") {
items.value = (await supabase.from("historyitems").select().eq("product",elementId)).data || []
} else if(type === "profile") {
items.value = (await supabase.from("historyitems").select().eq("profile",elementId)).data || []
} else if(type === "absencerequest") {
items.value = (await supabase.from("historyitems").select().eq("absenceRequest",elementId)).data || []
} else if(type === "event") {
items.value = (await supabase.from("historyitems").select().eq("event",elementId)).data || []
} else if(type === "task") {
items.value = (await supabase.from("historyitems").select().eq("task",elementId)).data || []
} else if(type === "vehicle") {
items.value = (await supabase.from("historyitems").select().eq("vehicle",elementId)).data || []
} else if(type === "space") {
items.value = (await supabase.from("historyitems").select().eq("space",elementId)).data || []
} else if(type === "trackingtrip") {
items.value = (await supabase.from("historyitems").select().eq("trackingtrip",elementId)).data || []
}*/
}
}
setup()
@@ -79,13 +46,13 @@ const addHistoryItemData = ref({
const addHistoryItem = async () => {
console.log(addHistoryItemData.value)
addHistoryItemData.value.createdBy = dataStore.activeProfile.id
addHistoryItemData.value.createdBy = profileStore.activeProfile.id
addHistoryItemData.value[type] = elementId
const {data,error} = await supabase
.from("historyitems")
.insert([{...addHistoryItemData.value, tenant: dataStore.currentTenant}])
.insert([{...addHistoryItemData.value, tenant: profileStore.currentTenant}])
.select()
if(error) {
@@ -100,9 +67,9 @@ const addHistoryItem = async () => {
let rawUsername = i[1]
return {
tenant: dataStore.currentTenant,
tenant: profileStore.currentTenant,
profile: profiles.find(x => x.username === rawUsername).id,
initiatingProfile: dataStore.activeProfile.id,
initiatingProfile: profileStore.activeProfile.id,
title: "Sie wurden im Logbuch erwähnt",
link: `/${type}s/show/${elementId}`,
message: addHistoryItemData.value.text
@@ -162,7 +129,7 @@ const renderText = (text) => {
</UCard>
</UModal>
<Toolbar
v-if="!renderHeadline"
v-if="!renderHeadline && elementId && type"
>
<UButton
@click="showAddHistoryItemModal = true"
@@ -170,7 +137,7 @@ const renderText = (text) => {
+ Eintrag
</UButton>
</Toolbar>
<div v-else>
<div v-else-if="renderHeadline && elementId && type">
<div :class="`flex justify-between`">
<p class=""><span class="text-xl">Logbuch</span> <UBadge variant="outline">{{items.length}}</UBadge></p>
<UButton
@@ -198,11 +165,11 @@ const renderText = (text) => {
:src="colorMode.value === 'light' ? '/Logo.png' : '/Logo_Dark.png' "
/>
<UAvatar
:alt="dataStore.getProfileById(item.createdBy).fullName"
:alt="profileStore.getProfileById(item.createdBy).fullName"
v-else
/>
<div>
<h3 v-if="item.createdBy">{{dataStore.getProfileById(item.createdBy) ? dataStore.getProfileById(item.createdBy).fullName : ""}}</h3>
<h3 v-if="item.createdBy">{{profileStore.getProfileById(item.createdBy) ? profileStore.getProfileById(item.createdBy).fullName : ""}}</h3>
<h3 v-else>FEDEO Bot</h3>
<span v-html="renderText(item.text)"/><br>
<span class="text-gray-500">{{dayjs(item.created_at).format("DD.MM.YY HH:mm")}}</span>

View File

@@ -1,43 +1,57 @@
<script setup>
const dataStore = useDataStore()
import {useRole} from "~/composables/useRole.js";
const profileStore = useProfileStore()
const route = useRoute()
const role = useRole()
const links = computed(() => {
return [
... profileStore.currentTenant === 21 ? [{
label: "Lieferschein Formular",
to: "https://loar.ma-agrarservice.de/erfassung",
icon: "i-heroicons-rectangle-stack",
target: "_blank",
}] : [],
{
id: 'dashboard',
label: "Dashboard",
to: "/",
icon: "i-heroicons-home"
}, {
id: 'historyitems',
label: "Logbuch",
to: "/historyitems",
icon: "i-heroicons-book-open"
},
{
label: "Organisation",
icon: "i-heroicons-rectangle-stack",
defaultOpen: false,
children: [
{
... role.checkRight("tasks") ? [{
label: "Aufgaben",
to: "/tasks",
to: "/standardEntity/tasks",
icon: "i-heroicons-rectangle-stack"
},
... dataStore.ownTenant.features.planningBoard ? [{
}] : [],
... profileStore.ownTenant.features.planningBoard ? [{
label: "Plantafel",
to: "/calendar/timeline",
icon: "i-heroicons-calendar-days"
}] : [],
... dataStore.ownTenant.features.calendar ? [{
... profileStore.ownTenant.features.calendar ? [{
label: "Kalender",
to: "/calendar/grid",
icon: "i-heroicons-calendar-days"
}] : [],
... dataStore.ownTenant.features.calendar ? [{
... profileStore.ownTenant.features.calendar ? [{
label: "Termine",
to: "/events",
to: "/standardEntity/events",
icon: "i-heroicons-calendar-days"
}] : [],
{
label: "Dokumente",
to: "/documents",
label: "Dateien",
to: "/files",
icon: "i-heroicons-document"
},
]
@@ -62,31 +76,26 @@ const links = computed(() => {
}
]
},
/*{
label: "E-Mail",
to: "/email",
icon: "i-heroicons-envelope"
},*/
... dataStore.ownTenant.features.contacts ? [{
... (role.checkRight("customers") || role.checkRight("vendors") || role.checkRight("contacts")) ? [{
label: "Kontakte",
defaultOpen: false,
icon: "i-heroicons-user-group",
children: [
{
... role.checkRight("customers") ? [{
label: "Kunden",
to: "/customers",
to: "/standardEntity/customers",
icon: "i-heroicons-user-group"
},
{
}] : [],
... role.checkRight("vendors") ? [{
label: "Lieferanten",
to: "/vendors",
to: "/standardEntity/vendors",
icon: "i-heroicons-truck"
},
{
}] : [],
... role.checkRight("contacts") ? [{
label: "Ansprechpartner",
to: "/contacts",
to: "/standardEntity/contacts",
icon: "i-heroicons-user-group"
},
}] : [],
]
},] : [],
{
@@ -94,24 +103,29 @@ const links = computed(() => {
defaultOpen:false,
icon: "i-heroicons-user-group",
children: [
... dataStore.ownTenant.features.timeTracking ? [{
... profileStore.ownTenant.features.timeTracking ? [{
label: "Zeiterfassung",
to: "/employees/timetracking",
icon: "i-heroicons-clock"
}] : [],
... dataStore.ownTenant.features.workingTimeTracking ? [{
... profileStore.ownTenant.features.workingTimeTracking ? [{
label: "Anwesenheiten",
to: "/workingtimes",
icon: "i-heroicons-clock"
}] : [],
{
... role.checkRight("absencerequests") ? [{
label: "Abwesenheiten",
to: "/absenceRequests",
to: "/standardEntity/absencerequests",
icon: "i-heroicons-document-text"
}] : [],
{
label: "Fahrten",
to: "/trackingTrips",
icon: "i-heroicons-map"
},
]
},
... dataStore.ownTenant.features.accounting ? [{
... profileStore.ownTenant.features.accounting ? [{
label: "Buchhaltung",
defaultOpen: false,
icon: "i-heroicons-chart-bar-square",
@@ -136,7 +150,7 @@ const links = computed(() => {
},
]
},] : [],
... dataStore.ownTenant.features.inventory ? [{
... role.checkRight("inventory") ? [{
label: "Lager",
icon: "i-heroicons-puzzle-piece",
defaultOpen: false,
@@ -145,37 +159,16 @@ const links = computed(() => {
label: "Vorgänge",
to: "/inventory",
icon: "i-heroicons-square-3-stack-3d"
},{
},/*{
label: "Bestände",
to: "/inventory/stocks",
icon: "i-heroicons-square-3-stack-3d"
},
{
},*/
... role.checkRight("spaces") ? [{
label: "Lagerplätze",
to: "/spaces",
to: "/standardEntity/spaces",
icon: "i-heroicons-square-3-stack-3d"
},
{
label: "Inventar",
to: "/inventoryitems",
icon: "i-heroicons-puzzle-piece"
},
]
},] : [],
... dataStore.ownTenant.features.vehicles ? [{
label: "Fuhrpark",
defaultOpen: false,
icon: "i-heroicons-truck",
children: [
{
label: "Fahrzeuge",
to: "/vehicles",
icon: "i-heroicons-truck"
},{
label: "Fahrten",
to: "/trackingTrips",
icon: "i-heroicons-map"
},
}] : [],
]
},] : [],
{
@@ -183,58 +176,61 @@ const links = computed(() => {
defaultOpen: false,
icon: "i-heroicons-clipboard-document",
children: [
{
... role.checkRight("products") ? [{
label: "Artikel",
to: "/products",
to: "/standardEntity/products",
icon: "i-heroicons-puzzle-piece"
},{
}] : [],
... role.checkRight("productcategories") ? [{
label: "Artikelkategorien",
to: "/productcategories",
to: "/standardEntity/productcategories",
icon: "i-heroicons-puzzle-piece"
},{
}] : [],
... role.checkRight("services") ? [{
label: "Leistungen",
to: "/services",
icon: "i-heroicons-puzzle-piece"
},{
to: "/standardEntity/services",
icon: "i-heroicons-wrench-screwdriver"
}] : [],
... role.checkRight("servicecategories") ? [{
label: "Leistungskategorien",
to: "/servicecategories",
icon: "i-heroicons-puzzle-piece"
to: "/standardEntity/servicecategories",
icon: "i-heroicons-wrench-screwdriver"
}] : [],
{
label: "Mitarbeiter",
to: "/profiles",
icon: "i-heroicons-user-group"
},
... role.checkRight("vehicles") ? [{
label: "Fahrzeuge",
to: "/standardEntity/vehicles",
icon: "i-heroicons-truck"
}] : [],
... role.checkRight("inventoryitems") ? [{
label: "Inventar",
to: "/standardEntity/inventoryitems",
icon: "i-heroicons-puzzle-piece"
}] : [],
]
},
/*... dataStore.ownTenant.features.projects ? [{
label: "Projekte",
defaultOpen: false,
icon: "i-heroicons-clipboard-document-check",
children: [
...dataStore.ownTenant.projecttypes.map(i => {
return {
label: i.label,
to: `/projects?type=${i.label}`,
icon: i.icon
}
})
]
},] : [],*/
{
... role.checkRight("checks") ? [{
label: "Überprüfungen",
to: "/checks",
to: "/standardEntity/checks",
icon: "i-heroicons-magnifying-glass"
},
... dataStore.ownTenant.features.projects ? [{
},] : [],
... role.checkRight("projects") ? [{
label: "Projekte",
to: "/projects",
to: "/standardEntity/projects",
icon: "i-heroicons-clipboard-document-check"
},] : [],
... dataStore.ownTenant.features.contracts ? [{
... role.checkRight("contracts") ? [{
label: "Verträge",
to: "/contracts",
to: "/standardEntity/contracts",
icon: "i-heroicons-clipboard-document"
}] : [],
... dataStore.ownTenant.features.objects ? [{
... role.checkRight("plants") ? [{
label: "Objekte",
to: "/plants",
to: "/standardEntity/plants",
icon: "i-heroicons-clipboard-document"
},] : [],
@@ -244,21 +240,26 @@ const links = computed(() => {
icon: "i-heroicons-cog-8-tooth",
children: [
{
label: "Abrechnung",
to: "https://billing.stripe.com/p/login/cN29Eb32Vdx0gOk288",
icon: "i-heroicons-document-currency-euro",
target: "_blank"
},{
label: "Nummernkreise",
to: "/settings/numberRanges",
icon: "i-heroicons-clipboard-document-list"
},{
label: "Mitarbeiter",
to: "/profiles",
icon: "i-heroicons-clipboard-document-list"
label: "Rollen",
to: "/roles",
icon: "i-heroicons-key"
},{
label: "E-Mail Konten",
to: "/settings/emailAccounts",
icon: "i-heroicons-clipboard-document-list"
icon: "i-heroicons-envelope"
},{
label: "Bankkonten",
to: "/settings/banking",
icon: "i-heroicons-clipboard-document-list"
icon: "i-heroicons-currency-euro"
},{
label: "Text Vorlagen",
to: "/settings/texttemplates",
@@ -270,68 +271,20 @@ const links = computed(() => {
},*/{
label: "Firmeneinstellungen",
to: "/settings/tenant",
icon: "i-heroicons-clipboard-document-list"
icon: "i-heroicons-building-office"
},{
label: "Projekttypen",
to: "/projecttypes",
icon: "i-heroicons-clipboard-document-list"
}/*,{
label: "Integrationen",
to: "/settings/integrations",
icon: "i-heroicons-clipboard-document-list"
}*/,{
label: "Labels",
to: "/settings/labels/",
icon: "i-heroicons-bars-3-solid"
}
]
}
]
})
/*const setOpen = (index,open) => {
console.log(!open)
}*/
</script>
<template>
<!--
<UAccordion :items="links" :ui="{ wrapper: 'flex flex-col w-full' }">
<template #default="{ item, index, open }">
<UButton color="gray" variant="ghost" class="border-b border-gray-200 dark:border-gray-700" :ui="{ rounded: 'rounded-none', padding: { sm: 'p-3' } }">
<template #leading>
&lt;!&ndash; <div class="w-6 h-6 rounded-full bg-primary-500 dark:bg-primary-400 flex items-center justify-center -my-1">
<UIcon :name="item.icon" class="w-4 h-4 text-white dark:text-gray-900" />
</div>&ndash;&gt;
<UIcon :name="item.icon" class="w-4 h-4 dark:text-white text-gray-900" />
</template>
<span class="truncate">{{ item.label }}</span>
<template #trailing>
<UIcon
name="i-heroicons-chevron-right-20-solid"
class="w-5 h-5 ms-auto transform transition-transform duration-200"
:class="[open && 'rotate-90']"
/>
</template>
</UButton>
</template>
<template #item="{item}">
<div v-if="item.children" class="flex flex-col">
<UButton
v-for="child in item.children"
variant="ghost"
color="gray"
>
{{child.label}}
</UButton>
</div>
</template>
</UAccordion>
-->
<div
v-for="item in links"
>
@@ -365,6 +318,7 @@ const links = computed(() => {
v-for="child in item.children"
class="ml-4"
:to="child.to"
:target="child.target"
>
{{child.label}}
</UButton>

View File

@@ -1,30 +1,29 @@
<script setup>
const dataStore = useDataStore()
const profileStore = useProfileStore()
const supabase = useSupabaseClient()
//const tenants = ref(dataStore.getOwnProfile ? dataStore.getOwnProfile.tenants : [])
//const tenant = ref(dataStore.currentTenant)
const selectedProfile = ref(dataStore.activeProfile.id)
const selectedProfile = ref(profileStore.activeProfile.id)
</script>
<template>
<USelectMenu
:options="dataStore.ownProfiles"
:options="profileStore.ownProfiles"
value-attribute="id"
class="w-40"
@change="dataStore.changeProfile(selectedProfile)"
@change="profileStore.changeProfile(selectedProfile)"
v-model="selectedProfile"
>
<UButton color="gray" variant="ghost" :class="[open && 'bg-gray-50 dark:bg-gray-800']" class="w-full">
<UAvatar :alt="dataStore.tenants.find(i => dataStore.getProfileById(selectedProfile).tenant === i.id).name" size="md" />
<UAvatar :alt="profileStore.tenants.find(i => profileStore.profiles.find(i => i.id === selectedProfile).tenant === i.id).name" size="md" />
<span class="truncate text-gray-900 dark:text-white font-semibold">{{dataStore.tenants.find(i => dataStore.getProfileById(selectedProfile).tenant === i.id).name}}</span>
<span class="truncate text-gray-900 dark:text-white font-semibold">{{profileStore.tenants.find(i => profileStore.profiles.find(i => i.id === selectedProfile).tenant === i.id).name}}</span>
</UButton>
<template #option="{option}">
{{dataStore.tenants.find(i => i.id === option.tenant).name}}
{{profileStore.tenants.find(i => i.id === option.tenant).name}}
</template>
</USelectMenu>
</template>

View File

@@ -1,6 +1,6 @@
<script setup>
const dataStore = useDataStore()
const profileStore = useProfileStore()
@@ -9,11 +9,11 @@ const dataStore = useDataStore()
<template>
<div class="w-1/2 mx-auto">
<p class="text-2xl mb-5 text-center">Bitte wähle dein gewünschtes Profil</p>
<div v-for="profile in dataStore.ownProfiles" class="flex justify-between my-3">
{{dataStore.tenants.find(i => i.id === profile.tenant).name}}
<div v-for="profile in profileStore.ownProfiles" class="flex justify-between my-3">
{{profileStore.tenants.find(i => i.id === profile.tenant).name}}
<UButton
variant="outline"
@click="dataStore.changeProfile(profile.id)"
@click="profileStore.changeProfile(profile.id)"
>Auswählen</UButton>
</div>
</div>

View File

@@ -1,9 +1,10 @@
<script setup lang="ts">
<script setup>
const { isHelpSlideoverOpen } = useDashboard()
const { isDashboardSearchModalOpen } = useUIState()
const { metaSymbol } = useShortcuts()
const user = useSupabaseUser()
const dataStore = useDataStore()
const profileStore = useProfileStore()
const supabase = useSupabaseClient()
const router = useRouter()
@@ -15,13 +16,13 @@ const items = computed(() => [
}], [{
label: 'Mein Profil',
icon: 'i-heroicons-user',
to: `/profiles/show/${dataStore.activeProfile.id}`
to: `/profiles/show/${profileStore.activeProfile.id}`
},{
label: 'Abmelden',
icon: 'i-heroicons-arrow-left-on-rectangle',
click: async () => {
await supabase.auth.signOut()
await dataStore.clearStore()
//await dataStore.clearStore()
await router.push('/login')
}
@@ -32,9 +33,9 @@ const items = computed(() => [
<template>
<UDropdown mode="hover" :items="items" :ui="{ width: 'w-full', item: { disabled: 'cursor-text select-text' } }" :popper="{ strategy: 'absolute', placement: 'top' }" class="w-full">
<template #default="{ open }">
<UButton color="gray" variant="ghost" class="w-full" :label="dataStore.activeProfile.fullName" :class="[open && 'bg-gray-50 dark:bg-gray-800']">
<UButton color="gray" variant="ghost" class="w-full" :label="profileStore.activeProfile.fullName" :class="[open && 'bg-gray-50 dark:bg-gray-800']">
<template #leading>
<UAvatar :alt="dataStore.activeProfile ? dataStore.activeProfile.fullName : ''" size="xs" />
<UAvatar :alt="profileStore.activeProfile ? profileStore.activeProfile.fullName : ''" size="xs" />
</template>
<template #trailing>
@@ -49,7 +50,7 @@ const items = computed(() => [
Angemeldet als
</p>
<p class="truncate font-medium text-gray-900 dark:text-white">
{{dataStore.activeProfile.email}}
{{profileStore.activeProfile.email}}
</p>
</div>
</template>

View File

@@ -0,0 +1,14 @@
<script setup>
const props = defineProps({
row: {
type: Object,
required: true,
default: {}
}
})
</script>
<template>
<span v-if="props.row.active" class="text-primary">Ja</span>
<span v-else class="text-rose-600">Nein</span>
</template>

View File

@@ -0,0 +1,17 @@
<script setup>
const props = defineProps({
row: {
type: Object,
required: true,
default: {}
}
})
</script>
<template>
<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>
</template>

View File

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

View File

@@ -0,0 +1,15 @@
<script setup>
import dayjs from "dayjs";
const props = defineProps({
row: {
type: Object,
required: true,
default: {}
}
})
</script>
<template>
<span v-if="props.row.created_at">{{dayjs(props.row.created_at).format("DD.MM.YYYY HH:mm")}}</span>
</template>

View File

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

View File

@@ -0,0 +1,13 @@
<script setup>
const props = defineProps({
row: {
type: Object,
required: true,
default: {}
}
})
</script>
<template>
<div v-if="props.row.description" v-html="props.row.description.html"/>
</template>

View File

@@ -0,0 +1,15 @@
<script setup>
import dayjs from "dayjs";
const props = defineProps({
row: {
type: Object,
required: true,
default: {}
}
})
</script>
<template>
<span v-if="props.row.endDate">{{dayjs(props.row.endDate).format("DD.MM.YYYY")}}</span>
</template>

View File

@@ -0,0 +1,13 @@
<script setup>
const props = defineProps({
row: {
type: Object,
required: true,
default: {}
}
})
</script>
<template>
<span>{{props.row.isCompany ? 'Unternehmen' : 'Privat'}}</span>
</template>

View File

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

View File

@@ -0,0 +1,16 @@
<script setup>
const props = defineProps({
row: {
type: Object,
required: true,
default: {}
}
})
const profileStore = useProfileStore()
</script>
<template>
<span v-if="props.row.profile">{{props.row.profile.id ? profileStore.getProfileById(props.row.profile.id).fullName : profileStore.getProfileById(props.row.profile).fullName}}</span>
</template>

View File

@@ -0,0 +1,27 @@
<script setup>
const props = defineProps({
row: {
type: Object,
required: true,
default: {}
},
inShow: {
type: Boolean,
default: false
}
})
const profileStore = useProfileStore()
const profiles = computed(() => props.row.profiles.map(id => profileStore.getProfileById(id).fullName).join(', '))
</script>
<template>
<div v-if="props.row.profiles">
<div v-if="props.inShow">
<nuxt-link v-for="(profileId, index) in props.row.profiles" :to="`/profiles/show/${profileId}`">{{profileStore.getProfileById(profileId).fullName}}{{index < props.row.profiles.length - 1 ? "," : ""}}</nuxt-link>
</div>
<span v-else>{{props.row.profiles ? profiles : ''}}</span>
</div>
</template>

View File

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

View File

@@ -0,0 +1,13 @@
<script setup>
const props = defineProps({
row: {
type: Object,
required: true,
default: {}
}
})
</script>
<template>
<span>{{props.row.purchasePrice ? useCurrency(props.row.purchasePrice) : ''}}</span>
</template>

View File

@@ -0,0 +1,14 @@
<script setup>
const props = defineProps({
row: {
type: Object,
required: true,
default: {}
}
})
</script>
<template>
<span v-if="props.row.recurring">Ja</span>
<span v-else>Nein</span>
</template>

View File

@@ -0,0 +1,13 @@
<script setup>
const props = defineProps({
row: {
type: Object,
required: true,
default: {}
}
})
</script>
<template>
<span>{{props.row.sellingPrice ? useCurrency(props.row.sellingPrice) : ''}}</span>
</template>

View File

@@ -0,0 +1,13 @@
<script setup>
const props = defineProps({
row: {
type: Object,
required: true,
default: {}
}
})
</script>
<template>
<span v-if="props.row.sellingPriceComposed">{{ useCurrency(props.row.sellingPriceComposed.material)}}</span>
</template>

View File

@@ -0,0 +1,13 @@
<script setup>
const props = defineProps({
row: {
type: Object,
required: true,
default: {}
}
})
</script>
<template>
<span v-if="props.row.sellingPriceComposed">{{ useCurrency(props.row.sellingPriceComposed.total) }}</span>
</template>

View File

@@ -0,0 +1,13 @@
<script setup>
const props = defineProps({
row: {
type: Object,
required: true,
default: {}
}
})
</script>
<template>
<span v-if="props.row.sellingPriceComposed">{{ useCurrency(props.row.sellingPriceComposed.worker) }}</span>
</template>

View File

@@ -0,0 +1,14 @@
<script setup>
const props = defineProps({
row: {
type: Object,
required: true,
default: {}
}
})
</script>
<template>
<span v-if="props.row.hasSEPA">Ja</span>
<span v-else>Nein</span>
</template>

View File

@@ -0,0 +1,21 @@
<script setup>
const props = defineProps({
row: {
type: Object,
required: true,
default: {}
}
})
const servicecategories = ref([])
const setup = async () => {
servicecategories.value = await useSupabaseSelect("servicecategories")
}
setup()
</script>
<template>
<span v-if="props.row.servicecategories && servicecategories.length > 0">{{props.row.servicecategories.map(i => servicecategories.find(x => x.id === i).name).join(", ")}}</span>
</template>

View File

@@ -0,0 +1,15 @@
<script setup>
import dayjs from "dayjs";
const props = defineProps({
row: {
type: Object,
required: true,
default: {}
}
})
</script>
<template>
<span v-if="props.row.startDate">{{dayjs(props.row.startDate).format("DD.MM.YYYY")}}</span>
</template>

View File

@@ -0,0 +1,13 @@
<script setup>
const props = defineProps({
row: {
type: Object,
required: true,
default: {}
}
})
</script>
<template>
<span>{{props.row.unit ? props.row.unit.name : ''}}</span>
</template>

View File

@@ -0,0 +1,13 @@
<script setup>
const props = defineProps({
row: {
type: Object,
required: true,
default: {}
}
})
</script>
<template>
<span>{{props.row.usePlanning ? 'Ja' : 'Nein'}}</span>
</template>

View File

@@ -0,0 +1,22 @@
<script setup>
const props = defineProps({
row: {
type: Object,
required: true,
default: {}
},
inShow: {
type: Boolean,
default: false
}
})
</script>
<template>
<div v-if="props.row.vehicle">
<nuxt-link v-if="props.inShow" :to="`/standardEntity/vehicles/show/${props.row.vehicle.id}`">
{{ props.row.vehicle ? props.row.vehicle.name : '' }}
</nuxt-link>
<span v-else>{{ props.row.vehicle ? props.row.vehicle.name : '' }}</span>
</div>
</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.vendor">
<nuxt-link v-if="props.inShow" :to="`/standardEntity/vendors/show/${props.row.vendor.id}`">{{props.row.vendor ? props.row.vendor.name : ''}}</nuxt-link>
<span v-else>{{props.row.vendor ? props.row.vendor.name : ''}}</span>
</div>
</template>

View File

@@ -6,13 +6,14 @@ dayjs.extend(customParseFormat)
const supabase = useSupabaseClient()
const dataStore = useDataStore()
const profileStore = useProfileStore()
let incomeData = ref({})
let expenseData = ref({})
const setup = async () => {
let incomeRawData = (await supabase.from("createddocuments").select().eq("tenant",dataStore.currentTenant).in('type',['invoices'])).data
let expenseRawData =(await supabase.from("incominginvoices").select().eq("tenant",dataStore.currentTenant)).data
let incomeRawData = (await supabase.from("createddocuments").select().eq("tenant",profileStore.currentTenant).in('type',['invoices'])).data
let expenseRawData =(await supabase.from("incominginvoices").select().eq("tenant",profileStore.currentTenant)).data
expenseRawData = expenseRawData.filter(i => i.date).map(i => {
let amount = 0
@@ -76,6 +77,7 @@ const setup = async () => {
amount = Number(amount.toFixed(2))
console.log(amount)
return {
id: i.id,
date: dayjs(i.documentDate).format("DD-MM-YY"),

View File

@@ -1,15 +1,31 @@
<script setup>
let unpaidCreatedDocumentsSum = ref(0)
let unpaidInvoicesSum = ref(0)
let unpaidInvoicesCount = ref(0)
let draftInvoicesSum = ref(0)
let draftInvoicesCount = ref(0)
let unallocatedStatements = ref(0)
const setupPage = async () => {
let documents = (await useSupabaseSelect("createddocuments","*, statementallocations(*), customer(id,name)")).filter(i => i.type === "invoices" ||i.type === "advanceInvoices")
let documents = (await useSupabaseSelect("createddocuments","*, statementallocations(*), customer(id,name)")).filter(i => i.type === "invoices" ||i.type === "advanceInvoices").filter(i => !i.archived)
documents = documents.filter(i => i.statementallocations.reduce((n,{amount}) => n + amount, 0).toFixed(2) !== getDocumentSum(i).toFixed(2))
let draftDocuments = documents.filter(i => i.state === "Entwurf")
let finalizedDocuments = documents.filter(i => i.state === "Gebucht")
documents.forEach(i => {
unpaidCreatedDocumentsSum.value += getDocumentSum(i) - i.statementallocations.reduce((n,{amount}) => n + amount, 0)
console.log(finalizedDocuments)
finalizedDocuments = finalizedDocuments.filter(i => i.statementallocations.reduce((n,{amount}) => n + amount, 0).toFixed(2) !== getDocumentSum(i).toFixed(2))
finalizedDocuments.forEach(i => {
console.log(getDocumentSum(i))
unpaidInvoicesSum.value += getDocumentSum(i) - i.statementallocations.reduce((n,{amount}) => n + amount, 0)
})
unpaidInvoicesCount.value = finalizedDocuments.length
draftDocuments.forEach(i => {
draftInvoicesSum.value += getDocumentSum(i) - i.statementallocations.reduce((n,{amount}) => n + amount, 0)
})
draftInvoicesCount.value = draftDocuments.length
let bankstatements = await useSupabaseSelect("bankstatements","*, statementallocations(*)","date",true)
unallocatedStatements.value = bankstatements.filter(i => calculateOpenSum(i) !== 0).length
@@ -40,23 +56,25 @@ const calculateOpenSum = (statement) => {
return startingAmount.toFixed(2)
}
const displayCurrency = (value, currency = "€") => {
return `${Number(value).toFixed(2).replace(".",",")} ${currency}`
}
</script>
<template>
<table>
<tr>
<td>Offene Ausgangsbelege:</td>
<td>Offene Rechnungen:</td>
<td
v-if="unpaidCreatedDocumentsSum > 0"
v-if="unpaidInvoicesSum > 0"
class="text-rose-600 font-bold text-nowrap"
>{{displayCurrency(unpaidCreatedDocumentsSum)}}</td>
<td v-else class="text-primary-500 font-bold text-no-wrap">0,00</td>
>{{unpaidInvoicesCount}} Stk / {{useCurrency(unpaidInvoicesSum)}}</td>
<td v-else class="text-primary-500 font-bold text-no-wrap">0 Stk / 0,00</td>
</tr>
<tr>
<td>Angelegte Rechnungsentwürfe:</td>
<td
v-if="draftInvoicesSum > 0"
class="text-rose-600 font-bold text-nowrap"
>{{draftInvoicesCount}} Stk / {{useCurrency(draftInvoicesSum)}}</td>
<td v-else class="text-primary-500 font-bold text-no-wrap">0 Stk / 0,00</td>
</tr>
<tr>
<td>Nicht zugewiesene Bankbuchungen:</td>

View File

@@ -0,0 +1,101 @@
<script setup>
import dayjs from "dayjs";
const profileStore = useProfileStore();
const supabase = useSupabaseClient()
const toast = useToast()
const runningTimeInfo = ref({})
const setupPage = async () => {
runningTimeInfo.value = (await supabase.from("workingtimes").select().eq("profile", profileStore.activeProfile.id).is("endDate", null).single()).data || {}
console.log(runningTimeInfo.value)
}
setupPage()
/*if(dataStore.workingtimes.find(time => time.profile === profileStore.activeProfile.id && !time.endDate)) {
runningTimeInfo.value = dataStore.workingtimes.find(time => time.profile === profileStore.activeProfile.id && !time.end)
}*/
const startTime = async () => {
console.log("started")
runningTimeInfo.value = {
profile: profileStore.activeProfile.id,
startDate: dayjs(),
tenant: profileStore.currentTenant,
state: "Im Web gestartet",
source: "Dashboard"
}
const {data,error} = await supabase
.from("workingtimes")
.insert([runningTimeInfo.value])
.select()
if(error) {
console.log(error)
toast.add({title: "Fehler beim starten der Zeit",color:"rose"})
} else if(data) {
toast.add({title: "Zeit erfolgreich gestartet"})
runningTimeInfo.value = data[0]
console.log(runningTimeInfo.value)
}
}
const stopStartedTime = async () => {
runningTimeInfo.value.endDate = dayjs()
runningTimeInfo.value.state = "Im Web gestoppt"
const {error,status} = await supabase
.from("workingtimes")
.update(runningTimeInfo.value)
.eq('id',runningTimeInfo.value.id)
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"})
} else {
toast.add({title: "Zeit erfolgreich gestoppt"})
runningTimeInfo.value = {}
}
}
</script>
<template>
<div v-if="runningTimeInfo.startDate">
<p>Start: {{dayjs(runningTimeInfo.startDate).format("HH:mm")}}</p>
<p>Dauer: {{dayjs().diff(dayjs(runningTimeInfo.startDate),'minutes') > 59 ? `${Math.floor(dayjs().diff(dayjs(runningTimeInfo.startDate),'minutes') / 60)}:${dayjs().diff(dayjs(runningTimeInfo.startDate),'minutes') % 60} h` : dayjs().diff(dayjs(runningTimeInfo.startDate),'minutes') + ' min' }}</p>
<UFormGroup
class="mt-2"
label="Notizen:"
>
<UTextarea
v-model="runningTimeInfo.notes"
/>
</UFormGroup>
<UButton
class="mt-3"
@click="stopStartedTime"
:disabled="!runningTimeInfo.id"
>
Stop
</UButton>
</div>
<div v-else>
<p>Keine Anwesenheit gestartet</p>
<UButton
class="mt-3"
@click="startTime"
>Starten</UButton>
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,13 @@
<script setup>
const props = defineProps({
item: {
type: Object,
required: true,
default: {}
}
})
</script>
<template>
<span>{{props.item.serialNumber ? 'Menge deaktiviert durch Eingabe der Seriennummer' : 'Für Einzelartikel Menge gleich 0'}}</span>
</template>

View File

@@ -0,0 +1,132 @@
<script setup>
import { v4 as uuidv4 } from 'uuid';
const supabase = useSupabaseClient()
const props = defineProps({
item: {
type: Object,
required: true
}
})
const products = ref([])
const units = ref([])
const setup = async () => {
products.value = await useSupabaseSelect("products")
units.value = (await supabase.from("units").select()).data
}
setup()
const addProductToMaterialComposition = () => {
if(!props.item.materialComposition) props.item.materialComposition = []
props.item.materialComposition.push({
id: uuidv4(),
unit: 1,
quantity: 1,
price: 0,
})
calculateTotalMaterialPrice()
}
const removeProductFromMaterialComposition = (id) => {
props.item.materialComposition = props.item.materialComposition.filter((item) => item.id !== id)
calculateTotalMaterialPrice()
}
const calculateTotalMaterialPrice = () => {
let total = 0
props.item.materialComposition.forEach((item) => {
let rowSum = item.quantity * item.price
total += rowSum
})
props.item.sellingPriceComposed.material = Number(total.toFixed(2))
props.item.sellingPriceComposed.total = Number((props.item.sellingPriceComposed.worker + props.item.sellingPriceComposed.material).toFixed(2))
}
const setRowData = (row) => {
let product = products.value.find(i => i.id === row.product)
row.unit = product.unit
row.price = product.sellingPrice
}
</script>
<template>
<UCard class="w-full">
<UButton
@click="addProductToMaterialComposition"
>
+ Artikel
</UButton>
<table class="w-full mt-3">
<tr
v-for="product in props.item.materialComposition"
>
<td>
<USelectMenu
searchable
:search-attributes="['name']"
:options="products"
value-attribute="id"
option-attribute="name"
v-model="product.product"
:color="product.product ? 'primary' : 'rose'"
@change="setRowData(product)"
>
<template #label>
{{products.find(i => i.id === product.product) ? products.find(i => i.id === product.product).name : 'Kein Artikel ausgewählt'}}
</template>
</USelectMenu>
</td>
<td>
<UInput
type="number"
v-model="product.quantity"
:step="units.find(i => i.id === product.unit) ? units.find(i => i.id === product.unit).step : 1"
@change="calculateTotalMaterialPrice"
/>
</td>
<td>
<USelectMenu
:options="units"
value-attribute="id"
option-attribute="name"
v-model="product.unit"
></USelectMenu>
</td>
<td>
<UInput
type="number"
v-model="product.price"
step="0.01"
@change="calculateTotalMaterialPrice"
/>
</td>
<td>
<UButton
icon="i-heroicons-x-mark"
@click="removeProductFromMaterialComposition(product.id)"
variant="outline"
color="rose"
/>
</td>
</tr>
</table>
</UCard>
</template>
<style scoped>
td {
margin-bottom: 0.5em;
}
</style>

View File

@@ -0,0 +1,24 @@
<script setup>
const supabase = useSupabaseClient()
const profileStore = useProfileStore()
const workingtimes = ref([])
const setupPage = async () => {
workingtimes.value = (await supabase.from("workingtimes").select().eq("tenant",profileStore.currentTenant).is("endDate",null)).data
}
setupPage()
</script>
<template>
<div v-if="workingtimes.length > 0">
<p v-for="time in workingtimes"><UIcon name="i-heroicons-check"/> {{profileStore.getProfileById(time.profile).fullName}}</p>
</div>
<div v-else>
<p>Keine Mitarbeiter anwesend</p>
</div>
</template>
<style scoped>
</style>