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

BIN
RechteDoku.xlsx Normal file

Binary file not shown.

View File

@@ -6,8 +6,6 @@ const tenants = (await supabase.from("tenants").select()).data
const dataStore = useDataStore()
const viewport = useViewport()
console.log("1.")
/*watch(viewport.breakpoint, (newBreakpoint, oldBreakpoint) => {
console.log('Breakpoint updated:', oldBreakpoint, '->', newBreakpoint)
})*/
@@ -52,6 +50,8 @@ useSeoMeta({
</NuxtLayout>
<UNotifications/>
<USlideovers />
<UModals />
<VitePwaManifest/>
@@ -98,7 +98,6 @@ useSeoMeta({
.scrollList {
overflow-y: scroll;
height: 85vh;
//margin-top: 1em;
}

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>

View File

@@ -0,0 +1,3 @@
export const useCurrency = (value,currencyString = " €") => {
return `${Number(value).toFixed(2).replace(".",",")} ${currencyString}`
}

25
composables/useError.js Normal file
View File

@@ -0,0 +1,25 @@
export const useError = (resourceType) => {
const supabase = useSupabaseClient()
const toast = useToast()
const profileStore = useProfileStore()
const logError = async (error) => {
let errorData = {
message: error,
tenant: profileStore.currentTenant,
profile: profileStore.activeProfile.id
}
const {data:supabaseData,error:supabaseError} = await supabase.from("errors").insert(errorData).select().single()
if(supabaseError) {
console.error(supabaseError)
} else if(supabaseData) {
return supabaseData.id
}
}
return { logError}
}

214
composables/useFiles.js Normal file
View File

@@ -0,0 +1,214 @@
import index from "v-calendar";
export const useFiles = () => {
const supabase = useSupabaseClient()
const toast = useToast()
let bucket = "filesdev"
const profileStore = useProfileStore()
const uploadFiles = async (formData, files,tags, upsert) => {
const uploadSingleFile = async (file) => {
//Create File Entry to Get ID for Folder
const {data:createdFileData,error:createdFileError} = await supabase
.from("files")
.insert({
tenant: profileStore.currentTenant,
})
.select()
.single()
if(createdFileError){
console.log(createdFileError)
toast.add({title: "Hochladen fehlgeschlagen", icon: "i-heroicons-x-circle", color: "rose", timeout: 10000})
} else if(createdFileData) {
//Upload File to ID Folder
const {data:uploadData, error: uploadError} = await supabase
.storage
.from(bucket)
.upload(`${profileStore.currentTenant}/filesbyid/${createdFileData.id}/${file.name}`, file, {upsert: upsert})
if(uploadError) {
console.log(uploadError)
console.log(uploadError.statusCode)
if(uploadError.statusCode === '400') {
console.log("is 400")
toast.add({title: "Hochladen fehlgeschlagen", description: "Die Datei enthält ungültige Zeichen", icon: "i-heroicons-x-circle", color: "rose", timeout: 10000})
} else if(uploadError.statusCode === '409') {
console.log("is 409")
toast.add({title: "Hochladen fehlgeschlagen", description: "Es existiert bereits eine Datei mit diesem Namen", icon: "i-heroicons-x-circle", color: "rose", timeout: 10000})
} else {
toast.add({title: "Hochladen fehlgeschlagen", icon: "i-heroicons-x-circle", color: "rose", timeout: 10000})
}
} else if(uploadData) {
//Update File with Corresponding Path
const {data:updateFileData, error:updateFileError} = await supabase
.from("files")
.update({
...formData,
path: uploadData.path,
})
.eq("id", createdFileData.id)
if(updateFileError) {
console.log(updateFileError)
toast.add({title: "Hochladen fehlgeschlagen", icon: "i-heroicons-x-circle", color: "rose", timeout: 10000})
} else {
const {data:tagData, error:tagError} = await supabase
.from("filetagmembers")
.insert(tags.map(tag => {
return {
file_id: createdFileData.id,
tag_id: tag
}
}))
toast.add({title: "Hochladen erfolgreich"})
}
}
}
}
if(files.length === 1) {
await uploadSingleFile(files[0])
} else if( files.length > 1) {
for(let i = 0; i < files.length; i++){
await uploadSingleFile(files[i])
}
}
}
const selectDocuments = async (sortColumn = null, folder = null) => {
let data = []
if(sortColumn !== null ) {
data = (await supabase
.from("files")
.select('*, incominginvoice(*), project(*), vendor(*), customer(*), contract(*), plant(*), createddocument(*), vehicle(*), product(*), profile(*), check(*), inventoryitem(*)')
.eq("tenant", profileStore.currentTenant)
.not("path","is",null)
.not("archived","is",true)
.order(sortColumn, {ascending: true})).data
} else {
data = (await supabase
.from("files")
.select('*, incominginvoice(*), project(*), vendor(*), customer(*), contract(*), plant(*), createddocument(*), vehicle(*), product(*), profile(*), check(*), inventoryitem(*)')
.eq("tenant", profileStore.currentTenant)
.not("path","is",null)
.not("archived","is",true)).data
}
if(data.length > 0){
let paths = []
data.forEach(doc => {
paths.push(doc.path)
})
const {data: supabaseData,error} = await supabase.storage.from(bucket).createSignedUrls(paths,3600)
data = data.map((doc,index) => {
return {
...doc,
url: supabaseData[index].signedUrl
}
})
}
//console.log(data)
return data
}
const selectSomeDocuments = async (documentIds, sortColumn = null, folder = null) => {
let data = null
if(sortColumn !== null ) {
data = (await supabase
.from("files")
.select('*, incominginvoice(*), project(*), vendor(*), customer(*), contract(*), plant(*), createddocument(*), vehicle(*), product(*), profile(*), check(*), inventoryitem(*)')
.in("id",documentIds)
.eq("tenant", profileStore.currentTenant)
.not("path","is",null)
.not("archived","is",true)
.order(sortColumn, {ascending: true})).data
} else {
data = (await supabase
.from("files")
.select('*, incominginvoice(*), project(*), vendor(*), customer(*), contract(*), plant(*), createddocument(*, customer(*)), vehicle(*), product(*), profile(*), check(*), inventoryitem(*)')
.in("id",documentIds)
.not("path","is",null)
.not("archived","is",true)
.eq("tenant", profileStore.currentTenant)).data
}
if(data.length > 0){
let paths = []
data.forEach(doc => {
paths.push(doc.path)
})
const {data: supabaseData,error} = await supabase.storage.from(bucket).createSignedUrls(paths,3600)
data = data.map((doc,index) => {
return {
...doc,
url: supabaseData[index].signedUrl
}
})
}
//console.log(data)
return data
}
const selectDocument = async (id) => {
const {data,error} = await supabase
.from("files")
.select('*')
.eq("id",id)
.single()
const {data: supabaseData,error:supabaseError} = await supabase.storage.from(bucket).createSignedUrl(data.path,3600)
return {
...data,
url: supabaseData.signedUrl
}
/*
if(data.length > 0){
let paths = []
data.forEach(doc => {
paths.push(doc.path)
})
const {data: supabaseData,error} = await supabase.storage.from(bucket).createSignedUrls(paths,3600)
data = data.map((doc,index) => {
return {
...doc,
url: supabaseData[index].signedUrl
}
})
}
//console.log(data)
return data[0]*/
}
return {uploadFiles, selectDocuments, selectSomeDocuments, selectDocument}
}

View File

@@ -0,0 +1,42 @@
import axios from "axios";
import dayjs from "dayjs";
const baseURL = "https://functions.fedeo.io"
export const useFunctions = () => {
const supabase = useSupabaseClient()
const getWorkingTimesEvaluationData = async (profileId, startDate, endDate) => {
const {data:{session:{access_token}}} = await supabase.auth.getSession()
return (await axios({
method: "POST",
url: `${baseURL}/functions/workingtimeevaluation`,
data: {
profile: profileId,
startDate: dayjs(startDate).format("YYYY-MM-DD"),
endDate: dayjs(endDate).format("YYYY-MM-DD"),
},
headers: {
Authorization: `Bearer ${access_token}`
}
})).data
}
const useNextNumber = async (numberRange) => {
const {data:{session:{access_token}}} = await supabase.auth.getSession()
return (await axios({
method: "POST",
url: `${baseURL}/functions/usenextnumber`,
data: {
numberRange: numberRange,
},
headers: {
Authorization: `Bearer ${access_token}`
}
})).data.usedNumber
}
return {getWorkingTimesEvaluationData, useNextNumber}
}

View File

@@ -3,8 +3,9 @@ export const useNumberRange = (resourceType) => {
const supabase = useSupabaseClient()
const dataStore = useDataStore()
const profileStore = useProfileStore()
const numberRanges = dataStore.ownTenant.numberRanges
const numberRanges = profileStore.ownTenant.numberRanges
const numberRange = numberRanges[resourceType]
@@ -19,13 +20,12 @@ export const useNumberRange = (resourceType) => {
const {data,error} = await supabase
.from("tenants")
.update({numberRanges: newNumberRanges})
.eq('id',dataStore.currentTenant)
.eq('id',profileStore.currentTenant)
dataStore.fetchOwnTenant()
await profileStore.fetchOwnTenant()
return (numberRange.prefix ? numberRange.prefix : "") + nextNumber + (numberRange.suffix ? numberRange.suffix : "")
}
return { useNextNumber}

View File

@@ -4,9 +4,10 @@ import Handlebars from "handlebars";
export const usePrintLabel = async (printServerId,printerName , rawZPL ) => {
const supabase = useSupabaseClient()
const dataStore = useDataStore()
const profileStore = useProfileStore()
await supabase.from("printJobs").insert({
tenant: dataStore.currentTenant,
tenant: profileStore.currentTenant,
rawContent: rawZPL,
printerName: printerName,
printServer: printServerId

250
composables/useRole.js Normal file
View File

@@ -0,0 +1,250 @@
export const useRole = () => {
const profileStore = useProfileStore()
console.log(profileStore.currentTenant)
const generalAvailableRights = ref({
projects: {
label: "Projekte",
showToAllUsers: false
},
"projects-viewAll": {
label: "Alle Projekte einsehen",
parent: "projects"
},
"projects-create": {
label: "Projekte erstellen",
parent: "projects"
},
contracts: {
label: "Verträge",
showToAllUsers: false
},
"contracts-viewAll": {
label: "Alle Verträge einsehen",
parent: "contracts"
},
"contracts-create": {
label: "Verträge erstellen",
parent: "contracts"
},
plants: {
label: "Objekte",
showToAllUsers: false
},
"plants-viewAll": {
label: "Alle Objekte einsehen",
parent: "plants"
},
"plants-create": {
label: "Objekte erstellen",
parent: "plants"
},
products: {
label: "Artikel",
showToAllUsers: true
},
"products-create": {
label: "Artikel erstellen",
parent: "products"
},
productcategories: {
label: "Artikelkategorie",
showToAllUsers: true
},
"productcategories-create": {
label: "Artikelkategorie erstellen",
parent: "productcategories"
},
services: {
label: "Leistungen",
showToAllUsers: true
},
"services-create": {
label: "Leistungen erstellen",
parent: "services"
},
servicecategories: {
label: "Leistungskategorien",
showToAllUsers: true
},
"servicecategories-create": {
label: "Leistungskategorien erstellen",
parent: "servicecategories"
},
customers: {
label: "Kunden",
showToAllUsers: false
},
"customers-viewAll": {
label: "Alle Kunden einsehen",
parent: "customers"
},
"customers-create": {
label: "Kunden erstellen",
parent: "customers"
},
contacts: {
label: "Kontakte",
showToAllUsers: false
},
"contacts-viewAll": {
label: "Alle Kontakte einsehen",
parent: "contacts"
},
"contacts-create": {
label: "Kontakte erstellen",
parent: "contacts"
},
vendors: {
label: "Lieferanten",
showToAllUsers: false
},
"vendors-viewAll": {
label: "Alle Lieferanten einsehen",
parent: "vendors"
},
"vendors-create": {
label: "Lieferanten erstellen",
parent: "vendors"
},
checks: {
label: "Überprüfungen",
showToAllUsers: false
},
"checks-viewAll": {
label: "Alle Überprüfungen einsehen",
parent: "checks"
},
"checks-create": {
label: "Überprüfungen erstellen",
parent: "checks"
},
vehicles: {
label: "Fahrzeuge",
showToAllUsers: false
},
"vehicles-viewAll": {
label: "Alle Fahrzeuge einsehen",
parent: "vehicles"
},
"vehicles-create": {
label: "Fahrzeuge erstellen",
parent: "vehicles"
},
inventoryitems: {
label: "Inventarartikel",
showToAllUsers: false
},
"inventoryitems-viewAll": {
label: "Alle Inventarartikel einsehen",
parent: "inventoryitems"
},
"inventoryitems-create": {
label: "Inventarartikel erstellen",
parent: "inventoryitems"
},
absencerequests: {
label: "Abwesenheiten",
showToAllUsers: false
},
"absencerequests-viewAll": {
label: "Alle Abwesenheiten einsehen",
parent: "absencerequests"
},
"absencerequests-create": {
label: "Abwesenheiten erstellen",
parent: "absencerequests"
},
events: {
label: "Termine",
showToAllUsers: false
},
"events-viewAll": {
label: "Alle Termine einsehen",
parent: "events"
},
"events-create": {
label: "Termine erstellen",
parent: "events"
},
spaces: {
label: "Lagerplätze",
showToAllUsers: false
},
"spaces-viewAll": {
label: "Alle Lagerplätze einsehen",
parent: "spaces"
},
"spaces-create": {
label: "Lagerplätze erstellen",
parent: "spaces"
},
roles: {
label: "Rollen",
showToAllUsers: false
},
"roles-viewAll": {
label: "Alle Rollen einsehen",
parent: "roles"
},
"roles-create": {
label: "Rollen erstellen",
parent: "roles"
},
tasks: {
label: "Aufgaben",
showToAllUsers: false
},
"tasks-viewAll": {
label: "Alle Aufgaben einsehen",
parent: "tasks"
},
"tasks-create": {
label: "Aufgaben erstellen",
parent: "tasks"
},
"inventory": {
label: "Lager",
},
})
let role = profileStore.activeProfile.role
const checkRight = (right) => {
let rightsToCheck = [right]
//console.log(right.split("-"))
if(right.split("-").length > 1) {
rightsToCheck.push(right.split("-")[0])
}
//console.log(rightsToCheck)
let hasAllNeccessaryRights = true
//console.log(role.rights)
rightsToCheck.forEach(i => {
if(!role.rights.includes(i)){
hasAllNeccessaryRights = false
}
})
return hasAllNeccessaryRights
}
return {
role,
generalAvailableRights,
checkRight
}
}

View File

@@ -2,7 +2,7 @@
export const useSupabaseSelect = async (relation,select = '*', sortColumn = null, ascending = true) => {
const supabase = useSupabaseClient()
const dataStore = useDataStore()
const profileStore = useProfileStore()
let data = null
@@ -10,67 +10,21 @@ export const useSupabaseSelect = async (relation,select = '*', sortColumn = null
data = (await supabase
.from(relation)
.select(select)
.eq("tenant", dataStore.currentTenant)
.eq("tenant", profileStore.currentTenant)
.order(sortColumn, {ascending: ascending})).data
} else {
data = (await supabase
.from(relation)
.select(select)
.eq("tenant", dataStore.currentTenant)).data
.eq("tenant", profileStore.currentTenant)).data
}
return data
}
export const useSupabaseSelectDocuments = async (select = '*', sortColumn = null, folderPath = "_") => {
const supabase = useSupabaseClient()
const dataStore = useDataStore()
let data = null
if(sortColumn !== null ) {
data = (await supabase
.from("documents")
.select(select)
.eq("tenant", dataStore.currentTenant)
.eq("folderPath", folderPath)
.order(sortColumn, {ascending: true})).data
} else {
data = (await supabase
.from("documents")
.select(select)
.eq("tenant", dataStore.currentTenant)
.eq("folderPath",folderPath)).data
}
if(data.length > 0){
let paths = []
data.forEach(doc => {
paths.push(doc.path)
})
const {data: supabaseData,error} = await supabase.storage.from('files').createSignedUrls(paths,3600)
data = data.map((doc,index) => {
return {
...doc,
url: supabaseData[index].signedUrl
}
})
}
//console.log(data)
return data
}
export const useSupabaseSelectSingle = async (relation,idToEq,select = '*' ) => {
const supabase = useSupabaseClient()
const dataStore = useDataStore()
const profileStore = useProfileStore()
let data = null
@@ -78,14 +32,14 @@ export const useSupabaseSelectSingle = async (relation,idToEq,select = '*' ) =>
data = (await supabase
.from(relation)
.select(select)
.eq("tenant", dataStore.currentTenant)
.eq("tenant", profileStore.currentTenant)
.eq("id",idToEq)
.single()).data
} else {
data = (await supabase
.from(relation)
.select(select)
.eq("tenant", dataStore.currentTenant)
.eq("tenant", profileStore.currentTenant)
.single()).data
}

View File

@@ -1,10 +1,7 @@
export const useZipCheck = async (zip) => {
const supabase = useSupabaseClient()
console.log((await supabase.from("citys").select().eq("zip",Number(zip)).maybeSingle()).data)
const result = (await supabase.from("citys").select().eq("zip",Number(zip)).maybeSingle()).data
return result ? result.short : null

View File

@@ -3,398 +3,20 @@
import MainNav from "~/components/MainNav.vue";
import dayjs from "dayjs";
import {useProfileStore} from "~/stores/profile.js";
const dataStore = useDataStore()
const profileStore = useProfileStore()
const colorMode = useColorMode()
const { isHelpSlideoverOpen } = useDashboard()
const supabase = useSupabaseClient()
const router = useRouter()
const route = useRoute()
console.log("2.")
dataStore.initializeData((await supabase.auth.getUser()).data.user.id)
profileStore.initializeData((await supabase.auth.getUser()).data.user.id)
const month = dayjs().format("MM")
/*const isLight = computed({
get() {
return colorMode.value !== 'dark'
},
set() {
colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
}
})
const navLinks = [
{
label: "Home",
to: "/",
icon: 'i-heroicons-home'
},
{
label: "Kontakte",
icon: 'i-heroicons-user-group',
children: [
{
label: "Kunden",
to: "/customers",
icon: "i-heroicons-user-group"
},
{
label: "Lieferanten",
to: "/vendors",
icon: "i-heroicons-truck"
},
{
label: "Ansprechpartner",
to: "/contacts",
icon: "i-heroicons-user-group"
},
]
},
{
label: "Buchhaltung",
icon: 'i-heroicons-document-chart-bar',
children: [
{
label: "Belege",
to: "/receipts",
icon: "i-heroicons-document-text"
},
{
label: "Bank",
to: "/banking",
icon: "i-heroicons-currency-euro"
},
]
},
{
label: "Aufträge",
icon: "i-heroicons-square-3-stack-3d",
children: [
{
label: "Projekte",
to: "/projects",
icon: "i-heroicons-clipboard-document-check"
},
{
label: "Verträge",
to: "/contracts",
icon: "i-heroicons-clipboard-document"
},
{
label: "Objekte",
to: "/plants",
icon: "i-heroicons-clipboard-document"
},
]
},
{
label: "Verwaltung",
icon: "i-heroicons-square-3-stack-3d",
children: [
{
label: "Aufgaben",
to: "/tasks",
icon: "i-heroicons-rectangle-stack"
},
{
label: "Plantafel",
to: "/calendar/timeline",
icon: "i-heroicons-calendar-days"
},
{
label: "Kalender",
to: "/calendar/grid",
icon: "i-heroicons-calendar-days"
},
{
label: "Dokumente",
to: "/documents",
icon: "i-heroicons-document"
},
{
label: "E-Mail",
to: "/email",
icon: "i-heroicons-envelope"
},
]
},/!*
{
label: "Chat",
to: "/chat",
icon:'i-heroicons-chat-bubble-left-right'
},*!/
{
label: "Mitarbeiter",
icon: 'i-heroicons-user',
children: [
{
label: "Zeiterfassung",
to: "/employees/timetracking",
icon: "i-heroicons-clock"
},
{
label: "Anwesenheiten",
to: "/workingtimes",
icon: "i-heroicons-clock"
},
{
label: "Abwesenheiten",
to: "/absenceRequests",
icon: "i-heroicons-document-text"
}
]
},
{
label: "Lager",
icon: 'i-heroicons-home',
children: [
{
label: "Steuerung",
to: "/inventory",
icon: "i-heroicons-square-3-stack-3d"
},
{
label: "Artikelstamm",
to: "/products",
icon: "i-heroicons-puzzle-piece"
},
{
label: "Lagerplätze",
to: "/spaces",
icon: "i-heroicons-square-3-stack-3d"
},
{
label: "Fahrzeuge",
to: "/vehicles",
icon: "i-heroicons-truck"
}, {
label: "Inventar",
to: "/inventoryitems",
icon: "i-heroicons-puzzle-piece"
},
]
},
]*/
//const activeFeatures = dataStore.tenants.find(dataStore.currentTenant).features
let links = computed(() => {
return [
{
id: 'dashboard',
label: "Dashboard",
to: "/",
icon: "i-heroicons-home"
},
{
label: "Organisation",
icon: "i-heroicons-rectangle-stack",
defaultOpen: false,
children: [
{
label: "Aufgaben",
to: "/tasks",
icon: "i-heroicons-rectangle-stack"
},
... dataStore.ownTenant.features.planningBoard ? [{
label: "Plantafel",
to: "/calendar/timeline",
icon: "i-heroicons-calendar-days"
}] : [],
... dataStore.ownTenant.features.calendar ? [{
label: "Kalender",
to: "/calendar/grid",
icon: "i-heroicons-calendar-days"
}] : [],
... dataStore.ownTenant.features.calendar ? [{
label: "Termine",
to: "/events",
icon: "i-heroicons-calendar-days"
}] : [],
{
label: "Dokumente",
to: "/documents",
icon: "i-heroicons-document"
},
]
},
/*{
label: "E-Mail",
to: "/email",
icon: "i-heroicons-envelope"
},*/
... dataStore.ownTenant.features.contacts ? [{
label: "Kontakte",
defaultOpen: false,
icon: "i-heroicons-user-group",
children: [
{
label: "Kunden",
to: "/customers",
icon: "i-heroicons-user-group"
},
{
label: "Lieferanten",
to: "/vendors",
icon: "i-heroicons-truck"
},
{
label: "Ansprechpartner",
to: "/contacts",
icon: "i-heroicons-user-group"
},
]
},] : [],
{
label: "Mitarbeiter",
defaultOpen:false,
icon: "i-heroicons-user-group",
children: [
... dataStore.ownTenant.features.timeTracking ? [{
label: "Zeiterfassung",
to: "/employees/timetracking",
icon: "i-heroicons-clock"
}] : [],
... dataStore.ownTenant.features.workingTimeTracking ? [{
label: "Anwesenheiten",
to: "/workingtimes",
icon: "i-heroicons-clock"
}] : [],
{
label: "Abwesenheiten",
to: "/absenceRequests",
icon: "i-heroicons-document-text"
},
]
},
... dataStore.ownTenant.features.accounting ? [{
label: "Buchhaltung",
defaultOpen: false,
icon: "i-heroicons-chart-bar-square",
children: [
{
label: "Ausgangsbelege",
to: "/createDocument",
icon: "i-heroicons-document-text"
},{
label: "Eingangsbelege",
to: "/incomingInvoices",
icon: "i-heroicons-document-text"
},
{
label: "Bank",
to: "/banking",
icon: "i-heroicons-document-text"
},
]
},] : [],
... dataStore.ownTenant.features.inventory ? [{
label: "Lager",
icon: "i-heroicons-puzzle-piece",
defaultOpen: false,
children: [
{
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"
},
{
label: "Lagerplätze",
to: "/spaces",
icon: "i-heroicons-square-3-stack-3d"
},
]
},] : [],
{
label: "Inventar",
to: "/inventoryitems",
icon: "i-heroicons-puzzle-piece"
},
... dataStore.ownTenant.features.vehicles ? [{
label: "Fahrzeuge",
to: "/vehicles",
icon: "i-heroicons-truck",
}] : [],
{
label: "Stammdaten",
defaultOpen: false,
icon: "i-heroicons-clipboard-document",
children: [
{
label: "Artikelstamm",
to: "/products",
icon: "i-heroicons-puzzle-piece"
},{
label: "Leistungsstamm",
to: "/services",
icon: "i-heroicons-puzzle-piece"
},
]
},
... dataStore.ownTenant.features.projects ? [{
label: "Projekte",
to: "/projects",
icon: "i-heroicons-clipboard-document-check"
},] : [],
... dataStore.ownTenant.features.contracts ? [{
label: "Verträge",
to: "/contracts",
icon: "i-heroicons-clipboard-document"
}] : [],
... dataStore.ownTenant.features.objects ? [{
label: "Objekte",
to: "/plants",
icon: "i-heroicons-clipboard-document"
},] : [],
{
label: "Einstellungen",
defaultOpen: false,
icon: "i-heroicons-cog-8-tooth",
children: [
{
label: "Nummernkreise",
to: "/settings/numberRanges",
icon: "i-heroicons-clipboard-document-list"
},{
label: "Mitarbeiter",
to: "/profiles",
icon: "i-heroicons-clipboard-document-list"
},{
label: "Bankkonten",
to: "/settings/banking",
icon: "i-heroicons-clipboard-document-list"
},{
label: "Text Vorlagen",
to: "/settings/texttemplates",
icon: "i-heroicons-clipboard-document-list"
},{
label: "Eigene Felder",
to: "/settings/ownfields",
icon: "i-heroicons-clipboard-document-list"
},{
label: "Firmeneinstellungen",
to: "/settings/tenant",
icon: "i-heroicons-clipboard-document-list"
},{
label: "Reparaturprozesse",
to: "/repairs/prozess/",
icon: "i-heroicons-bars-3-solid"
}
]
},
]
})
const actions = [
{
id: 'new-customer',
@@ -489,7 +111,7 @@ const footerLinks = [/*{
</script>
<template>
<UDashboardLayout v-if="dataStore.loaded">
<UDashboardLayout v-if="profileStore.loaded">
<UDashboardPanel :width="250" :resizable="{ min: 200, max: 300 }" collapsible>
<UDashboardNavbar class="!border-transparent" :ui="{ left: 'flex-1' }">
@@ -503,18 +125,8 @@ const footerLinks = [/*{
<UDashboardSearchButton label="Suche..."/>
</template>
<!-- <UDashboardSidebarLinks :links="links">
</UDashboardSidebarLinks>-->
<MainNav/>
<!-- <UAccordion
:items="links"
>
</UAccordion>-->
<div class="flex-1" />
<UDashboardSidebarLinks :links="footerLinks" />

View File

@@ -28,7 +28,7 @@ export default defineNuxtConfig({
transpile: ['@vuepic/vue-datepicker']
},
modules: ['@pinia/nuxt', '@nuxt/ui', '@nuxt/content', '@nuxtjs/supabase', "nuxt-editorjs", '@nuxtjs/fontaine', '@nuxtjs/google-fonts', '@vite-pwa/nuxt', 'nuxt-viewport', 'nuxt-tiptap-editor', '@nuxtjs/leaflet'],
modules: ['@pinia/nuxt', '@nuxt/ui', '@nuxt/content', '@nuxtjs/supabase', "nuxt-editorjs", '@nuxtjs/fontaine', '@vite-pwa/nuxt', 'nuxt-viewport', 'nuxt-tiptap-editor', '@nuxtjs/leaflet'],
routeRules: {
'/printing': {ssr: false}
@@ -54,26 +54,7 @@ export default defineNuxtConfig({
preference: 'system'
},
/*pwa: {
manifest: {
name: "FEDEO",
short_name: "FEDEO",
description: "FEDEO",
icons: [
{
src: "/icon.png",
sizes: "512x512",
}
],
display: "standalone",
theme_color: "green"
},
devOptions: {
enabled: false,
type: "module"
}
},*/
tiptap: {
prefix: "Tiptap"
},

View File

@@ -1,208 +0,0 @@
<script setup>
import dayjs from "dayjs";
import HistoryDisplay from "~/components/HistoryDisplay.vue";
definePageMeta({
middleware: "auth"
})
const dataStore = useDataStore()
const route = useRoute()
const router = useRouter()
const toast = useToast()
const id = ref(route.params.id ? route.params.id : null )
//Working
const mode = ref(route.params.mode || "show")
const itemInfo = ref({
approved: "Offen"
})
const states = ["Offen","Genehmigt", "Abgelehnt"]
const absenceReasons = [
"Elternzeit",
"Kind krank - Kinderbetreuung",
"Krankheit",
"Krankheit 1 Tag (mit Attest)",
"Krankheit ab 2. Tag (mit Attest)",
"Mutterschutz",
"Sonderurlaub (bezahlt)",
"Überstundenausgleich",
"Unbezahlter Urlaub",
"Urlaub"
]
//Functions
const setupPage = () => {
if(mode.value === "show" || mode.value === "edit"){
itemInfo.value = dataStore.getAbsenceRequestById(Number(useRoute().params.id))
}
}
const editItem = async () => {
router.push(`/absenceRequests/edit/${itemInfo.value.id}`)
setupPage()
}
const cancelEditorCreate = () => {
if(itemInfo.value) {
router.push(`/absenceRequests/show/${itemInfo.value.id}`)
} else {
router.push(`/absenceRequests/`)
}
}
setupPage()
</script>
<template>
<UDashboardNavbar :title="itemInfo ? itemInfo.name : (mode === 'create' ? 'Abwesenheit erstellen' : 'Abwesenheit bearbeiten')">
<template #right>
<UButton
v-if="mode === 'edit'"
@click="dataStore.updateItem('absencerequests',itemInfo)"
>
Speichern
</UButton>
<UButton
v-else-if="mode === 'create'"
@click="dataStore.createNewItem('absencerequests',itemInfo)"
>
Erstellen
</UButton>
<UButton
@click="cancelEditorCreate"
color="red"
class="ml-2"
v-if="mode === 'edit' || mode === 'create'"
>
Abbrechen
</UButton>
<UButton
v-if="mode === 'show'"
@click="editItem"
>
Bearbeiten
</UButton>
</template>
<template #badge v-if="itemInfo">
<UBadge
v-if="itemInfo.approved === 'Offen'"
color="blue"
>{{itemInfo.approved}}</UBadge>
<UBadge
v-else-if="itemInfo.approved === 'Genehmigt'"
color="primary"
>{{itemInfo.approved}}</UBadge>
<UBadge
v-else-if="itemInfo.approved === 'Abgelehnt'"
color="rose"
>{{itemInfo.approved}}</UBadge>
</template>
</UDashboardNavbar>
<UTabs
:items="[{label: 'Informationen'}, {label: 'Logbuch'}]"
v-if="itemInfo && mode == 'show'"
class="p-5"
>
<template #item="{item}">
<UCard class="mt-5">
<div v-if="item.label === 'Informationen'">
<div class="truncate">
<p>Mitarbeiter: {{dataStore.profiles.find(item => item.id === itemInfo.user) ? dataStore.profiles.find(item => item.id === itemInfo.user).fullName : ""}}</p>
<p>Start: {{dayjs(itemInfo.start).format("DD.MM.YYYY")}}</p>
<p>Ende: {{dayjs(itemInfo.end).format("DD.MM.YYYY")}}</p>
<p>Grund: {{itemInfo.reason}}</p>
<p>Notizen: {{itemInfo.notes}}</p>
</div>
</div>
<div v-else-if="item.label === 'Logbuch'">
<HistoryDisplay
type="absencerequest"
v-if="itemInfo"
:element-id="itemInfo.id"
/>
</div>
</UCard>
</template>
</UTabs>
<UForm v-else-if="mode == 'edit' || mode == 'create'" class="p-5" >
<UFormGroup
label="Status:"
>
<USelectMenu
v-model="itemInfo.approved"
:options="states"
/>
</UFormGroup>
<UFormGroup
label="Grund:"
>
<USelectMenu
v-model="itemInfo.reason"
:options="absenceReasons"
/>
</UFormGroup>
<UFormGroup
label="Mitarbeiter:"
>
<USelectMenu
v-model="itemInfo.user"
:options="dataStore.profiles"
option-attribute="fullName"
value-attribute="id"
searchable
:search-attributes="['fullName']"
>
<template #label>
{{dataStore.getProfileById(itemInfo.user) ? dataStore.getProfileById(itemInfo.user).fullName : "Mitarbeiter auswählen"}}
</template>
</USelectMenu>
</UFormGroup>
<UFormGroup label="Start:">
<UPopover :popper="{ placement: 'bottom-start' }">
<UButton
icon="i-heroicons-calendar-days-20-solid"
:label="itemInfo.start ? dayjs(itemInfo.start).format('DD.MM.YYYY') : 'Datum auswählen'"
variant="outline"
/>
<template #panel="{ close }">
<LazyDatePicker v-model="itemInfo.start" @close="close" />
</template>
</UPopover>
</UFormGroup>
<UFormGroup label="Ende:">
<UPopover :popper="{ placement: 'bottom-start' }">
<UButton
variant="outline"
icon="i-heroicons-calendar-days-20-solid"
:label="itemInfo.end ? dayjs(itemInfo.end).format('DD.MM.YYYY') : 'Datum auswählen'"
/>
<template #panel="{ close }">
<LazyDatePicker v-model="itemInfo.end" @close="close" />
</template>
</UPopover>
</UFormGroup>
<UFormGroup
label="Notizen:"
>
<UTextarea
v-model="itemInfo.note"
/>
</UFormGroup>
</UForm>
</template>
<style scoped>
</style>

View File

@@ -1,140 +0,0 @@
<template>
<UDashboardNavbar title="Abwesenheiten" :badge="filteredRows ? filteredRows.length : null">
<template #right>
<UInput
id="searchinput"
v-model="searchString"
icon="i-heroicons-funnel"
autocomplete="off"
placeholder="Suche..."
class="hidden lg:block"
@keydown.esc="$event.target.blur()"
>
<template #trailing>
<UKbd value="/" />
</template>
</UInput>
<UButton @click="router.push(`/absenceRequests/create`)">+ Abwesenheit</UButton>
</template>
</UDashboardNavbar>
<UDashboardToolbar>
<template #right>
<USelectMenu
v-model="selectedColumns"
icon="i-heroicons-adjustments-horizontal-solid"
:options="templateColumns"
multiple
class="hidden lg:block"
by="key"
>
<template #label>
Spalten
</template>
</USelectMenu>
</template>
</UDashboardToolbar>
<UTable
:rows="filteredRows"
:columns="columns"
class="w-full"
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
@select="(i) => router.push(`/absenceRequests/show/${i.id}`) "
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Abwesenheiten anzuzeigen' }"
>
<template #approved-data="{row}">
<span v-if="!row.approved">
Genehmigung offen
</span>
<span
v-else-if="row.approved"
class="text-primary"
>
Genemigt
</span>
<span
v-else
class="text-rose"
>
Abgelehnt
</span>
</template>
<template #user-data="{row}">
{{dataStore.profiles.find(profile => profile.id === row.user) ? dataStore.profiles.find(profile => profile.id === row.user).fullName : ""}}
</template>
</UTable>
</template>
<script setup>
definePageMeta({
middleware: "auth"
})
defineShortcuts({
'/': () => {
//console.log(searchinput)
//searchinput.value.focus()
document.getElementById("searchinput").focus()
},
'+': () => {
router.push("/absenceRequests/create")
}
})
const dataStore = useDataStore()
const router = useRouter()
const templateColumns = [
{
key: "approved",
label: "Genehmigt",
sortable: true
},
{
key: "user",
label: "Mitarbeiter",
sortable: true
},{
key: "reason",
label: "Grund",
sortable: true
},{
key: "start",
label: "Start",
sortable: true
},{
key: "end",
label: "Ende",
sortable: true
},{
key: "note",
label: "Notizen",
sortable: true
}
]
const selectedColumns = ref(templateColumns)
const columns = computed(() => templateColumns.filter((column) => selectedColumns.value.includes(column)))
const searchString = ref('')
const filteredRows = computed(() => {
let items = dataStore.absencerequests
if(!searchString.value) {
return items
}
return items.filter(item => {
return Object.values(item).some((value) => {
return String(value).toLowerCase().includes(searchString.value.toLowerCase())
})
})
})
</script>
<style scoped>
</style>

View File

@@ -20,6 +20,7 @@ defineShortcuts({
})
const dataStore = useDataStore()
const profileStore = useProfileStore()
const router = useRouter()
const supabase = useSupabaseClient()
@@ -27,7 +28,7 @@ const bankstatements = ref([])
const setupPage = async () => {
bankstatements.value = (await supabase.from("bankstatements").select("*, statementallocations(*)").eq('tenant', dataStore.currentTenant).order("date", {ascending:false})).data
bankstatements.value = (await supabase.from("bankstatements").select("*, statementallocations(*)").eq('tenant', profileStore.currentTenant).order("date", {ascending:false})).data
}

View File

@@ -14,6 +14,7 @@ defineShortcuts({
})
const dataStore = useDataStore()
const profileStore = useProfileStore()
const route = useRoute()
const router = useRouter()
const mode = ref(route.params.mode || "show")
@@ -108,10 +109,12 @@ const calculateAllocatedSum = computed(() => {
let startingAmount = 0
itemInfo.value.statementallocations.forEach(item => {
console.log(item)
if(item.cd_id) {
startingAmount = startingAmount + item.amount
} else if(item.ii_id) {
startingAmount = Number(startingAmount) - item.amount
startingAmount = Number(startingAmount) + item.amount
}
})
@@ -132,7 +135,7 @@ const saveAllocations = async () => {
const saveAllocation = async (allocation) => {
const {data,error} = await supabase.from("statementallocations").insert({
...allocation,
tenant: dataStore.currentTenant
tenant: profileStore.currentTenant
}).select()
if(data) {
@@ -251,92 +254,95 @@ setup()
</template>
<table class="w-full" v-if="itemInfo.id">
<tr class="flex-row flex justify-between">
<td>
<span class="font-semibold">Buchungsdatum:</span>
</td>
<td>
{{dayjs(itemInfo.date).format("DD.MM.YYYY")}}
</td>
</tr>
<tr class="flex-row flex justify-between">
<td>
<span class="font-semibold">Wertstellungsdatum:</span>
</td>
<td>
{{dayjs(itemInfo.valueDate).format("DD.MM.YYYY")}}
</td>
</tr>
<tr class="flex-row flex justify-between text-right">
<td>
<span class="font-semibold">Partner:</span>
</td>
<td>
{{itemInfo.amount > 0 ? itemInfo.debName : itemInfo.credName}}
<tbody>
<tr class="flex-row flex justify-between">
<td>
<span class="font-semibold">Buchungsdatum:</span>
</td>
<td>
{{dayjs(itemInfo.date).format("DD.MM.YYYY")}}
</td>
</tr>
<tr class="flex-row flex justify-between">
<td>
<span class="font-semibold">Wertstellungsdatum:</span>
</td>
<td>
{{dayjs(itemInfo.valueDate).format("DD.MM.YYYY")}}
</td>
</tr>
<tr class="flex-row flex justify-between text-right">
<td>
<span class="font-semibold">Partner:</span>
</td>
<td>
{{itemInfo.amount > 0 ? itemInfo.debName : itemInfo.credName}}
</td>
</tr>
<tr class="flex-row flex justify-between">
<td>
<span class="font-semibold">Partner IBAN:</span>
</td>
<td>
{{itemInfo.amount > 0 ? separateIBAN(itemInfo.debIban) : separateIBAN(itemInfo.credIban)}}
</td>
</tr>
<tr class="flex-row flex justify-between">
<td>
<span class="font-semibold">Konto:</span>
</td>
<td>
{{dataStore.getBankAccountById(itemInfo.account) ? dataStore.getBankAccountById(itemInfo.account).name || separateIBAN(dataStore.getBankAccountById(itemInfo.account).iban) : ""}}
</td>
</tr>
<tr class="flex-row flex justify-between">
<td colspan="2">
<span class="font-semibold">Verknüpfte Dokumente:</span>
</td>
</tr>
<tr
class="flex-row flex justify-between mb-3"
v-for="item in itemInfo.statementallocations"
>
</td>
</tr>
<tr class="flex-row flex justify-between">
<td>
<span class="font-semibold">Partner IBAN:</span>
</td>
<td>
{{itemInfo.amount > 0 ? separateIBAN(itemInfo.debIban) : separateIBAN(itemInfo.credIban)}}
</td>
</tr>
<tr class="flex-row flex justify-between">
<td>
<span class="font-semibold">Konto:</span>
</td>
<td>
{{dataStore.getBankAccountById(itemInfo.account) ? dataStore.getBankAccountById(itemInfo.account).name || separateIBAN(dataStore.getBankAccountById(itemInfo.account).iban) : ""}}
</td>
</tr>
<tr class="flex-row flex justify-between">
<td colspan="2">
<span class="font-semibold">Verknüpfte Dokumente:</span>
</td>
</tr>
<tr
class="flex-row flex justify-between mb-3"
v-for="item in itemInfo.statementallocations"
>
<td>
<!-- <span v-if="itemInfo.createdDocument"><nuxt-link :to="`/createDocument/show/${itemInfo.createdDocument}`">{{dataStore.createddocuments.find(i => i.id === itemInfo.createdDocument).documentNumber}}</nuxt-link></span>
<span v-else-if="itemInfo.incomingInvoice"><nuxt-link :to="`/incominInvoices/show/${itemInfo.incomingInvoice}`">{{dataStore.getIncomingInvoiceById(itemInfo.incomingInvoice).reference}}</nuxt-link></span>-->
<span v-if="item.cd_id">
{{dataStore.getCreatedDocumentById(item.cd_id).documentNumber}}
</span>
<span v-else-if="item.ii_id">
{{dataStore.getVendorById(dataStore.getIncomingInvoiceById(item.ii_id).vendor).name}} - {{dataStore.getIncomingInvoiceById(item.ii_id).reference}}
</span>
</td>
<td>
<UButton
variant="outline"
icon="i-heroicons-arrow-right-end-on-rectangle"
@click="router.push(`/createDocument/show/${item.cd_id}`)"
v-if="item.cd_id"
/>
<UButton
variant="outline"
icon="i-heroicons-arrow-right-end-on-rectangle"
@click="router.push(`/incominginvoices/show/${item.ii_id}`)"
v-else-if="item.ii_id"
/>
</td>
</tr>
<tr class="flex-row flex justify-between">
<td colspan="2">
<span class="font-semibold">Beschreibung:</span>
</td>
</tr>
<tr>
<td colspan="2">
{{itemInfo.text}}
</td>
</tr>
</tbody>
<td>
<!-- <span v-if="itemInfo.createdDocument"><nuxt-link :to="`/createDocument/show/${itemInfo.createdDocument}`">{{dataStore.createddocuments.find(i => i.id === itemInfo.createdDocument).documentNumber}}</nuxt-link></span>
<span v-else-if="itemInfo.incomingInvoice"><nuxt-link :to="`/incominInvoices/show/${itemInfo.incomingInvoice}`">{{dataStore.getIncomingInvoiceById(itemInfo.incomingInvoice).reference}}</nuxt-link></span>-->
<span v-if="item.cd_id">
{{dataStore.getCreatedDocumentById(item.cd_id).documentNumber}}
</span>
<span v-else-if="item.ii_id">
{{dataStore.getVendorById(dataStore.getIncomingInvoiceById(item.ii_id).vendor).name}} - {{dataStore.getIncomingInvoiceById(item.ii_id).reference}}
</span>
</td>
<td>
<UButton
variant="outline"
icon="i-heroicons-arrow-right-end-on-rectangle"
@click="router.push(`/createDocument/show/${item.cd_id}`)"
v-if="item.cd_id"
/>
<UButton
variant="outline"
icon="i-heroicons-arrow-right-end-on-rectangle"
@click="router.push(`/incominginvoices/show/${item.ii_id}`)"
v-else-if="item.ii_id"
/>
</td>
</tr>
<tr class="flex-row flex justify-between">
<td colspan="2">
<span class="font-semibold">Beschreibung:</span>
</td>
</tr>
<tr>
<td colspan="2">
{{itemInfo.text}}
</td>
</tr>
</table>
</UCard>

View File

@@ -6,6 +6,7 @@ import timeGridPlugin from "@fullcalendar/timegrid"
import resourceTimelinePlugin from "@fullcalendar/resource-timeline";
import interactionPlugin from "@fullcalendar/interaction";
import dayjs from "dayjs";
import profiles from "~/components/columnRenderings/profiles.vue";
definePageMeta({
middleware: "auth"
@@ -17,9 +18,10 @@ const router = useRouter()
const mode = ref(route.params.mode || "grid")
const supabase = useSupabaseClient()
const dataStore = useDataStore()
const resources = dataStore.getResources
const eventTypes = dataStore.getEventTypes
//const resources = dataStore.getResources
//const eventTypes = dataStore.getEventTypes
const profileStore = useProfileStore()
//Working
const newEventData = ref({
@@ -36,66 +38,40 @@ const showEventModal = ref(false)
const selectedEvent = ref({})
const selectedResources = ref([])
const events = ref([])
const calendarOptionsGrid = computed(() => {
return {
locale: deLocale,
plugins: [dayGridPlugin, interactionPlugin, timeGridPlugin],
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: 'dayGridMonth,timeGridWeek,timeGridDay'
},
initialView: "timeGridWeek",
initialEvents: events.value,
nowIndicator: true,
height: "80vh",
selectable: true,
weekNumbers: true,
select: function (info) {
console.log(info)
//Functions
const convertResourceIds = () => {
newEventData.value.resources = selectedResources.value.map(i => {
/*if(i.type !== 'Mitarbeiter') {
return {id: Number(i.id.split('-')[1]), type: i.type}
} else {
return {id: i.id, type: i.type}
}*/
if((String(i).match(/-/g) || []).length > 1) {
return {type: "Mitarbeiter", id: i}
} else {
if(i.split('-')[0] === 'I') {
return {id: Number(i.split('-')[1]), type: "Inventar"}
} else if(i.split('-')[0] === 'F') {
return {id: Number(i.split('-')[1]), type: "Fahrzeug"}
router.push(`/standardEntity/events/create/?startDate=${encodeURIComponent(info.startStr)}&endDate=${encodeURIComponent(info.endStr)}`)
},
eventClick: function (info) {
console.log(info.event)
if(info.event.extendedProps.entrytype === "absencerequest"){
router.push(`/standardEntity/absencerequests/show/${info.event.extendedProps.absencerequestId}`)
} else {
router.push(`/standardEntity/events/show/${info.event.extendedProps.eventId}`)
}
}
})
}
//Calendar Config
const calendarOptionsGrid = reactive({
locale: deLocale,
plugins: [dayGridPlugin, interactionPlugin, timeGridPlugin],
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: 'dayGridMonth,timeGridWeek,timeGridDay'
},
initialView: "dayGridMonth",
initialEvents: dataStore.getEvents,
nowIndicator: true,
height: "80vh",
selectable: true,
weekNumbers: true,
select: function(info) {
console.log(info)
router.push(`/events/edit/?start=${info.startStr}&end=${info.endStr}&source=grid`)
},
eventClick: function (info){
console.log(info)
if(info.event.title.startsWith("Abw.:")){
router.push(`/absencerequests/show/${info.event.id}`)
} else {
router.push(`/events/show/${info.event.id}`)
}
},
},
}
})
const calendarOptionsTimeline = reactive({
const calendarOptionsTimeline = ref({
schedulerLicenseKey: "CC-Attribution-NonCommercial-NoDerivatives",
locale: deLocale,
plugins: [resourceTimelinePlugin, interactionPlugin],
@@ -106,21 +82,26 @@ const calendarOptionsTimeline = reactive({
center: 'title',
right: 'resourceTimelineDay,resourceTimeline3Hours,resourceTimelineMonth'
},
initialEvents: dataStore.getEventsByResource,
initialEvents: [{
title:"Test",
resourceId:"F-27",
start:"2025-01-01"
}],
selectable: true,
select: function (info) {
router.push(`/events/edit/?start=${info.startStr}&end=${info.endStr}&resources=${JSON.stringify([info.resource.id])}&source=timeline`)
router.push(`/events/edit/?startDate=${info.startStr}&endDate=${info.endStr}&resources=${JSON.stringify([info.resource.id])}&source=timeline`)
},
eventClick: function (info){
if(info.event.title.startsWith("Abw.:")){
router.push(`/absencerequests/show/${info.event.id}`)
console.log(info.event)
if(info.event.extendedProps.entrytype === "absencerequest"){
router.push(`/standardEntity/absencerequests/show/${info.event.extendedProps.absencerequestId}`)
} else {
router.push(`/events/show/${info.event.id}`)
router.push(`/standardEntity/events/show/${info.event.extendedProps.eventId}`)
}
},
resourceGroupField: "type",
resourceOrder: "-type",
resources: resources,
resources: [],
nowIndicator:true,
views: {
resourceTimeline3Hours: {
@@ -146,25 +127,280 @@ const calendarOptionsTimeline = reactive({
height: '80vh',
})
const loaded = ref(false)
const setupPage = async () => {
let tempData = await useSupabaseSelect("events", "*, vehicles(*), inventoryitems(*)")
let absencerequests = await useSupabaseSelect("absencerequests", "*")
let projects = await useSupabaseSelect("projects", "*")
let inventoryitems = await useSupabaseSelect("inventoryitems", "*")
let profiles = await useSupabaseSelect("profiles", "*")
let vehicles = await useSupabaseSelect("vehicles", "*")
/*events.value = [
...tempData.map(event => {
let eventColor = profileStore.ownTenant.calendarConfig.eventTypes.find(type => type.label === event.eventtype).color
let title = ""
if (event.name) {
title = event.name
} else if (event.project) {
projects.find(i => i.id === event.project) ? projects.find(i => i.id === event.project).name : ""
}
return {
...event,
start: event.startDate,
end: event.endDate,
title: title,
borderColor: eventColor,
textColor: eventColor,
backgroundColor: "black"
}
}),
...absencerequests.map(absence => {
return {
id: absence.id,
resourceId: absence.user,
resourceType: "person",
title: `Abw.: ${absence.reason}`,
start: dayjs(absence.start).toDate(),
end: dayjs(absence.end).add(1, 'day').toDate(),
allDay: true,
}
})
]*/
calendarOptionsGrid.value.initialEvents = [
...tempData.map(event => {
let eventColor = profileStore.ownTenant.calendarConfig.eventTypes.find(type => type.label === event.eventtype).color
let title = ""
if (event.name) {
title = event.name
} else if (event.project) {
projects.find(i => i.id === event.project) ? projects.find(i => i.id === event.project).name : ""
}
return {
...event,
start: event.startDate,
end: event.endDate,
title: title,
borderColor: eventColor,
textColor: eventColor,
backgroundColor: "black",
entrytype: "event",
eventId: event.id
}
}),
...absencerequests.map(absence => {
return {
id: absence.id,
resourceId: absence.user,
resourceType: "person",
title: `${absence.reason} - ${absence.name}`,
start: dayjs(absence.start).toDate(),
end: dayjs(absence.end).add(1, 'day').toDate(),
allDay: true,
absencerequestId: absence.id,
entrytype: "absencerequest",
}
})
]
calendarOptionsTimeline.value.resources = [
...profiles.filter(i => i.tenant === profileStore.currentTenant).map(profile => {
return {
type: 'Mitarbeiter',
title: profile.fullName,
id: profile.id
}
}),
...vehicles.map(vehicle => {
return {
type: 'Fahrzeug',
title: vehicle.licensePlate,
id: `F-${vehicle.id}`
}
}),
...inventoryitems.filter(i=> i.usePlanning).map(item => {
return {
type: 'Inventar',
title: item.name,
id: `I-${item.id}`
}
})
]
console.log(calendarOptionsTimeline.value.resources)
/*
calendarOptionsTimeline.value.initialEvents = [
...events.value.map(event => {
let eventColor = profileStore.ownTenant.calendarConfig.eventTypes.find(type => type.label === event.eventtype).color
let title = ""
if (event.name) {
title = event.name
} else if (event.project) {
projects.find(i => i.id === event.project) ? projects.find(i => i.id === event.project).name : ""
}
let resource = ""
let resourceType = ""
return {
...event,
start: event.startDate,
end: event.endDate,
title: title,
borderColor: eventColor,
textColor: eventColor,
backgroundColor: "black"
}
})
]
*/
let tempEvents = []
tempData.forEach(event => {
console.log(event)
let eventColor = profileStore.ownTenant.calendarConfig.eventTypes.find(type => type.label === event.eventtype).color
let title = ""
if (event.name) {
title = event.name
} else if (event.project) {
projects.find(i => i.id === event.project) ? projects.find(i => i.id === event.project).name : ""
}
let returnData = {
title: title,
borderColor: eventColor,
textColor: eventColor,
backgroundColor: "black",
start: event.startDate,
end: event.endDate,
resourceIds: [],
entrytype: "event",
eventId: event.id
}
if(event.profiles.length > 0) {
console.log("Profiles found")
event.profiles.forEach(profile => {
returnData.resourceIds.push(profile)
})
}
if(event.vehicles.length > 0) {
console.log("Vehicles found")
event.vehicles.forEach(vehicle => {
returnData.resourceIds.push(`F-${vehicle.id}`)
})
}
if(event.inventoryitems.length > 0) {
console.log("Inventoryitems found")
event.inventoryitems.forEach(inventoryitem => {
returnData.resourceIds.push(`I-${inventoryitem.id}`)
})
}
tempEvents.push(returnData)
})
absencerequests.forEach(absencerequest => {
let returnData = {
title: `${absencerequest.reason} - ${absencerequest.name}`,
backgroundColor: "black",
start: absencerequest.startDate,
end: absencerequest.endDate,
resourceIds: [absencerequest.profile],
entrytype: "absencerequest",
allDay: true,
absencerequestId: absencerequest.id
}
tempEvents.push(returnData)
})
calendarOptionsTimeline.value.initialEvents = tempEvents
console.log(calendarOptionsTimeline.value)
loaded.value = true
}
setupPage()
//Functions
/*
const convertResourceIds = () => {
newEventData.value.resources = selectedResources.value.map(i => {
/!*if(i.type !== 'Mitarbeiter') {
return {id: Number(i.id.split('-')[1]), type: i.type}
} else {
return {id: i.id, type: i.type}
}*!/
if((String(i).match(/-/g) || []).length > 1) {
return {type: "Mitarbeiter", id: i}
} else {
if(i.split('-')[0] === 'I') {
return {id: Number(i.split('-')[1]), type: "Inventar"}
} else if(i.split('-')[0] === 'F') {
return {id: Number(i.split('-')[1]), type: "Fahrzeug"}
}
}
})
}
*/
//Calendar Config
</script>
<template>
<UDashboardNavbar :title="currentItem ? currentItem.name : ''">
<template #right>
<UDashboardNavbar title="Kalender">
<template #center>
<h1
:class="['text-xl','font-medium']"
>Kalender</h1>
</template>
<template #right>
</template>
</UDashboardNavbar>
<div v-if="mode === 'grid'" class="p-5">
<FullCalendar
:options="calendarOptionsGrid"
/>
</div>
<div v-else-if="mode === 'timeline'" class="p-5">
</template>
</UDashboardNavbar>
<UDashboardPanelContent>
<div v-if="mode === 'grid' && loaded" class="p-5">
<FullCalendar
:options="calendarOptionsGrid"
/>
</div>
<div v-else-if="mode === 'timeline' && loaded" class="p-5">
<FullCalendar
:options="calendarOptionsTimeline"
/>
</div>
</UDashboardPanelContent>
</template>
<style scoped>

View File

@@ -11,6 +11,7 @@ const supabase = useSupabaseClient()
const user = useSupabaseUser()
const dataStore = useDataStore()
const profileStore = useProfileStore()
const selectedChat = ref({})
const messageText = ref("")
@@ -24,8 +25,8 @@ const messageText = ref("")
@click="selectedChat = chat"
>
<UAlert
:title="chat.title ? chat.title : chat.members.map(i => { if(i !== user.id) return dataStore.getProfileById(i).fullName}).join(' ')"
:avatar="{alt: chat.members.map(i => { if(i !== user.id) return dataStore.getProfileById(i).fullName}).join(' ')}"
:title="chat.title ? chat.title : chat.members.map(i => { if(i !== user.id) return profileStore.getProfileById(i).fullName}).join(' ')"
:avatar="{alt: chat.members.map(i => { if(i !== user.id) return profileStore.getProfileById(i).fullName}).join(' ')}"
:color="selectedChat.id === chat.id ? 'primary' : 'white'"
variant="outline"
>
@@ -49,13 +50,13 @@ const messageText = ref("")
{{message.text}}
</div>
<UAvatar
:alt="dataStore.getProfileById(message.origin) ? dataStore.getProfileById(message.origin).fullName : ''"
:alt="profileStore.getProfileById(message.origin) ? profileStore.getProfileById(message.origin).fullName : ''"
size="md"
/>
</div>
<div class="flex justify-start mb-4" v-else>
<UAvatar
:alt="dataStore.getProfileById(message.origin) ? dataStore.getProfileById(message.origin).fullName : ''"
:alt="profileStore.getProfileById(message.origin) ? profileStore.getProfileById(message.origin).fullName : ''"
size="md"
/>
<div

View File

@@ -1,6 +1,6 @@
<script setup>
const dataStore = useDataStore()
const profileStore = useProfileStore()
const supabase = useSupabaseClient()
const profiles = ref([])
@@ -10,14 +10,14 @@ const selectedProfiles = ref([])
const setup = async () => {
profiles.value = await useSupabaseSelect("profiles")
selectedProfiles.value = [dataStore.activeProfile.id]
selectedProfiles.value = [profileStore.activeProfile.id]
}
setup()
const createChat = async () => {
const {data,error} = await supabase.from("chats").insert({
tenant: dataStore.currentTenant,
tenant: profileStore.currentTenant,
name: itemInfo.value.name
}).select()
@@ -105,7 +105,7 @@ const createChat = async () => {
:search-attributes="['fullName']"
>
<template #label>
{{selectedProfiles.length > 0 ? selectedProfiles.map(i => dataStore.getProfileById(i).fullName).join(", ") : "Keine Benutzer ausgewählt"}}
{{selectedProfiles.length > 0 ? selectedProfiles.map(i => profileStore.getProfileById(i).fullName).join(", ") : "Keine Benutzer ausgewählt"}}
</template>
</USelectMenu>
</UFormGroup>

View File

@@ -13,17 +13,17 @@ defineShortcuts({
})
const itemInfo = ref({})
const dataStore = useDataStore()
const profileStore = useProfileStore()
const supabase = useSupabaseClient()
const setup = async () => {
itemInfo.value = await useSupabaseSelectSingle("chats",useRoute().params.id,"*, profiles(*), chatmessages(*)")
let unseenMessages = itemInfo.value.chatmessages.filter(i => !i.seenBy.includes(dataStore.activeProfile.id))
let unseenMessages = itemInfo.value.chatmessages.filter(i => !i.seenBy.includes(profileStore.activeProfile.id))
for await (const message of unseenMessages){
await supabase.from("chatmessages").update({seenBy: [...message.seenBy, dataStore.activeProfile.id]}).eq("id",message.id)
await supabase.from("chatmessages").update({seenBy: [...message.seenBy, profileStore.activeProfile.id]}).eq("id",message.id)
}
@@ -34,11 +34,11 @@ const messageText = ref("")
const sendMessage = async () => {
if(messageText.value.length > 0) {
const message = {
origin: dataStore.activeProfile.id,
origin: profileStore.activeProfile.id,
destinationchat: itemInfo.value.id,
text: messageText.value,
tenant: dataStore.currentTenant,
seenBy: [dataStore.activeProfile.id]
tenant: profileStore.currentTenant,
seenBy: [profileStore.activeProfile.id]
}
const {data,error} = await supabase.from("chatmessages").insert(message)
@@ -49,11 +49,11 @@ const sendMessage = async () => {
//Reset
messageText.value = ""
//Create Notifications
let notifications = itemInfo.value.profiles.filter(i => i.id !== dataStore.activeProfile.id).map(i => {
let notifications = itemInfo.value.profiles.filter(i => i.id !== profileStore.activeProfile.id).map(i => {
return {
tenant: dataStore.currentTenant,
tenant: profileStore.currentTenant,
profile: i.id,
initiatingProfile: dataStore.activeProfile.id,
initiatingProfile: profileStore.activeProfile.id,
title: `Sie haben eine neue Nachricht im Chat ${itemInfo.value.name}`,
link: `/chats/show/${itemInfo.value.id}`,
message: message.text
@@ -88,13 +88,13 @@ const sendMessage = async () => {
</UDashboardNavbar>
<div class="scrollList p-5">
<UAlert
:color="message.seenBy.includes(dataStore.activeProfile.id) ? 'white' : 'primary'"
:variant="message.seenBy.includes(dataStore.activeProfile.id) ? 'solid' : 'outline'"
:color="message.seenBy.includes(profileStore.activeProfile.id) ? 'white' : 'primary'"
:variant="message.seenBy.includes(profileStore.activeProfile.id) ? 'solid' : 'outline'"
class="my-2"
v-for="message in itemInfo.chatmessages"
:description="message.text"
:avatar="{ alt: dataStore.getProfileById(message.origin).fullName }"
:title="`${dataStore.getProfileById(message.origin).fullName} - ${isToday(new Date(message.created_at)) ? dayjs(message.created_at).format('HH:mm') : dayjs(message.created_at).format('DD.MM.YYYY HH:mm')}`"
:avatar="{ alt: profileStore.getProfileById(message.origin).fullName }"
:title="`${profileStore.getProfileById(message.origin).fullName} - ${isToday(new Date(message.created_at)) ? dayjs(message.created_at).format('HH:mm') : dayjs(message.created_at).format('DD.MM.YYYY HH:mm')}`"
/>
</div>

View File

@@ -1,337 +0,0 @@
<script setup>
import HistoryDisplay from "~/components/HistoryDisplay.vue";
import dayjs from "dayjs";
import {useSupabaseSelectSingle} from "~/composables/useSupabase.js";
definePageMeta({
middleware: "auth"
})
defineShortcuts({
'backspace': () => {
router.push("/checks")
},
'arrowleft': () => {
if(openTab.value > 0){
openTab.value -= 1
}
},
'arrowright': () => {
if(openTab.value < 3) {
openTab.value += 1
}
},
})
const dataStore = useDataStore()
const route = useRoute()
const router = useRouter()
const toast = useToast()
const id = ref(route.params.id ? route.params.id : null )
const openTab = ref(0)
const vehicles = ref([])
const profiles = ref([])
const inventoryitems = ref([])
//Working
const mode = ref(route.params.mode || "show")
const itemInfo = ref({
name: "",
vehicle: null,
inventoryitem: null,
profile: null,
distance: 1,
distanceUnit: "days"
})
//Functions
const setupPage = async () => {
profiles.value = await useSupabaseSelect("profiles")
vehicles.value = await useSupabaseSelect("vehicles")
inventoryitems.value = await useSupabaseSelect("inventoryitems")
if(mode.value === "show" ){
itemInfo.value = await useSupabaseSelectSingle("checks", route.params.id, "*, checkexecutions(*, executed_by(fullName)), vehicle(*), profile(*), inventoryitem(*)")
} else if (mode.value === "edit"){
itemInfo.value = await useSupabaseSelectSingle("checks", route.params.id, "*")
}
if(mode.value === "create") {
let query = route.query
}
}
const cancelEditorCreate = () => {
if(itemInfo.value) {
router.push(`/checks/show/${itemInfo.value.id}`)
} else {
router.push(`/checks/`)
}
}
setupPage()
</script>
<template>
<UDashboardNavbar>
<template #left>
<UButton
icon="i-heroicons-chevron-left"
variant="outline"
@click="router.push(`/checks`)"
>
Überprüfungen
</UButton>
</template>
<template #center>
<h1
v-if="itemInfo"
:class="['text-xl','font-medium']"
>{{itemInfo ? `Überprüfung: ${itemInfo.name}` : (mode === 'create' ? 'Überprüfung erstellen' : 'Überprüfung bearbeiten')}}</h1>
</template>
<template #right>
<UButton
v-if="mode === 'edit'"
@click="dataStore.updateItem('checks',itemInfo)"
>
Speichern
</UButton>
<UButton
v-else-if="mode === 'create'"
@click="dataStore.createNewItem('checks',itemInfo)"
>
Erstellen
</UButton>
<UButton
@click="cancelEditorCreate"
color="red"
class="ml-2"
v-if="mode === 'edit' || mode === 'create'"
>
Abbrechen
</UButton>
<UButton
v-if="mode === 'show'"
@click="router.push(`/checks/edit/${itemInfo.id}`)"
>
Bearbeiten
</UButton>
</template>
</UDashboardNavbar>
<UDashboardPanelContent>
<UTabs
v-if="itemInfo.id && mode === 'show'"
:items="[{label: 'Informationen'}, {label: 'Dokumente'}, {label: 'Ausführungen'}]"
class="p-5"
v-model="openTab"
>
<template #item="{item}">
<div v-if="item.label === 'Informationen'" class="flex mt-5">
<div class="w-1/2 mr-5">
<UCard>
<div class="text-wrap">
<table class="mb-3 w-full">
<tr>
<td>Name:</td>
<td>{{itemInfo.name}}</td>
</tr>
<tr>
<td>Rhythmus:</td>
<td>
{{itemInfo.distance}}
<span v-if="itemInfo.distanceUnit === 'dayjs'">Tage</span>
<span v-if="itemInfo.distanceUnit === 'years'">Jahre</span>
</td>
</tr>
<tr v-if="itemInfo.vehicle">
<td>Fahrzeug:</td>
<td>{{itemInfo.vehicle.licensePlate}}</td>
</tr>
<tr v-if="itemInfo.profile">
<td>Person:</td>
<td>{{itemInfo.profile.fullName}}</td>
</tr>
<tr v-if="itemInfo.inventoryitem">
<td>Inventarartikel:</td>
<td>{{itemInfo.inventoryitem.name}}</td>
</tr>
<tr>
<td>Beschreibung:</td>
<td>{{itemInfo.description}}</td>
</tr>
</table>
</div>
</UCard>
</div>
<div class="w-1/2">
<UCard class="h-full">
<HistoryDisplay
type="check"
v-if="itemInfo"
:element-id="itemInfo.id"
:render-headline="true"
/>
</UCard>
</div>
</div>
<div v-else-if="item.label === 'Dokumente'">
<UCard class="mt-5">
<Toolbar>
<DocumentUpload
type="check"
:element-id="itemInfo.id"
/>
</Toolbar>
<DocumentList
:documents="dataStore.getDocumentsByContractId(itemInfo.id)"
/>
</UCard>
</div>
<div v-else-if="item.label === 'Ausführungen'">
<UCard class="mt-5">
<UTable
:rows="itemInfo.checkexecutions"
:columns="[{key:'created_at',label:'Ersellt am:'},{key:'executed_at',label:'Ausgeführt am:'},{key:'executed_by',label:'Ausgeführt von:'},{key:'description',label:'Beschreibung:'}]"
class="w-full"
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Ausführungen anzuzeigen' }"
>
<template #created_at-data="{row}">
{{dayjs(row.created_at).format("DD.MM.YYYY HH:mm")}}
</template>
<template #executed_at-data="{row}">
{{dayjs(row.executed_at).format("DD.MM.YYYY HH:mm")}}
</template>
<template #executed_by-data="{row}">
{{row.executed_by.fullName}}
</template>
</UTable>
</UCard>
</div>
</template>
</UTabs>
<UForm v-else-if="mode === 'edit' || mode === 'create'" class="p-5" >
<div class="mx-auto w-4/5">
<div class="flex flex-row justify-around">
<div class="w-1/2">
<UDivider>Allgemeines</UDivider>
<UFormGroup
label="Name:"
>
<UInput
v-model="itemInfo.name"
/>
</UFormGroup>
<UFormGroup
label="Typ:"
>
<UInput
v-model="itemInfo.type"
/>
</UFormGroup>
<UFormGroup
label="Rhythmus"
class="w-full"
>
<InputGroup class="w-full">
<UInput
v-model="itemInfo.distance"
type="number"
class="flex-auto"
></UInput>
<USelectMenu
:options="[{key: 'days', label: 'Tage'},{key:'years',label:'Jahre'}]"
option-attribute="label"
value-attribute="key"
v-model="itemInfo.distanceUnit"
class="w-40"
></USelectMenu>
</InputGroup>
</UFormGroup>
</div>
<div class=" ml-5 w-1/2">
<UDivider>Verknüpftes Element</UDivider>
<UFormGroup
label="Fahrzeug:"
>
<USelectMenu
:options="vehicles"
option-attribute="licensePlate"
value-attribute="id"
v-model="itemInfo.vehicle"
:disabled="itemInfo.inventoryitem || itemInfo.profile"
>
</USelectMenu>
</UFormGroup>
<UFormGroup
label="Person:"
>
<USelectMenu
:options="profiles"
option-attribute="fullName"
value-attribute="id"
v-model="itemInfo.profile"
:disabled="itemInfo.vehicle || itemInfo.inventoryitem"
>
</USelectMenu>
</UFormGroup>
<UFormGroup
label="Inventarartikel:"
>
<USelectMenu
:options="inventoryitems"
option-attribute="name"
value-attribute="id"
v-model="itemInfo.inventoryitem"
:disabled="itemInfo.vehicle || itemInfo.profile"
>
</USelectMenu>
</UFormGroup>
</div>
</div>
<UFormGroup
label="Beschreibung:"
>
<UTextarea
v-model="itemInfo.description"
rows="6"
maxrows="12"
/>
</UFormGroup>
</div>
</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

@@ -1,148 +0,0 @@
<template>
<UDashboardNavbar title="Überprüfungen" :badge="filteredRows.length">
<template #right>
<UInput
id="searchinput"
v-model="searchString"
icon="i-heroicons-funnel"
autocomplete="off"
placeholder="Suche..."
class="hidden lg:block"
@keydown.esc="$event.target.blur()"
>
<template #trailing>
<UKbd value="/" />
</template>
</UInput>
<UButton @click="router.push(`/checks/create`)">+ Überprüfung</UButton>
</template>
</UDashboardNavbar>
<UDashboardToolbar>
<template #right>
<USelectMenu
v-model="selectedColumns"
icon="i-heroicons-adjustments-horizontal-solid"
:options="templateColumns"
multiple
class="hidden lg:block"
by="key"
>
<template #label>
Spalten
</template>
</USelectMenu>
</template>
</UDashboardToolbar>
<UTable
:rows="filteredRows"
:columns="columns"
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 #name-data="{row}">
<span v-if="row === filteredRows[selectedItem]" class="text-primary-500 font-bold">{{row.name}}</span>
<span v-else>{{row.name}}</span>
</template>
<template #vehicle-data="{row}">
<span v-if="row.vehicle">{{row.vehicle.licensePlate}}</span>
</template>
<template #profile-data="{row}">
<span v-if="row.profile">{{row.profile.fullName}}</span>
</template>
<template #inventoryitem-data="{row}">
<span v-if="row.inventoryitem">{{row.inventoryitem.name}}</span>
</template>
</UTable>
</template>
<script setup>
definePageMeta({
middleware: "auth"
})
defineShortcuts({
'/': () => {
//console.log(searchinput)
//searchinput.value.focus()
document.getElementById("searchinput").focus()
},
'+': () => {
router.push("/checks/create")
},
'Enter': {
usingInput: true,
handler: () => {
router.push(`/checks/show/${filteredRows.value[selectedItem.value].id}`)
}
},
'arrowdown': () => {
if(selectedItem.value < filteredRows.value.length - 1) {
selectedItem.value += 1
} else {
selectedItem.value = 0
}
},
'arrowup': () => {
if(selectedItem.value === 0) {
selectedItem.value = filteredRows.value.length - 1
} else {
selectedItem.value -= 1
}
}
})
const dataStore = useDataStore()
const router = useRouter()
const items = ref([])
const selectedItem = ref(0)
const setupPage = async () => {
items.value = await useSupabaseSelect("checks","*, vehicle(licensePlate), profile(fullName), inventoryitem(name)")
}
setupPage()
const templateColumns = [
{
key: "name",
label: "Name",
sortable: true
},
{
key: "vehicle",
label: "Fahrzeug"
},
{
key: "profile",
label: "Person"
},
{
key: "inventoryitem",
label: "Inventarartikel"
},
{
key: "notes",
label: "Notizen"
}
]
const selectedColumns = ref(templateColumns)
const columns = computed(() => templateColumns.filter((column) => selectedColumns.value.includes(column)))
const searchString = ref('')
const filteredRows = computed(() => {
return useSearch(searchString.value, items.value)
})
</script>
<style scoped>
</style>

View File

@@ -3,6 +3,7 @@ import dayjs from "dayjs"
const supabase = useSupabaseClient()
const dataStore = useDataStore()
const profileStore = useProfileStore()
const router = useRouter()
@@ -10,7 +11,7 @@ const items = ref([])
const setup = async () => {
items.value = (await supabase.from("historyitems").select().like('text',`%@${dataStore.activeProfile.username}%`)/*.textSearch("text", `'@${dataStore.activeProfile.username}'`)*/.order("created_at")).data
items.value = (await supabase.from("historyitems").select().like('text',`%@${profileStore.activeProfile.username}%`)/*.textSearch("text", `'@${profileStore.activeProfile.username}'`)*/.order("created_at")).data
}
const navigateToHistoryItem = (item) => {

View File

@@ -23,6 +23,7 @@ defineShortcuts({
})
const dataStore = useDataStore()
const profileStore = useProfileStore()
const route = useRoute()
const router = useRouter()
const toast = useToast()
@@ -33,7 +34,8 @@ const openTab = ref(0)
//Working
const mode = ref(route.params.mode || "show")
const itemInfo = ref({
active: true
active: true,
profiles: [profileStore.activeProfile.id]
})
const oldItemInfo = ref({})
@@ -54,21 +56,11 @@ const setupPage = async () => {
if(itemInfo.value) oldItemInfo.value = JSON.parse(JSON.stringify(itemInfo.value))
}
const cancelEditorCreate = () => {
if(itemInfo.value) {
router.push(`/contacts/show/${itemInfo.value.id}`)
} else {
router.push(`/contacts`)
}
}
setupPage()
</script>
<template>
<UDashboardNavbar
:title="itemInfo ? itemInfo.fullName : (mode === 'create' ? 'Ansprechpartner erstellen' : 'Ansprechpartner bearbeiten')"
:ui="{center: 'flex items-stretch gap-1.5 min-w-0'}"
>
<template #left>
@@ -84,7 +76,7 @@ setupPage()
<h1
v-if="itemInfo"
:class="['text-xl','font-medium', ... itemInfo.active ? ['text-primary'] : ['text-rose-500']]"
>{{itemInfo ? `Ansprechpartner: ${itemInfo.fullName}` : (mode === 'create' ? 'Ansprechpartner erstellen' : 'Ansprechpartner bearbeiten')}}</h1>
>{{itemInfo.id ? `Ansprechpartner: ${itemInfo.fullName}` : (mode === 'create' ? 'Ansprechpartner erstellen' : 'Ansprechpartner bearbeiten')}}</h1>
</template>
<template #right>
<UButton
@@ -100,7 +92,7 @@ setupPage()
Erstellen
</UButton>
<UButton
@click="cancelEditorCreate"
@click="router.push(itemInfo.id ? `/contacts/show/${itemInfo.value.id}` : `/contacts/`)"
color="red"
class="ml-2"
v-if="mode === 'edit' || mode === 'create'"
@@ -310,6 +302,23 @@ setupPage()
</template>
</UPopover>
</UFormGroup>
<UFormGroup
label="Berechtige Benutzer:"
>
<USelectMenu
v-model="itemInfo.profiles"
:options="profileStore.profiles"
option-attribute="fullName"
value-attribute="id"
searchable
multiple
:search-attributes="['fullName']"
>
<template #label>
{{itemInfo.profiles.length > 0 ? itemInfo.profiles.map(i => profileStore.getProfileById(i).fullName).join(", ") : "Kein Benutzer ausgewählt"}}
</template>
</USelectMenu>
</UFormGroup>
<UFormGroup
label="Notizen:"
>
@@ -317,6 +326,7 @@ setupPage()
v-model="itemInfo.notes"
/>
</UFormGroup>
</UForm>
</template>

View File

@@ -1,182 +1,24 @@
<template>
<UDashboardNavbar title="Ansprechpartner" :badge="filteredRows.length">
<template #right>
<UInput
id="searchinput"
v-model="searchString"
icon="i-heroicons-funnel"
autocomplete="off"
placeholder="Suche..."
class="hidden lg:block"
@keydown.esc="$event.target.blur()"
>
<template #trailing>
<UKbd value="/" />
</template>
</UInput>
<UButton @click="router.push(`/contacts/create`)">+ Kontakt</UButton>
</template>
</UDashboardNavbar>
<UDashboardToolbar>
<template #right>
<USelectMenu
v-model="selectedColumns"
icon="i-heroicons-adjustments-horizontal-solid"
:options="templateColumns"
multiple
class="hidden lg:block"
by="key"
>
<template #label>
Spalten
</template>
</USelectMenu>
</template>
</UDashboardToolbar>
<UTable
:rows="filteredRows"
:columns="columns"
class="w-full"
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
@select="(i) => router.push(`/contacts/show/${i.id}`)"
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Ansprechpartner anzuzeigen' }"
>
<template #fullName-data="{row}">
<span v-if="row === filteredRows[selectedItem]" class="text-primary-500 font-bold">{{row.fullName}}</span>
<span v-else>{{row.fullName}}</span>
</template>
<template #active-data="{row}">
<span v-if="row.active" class="text-primary-500">Aktiv</span>
<span v-else class="text-rose">Gesperrt</span>
</template>
<template #customer-data="{row}">
{{row.customer ? row.customer.name : ""}}
</template>
<template #vendor-data="{row}">
{{row.vendor ? row.vendor.name : ""}}
</template>
</UTable>
<EntityList
type="contacts"
:items="items"
></EntityList>
</template>
<script setup>
import {useSearch} from "~/composables/useSearch.js";
import EntityList from "~/components/EntityList.vue";
definePageMeta({
middleware: "auth"
})
defineShortcuts({
'/': () => {
//console.log(searchinput)
//searchinput.value.focus()
document.getElementById("searchinput").focus()
},
'+': () => {
router.push("/contacts/create")
},
'Enter': {
usingInput: true,
handler: () => {
router.push(`/contacts/show/${filteredRows.value[selectedItem.value].id}`)
}
},
'arrowdown': () => {
if(selectedItem.value < filteredRows.value.length - 1) {
selectedItem.value += 1
} else {
selectedItem.value = 0
}
},
'arrowup': () => {
if(selectedItem.value === 0) {
selectedItem.value = filteredRows.value.length - 1
} else {
selectedItem.value -= 1
}
}
})
const dataStore = useDataStore()
const router = useRouter()
const items = ref([])
const selectedItem = ref(0)
const setupPage = async () => {
items.value = await useSupabaseSelect("contacts","*, customer(name), vendor(name)")
}
setupPage()
const templateColumns = [
{
key: "fullName",
label: "Name",
sortable: true
},
{
key: "customer",
label: "Kunde",
sortable: true
},
{
key: "vendor",
label: "Lieferant",
sortable: true
},
{
key: "role",
label: "Rolle",
},
{
key: "email",
label: "E-Mail",
},
{
key: "phoneMobile",
label: "Mobil",
},
{
key: "phoneHome",
label: "Festnetz",
},
{
key: "active",
label: "Aktiv",
},
{
key: "birtday",
label: "Geburtstag",
}
]
const selectedColumns = ref(templateColumns)
const columns = computed(() => templateColumns.filter((column) => selectedColumns.value.includes(column)))
const searchString = ref('')
/*const filteredRows = computed(() => {
if(!searchString.value) {
return dataStore.contacts
}
return dataStore.contacts.filter(item => {
return Object.values(item).some((value) => {
return String(value).toLowerCase().includes(searchString.value.toLowerCase())
})
})
})*/
const filteredRows = computed(() => {
return useSearch(searchString.value, items.value)
})
</script>
<style scoped>

View File

@@ -1,534 +0,0 @@
<script setup>
import HistoryDisplay from "~/components/HistoryDisplay.vue";
import dayjs from "dayjs";
import {useSupabaseSelectSingle} from "~/composables/useSupabase.js";
definePageMeta({
middleware: "auth"
})
defineShortcuts({
'backspace': () => {
router.push("/contracts")
},
'arrowleft': () => {
if(openTab.value > 0){
openTab.value -= 1
}
},
'arrowright': () => {
if(openTab.value < 3) {
openTab.value += 1
}
},
})
const dataStore = useDataStore()
const route = useRoute()
const router = useRouter()
const toast = useToast()
const id = ref(route.params.id ? route.params.id : null )
const openTab = ref(0)
//Working
const mode = ref(route.params.mode || "show")
const itemInfo = ref({
name: "",
customer: null,
active: true,
ownFields: {}
})
//Functions
const setupPage = async () => {
if(mode.value === "show" ){
itemInfo.value = await useSupabaseSelectSingle("contracts", route.params.id, "*, customer(id,name,isCompany, customerNumber), contact(id,fullName)")
} else if (mode.value === "edit"){
itemInfo.value = await useSupabaseSelectSingle("contracts", route.params.id, "*")
}
if(mode.value === "create") {
let query = route.query
if(query.customer) itemInfo.value.customer = Number(query.customer)
}
}
const cancelEditorCreate = () => {
if(itemInfo.value) {
router.push(`/contracts/show/${itemInfo.value.id}`)
} else {
router.push(`/contracts/`)
}
}
setupPage()
</script>
<template>
<UDashboardNavbar :title="itemInfo ? itemInfo.name : (mode === 'create' ? 'Vertrag erstellen' : 'Vertrag bearbeiten')">
<template #left>
<UButton
icon="i-heroicons-chevron-left"
variant="outline"
@click="router.push(`/contracts`)"
>
Verträge
</UButton>
</template>
<template #center>
<h1
v-if="itemInfo"
:class="['text-xl','font-medium', ... itemInfo.active ? ['text-primary'] : ['text-rose-500']]"
>{{itemInfo ? `Vertrag: ${itemInfo.name}` : (mode === 'create' ? 'Vertrag erstellen' : 'Vertrag bearbeiten')}}</h1>
</template>
<template #right>
<UButton
v-if="mode === 'edit'"
@click="dataStore.updateItem('contracts',itemInfo)"
>
Speichern
</UButton>
<UButton
v-else-if="mode === 'create'"
@click="dataStore.createNewItem('contracts',itemInfo)"
>
Erstellen
</UButton>
<UButton
@click="cancelEditorCreate"
color="red"
class="ml-2"
v-if="mode === 'edit' || mode === 'create'"
>
Abbrechen
</UButton>
<UButton
v-if="mode === 'show'"
@click="router.push(`/contracts/edit/${itemInfo.id}`)"
>
Bearbeiten
</UButton>
</template>
</UDashboardNavbar>
<UDashboardPanelContent>
<UTabs
v-if="itemInfo.id && mode === 'show'"
:items="[{label: 'Informationen'}, {label: 'Dokumente'}]"
class="p-5"
v-model="openTab"
>
<template #item="{item}">
<div v-if="item.label === 'Informationen'" class="flex mt-5">
<div class="w-1/2 mr-5">
<UCard>
<div class="text-wrap">
<table class="mb-3">
<tr>
<td>Name:</td>
<td>{{itemInfo.name}}</td>
</tr>
<tr>
<td>Aktiv:</td>
<td>
<span v-if="itemInfo.active" class="text-primary-500">Ja</span>
<span v-else class="text-rose-600">Nein</span>
</td>
</tr>
<tr>
<td>Kunde:</td>
<td>
<nuxt-link
:to="`/customers/show/${itemInfo.customer.id}`"
v-if="itemInfo.customer"
>{{itemInfo.customer.customerNumber ? `${itemInfo.customer.customerNumber} - ` : ''}}{{itemInfo.customer ? itemInfo.customer.name : ""}}</nuxt-link>
</td>
</tr>
<tr v-if="itemInfo.customer.isCompany">
<td>Ansprechpartner:</td>
<td>
<nuxt-link
v-if="itemInfo.contact"
:to="`/contacts/show/${itemInfo.contact.id}`"
>{{itemInfo.contact ? itemInfo.contact.fullName : ""}}</nuxt-link>
</td>
</tr>
<tr>
<td>Wiederkehrend:</td>
<td>{{itemInfo.recurring ? "Ja" : "Nein"}}</td>
</tr>
<tr>
<td>Startdatum:</td>
<td>{{dayjs(itemInfo.startDate).format("DD.MM.YYYY")}}</td>
</tr>
<tr v-if="!itemInfo.active">
<td>Enddatum:</td>
<td>{{dayjs(itemInfo.endDate).format("DD.MM.YYYY")}}</td>
</tr>
<tr>
<td>Unterschrieben am:</td>
<td>{{dayjs(itemInfo.signDate).format("DD.MM.YYYY")}}</td>
</tr>
<tr>
<td>Laufzeit:</td>
<td>{{itemInfo.duration}}</td>
</tr>
<tr>
<td>Rechnungsversand:</td>
<td>{{itemInfo.invoiceDispatch}}</td>
</tr>
<tr>
<td>Zahlart:</td>
<td>{{itemInfo.paymentType}}</td>
</tr>
<tr v-if="itemInfo.paymentType === 'Einzug'">
<td>SEPA Mandatsreferenz:</td>
<td>{{itemInfo.sepaRef}}</td>
</tr>
<tr v-if="itemInfo.paymentType === 'Einzug'">
<td>SEPA Unterschrieben am:</td>
<td>{{dayjs(itemInfo.sepaDate).format("DD.MM.YYYY")}}</td>
</tr>
<tr>
<td>Bank:</td>
<td>{{itemInfo.bankingName}}</td>
</tr>
<tr>
<td>BIC:</td>
<td>{{itemInfo.bankingBIC}}</td>
</tr>
<tr>
<td>IBAN:</td>
<td>{{itemInfo.bankingIban}}</td>
</tr>
<tr>
<td>Kontoinhaber:</td>
<td>{{itemInfo.bankingOwner}}</td>
</tr>
<tr>
<td>Beschreibung:</td>
<td>{{itemInfo.notes}}</td>
</tr>
<tr>
<td colspan="2"><span class="font-bold">Eigene Felder:</span></td>
</tr>
<tr v-for="fieldKey in Object.keys(itemInfo.ownFields)">
<td>{{dataStore.ownTenant.ownFields.contracts.find(i => i.key === fieldKey).label}}</td>
<td>{{itemInfo.ownFields[fieldKey]}}</td>
</tr>
</table>
</div>
</UCard>
</div>
<div class="w-1/2">
<UCard class="h-full">
<HistoryDisplay
type="contract"
v-if="itemInfo"
:element-id="itemInfo.id"
:render-headline="true"
/>
</UCard>
</div>
<!-- <UBadge
v-if="itemInfo.active"
>
Vertrag aktiv
</UBadge>
<UBadge
v-else
color="red"
>
Vertrag gesperrt
</UBadge>-->
</div>
<div v-else-if="item.label === 'Dokumente'">
<UCard class="mt-5">
<Toolbar>
<DocumentUpload
type="contract"
:element-id="itemInfo.id"
/>
</Toolbar>
<DocumentList
:documents="dataStore.getDocumentsByContractId(itemInfo.id)"
/>
</UCard>
</div>
</template>
</UTabs>
<UForm v-else-if="mode == 'edit' || mode == 'create'" class="p-5" >
<div class="mx-auto w-4/5">
<div class="flex flex-row justify-around">
<div class="w-1/2">
<UDivider>Vertragsdaten</UDivider>
<UFormGroup
label="Name:"
>
<UInput
v-model="itemInfo.name"
/>
</UFormGroup>
<UFormGroup
label="Kunde:"
>
<USelectMenu
v-model="itemInfo.customer"
option-attribute="name"
value-attribute="id"
:options="dataStore.customers"
@change="itemInfo.contact = null"
searchable
:search-attributes="['name']"
>
<template #label>
{{dataStore.getCustomerById(itemInfo.customer) ? dataStore.getCustomerById(itemInfo.customer).name : "Kein Kunde ausgewählt" }}
</template>
</USelectMenu>
</UFormGroup>
<UFormGroup
label="Ansprechpartner:"
v-if="itemInfo.customer"
>
<USelectMenu
v-model="itemInfo.contact"
option-attribute="fullName"
value-attribute="id"
:options="dataStore.getContactsByCustomerId(itemInfo.customer)"
searchable
:search-attributes="['name']"
>
<template #label>
{{dataStore.getContactById(itemInfo.contact) ? dataStore.getContactById(itemInfo.contact).fullName : "Kein Ansprechpartner ausgewählt" }}
</template>
</USelectMenu>
</UFormGroup>
<InputGroup>
<UFormGroup
label="Vertrag aktiv:"
>
<UCheckbox
v-model="itemInfo.active"
/>
</UFormGroup>
<UFormGroup
label="Vertrag wiederkehrend:"
>
<UCheckbox
v-model="itemInfo.recurring"
/>
</UFormGroup>
</InputGroup>
<InputGroup>
<UFormGroup
label="Vertragsstart:"
class="mt-2"
>
<UPopover :popper="{ placement: 'bottom-start' }">
<UButton
icon="i-heroicons-calendar-days-20-solid"
:label="itemInfo.startDate ? dayjs(itemInfo.startDate).format('DD.MM.YYYY') : 'Datum auswählen'"
variant="outline"
/>
<template #panel="{ close }">
<LazyDatePicker v-model="itemInfo.startDate" @close="close" />
</template>
</UPopover>
</UFormGroup>
<UFormGroup
label="Vertragsende(voraussichtlich):"
class="mt-2"
>
<UPopover :popper="{ placement: 'bottom-start' }">
<UButton
icon="i-heroicons-calendar-days-20-solid"
:label="itemInfo.endDate ? dayjs(itemInfo.endDate).format('DD.MM.YYYY') : 'Datum auswählen'"
variant="outline"
/>
<template #panel="{ close }">
<LazyDatePicker v-model="itemInfo.endDate" @close="close" />
</template>
</UPopover>
</UFormGroup>
</InputGroup>
<UFormGroup
label="mindest Vertragslaufzeit:"
>
<USelectMenu
:options="['12 Monate','24 Monate','36 Monate','48 Monate']"
v-model="itemInfo.duration"
/>
</UFormGroup>
<UFormGroup
label="Datum der Unterzeichnung:"
class="mt-2"
>
<UPopover :popper="{ placement: 'bottom-start' }">
<UButton
icon="i-heroicons-calendar-days-20-solid"
:label="itemInfo.signDate ? dayjs(itemInfo.signDate).format('DD.MM.YYYY') : 'Datum auswählen'"
variant="outline"
/>
<template #panel="{ close }">
<LazyDatePicker v-model="itemInfo.signDate" @close="close" />
</template>
</UPopover>
</UFormGroup>
</div>
<div class=" ml-5 w-1/2">
<UDivider>Abrechnung</UDivider>
<UFormGroup
label="Rechnungsversand:"
>
<USelectMenu
:options="['E-Mail', 'Post']"
v-model="itemInfo.invoiceDispatch"
>
</USelectMenu>
</UFormGroup>
<UFormGroup
label="Zahlungsart:"
>
<USelectMenu
:options="['Einzug', 'Überweisung']"
v-model="itemInfo.paymentType"
>
</USelectMenu>
</UFormGroup>
<UFormGroup
label="Sepa Mandat"
>
<InputGroup>
<UPopover :popper="{ placement: 'bottom-start' }">
<UButton
icon="i-heroicons-calendar-days-20-solid"
:label="itemInfo.sepaDate ? dayjs(itemInfo.sepaDate).format('DD.MM.YYYY') : 'Datum auswählen'"
variant="outline"
/>
<template #panel="{ close }">
<LazyDatePicker v-model="itemInfo.sepaDate" @close="close" />
</template>
</UPopover>
<UInput
placeholder="Mandatsreferenz"
class="flex-auto"
v-model="itemInfo.sepaRef"
/>
</InputGroup>
</UFormGroup>
<UFormGroup
label="Kontodaten:"
>
<InputGroup>
<UInput
v-model="itemInfo.bankingIban"
placeholder="IBAN"
class="w-1/2"
/>
<UInput
v-model="itemInfo.bankingOwner"
class="w-1/2"
placeholder="Inhaber"
/>
</InputGroup>
</UFormGroup>
<UFormGroup
label="Bankdaten:"
>
<InputGroup>
<UInput
v-model="itemInfo.bankingName"
placeholder="Name"
class="w-1/2"
/>
<UInput
v-model="itemInfo.bankingBIC"
placeholder="BIC"
class="w-1/2"
/>
</InputGroup>
</UFormGroup>
</div>
</div>
<UFormGroup
label="Notizen:"
>
<UTextarea
v-model="itemInfo.notes"
rows="6"
maxrows="12"
/>
</UFormGroup>
<div
v-if="dataStore.ownTenant.ownFields"
>
<UDivider
class="mt-3"
>Eigene Felder</UDivider>
<UFormGroup
v-for="field in dataStore.ownTenant.ownFields.contracts"
:key="field.key"
:label="field.label"
>
<UInput
v-if="field.type === 'text'"
v-model="itemInfo.ownFields[field.key]"
/>
<USelectMenu
v-else-if="field.type === 'select'"
:options="field.options"
v-model="itemInfo.ownFields[field.key]"
/>
</UFormGroup>
</div>
</div>
</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

@@ -1,131 +0,0 @@
<template>
<UDashboardNavbar title="Verträge" :badge="filteredRows.length">
<template #right>
<UInput
id="searchinput"
v-model="searchString"
icon="i-heroicons-funnel"
autocomplete="off"
placeholder="Suche..."
class="hidden lg:block"
@keydown.esc="$event.target.blur()"
>
<template #trailing>
<UKbd value="/" />
</template>
</UInput>
<UButton @click="router.push(`/contracts/create`)">+ Vertrag</UButton>
</template>
</UDashboardNavbar>
<UDashboardToolbar>
<template #right>
<USelectMenu
v-model="selectedColumns"
icon="i-heroicons-adjustments-horizontal-solid"
:options="templateColumns"
multiple
class="hidden lg:block"
by="key"
>
<template #label>
Spalten
</template>
</USelectMenu>
</template>
</UDashboardToolbar>
<UTable
:rows="filteredRows"
:columns="columns"
class="w-full"
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
@select="(i) => router.push(`/contracts/show/${i.id}`) "
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Verträge anzuzeigen' }"
>
<template #customer-data="{row}">
<span v-if="row === filteredRows[selectedItem]" class="text-primary-500 font-bold">{{row.customer ? row.customer.name : ""}}</span>
<span v-else>{{row.customer ? row.customer.name : ""}}</span>
</template>
</UTable>
</template>
<script setup>
definePageMeta({
middleware: "auth"
})
defineShortcuts({
'/': () => {
//console.log(searchinput)
//searchinput.value.focus()
document.getElementById("searchinput").focus()
},
'+': () => {
router.push("/contracts/create")
},
'Enter': {
usingInput: true,
handler: () => {
router.push(`/contracts/show/${filteredRows.value[selectedItem.value].id}`)
}
},
'arrowdown': () => {
if(selectedItem.value < filteredRows.value.length - 1) {
selectedItem.value += 1
} else {
selectedItem.value = 0
}
},
'arrowup': () => {
if(selectedItem.value === 0) {
selectedItem.value = filteredRows.value.length - 1
} else {
selectedItem.value -= 1
}
}
})
const dataStore = useDataStore()
const router = useRouter()
const items = ref([])
const selectedItem = ref(0)
const setupPage = async () => {
items.value = await useSupabaseSelect("contracts","*, customer(id,name)")
}
setupPage()
const templateColumns = [
{
key: 'customer',
label: "Kunde",
sortable: true
},
{
key: "name",
label: "Name",
sortable: true
},
{
key: "notes",
label: "Notizen"
}
]
const selectedColumns = ref(templateColumns)
const columns = computed(() => templateColumns.filter((column) => selectedColumns.value.includes(column)))
const searchString = ref('')
const filteredRows = computed(() => {
return useSearch(searchString.value, items.value)
})
</script>
<style scoped>
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -111,7 +111,7 @@
</div>
</template>
<template #amount-data="{row}">
{{displayCurrency(calculateDocSum(row))}}
<span v-if="row.type !== 'deliveryNotes'">{{displayCurrency(calculateDocSum(row))}}</span>
</template>
</UTable>
@@ -162,7 +162,7 @@ const items = ref([])
const selectedItem = ref(0)
const setupPage = async () => {
items.value = await useSupabaseSelect("createddocuments","*, customer(id,name), statementallocations(id,amount)","documentDate")
items.value = (await useSupabaseSelect("createddocuments","*, customer(id,name), statementallocations(id,amount)","documentNumber")).filter(i => !i.archived)
}
setupPage()
@@ -215,6 +215,9 @@ const templateTypes = [
{
key: "invoices",
label: "Rechnungen"
},{
key: "cancellationInvoices",
label: "Stornorechnungen"
},{
key: "advanceInvoices",
label: "Abschlagsrechnungen"
@@ -269,7 +272,7 @@ const calculateDocSum = (row) => {
row.rows.forEach(row => {
if(row.mode === "normal" || row.mode === "service" || row.mode === "free") {
sum += row.quantity * row.price * (1 - row.discountPercent / 100) * (1 + row.taxPercent / 100)
sum += row.quantity * row.price * (1 - row.discountPercent / 100) * (1 + (row.taxPercent || 0) / 100)
}
})
@@ -280,10 +283,6 @@ const isPaid = (item) => {
let amountPaid = 0
item.statementallocations.forEach(allocation => amountPaid += allocation.amount)
console.log(item.documentNumber)
console.log(amountPaid)
console.log(calculateDocSum(item))
return Number(amountPaid.toFixed(2)) === Number(calculateDocSum(item))
}
</script>

View File

@@ -12,6 +12,7 @@ defineShortcuts({
const supabase = useSupabaseClient()
const dataStore = useDataStore()
const profileStore = useProfileStore()
const route = useRoute()
const router = useRouter()
@@ -20,13 +21,20 @@ const linkedDocument =ref({})
const currentTenant = ref({})
const setupPage = async () => {
if(route.params) {
if(route.params.id) itemInfo.value = await useSupabaseSelectSingle("createddocuments",route.params.id,"*")
const {data,error} = await supabase.from("documents").select("id").eq("createdDocument", route.params.id).order("id",{ascending:true})
linkedDocument.value = data[data.length -1]
if(route.params.id) itemInfo.value = await useSupabaseSelectSingle("createddocuments",route.params.id,"*, files(*)")
console.log(itemInfo.value)
linkedDocument.value = await useFiles().selectDocument(itemInfo.value.files[0].id)
//const {data,error} = await supabase.from("files").select("id").eq("createddocument", route.params.id).order("id",{ascending:true})
//linkedDocument.value = data[data.length -1]
}
currentTenant.value = (await supabase.from("tenants").select().eq("id",dataStore.currentTenant).single()).data
currentTenant.value = (await supabase.from("tenants").select().eq("id",profileStore.currentTenant).single()).data
console.log(currentTenant.value)
}
@@ -35,9 +43,9 @@ setupPage()
const openEmail = () => {
if(["invoices","advanceInvoices"].includes(itemInfo.value.type)){
router.push(`/email/new?loadDocuments=[${linkedDocument.value.id}]&bcc=${encodeURIComponent(currentTenant.value.standardEmailForInvoices)}`)
router.push(`/email/new?loadDocuments=["${linkedDocument.value.id}"]&bcc=${encodeURIComponent(currentTenant.value.standardEmailForInvoices)}`)
} else {
router.push(`/email/new?loadDocuments=[${linkedDocument.value.id}]`)
router.push(`/email/new?loadDocuments=["${linkedDocument.value.id}"]`)
}
}
</script>
@@ -56,10 +64,10 @@ const openEmail = () => {
>
Bearbeiten
</UButton>
<UButton
<!-- <UButton
:to="dataStore.documents.find(i => i.createdDocument === itemInfo.id) ? dataStore.documents.find(i => i.createdDocument === itemInfo.id).url : ''"
target="_blank"
>In neuen Tab anzeigen</UButton>
>In neuen Tab anzeigen</UButton>-->
<UButton
@click="router.push(`/createDocument/edit/?linkedDocument=${itemInfo.id}`)"
>
@@ -71,13 +79,32 @@ const openEmail = () => {
>
E-Mail
</UButton>
<UButton
@click="router.push(`/createDocument/edit/?linkedDocument=${itemInfo.id}&loadMode=storno`)"
variant="outline"
color="rose"
>
Stornieren
</UButton>
<UButton
v-if="itemInfo.project"
@click="router.push(`/standardEntity/projects/show/${itemInfo.project}`)"
icon="i-heroicons-arrow-right-end-on-rectangle"
variant="outline"
>
Projekt
</UButton>
</template>
</UDashboardToolbar>
<UDashboardPanelContent>
<object
:data="linkedDocument.url"
class="h-full"
/>
</UDashboardPanelContent>
<object
:data="dataStore.documents.find(i => i.id === linkedDocument.id) ? dataStore.documents.find(i => i.id === linkedDocument.id).url : ''"
class="h-full"
/>

View File

@@ -1,369 +0,0 @@
<script setup>
import {useSupabaseSelectSingle} from "~/composables/useSupabase.js";
definePageMeta({
middleware: "auth"
})
defineShortcuts({
'backspace': () => {
router.push("/customers")
},
'arrowleft': () => {
if(openTab.value > 0){
openTab.value -= 1
}
},
'arrowright': () => {
if(openTab.value < 3) {
openTab.value += 1
}
},
})
const dataStore = useDataStore()
const route = useRoute()
const router = useRouter()
const toast = useToast()
const id = ref(route.params.id ? route.params.id : null )
const openTab = ref(0)
//Working
const mode = ref(route.params.mode || "show")
const itemInfo = ref({
name: "",
infoData: {
country: "Deutschland"
},
active: true,
isCompany: true
})
const oldItemInfo = ref({})
//Functions
const setupPage = async () => {
if(mode.value === "show"){
itemInfo.value = await useSupabaseSelectSingle("customers",route.params.id,"*, contacts(*)")
} else if(mode.value === "edit") {
itemInfo.value = await useSupabaseSelectSingle("customers",route.params.id,"*")
}
if(itemInfo.value) oldItemInfo.value = JSON.parse(JSON.stringify(itemInfo.value))
}
const editItem = async () => {
await router.push(`/customers/edit/${itemInfo.value.id}`)
}
const setCityByZip = async () => {
itemInfo.value.infoData.city = await useZipCheck(itemInfo.value.infoData.zip)
}
setupPage()
</script>
<template>
<UDashboardNavbar
:title="itemInfo ? `Kunde: ${itemInfo.name}` : (mode === 'create' ? 'Kunde erstellen' : 'Kunde bearbeiten')"
:ui="{center: 'flex items-stretch gap-1.5 min-w-0'}"
>
<template #left>
<UButton
icon="i-heroicons-chevron-left"
variant="outline"
@click="router.push(`/customers`)"
>
Kunden
</UButton>
</template>
<template #center>
<h1
v-if="itemInfo"
:class="['text-xl','font-medium', ... itemInfo.active ? ['text-primary'] : ['text-rose-500']]"
>{{itemInfo ? `Kunde: ${itemInfo.name}` : (mode === 'create' ? 'Kunde erstellen' : 'Kunde bearbeiten')}}</h1>
</template>
<template #right>
<UButton
v-if="mode === 'edit'"
@click="dataStore.updateItem('customers',itemInfo,oldItemInfo)"
>
Speichern
</UButton>
<UButton
v-else-if="mode === 'create'"
@click="dataStore.createNewItem('customers',itemInfo)"
>
Erstellen
</UButton>
<UButton
@click="itemInfo ? router.push(`/customers/show/${itemInfo.id}`) : router.push(`/customers`)"
color="red"
class="ml-2"
v-if="mode === 'edit' || mode === 'create'"
>
Abbrechen
</UButton>
<UButton
v-if="mode === 'show'"
@click="editItem"
>
Bearbeiten
</UButton>
</template>
<template #badge v-if="itemInfo">
<UBadge
v-if="itemInfo.active"
>
Aktiv
</UBadge>
<UBadge
v-else
color="red"
>
Gesperrt
</UBadge>
</template>
</UDashboardNavbar>
<UDashboardPanelContent>
<UTabs
v-if="itemInfo.id && mode == 'show'"
:items="[{label: 'Informationen'},{label: 'Projekte'},{label: 'Objekte'},{label: 'Verträge'}]"
class="p-5"
v-model="openTab"
>
<template #item="{item}">
<div v-if="item.label === 'Informationen'" class="flex mt-5">
<div class="w-1/2 mr-5">
<UCard >
<div class="text-wrap">
<p>Kundennummer: {{itemInfo.customerNumber}}</p>
<p>Typ: {{itemInfo.isCompany ? 'Firma' : 'Privatperson'}}</p>
<p v-if="itemInfo.infoData.street">Straße + Hausnummer: {{itemInfo.infoData.street}}</p>
<p v-if="itemInfo.infoData.zip && itemInfo.infoData.city">PLZ + Ort: {{itemInfo.infoData.zip}} {{itemInfo.infoData.city}}</p>
<p v-if="itemInfo.infoData.tel">Telefon: {{itemInfo.infoData.tel}}</p>
<p v-if="itemInfo.infoData.email">E-Mail: {{itemInfo.infoData.email}}</p>
<p v-if="itemInfo.infoData.web">Web: {{itemInfo.infoData.web}}</p>
<p v-if="itemInfo.infoData.ustid">USt-Id: {{itemInfo.infoData.ustid}}</p>
<p>Notizen:<br> {{itemInfo.notes}}</p>
</div>
</UCard>
<UCard class="mt-5">
<Toolbar>
<UButton
@click="router.push(`/contacts/create?customer=${itemInfo.id}`)"
>
+ Ansprechpartner
</UButton>
</Toolbar>
<UTable
:rows="dataStore.getContactsByCustomerId(itemInfo.id)"
@select="(row) => router.push(`/contacts/show/${row.id}`)"
:columns="[{label: 'Anrede', key: 'salutation'},{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 class="w-1/2">
<UCard class="h-full">
<HistoryDisplay
type="customer"
v-if="itemInfo"
:element-id="itemInfo.id"
:render-headline="true"
/>
</UCard>
</div>
</div>
<UCard class="mt-5" v-else>
<div v-if="item.label === 'Projekte'">
<Toolbar>
<UButton
@click="router.push(`/projects/create?customer=${itemInfo.id}`)"
>
+ Projekt
</UButton>
</Toolbar>
<UTable
:rows="dataStore.getProjectsByCustomerId(itemInfo.id)"
@select="(row) => router.push(`/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>
</div>
<div v-else-if="item.label === 'Objekte'">
<Toolbar>
<UButton
@click="router.push(`/plants/create?customer=${itemInfo.id}`)"
>
+ Objekt
</UButton>
</Toolbar>
<UTable
:rows="dataStore.getPlantsByCustomerId(itemInfo.id)"
@select="(row) => router.push(`/plants/show/${row.id}`)"
:columns="[{label: 'Name', key: 'name'}]"
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine zugehörigen Objekte' }"
>
</UTable>
</div>
<div v-else-if="item.label === 'Verträge'">
<Toolbar>
<UButton
@click="router.push(`/contracts/create?customer=${itemInfo.id}`)"
>
+ Vertrag
</UButton>
</Toolbar>
<UTable
:rows="dataStore.getContractsByCustomerId(itemInfo.id)"
@select="(row) => router.push(`/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>
</div>
</UCard>
</template>
</UTabs>
<UForm v-else-if="mode === 'edit' || mode === 'create'" class="p-5">
<UFormGroup
label="Name:"
>
<UInput
v-model="itemInfo.name"
/>
</UFormGroup>
<UFormGroup
label="Kundennummer:"
>
<UInput
v-model="itemInfo.customerNumber"
placeholder="Leer lassen für automatisch generierte Nummer"
/>
</UFormGroup>
<UTooltip text="Ist ein Kunde nicht aktiv so wird er für neue Aufträge gesperrt">
<UFormGroup
label="Kunde aktiv:"
>
<UCheckbox
v-model="itemInfo.active"
/>
</UFormGroup>
</UTooltip>
<UFormGroup
label="Firmenkunde:"
>
<UCheckbox
v-model="itemInfo.isCompany"
/>
</UFormGroup>
<UFormGroup
label="Notizen:"
>
<UTextarea
v-model="itemInfo.notes"
/>
</UFormGroup>
<UFormGroup
label="Straße + Hausnummer"
>
<UInput
v-model="itemInfo.infoData.street"
/>
</UFormGroup>
<UFormGroup
label="Adresszusatz"
>
<UInput
v-model="itemInfo.infoData.special"
/>
</UFormGroup>
<UFormGroup
label="Postleitzahl"
>
<UInput
v-model="itemInfo.infoData.zip"
@focusout="setCityByZip"
/>
</UFormGroup>
<UFormGroup
label="Ort"
>
<UInput
v-model="itemInfo.infoData.city"
/>
</UFormGroup>
<UFormGroup
label="Land"
>
<USelectMenu
:options="['Deutschland','Niederlande','Belgien','Italien', 'Frankreich','Irland','USA','Spanien', 'Schweden']"
v-model="itemInfo.infoData.country"
/>
</UFormGroup>
<UFormGroup
label="Telefon:"
>
<UInput
v-model="itemInfo.infoData.tel"
/>
</UFormGroup>
<UFormGroup
label="E-Mail:"
>
<UInput
v-model="itemInfo.infoData.email"
/>
</UFormGroup>
<UFormGroup
label="Webseite:"
>
<UInput
v-model="itemInfo.infoData.web"
/>
</UFormGroup>
<UFormGroup
label="USt-Id:"
>
<UInput
v-model="itemInfo.infoData.ustid"
/>
</UFormGroup>
</UForm>
</UDashboardPanelContent>
</template>
<style scoped>
</style>

View File

@@ -1,168 +0,0 @@
<template>
<UDashboardNavbar title="Kunden" :badge="filteredRows.length">
<template #right>
<UInput
id="searchinput"
name="searchinput"
v-model="searchString"
icon="i-heroicons-funnel"
autocomplete="off"
placeholder="Suche..."
class="hidden lg:block"
@keydown.esc="$event.target.blur()"
>
<template #trailing>
<UKbd value="/" />
</template>
</UInput>
<UButton @click="router.push(`/customers/create/`)">+ Kunde</UButton>
</template>
</UDashboardNavbar>
<UDashboardToolbar>
<template #right>
<USelectMenu
v-model="selectedColumns"
icon="i-heroicons-adjustments-horizontal-solid"
:options="templateColumns"
multiple
class="hidden lg:block"
by="key"
>
<template #label>
Spalten
</template>
</USelectMenu>
</template>
</UDashboardToolbar>
<UTable
:rows="filteredRows"
:columns="columns"
class="w-full"
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
@select="(i) => router.push(`/customers/show/${i.id}`) "
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Kunden anzuzeigen' }"
>
<template #name-data="{row}">
<span v-if="row === filteredRows[selectedItem]" class="text-primary-500 font-bold">{{row.name}}</span>
<span v-else>{{row.name}}</span>
</template>
<template #isCompany-data="{row}">
<span v-if="row.isCompany">Firmenkunden</span>
<span v-else>Privatkunde</span>
</template>
<template #active-data="{row}">
<span v-if="row.active" class="text-primary-500">Aktiv</span>
<span v-else class="text-rose-500">Gesperrt</span>
</template>
<template #address-data="{row}">
{{row.infoData.street ? `${row.infoData.street}, ` : ''}}{{row.infoData.special ? `${row.infoData.special},` : ''}} {{row.infoData.zip ? row.infoData.zip : ""}} {{row.infoData.city ? `${row.infoData.city}, ` : ''}} {{row.infoData.country}}
</template>
</UTable>
</template>
<script setup>
definePageMeta({
middleware: "auth"
})
defineShortcuts({
'/': () => {
//console.log(searchinput)
//searchinput.value.focus()
document.getElementById("searchinput").focus()
},
'+': () => {
router.push("/customers/create")
},
'Enter': {
usingInput: true,
handler: () => {
router.push(`/customers/show/${filteredRows.value[selectedItem.value].id}`)
}
},
'arrowdown': () => {
if(selectedItem.value < filteredRows.value.length - 1) {
selectedItem.value += 1
} else {
selectedItem.value = 0
}
},
'arrowup': () => {
if(selectedItem.value === 0) {
selectedItem.value = filteredRows.value.length - 1
} else {
selectedItem.value -= 1
}
}
})
const dataStore = useDataStore()
const router = useRouter()
const supabase = useSupabaseClient()
const items = ref([])
const selectedItem = ref(0)
const setupPage = async () => {
items.value = await useSupabaseSelect("customers",null,"customerNumber")
}
setupPage()
const templateColumns = [
{
key: 'customerNumber',
label: "Kundennr.",
sortable: true
},
{
key: "name",
label: "Name",
sortable: true
},
{
key: "isCompany",
label: "Typ",
sortable: true
},
{
key: "notes",
label: "Notizen",
sortable: true
},
{
key: "active",
label: "Aktiv",
sortable: true
},
{
key: "address",
label: "Adresse",
sortable: true
}
]
const selectedColumns = ref(templateColumns)
const columns = computed(() => templateColumns.filter((column) => selectedColumns.value.includes(column)))
const searchString = ref('')
const filteredRows = computed(() => {
return useSearch(searchString.value, items.value)
})
</script>
<style scoped>
</style>

View File

@@ -1,580 +0,0 @@
<script setup>
import axios from 'axios'
import {sub} from 'date-fns'
const dataStore = useDataStore()
const accounts = ref([])
//accounts.value = dataStore.emailAccounts.map(i => { return { label: i.emailAddress, emailEngingeId: i.emailEngineId }})
const mailboxes = ref({})
const messages = ref([])
const selectedMailbox = ref("INBOX")
const selectedAccount = ref(null)
const selectedMessage = ref(null)
const setup = async () => {
accounts.value = dataStore.emailAccounts.map((i,index) => {
let item = { label: i.emailAddress, emailEngineId: i.emailEngineId }
if(index === 0) {
item.defaultOpen = true
}
return item
})
for await (const account of accounts.value) {
console.log(account.emailEngineId)
const {data,error} = await axios.get(`http://157.90.231.142:3000/v1/account/${account.emailEngineId}/mailboxes`, {headers: { 'Authorization': 'Bearer dadb572465fba648590f31557f68028a750b47b278d87c1773e8fd09670eec59'}})
console.log(data)
console.log(error)
mailboxes.value[account.emailEngineId] = data.mailboxes
}
}
const selectMailbox = async (account, mailbox) => {
selectedMailbox.value = mailbox
const {data,error} = await axios.get(`http://157.90.231.142:3000/v1/account/${account}/messages?path=${ mailbox.path}`, {headers: { 'Authorization': 'Bearer dadb572465fba648590f31557f68028a750b47b278d87c1773e8fd09670eec59'}})
console.log(data)
console.log(error)
messages.value = data.messages
selectedAccount.value = account
}
const messageHTML = ref(null)
const messageText = ref(null)
const selectMessage = async (account, message) => {
console.log(message)
selectedMessage.value = message
const {data,error} = await axios.get(`http://157.90.231.142:3000/v1/account/${account}/text/${message.text.id}`, {headers: { 'Authorization': 'Bearer dadb572465fba648590f31557f68028a750b47b278d87c1773e8fd09670eec59'}})
messageHTML.value = data.html
messageText.value = data.plain
}
const addFlags = async (account, message, flags) => {
console.log(flags)
const {data,error} = await axios({
method: "PUT",
url: `http://157.90.231.142:3000/v1/account/${account}/message/${message.id}`,
headers: { 'Authorization': 'Bearer dadb572465fba648590f31557f68028a750b47b278d87c1773e8fd09670eec59'},
data: {
flags: {
add: flags
}
}
})
console.log(data)
console.log(error)
}
const removeFlags = async (account, message, flags) => {
console.log(flags)
const {data,error} = await axios({
method: "PUT",
url: `http://157.90.231.142:3000/v1/account/${account}/message/${message.id}`,
headers: { 'Authorization': 'Bearer dadb572465fba648590f31557f68028a750b47b278d87c1773e8fd09670eec59'},
data: {
flags: {
delete: flags
}
}
})
console.log(data)
console.log(error)
}
const setSeen = async (seen,message) => {
if(seen) {
await addFlags(selectedAccount.value,message, ["\\Seen"])
await selectMailbox(selectedAccount.value, selectedMailbox.value)
} else {
await removeFlags(selectedAccount.value,message, ["\\Seen"])
await selectMailbox(selectedAccount.value, selectedMailbox.value)
}
}
const moveTo = async (destinationPath) => {
const {data,error} = await axios({
method: "PUT",
url: `http://157.90.231.142:3000/v1/account/${selectedAccount.value}/message/${selectedMessage.value.id}/move`,
headers: { 'Authorization': 'Bearer dadb572465fba648590f31557f68028a750b47b278d87c1773e8fd09670eec59'},
data: {
path: destinationPath
}
})
console.log(data)
console.log(error)
selectedMessage.value = null
messageHTML.value = null
messageText.value = null
selectMailbox(selectedAccount.value, selectedMailbox.value)
}
const downloadAttachment = async (attachment) => {
const {data,error} = await axios({
method: "GET",
url: `http://157.90.231.142:3000/v1/account/${selectedAccount.value}/attachment/${attachment.id}`,
headers: { 'Authorization': 'Bearer dadb572465fba648590f31557f68028a750b47b278d87c1773e8fd09670eec59'},
responseType: "blob"
})
const downloadURL = URL.createObjectURL(new Blob([data]))
const link = document.createElement('a')
link.href = downloadURL
link.setAttribute('download', attachment.filename)
document.body.appendChild(link)
link.click()
link.remove()
console.log(data)
console.log(error)
}
setup()
const selectedTab = ref(0)
const mails = ref([{
id: 1,
from: {
name: 'Alex Smith',
email: 'alex.smith@example.com',
avatar: {
src: 'https://i.pravatar.cc/128?u=1'
}
},
subject: 'Meeting Schedule',
body: 'Hi there, just a quick reminder about our meeting scheduled for 10 AM tomorrow. We\'ll be discussing the new marketing strategies and I would really appreciate your input on the matter. Looking forward to a productive session.',
date: new Date().toISOString()
}, {
id: 2,
unread: true,
from: {
name: 'Jordan Brown',
email: 'jordan.brown@example.com',
avatar: {
src: 'https://i.pravatar.cc/128?u=2'
}
},
subject: 'Project Update',
body: 'I wanted to provide you with the latest update on the project. We\'ve made significant progress on the development front and I\'ve attached a detailed report for your review. Please let me know your thoughts and any areas for improvement.',
date: sub(new Date(), { minutes: 7 }).toISOString()
}, {
id: 3,
unread: true,
from: {
name: 'Taylor Green',
email: 'taylor.green@example.com',
avatar: {
src: 'https://i.pravatar.cc/128?u=3'
}
},
subject: 'Lunch Plans',
body: 'Hey! I was wondering if you would like to grab lunch this Friday. I know a great spot downtown that serves the best Mexican cuisine. It would be a great opportunity for us to catch up and discuss the upcoming team event.',
date: sub(new Date(), { hours: 3 }).toISOString()
}, {
id: 4,
from: {
name: 'Morgan White',
email: 'morgan.white@example.com',
avatar: {
src: 'https://i.pravatar.cc/128?u=4'
}
},
subject: 'New Proposal',
body: 'I\'ve attached the new proposal for our next project. It outlines all the objectives, timelines, and resource allocations. I\'m particularly excited about the innovative approach we\'re taking this time. Please have a look and let me know your thoughts.',
date: sub(new Date(), { days: 1 }).toISOString()
}, {
id: 5,
from: {
name: 'Casey Gray',
email: 'casey.gray@example.com'
},
subject: 'Travel Itinerary',
body: 'Your travel itinerary for the upcoming business trip is ready. I\'ve included all flight details, hotel reservations, and meeting schedules. Please review and let me know if there are any changes you would like to make or any additional arrangements needed.',
date: sub(new Date(), { days: 1 }).toISOString()
}, {
id: 6,
from: {
name: 'Jamie Johnson',
email: 'jamie.johnson@example.com'
},
subject: 'Budget Report',
body: 'I\'ve completed the budget report for this quarter. It includes a detailed analysis of our expenditures and revenue, along with projections for the next quarter. I believe there are some areas where we can optimize our spending. Let\'s discuss this in our next finance meeting.',
date: sub(new Date(), { days: 2 }).toISOString()
}, {
id: 7,
from: {
name: 'Riley Davis',
email: 'riley.davis@example.com',
avatar: {
src: 'https://i.pravatar.cc/128?u=7'
}
},
subject: 'Training Session',
body: 'Just a reminder about the training session scheduled for next week. We\'ll be covering new software tools that are crucial for our workflow. It\'s important that everyone attends as this will greatly enhance our team\'s efficiency. Please confirm your availability.',
date: sub(new Date(), { days: 2 }).toISOString()
}, {
id: 8,
unread: true,
from: {
name: 'Kelly Wilson',
email: 'kelly.wilson@example.com',
avatar: {
src: 'https://i.pravatar.cc/128?u=8'
}
},
subject: 'Happy Birthday!',
body: 'Happy Birthday! Wishing you a fantastic day filled with joy and laughter. Your dedication and hard work throughout the year have been invaluable to our team. Enjoy your day to the fullest!',
date: sub(new Date(), { days: 2 }).toISOString()
}, {
id: 9,
from: {
name: 'Drew Moore',
email: 'drew.moore@example.com'
},
subject: 'Website Feedback',
body: 'We are in the process of revamping our company website and I would greatly appreciate your feedback on the new design. Your perspective is always insightful and could help us enhance the user experience significantly. Please let me know a convenient time for you to discuss this.',
date: sub(new Date(), { days: 5 }).toISOString()
}, {
id: 10,
from: {
name: 'Jordan Taylor',
email: 'jordan.taylor@example.com'
},
subject: 'Gym Membership',
body: 'This is a friendly reminder that your gym membership is due for renewal at the end of this month. We\'ve added several new classes and facilities that I think you\'ll really enjoy. Let me know if you would like a tour of the new facilities.',
date: sub(new Date(), { days: 5 }).toISOString()
}, {
id: 11,
unread: true,
from: {
name: 'Morgan Anderson',
email: 'morgan.anderson@example.com'
},
subject: 'Insurance Policy',
body: 'I\'m writing to inform you that your insurance policy details have been updated. The new document outlines the changes in coverage and premium rates. It\'s important to review these changes to ensure they meet your needs. Please don\'t hesitate to contact me if you have any questions.',
date: sub(new Date(), { days: 12 }).toISOString()
}, {
id: 12,
from: {
name: 'Casey Thomas',
email: 'casey.thomas@example.com'
},
subject: 'Book Club Meeting',
body: 'I\'m excited to remind you about our next book club meeting scheduled for next Thursday. We\'ll be discussing \'The Great Gatsby,\' and I\'m looking forward to hearing everyone\'s perspectives. Also, we will be choosing our next book, so bring your suggestions!',
date: sub(new Date(), { months: 1 }).toISOString()
}, {
id: 13,
from: {
name: 'Jamie Jackson',
email: 'jamie.jackson@example.com'
},
subject: 'Recipe Exchange',
body: 'Don\'t forget to send in your favorite recipe for our upcoming recipe exchange. It\'s a great opportunity to share and discover new and delicious meals. I\'m particularly excited to try out new dishes and add some variety to my cooking.',
date: sub(new Date(), { months: 1 }).toISOString()
}, {
id: 14,
from: {
name: 'Riley White',
email: 'riley.white@example.com'
},
subject: 'Yoga Class Schedule',
body: 'The new schedule for yoga classes is now available. We\'ve added some new styles and adjusted the timings to accommodate more participants. I believe these classes are a great way to relieve stress and stay healthy. Hope to see you there!',
date: sub(new Date(), { months: 1 }).toISOString()
}, {
id: 15,
from: {
name: 'Kelly Harris',
email: 'kelly.harris@example.com'
},
subject: 'Book Launch Event',
body: 'I\'m thrilled to invite you to my book launch event next month. It\'s been a journey writing this book and I\'m eager to share it with you. The event will include a reading session, Q&A, and a signing opportunity. Your support would mean a lot to me.',
date: sub(new Date(), { months: 1 }).toISOString()
}, {
id: 16,
from: {
name: 'Drew Martin',
email: 'drew.martin@example.com'
},
subject: 'Tech Conference',
body: 'Join us at the upcoming tech conference where we will be discussing the latest trends and innovations in technology. This is a great opportunity to network with industry leaders and learn about cutting-edge developments. Your participation would greatly contribute to our team\'s knowledge and growth.',
date: sub(new Date(), { months: 1, days: 4 }).toISOString()
}, {
id: 17,
from: {
name: 'Alex Thompson',
email: 'alex.thompson@example.com'
},
subject: 'Art Exhibition',
body: 'I wanted to invite you to check out the new art exhibition this weekend. It features some amazing contemporary artists and their latest works. It\'s a great opportunity to immerse yourself in the local art scene and get inspired. Let me know if you\'re interested in going together.',
date: sub(new Date(), { months: 1, days: 15 }).toISOString()
}, {
id: 18,
from: {
name: 'Jordan Garcia',
email: 'jordan.garcia@example.com'
},
subject: 'Networking Event',
body: 'I\'m looking forward to seeing you at the networking event next week. It\'s a great chance to connect with professionals from various industries and expand our professional network. There will also be guest speakers discussing key business trends. Your presence would add great value to the discussions.',
date: sub(new Date(), { months: 1, days: 18 }).toISOString()
}, {
id: 19,
from: {
name: 'Taylor Rodriguez',
email: 'taylor.rodriguez@example.com'
},
subject: 'Volunteer Opportunity',
body: 'We\'re looking for volunteers for the upcoming community event. It\'s a great opportunity to give back and make a positive impact. There are various roles available, so you can choose something that aligns with your interests and skills. Let me know if you\'re interested and I\'ll provide more details.',
date: sub(new Date(), { months: 1, days: 25 }).toISOString()
}, {
id: 20,
from: {
name: 'Morgan Lopez',
email: 'morgan.lopez@example.com'
},
subject: 'Car Service Reminder',
body: 'Just a reminder that your car is due for service next week. Regular maintenance is important to ensure your vehicle\'s longevity and performance. I\'ve included the details of the service center and the recommended services in this email. Feel free to contact them directly to schedule an appointment.',
date: sub(new Date(), { months: 2 }).toISOString()
}])
const dropdownItems = [[{
label: 'Mark as unread',
icon: 'i-heroicons-check-circle'
}, {
label: 'Mark as important',
icon: 'i-heroicons-exclamation-circle'
}], [{
label: 'Star thread',
icon: 'i-heroicons-star'
}, {
label: 'Mute thread',
icon: 'i-heroicons-pause-circle'
}]]
// Filter mails based on the selected tab
const filteredMails = computed(() => {
if (selectedTab.value === 1) {
return mails.value.filter(mail => !!mail.unread)
} else {
return mails.value
}
})
const selectedMail = ref(null)
const isMailPanelOpen = computed({
get () {
return !!selectedMail.value
},
set (value) {
if (!value) {
selectedMail.value = null
}
}
})
</script>
<template>
<!-- <div class="flex flex-row">
<div id="mailboxlist">
<UAccordion
:items="accounts"
>
<template #default="{ item, index, open }">
<UButton variant="soft" class="mt-3">
<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-for="mailbox in mailboxes[item.emailEngineId]"
class="my-3"
>
<UButton
@click="selectMailbox(item.emailEngineId, mailbox)"
variant="outline"
>
<span v-if="mailbox.name === 'Trash'">Papierkorb</span>
<span v-else-if="mailbox.name === 'INBOX'">Eingang</span>
<span v-else-if="mailbox.name === 'Sent'">Gesendet</span>
<span v-else-if="mailbox.name === 'Drafts'">Entwürfe</span>
<span v-else-if="mailbox.name === 'spambucket'">Spam</span>
<span v-else>{{mailbox.name}}</span>
<UBadge v-if="mailbox.messages > 0">{{mailbox.messages}}</UBadge>
</UButton>
</div>
</template>
</UAccordion>
</div>
<UDivider orientation="vertical" class="maiLDivider"/>
<div id="maillist">
<div
v-for="message in messages"
v-if="messages.length > 0"
>
<div
:class="message === selectedMessage ? ['message','text-primary-500'] : ['message']"
@click="selectMessage(selectedAccount, message),
!message.flags.includes('\\Seen') ? setSeen(true,message) : null"
>
<UChip
position="top-left"
:show="!message.flags.includes('\\Seen')"
>
<h1>{{message.from.name ||message.from.address}}</h1>
</UChip>
<h3>{{message.subject}}</h3>
</div>
<UDivider class="my-3"/>
</div>
<div
v-else
>
Keine E-Mails in diesem Postfach
</div>
</div>
<UDivider orientation="vertical" class="maiLDivider"/>
<div id="mailcontent" v-if="selectedMessage">
<Toolbar>
&lt;!&ndash;<UButton
@click="setup"
>Setup</UButton>
<UButton>+ Neu</UButton>
<UButton>Sync</UButton>
<UButton>Papierkorb</UButton>
<UButton>Weiterleiten</UButton>
<UButton>Antworten</UButton>&ndash;&gt;
<UButton
@click="setSeen(false,selectedMessage)"
>
Als Ungelesen markieren
</UButton>
<UButton
@click="moveTo('INBOX.Trash')"
icon="i-heroicons-trash"
>
</UButton>
</Toolbar>
<UAlert
v-if="selectedMessage"
:title="attachment.filename"
v-for="attachment in selectedMessage.attachments"
class="my-3"
:actions="[{label: 'Download', click:() => {downloadAttachment(attachment)}}]"
>
</UAlert>
<iframe
v-if="messageHTML"
style="width: 100%; height: 100%"
:srcdoc="messageHTML">
</iframe>
<pre
class="text-wrap"
v-else-if="messageText">
{{messageText}}
</pre>
</div>
</div>-->
<UDashboardPanel id="inbox" :width="400" :resizable="{ min: 300, max: 500 }">
<UDashboardNavbar title="Inbox" >
<template #right>
<UTabs v-model="selectedTab" :items="[{label: 'Alle'},{label: 'Ungelesen'}]" :ui="{ wrapper: '', list: { height: 'h-9', tab: { height: 'h-7', size: 'text-[13px]' } } }" />
</template>
</UDashboardNavbar>
<!-- ~/components/inbox/InboxList.vue -->
<InboxList v-model="selectedMail" :mails="filteredMails" />
</UDashboardPanel>
<UDashboardPanel v-model="isMailPanelOpen" collapsible grow side="right">
<template v-if="selectedMail">
<UDashboardNavbar>
<template #toggle>
<UDashboardNavbarToggle icon="i-heroicons-x-mark" />
<UDivider orientation="vertical" class="mx-1.5 lg:hidden" />
</template>
<template #left>
<UTooltip text="Archive">
<UButton icon="i-heroicons-archive-box" color="gray" variant="ghost" />
</UTooltip>
<UTooltip text="Move to junk">
<UButton icon="i-heroicons-archive-box-x-mark" color="gray" variant="ghost" />
</UTooltip>
<!-- <UDivider orientation="vertical" class="mx-1.5" />-->
<!-- <UPopover :popper="{ placement: 'bottom-start' }">
<template #default="{ open }">
<UTooltip text="Snooze" :prevent="open">
<UButton icon="i-heroicons-clock" color="gray" variant="ghost" :class="[open && 'bg-gray-50 dark:bg-gray-800']" />
</UTooltip>
</template>
<template #panel="{ close }">
<DatePicker @close="close" />
</template>
</UPopover>-->
</template>
<template #right>
<UTooltip text="Reply">
<UButton icon="i-heroicons-arrow-uturn-left" color="gray" variant="ghost" />
</UTooltip>
<UTooltip text="Forward">
<UButton icon="i-heroicons-arrow-uturn-right" color="gray" variant="ghost" />
</UTooltip>
<UDivider orientation="vertical" class="mx-1.5" />
<UDropdown :items="dropdownItems">
<UButton icon="i-heroicons-ellipsis-vertical" color="gray" variant="ghost" />
</UDropdown>
</template>
</UDashboardNavbar>
<!-- ~/components/inbox/InboxMail.vue -->
<InboxMail :mail="selectedMail" />
</template>
<div v-else class="flex-1 hidden lg:flex items-center justify-center">
<UIcon name="i-heroicons-inbox" class="w-32 h-32 text-gray-400 dark:text-gray-500" />
</div>
</UDashboardPanel>
</template>
<style scoped>
</style>

View File

@@ -1,369 +0,0 @@
<script setup>
import {BlobReader, BlobWriter, ZipWriter} from "@zip.js/zip.js";
import {useSupabaseSelectDocuments} from "~/composables/useSupabase.js";
definePageMeta({
middleware: "auth"
})
const dataStore = useDataStore()
const supabase = useSupabaseClient()
const user = useSupabaseUser()
const toast = useToast()
dataStore.fetchDocuments()
const uploadModalOpen = ref(false)
const uploadInProgress = ref(false)
const fileUploadFormData = ref({
tags: ["Eingang"],
path: "",
tenant: dataStore.currentTenant
})
let tags = dataStore.getDocumentTags
const selectedTags = ref("Eingang")
const documents = ref([])
const folders = ref([])
const selectedPath = ref("_")
const loadingDocs = ref(false)
const isDragTarget = ref(false)
const setupPage = async () => {
//documents.value = await useSupabaseSelectDocuments("*, project(id,name), customer(id,name), contract(id,name), vendor(id,name), plant(id,name), vehicle(id,licensePlate), product(id,name), profile(id,fullName) ")
//documents.value = await useSupabaseSelectDocuments("*",null, selectedPath.value)
//console.log(documents.value)
folders.value = dataStore.ownTenant.documentFolders
documents.value = await useSupabaseSelectDocuments("*",null,selectedPath.value)
//await supabase.from("documents").select().eq("folderPath",selectedPath.value).eq("tenant",dataStore.currentTenant)
//console.log(await supabase.from("documents").select().eq("folderPath",selectedPath.value))
const dropZone = document.getElementById("drop_zone")
dropZone.ondragover = function (event) {
console.log(event)
isDragTarget.value = true
event.preventDefault()
}
dropZone.ondragleave = function (event) {
isDragTarget.value = false
}
dropZone.ondrop = async function (event) {
console.log("files dropped")
event.preventDefault()
await uploadFiles(event.dataTransfer.files)
isDragTarget.value = false
setupPage()
}
loadingDocs.value = false
}
setupPage()
const currentFolders = computed(() => {
if(folders.value.length > 0) {
/*console.log(folders.value[0].path.split("/").filter(x => x.length > 0))
console.log(selectedPath.value.split("/").filter(x => x.length > 0))*/
let tempFolders = folders.value.filter(i => (i.path.split("_").filter(x => x.length > 0) || []).length === selectedPath.value.split("_").filter(x => x.length > 0).length + 1)
tempFolders = tempFolders.filter(i => i.path.includes(selectedPath.value))
return tempFolders
}
})
const breadcrumbLinks = computed(() => {
return [{
label: "Home",
click: () => {
changePath("_")
},
icon: "i-heroicons-folder"
},...selectedPath.value.split("_").filter(x => x.length > 0).map((i,index) => {
let re = new RegExp(".+?" + i )
let path = selectedPath.value.match(re)[0]
return {
label: folders.value.find(x => x.path === path).name ||path,
click: () => {
changePath(path)
},
icon: "i-heroicons-folder"
}
})]
})
const filteredDocuments = computed(() => {
/*if(selectedTags.value !== "Archiviert") {
return documents.value.filter(i => i.tags.find(t => selectedTags.value === t) && !i.tags.includes("Archiviert"))
} else {
return documents.value.filter(i => i.tags.find(t => selectedTags.value === t))
}*/
return documents.value
})
const changePath = async (newPath) => {
loadingDocs.value = true
selectedPath.value = newPath
setupPage()
}
const uploadFiles = async (files) => {
uploadInProgress.value = true;
if(files) {
await dataStore.uploadFiles({tags: ["Ablage"],tenant: dataStore.currentTenant, folderPath: selectedPath.value}, files, true)
} else {
await dataStore.uploadFiles(fileUploadFormData.value, document.getElementById("fileUploadInput").files, true)
}
uploadModalOpen.value = false;
uploadInProgress.value = false;
}
const downloadSelected = async () => {
const bucket = "files";
let files = []
dataStore.documents.filter(doc => doc.selected).forEach(doc => files.push(doc.path))
console.log(files)
// If there are no files in the folder, throw an error
if (!files || !files.length) {
throw new Error("No files to download");
}
const promises = [];
// Download each file in the folder
files.forEach((file) => {
promises.push(
supabase.storage.from(bucket).download(`${file}`)
);
});
// Wait for all the files to download
const response = await Promise.allSettled(promises);
// Map the response to an array of objects containing the file name and blob
const downloadedFiles = response.map((result, index) => {
if (result.status === "fulfilled") {
console.log(files[index].split("/")[files[index].split("/").length -1])
return {
name: files[index].split("/")[files[index].split("/").length -1],
blob: result.value.data,
};
}
});
// Create a new zip file
const zipFileWriter = new BlobWriter("application/zip");
const zipWriter = new ZipWriter(zipFileWriter, { bufferedWrite: true });
// Add each file to the zip file
downloadedFiles.forEach((downloadedFile) => {
if (downloadedFile) {
zipWriter.add(downloadedFile.name, new BlobReader(downloadedFile.blob));
}
});
// Download the zip file
const url = URL.createObjectURL(await zipWriter.close());
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", "documents.zip");
document.body.appendChild(link);
link.click();
}
</script>
<template>
<UDashboardNavbar
title="Dokumente"
>
</UDashboardNavbar>
<!-- <UDashboardToolbar>
<template #right>
<UButton @click="uploadModalOpen = true">Hochladen</UButton>
&lt;!&ndash; <UButton
@click="downloadSelected"
:disabled="dataStore.documents.filter(doc => doc.selected).length === 0"
>Herunterladen</UButton>&ndash;&gt;
</template>
&lt;!&ndash; <template #right>
<USelectMenu
:options="tags"
v-model="selectedTags"
class="w-40"
>
<template #label>
{{selectedTags}}
</template>
</USelectMenu>
</template>&ndash;&gt;
</UDashboardToolbar>-->
<UDashboardToolbar>
<template #left>
<UBreadcrumb
:links="breadcrumbLinks"
/>
</template>
<template #right>
<UButton @click="uploadModalOpen = true">Hochladen</UButton>
</template>
</UDashboardToolbar>
<div id="drop_zone" class="h-full scrollList">
<UDashboardPanelContent v-if="!isDragTarget" >
<div class="flex flex-row w-full flex-wrap" v-if="currentFolders.length > 0">
<a
class="w-1/6 folderIcon flex flex-col p-5 m-2"
v-for="folder in currentFolders"
@click="changePath(folder.path)"
>
<UIcon
name="i-heroicons-folder"
class="w-20 h-20"
/>
<span class="text-center truncate">{{folder.name}}</span>
</a>
</div>
<UDivider class="my-5" v-if="currentFolders.length > 0">Dokumente</UDivider>
<div v-if="!loadingDocs">
<DocumentList
v-if="filteredDocuments.length > 0"
:documents="filteredDocuments"
@selectDocument="(info) => console.log(info)"
/>
<UAlert
v-else
class="mt-5 w-1/2 mx-auto"
icon="i-heroicons-light-bulb"
title="Keine Dokumente vorhanden"
color="primary"
variant="outline"
/>
</div>
<UProgress
animation="carousel"
v-else
class="w-2/3 my-5 mx-auto"
/>
</UDashboardPanelContent>
<UCard
class=" m-5"
v-else>
<template #header>
<p class="mx-auto">Dateien zum hochladen hierher ziehen</p>
</template>
</UCard>
</div>
<USlideover
v-model="uploadModalOpen"
>
<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>
Datei Hochladen
</template>
<div class="h-full">
<UFormGroup
label="Datei:"
>
<UInput
type="file"
id="fileUploadInput"
multiple
/>
</UFormGroup>
<UFormGroup
label="Tags:"
class="mt-3"
>
<USelectMenu
multiple
searchable
searchable-placeholder="Suchen..."
:options="tags"
v-model="fileUploadFormData.tags"
/>
</UFormGroup>
</div>
<template #footer>
<UButton
v-if="!uploadInProgress"
class="mt-3"
@click="uploadFiles"
>Hochladen</UButton>
<UProgress
v-else
animation="carousel"
/>
</template>
</UCard>
</USlideover>
</template>
<style scoped>
.folderIcon {
border: 1px solid lightgrey;
border-radius: 10px;
color: dimgrey;
}
.folderIcon:hover {
border: 1px solid #69c350;
color: #69c350;
}
</style>

View File

@@ -1,11 +0,0 @@
<script setup>
</script>
<template>
<tiptap/>
</template>
<style scoped>
</style>

View File

@@ -2,6 +2,7 @@
const supabase = useSupabaseClient()
const dataStore = useDataStore()
const profileStore = useProfileStore()
const selectedTab = ref(0)
@@ -12,7 +13,7 @@ const accountData = ref(null)
const availableAccounts = ref(null)
const selectedAccount = ref(null)
const setupPage = async () => {
availableAccounts.value = (await supabase.from("emailAccounts").select("*").contains("profiles",[dataStore.activeProfile.id])).data
availableAccounts.value = (await supabase.from("emailAccounts").select("*").contains("profiles",[profileStore.activeProfile.id])).data
console.log(availableAccounts.value)
if(availableAccounts.value.length > 0) {

View File

@@ -2,6 +2,7 @@
const supabase = useSupabaseClient()
const dataStore = useDataStore()
const profileStore = useProfileStore()
const route = useRoute()
const router = useRouter()
const toast = useToast()
@@ -29,7 +30,7 @@ const setupPage = async () => {
} else {
emailData.value.account = emailAccounts.value[0].id
preloadedContent.value = `<p></p><p></p><p></p>${dataStore.activeProfile.emailSignature}`
preloadedContent.value = `<p></p><p></p><p></p>${profileStore.activeProfile.emailSignature || ""}`
//Check Query
if(route.query.to) emailData.value.to = route.query.to
@@ -39,18 +40,28 @@ const setupPage = async () => {
if(route.query.loadDocuments) {
//console.log(JSON.parse(route.query.loadDocuments))
const {data,error} = await supabase.from("documents").select('*, createdDocument(id,documentNumber,title,contact(email))').in('id',JSON.parse(route.query.loadDocuments))
console.log(JSON.parse(route.query.loadDocuments))
const data = await useFiles().selectSomeDocuments(JSON.parse(route.query.loadDocuments))
console.log(data)
if(error) console.log(error)
if(data) loadedDocuments.value = data
//console.log(loadedDocuments.value)
if(loadedDocuments.value.length > 0) {
emailData.value.subject = loadedDocuments.value[0].createdDocument.title
emailData.value.to = loadedDocuments.value[0].createdDocument.contact ? loadedDocuments.value[0].createdDocument.contact.email : ""
console.log(loadedDocuments.value[0])
emailData.value.subject = loadedDocuments.value[0].createddocument.title
if(loadedDocuments.value[0].createddocument.contact && loadedDocuments.value[0].createddocument.contact.email) {
console.log("Contact")
emailData.value.to = loadedDocuments.value[0].createddocument.contact.email
} else if(loadedDocuments.value[0].createddocument.customer && loadedDocuments.value[0].createddocument.customer.infoData.email) {
emailData.value.to = loadedDocuments.value[0].createddocument.customer.infoData.email
}
}
}
@@ -121,7 +132,7 @@ const sendEmail = async () => {
for await (const doc of loadedDocuments.value) {
const {data,error} = await supabase.storage.from("files").download(doc.path)
const {data,error} = await supabase.storage.from("filesdev").download(doc.path)
body.attachments.push({
filename: doc.path.split("/")[doc.path.split("/").length -1],
@@ -191,30 +202,39 @@ const sendEmail = async () => {
/>
</UFormGroup>
<UDivider class="my-3"/>
<UInput
class="w-full my-1"
placeholder="Empfänger"
variant="ghost"
v-model="emailData.to"
/>
<UInput
class="w-full my-1"
placeholder="Kopie"
variant="ghost"
v-model="emailData.cc"
/>
<UInput
class="w-full my-1"
placeholder="Blindkopie"
variant="ghost"
v-model="emailData.bcc"
/>
<UInput
placeholder="Betreff"
class="w-full my-1"
variant="ghost"
v-model="emailData.subject"
/>
<UFormGroup
label="Empfänger"
>
<UInput
class="w-full my-1"
v-model="emailData.to"
/>
</UFormGroup>
<UFormGroup
label="Kopie"
>
<UInput
class="w-full my-1"
v-model="emailData.cc"
/>
</UFormGroup>
<UFormGroup
label="Blindkopie"
>
<UInput
class="w-full my-1"
placeholder=""
v-model="emailData.bcc"
/>
</UFormGroup>
<UFormGroup
label="Betreff"
>
<UInput
class="w-full my-1"
v-model="emailData.subject"
/>
</UFormGroup>
</div>
</UDashboardToolbar>
@@ -227,7 +247,7 @@ const sendEmail = async () => {
class="mx-auto w-10 h-10"
/>
<span class="text-center text-2xl">Anhänge hochladen</span>-->
<input
<UInput
id="inputAttachments"
type="file"
multiple
@@ -242,13 +262,14 @@ const sendEmail = async () => {
class="list-disc"
v-for="doc in loadedDocuments"
>
<span v-if="doc.createdDocument">Dokument - {{doc.createdDocument.documentNumber}}</span>
<span v-if="doc.createddocument">Dokument - {{doc.createddocument.documentNumber}}</span>
</li>
</ul>
</div>
<Tiptap
class="mt-3"
@updateContent="contentChanged"
:preloadedContent="preloadedContent"
/>

View File

@@ -9,6 +9,7 @@ definePageMeta({
})
const dataStore = useDataStore()
const profileStore = useProfileStore()
const supabase = useSupabaseClient()
const user = useSupabaseUser()
const toast = useToast()
@@ -16,22 +17,29 @@ const toast = useToast()
const timeTypes = dataStore.getTimeTypes
const timeInfo = ref({
user: "",
start: "",
end: null,
profile: "",
startDate: "",
endDate: null,
notes: null,
projectId: null,
project: null,
type: null
})
const filterUser = ref(user.value.id || "")
const filterUser = ref(profileStore.activeProfile.id || "")
const times = ref([])
const setup = async () => {
times.value = await useSupabaseSelect("times","*, profile(*)")
}
setup()
const filteredRows = computed(() => {
let times = dataStore.times
//let times = times.value
if(dataStore.hasRight('viewTimes')) {
/*if(dataStore.hasRight('viewTimes')) {
if(filterUser.value !== "") {
times = times.filter(i => i.user === filterUser.value)
}
@@ -39,9 +47,9 @@ const filteredRows = computed(() => {
times = times.filter(i => i.user === user.value.id)
} else {
times = []
}
}*/
return times
return times.value
})
@@ -62,42 +70,34 @@ const columns = [
{
key:"state",
label: "Status",
sortable:true
},
{
key: "user",
label: "Benutzer",
sortable:true
},
{
key:"start",
key:"startDate",
label:"Start",
sortable:true
},
{
key: "endDate",
label: "Ende",
},
{
key:"type",
label:"Typ",
sortable:true
},
{
key: "end",
label: "Ende",
sortable:true
},
{
key: "duration",
label: "Dauer",
sortable:true
},
{
key: "projectId",
key: "project",
label: "Projekt",
sortable:true
},
{
key: "notes",
label: "Notizen",
sortable:true
}
]
@@ -109,9 +109,9 @@ const configTimeMode = ref("create")
const startTime = async () => {
console.log("started")
timeInfo.value.user = user.value.id
timeInfo.value.profile = profileStore.activeProfile.id
timeInfo.value.start = new Date().toISOString()
timeInfo.value.tenant = dataStore.currentTenant
timeInfo.value.tenant = profileStore.currentTenant
const {data,error} = await supabase
.from("times")
@@ -123,7 +123,7 @@ const startTime = async () => {
} else if(data) {
//timeInfo.value = data[0]
await dataStore.fetchTimes()
runningTimeInfo.value = dataStore.times.find(time => time.user === user.value.id && !time.end)
runningTimeInfo.value = dataStore.times.find(time => time.profile === profileStore.activeProfile.id && !time.end)
}
}
@@ -154,15 +154,15 @@ const stopStartedTime = async () => {
}
}
if(dataStore.times.find(time => time.user == user.value.id && !time.end)) {
runningTimeInfo.value = dataStore.times.find(time => time.user == user.value.id && !time.end)
if(times.value.find(time => time.profile == profileStore.activeProfile.id && !time.end)) {
runningTimeInfo.value = times.value.find(time => time.profile == profileStore.activeProfile.id && !time.end)
}
const createTime = async () => {
const {data,error} = await supabase
.from("times")
.insert({...itemInfo.value, tenant: dataStore.currentTenant})
.insert({...itemInfo.value, tenant: profileStore.currentTenant})
.select()
if(error) {
@@ -240,14 +240,13 @@ const setState = async (newState) => {
Erstellen
</UButton>
<USelectMenu
v-if="dataStore.hasRight('viewTimes')"
:options="dataStore.profiles"
:options="profileStore.profiles"
option-attribute="fullName"
value-attribute="id"
v-model="filterUser"
>
<template #label>
{{dataStore.getProfileById(filterUser) ? dataStore.getProfileById(filterUser).fullName : "Kein Benutzer ausgewählt"}}
{{profileStore.getProfileById(filterUser) ? profileStore.getProfileById(filterUser).fullName : "Kein Benutzer ausgewählt"}}
</template>
</USelectMenu>
</template>
@@ -339,14 +338,14 @@ const setState = async (newState) => {
label="Benutzer:"
>
<USelectMenu
:options="dataStore.profiles"
:options="profileStore.profiles"
v-model="itemInfo.user"
option-attribute="fullName"
value-attribute="id"
:disabled="(configTimeMode === 'create' ? false : itemInfo.state !== 'Entwurf') || (!dataStore.hasRight('createTime') || !dataStore.hasRight('createOwnTime'))"
>
<template #label>
{{dataStore.profiles.find(profile => profile.id === itemInfo.user) ? dataStore.profiles.find(profile => profile.id === itemInfo.user).fullName : "Benutzer auswählen"}}
{{profileStore.profiles.find(profile => profile.id === itemInfo.user) ? profileStore.profiles.find(profile => profile.id === itemInfo.user).fullName : "Benutzer auswählen"}}
</template>
</USelectMenu>
</UFormGroup>
@@ -444,7 +443,7 @@ const setState = async (newState) => {
>{{row.state}}</span>
</template>
<template #user-data="{row}">
{{dataStore.profiles.find(profile => profile.id === row.user) ? dataStore.profiles.find(profile => profile.id === row.user).fullName : row.user }}
{{profileStore.profiles.find(profile => profile.id === row.user) ? profileStore.profiles.find(profile => profile.id === row.user).fullName : row.user }}
</template>
<template #start-data="{row}">

View File

@@ -1,259 +0,0 @@
<script setup>
import dayjs from "dayjs";
const route = useRoute()
const router = useRouter()
const dataStore = useDataStore()
const mode = ref(route.params.mode || "show")
const itemInfo = ref({
resources: []
})
const oldItemInfo = ref({})
const resourceToAdd = ref(dataStore.activeProfile.id)
/*const mapResources = () => {
console.log(itemInfo.value.resources)
itemInfo.value.resources.map(resource => {
console.log(resource)
return {
id: resource.id,
type: resource.type
}
})
}*/
const setupPage = async () => {
if(mode.value === "show"){
itemInfo.value = await useSupabaseSelectSingle("events",route.params.id,"*, project(id,name)")
} else if(mode.value === "edit" && route.params.id) {
itemInfo.value = await useSupabaseSelectSingle("events",route.params.id,"*")
}
if(route.query.start) itemInfo.value.start = new Date(route.query.start.replace(" ", "+"))
if(route.query.end) itemInfo.value.end = new Date(route.query.end.replace(" ", "+"))
if(route.query.resources) {
itemInfo.value.resources = JSON.parse(route.query.resources).map(resource => {
return dataStore.getResourcesList.find(i => i.id === resource)
})
}
if(route.query.project) itemInfo.value.project = route.query.project
if(itemInfo.value.id) oldItemInfo.value = JSON.parse(JSON.stringify(itemInfo.value))
}
setupPage()
</script>
<template>
<UDashboardNavbar :title="mode === 'show' ? 'Termin: ' + itemInfo.title : 'Neuen Termin erstellen'">
<template #right>
<UButton
color="rose"
@click="router.push(route.params.id ? `/events/show/${route.params.id}` : `/events`)"
v-if="mode === 'edit'"
>
Abbrechen
</UButton>
<UButton
@click="dataStore.createNewItem('events',itemInfo)"
v-if="mode === 'edit' && !route.params.id"
>
Erstellen
</UButton>
<UButton
@click="dataStore.updateItem('events',itemInfo,oldItemInfo)"
v-else-if="mode === 'edit' && route.params.id"
>
Speichern
</UButton>
<UButton
v-if="mode === 'show'"
@click="router.push(`/events/edit/${itemInfo.id}`)"
>
Bearbeiten
</UButton>
</template>
</UDashboardNavbar>
<!-- <UDashboardToolbar>
</UDashboardToolbar>-->
<UTabs
v-if="mode === 'show'"
:items="[{label:'Informationen'},{label:'Logbuch'}]"
class="p-5"
>
<template #item="{item}">
<UCard class="mt-5">
<div v-if="item.label === 'Informationen'">
<div class="truncate" >
<p>Titel: {{itemInfo.title ? itemInfo.title : ''}}</p>
<p>Typ: {{itemInfo.type? itemInfo.type : ''}}</p>
<p>Start: {{dayjs(itemInfo.start).format("DD.MM.YY HH:mm")}}</p>
<p>Ende: {{dayjs(itemInfo.end).format("DD.MM.YY HH:mm")}}</p>
<p>Projekt: {{itemInfo.project ? itemInfo.project.name : ''}}</p>
<p>Link: <a v-if="itemInfo.link" :href="itemInfo.link">{{itemInfo.link }}</a></p>
<p>Resources: {{itemInfo.resources.map((x) => `${x.type} - ${x.title}`).join(",")}}</p>
<p>Notizen: {{itemInfo.notes ? itemInfo.notes : ''}}</p>
</div>
</div>
<div v-if="item.label === 'Logbuch'">
<HistoryDisplay
type="event"
v-if="itemInfo"
:element-id="itemInfo.id"
/>
</div>
</UCard>
</template>
</UTabs>
<UForm class="p-5" v-if="mode === 'edit'">
<UFormGroup
label="Titel:"
>
<UInput
v-model="itemInfo.title"
/>
</UFormGroup>
<UFormGroup
label="Projekt:"
>
<USelectMenu
v-model="itemInfo.project"
:options="dataStore.projects"
option-attribute="name"
value-attribute="id"
searchable
searchable-placeholder="Suche..."
:search-attributes="['name']"
>
<template #label>
{{dataStore.getProjectById(itemInfo.project) ? dataStore.getProjectById(itemInfo.project).name : "Kein Projekt ausgewählt"}}
</template>
</USelectMenu>
</UFormGroup>
<UFormGroup
label="Typ:"
>
<USelectMenu
v-model="itemInfo.type"
:options="dataStore.getEventTypes"
option-attribute="label"
value-attribute="label"
>
</USelectMenu>
</UFormGroup>
<UFormGroup
label="Link:"
>
<UInput
v-model="itemInfo.link"
/>
</UFormGroup>
<UFormGroup
label="Notizen:"
>
<UTextarea
v-model="itemInfo.notes"
rows="3"
/>
</UFormGroup>
<UFormGroup
label="Start:"
>
<UPopover :popper="{ placement: 'bottom-start' }">
<UButton
variant="outline"
icon="i-heroicons-calendar-days-20-solid"
:label="itemInfo.start ? dayjs(itemInfo.start).format('DD.MM.YYYY HH:mm') : 'Datum auswählen'"
/>
<template #panel="{ close }">
<LazyDatePicker
v-model="itemInfo.start"
mode="dateTime"
/>
</template>
</UPopover>
</UFormGroup>
<UFormGroup
label="Ende:"
>
<UPopover :popper="{ placement: 'bottom-start' }">
<UButton
variant="outline"
icon="i-heroicons-calendar-days-20-solid"
:label="itemInfo.end ? dayjs(itemInfo.end).format('DD.MM.YYYY HH:mm') : 'Datum auswählen'"
/>
<template #panel="{ close }">
<LazyDatePicker
v-model="itemInfo.end"
mode="dateTime"
/>
</template>
</UPopover>
</UFormGroup>
<UFormGroup
label="Resource:"
class="w-full"
>
<InputGroup class="w-full">
<USelectMenu
v-model="resourceToAdd"
:options="dataStore.getResourcesList"
option-attribute="title"
value-attribute="id"
class="w-full"
></USelectMenu>
<UButton
@click="itemInfo.resources.push(dataStore.getResourcesList.find(i => i.id === resourceToAdd))"
:disabled="itemInfo.resources.find(i => i.id === resourceToAdd)"
>
+ Hinzufügen
</UButton>
</InputGroup>
</UFormGroup>
<UTable
v-if="itemInfo.resources.length > 0"
:rows="itemInfo.resources"
:columns="[
{
key:'type',
label: 'Type'
}, {
key: 'title',
label: 'Name'
}, {
key: 'remove'
}
]"
>
<template #remove-data="{row}">
<UButton
color="rose"
variant="outline"
@click="itemInfo.resources = itemInfo.resources.filter(i => i.id !== row.id)"
>
Entfernen
</UButton>
</template>
</UTable>
</UForm>
</template>
<style scoped>
</style>

View File

@@ -1,127 +0,0 @@
<template>
<UDashboardNavbar title="Termine" :badge="filteredRows.length">
<template #right>
<UInput
id="searchinput"
v-model="searchString"
icon="i-heroicons-funnel"
autocomplete="off"
placeholder="Suche..."
class="hidden lg:block"
@keydown.esc="$event.target.blur()"
>
<template #trailing>
<UKbd value="/" />
</template>
</UInput>
<UButton @click="router.push(`/events/edit`)">+ Termin</UButton>
</template>
</UDashboardNavbar>
<UDashboardToolbar>
<template #right>
<USelectMenu
v-model="selectedColumns"
icon="i-heroicons-adjustments-horizontal-solid"
:options="templateColumns"
multiple
class="hidden lg:block"
by="key"
>
<template #label>
Spalten
</template>
</USelectMenu>
</template>
</UDashboardToolbar>
<UTable
:rows="filteredRows"
:columns="columns"
class="w-full"
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
@select="(i) => router.push(`/events/show/${i.id}`) "
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Termine anzuzeigen' }"
>
<template #start-data="{row}">
{{dayjs(row.start).format("DD.MM.YY HH:mm")}}
</template>
<template #end-data="{row}">
{{dayjs(row.end).format("DD.MM.YY HH:mm")}}
</template>
<template #project-data="{row}">
{{row.project ? dataStore.getProjectById(row.project).name: ""}}
</template>
<template #resources-data="{row}">
{{row.resources ? row.resources.map(i => i.title).join(", ") : ""}}
</template>
</UTable>
</template>
<script setup>
import dayjs from "dayjs";
definePageMeta({
middleware: "auth"
})
defineShortcuts({
'/': () => {
//console.log(searchinput)
//searchinput.value.focus()
document.getElementById("searchinput").focus()
},
'+': () => {
router.push("/events/[mode]")
}
})
const dataStore = useDataStore()
const router = useRouter()
const templateColumns = [
{
key: 'title',
label: "Titel:",
sortable: true
},
{
key: "start",
label: "Start",
sortable: true
},
{
key: "end",
label: "Ende"
},
{
key: "resources",
label: "Resourcen"
},
{
key: "project",
label: "Projekt"
}
]
const selectedColumns = ref(templateColumns)
const columns = computed(() => templateColumns.filter((column) => selectedColumns.value.includes(column)))
const searchString = ref('')
const filteredRows = computed(() => {
if(!searchString.value) {
return dataStore.events
}
return dataStore.events.filter(item => {
return Object.values(item).some((value) => {
return String(value).toLowerCase().includes(searchString.value.toLowerCase())
})
})
})
</script>
<style scoped>
</style>

550
pages/files/index.vue Normal file
View File

@@ -0,0 +1,550 @@
<script setup>
import {BlobReader, BlobWriter, ZipWriter} from "@zip.js/zip.js";
import {useSupabaseSelectSingle} from "~/composables/useSupabase.js";
import DocumentDisplayModal from "~/components/DocumentDisplayModal.vue";
import DocumentUploadModal from "~/components/DocumentUploadModal.vue";
import dayjs from "dayjs";
definePageMeta({
middleware: "auth"
})
defineShortcuts({
/*'/': () => {
//console.log(searchinput)
//searchinput.value.focus()
document.getElementById("searchinput").focus()
},*/
'+': () => {
//Hochladen
uploadModalOpen.value = true
},
'Enter': {
usingInput: true,
handler: () => {
let entry = renderedFileList.value[selectedFileIndex.value]
if(entry.type === "file") {
showFile(entry.id)
console.log(entry)
} else {
changeFolder(currentFolders.value.find(i => i.id === entry.id))
}
}
},
'arrowdown': () => {
if(selectedFileIndex.value < renderedFileList.value.length - 1) {
selectedFileIndex.value += 1
} else {
selectedFileIndex.value = 0
}
},
'arrowup': () => {
if(selectedFileIndex.value === 0) {
selectedFileIndex.value = renderedFileList.value.length - 1
} else {
selectedFileIndex.value -= 1
}
}
})
const dataStore = useDataStore()
const profileStore = useProfileStore()
const supabase = useSupabaseClient()
const router = useRouter()
const route = useRoute()
const slideover = useSlideover()
const modal = useModal()
dataStore.fetchDocuments()
const uploadModalOpen = ref(false)
const createFolderModalOpen = ref(false)
const uploadInProgress = ref(false)
const fileUploadFormData = ref({
tags: ["Eingang"],
path: "",
tenant: profileStore.currentTenant,
folder: null
})
const files = useFiles()
let tags = dataStore.getDocumentTags
const displayMode = ref("list")
const displayModes = ref([{label: 'Liste',key:'list', icon: 'i-heroicons-list-bullet'},{label: 'Kacheln',key:'rectangles', icon: 'i-heroicons-squares-2x2'}])
const documents = ref([])
const folders = ref([])
const filetags = ref([])
const currentFolder = ref(null)
const loadingDocs = ref(false)
const isDragTarget = ref(false)
const setupPage = async () => {
folders.value = await useSupabaseSelect("folders")
documents.value = await files.selectDocuments()
filetags.value = await useSupabaseSelect("filetags")
if(route.query) {
if(route.query.folder) {
currentFolder.value = await useSupabaseSelectSingle("folders", route.query.folder)
}
}
const dropZone = document.getElementById("drop_zone")
dropZone.ondragover = function (event) {
console.log(event)
isDragTarget.value = true
event.preventDefault()
}
dropZone.ondragleave = function (event) {
isDragTarget.value = false
}
dropZone.ondrop = async function (event) {
console.log("files dropped")
event.preventDefault()
await uploadFiles(event.dataTransfer.files)
isDragTarget.value = false
setupPage()
}
loadingDocs.value = false
}
setupPage()
const currentFolders = computed(() => {
if(folders.value.length > 0) {
let tempFolders = folders.value.filter(i => currentFolder.value ? i.parent === currentFolder.value.id : !i.parent)
return tempFolders
} else return []
})
const breadcrumbLinks = computed(() => {
if(currentFolder.value) {
let parents = []
const addParent = (parent) => {
parents.push(parent)
if(parent.parent) {
addParent(folders.value.find(i => i.id === parent.parent))
}
}
if(currentFolder.value.parent) {
addParent(folders.value.find(i => i.id === currentFolder.value.parent))
}
return [{
label: "Home",
click: () => {
changeFolder(null)
},
icon: "i-heroicons-folder"
},
...parents.map(i => {
return {
label: folders.value.find(x => x.id === i.id).name,
click: () => {
changeFolder(i)
},
icon: "i-heroicons-folder"
}
}).reverse(),
{
label: currentFolder.value.name,
click: () => {
changeFolder(currentFolder.value)
},
icon: "i-heroicons-folder"
}]
} else {
return [{
label: "Home",
click: () => {
changeFolder(null)
},
icon: "i-heroicons-folder"
}]
}
})
const filteredDocuments = computed(() => {
return documents.value.filter(i => currentFolder.value ? i.folder === currentFolder.value.id : !i.folder)
})
const changeFolder = async (newFolder) => {
loadingDocs.value = true
currentFolder.value = newFolder
if(newFolder) {
fileUploadFormData.value.folder = newFolder.id
await router.push(`/files?folder=${newFolder.id}`)
} else {
fileUploadFormData.value.folder = null
await router.push(`/files`)
}
setupPage()
}
const createFolderData = ref({})
const createFolder = async () => {
const {data,error} = await supabase
.from("folders")
.insert({
tenant: profileStore.currentTenant,
parent: currentFolder.value ? currentFolder.value.id : undefined,
name: createFolderData.value.name,
})
createFolderModalOpen.value = false
setupPage()
}
const uploadFiles = async (files) => {
uploadInProgress.value = true;
if(files) {
//await dataStore.uploadFiles({tags: ["Ablage"],tenant: profileStore.currentTenant,folder: currentFolder.value.id}, files, true)
await dataStore.uploadFiles({tags: ["Ablage"],tenant: profileStore.currentTenant}, files, true)
} else {
await dataStore.uploadFiles(fileUploadFormData.value, document.getElementById("fileUploadInput").files, true)
}
uploadModalOpen.value = false;
uploadInProgress.value = false;
}
const downloadSelected = async () => {
const bucket = "files";
let files = []
dataStore.documents.filter(doc => doc.selected).forEach(doc => files.push(doc.path))
console.log(files)
// If there are no files in the folder, throw an error
if (!files || !files.length) {
throw new Error("No files to download");
}
const promises = [];
// Download each file in the folder
files.forEach((file) => {
promises.push(
supabase.storage.from(bucket).download(`${file}`)
);
});
// Wait for all the files to download
const response = await Promise.allSettled(promises);
// Map the response to an array of objects containing the file name and blob
const downloadedFiles = response.map((result, index) => {
if (result.status === "fulfilled") {
console.log(files[index].split("/")[files[index].split("/").length -1])
return {
name: files[index].split("/")[files[index].split("/").length -1],
blob: result.value.data,
};
}
});
// Create a new zip file
const zipFileWriter = new BlobWriter("application/zip");
const zipWriter = new ZipWriter(zipFileWriter, { bufferedWrite: true });
// Add each file to the zip file
downloadedFiles.forEach((downloadedFile) => {
if (downloadedFile) {
zipWriter.add(downloadedFile.name, new BlobReader(downloadedFile.blob));
}
});
// Download the zip file
const url = URL.createObjectURL(await zipWriter.close());
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", "documents.zip");
document.body.appendChild(link);
link.click();
}
const renderedFileList = computed(() => {
let files = filteredDocuments.value.map(i => {
return {
label: i.path.split("/")[i.path.split("/").length -1],
id: i.id,
type: "file"
}
})
let folders = currentFolders.value.map(i => {
return {
label: i.name,
id: i.id,
type: "folder"
}
})
return [...folders,...files]
})
const selectedFileIndex = ref(0)
const showFile = (fileId) => {
console.log(fileId)
modal.open(DocumentDisplayModal,{
documentData: documents.value.find(i => i.id === fileId),
})
}
</script>
<template>
<UDashboardNavbar
title="Dateien"
>
</UDashboardNavbar>
<UDashboardToolbar>
<template #left>
<UBreadcrumb
:links="breadcrumbLinks"
/>
</template>
<template #right>
<USelectMenu
:options="displayModes"
value-attribute="key"
option-attribute="label"
v-model="displayMode"
:ui-menu="{ width: 'min-w-max'}"
>
<template #label>
<UIcon class="w-5 h-5" :name="displayModes.find(i => i.key === displayMode).icon"/>
</template>
</USelectMenu>
<UButton @click="modal.open(DocumentUploadModal,{fileData: {folder: currentFolder.id}})">+ Hochladen</UButton>
<UButton
@click="createFolderModalOpen = true"
variant="outline"
>+ Ordner</UButton>
<UModal
v-model="createFolderModalOpen"
>
<UCard>
<template #header>
Ordner Erstellen
</template>
<UFormGroup
label="Ordner erstellen"
>
<UInput
v-model="createFolderData.name"
/>
</UFormGroup>
<template #footer>
<UButton
@click="createFolder"
>
Erstellen
</UButton>
</template>
</UCard>
</UModal>
</template>
</UDashboardToolbar>
<div id="drop_zone" class="h-full scrollList">
<UDashboardPanelContent v-if="!isDragTarget" >
<div v-if="displayMode === 'list'">
<table class="w-full">
<thead>
<tr>
<td class="font-bold">Name</td>
<td class="font-bold">Erstellt am</td>
</tr>
</thead>
<tr v-for="(entry,index) in renderedFileList">
<td>
<UIcon class="mr-1" :name="entry.type === 'folder' ? 'i-heroicons-folder' : 'i-heroicons-document'"/>
<a
style="cursor: pointer"
:class="[...index === selectedFileIndex ? ['text-primary', 'text-xl'] : ['dark:text-white','text-black','text-xl']]"
@click="entry.type === 'folder' ? changeFolder(currentFolders.find(i => i.id === entry.id)) : showFile(entry.id)"
>{{entry.label}}</a>
</td>
<td>
<span v-if="entry.type === 'file'" class="text-xl">{{dayjs(documents.find(i => i.id === entry.id).created_at).format("DD.MM.YY HH:mm")}}</span>
<span v-if="entry.type === 'folder'" class="text-xl">{{dayjs(currentFolders.find(i => i.id === entry.id).created_at).format("DD.MM.YY HH:mm")}}</span>
</td>
</tr>
</table>
</div>
<div v-else-if="displayMode === 'rectangles'">
<div class="flex flex-row w-full flex-wrap" v-if="currentFolders.length > 0">
<a
class="w-1/6 folderIcon flex flex-col p-5 m-2"
v-for="folder in currentFolders"
@click="changeFolder(folder)"
>
<UIcon
name="i-heroicons-folder"
class="w-20 h-20"
/>
<span class="text-center truncate">{{folder.name}}</span>
</a>
</div>
<UDivider class="my-5" v-if="currentFolder">{{currentFolder.name}}</UDivider>
<UDivider class="my-5" v-else>Ablage</UDivider>
<div v-if="!loadingDocs">
<DocumentList
v-if="filteredDocuments.length > 0"
:documents="filteredDocuments"
@selectDocument="(info) => console.log(info)"
/>
<UAlert
v-else
class="mt-5 w-1/2 mx-auto"
icon="i-heroicons-light-bulb"
title="Keine Dokumente vorhanden"
color="primary"
variant="outline"
/>
</div>
<UProgress
animation="carousel"
v-else
class="w-2/3 my-5 mx-auto"
/>
</div>
</UDashboardPanelContent>
<UCard
class=" m-5"
v-else>
<template #header>
<p class="mx-auto">Dateien zum hochladen hierher ziehen</p>
</template>
</UCard>
</div>
<USlideover
v-model="uploadModalOpen"
>
<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>
Datei Hochladen
</template>
<div class="h-full">
<UFormGroup
label="Datei:"
>
<UInput
type="file"
id="fileUploadInput"
multiple
/>
</UFormGroup>
<UFormGroup
label="Tags:"
class="mt-3"
>
<USelectMenu
multiple
searchable
searchable-placeholder="Suchen..."
option-attribute="name"
value-attribute="id"
:options="filetags"
v-model="fileUploadFormData.tags"
/>
</UFormGroup>
</div>
<template #footer>
<UButton
v-if="!uploadInProgress"
class="mt-3"
@click="uploadFiles"
>Hochladen</UButton>
<UProgress
v-else
animation="carousel"
/>
</template>
</UCard>
</USlideover>
</template>
<style scoped>
.folderIcon {
border: 1px solid lightgrey;
border-radius: 10px;
color: dimgrey;
}
.folderIcon:hover {
border: 1px solid #69c350;
color: #69c350;
}
tr:nth-child(odd) {
background-color: rgba(0, 0, 0, 0.05);
}
</style>

View File

@@ -0,0 +1,20 @@
<script setup lang="ts">
</script>
<template>
<UDashboardNavbar>
<template #center>
<h1
:class="['text-xl','font-medium']"
>Zentrales Logbuch</h1>
</template>
</UDashboardNavbar>
<UDashboardPanelContent>
<HistoryDisplay/>
</UDashboardPanelContent>
</template>
<style scoped>
</style>

View File

@@ -8,21 +8,22 @@ definePageMeta({
})
const dataStore = useDataStore()
const profileStore = useProfileStore()
const supabase = useSupabaseClient()
const route = useRoute()
const router = useRouter()
const toast = useToast()
const {vendors} = storeToRefs(useDataStore())
const {fetchVendorInvoices} = useDataStore()
const availableDocuments = ref([])
const setup = async () => {
let filetype = (await supabase.from("filetags").select().eq("tenant",profileStore.currentTenant).eq("incomingDocumentType","invoices").single()).data.id
console.log(filetype)
let ids = (await supabase.from("files").select("id").eq("tenant",profileStore.currentTenant).eq("type", filetype)).data.map(i => i.id)
availableDocuments.value = await useFiles().selectSomeDocuments(ids)
const availableDocuments = computed(() => {
}
//console.log(dataStore.documents.filter(i => i.tags.includes('Eingangsrechnung') && !dataStore.incominginvoices.find(x => x.document === i.id)))
//console.log(dataStore.documents.filter(i => i.tags.includes('Eingangsrechnung')).length)
return dataStore.documents.filter(i => i.tags.includes('Eingangsrechnung') && !i.tags.includes("Archiviert") && !dataStore.incominginvoices.find(x => x.document === i.id))
})
setup()
//let currentDocument = ref(null)
@@ -56,7 +57,7 @@ const itemInfo = ref({
//Functions
const setupPage = async () => {
if((mode.value === "show" || mode.value === "edit" ) && route.params.id){
itemInfo.value = await dataStore.getIncomingInvoiceById(Number(route.params.id))
itemInfo.value = await useSupabaseSelectSingle("incominginvoices",route.params.id)
//currentDocument.value = await dataStore.getDocumentById(currentVendorInvoice.value.document)
}
console.log(itemInfo.value)

View File

@@ -0,0 +1,479 @@
<script setup>
import InputGroup from "~/components/InputGroup.vue";
import dayjs from "dayjs";
import HistoryDisplay from "~/components/HistoryDisplay.vue";
definePageMeta({
middleware: "auth"
})
const dataStore = useDataStore()
const profileStore = useProfileStore()
const supabase = useSupabaseClient()
const route = useRoute()
const router = useRouter()
const toast = useToast()
const itemInfo = ref({
vendor: 0,
expense: true,
reference: "",
date: null,
dueDate: null,
paymentType: "Überweisung",
description: "",
state: "Entwurf",
accounts: [
{
account: null,
amountNet: null,
amountTax: null,
taxType: "19",
costCentre: null
}
]
})
const availableDocuments = ref([])
const setup = async () => {
let filetype = (await supabase.from("filetags").select().eq("tenant",profileStore.currentTenant).eq("incomingDocumentType","invoices").single()).data.id
console.log(filetype)
let ids = (await supabase.from("files").select("id").eq("tenant",profileStore.currentTenant).eq("type", filetype).is("incominginvoice",null)).data.map(i => i.id)
availableDocuments.value = await useFiles().selectSomeDocuments(ids)
}
setup()
const useNetMode = ref(false)
const loadedFile = ref(null)
const loadFile = async (id) => {
console.log(id)
loadedFile.value = await useFiles().selectDocument(id)
console.log(loadedFile.value)
}
const changeNetMode = (mode) => {
useNetMode.value = mode
itemInfo.value.accounts = [{account: null,amountNet: null,amountTax: null,taxType: '19'}]
}
const taxOptions = ref([
{
label: "19% USt",
percentage: 19,
key: "19"
},{
label: "7% USt",
percentage: 7,
key: "7"
},{
label: "Innergemeintschaftlicher Erwerb 19%",
percentage: 0,
key: "19I"
},{
label: "Innergemeintschaftlicher Erwerb 7%",
percentage: 0,
key: "7I"
},{
label: "§13b UStG",
percentage: 0,
key: "13B"
},{
label: "Keine USt",
percentage: 0,
key: "null"
},
])
const totalCalculated = computed(() => {
let totalNet = 0
let totalAmount19Tax = 0
let totalAmount7Tax = 0
let totalAmount0Tax = 0
let totalGross = 0
itemInfo.value.accounts.forEach(account => {
if(account.amountNet) totalNet += account.amountNet
if(account.taxType === "19" && account.amountTax) {
totalAmount19Tax += account.amountTax
}
})
totalGross = Number(totalNet + totalAmount19Tax)
return {
totalNet,
totalAmount19Tax,
totalGross
}
})
const createIncomingInvoice = async () => {
const data = await dataStore.createNewItem('incominginvoices',itemInfo.value)
console.log(data)
const {error} = await supabase.from("files").update({incominginvoice: data[0].id}).eq("id",loadedFile.value.id)
}
</script>
<template>
<UDashboardNavbar :title="'Eingangsbeleg erstellen'">
<template #right>
<UButton
@click="createIncomingInvoice"
>
Speichern
</UButton>
</template>
</UDashboardNavbar>
<UDashboardPanelContent>
<div v-if="!loadedFile">
<DocumentList
:documents="availableDocuments"
:return-document-id="true"
@selectDocument="(documentId) => loadFile(documentId)"
/>
</div>
<div
v-else
class="flex justify-between mt-5"
>
<object
v-if="loadedFile"
:data="loadedFile.url + '#toolbar=0&navpanes=0&scrollbar=0&statusbar=0&messages=0&scrollbar=0'"
type="application/pdf"
class="mx-5 documentPreview w-full"
/>
<div class="w-3/5 mx-5">
<div v-if="mode === 'show'">
<div class="truncate mb-5">
<p>Status: {{itemInfo.state}}</p>
<p>Datum: {{dayjs(itemInfo.date).format('DD.MM.YYYY')}}</p>
<p>Fälligkeitsdatum: {{dayjs(itemInfo.dueDate).format('DD.MM.YYYY')}}</p>
<p>Lieferant: <nuxt-link :to="`/vendors/show/${itemInfo.vendor}`">{{dataStore.getVendorById(itemInfo.vendor).name}}</nuxt-link></p>
<p>Bezahlt: {{itemInfo.paid}}</p>
<p>Beschreibung: {{itemInfo.description}}</p>
<!-- TODO: Buchungszeilen darstellen -->
</div>
<HistoryDisplay
type="incomingInvoice"
v-if="itemInfo"
:element-id="itemInfo.id"
:render-headline="true"
/>
</div>
<div v-else class=" scrollContainer">
<InputGroup class="mb-3">
<UButton
:variant="itemInfo.expense ? 'solid' : 'outline'"
@click="itemInfo.expense = true"
>
Ausgabe
</UButton>
<UButton
:variant="!itemInfo.expense ? 'solid' : 'outline'"
@click="itemInfo.expense = false"
>
Einnahme
</UButton>
</InputGroup>
<UFormGroup label="Lieferant:" required>
<InputGroup>
<USelectMenu
v-model="itemInfo.vendor"
:options="dataStore.vendors"
option-attribute="name"
value-attribute="id"
searchable
:search-attributes="['name','vendorNumber']"
class="flex-auto"
searchable-placeholder="Suche..."
>
<template #option="{option}">
{{option.vendorNumber}} - {{option.name}}
</template>
<template #label>
{{dataStore.vendors.find(vendor => vendor.id === itemInfo.vendor) ? dataStore.vendors.find(vendor => vendor.id === itemInfo.vendor).name : 'Lieferant auswählen'}}
</template>
</USelectMenu>
<UButton
@click="router.push('/vendors/create')"
>
+ Lieferant
</UButton>
</InputGroup>
</UFormGroup>
<UFormGroup
class="mt-3"
label="Rechnungsreferenz:"
required
>
<UInput
v-model="itemInfo.reference"
/>
</UFormGroup>
<InputGroup class="mt-3" gap="2">
<UFormGroup label="Rechnungsdatum:" required>
<UPopover :popper="{ placement: 'bottom-start' }">
<UButton icon="i-heroicons-calendar-days-20-solid" :label="itemInfo.date ? dayjs(itemInfo.date).format('DD.MM.YYYY') : 'Datum auswählen'" />
<template #panel="{ close }">
<LazyDatePicker v-model="itemInfo.date" @close="close" />
</template>
</UPopover>
</UFormGroup>
<UFormGroup label="Fälligkeitsdatum:" required>
<UPopover :popper="{ placement: 'bottom-start' }">
<UButton icon="i-heroicons-calendar-days-20-solid" :label="itemInfo.dueDate ? dayjs(itemInfo.dueDate).format('DD.MM.YYYY') : 'Datum auswählen'" />
<template #panel="{ close }">
<LazyDatePicker v-model="itemInfo.dueDate" @close="close" />
</template>
</UPopover>
</UFormGroup>
</InputGroup>
<UFormGroup label="Zahlart:" required>
<USelectMenu
:options="['Einzug','Kreditkarte','Überweisung','Sonstiges']"
v-model="itemInfo.paymentType"
/>
</UFormGroup>
<UFormGroup label="Beschreibung:" required>
<UTextarea
v-model="itemInfo.description"
/>
</UFormGroup>
<InputGroup class="my-3">
<UButton
:variant="!useNetMode ? 'solid' : 'outline'"
@click="changeNetMode(false)"
>
Brutto
</UButton>
<UButton
:variant="useNetMode ? 'solid' : 'outline'"
@click="changeNetMode(true)"
>
Netto
</UButton>
<!-- Brutto
<UToggle
v-model="useNetMode"
@update:model-value="itemInfo.accounts = [{account: null,amountNet: null,amountTax: null,taxType: '19'}]"
/>
Netto-->
</InputGroup>
<table v-if="itemInfo.accounts.length > 1" class="w-full">
<tr>
<td>Gesamt exkl. Steuer: </td>
<td class="text-right">{{totalCalculated.totalNet.toFixed(2).replace(".",",")}} </td>
</tr>
<tr>
<td>19% Steuer: </td>
<td class="text-right">{{totalCalculated.totalAmount19Tax.toFixed(2).replace(".",",")}} </td>
</tr>
<tr>
<td>Gesamt inkl. Steuer: </td>
<td class="text-right">{{totalCalculated.totalGross.toFixed(2).replace(".",",")}} </td>
</tr>
</table>
<div
class="my-3"
v-for="(item,index) in itemInfo.accounts"
>
<UFormGroup
label="Kategorie"
class=" mb-3"
>
<USelectMenu
:options="dataStore.accounts"
option-attribute="label"
value-attribute="id"
searchable
:search-attributes="['label']"
searchable-placeholder="Suche..."
v-model="item.account"
>
<template #label>
{{dataStore.accounts.find(account => account.id === item.account) ? dataStore.accounts.find(account => account.id === item.account).label : "Keine Kategorie ausgewählt" }}
</template>
</USelectMenu>
</UFormGroup>
<UFormGroup
label="Kostenstelle"
class=" mb-3"
>
<USelectMenu
:options="dataStore.getCostCentresComposed"
option-attribute="label"
value-attribute="id"
searchable
:search-attributes="['label']"
searchable-placeholder="Suche..."
v-model="item.costCentre"
>
<template #label>
{{dataStore.getCostCentresComposed.find(account => account.id === item.costCentre) ? dataStore.getCostCentresComposed.find(account => account.id === item.costCentre).label : "Keine Kostenstelle ausgewählt" }}
</template>
</USelectMenu>
</UFormGroup>
<InputGroup>
<UFormGroup
label="Umsatzsteuer"
class="w-32"
:help="`Betrag: ${item.amountTax ? String(item.amountTax).replace('.',',') : '0,00'} €`"
>
<USelectMenu
:options="taxOptions"
v-model="item.taxType"
value-attribute="key"
option-attribute="label"
@change="item.amountTax = Number(((item.amountNet ? item.amountNet : 0) * (Number(taxOptions.find(i => i.key === item.taxType).percentage)/100)).toFixed(2))"
>
<template #label>
<span class="truncate">{{taxOptions.find(i => i.key === item.taxType) ? taxOptions.find(i => i.key === item.taxType).label : ""}}</span>
</template>
</USelectMenu>
</UFormGroup>
<UFormGroup
v-if="useNetMode"
label="Gesamtbetrag exkl. Steuer in EUR"
class="flex-auto truncate"
:help="item.taxType !== null ? `Betrag inkl. Steuern: ${String(Number(item.amountNet + item.amountTax).toFixed(2)).replace('.',',')} €` : 'Zuerst Steuertyp festlegen' "
>
<UInput
type="number"
step="0.01"
v-model="item.amountNet"
:disabled="item.taxType === null"
@keyup="item.amountTax = Number((item.amountNet * (Number(taxOptions.find(i => i.key === item.taxType).percentage)/100)).toFixed(2))"
>
<template #trailing>
<span class="text-gray-500 dark:text-gray-400 text-xs">EUR</span>
</template>
</UInput>
</UFormGroup>
<UFormGroup
v-else
label="Gesamtbetrag inkl. Steuer in EUR"
class="flex-auto"
:help="item.taxType !== null ? `Betrag exkl. Steuern: ${item.amountNet ? String(item.amountNet.toFixed(2)).replace('.',',') : '0,00'} €` : 'Zuerst Steuertyp festlegen' "
>
<UInput
type="number"
step="0.01"
:disabled="item.taxType === null"
v-model="item.amountGross"
@keyup="item.amountNet = Number((item.amountGross / (1 + Number(item.taxType)/100)).toFixed(2)),
item.amountTax = Number((item.amountGross - item.amountNet).toFixed(2))"
>
<template #trailing>
<span class="text-gray-500 dark:text-gray-400 text-xs">EUR</span>
</template>
</UInput>
</UFormGroup>
</InputGroup>
<UButton
class="mt-3"
@click="itemInfo.accounts = [...itemInfo.accounts.slice(0,index+1),{account:null, amountNet: null, amountTax:null, taxType: '19'} , ...itemInfo.accounts.slice(index+1)]"
>
Position hinzufügen
</UButton>
<UButton
v-if="index !== 0"
class="mt-3"
variant="ghost"
color="rose"
@click="itemInfo.accounts = itemInfo.accounts.filter((account,itemIndex) => itemIndex !== index)"
>
Position entfernen
</UButton>
</div>
</div>
</div>
</div>
</UDashboardPanelContent>
</template>
<style scoped>
.documentPreview {
aspect-ratio: 1 / 1.414;
}
.scrollContainer {
overflow-y: scroll;
padding-left: 1em;
padding-right: 1em;
height: 75vh;
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
.scrollContainer::-webkit-scrollbar {
display: none;
}
.lineItemRow {
display: flex;
flex-direction: row;
}
</style>

View File

@@ -133,7 +133,7 @@ const isPaid = (item) => {
</template>
</UInput>
<UButton @click="router.push(`/incomingInvoices/edit`)">+ Beleg</UButton>
<UButton @click="router.push(`/incomingInvoices/create`)">+ Beleg</UButton>
</template>
</UDashboardNavbar>
<UDashboardToolbar>

View File

@@ -15,48 +15,28 @@
<UDashboardPanelContent>
<UDashboardCard
class="mt-3"
title="Anwesenheiten"
v-if="dataStore.getStartedWorkingTimes().length > 0"
>
<p v-for="time in dataStore.getStartedWorkingTimes()"><UIcon name="i-heroicons-check"/>{{dataStore.getProfileById(time.profile).fullName}}</p>
<display-present-profiles/>
</UDashboardCard>
<!--TODO: Fix Card Table overflowing <UDashboardCard
title="Offene Aufgaben"
v-if="dataStore.getOpenTasksCount > 0"
class="w-1/2 h-1/2"
>
<UTable
:rows="dataStore.tasks.filter(i => i.categorie !== 'Erledigt' && (i.profile === dataStore.activeProfile.id ||!i.profile))"
@select="(row) => router.push(`/tasks/show/${row.id}`)"
:columns="[
{
key: 'categorie',
label: 'Kategorie'
},{
key: 'name',
label: 'Name'
},
]"
>
<template #categorie-data="{row}">
<span v-if="row.categorie === 'Dringend'" class="text-rose-500">{{row.categorie}}</span>
<span v-else-if="row.categorie === 'In Bearbeitung'" class="text-primary-500">{{row.categorie}}</span>
<span v-else>{{row.categorie}}</span>
</template>
</UTable>
</UDashboardCard>-->
<UDashboardCard
:ui="{ body: { padding: '!pb-3 !px-5' }}"
class="mt-3"
>
<display-income-and-expenditure/>
</UDashboardCard>
<UDashboardCard
class="w-1/3 mt-3"
:ui="{ body: { padding: '!py-5 !px-5' }}"
>
<display-open-balances/>
</UDashboardCard>
<UDashboardCard
class="w-1/3 mt-3"
>
<display-running-time/>
</UDashboardCard>
</UDashboardPanelContent>
</UDashboardPanel>
</UDashboardPage>
@@ -64,11 +44,14 @@
<script setup>
import DisplayPresentProfiles from "~/components/noAutoLoad/displayPresentProfiles.vue";
definePageMeta({
middleware: "auth"
})
const dataStore = useDataStore()
const profileStore = useProfileStore()
const toast = useToast()
const router = useRouter()

View File

@@ -4,6 +4,7 @@ definePageMeta({
})
const dataStore = useDataStore()
const profileStore = useProfileStore()
const supabase = useSupabaseClient()
const router = useRouter()
const mode = ref("incoming")
@@ -41,8 +42,8 @@ const createMovement = async () => {
spaceId: inventoryChangeData.value.destinationSpaceId,
projectId: inventoryChangeData.value.destinationProjectId,
quantity: inventoryChangeData.value.quantity,
profileId: dataStore.activeProfile.id,
tenant: dataStore.currentTenant
profileId: profileStore.activeProfile.id,
tenant: profileStore.currentTenant
}
movements.push(movement)
@@ -61,8 +62,8 @@ const createMovement = async () => {
spaceId: inventoryChangeData.value.sourceSpaceId,
projectId: inventoryChangeData.value.sourceProjectId,
quantity: inventoryChangeData.value.quantity * -1,
profileId: dataStore.activeProfile.id,
tenant: dataStore.currentTenant
profileId: profileStore.activeProfile.id,
tenant: profileStore.currentTenant
}
movements.push(movement)
@@ -72,16 +73,16 @@ const createMovement = async () => {
spaceId: inventoryChangeData.value.sourceSpaceId,
projectId: inventoryChangeData.value.sourceProjectId,
quantity: inventoryChangeData.value.quantity * -1,
profileId: dataStore.activeProfile.id,
tenant: dataStore.currentTenant
profileId: profileStore.activeProfile.id,
tenant: profileStore.currentTenant
}
let inMovement = {
productId: inventoryChangeData.value.productId,
spaceId: inventoryChangeData.value.destinationSpaceId,
projectId: inventoryChangeData.value.destinationProjectId,
quantity: inventoryChangeData.value.quantity,
profileId: dataStore.activeProfile.id,
tenant: dataStore.currentTenant
profileId: profileStore.activeProfile.id,
tenant: profileStore.currentTenant
}
movements.push(outMovement)

View File

@@ -1,382 +0,0 @@
<script setup>
import HistoryDisplay from "~/components/HistoryDisplay.vue";
import dayjs from "dayjs";
import {useSupabaseSelectSingle} from "~/composables/useSupabase.js";
import DocumentUpload from "~/components/DocumentUpload.vue";
import DocumentList from "~/components/DocumentList.vue";
definePageMeta({
middleware: "auth"
})
const dataStore = useDataStore()
const route = useRoute()
const router = useRouter()
const toast = useToast()
const id = ref(route.params.id ? route.params.id : null )
//Working
const mode = ref(route.params.mode || "show")
const itemInfo = ref({
name: null,
description: null,
quantity: 0
})
//Functions
const setupPage = async () => {
if(mode.value === "show"){
itemInfo.value = await useSupabaseSelectSingle("inventoryitems", route.params.id, "*, vendor(*), checks(*)")
} else if(mode.value === "edit") {
itemInfo.value = await useSupabaseSelectSingle("inventoryitems", route.params.id, "*")
}
}
const cancelEditorCreate = () => {
if(itemInfo.value) {
router.push(`/inventoryitems/show/${itemInfo.value.id}`)
} else {
router.push(`/inventoryitems`)
}
}
setupPage()
</script>
<template>
<UDashboardNavbar :title="itemInfo ? itemInfo.name : (mode === 'create' ? 'Inventartikel erstellen' : 'Inventartikel bearbeiten')">
<template #left>
<UButton
icon="i-heroicons-chevron-left"
variant="outline"
@click="router.push(`/inventoryitems`)"
>
Inventar
</UButton>
</template>
<template #center>
<h1
v-if="itemInfo"
:class="['text-xl','font-medium']"
>{{itemInfo.id ? `Inventarartikel: ${itemInfo.name}` : (mode === 'create' ? 'Inventarartikel erstellen' : 'Inventarartikel bearbeiten')}}</h1>
</template>
<template #right>
<ButtonWithConfirm
color="rose"
variant="outline"
@confirmed="dataStore.updateItem('inventoryitems',{...itemInfo, archived: true})"
v-if="mode === 'edit'"
>
<template #button>
Archivieren
</template>
<template #header>
<span class="text-md text-black font-bold">Archivieren bestätigen</span>
</template>
Möchten Sie den Inventarartikel {{itemInfo.name}} wirklich archivieren?
</ButtonWithConfirm>
<UButton
v-if="mode === 'edit'"
@click="dataStore.updateItem('inventoryitems',itemInfo)"
>
Speichern
</UButton>
<UButton
v-if="mode === 'create'"
@click="dataStore.createNewItem('inventoryitems',itemInfo)"
>
Erstellen
</UButton>
<UButton
v-if="mode === 'create'"
class="ml-2"
@click="dataStore.createNewItem('inventoryitems',itemInfo);
itemInfo = {
name: null,
description: null,
quantity: 0
}"
>
Erstellen + Neu
</UButton>
<UButton
@click="cancelEditorCreate"
color="red"
class="ml-2"
v-if="mode === 'edit' || mode === 'create'"
>
Abbrechen
</UButton>
<UButton
v-if="mode === 'show'"
@click=" router.push(`/inventoryitems/edit/${itemInfo.id}`)"
>
Bearbeiten
</UButton>
</template>
</UDashboardNavbar>
<UTabs
:items="[{label: 'Informationen'},{label: 'Dokumente'},{label: 'Überprüfungen'}]"
v-if="itemInfo && mode === 'show'"
class="p-5"
>
<template #item="{item}">
<div v-if="item.label === 'Informationen'" class="flex-row flex mt-5">
<div class="w-1/2 mr-5">
<UCard>
<UAlert
v-if="itemInfo.archived"
color="rose"
variant="outline"
title="Objekt archiviert"
icon="i-heroicons-light-bulb"
class="mb-5"
/>
<table class="w-full">
<tr>
<td>Name: </td>
<td>{{itemInfo.name}}</td>
</tr>
<tr v-if="itemInfo.currentSpace">
<td>Lagerplatz: </td>
<td>{{dataStore.getSpaceById(itemInfo.currentSpace).spaceNumber}} - {{dataStore.getSpaceById(itemInfo.currentSpace).description}}</td>
</tr>
<tr>
<td>Seriennummer:</td>
<td>{{itemInfo.serialNumber}}</td>
</tr>
<tr>
<td>Menge:</td>
<td>{{itemInfo.quantity > 0 ? itemInfo.quantity : 'Einzelarktikel'}}</td>
</tr>
<tr>
<td>Artikelnummer:</td>
<td>{{itemInfo.articleNumber}}</td>
</tr>
<tr>
<td>Hersteller:</td>
<td>{{itemInfo.manufacturer}}</td>
</tr>
<tr>
<td>Herstellernummer:</td>
<td>{{itemInfo.manufacturerNumber}}</td>
</tr>
<tr>
<td>Lieferant:</td>
<td>{{itemInfo.vendor ? itemInfo.vendor.name : ''}}</td>
</tr>
<tr>
<td>Kaufdatum:</td>
<td>{{itemInfo.purchaseDate ? dayjs(itemInfo.purchaseDate).format("DD.MM.YYYY") : ''}}</td>
</tr>
<tr>
<td>Wert bei Kauf:</td>
<td>{{itemInfo.purchasePrice ? itemInfo.purchasePrice.toFixed(2).replace(".",",") + " €" : ''}}</td>
</tr>
<tr>
<td>Aktueller Wert:</td>
<td>{{itemInfo.currentValue ? itemInfo.currentValue.toFixed(2).replace(".",",") + " €" : ''}}</td>
</tr>
<tr>
<td>Beschreibung:</td>
<td>{{itemInfo.description}}</td>
</tr>
</table>
</UCard>
</div>
<div class="w-1/2">
<UCard>
<HistoryDisplay
type="inventoryitem"
v-if="itemInfo"
render-headline
:element-id="itemInfo.id"
/>
</UCard>
</div>
</div>
<div v-else-if="item.label === 'Dokumente'">
<UCard>
<DocumentUpload
type="inventoryitem"
:element-id="itemInfo.id"
/>
<DocumentList :documents="dataStore.getDocumentsByProfileId(itemInfo.id)"/>
</UCard>
</div>
<div v-else-if="item.label === 'Überprüfungen'">
<UCard>
<UTable
:rows="itemInfo.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>
</template>
</UTabs>
<UForm
v-else-if="mode == 'edit' || mode == 'create'"
class="p-5"
>
<div class="flex flex-row">
<div class="w-1/2 mr-5">
<UDivider>
Allgemeines
</UDivider>
<UFormGroup
label="Name:"
>
<UInput
v-model="itemInfo.name"
/>
</UFormGroup>
<UFormGroup
label="Artikelnummer:"
>
<UInput
v-model="itemInfo.articleNumber"
placeholder="Leer lassen für automatische generierte Nummer"
/>
</UFormGroup>
<UFormGroup
label="Lagerplatz:"
>
<USelectMenu
:options="dataStore.spaces"
v-model="itemInfo.currentSpace"
value-attribute="id"
>
<template #option="{option}">
<span class="truncate">{{option.spaceNumber}} - {{option.description}}</span>
</template>
<template #label>
<span v-if="itemInfo.currentSpace">{{dataStore.getSpaceById(itemInfo.currentSpace).spaceNumber }} - {{dataStore.getSpaceById(itemInfo.currentSpace).description}}</span>
<span v-else>Kein Lagerplatz ausgewählt</span>
</template>
</USelectMenu>
</UFormGroup>
<UFormGroup
label="Seriennummer:"
>
<UInput
v-model="itemInfo.serialNumber"
/>
</UFormGroup>
<UFormGroup
label="Menge:"
:help="itemInfo.serialNumber ? 'Menge deaktiviert durch Eingabe der Seriennummer' : 'Für Einzelartikel Menge gleich 0'"
>
<UInput
type="number"
v-model="itemInfo.quantity"
:disabled="itemInfo.serialNumber"
/>
</UFormGroup>
</div>
<div class="w-1/2">
<UDivider>
Anschaffung
</UDivider>
<UFormGroup
label="Hersteller:"
>
<UInput
v-model="itemInfo.manufacturer"
/>
</UFormGroup>
<UFormGroup
label="Herstellernr.:"
>
<UInput
v-model="itemInfo.manufacturerNumber"
/>
</UFormGroup>
<UFormGroup
label="Lieferant:"
>
<USelectMenu
:options="dataStore.vendors"
option-attribute="name"
value-attribute="id"
searchable
:search-attributes="['name']"
v-model="itemInfo.vendor"
/>
</UFormGroup>
<UFormGroup
label="Kaufdatum:"
>
<UPopover :popper="{ placement: 'bottom-start' }">
<UButton
icon="i-heroicons-calendar-days-20-solid"
:label="itemInfo.purchaseDate ? dayjs(itemInfo.purchaseDate).format('DD.MM.YYYY') : 'Datum auswählen'"
variant="outline"
/>
<template #panel="{ close }">
<LazyDatePicker v-model="itemInfo.purchaseDate" @close="close" />
</template>
</UPopover>
</UFormGroup>
<UFormGroup
label="Wert bei Kauf:"
>
<UInput
v-model="itemInfo.purchasePrice"
type="number"
steps="0.01"
>
<template #trailing>
<span class="text-gray-500 dark:text-gray-400 text-xs">EUR</span>
</template>
</UInput>
</UFormGroup>
<UFormGroup
label="Aktueller Wert:"
>
<UInput
v-model="itemInfo.currentValue"
type="number"
steps="0.01"
>
<template #trailing>
<span class="text-gray-500 dark:text-gray-400 text-xs">EUR</span>
</template>
</UInput>
</UFormGroup>
</div>
</div>
<UFormGroup
label="Beschreibung:"
>
<UTextarea
v-model="itemInfo.description"
/>
</UFormGroup>
</UForm>
</template>
<style scoped>
td {
border-bottom: 1px solid lightgrey;
vertical-align: top;
padding-bottom: 0.15em;
padding-top: 0.15em;
}
</style>

View File

@@ -1,117 +0,0 @@
<template>
<UDashboardNavbar title="Inventar" :badge="filteredRows.length">
<template #right>
<UInput
id="searchinput"
v-model="searchString"
icon="i-heroicons-funnel"
autocomplete="off"
placeholder="Suche..."
class="hidden lg:block"
@keydown.esc="$event.target.blur()"
>
<template #trailing>
<UKbd value="/" />
</template>
</UInput>
<UButton @click="router.push(`/inventoryitems/create`)">+ Inventarartikel</UButton>
</template>
</UDashboardNavbar>
<UDashboardToolbar>
<template #right>
<USelectMenu
v-model="selectedColumns"
icon="i-heroicons-adjustments-horizontal-solid"
:options="templateColumns"
multiple
class="hidden lg:block"
by="key"
>
<template #label>
Spalten
</template>
</USelectMenu>
</template>
</UDashboardToolbar>
<UTable
:rows="filteredRows"
:columns="columns"
class="w-full"
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
@select="(i) => router.push(`/inventoryitems/show/${i.id}`) "
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Inventarartikel anzuzeigen' }"
>
</UTable>
</template>
<script setup>
definePageMeta({
middleware: "auth"
})
defineShortcuts({
'/': () => {
//console.log(searchinput)
//searchinput.value.focus()
document.getElementById("searchinput").focus()
},
'+': () => {
router.push("/tasks/create")
}
})
const dataStore = useDataStore()
const router = useRouter()
const items = ref([])
const setupPage = async () => {
items.value = await useSupabaseSelect("inventoryitems","*", "articleNumber")
}
setupPage()
const templateColumns = [
{
key: "articleNumber",
label: "Artikelnummer",
sortable: true
},{
key: "name",
label: "Name",
sortable: true
},
{
key: "description",
label: "Beschreibung",
sortable: true
}
]
const selectedColumns = ref(templateColumns)
const columns = computed(() => templateColumns.filter((column) => selectedColumns.value.includes(column)))
const searchString = ref('')
const filteredRows = computed(() => {
if(!searchString.value) {
return items.value
}
return items.value.filter(product => {
return Object.values(product).some((value) => {
return String(value).toLowerCase().includes(searchString.value.toLowerCase())
})
})
})
</script>
<style scoped>
</style>

View File

@@ -1,5 +1,7 @@
<script setup >
import {useProfileStore} from "~/stores/profile.js";
definePageMeta({
layout: "notLoggedIn"
})
@@ -8,8 +10,8 @@ const supabase = useSupabaseClient()
const user = useSupabaseUser()
const router = useRouter()
const colorMode = useColorMode()
const dataStore = useDataStore()
const toast = useToast()
const profileStore = useProfileStore()
const isLight = computed({
get () {
@@ -61,7 +63,7 @@ const onSubmit = async (data) => {
} else {
//console.log("Login Successful")
dataStore.initializeData(user.id)
profileStore.initializeData(user.id)
router.push("/")

View File

@@ -1,255 +0,0 @@
<script setup>
import HistoryDisplay from "~/components/HistoryDisplay.vue";
import DocumentList from "~/components/DocumentList.vue";
import DocumentUpload from "~/components/DocumentUpload.vue";
import Toolbar from "~/components/Toolbar.vue";
definePageMeta({
middleware: "auth"
})
const dataStore = useDataStore()
const supabase = useSupabaseClient()
const route = useRoute()
const router = useRouter()
const toast = useToast()
const id = ref(route.params.id ? route.params.id : null )
//Working
const mode = ref(route.params.mode || "show")
const itemInfo = ref({
description: {
html: ""
}
})
const tabItems = [
{
label: "Informationen"
},{
label: "Projekte"
},{
label: "Aufgaben"
},{
label: "Dokumente"
}
]
//Functions
const setupPage = async () => {
if(mode.value === "show"){
itemInfo.value = (await supabase.from("plants").select("*, customer(*)").eq("id",useRoute().params.id).single()).data
} else if(mode.value === "edit") {
itemInfo.value = (await supabase.from("plants").select().eq("id",useRoute().params.id).single()).data
}
if(mode.value === "create") {
let query = route.query
if(query.customer) itemInfo.value.customer = Number(query.customer)
}
}
const contentChanged = (content) => {
itemInfo.value.description.html = content.html
itemInfo.value.description.text = content.text
itemInfo.value.description.json = content.json
}
setupPage()
</script>
<template>
<UDashboardNavbar :title="itemInfo ? itemInfo.name : (mode === 'create' ? 'Objekt erstellen' : 'Objekt bearbeiten')">
<template #left>
<UButton
icon="i-heroicons-chevron-left"
variant="outline"
@click="router.push(`/plants`)"
>
Objekte
</UButton>
</template>
<template #center>
<h1
v-if="itemInfo"
:class="['text-xl','font-medium']"
>{{itemInfo ? `Objekt: ${itemInfo.name}` : (mode === 'create' ? 'Objekt erstellen' : 'Objekt bearbeiten')}}</h1>
</template>
<template #right>
<ButtonWithConfirm
color="rose"
variant="outline"
@confirmed="dataStore.updateItem('plants',{...itemInfo, archived: true})"
v-if="mode === 'edit'"
>
<template #button>
Archivieren
</template>
<template #header>
<span class="text-md text-black font-bold">Archivieren bestätigen</span>
</template>
Möchten Sie das Objekt {{itemInfo.name}} wirklich archivieren?
</ButtonWithConfirm>
<UButton
v-if="mode === 'edit'"
@click="dataStore.updateItem('plants',itemInfo)"
>
Speichern
</UButton>
<UButton
v-else-if="mode === 'create'"
@click="dataStore.createNewItem('plants',itemInfo)"
>
Erstellen
</UButton>
<UButton
@click="router.push(itemInfo.id ? `/plants/show/${itemInfo.value.id}` : `/plants/`)"
color="red"
class="ml-2"
v-if="mode === 'edit' || mode === 'create'"
>
Abbrechen
</UButton>
<UButton
v-if="mode === 'show'"
@click="router.push(`/plants/edit/${itemInfo.id}`)"
>
Bearbeiten
</UButton>
</template>
</UDashboardNavbar>
<UTabs
:items="tabItems"
v-if="mode === 'show'"
class="p-5"
>
<template #item="{item}">
<div v-if="item.label === 'Informationen'" class="flex flex-row mt-5">
<UCard class="w-1/2 mr-5">
<UAlert
v-if="itemInfo.archived"
color="rose"
variant="outline"
title="Objekt archiviert"
icon="i-heroicons-light-bulb"
class="mb-5"
/>
<div class="text-wrap">
<table>
<tr>
<td>Kunde:</td>
<td><nuxt-link v-if="itemInfo.customer" :to="`/customers/show/${itemInfo.customer.id}`">{{itemInfo.customer.name}}</nuxt-link></td>
</tr>
</table>
</div>
<div v-if="itemInfo.description.html">
<p>Notizen:</p>
<span v-html="itemInfo.description.html"></span>
</div>
</UCard>
<UCard class="w-1/2">
<HistoryDisplay
type="plant"
v-if="itemInfo.id"
:element-id="itemInfo.id"
render-headline
/>
</UCard>
</div>
<div v-else-if="item.label === 'Projekte'">
<Toolbar>
<UButton
@click="router.push(`/projects/create?plant=${itemInfo.id}`)"
>
+ Projekt
</UButton>
</Toolbar>
<UTable
:rows="dataStore.getProjectsByPlantId(itemInfo.id)"
:columns="[{key: 'name', label: 'Name'}]"
@select="(row) => router.push(`/projects/show/${row.id}`)"
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine zugehörigen Projekte' }"
>
</UTable>
</div>
<div v-else-if="item.label === 'Aufgaben'">
<Toolbar>
<UButton
@click="router.push(`/tasks/create?plant=${itemInfo.id}`)"
>
+ Aufgabe
</UButton>
</Toolbar>
<UTable
:rows="dataStore.getTasksByPlantId(itemInfo.id)"
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine zugehörigen Aufgaben' }"
:columns="[{key: 'name', label: 'Name'},{key: 'categore', label: 'Kategorie'}]"
@select="(row) => router.push(`/tasks/show/${row.id}`)"
>
</UTable>
</div>
<div v-else-if="item.label === 'Dokumente'" class="space-y-3">
<Toolbar>
<DocumentUpload
type="plant"
:element-id="itemInfo.id"
/>
</Toolbar>
<DocumentList :documents="dataStore.getDocumentsByPlantId(itemInfo.id)"/>
</div>
</template>
</UTabs>
<UForm
v-else-if="mode === 'edit' || mode === 'create'"
class="p-5"
>
<UFormGroup
label="Name:"
>
<UInput
v-model="itemInfo.name"
/>
</UFormGroup>
<UFormGroup
label="Kundennummer:"
>
<USelectMenu
v-model="itemInfo.customer"
:options="dataStore.customers"
option-attribute="name"
value-attribute="id"
searchable
:search-attributes="['name']"
>
<template #label>
{{dataStore.customers.find(customer => customer.id === itemInfo.customer) ? dataStore.customers.find(customer => customer.id === itemInfo.customer).name : "Kunde auswählen"}}
</template>
</USelectMenu>
</UFormGroup>
<UFormGroup
label="Notizen:"
>
<Tiptap
v-if="itemInfo.description"
@updateContent="contentChanged"
:preloadedContent="itemInfo.description.html"
/>
</UFormGroup>
</UForm>
</template>
<style scoped>
</style>

View File

@@ -1,139 +0,0 @@
<template>
<UDashboardNavbar title="Objekte" :badge="filteredRows.length" >
<template #right>
<UInput
id="searchinput"
v-model="searchString"
icon="i-heroicons-funnel"
autocomplete="off"
placeholder="Suche..."
class="hidden lg:block"
@keydown.esc="$event.target.blur()"
>
<template #trailing>
<UKbd value="/" />
</template>
</UInput>
<UButton @click="router.push(`/plants/create`)">+ Objekt</UButton>
</template>
</UDashboardNavbar>
<UDashboardToolbar>
<template #left>
<UCheckbox
label="Erledigte Anzeigen"
v-model="showDone"
/>
</template>
<template #right>
<USelectMenu
v-model="selectedColumns"
icon="i-heroicons-adjustments-horizontal-solid"
:options="templateColumns"
multiple
class="hidden lg:block"
by="key"
>
<template #label>
Spalten
</template>
</USelectMenu>
</template>
</UDashboardToolbar>
<UTable
:rows="filteredRows"
:columns="columns"
class="w-full"
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
@select="(i) => router.push(`/plants/show/${i.id}`) "
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Objekte anzuzeigen' }"
>
<template #customer-data="{row}">
{{row.customer ? row.customer.name : ""}}
</template>
</UTable>
</template>
<script setup>
import {useListFilter} from "~/composables/useSearch.js";
definePageMeta({
middleware: "auth"
})
defineShortcuts({
'/': () => {
//console.log(searchinput)
//searchinput.value.focus()
document.getElementById("searchinput").focus()
},
'+': () => {
router.push("/plants/create")
},
'Enter': {
usingInput: true,
handler: () => {
router.push(`/plants/show/${filteredRows.value[selectedItem.value].id}`)
}
},
'arrowdown': () => {
if(selectedItem.value < filteredRows.value.length - 1) {
selectedItem.value += 1
} else {
selectedItem.value = 0
}
},
'arrowup': () => {
if(selectedItem.value === 0) {
selectedItem.value = filteredRows.value.length - 1
} else {
selectedItem.value -= 1
}
}
})
const dataStore = useDataStore()
const router = useRouter()
const items = ref([])
const selectedItem = ref(0)
const setupPage = async () => {
items.value = await useSupabaseSelect("plants","*, customer(id,name)")
}
setupPage()
const templateColumns = [
{
key: "name",
label: "Name",
sortable: true
},{
key: "customer",
label: "Kunde",
sortable: true
}
]
const selectedColumns = ref(templateColumns)
const columns = computed(() => templateColumns.filter((column) => selectedColumns.value.includes(column)))
const searchString = ref('')
const filteredRows = computed(() => {
return useListFilter(searchString.value, items.value)
})
</script>
<style scoped>
</style>

View File

@@ -1,179 +0,0 @@
<script setup>
import HistoryDisplay from "~/components/HistoryDisplay.vue";
import DocumentList from "~/components/DocumentList.vue";
import DocumentUpload from "~/components/DocumentUpload.vue";
import {useSupabaseSelect} from "~/composables/useSupabase.js";
import dayjs from "dayjs";
definePageMeta({
middleware: "auth"
})
defineShortcuts({
'backspace': () => {
router.push("/productcategories")
},
'arrowleft': () => {
if(openTab.value > 0){
openTab.value -= 1
}
},
'arrowright': () => {
if(openTab.value < 3) {
openTab.value += 1
}
},
})
const dataStore = useDataStore()
const route = useRoute()
const router = useRouter()
const toast = useToast()
const id = ref(route.params.id ? route.params.id : null )
//Working
const mode = ref(route.params.mode || "show")
const itemInfo = ref({})
const openTab = ref(0)
//Functions
const setupPage = async () => {
if(mode.value === "show" || mode.value === "edit"){
itemInfo.value = await useSupabaseSelectSingle("productcategories",route.params.id,"*")
}
}
const cancelEditorCreate = () => {
if(itemInfo.value) {
router.push(`/productcategories/show/${itemInfo.value.id}`)
} else {
router.push(`/productcategories/`)
}
}
setupPage()
</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(`/productcategories`)"
>
Artikelkategorien
</UButton>
</template>
<template #center>
<h1
v-if="itemInfo"
class="text-xl font-medium"
>{{itemInfo.name ? `Artikelkategorie: ${itemInfo.name}` : (mode === 'create' ? 'Artikelkategorie erstellen' : 'Artikelkategorie bearbeiten')}}</h1>
</template>
<template #right>
<UButton
v-if="mode === 'edit'"
@click="dataStore.updateItem('productcategories',itemInfo)"
>
Speichern
</UButton>
<UButton
v-else-if="mode === 'create'"
@click="dataStore.createNewItem('productcategories',itemInfo)"
>
Erstellen
</UButton>
<UButton
@click="cancelEditorCreate"
color="red"
class="ml-2"
v-if="mode === 'edit' || mode === 'create'"
>
Abbrechen
</UButton>
<UButton
v-if="mode === 'show'"
@click=" router.push(`/productcategories/edit/${itemInfo.id}`)"
>
Bearbeiten
</UButton>
</template>
</UDashboardNavbar>
<UTabs
:items="[{label: 'Informationen'}]"
v-if="mode === 'show' && itemInfo"
class="p-5"
v-model="openTab"
>
<template #item="{item}">
<div v-if="item.label === 'Informationen'" class="mr-5 flex flex-row">
<div class="w-1/2 mr-5">
<UCard>
<table class="w-full">
<tr>
<td>Name: </td>
<td>{{itemInfo.name}}</td>
</tr>
<tr>
<td>Beschreibung:</td>
<td>{{itemInfo.description}}</td>
</tr>
</table>
</UCard>
</div>
<div class="w-1/2">
<UCard>
<HistoryDisplay
type="product"
v-if="itemInfo"
:element-id="itemInfo.id"
render-headline
/>
</UCard>
</div>
</div>
</template>
</UTabs>
<UForm
v-else-if="mode == 'edit' || mode == 'create'"
class="p-5"
>
<UFormGroup
label="Name:"
>
<UInput
v-model="itemInfo.name"
autofocus
/>
</UFormGroup>
<UFormGroup
label="Beschreibung:"
>
<UTextarea
v-model="itemInfo.description"
/>
</UFormGroup>
</UForm>
</template>
<style scoped>
td {
border-bottom: 1px solid lightgrey;
vertical-align: top;
padding-bottom: 0.15em;
padding-top: 0.15em;
}
</style>

Some files were not shown because too many files have changed in this diff Show More