Merge branch 'beta' into 'main'

2025.19.1

See merge request fedeo/software!15
This commit is contained in:
2025-10-05 13:17:24 +00:00
115 changed files with 3631 additions and 7141 deletions

View File

@@ -1,12 +1,7 @@
<script setup> <script setup>
import * as Sentry from "@sentry/browser" import * as Sentry from "@sentry/browser"
const supabase = useSupabaseClient()
const user = useSupabaseUser()
const route = useRoute()
const tenants = (await supabase.from("tenants").select()).data
const dataStore = useDataStore()
const viewport = useViewport()
/*watch(viewport.breakpoint, (newBreakpoint, oldBreakpoint) => { /*watch(viewport.breakpoint, (newBreakpoint, oldBreakpoint) => {
console.log('Breakpoint updated:', oldBreakpoint, '->', newBreakpoint) console.log('Breakpoint updated:', oldBreakpoint, '->', newBreakpoint)
@@ -70,7 +65,6 @@ useSeoMeta({
<UNotifications :class="platform === 'mobile' ? ['mb-14'] : []"/> <UNotifications :class="platform === 'mobile' ? ['mb-14'] : []"/>
<USlideovers /> <USlideovers />
<UModals/> <UModals/>
<VitePwaManifest/>
</div> </div>

View File

@@ -0,0 +1,74 @@
<script setup>
const emit = defineEmits(['confirmed'])
const props = defineProps({
color: {
type: String,
required:false
},
variant: {
type: String,
required:false
},
type: {
type: String,
required:false
}
})
const {color,variant, type} = props
const dataStore = useDataStore()
const dataType = dataStore.dataTypes[type]
const showModal = ref(false)
const emitConfirm = () => {
showModal.value = false
emit('confirmed')
}
</script>
<template>
<UButton
:color="color"
:variant="variant"
@click="showModal = true"
>
Archivieren
</UButton>
<UModal v-model="showModal">
<UCard>
<template #header>
<span class="text-md font-bold">Archivieren bestätigen</span>
</template>
Möchten Sie diese/-s/-n {{dataType.labelSingle}} wirklich archivieren?
<template #footer>
<div class="text-right">
<UButtonGroup>
<UButton
variant="outline"
@click="showModal = false"
>
Abbrechen
</UButton>
<UButton
@click="emitConfirm"
class="ml-2"
color="rose"
>
Archivieren
</UButton>
</UButtonGroup>
</div>
</template>
</UCard>
</UModal>
</template>
<style scoped>
</style>

View File

@@ -41,7 +41,6 @@ const emitConfirm = () => {
<div class="text-right"> <div class="text-right">
<UButtonGroup> <UButtonGroup>
<UButton <UButton
color="rose"
variant="outline" variant="outline"
@click="showModal = false" @click="showModal = false"
> >
@@ -50,8 +49,9 @@ const emitConfirm = () => {
<UButton <UButton
@click="emitConfirm" @click="emitConfirm"
class="ml-2" class="ml-2"
color="rose"
> >
Bestätigen Archivieren
</UButton> </UButton>
</UButtonGroup> </UButtonGroup>

View File

@@ -3,7 +3,6 @@
import DocumentDisplayModal from "~/components/DocumentDisplayModal.vue"; import DocumentDisplayModal from "~/components/DocumentDisplayModal.vue";
const toast = useToast() const toast = useToast()
const supabase = useSupabaseClient()
const dataStore = useDataStore() const dataStore = useDataStore()
const modal = useModal() const modal = useModal()
const profileStore = useProfileStore() const profileStore = useProfileStore()
@@ -66,18 +65,18 @@ const showFile = (file) => {
<style scoped> <style scoped>
.documentListItem { .documentListItem {
display:block; display: block;
width: 15vw; width: 15vw;
aspect-ratio: 1 / 1.414; aspect-ratio: 1 / 1.414;
padding:1em; padding: 1em;
margin: 0.7em; margin: 0.7em;
border: 1px solid lightgrey;
border-radius: 15px; border-radius: 15px;
transition: box-shadow 0.2s ease; /* für smooth hover */
} }
.documentListItem:hover { .documentListItem:hover {
border: 1px solid #69c350;
cursor: pointer; cursor: pointer;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); /* sanfter Shadow beim Hover */
} }
.previewEmbed { .previewEmbed {

View File

@@ -1,7 +1,6 @@
<script setup> <script setup>
const toast = useToast() const toast = useToast()
const supabase = useSupabaseClient()
const dataStore = useDataStore() const dataStore = useDataStore()
const modal = useModal() const modal = useModal()
const props = defineProps({ const props = defineProps({
@@ -27,7 +26,7 @@ const filetypes = ref([])
const documentboxes = ref([]) const documentboxes = ref([])
const setup = async () => { const setup = async () => {
const {data} = await supabase.from("folders").select().eq("tenant",useProfileStore().currentTenant) const data = await useEntities("folders").select()
data.forEach(folder => { data.forEach(folder => {
let name = folder.name let name = folder.name
@@ -55,20 +54,12 @@ const setup = async () => {
} }
}) })
filetypes.value = await useSupabaseSelect("filetags") filetypes.value = await useEntities("filetags").select()
documentboxes.value = await useSupabaseSelect("documentboxes") documentboxes.value = await useEntities("documentboxes").select()
} }
setup() setup()
//Functions
const openDocument = async () => {
//selectedDocument.value = doc
openShowModal.value = true
console.log("open")
}
const updateDocument = async () => { const updateDocument = async () => {
const {url, ...objData} = props.documentData const {url, ...objData} = props.documentData
delete objData.url delete objData.url
@@ -91,12 +82,7 @@ const updateDocument = async () => {
console.log(objData) console.log(objData)
const {data,error} = await useEntities("files").update(objData.id, objData)
const {data,error} = await supabase
.from("files")
.update(objData)
.eq('id',objData.id)
.select()
if(error) { if(error) {
console.log(error) console.log(error)
@@ -114,13 +100,6 @@ const archiveDocument = async () => {
props.documentData.archived = true props.documentData.archived = true
await updateDocument() 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() modal.close()
emit("update") emit("update")
} }
@@ -139,19 +118,19 @@ const itemOptions = ref([])
const idToAssign = ref(null) const idToAssign = ref(null)
const getItemsBySelectedResource = async () => { const getItemsBySelectedResource = async () => {
if(resourceToAssign.value === "project") { if(resourceToAssign.value === "project") {
itemOptions.value = await useSupabaseSelect("projects") itemOptions.value = await useEntities("projects").select()
} else if(resourceToAssign.value === "customer") { } else if(resourceToAssign.value === "customer") {
itemOptions.value = await useSupabaseSelect("customers") itemOptions.value = await useEntities("customers").select()
} else if(resourceToAssign.value === "vendor") { } else if(resourceToAssign.value === "vendor") {
itemOptions.value = await useSupabaseSelect("vendors") itemOptions.value = await useEntities("vendors").select()
} else if(resourceToAssign.value === "vehicle") { } else if(resourceToAssign.value === "vehicle") {
itemOptions.value = await useSupabaseSelect("vehicles") itemOptions.value = await useEntities("vehicles").select()
} else if(resourceToAssign.value === "product") { } else if(resourceToAssign.value === "product") {
itemOptions.value = await useSupabaseSelect("products") itemOptions.value = await useEntities("products").select()
} else if(resourceToAssign.value === "plant") { } else if(resourceToAssign.value === "plant") {
itemOptions.value = await useSupabaseSelect("plants") itemOptions.value = await useEntities("plants").select()
} else if(resourceToAssign.value === "contract") { } else if(resourceToAssign.value === "contract") {
itemOptions.value = await useSupabaseSelect("contracts") itemOptions.value = await useEntities("contracts").select()
} else { } else {
itemOptions.value = [] itemOptions.value = []
} }
@@ -165,20 +144,9 @@ const updateDocumentAssignment = async () => {
const folderToMoveTo = ref(null) const folderToMoveTo = ref(null)
const moveFile = async () => { 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) { const res = await useEntities("files").update(props.documentData.id, {folder: folderToMoveTo.value})
console.log(error)
toast.add({title: "Fehler beim verschieben", color:"rose"})
} else {
toast.add({title: "Datei verschoben"})
console.log(data)
}
modal.close() modal.close()
} }
@@ -203,14 +171,11 @@ const moveFile = async () => {
</template> </template>
<div class="flex flex-row"> <div class="flex flex-row">
<div class="w-1/3"> <div :class="useCapacitor().getIsNative() ? ['w-full'] : ['w-1/3']">
<object <PDFViewer
class="bigPreview" v-if="props.documentData.id && props.documentData.path.toLowerCase().includes('pdf')"
:data="`${props.documentData.url}#toolbar=0&navpanes=0&scrollbar=0`" :file-id="props.documentData.id" />
type="application/pdf"
v-if="props.documentData.path.toLowerCase().includes('pdf')"
/>
<img <img
class=" w-full" class=" w-full"
:src="props.documentData.url" :src="props.documentData.url"
@@ -218,21 +183,14 @@ const moveFile = async () => {
v-else v-else
/> />
</div> </div>
<div class="w-2/3 p-5"> <div class="w-2/3 p-5" v-if="!useCapacitor().getIsNative()">
<UButtonGroup> <UButtonGroup>
<ButtonWithConfirm <ArchiveButton
color="rose" color="rose"
variant="outline" variant="outline"
type="files"
@confirmed="archiveDocument" @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 <UButton
:to="props.documentData.url" :to="props.documentData.url"

View File

@@ -18,7 +18,7 @@ const uploadInProgress = ref(false)
const availableFiletypes = ref([]) const availableFiletypes = ref([])
const setup = async () => { const setup = async () => {
availableFiletypes.value = await useSupabaseSelect("filetags") availableFiletypes.value = await useEntities("filetags").select()
} }
setup() setup()

View File

@@ -48,17 +48,14 @@ defineShortcuts({
}, },
}) })
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
const dataStore = useDataStore() const dataStore = useDataStore()
const profileStore = useProfileStore()
const supabase = useSupabaseClient()
const modal = useModal() const modal = useModal()
const dataType = dataStore.dataTypes[type] const dataType = dataStore.dataTypes[type]
const openTab = ref(0) const openTab = ref(0)
const item = ref(JSON.parse(props.item)) const item = ref(JSON.parse(props.item))
console.log(item.value) console.log(item.value)
@@ -152,11 +149,11 @@ const loadOptions = async () => {
for await(const option of optionsToLoad) { for await(const option of optionsToLoad) {
if(option.option === "countrys") { if(option.option === "countrys") {
loadedOptions.value[option.option] = (await supabase.from("countrys").select()).data loadedOptions.value[option.option] = useEntities("countrys").selectSpecial()
} else if(option.option === "units") { } else if(option.option === "units") {
loadedOptions.value[option.option] = (await supabase.from("units").select()).data loadedOptions.value[option.option] = useEntities("units").selectSpecial()
} else { } else {
loadedOptions.value[option.option] = (await useSupabaseSelect(option.option)) loadedOptions.value[option.option] = (await useEntities(option.option).select())
if(dataType.templateColumns.find(x => x.key === option.key).selectDataTypeFilter){ 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)) loadedOptions.value[option.option] = loadedOptions.value[option.option].filter(i => dataType.templateColumns.find(x => x.key === option.key).selectDataTypeFilter(i, item))
@@ -208,12 +205,14 @@ const createItem = async () => {
let ret = null let ret = null
if(props.inModal) { if(props.inModal) {
ret = await dataStore.createNewItem(type,item.value,true) ret = await useEntities(type).create(item.value, true)
} else { } else {
ret = dataStore.createNewItem(type,item.value) ret = await useEntities(type).create(item.value)//dataStore.createNewItem(type,item.value)
} }
emit('returnData', ret) emit('returnData', ret)
modal.close()
} }
const updateItem = async () => { const updateItem = async () => {
@@ -222,7 +221,8 @@ const updateItem = async () => {
if(props.inModal) { if(props.inModal) {
ret = await dataStore.updateItem(type,item.value, oldItem.value,true) ret = await dataStore.updateItem(type,item.value, oldItem.value,true)
} else { } else {
ret = await dataStore.updateItem(type,item.value, oldItem.value) console.log(item.value)
ret = await useEntities(type).update(item.value.id, item.value)//await dataStore.updateItem(type,item.value, oldItem.value)
} }
emit('returnData', ret) emit('returnData', ret)
@@ -255,20 +255,13 @@ const updateItem = async () => {
>{{item.id ? `${dataType.labelSingle} bearbeiten` : `${dataType.labelSingle} erstellen` }}</h1> >{{item.id ? `${dataType.labelSingle} bearbeiten` : `${dataType.labelSingle} erstellen` }}</h1>
</template> </template>
<template #right> <template #right>
<ButtonWithConfirm <ArchiveButton
v-if="platform !== 'mobile'"
color="rose" color="rose"
v-if="platform !== 'mobile'"
variant="outline" variant="outline"
@confirmed="dataStore.updateItem(type,{...item,archived: true}, oldItem)" :type="type"
> @confirmed="useEntities(type).archive(item.id)"
<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 <UButton
v-if="item.id" v-if="item.id"
@click="updateItem" @click="updateItem"

View File

@@ -2,7 +2,10 @@
import {useTempStore} from "~/stores/temp.js"; import {useTempStore} from "~/stores/temp.js";
import FloatingActionButton from "~/components/mobile/FloatingActionButton.vue"; import FloatingActionButton from "~/components/mobile/FloatingActionButton.vue";
import EntityTable from "~/components/EntityTable.vue"; import EntityTable from "~/components/EntityTable.vue";
import EntityListMobile from "~/components/EntityListMobile.vue"; import EntityTableMobile from "~/components/EntityTableMobile.vue";
const { has } = usePermission()
const props = defineProps({ const props = defineProps({
type: { type: {
@@ -15,9 +18,17 @@ const props = defineProps({
}, },
platform: { platform: {
required: true, required: true,
},
loading: {
required: true,
type: Boolean,
default: false
} }
}) })
const emit = defineEmits(["sort"]);
const {type} = props const {type} = props
defineShortcuts({ defineShortcuts({
@@ -74,7 +85,12 @@ const selectedFilters = ref(dataType.filters.filter(i => i.default).map(i => i.n
const filteredRows = computed(() => { const filteredRows = computed(() => {
let tempItems = props.items let tempItems = props.items.map(i => {
return {
...i,
class: i.archived ? 'bg-red-500/50 dark:bg-red-400/50' : null
}
})
if(selectedFilters.value.length > 0) { if(selectedFilters.value.length > 0) {
selectedFilters.value.forEach(filterName => { selectedFilters.value.forEach(filterName => {
@@ -83,23 +99,6 @@ const filteredRows = computed(() => {
}) })
} }
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) return useSearch(searchString.value, tempItems)
}) })
</script> </script>
@@ -139,13 +138,14 @@ const filteredRows = computed(() => {
/> />
<UButton <UButton
v-if="platform !== 'mobile' && useRole().checkRight(`${type}-create`)" v-if="platform !== 'mobile' && has(`${type}-create`)/*&& useRole().checkRight(`${type}-create`)*/"
@click="router.push(`/standardEntity/${type}/create`)" @click="router.push(`/standardEntity/${type}/create`)"
class="ml-3" class="ml-3"
>+ {{dataType.labelSingle}}</UButton> >+ {{dataType.labelSingle}}</UButton>
</template> </template>
</UDashboardNavbar> </UDashboardNavbar>
<UDashboardToolbar> <UDashboardToolbar>
<template #left v-if="$slots['left-toolbar']"> <template #left v-if="$slots['left-toolbar']">
<slot name="left-toolbar"/> <slot name="left-toolbar"/>
@@ -182,7 +182,7 @@ const filteredRows = computed(() => {
</USelectMenu> </USelectMenu>
</template> </template>
</UDashboardToolbar> </UDashboardToolbar>
<EntityListMobile <EntityTableMobile
v-if="platform === 'mobile'" v-if="platform === 'mobile'"
:type="props.type" :type="props.type"
:columns="columns" :columns="columns"
@@ -190,9 +190,11 @@ const filteredRows = computed(() => {
/> />
<EntityTable <EntityTable
v-else v-else
@sort="(i) => emit('sort',i)"
:type="props.type" :type="props.type"
:columns="columns" :columns="columns"
:rows="filteredRows" :rows="filteredRows"
:loading="props.loading"
/> />
</template> </template>

View File

@@ -45,6 +45,7 @@ const router = useRouter()
const route = useRoute() const route = useRoute()
const dataStore = useDataStore() const dataStore = useDataStore()
const modal = useModal() const modal = useModal()
const auth = useAuthStore()
const dataType = dataStore.dataTypes[type] const dataType = dataStore.dataTypes[type]
@@ -97,6 +98,43 @@ const onTabChange = (index) => {
router.push(`${router.currentRoute.value.path}?tabIndex=${index}`) router.push(`${router.currentRoute.value.path}?tabIndex=${index}`)
} }
const changePinned = async () => {
let newPins = []
if(auth.profile.pinned_on_navigation.find(i => i.datatype === type && i.id === props.item.id)){
//Remove Pin
newPins = auth.profile.pinned_on_navigation.filter(i => !(i.datatype === type && i.id === props.item.id))
} else {
//Add Pin
newPins = [
...auth.profile.pinned_on_navigation,
{
id: props.item.id,
icon: "i-heroicons-document",
type: "standardEntity",
datatype: type,
label: props.item[dataType.templateColumns.find(i => i.title).key]
}
]
}
const res = await useNuxtApp().$api(`/api/user/${auth.user.id}/profile`,{
method: "PUT",
body: {
data: {
pinned_on_navigation: newPins
}
}
})
await auth.fetchMe()
}
</script> </script>
<template> <template>
@@ -147,6 +185,13 @@ const onTabChange = (index) => {
>{{item ? `${dataType.labelSingle}${props.item[dataType.templateColumns.find(i => i.title).key] ? ': ' + props.item[dataType.templateColumns.find(i => i.title).key] : ''}`: '' }}</h1> >{{item ? `${dataType.labelSingle}${props.item[dataType.templateColumns.find(i => i.title).key] ? ': ' + props.item[dataType.templateColumns.find(i => i.title).key] : ''}`: '' }}</h1>
</template> </template>
<template #right> <template #right>
<UButton
v-if="auth.profile"
:variant="auth.profile?.pinned_on_navigation.find(i => i.datatype === type && i.id === props.item.id) ? 'solid' : 'outline'"
icon="i-heroicons-star"
color="yellow"
@click="changePinned"
></UButton>
<UButton <UButton
@click="router.push(`/standardEntity/${type}/edit/${item.id}`)" @click="router.push(`/standardEntity/${type}/edit/${item.id}`)"
> >
@@ -158,22 +203,6 @@ const onTabChange = (index) => {
v-else-if="!props.inModal && platform === 'mobile'" v-else-if="!props.inModal && platform === 'mobile'"
:ui="{center: 'flex items-stretch gap-1.5 min-w-0'}" :ui="{center: 'flex items-stretch gap-1.5 min-w-0'}"
> >
<!-- <template #left>
<UButton
icon="i-heroicons-chevron-left"
variant="outline"
@click="router.back()/*router.push(`/standardEntity/${type}`)*/"
>
Zurück
</UButton>
<UButton
icon="i-heroicons-chevron-left"
variant="outline"
@click="router.push(`/standardEntity/${type}`)"
>
Übersicht
</UButton>
</template>-->
<template #toggle> <template #toggle>
<div></div> <div></div>
</template> </template>
@@ -191,7 +220,6 @@ const onTabChange = (index) => {
</UButton> </UButton>
</template> </template>
</UDashboardNavbar> </UDashboardNavbar>
<UTabs <UTabs
:items="dataType.showTabs" :items="dataType.showTabs"
v-if="props.item.id && platform !== 'mobile'" v-if="props.item.id && platform !== 'mobile'"
@@ -226,6 +254,7 @@ const onTabChange = (index) => {
@updateNeeded="emit('updateNeeded')" @updateNeeded="emit('updateNeeded')"
:platform="platform" :platform="platform"
/> />
<!-- TODO Change Active Phase -->
<EntityShowSubPhases <EntityShowSubPhases
:item="props.item" :item="props.item"
:top-level-type="type" :top-level-type="type"
@@ -234,14 +263,14 @@ const onTabChange = (index) => {
@updateNeeded="emit('updateNeeded')" @updateNeeded="emit('updateNeeded')"
:platform="platform" :platform="platform"
/> />
<EntityShowSubCreatedDocuments <EntityShowSubCreatedDocuments
:item="props.item" :item="props.item"
:top-level-type="type" :top-level-type="type"
v-else-if="tab.label === 'Ausgangsbelege'" v-else-if="tab.label === 'Ausgangsbelege'"
:query-string-data="getAvailableQueryStringData()" :query-string-data="getAvailableQueryStringData()"
:platform="platform" :platform="platform"
/> />
<EntityShowSubCostCentreReport <EntityShowSubCostCentreReport
:top-level-type="type" :top-level-type="type"
:item="props.item" :item="props.item"
v-else-if="tab.label === 'Auswertung Kostenstelle'" v-else-if="tab.label === 'Auswertung Kostenstelle'"
@@ -259,13 +288,12 @@ const onTabChange = (index) => {
v-else-if="tab.label === 'Zeiten'" v-else-if="tab.label === 'Zeiten'"
:platform="platform" :platform="platform"
/> />
<EntityShowSub <EntityShowSub
:item="props.item" :item="props.item"
:query-string-data="getAvailableQueryStringData()" :query-string-data="getAvailableQueryStringData()"
:tab-label="tab.label" :tab-label="tab.label"
:top-level-type="type" :top-level-type="type"
:type="tab.key"
v-else v-else
:platform="platform" :platform="platform"
/> />
@@ -297,7 +325,7 @@ const onTabChange = (index) => {
@updateNeeded="emit('updateNeeded')" @updateNeeded="emit('updateNeeded')"
:platform="platform" :platform="platform"
/> />
<EntityShowSubPhases <!--<EntityShowSubPhases
:item="props.item" :item="props.item"
:top-level-type="type" :top-level-type="type"
v-else-if="sub.label === 'Phasen'" v-else-if="sub.label === 'Phasen'"
@@ -326,7 +354,7 @@ const onTabChange = (index) => {
:top-level-type="type" :top-level-type="type"
v-else v-else
:platform="platform" :platform="platform"
/> />-->
</div> </div>
</UDashboardPanelContent> </UDashboardPanelContent>

View File

@@ -27,8 +27,6 @@ const props = defineProps({
let type = ref("") let type = ref("")
const dataStore = useDataStore() const dataStore = useDataStore()
const tempStore = useTempStore() const tempStore = useTempStore()
@@ -42,6 +40,7 @@ const columns = computed(() => dataType.templateColumns.filter((column) => !colu
const loaded = ref(false) const loaded = ref(false)
const setup = () => { const setup = () => {
if(!props.type && props.tabLabel ) { if(!props.type && props.tabLabel ) {
if(props.tabLabel === "Aufgaben") { if(props.tabLabel === "Aufgaben") {
type.value = "tasks" type.value = "tasks"

View File

@@ -2,7 +2,6 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import {useSum} from "~/composables/useSum.js"; import {useSum} from "~/composables/useSum.js";
import {useSupabaseSelect} from "~/composables/useSupabase.js";
defineShortcuts({ defineShortcuts({
/*'/': () => { /*'/': () => {
//console.log(searchinput) //console.log(searchinput)
@@ -61,7 +60,8 @@ const router = useRouter()
const createddocuments = ref([]) const createddocuments = ref([])
const setup = async () => { const setup = async () => {
createddocuments.value = (await useSupabaseSelect("createddocuments")).filter(i => !i.archived) //createddocuments.value = (await useSupabaseSelect("createddocuments")).filter(i => !i.archived)
createddocuments.value = (await useEntities("createddocuments").select()).filter(i => !i.archived)
} }
setup() setup()
@@ -150,6 +150,7 @@ const selectItem = (item) => {
<span>Ausgangsbelege</span> <span>Ausgangsbelege</span>
</template> </template>
<Toolbar> <Toolbar>
<!-- TODO Rendering when Screen is too small -->
<UButton <UButton
@click="invoiceDeliveryNotes" @click="invoiceDeliveryNotes"
v-if="props.topLevelType === 'projects'" v-if="props.topLevelType === 'projects'"
@@ -179,7 +180,7 @@ const selectItem = (item) => {
<UButton <UButton
@click="showFinalInvoiceConfig = true" @click="showFinalInvoiceConfig = true"
v-if="props.topLevelType === 'projects'" v-if="props.topLevelType === 'projects'"
:disabled="!props.item.createddocuments.filter(i => !i.archived && i.type === 'advanceInvoices').length > 0" :disabled="!props.item.createddocuments?.filter(i => !i.archived && i.type === 'advanceInvoices').length > 0"
> >
+ Schlussrechnung + Schlussrechnung
</UButton> </UButton>

View File

@@ -31,7 +31,7 @@ const availableFiles = ref([])
const setup = async () => { const setup = async () => {
if(props.item.files) { if(props.item.files) {
availableFiles.value = await files.selectSomeDocuments(props.item.files.map(i => i.id)) || [] availableFiles.value = (await files.selectSomeDocuments(props.item.files.map(i => i.id))) || []
} }
} }
@@ -51,12 +51,12 @@ setup()
@uploadFinished="emit('updateNeeded')" @uploadFinished="emit('updateNeeded')"
/> />
</Toolbar> </Toolbar>
<DocumentList <DocumentList
:key="props.item.files.length" :key="props.item.files.length"
:documents="availableFiles" :documents="availableFiles"
v-if="availableFiles.length > 0" v-if="availableFiles.length > 0"
/> />
<UAlert <UAlert
v-else v-else
icon="i-heroicons-x-mark" icon="i-heroicons-x-mark"

View File

@@ -27,9 +27,8 @@ const dataType = dataStore.dataTypes[props.topLevelType]
<template> <template>
<UCard class="mt-5 scroll" :style="props.platform !== 'mobile' ? 'height: 80vh' : ''"> <UCard class="mt-5 scroll" :style="props.platform !== 'mobile' ? 'height: 80vh' : ''">
<HistoryDisplay <HistoryDisplay
:type="dataType.historyItemHolder" :type="props.topLevelType"
v-if="props v-if="props.item.id"
.item.id"
:element-id="props.item.id" :element-id="props.item.id"
render-headline render-headline
/> />

View File

@@ -26,7 +26,6 @@
} }
} }
}) })
const props = defineProps({ const props = defineProps({
rows: { rows: {
type: Array, type: Array,
@@ -40,9 +39,16 @@
type: { type: {
type: String, type: String,
required: true, required: true,
},
loading: {
type: Boolean,
required: true,
default: false
} }
}) })
const emit = defineEmits(["sort"]);
const dataStore = useDataStore() const dataStore = useDataStore()
const router = useRouter() const router = useRouter()
@@ -50,12 +56,20 @@
const dataType = dataStore.dataTypes[props.type] const dataType = dataStore.dataTypes[props.type]
const selectedItem = ref(0) const selectedItem = ref(0)
const sort = ref({
column: dataType.supabaseSortColumn || "date",
direction: 'desc'
})
</script> </script>
<template> <template>
<UTable <UTable
:loading="props.loading"
:loading-state="{ icon: 'i-heroicons-arrow-path-20-solid', label: 'Loading...' }"
sort-mode="manual"
v-model:sort="sort"
@update:sort="emit('sort',{sort_column: sort.column, sort_direction: sort.direction})"
v-if="dataType && columns" v-if="dataType && columns"
:rows="props.rows" :rows="props.rows"
:columns="props.columns" :columns="props.columns"
@@ -64,18 +78,26 @@
@select="(i) => router.push(`/standardEntity/${type}/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` }" :empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: `Keine ${dataType.label} anzuzeigen` }"
> >
<template <!-- <template
v-for="column in dataType.templateColumns.filter(i => !i.disabledInTable)" v-for="column in dataType.templateColumns.filter(i => !i.disabledInTable)"
v-slot:[`${column.key}-header`]="{row}"> v-slot:[`${column.key}-header`]="{row}">
<span class="text-nowrap">{{column.label}}</span> <span class="text-nowrap">{{column.label}}</span>
</template> </template>-->
<template #name-data="{row}"> <template #name-data="{row}">
<span <span
v-if="row.id === props.rows[selectedItem].id" v-if="row.id === props.rows[selectedItem].id"
class="text-primary-500 font-bold">{{row.name}} class="text-primary-500 font-bold">
</span> <UTooltip
:text="row.name"
>
{{dataType.templateColumns.find(i => i.key === "name").maxLength ? (row.name.length > dataType.templateColumns.find(i => i.key === "name").maxLength ? `${row.name.substring(0,dataType.templateColumns.find(i => i.key === "name").maxLength)}...` : row.name ) : row.name}}
</UTooltip> </span>
<span v-else> <span v-else>
{{row.name}} <UTooltip
:text="row.name"
>
{{dataType.templateColumns.find(i => i.key === "name").maxLength ? (row.name.length > dataType.templateColumns.find(i => i.key === "name").maxLength ? `${row.name.substring(0,dataType.templateColumns.find(i => i.key === "name").maxLength)}...` : row.name ) : row.name}}
</UTooltip>
</span> </span>
</template> </template>
<template #fullName-data="{row}"> <template #fullName-data="{row}">
@@ -100,7 +122,12 @@
v-for="column in dataType.templateColumns.filter(i => i.key !== 'name' && i.key !== 'fullName' && i.key !== 'licensePlate' && !i.disabledInTable)" 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}"> v-slot:[`${column.key}-data`]="{row}">
<component v-if="column.component" :is="column.component" :row="row"></component> <component v-if="column.component" :is="column.component" :row="row"></component>
<span v-else>{{row[column.key] ? `${row[column.key]} ${column.unit ? column.unit : ''}`: ''}}</span> <span v-else-if="row[column.key]">
<UTooltip :text="row[column.key]">
{{row[column.key] ? `${column.maxLength ? (row[column.key].length > column.maxLength ? `${row[column.key].substring(0,column.maxLength)}...` : row[column.key]) : row[column.key]} ${column.unit ? column.unit : ''}`: ''}}
</UTooltip>
</span>
</template> </template>
</UTable> </UTable>
</template> </template>

View File

@@ -18,11 +18,11 @@ const links = [{
onClick: () => { onClick: () => {
shortcuts.value = true shortcuts.value = true
} }
}, { },/* {
label: 'Tickets', label: 'Tickets',
icon: 'i-heroicons-clipboard-document', icon: 'i-heroicons-clipboard-document',
to: '/support', to: '/support',
}, { },*/ {
label: 'Webseite', label: 'Webseite',
icon: 'i-heroicons-globe-europe-africa', icon: 'i-heroicons-globe-europe-africa',
to: 'https://fedeo.de', to: 'https://fedeo.de',

View File

@@ -14,8 +14,9 @@ const props = defineProps({
default: false default: false
} }
}) })
const profileStore = useProfileStore()
const supabase = useSupabaseClient() const auth = useAuthStore()
const toast = useToast() const toast = useToast()
const showAddHistoryItemModal = ref(false) const showAddHistoryItemModal = ref(false)
const colorMode = useColorMode() const colorMode = useColorMode()
@@ -28,11 +29,10 @@ const setup = async () => {
if(await useCapacitor().getIsPhone()) platform.value = "mobile" if(await useCapacitor().getIsPhone()) platform.value = "mobile"
if(props.type && props.elementId){ if(props.type && props.elementId){
items.value = (await supabase.from("historyitems").select().eq(props.type,props.elementId).order("created_at",{ascending: true})).data || [] items.value = await useNuxtApp().$api(`/api/resource/${props.type}/${props.elementId}/history`)
} else { } /*else {
items.value = (await supabase.from("historyitems").select().order("created_at",{ascending: true})).data || []
} }*/
} }
setup() setup()
@@ -40,55 +40,24 @@ setup()
const addHistoryItemData = ref({ const addHistoryItemData = ref({
text: "", text: ""
config: {
type: props.type,
id: props.elementId
}
}) })
const addHistoryItem = async () => { const addHistoryItem = async () => {
console.log(addHistoryItemData.value)
addHistoryItemData.value.createdBy = profileStore.activeProfile.id
addHistoryItemData.value[props.type] = props.elementId const res = await useNuxtApp().$api(`/api/resource/${props.type}/${props.elementId}/history`, {
method: "POST",
body: addHistoryItemData.value
})
const {data,error} = await supabase
.from("historyitems")
.insert([{...addHistoryItemData.value, tenant: profileStore.currentTenant}])
.select()
if(error) {
console.log(error)
} else {
if(addHistoryItemData.value.text.includes("@")){
let usernames = [...addHistoryItemData.value.text.matchAll(/@(\S*)/gm)]
const {data:profiles} = await supabase.from("profiles").select("id,username")
let notifications = usernames.map(i => { addHistoryItemData.value = {}
let rawUsername = i[1] toast.add({title: "Eintrag erfolgreich erstellt"})
showAddHistoryItemModal.value = false
await setup()
return {
tenant: profileStore.currentTenant,
profile: profiles.find(x => x.username === rawUsername).id,
initiatingProfile: profileStore.activeProfile.id,
title: "Sie wurden im Logbuch erwähnt",
link: `/${props.type}s/show/${props.elementId}`,
message: addHistoryItemData.value.text
}
})
console.log(notifications)
const {error} = await supabase.from("notifications").insert(notifications)
}
addHistoryItemData.value = {}
toast.add({title: "Eintrag erfolgreich erstellt"})
showAddHistoryItemModal.value = false
await setup()
}
} }
@@ -170,15 +139,15 @@ const renderText = (text) => {
/> />
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<UAvatar <UAvatar
v-if="!item.createdBy" v-if="!item.created_by"
:src="colorMode.value === 'light' ? '/Logo.png' : '/Logo_Dark.png' " :src="colorMode.value === 'light' ? '/Logo.png' : '/Logo_Dark.png' "
/> />
<UAvatar <UAvatar
:alt="profileStore.getProfileById(item.createdBy).fullName" :alt="item.created_by_profile?.full_name"
v-else v-else
/> />
<div> <div>
<h3 v-if="item.createdBy">{{profileStore.getProfileById(item.createdBy) ? profileStore.getProfileById(item.createdBy).fullName : ""}}</h3> <h3 v-if="item.created_by">{{item.created_by_profile?.full_name}}</h3>
<h3 v-else>FEDEO Bot</h3> <h3 v-else>FEDEO Bot</h3>
<span v-html="renderText(item.text)"/><br> <span v-html="renderText(item.text)"/><br>
<span class="text-gray-500">{{dayjs(item.created_at).format("DD.MM.YY HH:mm")}}</span> <span class="text-gray-500">{{dayjs(item.created_at).format("DD.MM.YY HH:mm")}}</span>

View File

@@ -1,19 +1,32 @@
<script setup> <script setup>
import {useRole} from "~/composables/useRole.js";
const profileStore = useProfileStore()
const route = useRoute() const route = useRoute()
const auth = useAuthStore()
const {has} = usePermission()
const role = useRole()
const links = computed(() => { const links = computed(() => {
return [ return [
... profileStore.currentTenant === 21 ? [{ ...(auth.profile?.pinned_on_navigation || []).map(pin => {
label: "Lieferschein Formular", if(pin.type === "external") {
to: "https://loar.ma-agrarservice.de/erfassung", return {
icon: "i-heroicons-rectangle-stack", label: pin.label,
target: "_blank", to: pin.link,
}] : [], icon: pin.icon,
... profileStore.currentTenant === 5 ? [{ target: "_blank",
pinned: true
}
}else if(pin.type === "standardEntity") {
return {
label: pin.label,
to: `/standardEntity/${pin.datatype}/show/${pin.id}`,
icon: pin.icon,
pinned: true
}
}
}),
... false ? [{
label: "Support Tickets", label: "Support Tickets",
to: "/support", to: "/support",
icon: "i-heroicons-rectangle-stack", icon: "i-heroicons-rectangle-stack",
@@ -34,22 +47,22 @@ const links = computed(() => {
icon: "i-heroicons-rectangle-stack", icon: "i-heroicons-rectangle-stack",
defaultOpen: false, defaultOpen: false,
children: [ children: [
... role.checkRight("tasks") ? [{ ... has("tasks") ? [{
label: "Aufgaben", label: "Aufgaben",
to: "/standardEntity/tasks", to: "/standardEntity/tasks",
icon: "i-heroicons-rectangle-stack" icon: "i-heroicons-rectangle-stack"
}] : [], }] : [],
... profileStore.ownTenant.features.planningBoard ? [{ ... true ? [{
label: "Plantafel", label: "Plantafel",
to: "/calendar/timeline", to: "/calendar/timeline",
icon: "i-heroicons-calendar-days" icon: "i-heroicons-calendar-days"
}] : [], }] : [],
... profileStore.ownTenant.features.calendar ? [{ ... true ? [{
label: "Kalender", label: "Kalender",
to: "/calendar/grid", to: "/calendar/grid",
icon: "i-heroicons-calendar-days" icon: "i-heroicons-calendar-days"
}] : [], }] : [],
... profileStore.ownTenant.features.calendar ? [{ ... true ? [{
label: "Termine", label: "Termine",
to: "/standardEntity/events", to: "/standardEntity/events",
icon: "i-heroicons-calendar-days" icon: "i-heroicons-calendar-days"
@@ -86,7 +99,7 @@ const links = computed(() => {
label: "E-Mail", label: "E-Mail",
to: "/email/new", to: "/email/new",
icon: "i-heroicons-envelope" icon: "i-heroicons-envelope"
}, { }/*, {
label: "Logbücher", label: "Logbücher",
to: "/communication/historyItems", to: "/communication/historyItems",
icon: "i-heroicons-book-open" icon: "i-heroicons-book-open"
@@ -94,25 +107,25 @@ const links = computed(() => {
label: "Chats", label: "Chats",
to: "/chats", to: "/chats",
icon: "i-heroicons-chat-bubble-left" icon: "i-heroicons-chat-bubble-left"
} }*/
] ]
}, },
... (role.checkRight("customers") || role.checkRight("vendors") || role.checkRight("contacts")) ? [{ ... (has("customers") || has("vendors") || has("contacts")) ? [{
label: "Kontakte", label: "Kontakte",
defaultOpen: false, defaultOpen: false,
icon: "i-heroicons-user-group", icon: "i-heroicons-user-group",
children: [ children: [
... role.checkRight("customers") ? [{ ... has("customers") ? [{
label: "Kunden", label: "Kunden",
to: "/standardEntity/customers", to: "/standardEntity/customers",
icon: "i-heroicons-user-group" icon: "i-heroicons-user-group"
}] : [], }] : [],
... role.checkRight("vendors") ? [{ ... has("vendors") ? [{
label: "Lieferanten", label: "Lieferanten",
to: "/standardEntity/vendors", to: "/standardEntity/vendors",
icon: "i-heroicons-truck" icon: "i-heroicons-truck"
}] : [], }] : [],
... role.checkRight("contacts") ? [{ ... has("contacts") ? [{
label: "Ansprechpartner", label: "Ansprechpartner",
to: "/standardEntity/contacts", to: "/standardEntity/contacts",
icon: "i-heroicons-user-group" icon: "i-heroicons-user-group"
@@ -124,29 +137,29 @@ const links = computed(() => {
defaultOpen:false, defaultOpen:false,
icon: "i-heroicons-user-group", icon: "i-heroicons-user-group",
children: [ children: [
... profileStore.ownTenant.features.timeTracking ? [{ ... true ? [{
label: "Projektzeiten", label: "Projektzeiten",
to: "/times", to: "/times",
icon: "i-heroicons-clock" icon: "i-heroicons-clock"
}] : [], }] : [],
... profileStore.ownTenant.features.workingTimeTracking ? [{ ... true ? [{
label: "Anwesenheiten", label: "Anwesenheiten",
to: "/workingtimes", to: "/workingtimes",
icon: "i-heroicons-clock" icon: "i-heroicons-clock"
}] : [], }] : [],
... role.checkRight("absencerequests") ? [{ ... has("absencerequests") ? [{
label: "Abwesenheiten", label: "Abwesenheiten",
to: "/standardEntity/absencerequests", to: "/standardEntity/absencerequests",
icon: "i-heroicons-document-text" icon: "i-heroicons-document-text"
}] : [], }] : [],
{ /*{
label: "Fahrten", label: "Fahrten",
to: "/trackingTrips", to: "/trackingTrips",
icon: "i-heroicons-map" icon: "i-heroicons-map"
}, },*/
] ]
}, },
... profileStore.ownTenant.features.accounting ? [{ ... [{
label: "Buchhaltung", label: "Buchhaltung",
defaultOpen: false, defaultOpen: false,
icon: "i-heroicons-chart-bar-square", icon: "i-heroicons-chart-bar-square",
@@ -182,8 +195,8 @@ const links = computed(() => {
icon: "i-heroicons-document-text" icon: "i-heroicons-document-text"
}, },
] ]
},] : [], }],
... role.checkRight("inventory") ? [{ ... has("inventory") ? [{
label: "Lager", label: "Lager",
icon: "i-heroicons-puzzle-piece", icon: "i-heroicons-puzzle-piece",
defaultOpen: false, defaultOpen: false,
@@ -197,7 +210,7 @@ const links = computed(() => {
to: "/inventory/stocks", to: "/inventory/stocks",
icon: "i-heroicons-square-3-stack-3d" icon: "i-heroicons-square-3-stack-3d"
},*/ },*/
... role.checkRight("spaces") ? [{ ... has("spaces") ? [{
label: "Lagerplätze", label: "Lagerplätze",
to: "/standardEntity/spaces", to: "/standardEntity/spaces",
icon: "i-heroicons-square-3-stack-3d" icon: "i-heroicons-square-3-stack-3d"
@@ -209,22 +222,22 @@ const links = computed(() => {
defaultOpen: false, defaultOpen: false,
icon: "i-heroicons-clipboard-document", icon: "i-heroicons-clipboard-document",
children: [ children: [
... role.checkRight("products") ? [{ ... has("products") ? [{
label: "Artikel", label: "Artikel",
to: "/standardEntity/products", to: "/standardEntity/products",
icon: "i-heroicons-puzzle-piece" icon: "i-heroicons-puzzle-piece"
}] : [], }] : [],
... role.checkRight("productcategories") ? [{ ... has("productcategories") ? [{
label: "Artikelkategorien", label: "Artikelkategorien",
to: "/standardEntity/productcategories", to: "/standardEntity/productcategories",
icon: "i-heroicons-puzzle-piece" icon: "i-heroicons-puzzle-piece"
}] : [], }] : [],
... role.checkRight("services") ? [{ ... has("services") ? [{
label: "Leistungen", label: "Leistungen",
to: "/standardEntity/services", to: "/standardEntity/services",
icon: "i-heroicons-wrench-screwdriver" icon: "i-heroicons-wrench-screwdriver"
}] : [], }] : [],
... role.checkRight("servicecategories") ? [{ ... has("servicecategories") ? [{
label: "Leistungskategorien", label: "Leistungskategorien",
to: "/standardEntity/servicecategories", to: "/standardEntity/servicecategories",
icon: "i-heroicons-wrench-screwdriver" icon: "i-heroicons-wrench-screwdriver"
@@ -239,70 +252,67 @@ const links = computed(() => {
to: "/standardEntity/hourrates", to: "/standardEntity/hourrates",
icon: "i-heroicons-user-group" icon: "i-heroicons-user-group"
}, },
... role.checkRight("vehicles") ? [{ ... has("vehicles") ? [{
label: "Fahrzeuge", label: "Fahrzeuge",
to: "/standardEntity/vehicles", to: "/standardEntity/vehicles",
icon: "i-heroicons-truck" icon: "i-heroicons-truck"
}] : [], }] : [],
... role.checkRight("inventoryitems") ? [{ ... has("inventoryitems") ? [{
label: "Inventar", label: "Inventar",
to: "/standardEntity/inventoryitems", to: "/standardEntity/inventoryitems",
icon: "i-heroicons-puzzle-piece" icon: "i-heroicons-puzzle-piece"
}] : [], }] : [],
... role.checkRight("inventoryitems") ? [{ ... has("inventoryitems") ? [{
label: "Inventargruppen", label: "Inventargruppen",
to: "/standardEntity/inventoryitemgroups", to: "/standardEntity/inventoryitemgroups",
icon: "i-heroicons-puzzle-piece" icon: "i-heroicons-puzzle-piece"
}] : [], }] : [],
] ]
}, },
... role.checkRight("checks") ? [{
label: "Überprüfungen", ... has("projects") ? [{
to: "/standardEntity/checks",
icon: "i-heroicons-magnifying-glass"
},] : [],
... role.checkRight("projects") ? [{
label: "Projekte", label: "Projekte",
to: "/standardEntity/projects", to: "/standardEntity/projects",
icon: "i-heroicons-clipboard-document-check" icon: "i-heroicons-clipboard-document-check"
},] : [], },] : [],
... role.checkRight("contracts") ? [{ ... has("contracts") ? [{
label: "Verträge", label: "Verträge",
to: "/standardEntity/contracts", to: "/standardEntity/contracts",
icon: "i-heroicons-clipboard-document" icon: "i-heroicons-clipboard-document"
}] : [], }] : [],
... role.checkRight("plants") ? [{ ... has("plants") ? [{
label: "Objekte", label: "Objekte",
to: "/standardEntity/plants", to: "/standardEntity/plants",
icon: "i-heroicons-clipboard-document" icon: "i-heroicons-clipboard-document"
},] : [], },] : [],
... has("checks") ? [{
label: "Überprüfungen",
to: "/standardEntity/checks",
icon: "i-heroicons-magnifying-glass"
},] : [],
{ {
label: "Einstellungen", label: "Einstellungen",
defaultOpen: false, defaultOpen: false,
icon: "i-heroicons-cog-8-tooth", icon: "i-heroicons-cog-8-tooth",
children: [ children: [
{ {
label: "Abrechnung",
to: "https://billing.stripe.com/p/login/cN29Eb32Vdx0gOk288",
icon: "i-heroicons-document-currency-euro",
target: "_blank"
},{
label: "Nummernkreise", label: "Nummernkreise",
to: "/settings/numberRanges", to: "/settings/numberRanges",
icon: "i-heroicons-clipboard-document-list" icon: "i-heroicons-clipboard-document-list"
},{ },/*{
label: "Rollen", label: "Rollen",
to: "/roles", to: "/roles",
icon: "i-heroicons-key" icon: "i-heroicons-key"
},{ },*/{
label: "E-Mail Konten", label: "E-Mail Konten",
to: "/settings/emailAccounts", to: "/settings/emailAccounts",
icon: "i-heroicons-envelope" icon: "i-heroicons-envelope",
disabled: true
},{ },{
label: "Bankkonten", label: "Bankkonten",
to: "/settings/banking", to: "/settings/banking",
icon: "i-heroicons-currency-euro" icon: "i-heroicons-currency-euro",
disabled: true
},{ },{
label: "Textvorlagen", label: "Textvorlagen",
to: "/settings/texttemplates", to: "/settings/texttemplates",
@@ -328,59 +338,126 @@ const links = computed(() => {
} }
] ]
}) })
// nur Items mit Children → für Accordion
const accordionItems = computed(() =>
links.value.filter(item => Array.isArray(item.children) && item.children.length > 0)
)
// nur Items ohne Children → als Buttons
const buttonItems = computed(() =>
links.value.filter(item => !item.children || item.children.length === 0)
)
</script> </script>
<template> <template>
<!-- Standalone Buttons -->
<div <div class="flex flex-col gap-1">
v-for="item in links"
>
<UAccordion
v-if="item.children"
:items="[item]"
>
<template #default="{item,index,open}">
<UButton
variant="ghost"
:color="item.children.find(i => route.path.includes(i.to)) ? 'primary' : 'gray'"
:icon="item.icon"
>
{{item.label}}
<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, open}">
<div class="flex flex-col">
<UButton
variant="ghost"
:color="child.to === route.path ? 'primary' : 'gray'"
:icon="child.icon"
v-for="child in item.children"
class="ml-4"
:to="child.to"
:target="child.target"
>
{{child.label}}
</UButton>
</div>
</template>
</UAccordion>
<UButton <UButton
v-else v-for="item in buttonItems"
variant="ghost" :key="item.label"
:color="item.to === route.path ? 'primary' : 'gray'" :variant="item.pinned ? 'ghost' : 'ghost'"
class="w-full" :color="(item.to && route.path === item.to) ? 'primary' : (item.pinned ? 'amber' : 'gray')"
:icon="item.icon" :icon="item.pinned ? 'i-heroicons-star' : item.icon"
:to="item.to" class="w-full"
:to="item.to"
:target="item.target"
> >
{{item.label}} <UIcon
v-if="item.pinned"
:name="item.icon"
class="w-5 h-5 me-2"
/>
{{ item.label }}
</UButton> </UButton>
</div> </div>
<UDivider/>
<!-- Accordion für die Items mit Children -->
<UAccordion
:items="accordionItems"
:multiple="false"
class="mt-2"
>
<template #default="{ item, open }">
<UButton
:variant="'ghost'"
:color="(item.children?.some(c => route.path.includes(c.to))) ? 'primary' : 'gray'"
:icon="item.icon"
class="w-full"
>
{{ item.label }}
<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 class="flex flex-col">
<UButton
v-for="child in item.children"
:key="child.label"
variant="ghost"
:color="child.to === route.path ? 'primary' : 'gray'"
:icon="child.icon"
class="ml-4"
:to="child.to"
:target="child.target"
:disabled="child.disabled"
>
{{ child.label }}
</UButton>
</div>
</template>
</UAccordion>
<!-- <UAccordion
:items="links"
:multiple="false"
>
<template #default="{ item, index, open }">
<UButton
:variant="item.pinned ? 'ghost' : 'ghost'"
:color="(item.to && route.path === item.to) || (item.children?.some(c => route.path.includes(c.to))) ? 'primary' : (item.pinned ? 'amber' : 'gray')"
:icon="item.pinned ? 'i-heroicons-star' : item.icon"
class="w-full"
:to="item.to"
:target="item.target"
>
<UIcon
v-if="item.pinned"
:name="item.icon" class="w-5 h-5 me-2" />
{{ item.label }}
<template v-if="item.children" #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 class="flex flex-col" v-if="item.children?.length > 0">
<UButton
v-for="child in item.children"
:key="child.label"
variant="ghost"
:color="child.to === route.path ? 'primary' : 'gray'"
:icon="child.icon"
class="ml-4"
:to="child.to"
:target="child.target"
>
{{ child.label }}
</UButton>
</div>
</template>
</UAccordion>-->
</template> </template>

View File

@@ -0,0 +1,212 @@
<script setup>
import { ref, onMounted, watch } from "vue"
import { VPdfViewer, useLicense } from "@vue-pdf-viewer/viewer"
const props = defineProps({
// Beispiel: "FEDEO/26/filesbyid/11990345-8711-4e23-8851-c50f028fc915/RE25-1081.pdf"
fileId: {
type: String,
},
uri: {
type: String,
},
scale: {
type: Number,
default: 1.2,
},
})
const config = useRuntimeConfig()
useLicense(config.public.pdfLicense)
const pdfSrc = ref(null) // ObjectURL fürs Viewer
const { $api } = useNuxtApp()
async function loadPdf(id) {
try {
const arrayBuffer = await $api(`/api/files/download/${id}`, {
method: "POST",
responseType: "arrayBuffer", // wichtig für pdf.js
})
const blob = new Blob([arrayBuffer], { type: "application/pdf" })
pdfSrc.value = URL.createObjectURL(blob)
} catch (err) {
console.error("Fehler beim Laden der PDF:", err)
}
}
onMounted(() => {
if(props.fileId) {
loadPdf(props.fileId)
} else if(props.uri) {
pdfSrc.value = props.uri
}
//window.addEventListener("resize", handleZoomTool("pageWidth"), true);
})
watch(() => props.fileId, (newPath) => {
if (newPath) loadPdf(newPath)
})
const vpvRef = ref(null);
//Zoom Control
const zoomControl = computed(() => vpvRef.value?.zoomControl)
const currentScale = computed(() => {
return zoomControl.value?.scale
})
const handleZoomTool = (type) => {
console.log(type)
const zoomCtrl = unref(zoomControl)
if (!zoomCtrl) return
const scale = unref(currentScale)
if (type === "in") {
scale && zoomCtrl.zoom(scale + 0.25)
} else if (type === "out") {
scale && zoomCtrl.zoom(scale - 0.25)
} else {
zoomCtrl.zoom(type)
}
}
//Page Control
const pageControl = computed(() => vpvRef.value?.pageControl)
const currentPageInput = computed(() => pageControl.value?.currentPage)
const searchControl = computed(() => vpvRef.value?.searchControl)
const totalMatches = computed(() => searchControl.value?.searchMatches?.totalMatches)
const isNextPageButtonDisable = computed(() =>
pageControl.value?.currentPage === pageControl.value?.totalPages
)
const isPreviousPageButtonDisable = computed(() =>
pageControl.value?.currentPage === 1
)
const prevPage = () => {
const isFirstPage = pageControl.value?.currentPage === 1
if (isFirstPage) return
pageControl.value?.goToPage(pageControl.value?.currentPage - 1)
}
const nextPage = () => {
const isLastPage = pageControl.value?.currentPage === pageControl.value?.totalPages
if (isLastPage) return
pageControl.value?.goToPage(pageControl.value?.currentPage + 1)
}
const handleKeyPress = (event) => {
if (event.key === "Enter") {
handlePageInput(event)
}
}
//Handle Download
const downloadControl = computed(() => vpvRef.value?.downloadControl)
const handleDownloadFile = async () => {
await useFiles().downloadFile(props.fileId)
/*const downloadCtrl = unref(downloadControl)
if (!downloadCtrl) return
downloadCtrl.download()*/
}
watch(downloadControl, (downloadCtrl) => {
if (!downloadCtrl) return
downloadCtrl.onError = (error) => {
console.log("Download error", error)
}
downloadCtrl.onComplete = () => {
console.log("Download completed")
}
})
</script>
<template>
<div class="flex flex-col gap-4 justify-self-center">
<div class="flex items-center gap-4 text-[#7862FF] bg-pale-blue border-[#D7D1FB] rounded-lg p-2 justify-center">
<!-- Zoom out button -->
<UButton
@click="() => handleZoomTool('pageWidth')"
icon="i-heroicons-document-text"
variant="outline"
></UButton>
<UButton
@click="() => handleZoomTool('out')"
icon="i-heroicons-magnifying-glass-minus"
variant="outline"
></UButton>
<!-- Zoom in button -->
<UButton
@click="() => handleZoomTool('in')"
icon="i-heroicons-magnifying-glass-plus"
variant="outline"
></UButton>
<UButton
@click="handleDownloadFile"
variant="outline"
icon="i-heroicons-arrow-down-on-square"
/>
<UButton
@click="prevPage"
:disabled="isPreviousPageButtonDisable"
icon="i-heroicons-chevron-up"
variant="outline"
></UButton>
<!-- Page number input and total pages display -->
<div class="flex items-center text-sm font-normal">
<UInput
v-model="currentPageInput"
class="w-24 h-8 rounded-sm focus:outline-none"
@change="handleKeyPress"
>
<template #trailing>
/ {{ pageControl?.totalPages }}
</template>
</UInput>
</div>
<!-- Next page button -->
<UButton
@click="nextPage"
:disabled="isNextPageButtonDisable"
icon="i-heroicons-chevron-down"
variant="outline"
></UButton>
</div>
</div>
<div class="pdf-container">
<VPdfViewer
v-if="pdfSrc"
:src="pdfSrc"
style="height: 78vh; width: 100%;"
:toolbar-options="false"
ref="vpvRef"
/>
<div v-else>
<UProgress
class="mt-5 w-2/3 mx-auto"
animation="carousel"></UProgress>
</div>
</div></template>
<style scoped>
.pdf-container {
width: 100%;
height: 100%;
}
</style>

View File

@@ -1,29 +0,0 @@
<script setup>
const dataStore = useDataStore()
const profileStore = useProfileStore()
const supabase = useSupabaseClient()
const selectedProfile = ref(profileStore.activeProfile.id)
</script>
<template>
<USelectMenu
:options="profileStore.ownProfiles"
value-attribute="id"
class="w-40"
@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="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">{{profileStore.tenants.find(i => profileStore.profiles.find(i => i.id === selectedProfile).tenant === i.id).name}}</span>
</UButton>
<template #option="{option}">
{{profileStore.tenants.find(i => i.id === option.tenant).name}}
</template>
</USelectMenu>
</template>

View File

@@ -35,10 +35,10 @@ const item = ref({})
const setupPage = async () => { const setupPage = async () => {
if(props.mode === "show") { if(props.mode === "show") {
//Load Data for Show //Load Data for Show
item.value = await useSupabaseSelectSingle(props.type, props.id, dataType.supabaseSelectWithInformation || "*") item.value = await useEntities(props.type).selectSingle(props.id, dataType.supabaseSelectWithInformation || "*")
} else if(props.mode === "edit") { } else if(props.mode === "edit") {
//Load Data for Edit //Load Data for Edit
const data = JSON.stringify((await supabase.from(props.type).select().eq("id", props.id).single()).data) const data = JSON.stringify(await useEntities(props.type).selectSingle(props.id)/*(await supabase.from(props.type).select().eq("id", props.id).single()).data*/)
//await useSupabaseSelectSingle(type, route.params.id) //await useSupabaseSelectSingle(type, route.params.id)
item.value = data item.value = data
@@ -48,7 +48,7 @@ const setupPage = async () => {
} else if(props.mode === "list") { } else if(props.mode === "list") {
//Load Data for List //Load Data for List
items.value = await useSupabaseSelect(props.type, dataType.supabaseSelectWithInformation || "*", dataType.supabaseSortColumn,dataType.supabaseSortAscending || false) items.value = await useEntities(props.type).select(dataType.supabaseSelectWithInformation || "*", dataType.supabaseSortColumn,dataType.supabaseSortAscending || false)
} }
loaded.value = true loaded.value = true

View File

@@ -0,0 +1,27 @@
<script setup>
const auth = useAuthStore()
const selectedTenant = ref(auth.user.tenant_id)
</script>
<template>
<USelectMenu
:options="auth.tenants"
value-attribute="id"
class="w-40"
@change="auth.switchTenant(selectedTenant)"
v-model="selectedTenant"
>
<UButton color="gray" variant="ghost" :class="[open && 'bg-gray-50 dark:bg-gray-800']" class="w-full">
<UAvatar :alt="auth.activeTenantData?.name" size="md" />
<span class="truncate text-gray-900 dark:text-white font-semibold">{{auth.tenants.find(i => auth.activeTenant === i.id).name}}</span>
</UButton>
<template #option="{option}">
{{option.name}}
</template>
</USelectMenu>
</template>

View File

@@ -1,4 +1,5 @@
<script setup> <script setup>
const { isHelpSlideoverOpen } = useDashboard() const { isHelpSlideoverOpen } = useDashboard()
const { isDashboardSearchModalOpen } = useUIState() const { isDashboardSearchModalOpen } = useUIState()
const { metaSymbol } = useShortcuts() const { metaSymbol } = useShortcuts()
@@ -7,23 +8,26 @@ const dataStore = useDataStore()
const profileStore = useProfileStore() const profileStore = useProfileStore()
const supabase = useSupabaseClient() const supabase = useSupabaseClient()
const router = useRouter() const router = useRouter()
const auth = useAuthStore()
const items = computed(() => [ const items = computed(() => [
[{ [{
slot: 'account', slot: 'account',
label: '', label: '',
disabled: true disabled: true
}], [{ }], [/*{
label: 'Mein Profil', label: 'Mein Profil',
icon: 'i-heroicons-user', icon: 'i-heroicons-user',
to: `/profiles/show/${profileStore.activeProfile.id}` to: `/profiles/show/${profileStore.activeProfile.id}`
},*/{
label: 'Passwort ändern',
icon: 'i-heroicons-shield-check',
to: `/password-change`
},{ },{
label: 'Abmelden', label: 'Abmelden',
icon: 'i-heroicons-arrow-left-on-rectangle', icon: 'i-heroicons-arrow-left-on-rectangle',
click: async () => { click: async () => {
await supabase.auth.signOut() await auth.logout()
//await dataStore.clearStore()
await router.push('/login')
} }
}] }]
@@ -33,10 +37,10 @@ const items = computed(() => [
<template> <template>
<UDropdown mode="hover" :items="items" :ui="{ width: 'w-full', item: { disabled: 'cursor-text select-text' } }" :popper="{ strategy: 'absolute', placement: 'top' }" class="w-full"> <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 }"> <template #default="{ open }">
<UButton color="gray" variant="ghost" class="w-full" :label="profileStore.activeProfile.fullName" :class="[open && 'bg-gray-50 dark:bg-gray-800']"> <UButton color="gray" variant="ghost" class="w-full" :label="auth.user.email" :class="[open && 'bg-gray-50 dark:bg-gray-800']">
<template #leading> <!-- <template #leading>
<UAvatar :alt="profileStore.activeProfile ? profileStore.activeProfile.fullName : ''" size="xs" /> <UAvatar :alt="auth.user.email" size="xs" />
</template> </template>-->
<template #trailing> <template #trailing>
<UIcon name="i-heroicons-ellipsis-vertical" class="w-5 h-5 ml-auto" /> <UIcon name="i-heroicons-ellipsis-vertical" class="w-5 h-5 ml-auto" />
@@ -50,7 +54,7 @@ const items = computed(() => [
Angemeldet als Angemeldet als
</p> </p>
<p class="truncate font-medium text-gray-900 dark:text-white"> <p class="truncate font-medium text-gray-900 dark:text-white">
{{profileStore.activeProfile.email}} {{auth.user.email}}
</p> </p>
</div> </div>
</template> </template>

View File

@@ -7,10 +7,9 @@ const props = defineProps({
} }
}) })
const profileStore = useProfileStore()
</script> </script>
<template> <template>
<span v-if="props.row.driver">{{props.row.driver ? profileStore.getProfileById(props.row.driver).fullName : props.row.driver}}</span> <span v-if="props.row.driver">{{props.row.driver ? "" : props.row.driver}}</span>
</template> </template>

View File

@@ -13,7 +13,7 @@ const props = defineProps({
const incomingInvoices = ref({}) const incomingInvoices = ref({})
const setupPage = async () => { const setupPage = async () => {
incomingInvoices.value = (await supabase.from("incominginvoices").select().eq("tenant", profileStore.currentTenant)).data.filter(i => i.accounts.find(x => x.costCentre === props.item.id)) incomingInvoices.value = (await useEntities("incominginvoices").select()).filter(i => i.accounts.find(x => x.costCentre === props.item.id))
} }
setupPage() setupPage()

View File

@@ -1,15 +1,14 @@
<script setup> <script setup>
import dayjs from "dayjs"; import dayjs from "dayjs";
import {useSupabaseSelect} from "~/composables/useSupabase.js";
let unallocatedStatements = ref(0) let unallocatedStatements = ref(0)
let bankaccounts = ref([]) let bankaccounts = ref([])
const setupPage = async () => { const setupPage = async () => {
let bankstatements = (await useSupabaseSelect("bankstatements","*, statementallocations(*)","date",true)).filter(i => !i.archived) let bankstatements = (await useEntities("bankstatements").select("*, statementallocations(*)","date",true)).filter(i => !i.archived)
unallocatedStatements.value = bankstatements.filter(i => Number(calculateOpenSum(i)) !== 0).length unallocatedStatements.value = bankstatements.filter(i => Number(calculateOpenSum(i)) !== 0).length
bankaccounts.value = await useSupabaseSelect("bankaccounts") bankaccounts.value = await useEntities("bankaccounts").select()
} }
setupPage() setupPage()

View File

@@ -4,21 +4,20 @@ import dayjs from "dayjs";
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
const supabase = useSupabaseClient()
const dataStore = useDataStore()
const profileStore = useProfileStore()
let incomeData = ref({}) let incomeData = ref({})
let expenseData = ref({}) let expenseData = ref({})
const setup = async () => { const setup = async () => {
let incomeRawData = (await supabase.from("createddocuments").select().eq("tenant",profileStore.currentTenant).eq("state","Gebucht").in('type',['invoices','advanceInvoices','cancellationInvoices'])).data //let incomeRawData = (await supabase.from("createddocuments").select().eq("tenant",profileStore.currentTenant).eq("state","Gebucht").in('type',['invoices','advanceInvoices','cancellationInvoices'])).data
console.log(incomeRawData) let incomeRawData = (await useEntities("createddocuments").select()).filter(i => i.state === "Gebucht" && ['invoices','advanceInvoices','cancellationInvoices'].includes(i.type))
let incomeRawFilteredData = incomeRawData.filter(x => x.state === 'Gebucht' && incomeRawData.find(i => i.linkedDocument && i.linkedDocument.id === x.id && i.type === 'cancellationInvoices') && ['invoices','advanceInvoices'].includes(row.type)) let incomeRawFilteredData = incomeRawData.filter(x => x.state === 'Gebucht' && incomeRawData.find(i => i.linkedDocument && i.linkedDocument.id === x.id && i.type === 'cancellationInvoices') && ['invoices','advanceInvoices'].includes(row.type))
let expenseRawData =(await supabase.from("incominginvoices").select().eq("tenant",profileStore.currentTenant)).data //let expenseRawData =(await supabase.from("incominginvoices").select().eq("tenant",profileStore.currentTenant)).data
let withoutInvoiceRawData = (await supabase.from("statementallocations").select().eq("tenant",profileStore.currentTenant).not("account","is",null)).data let expenseRawData =(await useEntities("incominginvoices").select())
//let withoutInvoiceRawData = (await supabase.from("statementallocations").select().eq("tenant",profileStore.currentTenant).not("account","is",null)).data
let withoutInvoiceRawData = (await useEntities("statementallocations").select()).filter(i => i.account)
let withoutInvoiceRawDataExpenses = [] let withoutInvoiceRawDataExpenses = []
let withoutInvoiceRawDataIncomes = [] let withoutInvoiceRawDataIncomes = []
@@ -87,7 +86,7 @@ const setup = async () => {
} }
Object.keys(expenseMonths).forEach(month => { Object.keys(expenseMonths).forEach(month => {
let dates = Object.keys(expenseData.value).filter(i => i.split("-")[1] === month && i.split("-")[2] === dayjs().format("YY")) let dates = Object.keys(expenseData.value).filter(i => i.split("-")[1] === month && i.split("-")[2] === dayjs().format("YY"))
console.log(dates)
dates.forEach(date => { dates.forEach(date => {

View File

@@ -3,7 +3,6 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
const profileStore = useProfileStore(); const profileStore = useProfileStore();
const supabase = useSupabaseClient()
let unpaidInvoicesSum = ref(0) let unpaidInvoicesSum = ref(0)
let unpaidInvoicesCount = ref(0) let unpaidInvoicesCount = ref(0)
@@ -15,7 +14,7 @@ let draftInvoicesCount = ref(0)
let countPreparedOpenIncomingInvoices = ref(0) let countPreparedOpenIncomingInvoices = ref(0)
const setupPage = async () => { const setupPage = async () => {
let items = (await useSupabaseSelect("createddocuments","*, statementallocations(*), customer(id,name), linkedDocument(*)")).filter(i => !i.archived) let items = (await useEntities("createddocuments").select("*, statementallocations(*), customer(id,name), linkedDocument(*)")).filter(i => !i.archived)
let documents = items.filter(i => i.type === "invoices" ||i.type === "advanceInvoices") let documents = items.filter(i => i.type === "invoices" ||i.type === "advanceInvoices")
let draftDocuments = documents.filter(i => i.state === "Entwurf") let draftDocuments = documents.filter(i => i.state === "Entwurf")
@@ -25,11 +24,9 @@ const setupPage = async () => {
finalizedDocuments = finalizedDocuments.filter(x => (x.type === 'invoices' || x.type === 'advanceInvoices') && x.state === 'Gebucht' && !items.find(i => i.linkedDocument && i.linkedDocument.id === x.id)) finalizedDocuments = finalizedDocuments.filter(x => (x.type === 'invoices' || x.type === 'advanceInvoices') && x.state === 'Gebucht' && !items.find(i => i.linkedDocument && i.linkedDocument.id === x.id))
console.log(finalizedDocuments)
finalizedDocuments.forEach(i => { finalizedDocuments.forEach(i => {
//if(process.dev) console.log(i)
//if(process.dev) console.log(useSum().getCreatedDocumentSum(i, documents) - i.statementallocations.reduce((n,{amount}) => n + amount, 0))
if(dayjs().subtract(i.paymentDays,"days").isAfter(i.documentDate)) { if(dayjs().subtract(i.paymentDays,"days").isAfter(i.documentDate)) {
unpaidOverdueInvoicesSum.value += useSum().getCreatedDocumentSum(i, documents) - i.statementallocations.reduce((n,{amount}) => n + amount, 0) unpaidOverdueInvoicesSum.value += useSum().getCreatedDocumentSum(i, documents) - i.statementallocations.reduce((n,{amount}) => n + amount, 0)
unpaidOverdueInvoicesCount.value += 1 unpaidOverdueInvoicesCount.value += 1
@@ -45,7 +42,7 @@ const setupPage = async () => {
}) })
draftInvoicesCount.value = draftDocuments.length draftInvoicesCount.value = draftDocuments.length
countPreparedOpenIncomingInvoices.value = (await supabase.from("incominginvoices").select("id").eq("tenant",profileStore.currentTenant).eq("state", "Vorbereitet")).data.length countPreparedOpenIncomingInvoices.value = (await useEntities("incominginvoices").select("id, state")).filter(i => i.state === "Vorbereitet").length
} }

View File

@@ -1,12 +1,12 @@
<script setup> <script setup>
const openTasks = ref([]) const openTasks = ref([])
const supabase = useSupabaseClient()
const router = useRouter() const router = useRouter()
const auth = useAuthStore()
const setupPage = async () => { const setupPage = async () => {
openTasks.value = (await supabase.from("tasks").select().eq("tenant",useProfileStore().currentTenant).not("archived","is",true).neq("categorie","Abgeschlossen").eq("profile", useProfileStore().activeProfile.id)).data //TODO: BACKEND CHANGE Migrate to auth_users for profile
openTasks.value = (await useEntities("tasks").select()).filter(i => !i.archived && i.user_id === auth.user.id)
} }
setupPage() setupPage()

View File

@@ -3,7 +3,7 @@
const phasesCounter = ref({}) const phasesCounter = ref({})
const setupPage = async () => { const setupPage = async () => {
const projects = (await useSupabaseSelect("projects")).filter(i => !i.archived) const projects = (await useEntities("projects").select()).filter(i => !i.archived)
projects.forEach(project => { projects.forEach(project => {
if(project.phases && project.phases.length > 0){ if(project.phases && project.phases.length > 0){

View File

@@ -1,23 +1,14 @@
<script setup> <script setup>
const supabase = useSupabaseClient() const auth = useAuthStore()
const router = useRouter()
const profileStore = useProfileStore()
const tenant = ref({})
const setupPage = async () => {
tenant.value = (await supabase.from("tenants").select().eq("id",profileStore.currentTenant).single()).data
}
setupPage()
</script> </script>
<template> <template>
<div> <div>
<h1 class="font-bold text-xl">Willkommen zurück {{profileStore.activeProfile.fullName}}</h1> <h1 class="font-bold text-xl">Willkommen zurück {{auth.profile.full_name}}</h1>
<span v-if="tenant.id">bei {{tenant.name}}</span> <span v-if="auth.activeTenant">bei {{auth.activeTenantData.name}}</span>
</div> </div>
</template> </template>

View File

@@ -17,10 +17,14 @@ export const useCapacitor = () => {
return deviceInfo.model.toLowerCase().includes('iphone') return deviceInfo.model.toLowerCase().includes('iphone')
} }
const getIsNative = () => {
return Capacitor.isNativePlatform()
}
const getNetworkStatus = async () => { const getNetworkStatus = async () => {
return await Network.getStatus() return await Network.getStatus()
} }
return {getPlatform, getDeviceInfo, getNetworkStatus, getIsPhone} return {getPlatform, getDeviceInfo, getNetworkStatus, getIsPhone, getIsNative}
} }

143
composables/useEntities.ts Normal file
View File

@@ -0,0 +1,143 @@
import { useDataStore } from "~/stores/data"
export const useEntities = (
relation: string,
) => {
const dataStore = useDataStore()
const toast = useToast()
const router = useRouter()
const dataType = dataStore.dataTypes[relation]
const select = async (
select: string = "*",
sortColumn: string | null = null,
ascending: boolean = false,
noArchivedFiltering: boolean = false
) => {
const res = await useNuxtApp().$api(`/api/resource/${relation}`, {
method: "GET",
params: {
select,
sort: sortColumn || undefined,
asc: ascending
}
})
let data = res || []
if (dataType && dataType.isArchivable && !noArchivedFiltering) {
data = data.filter((i: any) => !i.archived)
}
return data
}
const selectSpecial = async (
select: string = "*",
sortColumn: string | null = null,
ascending: boolean = false,
) => {
const res = await useNuxtApp().$api(`/api/resource-special/${relation}`, {
method: "GET",
params: {
select,
sort: sortColumn || undefined,
asc: ascending
}
})
return res || []
}
const selectSingle = async (
idToEq: string | number,
select: string = "*",
withInformation: boolean = false
) => {
if (!idToEq) return null
const res = await useNuxtApp().$api(withInformation ? `/api/resource/${relation}/${idToEq}/${withInformation}` : `/api/resource/${relation}/${idToEq}`, {
method: "GET",
params: { select }
})
return res
}
const create = async (
payload: Record<string, any>,
noRedirect: boolean = false
) => {
const res = await useNuxtApp().$api(`/api/resource/${relation}`, {
method: "POST",
body: payload
})
toast.add({title: `${dataType.labelSingle} hinzugefügt`})
if(dataType.redirect && !noRedirect) {
if(dataType.isStandardEntity) {
await router.push(dataType.redirectToList ? `/standardEntity/${relation}` : `/standardEntity/${relation}/show/${res.id}`)
} else {
await router.push(dataType.redirectToList ? `/${relation}` : `/${relation}/show/${res.id}`)
}
}
//modal.close() TODO: Modal Close wenn in Modal
return res
}
const update = async (
id: string | number,
payload: Record<string, any>,
noRedirect: boolean = false
) => {
const res = await useNuxtApp().$api(`/api/resource/${relation}/${id}`, {
method: "PUT",
body: payload
})
toast.add({title: `${dataType.labelSingle} geändert`})
if(dataType.redirect && !noRedirect) {
if(dataType.isStandardEntity) {
await router.push(dataType.redirectToList ? `/standardEntity/${relation}` : `/standardEntity/${relation}/show/${res.id}`)
} else {
await router.push(dataType.redirectToList ? `/${relation}` : `/${relation}/show/${res.id}`)
}
}
//modal.close() TODO: Modal Close wenn in Modal
return res
}
/**
* Soft Delete = archived = true
*/
const archive = async (
id: string | number
) => {
const res = await useNuxtApp().$api(`/api/resource/${relation}/${id}`, {
method: "PUT",
body: { archived: true }
})
navigateTo(dataType.isStandardEntity ? `/standardEntity/${relation}` : `/${relation}`)
return res
}
return {select, create, update, archive, selectSingle, selectSpecial}
}

View File

@@ -1,5 +1,5 @@
export const useError = (resourceType) => { export const useErrorLogging = (resourceType) => {
const supabase = useSupabaseClient() const supabase = useSupabaseClient()
const toast = useToast() const toast = useToast()
const profileStore = useProfileStore() const profileStore = useProfileStore()

View File

@@ -1,211 +0,0 @@
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
}
})
}
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(*), contact(*)), 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}
}

129
composables/useFiles.ts Normal file
View File

@@ -0,0 +1,129 @@
export const useFiles = () => {
const supabase = useSupabaseClient()
const toast = useToast()
const auth = useAuthStore()
let bucket = "filesdev"
const uploadFiles = async (fileData, files,tags, upsert) => {
const uploadSingleFile = async (file) => {
//Create File Entry to Get ID for Folder
const formData = new FormData()
formData.append("file", file)
formData.append("meta", JSON.stringify(fileData))
const {fileReturn} = await useNuxtApp().$api("/api/files/upload",{
method: "POST",
body: formData
})
}
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 = []
data = await useEntities("files").select("*, incominginvoice(*), project(*), vendor(*), customer(*), contract(*), plant(*), createddocument(*), vehicle(*), product(*), profile(*), check(*), inventoryitem(*)")
const res = await useNuxtApp().$api("/api/files/presigned",{
method: "POST",
body: {
ids: data.map(i => i.id)
}
})
console.log(res)
return res.files
}
const selectSomeDocuments = async (documentIds, sortColumn = null, folder = null) => {
if(documentIds.length === 0) return []
const res = await useNuxtApp().$api("/api/files/presigned",{
method: "POST",
body: {
ids: documentIds
}
})
console.log(res)
return res.files
}
const selectDocument = async (id) => {
let documentIds = [id]
if(documentIds.length === 0) return []
const res = await useNuxtApp().$api("/api/files/presigned",{
method: "POST",
body: {
ids: documentIds
}
})
console.log(res)
return res.files[0]
}
const downloadFile = async (id?: string, ids?: string[], returnAsBlob: Boolean = false) => {
const url = id ? `/api/files/download/${id}` : `/api/files/download`
const body = ids ? { ids } : undefined
const res:any = await useNuxtApp().$api.raw(url, {
method: "POST",
body,
responseType: "blob", // wichtig!
})
// Dateiname bestimmen
let filename = "download"
if (id) {
// Einzeldatei → nimm den letzten Teil des Pfads aus Content-Disposition
const contentDisposition = res.headers?.get("content-disposition")
if (contentDisposition) {
const match = contentDisposition.match(/filename="?([^"]+)"?/)
if (match) filename = match[1]
}
} else {
filename = "dateien.zip"
}
// Direkt speichern
const blob = res._data as Blob
if(returnAsBlob) {
return blob
} else {
const link = document.createElement("a")
link.href = URL.createObjectURL(blob)
link.download = filename
link.click()
URL.revokeObjectURL(link.href)
}
}
return {uploadFiles, selectDocuments, selectSomeDocuments, selectDocument, downloadFile}
}

View File

@@ -24,7 +24,7 @@ export const useFunctions = () => {
} }
const useNextNumber = async (numberRange) => { const useNextNumber = async (numberRange) => {
const {data:{session:{access_token}}} = await supabase.auth.getSession() /*const {data:{session:{access_token}}} = await supabase.auth.getSession()
return (await axios({ return (await axios({
method: "POST", method: "POST",
@@ -35,7 +35,11 @@ export const useFunctions = () => {
headers: { headers: {
Authorization: `Bearer ${access_token}` Authorization: `Bearer ${access_token}`
} }
})).data.usedNumber })).data.usedNumber*/
return (await useNuxtApp().$api(`/api/functions/usenextnumber/${numberRange}`,)).usedNumber
} }
const useCreateTicket = async (subject,message,url,source) => { const useCreateTicket = async (subject,message,url,source) => {
@@ -80,9 +84,17 @@ export const useFunctions = () => {
} }
const useCreatePDF = async (invoiceData,path) => { const useCreatePDF = async (invoiceData,path) => {
const {data:{session:{access_token}}} = await supabase.auth.getSession() //const {data:{session:{access_token}}} = await supabase.auth.getSession()
const {data} = await axios({ const data = await useNuxtApp().$api(`/api/functions/createinvoicepdf`, {
method: "POST",
body: {
invoiceData: invoiceData,
backgroundPath: path,
}
})
/*const {data} = await axios({
method: "POST", method: "POST",
url: `${baseURL}/functions/createpdf`, url: `${baseURL}/functions/createpdf`,
data: { data: {
@@ -93,7 +105,7 @@ export const useFunctions = () => {
headers: { headers: {
Authorization: `Bearer ${access_token}` Authorization: `Bearer ${access_token}`
} }
}) })*/
console.log(data) console.log(data)

View File

@@ -0,0 +1,9 @@
export function usePermission() {
const auth = useAuthStore()
const has = (key: string) => {
return auth.hasPermission(key)
}
return { has }
}

View File

@@ -6,7 +6,6 @@ export const useSum = () => {
const getIncomingInvoiceSum = (invoice) => { const getIncomingInvoiceSum = (invoice) => {
let sum = 0 let sum = 0
invoice.accounts.forEach(account => { invoice.accounts.forEach(account => {
console.log(account)
sum += account.amountTax sum += account.amountTax
@@ -23,21 +22,9 @@ export const useSum = () => {
let total19 = 0 let total19 = 0
let total7 = 0 let total7 = 0
/*let usedadvanceinvoices = []
if(createddocument.usedAdvanceInvoices.length > 0) {
console.log(createddocument)
console.log(createddocument.usedAdvanceInvoices)
console.log((await supabase.from("createddocuments").select().in("id", createddocument.usedAdvanceInvoices)))
usedadvanceinvoices = (await supabase.from("createddocuments").select().in("id", createddocument.usedAdvanceInvoices)).data
console.log(usedadvanceinvoices)
}*/
createddocument.rows.forEach(row => { createddocument.rows.forEach(row => {
if(!['pagebreak','title','text'].includes(row.mode)){ if(!['pagebreak','title','text'].includes(row.mode)){
let rowPrice = Number(Number(row.quantity) * Number(row.price) * (1 - Number(row.discountPercent) /100) ).toFixed(3) let rowPrice = Number(Number(row.quantity) * Number(row.price) * (1 - Number(row.discountPercent) /100) ).toFixed(3)

View File

@@ -1,54 +0,0 @@
import {useDataStore} from "~/stores/data.js";
export const useSupabaseSelect = async (relation,select = '*', sortColumn = null, ascending = true,noArchivedFiltering = false) => {
const supabase = useSupabaseClient()
const profileStore = useProfileStore()
let data = null
const dataStore = useDataStore()
const dataType = dataStore.dataTypes[relation]
if(sortColumn !== null ) {
data = (await supabase
.from(relation)
.select(select)
.eq("tenant", profileStore.currentTenant)
.order(sortColumn, {ascending: ascending})).data
} else {
data = (await supabase
.from(relation)
.select(select)
.eq("tenant", profileStore.currentTenant)).data
}
if(dataType && dataType.isArchivable && !noArchivedFiltering) {
data = data.filter(i => !i.archived)
}
return data
}
export const useSupabaseSelectSingle = async (relation,idToEq,select = '*' ) => {
const supabase = useSupabaseClient()
const profileStore = useProfileStore()
let data = null
if(idToEq !== null) {
data = (await supabase
.from(relation)
.select(select)
.eq("tenant", profileStore.currentTenant)
.eq("id",idToEq)
.single()).data
} else {
data = (await supabase
.from(relation)
.select(select)
.eq("tenant", profileStore.currentTenant)
.single()).data
}
return data
}

28
composables/useUsers.ts Normal file
View File

@@ -0,0 +1,28 @@
import { useDataStore } from "~/stores/data"
export const useUsers = (
id: string | number,
) => {
const dataStore = useDataStore()
const toast = useToast()
const router = useRouter()
const getProfile = async () => {
const res = await useNuxtApp().$api(`/api/profiles/${id}`, {
method: "GET"
})
return res
}
return {getProfile}
}

View File

@@ -7,9 +7,8 @@ services:
# networks: # networks:
# - traefik # - traefik
environment: environment:
SUPABASE_URL: "https://uwppvcxflrcsibuzsbil.supabase.co" NUXT_PUBLIC_API_BASE: "https://backend.fedeo.io"
SUPABASE_KEY: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InV3cHB2Y3hmbHJjc2lidXpzYmlsIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MDA5MzgxOTQsImV4cCI6MjAxNjUxNDE5NH0.CkxYSQH0uLfwx9GVUlO6AYMU2FMLAxGMrwEKvyPv7Oo" # labels:
# labels:
# - "traefik.enable=true" # - "traefik.enable=true"
# - "traefik.docker.network=traefik" # - "traefik.docker.network=traefik"
# - "traefik.port=3000" # - "traefik.port=3000"

View File

@@ -7,11 +7,29 @@ const props = defineProps({
</script> </script>
<template> <template>
<div class="flex justify-center h-100 w-100"> <UCard class="w-1/2 mx-auto mt-10 text-center">
<div class="mt-20"> <template #header>
<h1 class="text-5xl text-center mb-3">Da ist etwas schief gelaufen</h1> <div class="text-center">
<UButton to="/">Zurück</UButton> <span class="text-xl text-center font-bold">Es gab ein Problem - {{error.statusCode}}</span>
</div> </div>
</template>
<UColorModeImage
light="/Logo.png"
dark="/Logo_Dark.png"
class="w-3/4 mx-auto "
/>
<UAccordion
class="mt-5"
:items="[{
label: 'Fehlerbeschreibung',
icon: 'i-heroicons-information-circle',
defaultOpen: false,
content: error
}]"
>
</div> </UAccordion>
<UButton to="/" class="mt-5">Zurück zur Startseite</UButton>
</UCard>
</template> </template>

View File

@@ -485,7 +485,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = App/App.entitlements; CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = GMCGQ8KK2P; DEVELOPMENT_TEAM = GMCGQ8KK2P;
INFOPLIST_FILE = App/Info.plist; INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.6; IPHONEOS_DEPLOYMENT_TARGET = 15.6;
@@ -493,7 +493,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.8; MARKETING_VERSION = 2.0;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = software.federspiel.fedeo; PRODUCT_BUNDLE_IDENTIFIER = software.federspiel.fedeo;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@@ -510,7 +510,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = App/App.entitlements; CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = GMCGQ8KK2P; DEVELOPMENT_TEAM = GMCGQ8KK2P;
INFOPLIST_FILE = App/Info.plist; INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.6; IPHONEOS_DEPLOYMENT_TARGET = 15.6;
@@ -518,7 +518,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.8; MARKETING_VERSION = 2.0;
PRODUCT_BUNDLE_IDENTIFIER = software.federspiel.fedeo; PRODUCT_BUNDLE_IDENTIFIER = software.federspiel.fedeo;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";

View File

@@ -13,6 +13,7 @@ def capacitor_pods
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios' pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
pod 'CapacitorDevice', :path => '../../node_modules/@capacitor/device' pod 'CapacitorDevice', :path => '../../node_modules/@capacitor/device'
pod 'CapacitorNetwork', :path => '../../node_modules/@capacitor/network' pod 'CapacitorNetwork', :path => '../../node_modules/@capacitor/network'
pod 'CapacitorPreferences', :path => '../../node_modules/@capacitor/preferences'
pod 'CapacitorPluginSafeArea', :path => '../../node_modules/capacitor-plugin-safe-area' pod 'CapacitorPluginSafeArea', :path => '../../node_modules/capacitor-plugin-safe-area'
pod 'CordovaPluginsStatic', :path => '../capacitor-cordova-ios-plugins' pod 'CordovaPluginsStatic', :path => '../capacitor-cordova-ios-plugins'
end end

View File

@@ -8,6 +8,8 @@ PODS:
- Capacitor - Capacitor
- CapacitorPluginSafeArea (4.0.0): - CapacitorPluginSafeArea (4.0.0):
- Capacitor - Capacitor
- CapacitorPreferences (6.0.3):
- Capacitor
- CordovaPluginsStatic (7.1.0): - CordovaPluginsStatic (7.1.0):
- CapacitorCordova - CapacitorCordova
- OneSignalXCFramework (= 5.2.10) - OneSignalXCFramework (= 5.2.10)
@@ -64,6 +66,7 @@ DEPENDENCIES:
- "CapacitorDevice (from `../../node_modules/@capacitor/device`)" - "CapacitorDevice (from `../../node_modules/@capacitor/device`)"
- "CapacitorNetwork (from `../../node_modules/@capacitor/network`)" - "CapacitorNetwork (from `../../node_modules/@capacitor/network`)"
- CapacitorPluginSafeArea (from `../../node_modules/capacitor-plugin-safe-area`) - CapacitorPluginSafeArea (from `../../node_modules/capacitor-plugin-safe-area`)
- "CapacitorPreferences (from `../../node_modules/@capacitor/preferences`)"
- CordovaPluginsStatic (from `../capacitor-cordova-ios-plugins`) - CordovaPluginsStatic (from `../capacitor-cordova-ios-plugins`)
- OneSignalXCFramework (< 6.0, >= 5.0) - OneSignalXCFramework (< 6.0, >= 5.0)
@@ -82,6 +85,8 @@ EXTERNAL SOURCES:
:path: "../../node_modules/@capacitor/network" :path: "../../node_modules/@capacitor/network"
CapacitorPluginSafeArea: CapacitorPluginSafeArea:
:path: "../../node_modules/capacitor-plugin-safe-area" :path: "../../node_modules/capacitor-plugin-safe-area"
CapacitorPreferences:
:path: "../../node_modules/@capacitor/preferences"
CordovaPluginsStatic: CordovaPluginsStatic:
:path: "../capacitor-cordova-ios-plugins" :path: "../capacitor-cordova-ios-plugins"
@@ -91,9 +96,10 @@ SPEC CHECKSUMS:
CapacitorDevice: 069faf433b3a99c3d5f0e500fbe634f60a8c6a84 CapacitorDevice: 069faf433b3a99c3d5f0e500fbe634f60a8c6a84
CapacitorNetwork: 30c2e78a0ed32530656cb426c8ee6c2caec10dbf CapacitorNetwork: 30c2e78a0ed32530656cb426c8ee6c2caec10dbf
CapacitorPluginSafeArea: 22031c3436269ca80fac90ec2c94bc7c1e59a81d CapacitorPluginSafeArea: 22031c3436269ca80fac90ec2c94bc7c1e59a81d
CapacitorPreferences: f3eadae2369ac3ab8e21743a2959145b0d1286a3
CordovaPluginsStatic: f722d4ff434f50099581e690d579b7c108f490e6 CordovaPluginsStatic: f722d4ff434f50099581e690d579b7c108f490e6
OneSignalXCFramework: 1a3b28dfbff23aabce585796d23c1bef37772774 OneSignalXCFramework: 1a3b28dfbff23aabce585796d23c1bef37772774
PODFILE CHECKSUM: ccfbce7f13cfefd953204fe26b280d6431731aa5 PODFILE CHECKSUM: d76fcd3d35c3f8c3708303de70ef45a76cc6e2b5
COCOAPODS: 1.16.2 COCOAPODS: 1.16.2

View File

@@ -3,19 +3,18 @@
import MainNav from "~/components/MainNav.vue"; import MainNav from "~/components/MainNav.vue";
import dayjs from "dayjs"; import dayjs from "dayjs";
import {useProfileStore} from "~/stores/profile.js";
import {useCapacitor} from "../composables/useCapacitor.js"; import {useCapacitor} from "../composables/useCapacitor.js";
import GlobalMessages from "~/components/GlobalMessages.vue"; import GlobalMessages from "~/components/GlobalMessages.vue";
import TenantDropdown from "~/components/TenantDropdown.vue";
const dataStore = useDataStore() const dataStore = useDataStore()
const profileStore = useProfileStore()
const colorMode = useColorMode() const colorMode = useColorMode()
const { isHelpSlideoverOpen } = useDashboard() const { isHelpSlideoverOpen } = useDashboard()
const supabase = useSupabaseClient()
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
profileStore.initializeData((await supabase.auth.getUser()).data.user.id) const auth = useAuthStore()
const month = dayjs().format("MM") const month = dayjs().format("MM")
@@ -65,8 +64,8 @@ const actions = [
] ]
const groups = computed(() => const groups = computed(() => [
[{ {
key: 'actions', key: 'actions',
commands: actions commands: actions
},{ },{
@@ -99,7 +98,8 @@ const groups = computed(() =>
commands: dataStore.projects.map(item => { return {id: item.id, label: item.name, to: `/projects/show/${item.id}`}}) commands: dataStore.projects.map(item => { return {id: item.id, label: item.name, to: `/projects/show/${item.id}`}})
} }
].filter(Boolean)) ].filter(Boolean))
const footerLinks = [/*{ const footerLinks = [
/*{
label: 'Invite people', label: 'Invite people',
icon: 'i-heroicons-plus', icon: 'i-heroicons-plus',
to: '/settings/members' to: '/settings/members'
@@ -112,56 +112,163 @@ const footerLinks = [/*{
</script> </script>
<template> <template>
<UDashboardLayout class="safearea" v-if="profileStore.loaded"> <div v-if="!auth.loading">
<div v-if="auth.activeTenantData?.locked === 'maintenance_tenant'">
<UDashboardPanel :width="250" :resizable="{ min: 200, max: 300 }" collapsible> <UContainer class="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900">
<UDashboardNavbar style="margin-top: env(safe-area-inset-top, 10px) !important;" :class="['!border-transparent']" :ui="{ left: 'flex-1' }"> <UCard class="max-w-lg text-center p-10">
<template #left> <UColorModeImage
<ProfileDropdown class="w-full" /> light="/Logo_Hell_Weihnachten.png"
</template> dark="/Logo_Dunkel_Weihnachten.png"
</UDashboardNavbar> class=" mx-auto my-10"
v-if="month === '12'"
<UDashboardSidebar id="sidebar"> />
<template #header> <UColorModeImage
<UDashboardSearchButton label="Suche..."/> light="/Logo.png"
</template> dark="/Logo_Dark.png"
class="mx-auto my-10"
<GlobalMessages/> v-else
/>
<MainNav/> <div class="flex justify-center mb-6">
<UIcon name="i-heroicons-exclamation-triangle-solid" class="w-16 h-16 text-yellow-500" />
<div class="flex-1" />
<template #footer>
<div class="flex flex-col w-full">
<UDashboardSidebarLinks :links="footerLinks" />
<UDivider class="sticky bottom-0" />
<UserDropdown style="margin-bottom: env(safe-area-inset-bottom, 10px) !important;"/>
</div> </div>
</template> <h1 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">
</UDashboardSidebar> Wartungsarbeiten
</UDashboardPanel> </h1>
<p class="text-gray-600 dark:text-gray-300 mb-8">
Dieser FEDEO Mandant wird derzeit gewartet. Bitte versuche es in einigen Minuten erneut oder verwende einen anderen Mandanten.
</p>
<div class="mx-auto text-left flex flex-row justify-between my-3" v-for="tenant in auth.tenants">
{{tenant.name}}
<UButton
:disabled="tenant.locked"
@click="auth.switchTenant(tenant.id)"
>Wählen</UButton>
</div>
<UDashboardPage>
<UDashboardPanel grow> </UCard>
<slot /> </UContainer>
</div>
<div v-else-if="auth.activeTenantData?.locked === 'maintenance'">
<UContainer class="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900">
<UCard class="max-w-lg text-center p-10">
<UColorModeImage
light="/Logo_Hell_Weihnachten.png"
dark="/Logo_Dunkel_Weihnachten.png"
class=" mx-auto my-10"
v-if="month === '12'"
/>
<UColorModeImage
light="/Logo.png"
dark="/Logo_Dark.png"
class="mx-auto my-10"
v-else
/>
<div class="flex justify-center mb-6">
<UIcon name="i-heroicons-exclamation-triangle-solid" class="w-16 h-16 text-yellow-500" />
</div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">
Wartungsarbeiten
</h1>
<p class="text-gray-600 dark:text-gray-300 mb-8">
FEDEO wird derzeit gewartet. Bitte versuche es in einigen Minuten erneut.
</p>
</UCard>
</UContainer>
</div>
<div v-else-if="auth.activeTenantData?.locked === 'no_subscription'">
<UContainer class="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900">
<UCard class="max-w-lg text-center p-10">
<UColorModeImage
light="/Logo_Hell_Weihnachten.png"
dark="/Logo_Dunkel_Weihnachten.png"
class=" mx-auto my-10"
v-if="month === '12'"
/>
<UColorModeImage
light="/Logo.png"
dark="/Logo_Dark.png"
class="mx-auto my-10"
v-else
/>
<div class="flex justify-center mb-6">
<UIcon name="i-heroicons-credit-card" class="w-16 h-16 text-red-600" />
</div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">
Kein Aktives Abonnement für diesen Mandant.
</h1>
<p class="text-gray-600 dark:text-gray-300 mb-8">
Bitte wenden Sie sich an den FEDEO Support um ein Abonnement zu erhalten oder verwenden Sie einen anderen Mandanten.
</p>
<div class="mx-auto text-left flex flex-row justify-between my-3" v-for="tenant in auth.tenants">
{{tenant.name}}
<UButton
:disabled="tenant.locked"
@click="auth.switchTenant(tenant.id)"
>Wählen</UButton>
</div>
</UCard>
</UContainer>
</div>
<UDashboardLayout class="safearea" v-else >
<UDashboardPanel :width="250" :resizable="{ min: 200, max: 300 }" collapsible>
<UDashboardNavbar style="margin-top: env(safe-area-inset-top, 10px) !important;" :class="['!border-transparent']" :ui="{ left: 'flex-1' }">
<template #left>
<TenantDropdown class="w-full" />
</template>
</UDashboardNavbar>
<UDashboardSidebar id="sidebar">
<!-- <template #header>
<UDashboardSearchButton label="Suche..."/>
</template>-->
<!--
<GlobalMessages/>
-->
<MainNav/>
<div class="flex-1" />
<template #footer>
<div class="flex flex-col w-full">
<UDashboardSidebarLinks :links="footerLinks" />
<UDivider class="sticky bottom-0" />
<UserDropdown style="margin-bottom: env(safe-area-inset-bottom, 10px) !important;"/>
</div>
</template>
</UDashboardSidebar>
</UDashboardPanel> </UDashboardPanel>
</UDashboardPage>
<UDashboardPage>
<UDashboardPanel grow>
<slot />
</UDashboardPanel>
</UDashboardPage>
<!-- ~/components/HelpSlideover.vue --> <HelpSlideover/>
<HelpSlideover/>
<!-- ~/components/NotificationsSlideover.vue --> <!--<NotificationsSlideover />
<NotificationsSlideover />
<ClientOnly>
<LazyUDashboardSearch :groups="groups" hide-color-mode/>
</ClientOnly>-->
</UDashboardLayout>
</div>
<ClientOnly>
<LazyUDashboardSearch :groups="groups" hide-color-mode/>
</ClientOnly>
</UDashboardLayout>
<div <div
v-else v-else
class="flex flex-col" class="flex flex-col"
@@ -178,12 +285,25 @@ const footerLinks = [/*{
class="w-1/3 mx-auto my-10" class="w-1/3 mx-auto my-10"
v-else v-else
/> />
<div v-if="profileStore.showProfileSelection">
<ProfileSelection/> <div v-if="!auth.activeTenant" class="w-1/2 mx-auto text-center">
<!-- Tenant Selection -->
<h3 class="text-center font-bold text-2xl mb-5">Kein Aktiver Mandant. Bitte wählen Sie ein Mandant.</h3>
<div class="mx-auto w-1/2 flex flex-row justify-between my-3" v-for="tenant in auth.tenants">
<span class="text-left">{{tenant.name}}</span>
<UButton
@click="auth.switchTenant(tenant.id)"
>Wählen</UButton>
</div>
<UButton
variant="outline"
color="rose"
@click="auth.logout()"
>Abmelden</UButton>
</div> </div>
<div v-else> <div v-else>
<UProgress animation="carousel" class="w-3/4 mx-auto mt-10" /> <UProgress animation="carousel" class="w-3/4 mx-auto mt-10" />
</div> </div>
@@ -192,7 +312,5 @@ const footerLinks = [/*{
</template> </template>
<style scoped> <style scoped>
.testclass {
color: red;
}
</style> </style>

View File

@@ -13,8 +13,9 @@ const { isHelpSlideoverOpen } = useDashboard()
const supabase = useSupabaseClient() const supabase = useSupabaseClient()
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
const auth = useAuthStore()
profileStore.initializeData((await supabase.auth.getUser()).data.user.id) //profileStore.initializeData((await supabase.auth.getUser()).data.user.id)
const month = dayjs().format("MM") const month = dayjs().format("MM")
@@ -64,40 +65,6 @@ const actions = [
] ]
const groups = computed(() =>
[{
key: 'actions',
commands: actions
},{
key: "customers",
label: "Kunden",
commands: dataStore.customers.map(item => { return {id: item.id, label: item.name, to: `/customers/show/${item.id}`}})
},{
key: "vendors",
label: "Lieferanten",
commands: dataStore.vendors.map(item => { return {id: item.id, label: item.name, to: `/vendors/show/${item.id}`}})
},{
key: "contacts",
label: "Ansprechpartner",
commands: dataStore.contacts.map(item => { return {id: item.id, label: item.fullName, to: `/contacts/show/${item.id}`}})
},{
key: "products",
label: "Artikel",
commands: dataStore.products.map(item => { return {id: item.id, label: item.name, to: `/products/show/${item.id}`}})
},{
key: "tasks",
label: "Aufgaben",
commands: dataStore.tasks.map(item => { return {id: item.id, label: item.name, to: `/tasks/show/${item.id}`}})
},{
key: "plants",
label: "Objekte",
commands: dataStore.plants.map(item => { return {id: item.id, label: item.name, to: `/plants/show/${item.id}`}})
},{
key: "projects",
label: "Projekte",
commands: dataStore.projects.map(item => { return {id: item.id, label: item.name, to: `/projects/show/${item.id}`}})
}
].filter(Boolean))
const footerLinks = [/*{ const footerLinks = [/*{
label: 'Invite people', label: 'Invite people',
icon: 'i-heroicons-plus', icon: 'i-heroicons-plus',
@@ -111,87 +78,163 @@ const footerLinks = [/*{
</script> </script>
<template> <template>
<UDashboardLayout class="safearea" v-if="profileStore.loaded"> <div v-if="!auth.loading">
<div v-if="auth.activeTenantData?.locked === 'maintenance_tenant'">
<UDashboardPanel :width="250" :resizable="{ min: 200, max: 300 }" collapsible> <UContainer class="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900">
<UDashboardNavbar style="margin-top: env(safe-area-inset-top, 10px) !important;" :class="['!border-transparent']" :ui="{ left: 'flex-1' }"> <UCard class="max-w-lg text-center p-10">
<template #left> <UColorModeImage
<ProfileDropdown class="w-full" /> light="/Logo_Hell_Weihnachten.png"
</template> dark="/Logo_Dunkel_Weihnachten.png"
</UDashboardNavbar> class=" mx-auto my-10"
v-if="month === '12'"
<UDashboardSidebar id="sidebar"> />
<template #header> <UColorModeImage
<UDashboardSearchButton v-if="!useCapacitor().getIsPhone()" label="Suche..."/> light="/Logo.png"
</template> dark="/Logo_Dark.png"
class="mx-auto my-10"
<MainNav/> v-else
/>
<div class="flex-1" /> <div class="flex justify-center mb-6">
<UIcon name="i-heroicons-exclamation-triangle-solid" class="w-16 h-16 text-yellow-500" />
<template #footer>
<div class="flex flex-col w-full">
<UDashboardSidebarLinks :links="footerLinks" />
<UDivider class="sticky bottom-0" />
<UserDropdown style="margin-bottom: env(safe-area-inset-bottom, 10px) !important;"/>
</div> </div>
</template> <h1 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">
</UDashboardSidebar> Wartungsarbeiten
</UDashboardPanel> </h1>
<p class="text-gray-600 dark:text-gray-300 mb-8">
Dieser FEDEO Mandant wird derzeit gewartet. Bitte versuche es in einigen Minuten erneut oder verwende einen anderen Mandanten.
</p>
<div class="mx-auto text-left flex flex-row justify-between my-3" v-for="tenant in auth.tenants">
{{tenant.name}}
<UButton
:disabled="tenant.locked"
@click="auth.switchTenant(tenant.id)"
>Wählen</UButton>
</div>
<UDashboardPage style="height: 90vh">
<UDashboardPanel grow>
<slot />
</UDashboardPanel>
</UDashboardPage>
<div class="mobileFooter bg-white dark:bg-gray-950"> </UCard>
<UButton </UContainer>
icon="i-heroicons-home"
to="/mobile/"
variant="ghost"
:color="route.fullPath === '/mobile' ? 'primary' : 'gray'"
/>
<UButton
icon="i-heroicons-clipboard-document-check"
to="/standardEntity/tasks"
variant="ghost"
:color="route.fullPath === '/standardEntity/tasks' ? 'primary' : 'gray'"
/>
<UButton
icon="i-heroicons-rectangle-stack"
to="/standardEntity/projects"
variant="ghost"
:color="route.fullPath === '/standardEntity/projects' ? 'primary' : 'gray'"
/>
<!-- <UButton
icon="i-heroicons-clock"
to="/workingtimes"
variant="ghost"
:color="route.fullPath === '/workingtimes' ? 'primary' : 'gray'"
/>-->
<UButton
icon="i-heroicons-bars-4"
to="/mobile/menu"
variant="ghost"
:color="route.fullPath === '/mobile/menu' ? 'primary' : 'gray'"
/>
</div> </div>
<div v-else-if="auth.activeTenantData?.locked === 'maintenance'">
<UContainer class="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900">
<UCard class="max-w-lg text-center p-10">
<UColorModeImage
light="/Logo_Hell_Weihnachten.png"
dark="/Logo_Dunkel_Weihnachten.png"
class=" mx-auto my-10"
v-if="month === '12'"
/>
<UColorModeImage
light="/Logo.png"
dark="/Logo_Dark.png"
class="mx-auto my-10"
v-else
/>
<div class="flex justify-center mb-6">
<UIcon name="i-heroicons-exclamation-triangle-solid" class="w-16 h-16 text-yellow-500" />
</div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">
Wartungsarbeiten
</h1>
<p class="text-gray-600 dark:text-gray-300 mb-8">
FEDEO wird derzeit gewartet. Bitte versuche es in einigen Minuten erneut.
</p>
</UCard>
</UContainer>
</div>
<div v-else-if="auth.activeTenantData?.locked === 'no_subscription'">
<UContainer class="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900">
<UCard class="max-w-lg text-center p-10">
<UColorModeImage
light="/Logo_Hell_Weihnachten.png"
dark="/Logo_Dunkel_Weihnachten.png"
class=" mx-auto my-10"
v-if="month === '12'"
/>
<UColorModeImage
light="/Logo.png"
dark="/Logo_Dark.png"
class="mx-auto my-10"
v-else
/>
<div class="flex justify-center mb-6">
<UIcon name="i-heroicons-credit-card" class="w-16 h-16 text-red-600" />
</div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">
Kein Aktives Abonnement für diesen Mandant.
</h1>
<p class="text-gray-600 dark:text-gray-300 mb-8">
Bitte wenden Sie sich an den FEDEO Support um ein Abonnement zu erhalten oder verwenden Sie einen anderen Mandanten.
</p>
<div class="mx-auto text-left flex flex-row justify-between my-3" v-for="tenant in auth.tenants">
{{tenant.name}}
<UButton
:disabled="tenant.locked"
@click="auth.switchTenant(tenant.id)"
>Wählen</UButton>
</div>
</UCard>
</UContainer>
</div>
<UDashboardLayout class="safearea" v-else>
<UDashboardPage style="height: 90vh">
<UDashboardPanel grow>
<slot />
</UDashboardPanel>
</UDashboardPage>
<div class="mobileFooter bg-white dark:bg-gray-950">
<UButton
icon="i-heroicons-home"
to="/mobile/"
variant="ghost"
:color="route.fullPath === '/mobile' ? 'primary' : 'gray'"
/>
<UButton
icon="i-heroicons-clipboard-document-check"
to="/standardEntity/tasks"
variant="ghost"
:color="route.fullPath === '/standardEntity/tasks' ? 'primary' : 'gray'"
/>
<UButton
icon="i-heroicons-rectangle-stack"
to="/standardEntity/projects"
variant="ghost"
:color="route.fullPath === '/standardEntity/projects' ? 'primary' : 'gray'"
/>
<!-- <UButton
icon="i-heroicons-clock"
to="/workingtimes"
variant="ghost"
:color="route.fullPath === '/workingtimes' ? 'primary' : 'gray'"
/>-->
<UButton
icon="i-heroicons-bars-4"
to="/mobile/menu"
variant="ghost"
:color="route.fullPath === '/mobile/menu' ? 'primary' : 'gray'"
/>
</div>
<!-- ~/components/HelpSlideover.vue --> <!-- ~/components/HelpSlideover.vue -->
<HelpSlideover/> <HelpSlideover/>
<!-- ~/components/NotificationsSlideover.vue --> <!-- ~/components/NotificationsSlideover.vue -->
<NotificationsSlideover /> <NotificationsSlideover />
</UDashboardLayout>
</div>
<ClientOnly>
<LazyUDashboardSearch :groups="groups" hide-color-mode/>
</ClientOnly>
</UDashboardLayout>
<div <div
v-else v-else
class="flex flex-col" class="flex flex-col"
@@ -208,15 +251,26 @@ const footerLinks = [/*{
class="w-1/3 mx-auto my-10" class="w-1/3 mx-auto my-10"
v-else v-else
/> />
<div v-if="dataStore.showProfileSelection"> <div v-if="!auth.activeTenant" class="w-full mx-auto text-center">
<ProfileSelection/> <!-- Tenant Selection -->
<h3 class="text-center font-bold text-2xl mb-5">Kein Aktiver Mandant. Bitte wählen Sie ein Mandant.</h3>
<div class="mx-auto w-5/6 flex flex-row justify-between my-3" v-for="tenant in auth.tenants">
<span class="text-left">{{tenant.name}}</span>
<UButton
@click="auth.switchTenant(tenant.id)"
>Wählen</UButton>
</div>
<UButton
variant="outline"
color="rose"
@click="auth.logout()"
>Abmelden</UButton>
</div> </div>
<div v-else> <div v-else>
<UProgress animation="carousel" class="w-3/4 mx-auto mt-10" /> <UProgress animation="carousel" class="w-3/4 mx-auto mt-10" />
</div> </div>
</div> </div>
</template> </template>

17
middleware/auth.global.ts Normal file
View File

@@ -0,0 +1,17 @@
export default defineNuxtRouteMiddleware(async (to, from) => {
const auth = useAuthStore()
// Wenn nicht eingeloggt → auf /login (außer er will schon dahin)
if (!auth.user && !["/login", "/password-reset"].includes(to.path)) {
return navigateTo("/login")
}
// Wenn eingeloggt → von /login auf /dashboard umleiten
if (auth.user && !auth.user?.must_change_password && to.path === "/login") {
return navigateTo("/")
} else if(auth.user && auth.user.must_change_password && to.path !== "/password-change") {
return navigateTo("/password-change")
}
})

View File

@@ -1,8 +0,0 @@
export default defineNuxtRouteMiddleware((to, _from) => {
const user = useSupabaseUser()
const router = useRouter()
if (!user.value) {
//useCookie('redirect', { path: '/' }).value = to.fullPath
return router.push("/login")
}
})

View File

@@ -3,7 +3,7 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
console.log(await useCapacitor().getIsPhone()) console.log(await useCapacitor().getIsPhone())
if(await useCapacitor().getIsPhone()) { if(await useCapacitor().getIsPhone() && _from.path !== '/mobile') {
return router.push('/mobile') return router.push('/mobile')
} }
}) })

View File

@@ -25,7 +25,7 @@ export default defineNuxtConfig({
transpile: ['@vuepic/vue-datepicker'] transpile: ['@vuepic/vue-datepicker']
}, },
modules: ['@pinia/nuxt', '@nuxt/ui', '@nuxt/content', '@nuxtjs/supabase', "nuxt-editorjs", '@nuxtjs/fontaine', 'nuxt-viewport', 'nuxt-tiptap-editor', '@nuxtjs/leaflet'], modules: ['@pinia/nuxt', '@nuxt/ui', '@nuxtjs/supabase', "nuxt-editorjs", '@nuxtjs/fontaine', 'nuxt-viewport', 'nuxt-tiptap-editor', '@nuxtjs/leaflet'],
routeRules: { routeRules: {
'/printing': {ssr: false} '/printing': {ssr: false}
@@ -34,7 +34,8 @@ export default defineNuxtConfig({
supabase: { supabase: {
key: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InV3cHB2Y3hmbHJjc2lidXpzYmlsIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MDA5MzgxOTQsImV4cCI6MjAxNjUxNDE5NH0.CkxYSQH0uLfwx9GVUlO6AYMU2FMLAxGMrwEKvyPv7Oo", key: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InV3cHB2Y3hmbHJjc2lidXpzYmlsIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MDA5MzgxOTQsImV4cCI6MjAxNjUxNDE5NH0.CkxYSQH0uLfwx9GVUlO6AYMU2FMLAxGMrwEKvyPv7Oo",
url: "https://uwppvcxflrcsibuzsbil.supabase.co" url: "https://uwppvcxflrcsibuzsbil.supabase.co",
redirect:false
}, },
vite: { vite: {
@@ -56,5 +57,13 @@ export default defineNuxtConfig({
prefix: "Tiptap" prefix: "Tiptap"
}, },
runtimeConfig: {
public: {
apiBase: '',
pdfLicense: ''
}
},
compatibilityDate: '2024-12-18' compatibilityDate: '2024-12-18'
}) })

0
package-lock.json generated
View File

View File

@@ -10,21 +10,21 @@
"postinstall": "nuxt prepare" "postinstall": "nuxt prepare"
}, },
"devDependencies": { "devDependencies": {
"@capacitor/cli": "^7.0.0",
"@nuxtjs/leaflet": "^1.2.3", "@nuxtjs/leaflet": "^1.2.3",
"@nuxtjs/supabase": "^1.1.4", "@nuxtjs/supabase": "^1.1.4",
"nuxt": "^3.14.1592", "nuxt": "^3.14.1592",
"nuxt-tiptap-editor": "^1.2.0", "nuxt-tiptap-editor": "^1.2.0",
"vite-plugin-pwa": "^0.17.3",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-router": "^4.2.5" "vue-router": "^4.2.5"
}, },
"dependencies": { "dependencies": {
"@capacitor/android": "^7.1.0", "@capacitor/android": "^7.0.0",
"@capacitor/cli": "^7.1.0", "@capacitor/core": "^7.0.0",
"@capacitor/core": "^7.1.0",
"@capacitor/device": "^7.0.0", "@capacitor/device": "^7.0.0",
"@capacitor/ios": "^7.1.0", "@capacitor/ios": "^7.0.0",
"@capacitor/network": "^7.0.0", "@capacitor/network": "^7.0.0",
"@capacitor/preferences": "^7.0.0",
"@fullcalendar/core": "^6.1.10", "@fullcalendar/core": "^6.1.10",
"@fullcalendar/daygrid": "^6.1.10", "@fullcalendar/daygrid": "^6.1.10",
"@fullcalendar/interaction": "^6.1.10", "@fullcalendar/interaction": "^6.1.10",
@@ -34,7 +34,6 @@
"@fullcalendar/timegrid": "^6.1.10", "@fullcalendar/timegrid": "^6.1.10",
"@fullcalendar/vue3": "^6.1.10", "@fullcalendar/vue3": "^6.1.10",
"@iconify/json": "^2.2.171", "@iconify/json": "^2.2.171",
"@nuxt/content": "^2.9.0",
"@nuxt/ui-pro": "^1.6.0", "@nuxt/ui-pro": "^1.6.0",
"@nuxtjs/fontaine": "^0.4.1", "@nuxtjs/fontaine": "^0.4.1",
"@nuxtjs/google-fonts": "^3.1.0", "@nuxtjs/google-fonts": "^3.1.0",
@@ -49,6 +48,7 @@
"@tiptap/vue-3": "^2.1.15", "@tiptap/vue-3": "^2.1.15",
"@vicons/ionicons5": "^0.12.0", "@vicons/ionicons5": "^0.12.0",
"@vue-leaflet/vue-leaflet": "^0.10.1", "@vue-leaflet/vue-leaflet": "^0.10.1",
"@vue-pdf-viewer/viewer": "^3.0.1",
"@vuepic/vue-datepicker": "^7.4.0", "@vuepic/vue-datepicker": "^7.4.0",
"@zip.js/zip.js": "^2.7.32", "@zip.js/zip.js": "^2.7.32",
"array-sort": "^1.0.0", "array-sort": "^1.0.0",
@@ -81,4 +81,4 @@
"vuetify": "^3.4.0-beta.1", "vuetify": "^3.4.0-beta.1",
"zebra-browser-print-wrapper": "^0.1.4" "zebra-browser-print-wrapper": "^0.1.4"
} }
} }

View File

@@ -1,11 +1,6 @@
<script setup> <script setup>
import dayjs from "dayjs"; import dayjs from "dayjs";
import {useSupabaseSelect} from "~/composables/useSupabase.js";
definePageMeta({
middleware: "auth"
})
defineShortcuts({ defineShortcuts({
'/': () => { '/': () => {
@@ -15,16 +10,14 @@ defineShortcuts({
} }
}) })
const profileStore = useProfileStore()
const tempStore = useTempStore() const tempStore = useTempStore()
const router = useRouter() const router = useRouter()
const supabase = useSupabaseClient()
const items = ref([]) const items = ref([])
const dataLoaded = ref(false) const dataLoaded = ref(false)
const setupPage = async () => { const setupPage = async () => {
items.value = (await supabase.from("accounts").select("*").order("number", {ascending:true})).data items.value = await useEntities("accounts").selectSpecial()
items.value = await Promise.all(items.value.map(async (i) => { items.value = await Promise.all(items.value.map(async (i) => {
let renderedAllocationsTemp = await renderedAllocations(i.id) let renderedAllocationsTemp = await renderedAllocations(i.id)
@@ -44,8 +37,8 @@ const setupPage = async () => {
const renderedAllocations = async (account) => { const renderedAllocations = async (account) => {
let statementallocations = (await supabase.from("statementallocations").select("*, bs_id(*)").eq("account", account).eq("tenant",profileStore.currentTenant).order("created_at",{ascending: true})).data let statementallocations = (await useEntities("statementallocations").select("*, bs_id(*)")).filter(i => i.account === account)
let incominginvoices = (await useSupabaseSelect("incominginvoices", "*, vendor(*)")).filter(i => i.accounts.find(x => x.account === account)) let incominginvoices = (await useEntities("incominginvoices").select("*, vendor(*)")).filter(i => i.accounts.find(x => x.account === account))
let tempstatementallocations = statementallocations.map(i => { let tempstatementallocations = statementallocations.map(i => {
return { return {

View File

@@ -1,21 +1,18 @@
<script setup> <script setup>
import dayjs from "dayjs"; import dayjs from "dayjs";
import {useSupabaseSelect} from "~/composables/useSupabase.js";
const supabase = useSupabaseClient()
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const profileStore = useProfileStore()
const itemInfo = ref(null) const itemInfo = ref(null)
const statementallocations = ref([]) const statementallocations = ref([])
const incominginvoices = ref([]) const incominginvoices = ref([])
const setup = async () => { const setup = async () => {
itemInfo.value = (await supabase.from("accounts").select("*").eq("id",route.params.id).single()).data itemInfo.value = (await useEntities("accounts").selectSpecial("*")).find(i => i.id === Number(route.params.id))
statementallocations.value = (await supabase.from("statementallocations").select("*, bs_id(*)").eq("account", route.params.id).eq("tenant",profileStore.currentTenant).order("created_at",{ascending: true})).data statementallocations.value = (await useEntities("statementallocations").select("*, bs_id(*)")).filter(i => i.account === Number(route.params.id))
incominginvoices.value = (await useSupabaseSelect("incominginvoices", "*, vendor(*)")).filter(i => i.accounts.find(x => x.account == route.params.id)) incominginvoices.value = (await useEntities("incominginvoices").select("*, vendor(*)")).filter(i => i.accounts.find(x => x.account === Number(route.params.id)))
} }
setup() setup()

View File

@@ -1,138 +0,0 @@
<script setup>
import dayjs from "dayjs";
definePageMeta({
middleware: "auth"
})
const dataStore = useDataStore()
const route = useRoute()
const supabase = useSupabaseClient()
const searchString = ref("")
const showAssigned = ref(false)
const selectedAccount = ref(0)
const filteredRows = computed(() => {
let statements = dataStore.bankStatements
if(!showAssigned.value) {
statements = statements.filter(statement => !statement.customerInvoice || !statement.vendorInvoice)
}
if(selectedAccount.value !== 0) {
statements = statements.filter(statement => statement.account === selectedAccount.value)
}
if(searchString.value.length > 0) {
statements = statements.filter(item => {
return Object.values(item).some((value) => {
return String(value).toLowerCase().includes(searchString.value.toLowerCase())
})
})
}
return statements
})
const showStatementSlideover = ref(false)
const selectedStatement = ref({})
const selectStatement = (statement) => {
selectedStatement.value = statement
showStatementSlideover.value = true
}
const statementColumns = [
{
key:"amount",
label: "Betrag"
},
{
key:"date",
label: "Datum",
sortable: true
},
{
key: "credName",
label: "Empfänger"
},
{
key: "debName",
label: "Sender"
},
{
key: "text",
label: "Verwendungszweck"
},
]
</script>
<template>
<USlideover
v-model="showStatementSlideover"
>
<UCard class="flex flex-col flex-1" :ui="{ body: { base: 'flex-1' }, ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
<DevOnly>
{{selectedStatement}}
</DevOnly>
</UCard>
</USlideover>
<InputGroup :gap="2">
<UInput
v-model="searchString"
placeholder="Suche..."
/>
<USelectMenu
:options="dataStore.bankAccounts.filter(account => account.used)"
v-model="selectedAccount"
option-attribute="iban"
value-attribute="id"
>
<template #label>
{{dataStore.bankAccounts.find(account => account.id === selectedAccount) ? dataStore.bankAccounts.find(account => account.id === selectedAccount).iban : "Kontoauswählen"}}
</template>
</USelectMenu>
<UCheckbox
v-model="showAssigned"
label="Zugeordnete Anzeigen"
/>
</InputGroup>
<UTable
:rows="filteredRows"
:columns="statementColumns"
@select="selectStatement"
>
<template #amount-data="{row}">
<span
v-if="row.amount >= 0"
class="text-primary-500"
>
{{row.amount.toFixed(2) + " €"}}
</span>
<span
v-if="row.amount < 0"
class="text-rose-600"
>
{{row.amount.toFixed(2) + " €"}}
</span>
</template>
<template #date-data="{row}">
{{dayjs(row.date).format("DD.MM.YY")}}
</template>
</UTable>
</template>
<style scoped>
</style>

View File

@@ -2,10 +2,6 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
definePageMeta({
middleware: "auth"
})
defineShortcuts({ defineShortcuts({
'/': () => { '/': () => {
//console.log(searchinput) //console.log(searchinput)
@@ -14,17 +10,15 @@ defineShortcuts({
} }
}) })
const profileStore = useProfileStore()
const tempStore = useTempStore() const tempStore = useTempStore()
const router = useRouter() const router = useRouter()
const supabase = useSupabaseClient()
const bankstatements = ref([]) const bankstatements = ref([])
const bankaccounts = ref([]) const bankaccounts = ref([])
const setupPage = async () => { const setupPage = async () => {
bankstatements.value = (await supabase.from("bankstatements").select("*, statementallocations(*)").is("archived",false).eq('tenant', profileStore.currentTenant).order("date", {ascending:false})).data bankstatements.value = (await useEntities("bankstatements").select("*, statementallocations(*)", "date", false))
bankaccounts.value = await useSupabaseSelect("bankaccounts") bankaccounts.value = await useEntities("bankaccounts").select()
} }
const templateColumns = [ const templateColumns = [

View File

@@ -3,9 +3,7 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import {filter} from "vuedraggable/dist/vuedraggable.common.js"; import {filter} from "vuedraggable/dist/vuedraggable.common.js";
definePageMeta({
middleware: "auth"
})
defineShortcuts({ defineShortcuts({
'backspace': () => { 'backspace': () => {
@@ -18,7 +16,6 @@ const profileStore = useProfileStore()
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const mode = ref(route.params.mode || "show") const mode = ref(route.params.mode || "show")
const supabase = useSupabaseClient()
const itemInfo = ref({statementallocations:[]}) const itemInfo = ref({statementallocations:[]})
const oldItemInfo = ref({}) const oldItemInfo = ref({})
@@ -38,21 +35,22 @@ const ownaccounts = ref([])
const loading = ref(true) const loading = ref(true)
const setup = async () => { const setup = async () => {
loading.value = true
if(route.params.id) { if(route.params.id) {
itemInfo.value = (await supabase.from("bankstatements").select("*, statementallocations(*, cd_id(*), ii_id(*))").eq("id",route.params.id).single()).data //dataStore.bankstatements.find(i => i.id === Number(route.params.id)) itemInfo.value = await useEntities("bankstatements").selectSingle(route.params.id,"*, statementallocations(*, cd_id(*), ii_id(*))", undefined, undefined, true)
} }
if(itemInfo.value) oldItemInfo.value = JSON.parse(JSON.stringify(itemInfo.value)) if(itemInfo.value) oldItemInfo.value = JSON.parse(JSON.stringify(itemInfo.value))
manualAllocationSum.value = calculateOpenSum.value manualAllocationSum.value = calculateOpenSum.value
createddocuments.value = (await useSupabaseSelect("createddocuments","*, statementallocations(*), customer(id,name)")) createddocuments.value = (await useEntities("createddocuments").select("*, statementallocations(*), customer(id,name)"))
const documents = createddocuments.value.filter(i => i.type === "invoices" ||i.type === "advanceInvoices") const documents = createddocuments.value.filter(i => i.type === "invoices" ||i.type === "advanceInvoices")
const incominginvoices = (await useSupabaseSelect("incominginvoices","*, statementallocations(*), vendor(id,name)")).filter(i => i.state === "Gebucht") const incominginvoices = (await useEntities("incominginvoices").select("*, statementallocations(*), vendor(id,name)")).filter(i => i.state === "Gebucht")
accounts.value = (await supabase.from("accounts").select().order("number",{ascending: true})).data accounts.value = (await useEntities("accounts").selectSpecial("*","number",true))
ownaccounts.value = (await supabase.from("ownaccounts").select()).data ownaccounts.value = (await useEntities("ownaccounts").select())
customers.value = (await supabase.from("customers").select()).data customers.value = (await useEntities("customers").select())
vendors.value = (await supabase.from("vendors").select()).data vendors.value = (await useEntities("vendors").select())
openDocuments.value = documents.filter(i => i.statementallocations.reduce((n,{amount}) => n + amount, 0).toFixed(2) !== useSum().getCreatedDocumentSum(i,createddocuments.value).toFixed(2)) openDocuments.value = documents.filter(i => i.statementallocations.reduce((n,{amount}) => n + amount, 0).toFixed(2) !== useSum().getCreatedDocumentSum(i,createddocuments.value).toFixed(2))
openDocuments.value = openDocuments.value.map(i => { openDocuments.value = openDocuments.value.map(i => {
@@ -71,15 +69,12 @@ const setup = async () => {
allocatedIncomingInvoices.value = incominginvoices.filter(i => i.statementallocations.find(x => x.bs_id === itemInfo.value.id)) allocatedIncomingInvoices.value = incominginvoices.filter(i => i.statementallocations.find(x => x.bs_id === itemInfo.value.id))
console.log(allocatedDocuments.value) console.log(allocatedDocuments.value)
console.log(allocatedIncomingInvoices.value) console.log(allocatedIncomingInvoices.value)
//openIncomingInvoices.value = (await useSupabaseSelect("incominginvoices","*, statementallocations(*), vendor(*)")).filter(i => i.statementallocations.length === 0 ) openIncomingInvoices.value = (await useEntities("incominginvoices").select("*, statementallocations(*), vendor(*)")).filter(i => i.statementallocations.reduce((n,{amount}) => n + amount, 0).toFixed(2) !== getInvoiceSum(i,false))
openIncomingInvoices.value = (await useSupabaseSelect("incominginvoices","*, statementallocations(*), vendor(*)")).filter(i => i.statementallocations.reduce((n,{amount}) => n + amount, 0).toFixed(2) !== getInvoiceSum(i,false))
//console.log(openIncomingInvoices.value) //console.log(openIncomingInvoices.value)
// return incominginvoices.value.filter(i => bankstatements.value.filter(x => x.assignments.find(y => y.type === 'incomingInvoice' && y.id === i.id)).length === 0) // return incominginvoices.value.filter(i => bankstatements.value.filter(x => x.assignments.find(y => y.type === 'incomingInvoice' && y.id === i.id)).length === 0)
let allocations = (await supabase.from("createddocuments").select(`*, statementallocations(*)`).eq("id",50)).data
loading.value = false loading.value = false
} }
@@ -151,15 +146,17 @@ const showMoreWithoutRecipe = ref(false)
const showMoreText = ref(false) const showMoreText = ref(false)
const saveAllocation = async (allocation) => { const saveAllocation = async (allocation) => {
//TODO: BACKEND CHANGE SAVE/REMOVE
console.log(allocation) console.log(allocation)
const {data,error} = await supabase.from("statementallocations").insert({ const res = await useNuxtApp().$api("/api/banking/statements",{
...allocation, method: "POST",
tenant: profileStore.currentTenant body: {
}).select() data: allocation
}
})
if(data) { if(res) {
await setup() await setup()
accountToSave.value = null accountToSave.value = null
vendorAccountToSave.value = null vendorAccountToSave.value = null
@@ -173,7 +170,9 @@ const saveAllocation = async (allocation) => {
} }
const removeAllocation = async (allocationId) => { const removeAllocation = async (allocationId) => {
const {data,error} = await supabase.from("statementallocations").delete().eq("id",allocationId) const res = await useNuxtApp().$api(`/api/banking/statements/${allocationId}`,{
method: "DELETE"
})
await setup() await setup()
} }
@@ -202,15 +201,7 @@ const archiveStatement = async () => {
let temp = itemInfo.value let temp = itemInfo.value
delete temp.statementallocations delete temp.statementallocations
await dataStore.updateItem("bankstatements", {...temp,archived:true}) await useEntities("bankstatements").archive(temp.id)
const {data,error} = await supabase.from("historyitems").insert({
createdBy: useProfileStore().activeProfile.id,
tenant: useProfileStore().currentTenant,
text: "Bankbuchung archiviert",
bankStatement: itemInfo.value.id
})
} }
</script> </script>
@@ -254,19 +245,12 @@ const archiveStatement = async () => {
</UBadge> </UBadge>
</template> </template>
<template #right> <template #right>
<ButtonWithConfirm <ArchiveButton
color="rose" color="rose"
variant="outline" variant="outline"
type="bankstatements"
@confirmed="archiveStatement" @confirmed="archiveStatement"
> />
<template #button>
Archivieren
</template>
<template #header>
<span class="text-md text-black font-bold">Archivieren bestätigen</span>
</template>
Möchten Sie die Kontobewegung wirklich archivieren?
</ButtonWithConfirm>
</template> </template>
</UDashboardNavbar> </UDashboardNavbar>
@@ -356,7 +340,9 @@ const archiveStatement = async () => {
<span class="font-semibold">Konto:</span> <span class="font-semibold">Konto:</span>
</td> </td>
<td> <td>
<!--
{{dataStore.getBankAccountById(itemInfo.account) ? dataStore.getBankAccountById(itemInfo.account).name || separateIBAN(dataStore.getBankAccountById(itemInfo.account).iban) : ""}} {{dataStore.getBankAccountById(itemInfo.account) ? dataStore.getBankAccountById(itemInfo.account).name || separateIBAN(dataStore.getBankAccountById(itemInfo.account).iban) : ""}}
-->
</td> </td>
</tr> </tr>
<!-- <tr class="flex-row flex justify-between"> <!-- <tr class="flex-row flex justify-between">

View File

@@ -7,15 +7,12 @@ import resourceTimelinePlugin from "@fullcalendar/resource-timeline";
import interactionPlugin from "@fullcalendar/interaction"; import interactionPlugin from "@fullcalendar/interaction";
import dayjs from "dayjs"; import dayjs from "dayjs";
definePageMeta({ //TODO BACKEND CHANGE COLOR IN TENANT FOR RENDERING
middleware: "auth"
})
//Config //Config
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const mode = ref(route.params.mode || "grid") const mode = ref(route.params.mode || "grid")
const supabase = useSupabaseClient()
const dataStore = useDataStore() const dataStore = useDataStore()
const profileStore = useProfileStore() const profileStore = useProfileStore()
@@ -130,13 +127,13 @@ const calendarOptionsTimeline = ref({
const loaded = ref(false) const loaded = ref(false)
const setupPage = async () => { const setupPage = async () => {
let tempData = (await useSupabaseSelect("events", "*")).filter(i => !i.archived) let tempData = (await useEntities("events").select()).filter(i => !i.archived)
let absencerequests = (await useSupabaseSelect("absencerequests", "*, profile(*)")).filter(i => !i.archived) let absencerequests = (await useEntities("absencerequests").select("*, profile(*)")).filter(i => !i.archived)
let projects = (await useSupabaseSelect("projects", "*")).filter(i => !i.archived) let projects = (await useEntities("projects").select( "*")).filter(i => !i.archived)
let inventoryitems = (await useSupabaseSelect("inventoryitems", "*")).filter(i => !i.archived) let inventoryitems = (await useEntities("inventoryitems").select()).filter(i => !i.archived)
let inventoryitemgroups = (await useSupabaseSelect("inventoryitemgroups", "*")).filter(i => !i.archived) let inventoryitemgroups = (await useEntities("inventoryitemgroups").select()).filter(i => !i.archived)
let profiles = (await useSupabaseSelect("profiles", "*")).filter(i => !i.archived) let profiles = (await useEntities("profiles").select()).filter(i => !i.archived)
let vehicles = (await useSupabaseSelect("vehicles", "*")).filter(i => !i.archived) let vehicles = (await useEntities("vehicles").select()).filter(i => !i.archived)
calendarOptionsGrid.value.initialEvents = [ calendarOptionsGrid.value.initialEvents = [
...tempData.map(event => { ...tempData.map(event => {

View File

@@ -1,108 +0,0 @@
<script setup>
definePageMeta({
layout: "default"
})
import dayjs from "dayjs";
const supabase = useSupabaseClient()
const user = useSupabaseUser()
const dataStore = useDataStore()
const profileStore = useProfileStore()
const selectedChat = ref({})
const messageText = ref("")
</script>
<template>
<div class="flex h-full">
<div class="w-1/5 mr-2 scrollList">
<a
v-for="chat in dataStore.chats"
@click="selectedChat = chat"
>
<UAlert
: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"
>
<template #title="{title}">
{{title}} <!-- TODO: Add Unread Counter <UBadge class="ml-1">{{dataStore.getMessagesByChatId(chat.id).filter(i => !i.read ).length}}</UBadge>-->
</template>
</UAlert>
</a>
</div>
<div class="w-full h-full px-5 flex flex-col justify-between" v-if="selectedChat.id">
<div class="flex flex-col mt-5 scrollList">
<div
v-for="message in dataStore.getMessagesByChatId(selectedChat.id)"
>
<div class="flex justify-end mb-4" v-if="message.origin === user.id">
<div
class="mr-2 py-3 px-4 bg-primary-400 rounded-bl-3xl rounded-tl-3xl rounded-tr-xl text-white"
>
{{message.text}}
</div>
<UAvatar
:alt="profileStore.getProfileById(message.origin) ? profileStore.getProfileById(message.origin).fullName : ''"
size="md"
/>
</div>
<div class="flex justify-start mb-4" v-else>
<UAvatar
:alt="profileStore.getProfileById(message.origin) ? profileStore.getProfileById(message.origin).fullName : ''"
size="md"
/>
<div
class="ml-2 py-3 px-4 bg-gray-400 rounded-br-3xl rounded-tr-3xl rounded-tl-xl text-white"
>
{{message.text}}
</div>
</div>
</div>
</div>
<div class="py-5">
<UButtonGroup class="w-full">
<UInput
variant="outline"
color="primary"
placeholder="Neue Nachricht"
v-model="messageText"
@keyup.enter="dataStore.createNewItem('messages',{
text: messageText,
origin: user.id,
destination: selectedChat.id
})"
class="flex-auto"
/>
<UButton
@click="dataStore.createNewItem('messages',{
text: messageText,
origin: user.id,
destination: selectedChat.id
})"
>
Senden
</UButton>
</UButtonGroup>
</div>
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -1,119 +0,0 @@
<script setup>
const profileStore = useProfileStore()
const supabase = useSupabaseClient()
const profiles = ref([])
const itemInfo = ref({})
const selectedProfiles = ref([])
const setup = async () => {
profiles.value = await useSupabaseSelect("profiles")
selectedProfiles.value = [profileStore.activeProfile.id]
}
setup()
const createChat = async () => {
const {data,error} = await supabase.from("chats").insert({
tenant: profileStore.currentTenant,
name: itemInfo.value.name
}).select()
if(error) {
console.log(error)
} else if(data){
console.log(data)
let memberships = selectedProfiles.value.map(i => {
return {
profile_id: i,
chat_id: data[0].id
}
})
const {error} = await supabase.from("chatmembers").insert(memberships)
if(error) {
console.log(error)
} else {
navigateTo("/chats")
}
}
}
</script>
<template>
<UDashboardNavbar>
<template #left>
<UButton
icon="i-heroicons-chevron-left"
variant="outline"
@click="navigateTo(`/chats`)"
>
Chats
</UButton>
</template>
<template #center>
<h1
v-if="itemInfo"
class="text-xl font-medium"
>Chat Erstellen</h1>
</template>
<template #right>
<UButton
@click="createChat"
>
Erstellen
</UButton>
<UButton
@click="navigateTo(`/chats`)"
color="red"
class="ml-2"
>
Abbrechen
</UButton>
</template>
</UDashboardNavbar>
<UDashboardPanelContent>
<UForm>
<UFormGroup
label="Name:"
>
<UInput
v-model="itemInfo.name"
/>
</UFormGroup>
<UFormGroup
label="Beteiligte Benutzer:"
>
<USelectMenu
v-model="selectedProfiles"
:options="profiles"
option-attribute="fullName"
value-attribute="id"
searchable
multiple
:search-attributes="['fullName']"
>
<template #label>
{{selectedProfiles.length > 0 ? selectedProfiles.map(i => profileStore.getProfileById(i).fullName).join(", ") : "Keine Benutzer ausgewählt"}}
</template>
</USelectMenu>
</UFormGroup>
</UForm>
</UDashboardPanelContent>
</template>
<style scoped>
</style>

View File

@@ -1,125 +0,0 @@
<script setup>
definePageMeta({
middleware: "auth"
})
defineShortcuts({
'/': () => {
//console.log(searchinput)
//searchinput.value.focus()
document.getElementById("searchinput").focus()
},
'+': () => {
router.push("/chats/create")
},
'Enter': {
usingInput: true,
handler: () => {
router.push(`/chats/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 router = useRouter()
const items = ref([])
const selectedItem = ref(0)
const setup = async () => {
items.value = await useSupabaseSelect("chats","*, profiles(*)")
//items.value = await useSupabaseSelect("chats","*, profiles(*), chatmessages(*)")
}
const templateColumns = [
{
key: "name",
label: "Name"
},{
key: "profiles",
label: "Mitglieder"
},
]
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)
})
setup()
</script>
<template>
<UDashboardNavbar title="Chats" :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(`/chats/create`)">+ Chat</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(`/chats/show/${i.id}`) "
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Chats anzuzeigen' }"
>
<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 #profiles-data="{row}">
{{row.profiles.map(i => i.fullName).join(", ")}}
</template>
</UTable>
</template>
<style scoped>
</style>

View File

@@ -1,122 +0,0 @@
<script setup>
import { format, isToday } from 'date-fns'
import dayjs from "dayjs"
definePageMeta({
middleware: "auth"
})
defineShortcuts({
' ': () => {
document.getElementById("textinput").focus()
},
})
const itemInfo = ref({})
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(profileStore.activeProfile.id))
for await (const message of unseenMessages){
await supabase.from("chatmessages").update({seenBy: [...message.seenBy, profileStore.activeProfile.id]}).eq("id",message.id)
}
}
setup()
const messageText = ref("")
const sendMessage = async () => {
if(messageText.value.length > 0) {
const message = {
origin: profileStore.activeProfile.id,
destinationchat: itemInfo.value.id,
text: messageText.value,
tenant: profileStore.currentTenant,
seenBy: [profileStore.activeProfile.id]
}
const {data,error} = await supabase.from("chatmessages").insert(message)
if(error) {
console.log(error)
} else {
//Reset
messageText.value = ""
//Create Notifications
let notifications = itemInfo.value.profiles.filter(i => i.id !== profileStore.activeProfile.id).map(i => {
return {
tenant: profileStore.currentTenant,
profile: i.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
}
})
console.log(notifications)
const {error} = await supabase.from("notifications").insert(notifications)
setup()
}
}
}
</script>
<template>
<UDashboardNavbar :title="itemInfo.name">
<template #center>
</template>
<template #right>
<UAvatarGroup size="sm" :max="2">
<UAvatar
v-if="itemInfo.profiles"
v-for="avatar in itemInfo.profiles.map(i => i.fullName)"
:alt="avatar" size="sm" />
</UAvatarGroup>
</template>
</UDashboardNavbar>
<div class="scrollList p-5">
<UAlert
: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: 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>
<div class="flex flex-row justify-between p-5">
<UInput
class="flex-auto mr-2"
v-model="messageText"
@keyup.enter="sendMessage"
placeholder="Deine Nachricht"
id="textinput"
/>
<UButton
icon="i-heroicons-chevron-double-right-solid"
@click="sendMessage"
:disabled="messageText.length === 0"
>
</UButton>
</div>
</template>
<style scoped>
</style>

View File

@@ -1,84 +0,0 @@
<script setup>
import dayjs from "dayjs"
const supabase = useSupabaseClient()
const dataStore = useDataStore()
const profileStore = useProfileStore()
const router = useRouter()
const items = ref([])
const setup = async () => {
items.value = (await supabase.from("historyitems").select().like('text',`%@${profileStore.activeProfile.username}%`)/*.textSearch("text", `'@${profileStore.activeProfile.username}'`)*/.order("created_at")).data
}
const navigateToHistoryItem = (item) => {
/*if(item.customer) {
router.push(`/customers/show/${item.customer}`)
} else if(item.vendor) {
router.push(`/vendors/show/${item.vendor}`)
} else if(item.project) {
router.push(`/projects/show/${item.project}`)
} else if(item.plant) {
router.push(`/plants/show/${item.plant}`)
} else if(item.incomingInvoice) {
router.push(`/incomingInvoices/show/${item.incomingInvoice}`)
}/!* else if(item.document) {
router.push(`/documents/show/${item.document}`)
}*!/ else if(item.contact) {
router.push(`/contacts/show/${item.contact}`)
} else if(item.inventoryitem) {
router.push(`/inventoryitems/show/${item.inventoryitem}`)
} else if(item.product) {
router.push(`/products/show/${item.product}`)
} else if(item.profile) {
router.push(`/profiles/show/${item.profile}`)
} else if(item.absenceRequest) {
router.push(`/absenceRequests/show/${item.absenceRequest}`)
} else if(item.event) {
router.push(`/events/show/${item.event}`)
} else if(item.task) {
router.push(`/tasks/show/${item.task}`)
} else if(item.vehicle) {
router.push(`/vehicle/show/${item.vehicle}`)
} else if(item.bankStatement) {
router.push(`/bankStatements/show/${item.bankStatement}`)
} else if(item.space) {
router.push(`/spaces/show/${item.space}`)
} else if(item.trackingtrip) {
router.push(`/trackingtrips/show/${item.trackingtrip}`)
}*/
if(item.config && item.config.type !== "document") {
router.push(`/${item.config.type}s/show/${item.config.id}`)
}
}
setup()
</script>
<template>
<UDashboardNavbar title="Erwähnungen in Logbüchern" :badge="items.length">
</UDashboardNavbar>
<UTable
:rows="items"
@select="navigateToHistoryItem"
:columns="[{key:'created_at',label:'Datum'},{key:'config',label:'Typ'},{key:'text',label:'Text'}]"
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Erwähnungen anzuzeigen' }"
class="w-full"
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
>
<template #config-data="{row}">
<span v-if="row.config">{{dataStore.dataTypes[row.config.type + "s"].labelSingle}}</span>
</template>
<template #created_at-data="{row}">
{{dayjs(row.created_at).format("HH:mm DD.MM.YYYY")}}
</template>
</UTable>
</template>
<style scoped>
</style>

View File

@@ -1,340 +0,0 @@
<script setup>
import dayjs from "dayjs";
import {useSupabaseSelectSingle} from "~/composables/useSupabase.js";
definePageMeta({
middleware: "auth"
})
defineShortcuts({
'backspace': () => {
router.push("/contacts")
},
'arrowleft': () => {
if(openTab.value > 0){
openTab.value -= 1
}
},
'arrowright': () => {
if(openTab.value < 3) {
openTab.value += 1
}
},
})
const dataStore = useDataStore()
const profileStore = useProfileStore()
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({
active: true,
profiles: [profileStore.activeProfile.id]
})
const oldItemInfo = ref({})
//Functions
const setupPage = async () => {
if(mode.value === "show"){
itemInfo.value = await useSupabaseSelectSingle("contacts",route.params.id,"*, customer(id, name), vendor(id,name) ")
} else if(mode.value === "edit") {
itemInfo.value = await useSupabaseSelectSingle("contacts",route.params.id,"*")
}
if(mode.value === "create") {
let query = route.query
if(query.customer) itemInfo.value.customer = Number(query.customer)
if(query.vendor) itemInfo.value.vendor = Number(query.vendor)
}
if(itemInfo.value) oldItemInfo.value = JSON.parse(JSON.stringify(itemInfo.value))
}
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(`/contacts`)"
>
Ansprechpartner
</UButton>
</template>
<template #center>
<h1
v-if="itemInfo"
:class="['text-xl','font-medium', ... itemInfo.active ? ['text-primary'] : ['text-rose-500']]"
>{{itemInfo.id ? `Ansprechpartner: ${itemInfo.fullName}` : (mode === 'create' ? 'Ansprechpartner erstellen' : 'Ansprechpartner bearbeiten')}}</h1>
</template>
<template #right>
<UButton
v-if="mode === 'edit'"
@click="dataStore.updateItem('contacts',{...itemInfo, fullName: itemInfo.firstName + ' ' + itemInfo.lastName},oldItemInfo)"
>
Speichern
</UButton>
<UButton
v-else-if="mode === 'create'"
@click="dataStore.createNewItem('contacts',{...itemInfo, fullName: itemInfo.firstName + ' ' + itemInfo.lastName})"
>
Erstellen
</UButton>
<UButton
@click="router.push(itemInfo.id ? `/contacts/show/${itemInfo.value.id}` : `/contacts/`)"
color="red"
class="ml-2"
v-if="mode === 'edit' || mode === 'create'"
>
Abbrechen
</UButton>
<UButton
v-if="mode === 'show'"
@click="router.push(`/contacts/edit/${itemInfo.id}`)"
>
Bearbeiten
</UButton>
</template>
<!-- <template #badge v-if="itemInfo">
<UBadge
v-if="itemInfo.active"
>
Kontakt aktiv
</UBadge>
<UBadge
v-else
color="red"
>
Kontakt inaktiv
</UBadge>
</template>-->
</UDashboardNavbar>
<UTabs
:items="[{label: 'Informationen'}, {label: 'Logbuch'}]"
v-if="itemInfo && mode == 'show'"
class="p-5"
v-model="openTab"
>
<template #item="{item}">
<div v-if="item.label === 'Informationen'" class="flex flex-row mt-5">
<div class="w-1/2 mr-5">
<UCard>
<Toolbar>
<UButton
@click="router.push(`/email/new?to=${itemInfo.email}`)"
icon="i-heroicons-envelope"
:disabled="!itemInfo.email"
>
E-Mail
</UButton>
</Toolbar>
<table class="w-full">
<tr>
<td>Kunde: </td>
<td><nuxt-link v-if="itemInfo.customer" :to="`/customers/show/${itemInfo.customer?.id}`">{{itemInfo?.customer?.name}}</nuxt-link></td>
</tr>
<tr>
<td>Lieferant: </td>
<td><nuxt-link v-if="itemInfo.vendor" :to="`/customers/show/${itemInfo.vendor?.id}`">{{itemInfo?.vendor?.name}}</nuxt-link></td>
</tr>
<tr>
<td>E-Mail:</td>
<td>{{itemInfo.email}}</td>
</tr>
<tr>
<td>Mobil:</td>
<td>{{itemInfo.phoneMobile}}</td>
</tr>
<tr>
<td>Festnetz:</td>
<td>{{itemInfo.phoneHome}}</td>
</tr>
<tr>
<td>Rolle:</td>
<td>{{itemInfo.role}}</td>
</tr>
<tr>
<td>Geburtstag:</td>
<td>{{itemInfo.birthday ? dayjs(itemInfo.birthday).format("DD.MM.YYYY") : ""}}</td>
</tr>
<tr>
<td>Notizen:</td>
<td>{{itemInfo.notes}}</td>
</tr>
</table>
</UCard>
</div>
<div class="w-1/2">
<UCard>
<HistoryDisplay
type="contact"
v-if="itemInfo"
:element-id="itemInfo.id"
/>
</UCard>
</div>
</div>
</template>
</UTabs>
<UForm
v-else-if="mode == 'edit' || mode == 'create'"
class="p-5"
>
<UFormGroup
label="Anrede:"
>
<UInput
v-model="itemInfo.salutation"
/>
</UFormGroup>
<UFormGroup
label="Vorname:"
>
<UInput
v-model="itemInfo.firstName"
/>
</UFormGroup>
<UFormGroup
label="Nachname:"
>
<UInput
v-model="itemInfo.lastName"
/>
</UFormGroup>
<UFormGroup
label="Kunde:"
>
<USelectMenu
v-model="itemInfo.customer"
option-attribute="name"
value-attribute="id"
:options="dataStore.customers"
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="Lieferant:"
>
<USelectMenu
v-model="itemInfo.vendor"
option-attribute="name"
value-attribute="id"
:options="dataStore.vendors"
searchable
:search-attributes="['name']"
>
<template #label>
{{dataStore.vendors.find(vendor => vendor.id === itemInfo.vendor) ? dataStore.vendors.find(vendor => vendor.id === itemInfo.vendor).name : "Lieferant auswählen"}}
</template>
</USelectMenu>
</UFormGroup>
<UFormGroup
label="Kontakt aktiv:"
>
<UCheckbox
v-model="itemInfo.active"
/>
</UFormGroup>
<UFormGroup
label="E-Mail:"
>
<UInput
v-model="itemInfo.email"
/>
</UFormGroup>
<UFormGroup
label="Mobil:"
>
<UInput
v-model="itemInfo.phoneMobile"
/>
</UFormGroup>
<UFormGroup
label="Festnetz:"
>
<UInput
v-model="itemInfo.phoneHome"
/>
</UFormGroup>
<UFormGroup
label="Rolle:"
>
<UInput
v-model="itemInfo.role"
/>
</UFormGroup>
<UFormGroup
label="Geburtstag:"
>
<UPopover :popper="{ placement: 'bottom-start' }">
<UButton
variant="outline"
icon="i-heroicons-calendar-days-20-solid"
:label="itemInfo.birthday ? dayjs(itemInfo.birthday).format('DD.MM.YYYY') : 'Datum auswählen'"
/>
<template #panel="{ close }">
<LazyDatePicker
v-model="itemInfo.birthday"
mode="date"
/>
</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:"
>
<UTextarea
v-model="itemInfo.notes"
/>
</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,26 +0,0 @@
<template>
<EntityList
type="contacts"
:items="items"
></EntityList>
</template>
<script setup>
import EntityList from "~/components/EntityList.vue";
definePageMeta({
middleware: "auth"
})
const items = ref([])
const setupPage = async () => {
items.value = await useSupabaseSelect("contacts","*, customer(name), vendor(name)")
}
setupPage()
</script>
<style scoped>
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -30,26 +30,7 @@
</template> </template>
</UDashboardNavbar> </UDashboardNavbar>
<UDashboardToolbar> <UDashboardToolbar>
<template #left>
<!-- <UCheckbox
v-model="showDrafts"
label="Entwürfe Anzeigen"
class="my-auto mr-3"
/>-->
<USelectMenu
v-model="selectedTypes"
icon="i-heroicons-adjustments-horizontal-solid"
:options="templateTypes"
multiple
class="hidden lg:block"
by="key"
@change="tempStore.modifyFilter('createddocuments',selectedTypes)"
>
<template #label>
Typ
</template>
</USelectMenu>
</template>
<template #right> <template #right>
<USelectMenu <USelectMenu
@@ -66,10 +47,11 @@
</template> </template>
</USelectMenu> </USelectMenu>
<USelectMenu <USelectMenu
v-if="selectableFilters.length > 0"
icon="i-heroicons-adjustments-horizontal-solid" icon="i-heroicons-adjustments-horizontal-solid"
multiple multiple
v-model="selectedFilters" v-model="selectedFilters"
:options="['Nur offene anzeigen']" :options="selectableFilters"
:color="selectedFilters.length > 0 ? 'primary' : 'white'" :color="selectedFilters.length > 0 ? 'primary' : 'white'"
:ui-menu="{ width: 'min-w-max' }" :ui-menu="{ width: 'min-w-max' }"
> >
@@ -79,8 +61,16 @@
</USelectMenu> </USelectMenu>
</template> </template>
</UDashboardToolbar> </UDashboardToolbar>
<UTabs :items="selectedTypes" class="m-3"> <UTabs :items="selectedTypes" class="m-3">
<template #default="{item}">
{{item.label}}
<UBadge
variant="outline"
class="ml-2"
>
{{filteredRows.filter(i => item.key === 'invoices' ? ['invoices','advanceInvoices','cancellationInvoices'].includes(i.type) : item.key === i.type).length}}
</UBadge>
</template>
<template #item="{item}"> <template #item="{item}">
<div style="height: 80vh; overflow-y: scroll"> <div style="height: 80vh; overflow-y: scroll">
<UTable <UTable
@@ -128,8 +118,10 @@
</span> </span>
</template> </template>
<template #partner-data="{row}"> <template #partner-data="{row}">
<span v-if="row.customer">{{row.customer ? row.customer.name : ""}}</span> <span v-if="row.customer && row.customer.name.length <21">{{row.customer ? row.customer.name : ""}}</span>
<UTooltip v-else-if="row.customer && row.customer.name.length > 20" :text="row.customer.name">
{{row.customer.name.substring(0,20)}}...
</UTooltip>
</template> </template>
<template #reference-data="{row}"> <template #reference-data="{row}">
<span v-if="row === filteredRows[selectedItem]" class="text-primary-500 font-bold">{{row.documentNumber}}</span> <span v-if="row === filteredRows[selectedItem]" class="text-primary-500 font-bold">{{row.documentNumber}}</span>
@@ -165,10 +157,6 @@
<script setup> <script setup>
import dayjs from "dayjs"; import dayjs from "dayjs";
definePageMeta({
middleware: "auth"
})
defineShortcuts({ defineShortcuts({
'/': () => { '/': () => {
//console.log(searchinput) //console.log(searchinput)
@@ -204,11 +192,15 @@ const dataStore = useDataStore()
const tempStore = useTempStore() const tempStore = useTempStore()
const router = useRouter() const router = useRouter()
const type = "createddocuments"
const dataType = dataStore.dataTypes[type]
const items = ref([]) const items = ref([])
const selectedItem = ref(0) const selectedItem = ref(0)
const setupPage = async () => { const setupPage = async () => {
items.value = (await useSupabaseSelect("createddocuments","*, customer(id,name), statementallocations(id,amount),linkedDocument(*)","documentNumber")).filter(i => !i.archived) items.value = (await useEntities("createddocuments").select("*, customer(id,name), statementallocations(id,amount),linkedDocument(*)","documentNumber",true, true))
} }
setupPage() setupPage()
@@ -216,47 +208,38 @@ setupPage()
const templateColumns = [ const templateColumns = [
{ {
key: "reference", key: "reference",
label: "Referenz", label: "Referenz"
sortable: true
}, },
{ {
key: 'type', key: 'type',
label: "Typ", label: "Typ"
sortable: true }, {
},{
key: 'state', key: 'state',
label: "Status", label: "Status"
sortable: true
}, },
{ {
key: "amount", key: "amount",
label: "Betrag", label: "Betrag"
sortable: true
}, },
{ {
key: 'partner', key: 'partner',
label: "Kunde", label: "Kunde"
sortable: true
}, },
{ {
key: "date", key: "date",
label: "Datum", label: "Datum"
sortable: true
}, },
{ {
key: "amountOpen", key: "amountOpen",
label: "Offener Betrag", label: "Offener Betrag"
sortable: true
}, },
{ {
key: "paid", key: "paid",
label: "Bezahlt", label: "Bezahlt"
sortable: true
}, },
{ {
key: "dueDate", key: "dueDate",
label: "Fällig", label: "Fällig"
sortable: true
} }
] ]
const selectedColumns = ref(tempStore.columns["createddocuments"] ? tempStore.columns["createddocuments"] : templateColumns) const selectedColumns = ref(tempStore.columns["createddocuments"] ? tempStore.columns["createddocuments"] : templateColumns)
@@ -291,42 +274,52 @@ const types = computed(() => {
const selectItem = (item) => { const selectItem = (item) => {
console.log(item) console.log(item)
if(item.state === "Entwurf"){ if (item.state === "Entwurf") {
router.push(`/createDocument/edit/${item.id}`) router.push(`/createDocument/edit/${item.id}`)
} else if(item.state !== "Entwurf") { } else if (item.state !== "Entwurf") {
router.push(`/createDocument/show/${item.id}`) router.push(`/createDocument/show/${item.id}`)
} }
} }
const displayCurrency = (value, currency = "€") => { const displayCurrency = (value, currency = "€") => {
return `${Number(value).toFixed(2).replace(".",",")} ${currency}` return `${Number(value).toFixed(2).replace(".", ",")} ${currency}`
} }
const searchString = ref(tempStore.searchStrings['createddocuments'] ||'') const searchString = ref(tempStore.searchStrings['createddocuments'] || '')
const clearSearchString = () => { const clearSearchString = () => {
tempStore.clearSearchString('createddocuments') tempStore.clearSearchString('createddocuments')
searchString.value = '' searchString.value = ''
} }
const selectedFilters = ref([]) const selectableFilters = ref(dataType.filters.map(i => i.name))
const selectedFilters = ref(dataType.filters.filter(i => i.default).map(i => i.name) || [])
const filteredRows = computed(() => { const filteredRows = computed(() => {
let temp = items.value.filter(i => types.value.find(x => x.key === 'invoices' ? ['invoices','advanceInvoices','cancellationInvoices'].includes(i.type) : x.key === i.type)) let tempItems = items.value.filter(i => types.value.find(x => x.key === 'invoices' ? ['invoices', 'advanceInvoices', 'cancellationInvoices'].includes(i.type) : x.key === i.type))
temp = temp.filter(i => i.type !== "serialInvoices") tempItems = tempItems.filter(i => i.type !== "serialInvoices")
/*if(showDrafts.value === true) { tempItems = tempItems.map(i => {
temp = temp.filter(i => i.state === "Entwurf") return {
} else { ...i,
temp = temp.filter(i => i.state !== "Entwurf") class: i.archived ? 'bg-red-500/50 dark:bg-red-400/50' : null
}*/ }
})
if(selectedFilters.value.includes("Nur offene anzeigen")){ if(selectedFilters.value.length > 0) {
temp = temp.filter(i => !isPaid(i)) selectedFilters.value.forEach(filterName => {
let filter = dataType.filters.find(i => i.name === filterName)
tempItems = tempItems.filter(filter.filterFunction)
})
} }
return useSearch(searchString.value, temp.slice().reverse())
tempItems = useSearch(searchString.value, tempItems)
return useSearch(searchString.value, tempItems.slice().reverse())
}) })

View File

@@ -126,13 +126,20 @@ const items = ref([])
const selectedItem = ref(0) const selectedItem = ref(0)
const setupPage = async () => { const setupPage = async () => {
items.value = await useSupabaseSelect("createddocuments","*, customer(id,name)","documentDate") items.value = await useEntities("createddocuments").select("*, customer(id,name)","documentDate",undefined,true)
} }
const searchString = ref("") const searchString = ref("")
const filteredRows = computed(() => { const filteredRows = computed(() => {
let temp = items.value.filter(i => i.type === "serialInvoices")
let temp = items.value.filter(i => i.type === "serialInvoices").map(i => {
return {
...i,
class: i.archived ? 'bg-red-500/50 dark:bg-red-400/50' : null
}
})
/*if(showDrafts.value === true) { /*if(showDrafts.value === true) {
temp = temp.filter(i => i.state === "Entwurf") temp = temp.filter(i => i.state === "Entwurf")

View File

@@ -1,9 +1,7 @@
<script setup> <script setup>
import CopyCreatedDocumentModal from "~/components/copyCreatedDocumentModal.vue"; import CopyCreatedDocumentModal from "~/components/copyCreatedDocumentModal.vue";
definePageMeta({
middleware: "auth"
})
defineShortcuts({ defineShortcuts({
'backspace': () => { 'backspace': () => {
@@ -11,41 +9,29 @@ defineShortcuts({
}, },
}) })
const supabase = useSupabaseClient()
const modal = useModal() const modal = useModal()
const dataStore = useDataStore()
const profileStore = useProfileStore()
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const auth = useAuthStore()
const dataStore = useDataStore()
const itemInfo = ref({}) const itemInfo = ref({})
const linkedDocument =ref({}) const linkedDocument =ref({})
const currentTenant = ref({})
const setupPage = async () => { const setupPage = async () => {
if(route.params) { if(route.params) {
if(route.params.id) itemInfo.value = await useSupabaseSelectSingle("createddocuments",route.params.id,"*, files(*)") if(route.params.id) itemInfo.value = await useEntities("createddocuments").selectSingle(route.params.id,"*,files(*),linkedDocument(*)")
console.log(itemInfo.value) console.log(itemInfo.value)
linkedDocument.value = await useFiles().selectDocument(itemInfo.value.files[0].id) 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",profileStore.currentTenant).single()).data
console.log(currentTenant.value)
} }
setupPage() setupPage()
const openEmail = () => { const openEmail = () => {
if(["invoices","advanceInvoices"].includes(itemInfo.value.type)){ 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(auth.activeTenantData.standardEmailForInvoices || "")}`)
} else { } else {
router.push(`/email/new?loadDocuments=["${linkedDocument.value.id}"]`) router.push(`/email/new?loadDocuments=["${linkedDocument.value.id}"]`)
} }
@@ -54,7 +40,7 @@ const openEmail = () => {
<template> <template>
<UDashboardNavbar <UDashboardNavbar
title="Erstelltes Dokument anzeigen" title="Erstelltes Dokument anzeigen"
> >
</UDashboardNavbar> </UDashboardNavbar>
@@ -66,10 +52,10 @@ const openEmail = () => {
> >
Bearbeiten Bearbeiten
</UButton> </UButton>
<!-- <UButton <!-- <UButton
:to="dataStore.documents.find(i => i.createdDocument === itemInfo.id) ? dataStore.documents.find(i => i.createdDocument === itemInfo.id).url : ''" :to="dataStore.documents.find(i => i.createdDocument === itemInfo.id) ? dataStore.documents.find(i => i.createdDocument === itemInfo.id).url : ''"
target="_blank" target="_blank"
>In neuen Tab anzeigen</UButton>--> >In neuen Tab anzeigen</UButton>-->
<UButton <UButton
icon="i-heroicons-arrow-right-end-on-rectangle" icon="i-heroicons-arrow-right-end-on-rectangle"
@click="modal.open(CopyCreatedDocumentModal, { @click="modal.open(CopyCreatedDocumentModal, {
@@ -81,8 +67,8 @@ const openEmail = () => {
Kopieren Kopieren
</UButton> </UButton>
<UButton <UButton
@click="openEmail" @click="openEmail"
icon="i-heroicons-envelope" icon="i-heroicons-envelope"
> >
E-Mail E-Mail
</UButton> </UButton>
@@ -96,28 +82,41 @@ const openEmail = () => {
</UButton> </UButton>
<UButton <UButton
v-if="itemInfo.project" v-if="itemInfo.project"
@click="router.push(`/standardEntity/projects/show/${itemInfo.project}`)" @click="router.push(`/standardEntity/projects/show/${itemInfo.project}`)"
icon="i-heroicons-link" icon="i-heroicons-link"
variant="outline" variant="outline"
> >
Projekt Projekt
</UButton> </UButton>
<UButton <UButton
v-if="itemInfo.customer" v-if="itemInfo.customer"
@click="router.push(`/standardEntity/customers/show/${itemInfo.customer}`)" @click="router.push(`/standardEntity/customers/show/${itemInfo.customer}`)"
icon="i-heroicons-link" icon="i-heroicons-link"
variant="outline" variant="outline"
> >
Kunde Kunde
</UButton> </UButton>
<UButton
v-if="itemInfo.linkedDocument"
@click="router.push(`/standardEntity/createDocument/show/${itemInfo.linkedDocument}`)"
icon="i-heroicons-link"
variant="outline"
>
{{dataStore.documentTypesForCreation[itemInfo.linkedDocument.type].labelSingle}} - {{itemInfo.linkedDocument.documentNumber}}
</UButton>
</template> </template>
</UDashboardToolbar> </UDashboardToolbar>
<UDashboardPanelContent> <UDashboardPanelContent>
<object <!-- <object
:data="linkedDocument.url" :data="linkedDocument.url"
class="w-full previewDocumentMobile" class="w-full previewDocumentMobile"
/> />-->
<PDFViewer v-if="linkedDocument.id" :file-id="linkedDocument.id" />
</UDashboardPanelContent> </UDashboardPanelContent>
</template> </template>

View File

@@ -0,0 +1,108 @@
<script setup>
import {useFunctions} from "~/composables/useFunctions.js";
import dayjs from "dayjs";
const profileStore = useProfileStore();
const preloadedContent = ref("")
const letterheads = ref([])
const itemInfo = ref({
contentHTML: "",
contentJSON: {},
contentText: ""
})
const showDocument = ref(false)
const uri = ref("")
const setupPage = async () => {
letterheads.value = await useSupabaseSelect("letterheads","*")
preloadedContent.value = `<p></p><p></p><p></p>`
}
setupPage()
const onChangeTab = (index) => {
if(index === 1) {
generateDocument()
}
}
const getDocumentData = () => {
/*const returnData = {
adressLine: `${businessInfo.name}, ${businessInfo.street}, ${businessInfo.zip} ${businessInfo.city}`,
recipient: [
customerData.name,
... customerData.nameAddition ? [customerData.nameAddition] : [],
... contactData ? [`${contactData.firstName} ${contactData.lastName}`] : [],
itemInfo.value.address.street,
... itemInfo.value.address.special ? [itemInfo.value.address.special] : [],
`${itemInfo.value.address.zip} ${itemInfo.value.address.city}`,
],
}*/
const returnData = {
adressLine: `Federspiel Technology UG, Am Schwarzen Brack 14, 26452 Sande`,
recipient: [
"Federspiel Technology",
"UG haftungsbeschränkt",
"Florian Federspiel",
"Am Schwarzen Brack 14",
"Zusatz",
"26452 Sande",
],
contentJSON: itemInfo.value.contentJSON,
}
return returnData
}
const generateDocument = async () => {
const ownTenant = profileStore.ownTenant
const path = letterheads.value[0].path
uri.value = await useFunctions().useCreateLetterPDF(getDocumentData(), path)
showDocument.value = true
}
const contentChanged = (content) => {
itemInfo.value.contentHTML = content.html
itemInfo.value.contentJSON = content.json
itemInfo.value.contentText = content.text
}
</script>
<template>
<UDashboardNavbar title="Anschreiben bearbeiten"/>
{{itemInfo}}
<UDashboardPanelContent>
<UTabs @change="onChangeTab" :items="[{label: 'Editor'},{label: 'Vorschau'}]">
<template #item="{item}">
<div v-if="item.label === 'Editor'">
<Tiptap
class="mt-3"
@updateContent="contentChanged"
:preloadedContent="preloadedContent"
/>
</div>
<div v-else-if="item.label === 'Vorschau'">
<object
:data="uri"
v-if="showDocument"
type="application/pdf"
class="w-full previewDocumentMobile"
/>
</div>
</template>
</UTabs>
</UDashboardPanelContent>
</template>
<style scoped>
.previewDocumentMobile {
aspect-ratio: 1 / 1.414;
}
</style>

View File

@@ -1,280 +0,0 @@
<script setup>
const supabase = useSupabaseClient()
const dataStore = useDataStore()
const profileStore = useProfileStore()
const selectedTab = ref(0)
const selectedMail = ref(null)
const selectedMailboxPath = ref("INBOX")
const emails = ref([])
const accountData = ref(null)
const availableAccounts = ref(null)
const selectedAccount = ref(null)
const setupPage = async () => {
availableAccounts.value = (await supabase.from("emailAccounts").select("*").contains("profiles",[profileStore.activeProfile.id])).data
console.log(availableAccounts.value)
if(availableAccounts.value.length > 0) {
selectedAccount.value = availableAccounts.value[0].id
accountData.value = await useSupabaseSelectSingle("emailAccounts", selectedAccount.value)
emails.value = await useSupabaseSelect("emailMessages", "*", "date", false)
}
}
const filteredMails = computed(() => {
let temp = emails.value.filter(i => i.mailboxPath.toLowerCase() === selectedMailboxPath.value.toLowerCase())
if(selectedTab.value === 0){
return temp
} else {
return temp.filter(i => !i.seen)
}
return emails.value
})
const isMailPanelOpen = computed({
get() {
return !!selectedMail.value
},
set(value) {
if (!value) {
selectedMail.value = null
}
}
})
const changeSeen = async (seen) => {
const {data,error} = await supabase.functions.invoke('emailcontrol',{
body: {
method: seen ? "makeSeen" : "makeUnseen",
emailId: selectedMail.value.id
}
})
if(data) {
setupPage()
}
}
setupPage()
</script>
<template>
<!-- <UDashboardNavbar title="E-Mails">
</UDashboardNavbar>
<UDashboardToolbar>
</UDashboardToolbar>-->
<UDashboardPage
v-if="selectedAccount"
>
<UDashboardPanel
id="inbox"
:width="400"
:resizable="{ min: 300, max: 500 }"
>
<UDashboardNavbar>
<template #left>
<USelectMenu
v-if="accountData"
class="w-40"
:options="accountData.mailboxes"
option-attribute="name"
value-attribute="path"
v-model="selectedMailboxPath"
/>
</template>
<template #right>
<UButton
icon="i-heroicons-arrow-path"
variant="ghost"
color="gray"
@click="setupPage"
/>
<UTabs
v-model="selectedTab"
:items="[{label: 'All'}, {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"
@emailSelected=" !selectedMail.seen ? changeSeen(true): ''"
/>
</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="Ungelesen">
<UButton
icon="i-heroicons-eye-slash"
color="gray"
variant="ghost"
@click="changeSeen(false)"
/>
</UTooltip>
<!-- <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>
</UDashboardPage>
<div
v-else
class="flex-1 flex-col hidden lg:flex items-center justify-center"
>
<UIcon
name="i-heroicons-inbox"
class="w-32 h-32 text-gray-400 dark:text-gray-500"
/>
<span class="font-bold text-2xl">Kein E-Mail Account verfügbar</span>
</div>
<!-- <UInput
placeholder="Empfänger"
variant="ghost"
class="m-2"
/>
<UInput
placeholder="Betreff"
variant="ghost"
class="m-2"
/>
<UInput
placeholder="CC"
variant="ghost"
class="m-2"
/>
<UInput
placeholder="BCC"
variant="ghost"
class="m-2"
/>
<UDivider
class="my-5"
/>
<Tiptap/>-->
</template>
<style scoped>
</style>

View File

@@ -1,11 +1,11 @@
<script setup> <script setup>
//TODO: BACKENDCHANGE EMAIL SENDING
const supabase = useSupabaseClient()
const dataStore = useDataStore() const dataStore = useDataStore()
const profileStore = useProfileStore() const profileStore = useProfileStore()
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const toast = useToast() const toast = useToast()
const auth = useAuthStore()
const emailData = ref({ const emailData = ref({
to:"", to:"",
@@ -23,14 +23,16 @@ const loadedDocuments = ref([])
const loaded = ref(false) const loaded = ref(false)
const noAccountsPresent = ref(false) const noAccountsPresent = ref(false)
const setupPage = async () => { const setupPage = async () => {
emailAccounts.value = await useSupabaseSelect("emailAccounts") //emailAccounts.value = await useEntities("emailAccounts").select()
emailAccounts.value = await useNuxtApp().$api("/api/email/accounts")
if(emailAccounts.value.length === 0) { if(emailAccounts.value.length === 0) {
noAccountsPresent.value = true noAccountsPresent.value = true
} else { } else {
emailData.value.account = emailAccounts.value[0].id emailData.value.account = emailAccounts.value[0].id
preloadedContent.value = `<p></p><p></p><p></p>${profileStore.activeProfile.emailSignature || ""}` preloadedContent.value = `<p></p><p></p><p></p>${auth.profile.email_signature || ""}`
//Check Query //Check Query
if(route.query.to) emailData.value.to = route.query.to if(route.query.to) emailData.value.to = route.query.to
@@ -44,15 +46,13 @@ const setupPage = async () => {
const data = await useFiles().selectSomeDocuments(JSON.parse(route.query.loadDocuments)) const data = await useFiles().selectSomeDocuments(JSON.parse(route.query.loadDocuments))
console.log(data) console.log(data)
if(data) loadedDocuments.value = data if(data) loadedDocuments.value = data
//console.log(loadedDocuments.value) //console.log(loadedDocuments.value)
if(loadedDocuments.value.length > 0) { if(loadedDocuments.value.length > 0) {
console.log(loadedDocuments.value[0]) console.log(loadedDocuments.value[0])
emailData.value.subject = loadedDocuments.value[0].createddocument.title emailData.value.subject = `${loadedDocuments.value[0].createddocument.title} von ${auth.activeTenantData.businessInfo.name}`
if(loadedDocuments.value[0].createddocument.contact && loadedDocuments.value[0].createddocument.contact.email) { if(loadedDocuments.value[0].createddocument.contact && loadedDocuments.value[0].createddocument.contact.email) {
console.log("Contact") console.log("Contact")
@@ -63,7 +63,6 @@ const setupPage = async () => {
emailData.value.to = loadedDocuments.value[0].createddocument.customer.infoData.email emailData.value.to = loadedDocuments.value[0].createddocument.customer.infoData.email
} }
} }
} }
loaded.value = true loaded.value = true
@@ -132,25 +131,33 @@ const sendEmail = async () => {
for await (const doc of loadedDocuments.value) { for await (const doc of loadedDocuments.value) {
const {data,error} = await supabase.storage.from("filesdev").download(doc.path)
const res = await useFiles().downloadFile(doc.id, null, true)
body.attachments.push({ body.attachments.push({
filename: doc.path.split("/")[doc.path.split("/").length -1], filename: doc.path.split("/")[doc.path.split("/").length -1],
content: await blobToBase64(data), content: await blobToBase64(res),
contentType: data.type, contentType: res.type,
encoding: "base64", encoding: "base64",
contentDisposition: "attachment" contentDisposition: "attachment"
}) })
} }
const { data, error } = await supabase.functions.invoke('send_email', { console.log(body)
body
const res = await useNuxtApp().$api("/api/email/send",{
method: "POST",
body: body,
}) })
if(error) {
console.log(res)
if(!res.success) {
toast.add({title: "Fehler beim Absenden der E-Mail", color: "rose"}) toast.add({title: "Fehler beim Absenden der E-Mail", color: "rose"})
} else if(data) { } else {
router.push("/") navigateTo("/")
toast.add({title: "E-Mail zum Senden eingereiht"}) toast.add({title: "E-Mail zum Senden eingereiht"})
} }
@@ -200,7 +207,7 @@ const sendEmail = async () => {
> >
<USelectMenu <USelectMenu
:options="emailAccounts" :options="emailAccounts"
option-attribute="emailAddress" option-attribute="email"
value-attribute="id" value-attribute="id"
v-model="emailData.account" v-model="emailData.account"
/> />

162
pages/export.vue Normal file
View File

@@ -0,0 +1,162 @@
<script setup lang="ts">
import dayjs from "dayjs"
const exports = ref([])
const auth = useAuthStore()
const toast = useToast()
const setupPage = async () => {
exports.value = await useNuxtApp().$api("/api/exports",{
method: "GET"
})
}
setupPage()
function downloadFile(row) {
const a = document.createElement("a")
a.href = row.url
a.download = row.file_path.split("/").pop()
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
}
const showCreateExportModal = ref(false)
const createExportData = ref({
start_date: null,
end_date: null,
beraternr:null,
mandantennr: null
})
const createExport = async () => {
const res = await useNuxtApp().$api("/api/exports/datev",{
method: "POST",
body: createExportData.value
})
showCreateExportModal.value = false
if(res.success) {
toast.add({title: "Export wird erstellt. Sie erhalten eine Benachrichtigung sobald es soweit ist."})
} else {
toast.add({title: "Es gab einen Fehler beim erstellen", color: "rose"})
}
}
</script>
<template>
<UDashboardNavbar
title="Exporte"
>
<template #right>
<UButton
@click="showCreateExportModal = true"
>+ Export</UButton>
</template>
</UDashboardNavbar>
<UTable
:rows="exports"
:columns="[
{
key: 'created_at',
label: 'Erstellt am',
},{
key: 'start_date',
label: 'Start',
},{
key: 'end_date',
label: 'Ende',
},{
key: 'valid_until',
label: 'Gültig bis',
},{
key: 'type',
label: 'Typ',
},{
key: 'download',
label: 'Download',
},
]"
>
<template #created_at-data="{row}">
{{dayjs(row.created_at).format("DD.MM.YYYY HH:mm")}}
</template>
<template #start_date-data="{row}">
{{dayjs(row.start_date).format("DD.MM.YYYY HH:mm")}}
</template>
<template #end_date-data="{row}">
{{dayjs(row.end_date).format("DD.MM.YYYY HH:mm")}}
</template>
<template #valid_until-data="{row}">
{{dayjs(row.valid_until).format("DD.MM.YYYY HH:mm")}}
</template>
<template #download-data="{row}">
<UButton
@click="downloadFile(row)"
>
Download
</UButton>
</template>
</UTable>
<UModal v-model="showCreateExportModal">
<UCard>
<template #header>
Export erstellen
</template>
<UFormGroup
label="Start:"
>
<UPopover :popper="{ placement: 'bottom-start' }">
<UButton
icon="i-heroicons-calendar-days-20-solid"
:label="createExportData.start_date ? dayjs(createExportData.start_date).format('DD.MM.YYYY') : 'Datum auswählen'"
variant="outline"
class="mx-auto"
/>
<template #panel="{ close }">
<LazyDatePicker v-model="createExportData.start_date" @close="close" />
</template>
</UPopover>
</UFormGroup>
<UFormGroup
label="Ende:"
>
<UPopover :popper="{ placement: 'bottom-start' }">
<UButton
icon="i-heroicons-calendar-days-20-solid"
:label="createExportData.end_date ? dayjs(createExportData.end_date).format('DD.MM.YYYY') : 'Datum auswählen'"
variant="outline"
class="mx-auto"
/>
<template #panel="{ close }">
<LazyDatePicker v-model="createExportData.end_date" @close="close" />
</template>
</UPopover>
</UFormGroup>
<template #footer>
<UButton
@click="createExport"
>
Erstellen
</UButton>
</template>
</UCard>
</UModal>
</template>
<style scoped>
</style>

View File

@@ -2,23 +2,14 @@
import {BlobReader, BlobWriter, ZipWriter} from "@zip.js/zip.js"; import {BlobReader, BlobWriter, ZipWriter} from "@zip.js/zip.js";
import {useSupabaseSelectSingle} from "~/composables/useSupabase.js";
import DocumentDisplayModal from "~/components/DocumentDisplayModal.vue"; import DocumentDisplayModal from "~/components/DocumentDisplayModal.vue";
import DocumentUploadModal from "~/components/DocumentUploadModal.vue"; import DocumentUploadModal from "~/components/DocumentUploadModal.vue";
import dayjs from "dayjs"; import dayjs from "dayjs";
import arraySort from "array-sort"; import arraySort from "array-sort";
import {useTempStore} from "~/stores/temp.js";
definePageMeta({
middleware: "auth"
})
defineShortcuts({ defineShortcuts({
/*'/': () => {
//console.log(searchinput)
//searchinput.value.focus()
document.getElementById("searchinput").focus()
},*/
'+': () => { '+': () => {
//Hochladen //Hochladen
uploadModalOpen.value = true uploadModalOpen.value = true
@@ -30,9 +21,10 @@ defineShortcuts({
if(entry.type === "file") { if(entry.type === "file") {
showFile(entry.id) showFile(entry.id)
console.log(entry) } else if(createFolderModalOpen.value === false && entry.type === "folder") {
} else {
changeFolder(currentFolders.value.find(i => i.id === entry.id)) changeFolder(currentFolders.value.find(i => i.id === entry.id))
} else if(createFolderModalOpen.value === true) {
createFolder()
} }
} }
@@ -55,13 +47,11 @@ defineShortcuts({
const dataStore = useDataStore() const dataStore = useDataStore()
const tempStore = useTempStore() const tempStore = useTempStore()
const profileStore = useProfileStore()
const supabase = useSupabaseClient()
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
const modal = useModal() const modal = useModal()
dataStore.fetchDocuments() const auth = useAuthStore()
const uploadModalOpen = ref(false) const uploadModalOpen = ref(false)
const createFolderModalOpen = ref(false) const createFolderModalOpen = ref(false)
@@ -69,7 +59,7 @@ const uploadInProgress = ref(false)
const fileUploadFormData = ref({ const fileUploadFormData = ref({
tags: ["Eingang"], tags: ["Eingang"],
path: "", path: "",
tenant: profileStore.currentTenant, tenant: auth.activeTenant,
folder: null folder: null
}) })
@@ -92,15 +82,16 @@ const isDragTarget = ref(false)
const loaded = ref(false) const loaded = ref(false)
const setupPage = async () => { const setupPage = async () => {
folders.value = await useSupabaseSelect("folders") folders.value = await useEntities("folders").select()
documents.value = await files.selectDocuments() documents.value = await files.selectDocuments()
filetags.value = await useSupabaseSelect("filetags") filetags.value = await useEntities("filetags").select()
if(route.query) { if(route.query) {
if(route.query.folder) { if(route.query.folder) {
currentFolder.value = await useSupabaseSelectSingle("folders", route.query.folder) currentFolder.value = await useEntities("folders").selectSingle(route.query.folder)
} }
} }
@@ -118,7 +109,6 @@ const setupPage = async () => {
} }
dropZone.ondrop = async function (event) { dropZone.ondrop = async function (event) {
console.log("files dropped")
event.preventDefault() event.preventDefault()
} }
@@ -213,14 +203,10 @@ const changeFolder = async (newFolder) => {
const createFolderData = ref({}) const createFolderData = ref({})
const createFolder = async () => { const createFolder = async () => {
const {data,error} = await supabase const res = await useEntities("folders").create({
.from("folders") parent: currentFolder.value ? currentFolder.value.id : undefined,
.insert({ name: createFolderData.value.name,
tenant: profileStore.currentTenant, })
parent: currentFolder.value ? currentFolder.value.id : undefined,
name: createFolderData.value.name,
})
createFolderModalOpen.value = false createFolderModalOpen.value = false
@@ -229,61 +215,14 @@ const createFolder = async () => {
} }
const downloadSelected = async () => { const downloadSelected = async () => {
const bucket = "filesdev";
let files = [] let files = []
files = filteredDocuments.value.filter(i => selectedFiles.value[i.id] === true).map(i => i.path) files = filteredDocuments.value.filter(i => selectedFiles.value[i.id] === true).map(i => i.path)
// If there are no files in the folder, throw an error
if (!files || !files.length) {
throw new Error("No files to download");
}
const promises = []; await useFiles().downloadFile(undefined,Object.keys(selectedFiles.value))
// 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") {
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", "dateien.zip");
document.body.appendChild(link);
link.click();
} }
const searchString = ref(tempStore.searchStrings["files"] ||'') const searchString = ref(tempStore.searchStrings["files"] ||'')
@@ -295,7 +234,6 @@ const renderedFileList = computed(() => {
type: "file" type: "file"
} }
}) })
console.log(currentFolders.value)
arraySort(files, (a,b) => { arraySort(files, (a,b) => {
let aVal = a.path ? a.path.split("/")[a.path.split("/").length -1] : null let aVal = a.path ? a.path.split("/")[a.path.split("/").length -1] : null
@@ -338,7 +276,6 @@ const renderedFileList = computed(() => {
const selectedFileIndex = ref(0) const selectedFileIndex = ref(0)
const showFile = (fileId) => { const showFile = (fileId) => {
console.log(fileId)
modal.open(DocumentDisplayModal,{ modal.open(DocumentDisplayModal,{
documentData: documents.value.find(i => i.id === fileId), documentData: documents.value.find(i => i.id === fileId),
onUpdatedNeeded: setupPage() onUpdatedNeeded: setupPage()
@@ -413,7 +350,10 @@ const clearSearchString = () => {
</USelectMenu> </USelectMenu>
<UButton @click="modal.open(DocumentUploadModal,{fileData: {folder: currentFolder.id, type: currentFolder.standardFiletype, typeEnabled: currentFolder.standardFiletypeIsOptional}, onUploadFinished: () => {setupPage()}})">+ Datei</UButton> <UButton
:disabled="!currentFolder"
@click="modal.open(DocumentUploadModal,{fileData: {folder: currentFolder.id, type: currentFolder.standardFiletype, typeEnabled: currentFolder.standardFiletypeIsOptional}, onUploadFinished: () => {setupPage()}})"
>+ Datei</UButton>
<UButton <UButton
@click="createFolderModalOpen = true" @click="createFolderModalOpen = true"
variant="outline" variant="outline"

View File

@@ -1,88 +0,0 @@
<script setup>
const route = useRoute()
const router = useRouter()
const supabase = useSupabaseClient()
const currentSubmission = (await supabase.from("formSubmits").select().eq('id',route.params.id)).data[0]
const form = (await supabase.from("forms").select().eq('id',currentSubmission.formType)).data[0]
const formData = ref({})
const submitted = ref(currentSubmission.submitted)
const submitForm = async () => {
submitted.value = true
console.log(formData.value)
const {data,error} = await supabase
.from("formSubmits")
.update({values: formData.value, submitted: true})
.eq('id',currentSubmission.id)
.select()
if(error) {
console.log(error)
} else if( data) {
formData.value = {}
}
}
</script>
<template>
<div>
<UForm
v-if="!submitted"
@submit="submitForm"
@reset="formData = {}"
>
<div
v-for="item in form.fields"
>
<p v-if="item.type === 'header'">{{item.label}}</p>
<UFormGroup
v-else-if="item.type.includes('Input')"
:label="item.required ? item.label + '*' : item.label"
>
<UInput
v-if="item.type === 'textInput'"
v-model="formData[item.key]"
:required="item.required"
/>
<UInput
v-else-if="item.type === 'numberInput'"
v-model="formData[item.key]"
:required="item.required"
type="number"
inputmode="numeric"
/>
</UFormGroup>
</div>
<UButton type="submit">
Abschicken
</UButton>
<UButton
type="reset"
color="rose"
class="m-2"
>
Zurücksetzen
</UButton>
</UForm>
<div v-else>
Dieses Formular wurde bereits abgeschickt. Möchten Sie erneut Daten abschicken, sprechen Sie bitte Ihren Ansprechpartner an, um das Formular freizuschalten.
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -1,676 +0,0 @@
<script setup>
import InputGroup from "~/components/InputGroup.vue";
import dayjs from "dayjs";
import HistoryDisplay from "~/components/HistoryDisplay.vue";
import {useSupabaseSelect} from "~/composables/useSupabase.js";
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 costcentres = ref([])
const accounts = ref([])
const vendors = 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)
accounts.value = (await supabase.from("accounts").select().order("number",{ascending:true})).data
vendors.value = await useSupabaseSelect("vendors")
}
setup()
const loadCostCentres = async () => {
costcentres.value = await useSupabaseSelect("costcentres")
}
loadCostCentres()
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,true)
const {error} = await supabase.from("files").update({incominginvoice: data.id}).eq("id",loadedFile.value.id)
router.push(`/incominginvoices/show/${data.id}`)
}
const setCostCentre = async (item,data) => {
await loadCostCentres()
item.costCentre = data.id
}
const gptLoading = ref(false)
const getInvoiceData = async () => {
gptLoading.value = true
console.log(loadedFile.value)
//loadedFile.value.url
/*let data = {
"invoice_number": "3423478673",
"invoice_date": "2025-05-30",
"invoice_type": "incoming",
"delivery_type": "null",
"delivery_note_number": "null",
"reference": "null",
"issuer": {
"name": "Boels Rental Germany GmbH",
"address": "Emeranstraße 49-51, 85622 Feldkirchen, Deutschland",
"phone": "+49-(0)1801663225",
"email": "fakturierung@boels.de",
"bank": "ABN AMRO Bank N.V.",
"bic": "ABNANL2A",
"iban": "NL09 ABNA 0520 5585 61"
},
"recipient": {
"name": "Federspiel Technology UG",
"address": "Am Schwarzen Brack 14, 26452 Sande, Deutschland",
"phone": "null",
"email": "null"
},
"invoice_items": [
{
"description": "Bautrockner 50 ltr.",
"unit": "piece",
"quantity": 1,
"total": 395.22
},
{
"description": "Servicepauschale Kat. A",
"unit": "piece",
"quantity": 1,
"total": 32.1
},
{
"description": "Haftungsbegrenzung A: (Schäden, exkl. Feuer/Diebstahl/Einbruch)",
"unit": "piece",
"quantity": 1,
"total": 3.2
},
{
"description": "Haftungsbegrenzung B: (Feuer/Diebstahl/Einbruch)",
"unit": "piece",
"quantity": 1,
"total": 16.93
}
],
"subtotal": 89.1,
"tax_rate": 19,
"tax": 16.93,
"total": 106.03,
"terms": "Dieser Betrag wird automatisch mittels Lastschrift von ihrem Konto eingezogen"
}
console.log(data)
console.log(data.subtotal)*/
let data = await useFunctions().useGetInvoiceData(loadedFile.value)
if(data.invoice_number) itemInfo.value.reference = data.invoice_number
if(data.invoice_date) itemInfo.value.date = dayjs(data.invoice_date)
if(data.issuer.id) itemInfo.value.vendor = data.issuer.id
if(data.invoice_duedate) itemInfo.value.dueDate = dayjs(data.invoice_duedate)
if(data.terms) itemInfo.value.paymentType = data.terms
if(data.subtotal) {
itemInfo.value.accounts = [
{
account: null,
amountNet: data.subtotal,
amountTax: data.tax,
taxType: String(data.tax_rate),
costCentre: null,
amountGross: Number(data.subtotal) + Number(data.tax)
}
]
}
if(data.terms === "Direct Debit") {
itemInfo.value.paymentType = "Einzug"
} else if(data.terms === "Transfer") {
itemInfo.value.paymentType = "Überweisung"
} else if(data.terms === "Credit Card") {
itemInfo.value.paymentType = "Kreditkarte"
} else if(data.terms === "Other") {
itemInfo.value.paymentType = "Sonstiges"
}
let description = ""
if(data.delivery_note_number) description += `Lieferschein: ${data.delivery_note_number} \n`
if(data.reference) description += `Referenz: ${data.reference} \n`
if(data.invoice_items) {
data.invoice_items.forEach(item => {
description += `${item.description} - ${item.quantity} ${item.unit} - ${item.total}\n`
})
}
itemInfo.value.description = description
gptLoading.value = false
}
</script>
<template>
<UDashboardNavbar :title="'Eingangsbeleg erstellen'">
<template #right>
<UButton
@click="createIncomingInvoice"
>
Speichern
</UButton>
</template>
</UDashboardNavbar>
<UDashboardPanelContent>
<div v-if="!loadedFile">
<DocumentList
v-if="availableDocuments.length > 0"
:documents="availableDocuments"
:return-document-id="true"
@selectDocument="(documentId) => loadFile(documentId)"
/>
<div v-else class="w-full text-center">
<span class="text-xl font-medium mt-10">Keine Dateien zum zuweisen verfügbar</span>
</div>
</div>
<div
v-else
class="flex justify-between mt-5 workingContainer"
>
<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"
/>
<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">
<UButton
icon="i-heroicons-sparkles"
class="my-3"
variant="outline"
@click="getInvoiceData"
:disabled="gptLoading"
>
KI - Vorschlag
<UProgress v-if="gptLoading" animation="carousel"/>
</UButton>
<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:" >
<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..."
:color="!itemInfo.vendor ? 'rose' : 'primary'"
@change="vendors.find(i => i.id === itemInfo.vendor).defaultPaymentMethod ? itemInfo.paymentType = vendors.find(i => i.id === itemInfo.vendor).defaultPaymentMethod : null"
>
<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>
<EntityModalButtons
type="vendors"
:id="itemInfo.vendor"
@return-data="(data) => itemInfo.vendor = data.id"
/>
</InputGroup>
</UFormGroup>
<UFormGroup
class="mt-3"
label="Rechnungsreferenz:"
>
<UInput
v-model="itemInfo.reference"
/>
</UFormGroup>
<InputGroup class="mt-3" gap="2">
<UFormGroup label="Rechnungsdatum:">
<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'"
variant="outline"
:color="!itemInfo.date ? 'rose' : 'primary'"
/>
<template #panel="{ close }">
<LazyDatePicker v-model="itemInfo.date" @close="itemInfo.dueDate = itemInfo.date" />
</template>
</UPopover>
</UFormGroup>
<UFormGroup label="Fälligkeitsdatum:">
<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'"
variant="outline"
/>
<template #panel="{ close }">
<LazyDatePicker v-model="itemInfo.dueDate" @close="close" />
</template>
</UPopover>
</UFormGroup>
</InputGroup>
<UFormGroup label="Zahlart:" >
<USelectMenu
:options="['Einzug','Kreditkarte','Überweisung','Sonstiges']"
v-model="itemInfo.paymentType"
/>
</UFormGroup>
<UFormGroup label="Beschreibung:" >
<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="accounts"
option-attribute="label"
value-attribute="id"
searchable
:search-attributes="['label']"
searchable-placeholder="Suche..."
v-model="item.account"
:color="!item.account ? 'rose' : 'primary'"
>
<template #label>
{{accounts.find(account => account.id === item.account) ? accounts.find(account => account.id === item.account).label : "Keine Kategorie ausgewählt" }}
</template>
<template #option="{option}">
{{option.number}} - {{option.label}}
</template>
</USelectMenu>
</UFormGroup>
<UFormGroup
label="Kostenstelle"
class=" mb-3"
>
<InputGroup class="w-full">
<USelectMenu
:options="costcentres"
option-attribute="name"
value-attribute="id"
searchable
:search-attributes="['label']"
searchable-placeholder="Suche..."
v-model="item.costCentre"
class="flex-auto"
>
<template #label>
{{costcentres.find(i => i.id === item.costCentre) ? costcentres.find(i => i.id === item.costCentre).name : "Keine Kostenstelle ausgewählt" }}
</template>
<template #option="{option}">
<span v-if="option.vehicle">{{option.number}} - Fahrzeug - {{option.name}}</span>
<span v-else-if="option.project">{{option.number}} - Projekt - {{option.name}}</span>
<span v-else-if="option.inventoryitem">{{option.number}} - Inventarartikel - {{option.name}}</span>
<span v-else>{{option.number}} - {{option.name}}</span>
</template>
</USelectMenu>
<UButton
variant="outline"
color="rose"
v-if="item.costCentre"
icon="i-heroicons-x-mark"
@click="item.costCentre = null"
/>
<EntityModalButtons
type="costcentres"
:id="item.costCentre"
@return-data="(data) => setCostCentre(item,data)"
/>
</InputGroup>
</UFormGroup>
<InputGroup>
<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"
:color="!item.amountNet ? 'rose' : 'primary'"
:disabled="item.taxType === null"
@keyup="item.amountTax = Number((item.amountNet * (Number(taxOptions.find(i => i.key === item.taxType).percentage)/100)).toFixed(2)),
item.amountGross = Number(item.amountNet) + Number(item.amountTax)"
>
<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"
:color="!item.amountGross ? 'rose' : 'primary'"
:ui-menu="{ width: 'min-w-max' }"
@keyup="item.amountNet = Number((item.amountGross / (1 + Number(taxOptions.find(i => i.key === item.taxType).percentage)/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>
<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"
:ui-menu="{ width: 'min-w-max' }"
option-attribute="label"
@change="item.amountNet = Number((item.amountGross / (1 + Number(taxOptions.find(i => i.key === item.taxType).percentage)/100)).toFixed(2)),
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>
</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)]"
>
Betrag aufteilen
</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;
height: 80vh;
}
.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;
}
.workingContainer {
height: 80vh;
}
</style>

View File

@@ -2,18 +2,11 @@
import InputGroup from "~/components/InputGroup.vue"; import InputGroup from "~/components/InputGroup.vue";
import dayjs from "dayjs"; import dayjs from "dayjs";
import HistoryDisplay from "~/components/HistoryDisplay.vue"; import HistoryDisplay from "~/components/HistoryDisplay.vue";
import {useSupabaseSelect} from "~/composables/useSupabase.js";
definePageMeta({
middleware: "auth"
})
const dataStore = useDataStore() const dataStore = useDataStore()
const profileStore = useProfileStore()
const supabase = useSupabaseClient()
const route = useRoute() const route = useRoute()
const router = useRouter()
const toast = useToast()
const itemInfo = ref({ const itemInfo = ref({
vendor: 0, vendor: 0,
@@ -36,14 +29,18 @@ const itemInfo = ref({
}) })
const costcentres = ref([]) const costcentres = ref([])
const vendors = ref([])
const accounts = ref([])
const setup = async () => { const setup = async () => {
let filetype = (await supabase.from("filetags").select().eq("tenant",profileStore.currentTenant).eq("incomingDocumentType","invoices").single()).data.id let filetype = (await useEntities("filetags").select()).find(i=> i.incomingDocumentType === "invoices").id
console.log(filetype) console.log(filetype)
costcentres.value = await useSupabaseSelect("costcentres") costcentres.value = await useEntities("costcentres").select()
vendors.value = await useEntities("vendors").select()
accounts.value = await useEntities("accounts").selectSpecial()
itemInfo.value = await useSupabaseSelectSingle("incominginvoices", route.params.id, "*, files(*)") itemInfo.value = await useEntities("incominginvoices").selectSingle(route.params.id, "*, files(*)")
await loadFile(itemInfo.value.files[itemInfo.value.files.length-1].id) await loadFile(itemInfo.value.files[itemInfo.value.files.length-1].id)
} }
@@ -65,7 +62,7 @@ const loadFile = async (id) => {
const changeNetMode = (mode) => { const changeNetMode = (mode) => {
useNetMode.value = mode useNetMode.value = mode
itemInfo.value.accounts = [{account: null,amountNet: null,amountTax: null,taxType: '19'}] //itemInfo.value.accounts = [{account: null,amountNet: null,amountTax: null,taxType: '19'}]
} }
@@ -139,7 +136,7 @@ const updateIncomingInvoice = async (setBooked = false) => {
} else { } else {
item.state = "Entwurf" item.state = "Entwurf"
} }
const data = await dataStore.updateItem('incominginvoices',item) const data = await useEntities('incominginvoices').update(itemInfo.value.id,item)
} }
const findIncomingInvoiceErrors = computed(() => { const findIncomingInvoiceErrors = computed(() => {
@@ -172,6 +169,12 @@ const findIncomingInvoiceErrors = computed(() => {
<template> <template>
<UDashboardNavbar :title="'Eingangsbeleg erstellen'"> <UDashboardNavbar :title="'Eingangsbeleg erstellen'">
<template #right> <template #right>
<ArchiveButton
color="rose"
variant="outline"
type="incominginvoices"
@confirmed="useEntities('incominginvoices').archive(route.params.id)"
/>
<UButton <UButton
@click="updateIncomingInvoice(false)" @click="updateIncomingInvoice(false)"
> >
@@ -234,7 +237,7 @@ const findIncomingInvoiceErrors = computed(() => {
<InputGroup> <InputGroup>
<USelectMenu <USelectMenu
v-model="itemInfo.vendor" v-model="itemInfo.vendor"
:options="dataStore.vendors" :options="vendors"
option-attribute="name" option-attribute="name"
value-attribute="id" value-attribute="id"
searchable searchable
@@ -247,7 +250,7 @@ const findIncomingInvoiceErrors = computed(() => {
{{option.vendorNumber}} - {{option.name}} {{option.vendorNumber}} - {{option.name}}
</template> </template>
<template #label> <template #label>
{{dataStore.vendors.find(vendor => vendor.id === itemInfo.vendor) ? dataStore.vendors.find(vendor => vendor.id === itemInfo.vendor).name : 'Lieferant auswählen'}} {{vendors.find(vendor => vendor.id === itemInfo.vendor) ? vendors.find(vendor => vendor.id === itemInfo.vendor).name : 'Lieferant auswählen'}}
</template> </template>
</USelectMenu> </USelectMenu>
<EntityModalButtons <EntityModalButtons
@@ -256,10 +259,10 @@ const findIncomingInvoiceErrors = computed(() => {
@return-data="(data) => itemInfo.vendor = data.id" @return-data="(data) => itemInfo.vendor = data.id"
/> />
<UButton <UButton
icon="i-heroicons-x-mark" icon="i-heroicons-x-mark"
variant="outline" variant="outline"
color="rose" color="rose"
@click="itemInfo.vendor = null" @click="itemInfo.vendor = null"
/> />
</InputGroup> </InputGroup>
@@ -323,26 +326,26 @@ const findIncomingInvoiceErrors = computed(() => {
<InputGroup class="my-3"> <InputGroup class="my-3">
<UButton <UButton
:variant="!useNetMode ? 'solid' : 'outline'" :variant="!useNetMode ? 'solid' : 'outline'"
@click="changeNetMode(false)" @click="changeNetMode(false)"
> >
Brutto Brutto
</UButton> </UButton>
<UButton <UButton
:variant="useNetMode ? 'solid' : 'outline'" :variant="useNetMode ? 'solid' : 'outline'"
@click="changeNetMode(true)" @click="changeNetMode(true)"
> >
Netto Netto
</UButton> </UButton>
<!-- Brutto <!-- Brutto
<UToggle <UToggle
v-model="useNetMode" v-model="useNetMode"
@update:model-value="itemInfo.accounts = [{account: null,amountNet: null,amountTax: null,taxType: '19'}]" @update:model-value="itemInfo.accounts = [{account: null,amountNet: null,amountTax: null,taxType: '19'}]"
/> />
Netto--> Netto-->
</InputGroup> </InputGroup>
<table v-if="itemInfo.accounts.length > 1" class="w-full"> <table v-if="itemInfo.accounts.length > 1" class="w-full">
@@ -369,7 +372,7 @@ const findIncomingInvoiceErrors = computed(() => {
class="mb-3" class="mb-3"
> >
<USelectMenu <USelectMenu
:options="dataStore.accounts" :options="accounts"
option-attribute="label" option-attribute="label"
value-attribute="id" value-attribute="id"
searchable searchable
@@ -379,7 +382,7 @@ const findIncomingInvoiceErrors = computed(() => {
:color="!item.account ? 'rose' : 'primary'" :color="!item.account ? 'rose' : 'primary'"
> >
<template #label> <template #label>
{{dataStore.accounts.find(account => account.id === item.account) ? dataStore.accounts.find(account => account.id === item.account).label : "Keine Kategorie ausgewählt" }} {{accounts.find(account => account.id === item.account) ? accounts.find(account => account.id === item.account).label : "Keine Kategorie ausgewählt" }}
</template> </template>
</USelectMenu> </USelectMenu>

View File

@@ -1,9 +1,7 @@
<script setup> <script setup>
import dayjs from "dayjs" import dayjs from "dayjs"
import {useSum} from "~/composables/useSum.js"; import {useSum} from "~/composables/useSum.js";
definePageMeta({
middleware: "auth"
})
defineShortcuts({ defineShortcuts({
'/': () => { '/': () => {
@@ -44,64 +42,53 @@ const sum = useSum()
const items = ref([]) const items = ref([])
const selectedItem = ref(0) const selectedItem = ref(0)
const sort = ref({
column: 'date',
direction: 'desc'
})
const type = "incominginvoices"
const dataType = dataStore.dataTypes[type]
const setupPage = async () => { const setupPage = async () => {
items.value = await useSupabaseSelect("incominginvoices","*, vendor(id,name), statementallocations(id,amount)","created_at",false) items.value = await useEntities(type).select("*, vendor(id,name), statementallocations(id,amount)",sort.value.column,sort.value.direction === "asc")
} }
setupPage() setupPage()
const templateColumns = [ const selectedColumns = ref(tempStore.columns[type] ? tempStore.columns[type] : dataType.templateColumns.filter(i => !i.disabledInTable))
{ const columns = computed(() => dataType.templateColumns.filter((column) => !column.disabledInTable && selectedColumns.value.find(i => i.key === column.key)))
key: 'reference',
label: "Referenz:"
}, {
key: 'state',
label: "Status:"
},
{
key: "date",
label: "Datum"
},
{
key: "vendor",
label: "Lieferant"
},
{
key: "amount",
label: "Betrag"
},
{
key: "dueDate",
label: "Fälligkeitsdatum"
},
{
key: "paid",
label: "Bezahlt"
},
{
key: "paymentType",
label: "Zahlart"
},
{
key: "description",
label: "Beschreibung"
}
]
const selectedColumns = ref(templateColumns)
const columns = computed(() => templateColumns.filter((column) => selectedColumns.value.includes(column)))
const selectableFilters = ref(dataType.filters.map(i => i.name))
const selectedFilters = ref(dataType.filters.filter(i => i.default).map(i => i.name) || [])
const searchString = ref(tempStore.searchStrings['incominginvoices'] ||'') const searchString = ref(tempStore.searchStrings[type] ||'')
const clearSearchString = () => { const clearSearchString = () => {
tempStore.clearSearchString('incominginvoices') tempStore.clearSearchString(type)
searchString.value = '' searchString.value = ''
} }
const filteredRows = computed(() => { const filteredRows = computed(() => {
let filteredItems = useSearch(searchString.value, items.value) let tempItems = items.value.map(i => {
return {
...i,
class: i.archived ? 'bg-red-500/50 dark:bg-red-400/50' : null
}
})
return [...filteredItems.filter(i => i.state === "Vorbereitet"), ...filteredItems.filter(i => i.state !== "Vorbereitet")] if(selectedFilters.value.length > 0) {
selectedFilters.value.forEach(filterName => {
let filter = dataType.filters.find(i => i.name === filterName)
tempItems = tempItems.filter(filter.filterFunction)
})
}
tempItems = useSearch(searchString.value, tempItems)
return [...tempItems.filter(i => i.state === "Vorbereitet"), ...tempItems.filter(i => i.state !== "Vorbereitet")]
}) })
@@ -125,7 +112,7 @@ const isPaid = (item) => {
} }
const selectIncomingInvoice = (invoice) => { const selectIncomingInvoice = (invoice) => {
if(invoice.state === "Vorbereitet") { if(invoice.state === "Vorbereitet" ) {
router.push(`/incomingInvoices/edit/${invoice.id}`) router.push(`/incomingInvoices/edit/${invoice.id}`)
} else { } else {
router.push(`/incomingInvoices/show/${invoice.id}`) router.push(`/incomingInvoices/show/${invoice.id}`)
@@ -149,7 +136,7 @@ const selectIncomingInvoice = (invoice) => {
placeholder="Suche..." placeholder="Suche..."
class="hidden lg:block" class="hidden lg:block"
@keydown.esc="$event.target.blur()" @keydown.esc="$event.target.blur()"
@change="tempStore.modifySearchString('incominginvoices',searchString)" @change="tempStore.modifySearchString(type,searchString)"
> >
<template #trailing> <template #trailing>
<UKbd value="/" /> <UKbd value="/" />
@@ -171,54 +158,90 @@ const selectIncomingInvoice = (invoice) => {
<USelectMenu <USelectMenu
v-model="selectedColumns" v-model="selectedColumns"
icon="i-heroicons-adjustments-horizontal-solid" icon="i-heroicons-adjustments-horizontal-solid"
:options="templateColumns" :options="dataType.templateColumns.filter(i => !i.disabledInTable)"
multiple multiple
class="hidden lg:block" class="hidden lg:block"
by="key" by="key"
:color="selectedColumns.length !== dataType.templateColumns.filter(i => !i.disabledInTable).length ? 'primary' : 'white'"
:ui-menu="{ width: 'min-w-max' }"
@change="tempStore.modifyColumns(type,selectedColumns)"
> >
<template #label> <template #label>
Spalten Spalten
</template> </template>
</USelectMenu> </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
</template>
</USelectMenu>
</template> </template>
</UDashboardToolbar> </UDashboardToolbar>
<UDashboardPanelContent> <UTabs
<UTable class="m-3"
:rows="filteredRows" :items="[{label: 'In Bearbeitung'},{label: 'Gebucht'}]"
:columns="columns" >
class="w-full" <template #default="{item}">
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }" {{item.label}}
@select="(i) => selectIncomingInvoice(i) " <UBadge
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Belege anzuzeigen' }" variant="outline"
> class="ml-2"
<template #reference-data="{row}"> >
<span v-if="row === filteredRows[selectedItem]" class="text-primary-500 font-bold">{{row.reference}}</span> {{filteredRows.filter(i => item.label === 'Gebucht' ? i.state === 'Gebucht' : i.state !== 'Gebucht' ).length}}
<span v-else>{{row.reference}}</span> </UBadge>
</template> </template>
<template #state-data="{row}"> <template #item="{item}">
<span v-if="row.state === 'Vorbereitet'" class="text-cyan-500">{{row.state}}</span> <div style="height: 80dvh; overflow-y: scroll">
<span v-else-if="row.state === 'Entwurf'" class="text-red-500">{{row.state}}</span> <UTable
<span v-else-if="row.state === 'Gebucht'" class="text-primary-500">{{row.state}}</span> v-model:sort="sort"
</template> sort-mode="manual"
<template #date-data="{row}"> @update:sort="setupPage"
{{dayjs(row.date).format("DD.MM.YYYY")}} :rows="filteredRows.filter(i => item.label === 'Gebucht' ? i.state === 'Gebucht' : i.state !== 'Gebucht' )"
</template> :columns="columns"
<template #vendor-data="{row}"> class="w-full"
{{row.vendor ? row.vendor.name : ""}} :ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
</template> @select="(i) => selectIncomingInvoice(i) "
<template #amount-data="{row}"> :empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Belege anzuzeigen' }"
{{displayCurrency(sum.getIncomingInvoiceSum(row))}} >
</template> <template #reference-data="{row}">
<template #dueDate-data="{row}"> <span v-if="row === filteredRows[selectedItem]" class="text-primary-500 font-bold">{{row.reference}}</span>
<span v-if="row.dueDate">{{dayjs(row.dueDate).format("DD.MM.YYYY")}}</span> <span v-else>{{row.reference}}</span>
</template> </template>
<template #paid-data="{row}"> <template #state-data="{row}">
<span v-if="isPaid(row)" class="text-primary-500">Bezahlt</span> <span v-if="row.state === 'Vorbereitet'" class="text-cyan-500">{{row.state}}</span>
<span v-else class="text-rose-600">Offen</span> <span v-else-if="row.state === 'Entwurf'" class="text-red-500">{{row.state}}</span>
</template> <span v-else-if="row.state === 'Gebucht'" class="text-primary-500">{{row.state}}</span>
</UTable> </template>
</UDashboardPanelContent> <template #date-data="{row}">
{{dayjs(row.date).format("DD.MM.YYYY")}}
</template>
<template #vendor-data="{row}">
{{row.vendor ? row.vendor.name : ""}}
</template>
<template #amount-data="{row}">
{{displayCurrency(sum.getIncomingInvoiceSum(row))}}
</template>
<template #dueDate-data="{row}">
<span v-if="row.dueDate">{{dayjs(row.dueDate).format("DD.MM.YYYY")}}</span>
</template>
<template #paid-data="{row}">
<span v-if="isPaid(row)" class="text-primary-500">Bezahlt</span>
<span v-else class="text-rose-600">Offen</span>
</template>
</UTable>
</div>
</template>
</UTabs>
</template> </template>

View File

@@ -1,13 +1,10 @@
<script setup> <script setup>
import dayjs from "dayjs"; import dayjs from "dayjs";
definePageMeta({
middleware: "auth"
})
const dataStore = useDataStore() const dataStore = useDataStore()
const profileStore = useProfileStore() const profileStore = useProfileStore()
const supabase = useSupabaseClient()
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
@@ -45,7 +42,7 @@ const loading = ref(true)
const setupPage = async () => { const setupPage = async () => {
if((mode.value === "show") && route.params.id){ if((mode.value === "show") && route.params.id){
itemInfo.value = await useSupabaseSelectSingle("incominginvoices",route.params.id,"*, files(*), vendor(*)") itemInfo.value = await useEntities("incominginvoices").selectSingle(route.params.id,"*, files(*), vendor(*)")
if(process.dev) console.log(itemInfo.value) if(process.dev) console.log(itemInfo.value)
currentDocument.value = await useFiles().selectDocument(itemInfo.value.files[0].id) currentDocument.value = await useFiles().selectDocument(itemInfo.value.files[0].id)
} }

View File

@@ -1,13 +1,13 @@
<template> <template>
<UDashboardNavbar title="Home"> <UDashboardNavbar title="Home">
<template #right> <template #right>
<UTooltip text="Notifications" :shortcuts="['N']"> <!-- <UTooltip text="Notifications" :shortcuts="['N']">
<UButton color="gray" variant="ghost" square @click="isNotificationsSlideoverOpen = true"> <UButton color="gray" variant="ghost" square @click="isNotificationsSlideoverOpen = true">
<UChip :show="unreadMessages" color="primary" inset> <UChip :show="unreadMessages" color="primary" inset>
<UIcon name="i-heroicons-bell" class="w-5 h-5" /> <UIcon name="i-heroicons-bell" class="w-5 h-5" />
</UChip> </UChip>
</UButton> </UButton>
</UTooltip> </UTooltip>-->
</template> </template>
</UDashboardNavbar> </UDashboardNavbar>
@@ -23,13 +23,11 @@
<UPageGrid> <UPageGrid>
<UDashboardCard <UDashboardCard
title="Buchhaltung" title="Buchhaltung"
v-if="profileStore.ownTenant.features.accounting"
> >
<display-open-balances/> <display-open-balances/>
</UDashboardCard> </UDashboardCard>
<UDashboardCard <UDashboardCard
title="Bank" title="Bank"
v-if="profileStore.ownTenant.features.accounting"
> >
<display-bankaccounts/> <display-bankaccounts/>
</UDashboardCard> </UDashboardCard>
@@ -38,7 +36,7 @@
> >
<display-projects-in-phases/> <display-projects-in-phases/>
</UDashboardCard> </UDashboardCard>
<UDashboardCard <!--<UDashboardCard
title="Anwesende" title="Anwesende"
> >
<display-present-profiles/> <display-present-profiles/>
@@ -52,7 +50,7 @@
title="Anwesenheiten" title="Anwesenheiten"
> >
<display-running-working-time/> <display-running-working-time/>
</UDashboardCard> </UDashboardCard>-->
<UDashboardCard <UDashboardCard
title="Aufgaben" title="Aufgaben"
> >
@@ -64,36 +62,14 @@
<script setup> <script setup>
import DisplayPresentProfiles from "~/components/noAutoLoad/displayPresentProfiles.vue";
definePageMeta({ definePageMeta({
middleware: ["auth","redirect-to-mobile-index"] middleware: 'redirect-to-mobile-index'
}) })
const dataStore = useDataStore()
const profileStore = useProfileStore()
const toast = useToast()
const router = useRouter()
const { isNotificationsSlideoverOpen } = useDashboard() const { isNotificationsSlideoverOpen } = useDashboard()
const items = [[{
label: 'Aufgabe',
icon: 'i-heroicons-paper-airplane',
to: '/tasks/create'
}, {
label: 'Kunde',
icon: 'i-heroicons-user-plus',
to: '/customers/create'
}]]
const supabase = useSupabaseClient()
const user = useSupabaseUser()
const unreadMessages = ref(false)
const setup = async () => { const setup = async () => {
unreadMessages.value = (await supabase.from("notifications").select("id,read").eq("read",false)).data.length > 0
} }
setup() setup()

View File

@@ -1,428 +0,0 @@
<script setup>
definePageMeta({
middleware: "auth"
})
const dataStore = useDataStore()
const profileStore = useProfileStore()
const supabase = useSupabaseClient()
const router = useRouter()
const mode = ref("incoming")
const toast = useToast()
const inventoryChangeData = ref({
productId: null,
sourceSpaceId: null,
sourceProjectId: null,
destinationSpaceId: null,
destinationProjectId: null,
quantity: 1,
serials: []
})
const resetInput = () => {
inventoryChangeData.value = {
productId: null,
sourceSpaceId: null,
sourceProjectId: null,
destinationSpaceId: null,
destinationProjectId: null,
quantity: 1
}
}
const createMovement = async () => {
let movements = []
if(mode.value === 'incoming'){
let movement = {
productId: inventoryChangeData.value.productId,
spaceId: inventoryChangeData.value.destinationSpaceId,
projectId: inventoryChangeData.value.destinationProjectId,
quantity: inventoryChangeData.value.quantity,
profileId: profileStore.activeProfile.id,
tenant: profileStore.currentTenant
}
movements.push(movement)
/*const {error} = await supabase
.from("movements")
.insert([inventoryChangeData.value])
.select()
if(error) console.log(error)*/
} else if (mode.value === 'outgoing'){
let movement = {
productId: inventoryChangeData.value.productId,
spaceId: inventoryChangeData.value.sourceSpaceId,
projectId: inventoryChangeData.value.sourceProjectId,
quantity: inventoryChangeData.value.quantity * -1,
profileId: profileStore.activeProfile.id,
tenant: profileStore.currentTenant
}
movements.push(movement)
} else if (mode.value === 'change'){
let outMovement = {
productId: inventoryChangeData.value.productId,
spaceId: inventoryChangeData.value.sourceSpaceId,
projectId: inventoryChangeData.value.sourceProjectId,
quantity: inventoryChangeData.value.quantity * -1,
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: profileStore.activeProfile.id,
tenant: profileStore.currentTenant
}
movements.push(outMovement)
movements.push(inMovement)
}
console.log(movements)
const {error} = await supabase
.from("movements")
.insert(movements)
.select()
if(error) {
console.log(error)
} else {
resetInput()
}
}
defineShortcuts({
meta_enter: {
usingInput: true,
handler: () => {
createMovement()
}
}
})
function checkProductId(productId) {
return dataStore.products.filter(product =>product.id === productId).length > 0;
}
function checkSpaceId(spaceId) {
return dataStore.spaces.filter(space => space.id === spaceId).length > 0;
}
function checkProjectId(projectId) {
return dataStore.projects.some(i => i.id === projectId)
}
function changeFocusToSpaceId() {
document.getElementById('spaceIdInput').focus()
}
function changeFocusToQuantity() {
document.getElementById('quantityInput').focus()
}
function changeFocusToBarcode() {
document.getElementById('barcodeInput').focus()
}
const findProductByBarcodeOrEAN = (input) => {
return dataStore.products.find(i => i.barcode === input || i.ean === input || i.articleNumber === input)
}
const findSpaceBySpaceNumber = (input) => {
return dataStore.spaces.find(i => i.spaceNumber === input)
}
const barcodeInput = ref("")
const showBarcodeTip = ref(true)
const serialInput = ref("")
const processBarcodeInput = () => {
if(findProductByBarcodeOrEAN(barcodeInput.value) && !findSpaceBySpaceNumber(barcodeInput.value)){
//Set Product
inventoryChangeData.value.productId = findProductByBarcodeOrEAN(barcodeInput.value).id
} else if (!findProductByBarcodeOrEAN(barcodeInput.value) && findSpaceBySpaceNumber(barcodeInput.value)){
//Set Space
if(mode.value === 'incoming'){
inventoryChangeData.value.destinationSpaceId = findSpaceBySpaceNumber(barcodeInput.value).id
} else if(mode.value === 'outgoing') {
inventoryChangeData.value.sourceSpaceId = findSpaceBySpaceNumber(barcodeInput.value).id
} else if(mode.value === 'change') {
if(!inventoryChangeData.value.sourceSpaceId){
inventoryChangeData.value.sourceSpaceId = findSpaceBySpaceNumber(barcodeInput.value).id
} else {
inventoryChangeData.value.destinationSpaceId = findSpaceBySpaceNumber(barcodeInput.value).id
}
}
//console.log(findSpaceBySpaceNumber(barcodeInput.value))
}
barcodeInput.value = ""
//console.log(movementData.value)
}
</script>
<template>
<UDashboardNavbar
title="Lager Vorgänge"
>
<template #right>
<UButton
@click="resetInput"
class="mt-3"
color="rose"
variant="outline"
>
Abbrechen
</UButton>
<UButton
@click="createMovement"
:disabled="mode === '' && checkSpaceId(inventoryChangeData.spaceId) && checkProductId(inventoryChangeData.productId)"
class="mt-3"
>
Bestätigen
</UButton>
</template>
</UDashboardNavbar>
<UDashboardPanelContent>
<div class="w-80 mx-auto mt-5">
<div class="flex flex-col">
<UButton
@click="mode = 'incoming'"
class="my-2"
:variant="mode === 'incoming' ? 'solid' : 'outline'"
>Wareneingang</UButton>
<UButton
@click="mode = 'outgoing'"
class="my-2"
:variant="mode === 'outgoing' ? 'solid' : 'outline'"
>Warenausgang</UButton>
<UButton
@click="mode = 'change'"
class="my-2"
:variant="mode === 'change' ? 'solid' : 'outline'"
>Umlagern</UButton>
</div>
<UAlert
title="Info"
variant="outline"
color="primary"
v-if="showBarcodeTip"
@close="showBarcodeTip = false"
:close-button="{ icon: 'i-heroicons-x-mark-20-solid', color: 'gray', variant: 'link', padded: false }"
description="Über die Barcode Eingabe können folgende Werte automatisch erkannt werden: Quelllagerplatz, Ziellagerplatz, Artikel (EAN oder Barcode). Es wird immer zuerst der Quell- und anschließend der Ziellagerplatz ausgefüllt."
/>
<!-- <UTooltip
text="Über die Barcode Eingabe könenn folgende Werte automatisch erkannt werden: Quell Lagerplatz, Ziellagerplatz, Artikel(EAN oder Barcode). Es wird immer zuerst der Quell- und anschließend der Ziellagerplatz ausgefüllt."
>-->
<UFormGroup
label="Barcode:"
class="mt-3"
>
<UInput
@keyup.enter="processBarcodeInput"
@focusout="processBarcodeInput"
@input="processBarcodeInput"
v-model="barcodeInput"
id="barcodeInput"
/>
</UFormGroup>
<!-- <template #text>
<span class="text-wrap">Über die Barcode Eingabe könenn folgende Werte automatisch erkannt werden: Quell Lagerplatz, Ziellagerplatz, Artikel(EAN oder Barcode). Es wird immer zuerst der Quell- und anschließend der Ziellagerplatz ausgefüllt.</span>
</template>
</UTooltip>-->
<UDivider
class="mt-5 w-80"
v-if="mode !== 'incoming'"
/>
<UFormGroup
label="Quell Lagerplatz:"
class="mt-3 w-80"
v-if="mode !== 'incoming' "
>
<USelectMenu
:options="dataStore.spaces"
searchable
option-attribute="spaceNumber"
:color="checkSpaceId(inventoryChangeData.sourceSpaceId) ? 'primary' : 'rose'"
v-model="inventoryChangeData.sourceSpaceId"
@change="inventoryChangeData.sourceProjectId = null"
value-attribute="id"
>
<template #label>
{{dataStore.spaces.find(space => space.id === inventoryChangeData.sourceSpaceId) ? dataStore.spaces.find(space => space.id === inventoryChangeData.sourceSpaceId).description : "Kein Lagerplatz ausgewählt"}}
</template>
</USelectMenu>
</UFormGroup>
<UFormGroup
label="Quell Projekt:"
class="mt-3 w-80"
v-if="mode !== 'incoming' "
>
<USelectMenu
:options="dataStore.projects"
searchable
option-attribute="name"
:color="checkProjectId(inventoryChangeData.sourceProjectId) ? 'primary' : 'rose'"
v-model="inventoryChangeData.sourceProjectId"
@change="inventoryChangeData.sourceSpaceId = null"
value-attribute="id"
>
<template #label>
{{dataStore.getProjectById(inventoryChangeData.sourceProjectId) ? dataStore.getProjectById(inventoryChangeData.sourceProjectId).name : "Kein Projekt ausgewählt"}}
</template>
</USelectMenu>
</UFormGroup>
<UDivider
class="mt-5 w-80"
/>
<UFormGroup
label="Artikel:"
class="mt-3 w-80"
>
<USelectMenu
:options="dataStore.products"
option-attribute="name"
value-attribute="id"
variant="outline"
searchable
:search-attributes="['name','ean', 'barcode']"
:color="checkProductId(inventoryChangeData.productId) ? 'primary' : 'rose'"
v-model="inventoryChangeData.productId"
v-on:select="changeFocusToSpaceId"
>
<template #label>
{{dataStore.products.find(product => product.id === inventoryChangeData.productId) ? dataStore.products.find(product => product.id === inventoryChangeData.productId).name : "Bitte Artikel auswählen"}}
</template>
</USelectMenu>
</UFormGroup>
<UDivider
class="mt-5 w-80"
v-if="mode !== 'outgoing'"
/>
<UFormGroup
label="Ziel Lagerplatz:"
class="mt-3 w-80"
v-if="mode !== 'outgoing'"
>
<USelectMenu
:options="dataStore.spaces"
searchable
option-attribute="spaceNumber"
:color="checkSpaceId(inventoryChangeData.destinationSpaceId) ? 'primary' : 'rose'"
v-model="inventoryChangeData.destinationSpaceId"
@change="inventoryChangeData.destinationProjectId = null"
value-attribute="id"
>
<template #label>
{{dataStore.spaces.find(space => space.id === inventoryChangeData.destinationSpaceId) ? dataStore.spaces.find(space => space.id === inventoryChangeData.destinationSpaceId).description : "Kein Lagerplatz ausgewählt"}}
</template>
</USelectMenu>
</UFormGroup>
<UFormGroup
label="Ziel Projekt:"
class="mt-3 w-80"
v-if="mode !== 'outgoing'"
>
<USelectMenu
:options="dataStore.projects"
searchable
option-attribute="name"
:color="checkProjectId(inventoryChangeData.destinationProjectId) ? 'primary' : 'rose'"
v-model="inventoryChangeData.destinationProjectId"
value-attribute="id"
@change="inventoryChangeData.destinationSpaceId = null"
>
<template #label>
{{dataStore.getProjectById(inventoryChangeData.destinationProjectId) ? dataStore.getProjectById(inventoryChangeData.destinationProjectId).name : "Kein Projekt ausgewählt"}}
</template>
</USelectMenu>
</UFormGroup>
<UDivider
class="mt-5 w-80"
/>
<UFormGroup
label="Anzahl:"
class="mt-3 w-80"
>
<UInput
variant="outline"
color="primary"
placeholder="Anzahl"
v-model="inventoryChangeData.quantity"
type="number"
id="quantityInput"
/>
</UFormGroup>
<UFormGroup
label="Seriennummern:"
class="mt-3 w-80"
>
<InputGroup class="w-full">
<UInput
variant="outline"
color="primary"
placeholder="Seriennummern"
v-model="serialInput"
/>
<UButton
@click="inventoryChangeData.serials.push(serialInput)"
>
+
</UButton>
</InputGroup>
</UFormGroup>
<ul>
<li v-for="serial in inventoryChangeData.serials">{{serial}}</li>
</ul>
</div>
</UDashboardPanelContent>
</template>
<style scoped>
</style>

View File

@@ -1,152 +0,0 @@
<template>
<UDashboardNavbar title="Bestände" :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(`/products/create`)">+ Artikel</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(`/products/show/${i.id}`) "
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Artikel 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 #stock-data="{row}">
{{`${dataStore.getStockByProductId(row.id)} ${(dataStore.units.find(unit => unit.id === row.unit) ? dataStore.units.find(unit => unit.id === row.unit).name : "")}`}}
</template>
</UTable>
</template>
<script setup>
definePageMeta({
middleware: "auth"
})
defineShortcuts({
'/': () => {
document.getElementById("searchinput").focus()
},
'+': () => {
router.push("/products/create")
},
'Enter': {
usingInput: true,
handler: () => {
router.push(`/products/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("products","*")
}
setupPage()
const templateColumns = [
{
key: "stock",
label: "Bestand"
},
{
key: "name",
label: "Name",
sortable: true
},
]
const selectedColumns = ref(templateColumns)
const columns = computed(() => templateColumns.filter((column) => selectedColumns.value.includes(column)))
const templateTags = computed(() => {
let temp = []
dataStore.products.forEach(row => {
row.tags.forEach(tag => {
if(!temp.includes(tag)) temp.push(tag)
})
})
return temp
})
const selectedTags = ref(templateTags.value)
const searchString = ref('')
const filteredRows = computed(() => {
let temp = items.value.filter(i => i.tags.some(x => selectedTags.value.includes(x)) || i.tags.length === 0)
return useSearch(searchString.value, temp)
})
</script>
<style scoped>
</style>

View File

@@ -1,120 +1,47 @@
<script setup > <script setup lang="ts">
import {useProfileStore} from "~/stores/profile.js";
import {useCapacitor} from "~/composables/useCapacitor.js";
definePageMeta({ definePageMeta({
layout: "notLoggedIn" layout: "notLoggedIn"
}) })
const supabase = useSupabaseClient() const auth = useAuthStore()
const user = useSupabaseUser()
const router = useRouter()
const colorMode = useColorMode()
const toast = useToast() const toast = useToast()
const profileStore = useProfileStore()
const isLight = computed({
get () {
return colorMode.value !== 'dark'
},
set () {
colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
}
})
const email = ref("")
const password = ref("")
const fields = [{
name: 'email',
type: 'text',
label: 'Email',
placeholder: 'E-Mail Adresse'
}, {
name: 'password',
label: 'Password',
type: 'password',
placeholder: 'Passwort'
}]
const authenticateWithAzure = async () => { const doLogin = async (data:any) => {
const { data, error } = await supabase.auth.signInWithOAuth({ try {
provider: 'azure', await auth.login(data.email, data.password)
options: { // Weiterleiten nach erfolgreichem Login
scopes: 'email', toast.add({title:"Einloggen erfolgreich"})
}, if(useCapacitor().getIsNative()) {
}) return navigateTo("/mobile")
console.log(data)
console.log(error)
}
const onSubmit = async (data) => {
const {error, data:{ user}} = await supabase.auth.signInWithPassword({
email: data.email,
password: data.password
})
if(error) {
if(error.toString().toLowerCase().includes("invalid")){
toast.add({title:"Zugangsdaten falsch",color:"rose"})
}
} else {
//console.log("Login Successful")
profileStore.initializeData(user.id)
if(await useCapacitor().getIsPhone()) {
router.push("/mobile")
} else { } else {
router.push("/") return navigateTo("/")
} }
} catch (err: any) {
toast.add({title:"Zugangsdaten falsch. Bitte überprüfen Sie Ihre Eingaben",color:"rose"})
} }
} }
</script> </script>
<template> <template>
<!-- <div id="loginSite">
<div id="loginForm">
<UFormGroup
label="E-Mail:"
>
<UInput
v-model="email"
/>
</UFormGroup>
<UFormGroup
label="Passwort:"
>
<UInput
v-model="password"
type="password"
@keyup.enter="onSubmit"
/>
</UFormGroup>
<UButton
@click="onSubmit"
class="mt-3"
>
Einloggen
</UButton>
</div>
</div>-->
<UCard class="max-w-sm w-full mx-auto mt-5"> <UCard class="max-w-sm w-full mx-auto mt-5">
<UColorModeImage <UColorModeImage
light="/Logo.png" light="/Logo.png"
dark="/Logo_Dark.png" dark="/Logo_Dark.png"
/> />
<UAlert
title="Achtung"
description="Es wurden alle Benutzerkonten zurückgesetzt. Bitte fordert über Passwort vergessen ein neues Passwort an."
color="rose"
variant="outline"
class="my-5"
>
</UAlert>
<UAuthForm <UAuthForm
title="Login" title="Login"
description="Geben Sie Ihre Anmeldedaten ein um Zugriff auf Ihren Account zu erhalten." description="Geben Sie Ihre Anmeldedaten ein um Zugriff auf Ihren Account zu erhalten."
@@ -131,29 +58,13 @@ const onSubmit = async (data) => {
placeholder: 'Dein Passwort' placeholder: 'Dein Passwort'
}]" }]"
:loading="false" :loading="false"
@submit="onSubmit" @submit="doLogin"
:providers="[{label: 'MS365',icon: 'i-simple-icons-microsoft',color: 'gray',click: authenticateWithAzure}]"
:submit-button="{label: 'Weiter'}" :submit-button="{label: 'Weiter'}"
divider="oder" divider="oder"
> >
<template #password-hint>
<NuxtLink to="/password-reset" class="text-primary font-medium">Passwort vergessen?</NuxtLink>
</template>
</UAuthForm> </UAuthForm>
</UCard> </UCard>
</template>
</template>
<style scoped>
#loginSite {
display: flex;
align-content: center;
justify-content: center;
}
#loginForm {
width: 30vw;
height: 30vh;
}
</style>

View File

@@ -6,7 +6,7 @@ definePageMeta({
layout: 'mobile' layout: 'mobile'
}) })
const profileStore = useProfileStore() //const profileStore = useProfileStore()
</script> </script>
@@ -21,7 +21,7 @@ const profileStore = useProfileStore()
> >
<display-open-tasks/> <display-open-tasks/>
</UDashboardCard> </UDashboardCard>
<UDashboardCard <!--<UDashboardCard
title="Anwesenheit" title="Anwesenheit"
> >
<display-running-working-time/> <display-running-working-time/>
@@ -36,7 +36,7 @@ const profileStore = useProfileStore()
v-if="profileStore.ownTenant.features.accounting" v-if="profileStore.ownTenant.features.accounting"
> >
<display-open-balances/> <display-open-balances/>
</UDashboardCard> </UDashboardCard>-->
<UDashboardCard <UDashboardCard
title="Projekte" title="Projekte"
> >

View File

@@ -4,7 +4,7 @@ definePageMeta({
layout: 'mobile', layout: 'mobile',
}) })
const profileStore = useProfileStore() const auth = useAuthStore()
</script> </script>
@@ -65,17 +65,23 @@ const profileStore = useProfileStore()
> >
Objekte Objekte
</UButton> </UButton>
<UButton
class="w-full my-1"
@click="auth.logout()"
color="rose"
variant="outline"
>
Abmelden
</UButton>
<UDivider class="my-5">Unternehmen wechseln</UDivider> <UDivider class="my-5">Unternehmen wechseln</UDivider>
<UButton <div class="w-full flex flex-row justify-between my-3" v-for="tenant in auth.tenants">
v-for="option in profileStore.ownProfiles" <span class="text-left">{{tenant.name}}</span>
class="my-1" <UButton
variant="outline" @click="auth.switchTenant(tenant.id)"
@click="profileStore.changeProfile(option.id)" >Wechseln</UButton>
> </div>
{{profileStore.tenants.find(i => i.id === option.tenant).name}}
</UButton>
</UDashboardPanelContent> </UDashboardPanelContent>

62
pages/password-change.vue Normal file
View File

@@ -0,0 +1,62 @@
<script setup lang="ts">
definePageMeta({
layout: "notLoggedIn"
})
const auth = useAuthStore()
const toast = useToast()
const doChange = async (data:any) => {
try {
const res = await useNuxtApp().$api("/api/auth/password/change", {
method: "POST",
body: {
old_password: data.oldPassword,
new_password: data.newPassword,
}
})
// Weiterleiten nach erfolgreichem Login
toast.add({title:"Ändern erfolgreich"})
await auth.logout()
return navigateTo("/login")
} catch (err: any) {
toast.add({title:"Es gab ein Problem beim ändern",color:"rose"})
}
}
</script>
<template>
<UCard class="max-w-sm w-full mx-auto mt-5">
<UColorModeImage
light="/Logo.png"
dark="/Logo_Dark.png"
/>
<UAuthForm
title="Passwort zurücksetzen"
description="Geben Sie Ihre E-Mail ein um ein neues Passwort per E-Mail zu erhalten."
align="bottom"
:fields="[{
name: 'oldPassword',
label: 'Altes Passwort',
type: 'password',
placeholder: 'Dein altes Passwort'
},{
name: 'newPassword',
label: 'Neues Passwort',
type: 'password',
placeholder: 'Dein neues Passwort'
}]"
:loading="false"
@submit="doChange"
:submit-button="{label: 'Ändern'}"
divider="oder"
>
</UAuthForm>
</UCard>
</template>

55
pages/password-reset.vue Normal file
View File

@@ -0,0 +1,55 @@
<script setup lang="ts">
definePageMeta({
layout: "notLoggedIn"
})
const auth = useAuthStore()
const toast = useToast()
const doReset = async (data:any) => {
try {
const res = await useNuxtApp().$api("/auth/password/reset", {
method: "POST",
body: {
email: data.email
}
})
// Weiterleiten nach erfolgreichem Login
toast.add({title:"Zurücksetzen erfolgreich"})
return navigateTo("/login")
} catch (err: any) {
toast.add({title:"Problem beim zurücksetzen",color:"rose"})
}
}
</script>
<template>
<UCard class="max-w-sm w-full mx-auto mt-5">
<UColorModeImage
light="/Logo.png"
dark="/Logo_Dark.png"
/>
<UAuthForm
title="Passwort zurücksetzen"
description="Geben Sie Ihre E-Mail ein um ein neues Passwort per E-Mail zu erhalten."
align="bottom"
:fields="[{
name: 'email',
type: 'text',
label: 'Email',
placeholder: 'Deine E-Mail Adresse'
}]"
:loading="false"
@submit="doReset"
:submit-button="{label: 'Zurücksetzen'}"
divider="oder"
>
</UAuthForm>
</UCard>
</template>

View File

@@ -3,13 +3,21 @@
const profileStore = useProfileStore() const profileStore = useProfileStore()
const router = useRouter() const router = useRouter()
const items = ref([])
const setupPage = async () => {
items.value = (await useNuxtApp().$api("/api/tenant/users")).users
items.value = items.value.map(i => i.profile)
}
setupPage()
const templateColumns = [ const templateColumns = [
{ {
key: 'employeeNumber', key: 'employee_number',
label: "MA-Nummer", label: "MA-Nummer",
},{ },{
key: 'fullName', key: 'full_name',
label: "Name", label: "Name",
},{ },{
key: "email", key: "email",
@@ -26,6 +34,7 @@
<template #right> <template #right>
<UButton <UButton
@click="router.push(`/profiles/create`)" @click="router.push(`/profiles/create`)"
disabled
> >
+ Mitarbeiter + Mitarbeiter
</UButton> </UButton>
@@ -33,8 +42,7 @@
</UDashboardNavbar> </UDashboardNavbar>
<UTable <UTable
:rows="profileStore.profiles" :rows="items"
@select="(item) => router.push(`/profiles/show/${item.id}`)"
:columns="columns" :columns="columns"
> >

View File

@@ -2,9 +2,7 @@
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
definePageMeta({
middleware: "auth"
})
defineShortcuts({ defineShortcuts({
'backspace': () => { 'backspace': () => {
@@ -23,8 +21,6 @@ defineShortcuts({
}) })
const openTab = ref(0) const openTab = ref(0)
const dataStore = useDataStore()
const supabase = useSupabaseClient()
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const toast = useToast() const toast = useToast()
@@ -55,9 +51,9 @@ const setKeys = () => {
const setupPage = async() => { const setupPage = async() => {
if(mode.value === "show" ){ if(mode.value === "show" ){
itemInfo.value = await useSupabaseSelectSingle("projecttypes",route.params.id,"*") itemInfo.value = await useEntities("projecttypes").selectSingle(route.params.id,"*")
} else if (mode.value === "edit") { } else if (mode.value === "edit") {
itemInfo.value = await useSupabaseSelectSingle("projecttypes",route.params.id,"*") itemInfo.value = await useEntities("projecttypes").selectSingle(route.params.id,"*")
} }
if(mode.value === "create") { if(mode.value === "create") {
@@ -76,7 +72,7 @@ setupPage()
const addPhase = () => { const addPhase = () => {
itemInfo.value.initialPhases.push({label: '', icon: ''}), itemInfo.value.initialPhases.push({label: '', icon: ''}),
setKeys setKeys
} }
</script> </script>
@@ -101,13 +97,13 @@ const addPhase = () => {
<template #right> <template #right>
<UButton <UButton
v-if="mode === 'edit'" v-if="mode === 'edit'"
@click="dataStore.updateItem('projecttypes',itemInfo,oldItemInfo)" @click="useEntities('projecttypes').update(itemInfo.id, itemInfo)"
> >
Speichern Speichern
</UButton> </UButton>
<UButton <UButton
v-else-if="mode === 'create'" v-else-if="mode === 'create'"
@click="dataStore.createNewItem('projecttypes', itemInfo)" @click="useEntities('projecttypes').create( itemInfo)"
> >
Erstellen Erstellen
</UButton> </UButton>
@@ -160,7 +156,7 @@ const addPhase = () => {
variant="outline" variant="outline"
class="mb-5" class="mb-5"
v-if="mode === 'edit'" v-if="mode === 'edit'"
description="Achtung Änderungen an diesem Projekttypen betreffen nur Projekte die damit neu erstellt werden. Bestehende Projekte bleiben unverändert." description="Achtung Änderungen an diesem Projekttypen betreffen nur Projekte die damit neu erstellt werden. Bestehende Projekte bleiben unverändert."
/> />
<UFormGroup <UFormGroup
@@ -244,9 +240,9 @@ const addPhase = () => {
selectedKeyForQuickAction = phase.key" selectedKeyForQuickAction = phase.key"
>+ Schnellaktion</UButton> >+ Schnellaktion</UButton>
<UButton <UButton
@click="phase.quickactions = phase.quickactions.filter(i => i.label !== button.label)" @click="phase.quickactions = phase.quickactions.filter(i => i.label !== button.label)"
v-for="button in phase.quickactions" v-for="button in phase.quickactions"
class="ml-1" class="ml-1"
> >
{{ button.label }} {{ button.label }}
</UButton> </UButton>

View File

@@ -1,7 +1,5 @@
<script setup> <script setup>
definePageMeta({
middleware: "auth"
})
defineShortcuts({ defineShortcuts({
'/': () => { '/': () => {
@@ -19,14 +17,14 @@ defineShortcuts({
} }
}, },
'arrowdown': () => { 'arrowdown': () => {
if(selectedItem.value < filteredRows.value.length - 1) { if (selectedItem.value < filteredRows.value.length - 1) {
selectedItem.value += 1 selectedItem.value += 1
} else { } else {
selectedItem.value = 0 selectedItem.value = 0
} }
}, },
'arrowup': () => { 'arrowup': () => {
if(selectedItem.value === 0) { if (selectedItem.value === 0) {
selectedItem.value = filteredRows.value.length - 1 selectedItem.value = filteredRows.value.length - 1
} else { } else {
selectedItem.value -= 1 selectedItem.value -= 1
@@ -36,14 +34,14 @@ defineShortcuts({
const router = useRouter() const router = useRouter()
const tempStore = useTempStore()
const items = ref([]) const items = ref([])
const selectedItem = ref(0) const selectedItem = ref(0)
const setup = async () => { const setup = async () => {
items.value = await useSupabaseSelect("projecttypes","*") items.value = await useEntities("projecttypes").select()
} }
setup() setup()
@@ -58,7 +56,7 @@ const templateColumns = [
const selectedColumns = ref(templateColumns) const selectedColumns = ref(templateColumns)
const columns = computed(() => templateColumns.filter((column) => selectedColumns.value.includes(column))) const columns = computed(() => templateColumns.filter((column) => selectedColumns.value.includes(column)))
const searchString = ref("") const searchString = ref(tempStore.searchStrings["projecttypes"] || '')
const filteredRows = computed(() => { const filteredRows = computed(() => {
return useListFilter(searchString.value, items.value) return useListFilter(searchString.value, items.value)
@@ -77,9 +75,10 @@ const filteredRows = computed(() => {
placeholder="Suche..." placeholder="Suche..."
class="hidden lg:block" class="hidden lg:block"
@keydown.esc="$event.target.blur()" @keydown.esc="$event.target.blur()"
@change="tempStore.modifySearchString('projecttypes',searchString)"
> >
<template #trailing> <template #trailing>
<UKbd value="/" /> <UKbd value="/"/>
</template> </template>
</UInput> </UInput>
@@ -96,8 +95,8 @@ const filteredRows = computed(() => {
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Projekttypen anzuzeigen' }" :empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Projekttypen anzuzeigen' }"
> >
<template #name-data="{row}"> <template #name-data="{row}">
<span class="text-primary-500 font-bold" v-if="row === filteredRows[selectedItem]">{{row.name}}</span> <span class="text-primary-500 font-bold" v-if="row === filteredRows[selectedItem]">{{ row.name }}</span>
<span v-else>{{row.name}}</span> <span v-else>{{ row.name }}</span>
</template> </template>
</UTable> </UTable>

View File

@@ -1,12 +1,7 @@
<script setup> <script setup>
import HistoryDisplay from "~/components/HistoryDisplay.vue"; import HistoryDisplay from "~/components/HistoryDisplay.vue";
import DocumentList from "~/components/DocumentList.vue";
import DocumentUpload from "~/components/DocumentUpload.vue";
import {useSupabaseSelect} from "~/composables/useSupabase.js";
definePageMeta({
middleware: "auth"
})
defineShortcuts({ defineShortcuts({
'backspace': () => { 'backspace': () => {

View File

@@ -1,7 +1,5 @@
<script setup> <script setup>
definePageMeta({
middleware: "auth"
})
const items = ref([]) const items = ref([])
const setup = async () => { const setup = async () => {

View File

@@ -30,13 +30,7 @@ const checkBIC = async () => {
const generateLink = async (bankId) => { const generateLink = async (bankId) => {
try { try {
/*const {data,error} = await supabase.functions.invoke(`bankstatement_gateway`,{
body: {
method: "generateLink",
institutionId: bankData.value.id,
tenant: profileStore.currentTenant
}
})*/
const link = await useFunctions().useBankingGenerateLink(bankId || bankData.value.id) const link = await useFunctions().useBankingGenerateLink(bankId || bankData.value.id)
await navigateTo(link, { await navigateTo(link, {

View File

@@ -1,10 +1,7 @@
<script setup> <script setup>
definePageMeta({
middleware: "auth"
})
const dataStore = useDataStore() const dataStore = useDataStore()
const profileStore = useProfileStore() const profileStore = useProfileStore()
const supabase = useSupabaseClient()
const router = useRouter() const router = useRouter()
const items = [{ const items = [{

View File

@@ -1,127 +0,0 @@
<template>
<UDashboardNavbar >
<template #left>
<UButton
icon="i-heroicons-chevron-left"
variant="outline"
@click="router.push(`/settings/labels`)"
>
Labels
</UButton>
</template>
<template #center>
<h1
v-if="itemInfo"
class="text-xl font-medium"
>{{itemInfo.name ? `Label: ${itemInfo.name}` : (mode === 'create' ? 'Label erstellen' : 'Label bearbeiten')}}</h1>
</template>
</UDashboardNavbar>
<UTabs
:items="[{label: 'Informationen'}]"
v-if="mode === 'show' && itemInfo"
class="p-5"
v-model="openTab"
>
<template #item="{item}">
<UCard class="mt-5">
<div
v-if="item.label === 'Informationen'"
class="flex flex-row"
>
<div class="w-1/2 mr-5">
<UDivider>Allgemeines</UDivider>
<Toolbar>
<UButton @click="usePrintLabel('0dbe30f3-3008-4cde-8a7c-e785b1c22bfc','ZD411',useGenerateZPL(itemInfo.handlebarsZPL,{barcode:'XXX'}))">Test Druck</UButton>
</Toolbar>
<p>Name: {{itemInfo.name}}</p>
<p>Breite in Zoll: {{itemInfo.widthInch}}"</p>
<p>Höhe in Zoll: {{itemInfo.heightInch}}"</p>
<p>ZPL:</p>
<pre>{{itemInfo.handlebarsZPL}}</pre>
</div>
<div class="w-1/2">
<UDivider>Vorschau</UDivider>
<img
class="mx-auto mt-5"
v-if="demoZPL"
:src="`https://api.labelary.com/v1/printers/8dpmm/labels/${itemInfo.widthInch}x${itemInfo.heightInch}/0/${demoZPL}`"
/>
</div>
</div>
</UCard>
</template>
</UTabs>
</template>
<script setup>
defineShortcuts({
'backspace': () => {
router.push("/settings/labels")
},
'arrowleft': () => {
if(openTab.value > 0){
openTab.value -= 1
}
},
'arrowright': () => {
if(openTab.value < 3) {
openTab.value += 1
}
},
})
const router = useRouter()
const supabase = useSupabaseClient()
const dataStore = useDataStore()
const profileStore = useProfileStore()
const mode = useRoute().params.mode
const openTab = ref(0)
const itemInfo = ref({})
const setupPage = async () => {
itemInfo.value = await useSupabaseSelectSingle("printLabels",useRoute().params.id,'*')
renderDemoZPL()
}
const demoZPL = ref("")
const renderDemoZPL = () => {
let template = Handlebars.compile(itemInfo.value.handlebarsZPL)
demoZPL.value = template({barcode: "XXX"})
}
const printLabel = async () => {
await supabase.from("printJobs").insert({
tenant: profileStore.currentTenant,
rawContent: useGenerateZPL(itemInfo.value.handlebarsZPL,{barcode:"XXX"}),
printerName: "ZD411",
printServer: "0dbe30f3-3008-4cde-8a7c-e785b1c22bfc"
})
}
setupPage()
</script>
<style scoped>
img {
border: 1px solid black
}
</style>

View File

@@ -1,113 +0,0 @@
<template>
<UDashboardNavbar
title="Labels"
>
<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(`/settings/labels/create`)" disabled>+ Label</UButton>
</template>
</UDashboardNavbar>
<!-- <UDashboardToolbar>
</UDashboardToolbar>-->
<UTable
:rows="items"
:columns="columns"
@select="(i) => router.push(`/settings/labels/show/${i.id}`)"
class="w-full"
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Artikel 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>
</UTable>
</template>
<script setup>
definePageMeta({
middleware: "auth"
})
defineShortcuts({
'/': () => {
//console.log(searchinput)
//searchinput.value.focus()
document.getElementById("searchinput").focus()
},
'+': () => {
router.push("/settings/labels/create")
},
'Enter': {
usingInput: true,
handler: () => {
router.push(`/settings/labels/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 router = useRouter()
const items = ref([])
const selectedItem = ref(0)
const setupPage = async () => {
items.value = await useSupabaseSelect("printLabels","*")
}
setupPage()
const templateColumns = [{key: 'name',label:'Name'},{key: 'widthInch',label:'Breite in Zoll'},{key: 'heightInch',label:'Höhe in Zoll'}]
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,13 +1,5 @@
<script setup> <script setup>
definePageMeta({ const auth = useAuthStore()
middleware: "auth"
})
const supabase = useSupabaseClient()
const dataStore = useDataStore()
const profileStore = useProfileStore()
const resources = { const resources = {
customers: { customers: {
@@ -45,16 +37,19 @@ const resources = {
} }
} }
const numberRanges = ref(profileStore.ownTenant.numberRanges) const numberRanges = ref(auth.activeTenantData.numberRanges)
const updateNumberRanges = async (range) => { const updateNumberRanges = async (range) => {
const {data,error} = await supabase const res = await useNuxtApp().$api(`/api/tenant/numberrange/${range}`,{
.from("tenants") method: "PUT",
.update({numberRanges: numberRanges.value}) body: {
.eq('id',profileStore.currentTenant) numberRange: numberRanges.value[range]
}
})
console.log(res)
await profileStore.fetchOwnTenant()
} }
@@ -62,7 +57,7 @@ const updateNumberRanges = async (range) => {
<template> <template>
<UDashboardNavbar <UDashboardNavbar
title="Nummernkreise bearbeiten" title="Nummernkreise bearbeiten"
> >
</UDashboardNavbar> </UDashboardNavbar>
@@ -76,7 +71,7 @@ const updateNumberRanges = async (range) => {
</UDashboardToolbar> </UDashboardToolbar>
<table <table
class="m-3" class="m-3"
> >
<tr class="text-left"> <tr class="text-left">
<th>Typ</th> <th>Typ</th>
@@ -85,19 +80,19 @@ const updateNumberRanges = async (range) => {
<th>Suffix</th> <th>Suffix</th>
</tr> </tr>
<tr <tr
v-for="key in Object.keys(resources)" v-for="key in Object.keys(resources)"
> >
<td>{{resources[key].label}}</td> <td>{{resources[key].label}}</td>
<td> <td>
<UInput <UInput
v-model="numberRanges[key].prefix" v-model="numberRanges[key].prefix"
@change="updateNumberRanges" @change="updateNumberRanges(key)"
/> />
</td> </td>
<td> <td>
<UInput <UInput
v-model="numberRanges[key].nextNumber" v-model="numberRanges[key].nextNumber"
@change="updateNumberRanges" @change="updateNumberRanges(key)"
type="number" type="number"
step="1" step="1"
/> />
@@ -105,7 +100,7 @@ const updateNumberRanges = async (range) => {
<td> <td>
<UInput <UInput
v-model="numberRanges[key].suffix" v-model="numberRanges[key].suffix"
@change="updateNumberRanges" @change="updateNumberRanges(key)"
/> />
</td> </td>
</tr> </tr>
@@ -114,4 +109,4 @@ const updateNumberRanges = async (range) => {
<style scoped> <style scoped>
</style> </style>

View File

@@ -1,78 +0,0 @@
<script setup>
const supabase = useSupabaseClient()
const data = await supabase.from("profiles").select('* , tenants (id, name)')
console.log(data)
let rights = {
createUser: {label: "Benutzer erstellen"},
modifyUser: {label: "Benutzer bearbeiten"},
deactivateUser: {label: "Benutzer sperren"},
createProject: {label: "Projekt erstellen"},
viewOwnProjects: {label: "Eigene Projekte sehen"},
viewAllProjects: {label: "Alle Projekte sehen"},
createTask: {label: "Aufgabe erstellen"},
viewOwnTasks: {label:"Eigene Aufgaben sehen"},
viewAllTasks: {label: "Alle Aufgaben sehen"},
trackOwnTime: {label:"Eigene Zeite erfassen"},
createOwnTime: {label:"Eigene Zeiten erstellen"},
createTime: {label:"Zeiten erstellen"},
viewOwnTimes: {label:"Eigene Zeiten anzeigen"},
viewTimes: {label:"Zeiten anzeigen"},
}
let roles = [
{
key: "tenantAdmin",
label: "Firmenadministrator",
rights: [
...Object.keys(rights)
]
},
{
key:"worker",
label: "Monteur",
rights: [
"viewOwnProjects",
"createTasks",
"viewOwnTasks"
]
},
{
key:"manager",
label: "Vorarbeiter",
rights: [
"createProjects",
"viewOwnProjects",
"createTasks",
"viewOwnTasks",
]
},
{
key:"booker",
label: "Buchhalter",
rights: [
"createTasks",
"viewOwnTasks",
"createTime",
"viewAllTimes"
]
}
]
</script>
<template>
</template>
<style scoped>
</style>

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