Merge branch 'devCorrected' into 'beta'

Dev corrected

See merge request fedeo/software!11
This commit is contained in:
2025-09-29 17:03:50 +00:00
41 changed files with 114 additions and 2996 deletions

View File

@@ -3,7 +3,6 @@
import DocumentDisplayModal from "~/components/DocumentDisplayModal.vue";
const toast = useToast()
const supabase = useSupabaseClient()
const dataStore = useDataStore()
const modal = useModal()
const profileStore = useProfileStore()

View File

@@ -172,13 +172,10 @@ const moveFile = async () => {
</template>
<div class="flex flex-row">
<div :class="useCapacitor().getIsNative() ? ['w-full'] : ['w-1/3']">
<object
class="bigPreview"
:data="`${props.documentData.url}#toolbar=0&navpanes=0&scrollbar=0`"
type="application/pdf"
v-if="props.documentData.path.toLowerCase().includes('pdf')"
<PDFViewer
v-if="props.documentData.id && props.documentData.path.toLowerCase().includes('pdf')"
:file-id="props.documentData.id" />
/>
<img
class=" w-full"
:src="props.documentData.url"

View File

@@ -48,7 +48,6 @@ defineShortcuts({
},
})
const supabase = useSupabaseClient()
const router = useRouter()
const route = useRoute()
const dataStore = useDataStore()
@@ -150,9 +149,9 @@ const loadOptions = async () => {
for await(const option of optionsToLoad) {
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") {
loadedOptions.value[option.option] = (await supabase.from("units").select()).data
loadedOptions.value[option.option] = useEntities("units").selectSpecial()
} else {
loadedOptions.value[option.option] = (await useEntities(option.option).select())

View File

@@ -2,7 +2,6 @@
import dayjs from "dayjs";
import {useSum} from "~/composables/useSum.js";
import {useSupabaseSelect} from "~/composables/useSupabase.js";
defineShortcuts({
/*'/': () => {
//console.log(searchinput)

View File

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

View File

@@ -29,10 +29,8 @@ const setup = async () => {
if(await useCapacitor().getIsPhone()) platform.value = "mobile"
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 {
items.value = (await supabase.from("historyitems").select().order("created_at",{ascending: true})).data || []
}*/
}

View File

@@ -152,11 +152,11 @@ const links = computed(() => {
to: "/standardEntity/absencerequests",
icon: "i-heroicons-document-text"
}] : [],
{
/*{
label: "Fahrten",
to: "/trackingTrips",
icon: "i-heroicons-map"
},
},*/
]
},
... [{

View File

@@ -1,12 +1,14 @@
<script setup>
import { ref, onMounted, watch } from "vue"
import { VPdfViewer } from "@vue-pdf-viewer/viewer"
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,
required: true,
},
uri: {
type: String,
},
scale: {
type: Number,
@@ -14,6 +16,9 @@ const props = defineProps({
},
})
const config = useRuntimeConfig()
useLicense(config.public.pdfViewerLicense || "eyJkYXRhIjoiZXlKMElqb2laR1YyWld4dmNHVnlJaXdpWVhaMUlqb3hOemt3TmpNNU9UazVMQ0prYlNJNklteHZZMkZzYUc5emREb3pNREF3SWl3aWJpSTZJbVkxT1RRM1ptWTBOVFJsTkdKaU16a2lMQ0psZUhBaU9qRTNPVEEyTXprNU9Ua3NJbVJ0ZENJNkluTndaV05wWm1saklpd2ljQ0k2SW5acFpYZGxjaUo5Iiwic2lnbmF0dXJlIjoibGVpVmNkbENzQStiNCt5ODBNVnZzY2lHcEpjZ3F4T2M1R2V3VFJJVzY5SkQwSGxteERRcmtmSzQwOWhzYXdJN2pSVXM3QmdvNzJIU3ZYRmZtS1Rhejl1N0o0eEt0Nk1VL2E5cjFMblA1aUZzZFBtemRMbFJFSDJoSG00Zmk2Y25UbEhnQTdHekkrL2JVVlJhUEJXYWFDdlFSK3ByRHg2NHl4aFhjOElPT2swb1pldWtKc2tCTjNCS0NiZ3VkSGhmbXB5TE9WUDc4SUR4U1FNR3BOU2I4eWRqakdQSHBuNjZLQWVOWGpRc05EbHNIdmJkaFFyNk1ZaldzV3JkRm50ditPR2FyT2MwQzJLNVJmZkRKWTBldVpXOFNiRTRLNlJkWTVSY084eVpBNzcrQWVlV0hOdHg0SllhV1hFMW5sYmZLZDVvM3V6b0dLTFYzODBUMmlvSERnPT0ifQ==")
const pdfSrc = ref(null) // ObjectURL fürs Viewer
const { $api } = useNuxtApp()
@@ -30,7 +35,16 @@ async function loadPdf(id) {
}
}
onMounted(() => loadPdf(props.fileId))
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)
})
@@ -38,6 +52,7 @@ watch(() => props.fileId, (newPath) => {
const vpvRef = ref(null);
//Zoom Control
const zoomControl = computed(() => vpvRef.value?.zoomControl)
const currentScale = computed(() => {
@@ -45,6 +60,7 @@ const currentScale = computed(() => {
})
const handleZoomTool = (type) => {
console.log(type)
const zoomCtrl = unref(zoomControl)
if (!zoomCtrl) return
@@ -57,7 +73,6 @@ const handleZoomTool = (type) => {
zoomCtrl.zoom(type)
}
}
//Page Control
const pageControl = computed(() => vpvRef.value?.pageControl)
const currentPageInput = computed(() => pageControl.value?.currentPage)
@@ -93,10 +108,12 @@ const handleKeyPress = (event) => {
//Handle Download
const downloadControl = computed(() => vpvRef.value?.downloadControl)
const handleDownloadFile = () => {
const downloadCtrl = unref(downloadControl)
const handleDownloadFile = async () => {
await useFiles().downloadFile(props.fileId)
/*const downloadCtrl = unref(downloadControl)
if (!downloadCtrl) return
downloadCtrl.download()
downloadCtrl.download()*/
}
watch(downloadControl, (downloadCtrl) => {
@@ -136,6 +153,11 @@ watch(downloadControl, (downloadCtrl) => {
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"
@@ -143,11 +165,7 @@ watch(downloadControl, (downloadCtrl) => {
variant="outline"
></UButton>
<UButton
@click="handleDownloadFile"
variant="outline"
icon="i-heroicons-arrow-down-on-square"
/>
<!-- Page number input and total pages display -->
<div class="flex items-center text-sm font-normal">
@@ -179,7 +197,11 @@ watch(downloadControl, (downloadCtrl) => {
:toolbar-options="false"
ref="vpvRef"
/>
<div v-else>Lade PDF</div>
<div v-else>
<UProgress
class="mt-5 w-2/3 mx-auto"
animation="carousel"></UProgress>
</div>
</div></template>
<style scoped>

View File

@@ -1,7 +1,6 @@
<script setup>
import dayjs from "dayjs";
import {useSupabaseSelect} from "~/composables/useSupabase.js";
let unallocatedStatements = ref(0)
let bankaccounts = ref([])

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
}

View File

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

View File

@@ -57,5 +57,13 @@ export default defineNuxtConfig({
prefix: "Tiptap"
},
runtimeConfig: {
public: {
apiBase: '',
pdfviewerLicense: ''
}
},
compatibilityDate: '2024-12-18'
})

View File

@@ -1,7 +1,6 @@
<script setup>
import dayjs from "dayjs";
import {useSupabaseSelect} from "~/composables/useSupabase.js";
const route = useRoute()
const router = useRouter()

View File

@@ -1,136 +0,0 @@
<script setup>
import dayjs from "dayjs";
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

@@ -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,123 +0,0 @@
<script setup>
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,120 +0,0 @@
<script setup>
import { format, isToday } from 'date-fns'
import dayjs from "dayjs"
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

@@ -134,7 +134,6 @@ const setupPage = async () => {
if (route.query.loadMode === "deliveryNotes") {
let linkedDocuments = (await useEntities("createddocuments").select()).filter(i => JSON.parse(route.query.linkedDocuments).includes(i.id))
//let linkedDocuments = (await supabase.from("createddocuments").select().in("id", JSON.parse(route.query.linkedDocuments))).data
//TODO: Implement Checking for Same Customer, Contact and Project
@@ -203,7 +202,6 @@ const setupPage = async () => {
} else if (route.query.loadMode === "finalInvoice") {
let linkedDocuments = (await useEntities("createddocuments").select()).filter(i => JSON.parse(route.query.linkedDocuments).includes(i.id))
//let linkedDocuments = (await supabase.from("createddocuments").select().in("id", JSON.parse(route.query.linkedDocuments))).data
//TODO: Implement Checking for Same Customer, Contact and Project
@@ -3030,12 +3028,17 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
>
Show
</UButton>-->
<object
<PDFViewer
v-if="showDocument"
:uri="uri"
/>
<!-- <object
:data="uri"
v-if="showDocument"
type="application/pdf"
class="w-full previewDocumentMobile"
/>
/>-->
</div>
</template>

View File

@@ -1,6 +1,5 @@
<script setup>
//TODO: BACKENDCHANGE EMAIL SENDING
const supabase = useSupabaseClient()
const dataStore = useDataStore()
const profileStore = useProfileStore()
const route = useRoute()
@@ -24,7 +23,9 @@ const loadedDocuments = ref([])
const loaded = ref(false)
const noAccountsPresent = ref(false)
const setupPage = async () => {
emailAccounts.value = await useEntities("emailAccounts").select()
//emailAccounts.value = await useEntities("emailAccounts").select()
emailAccounts.value = await useNuxtApp().$api("/api/email/accounts")
if(emailAccounts.value.length === 0) {
noAccountsPresent.value = true
@@ -130,7 +131,6 @@ const sendEmail = async () => {
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)
@@ -143,7 +143,9 @@ const sendEmail = async () => {
})
}
const res = await useNuxtApp().$api("/api/emailasuser/send",{
console.log(body)
const res = await useNuxtApp().$api("/api/email/send",{
method: "POST",
body: body,
})
@@ -151,9 +153,6 @@ const sendEmail = async () => {
console.log(res)
/*const { data, error } = await supabase.functions.invoke('send_email', {
body
})*/
if(!res.success) {
toast.add({title: "Fehler beim Absenden der E-Mail", color: "rose"})
@@ -208,7 +207,7 @@ const sendEmail = async () => {
>
<USelectMenu
:options="emailAccounts"
option-attribute="emailAddress"
option-attribute="email"
value-attribute="id"
v-model="emailData.account"
/>

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,674 +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";
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,7 +2,6 @@
import InputGroup from "~/components/InputGroup.vue";
import dayjs from "dayjs";
import HistoryDisplay from "~/components/HistoryDisplay.vue";
import {useSupabaseSelect} from "~/composables/useSupabase.js";

View File

@@ -51,7 +51,6 @@ const type = "incominginvoices"
const dataType = dataStore.dataTypes[type]
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")
}

View File

@@ -5,7 +5,6 @@ import dayjs from "dayjs";
const dataStore = useDataStore()
const profileStore = useProfileStore()
const supabase = useSupabaseClient()
const route = useRoute()
const router = useRouter()
@@ -43,7 +42,6 @@ const loading = ref(true)
const setupPage = async () => {
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)
currentDocument.value = await useFiles().selectDocument(itemInfo.value.files[0].id)

View File

@@ -66,23 +66,8 @@ definePageMeta({
middleware: 'redirect-to-mobile-index'
})
import DisplayPresentProfiles from "~/components/noAutoLoad/displayPresentProfiles.vue";
const dataStore = useDataStore()
const profileStore = useProfileStore()
const toast = useToast()
const router = useRouter()
const { isNotificationsSlideoverOpen } = useDashboard()
const user = useSupabaseUser()
const setup = async () => {
}

View File

@@ -1,426 +0,0 @@
<script setup>
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,150 +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>
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,8 +1,5 @@
<script setup>
import HistoryDisplay from "~/components/HistoryDisplay.vue";
import DocumentList from "~/components/DocumentList.vue";
import DocumentUpload from "~/components/DocumentUpload.vue";
import {useSupabaseSelect} from "~/composables/useSupabase.js";

View File

@@ -30,13 +30,7 @@ const checkBIC = async () => {
const generateLink = async (bankId) => {
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)
await navigateTo(link, {

View File

@@ -2,7 +2,6 @@
const dataStore = useDataStore()
const profileStore = useProfileStore()
const supabase = useSupabaseClient()
const router = useRouter()
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,111 +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>
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,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>

View File

@@ -6,12 +6,13 @@ import dayjs from "dayjs";
const supabase = useSupabaseClient()
const profileStore = useProfileStore()
const toast = useToast()
const auth = useAuthStore()
const itemInfo = ref({})
const loaded = ref(false)
const setup = async () => {
itemInfo.value = (await supabase.from("tickets").select("*, ticketmessages(*, profile(fullName, tenant, id)), created_by(*)").eq("id",useRoute().params.id).single()).data
itemInfo.value = await useEntities("tickets").selectSingle(useRoute().params.id,"*, ticketmessages(*), created_by(*)")
loaded.value = true
}
@@ -20,86 +21,17 @@ setup()
const messageContent = ref("")
const addMessage = async () => {
const {data,error} = await supabase.from("ticketmessages").insert({
profile: profileStore.activeProfile.id,
const res = await useEntities("ticketmessages").create({
auth_user: auth.user.user_id,
content: messageContent.value,
ticket: itemInfo.value.id,
internal: false,
type: "Nachricht"
}).select().single()
if(error) {
toast.add({title: "Erstellen fehlgeschlagen", color: "rose"})
} else {
toast.add({title: "Erstellen erfolgreich"})
messageContent.value=""
setup()
if(profileStore.currentTenant !== 5) {
await useFunctions().useSendTelegramNotification(`Neue Nachricht im Ticket ${useRoute().params.id} von ${profileStore.activeProfile.fullName}: ${data.content}`)
} else if(profileStore.activeProfile.id !== itemInfo.value.created_by) {
let notification = {
tenant: itemInfo.value.tenant,
profile: itemInfo.value.created_by.id,
initiatingProfile: profileStore.activeProfile.id,
title: `Sie haben eine neue Nachricht von ${profileStore.activeProfile.fullName} im Ticket ${itemInfo.value.title}`,
link: `/support/${itemInfo.value.id}`,
message: data.content
})
}
console.log(notification)
const {error} = await supabase.from("notifications").insert(notification)
}
}
}
const showAddEntryModal = ref(false)
const addEntryData = ref({})
const addEntry = async () => {
const {data,error} = await supabase.from("ticketmessages").insert({
profile: profileStore.activeProfile.id,
content: addEntryData.value.content,
ticket: itemInfo.value.id,
internal: addEntryData.value.internal,
type: addEntryData.value.type
}).select().single()
if(error) {
toast.add({title: "Erstellen fehlgeschlagen", color: "rose"})
} else {
toast.add({title: "Erstellen erfolgreich"})
addEntryData.value = {}
setup()
showAddEntryModal.value = false
}
}
const closeTicket = async () => {
const {data, error} = await supabase.from("tickets").update({status: "Geschlossen"}).eq("id",useRoute().params.id).single()
if(error) {
console.log(error)
} else {
console.log(data)
addEntryData.value.type = "Notiz"
addEntryData.value.internal = false
addEntryData.value.content = `Ticket durch ${profileStore.activeProfile.fullName} geschlossen`
addEntry()
}
setup()
}
</script>
<template>
@@ -120,7 +52,7 @@ const closeTicket = async () => {
{{itemInfo.title}}
</template>
<template #right>
<UButton
<!-- <UButton
v-if="profileStore.currentTenant === 5"
variant="outline"
@click="closeTicket"
@@ -171,45 +103,14 @@ const closeTicket = async () => {
</UButton>
</template>
</UCard>
</UModal>
</UModal>-->
</template>
</UDashboardNavbar>
<UDashboardPanelContent v-if="loaded">
<div
v-if="profileStore.currentTenant === 5"
v-for="item in itemInfo.ticketmessages"
class="mb-3 flex flex-row p-5"
:style="item.internal ? 'border: 1px solid red; border-radius: 15px' : item.profile.tenant === 5 ? 'border: 1px solid #69c350; border-radius: 15px' : 'border: 1px solid #fff; border-radius: 15px'"
>
<UAvatar :alt="item.profile.fullName" class="mr-3"/>
<div>
<p class="text-xl">{{item.type}} - {{item.profile.fullName}}</p>
<p v-html="item.content"></p>
<p class="mt-1 text-gray-600 dark:text-gray-400">{{dayjs(item.created_at).format("DD.MM.YYYY HH:mm")}}</p>
</div>
</div>
<!-- <UAlert
v-if="profileStore.currentTenant === 5"
v-for="item in itemInfo.ticketmessages"
:avatar="{ alt: item.profile.fullName}"
:title="`${item.type} - ${item.profile.fullName}`"
class="mb-3"
:color="item.internal ? 'rose' : item.profile.tenant === 5 ? 'primary' : 'white'"
variant="outline"
>
<template #description>
<span v-html="item.content"></span>
<p class="mt-1 text-gray-600 dark:text-gray-400">{{dayjs(item.created_at).format("DD.MM.YYYY HH:mm")}}</p>
</template>
</UAlert>-->
<UAlert
v-else
v-for="item in itemInfo.ticketmessages.filter(i => !i.internal)"
:avatar="{ alt: item.profile.fullName}"
:title="`${item.type} - ${item.profile.fullName}`"
:title="`${item.type}`"
class="mb-3"
:color="item.profile.tenant === 5 ? 'primary' : 'white'"
variant="outline"

View File

@@ -1,42 +1,30 @@
<script setup>
import {useFunctions} from "~/composables/useFunctions.js";
const supabase = useSupabaseClient()
const profileStore = useProfileStore()
const router = useRouter()
const itemInfo = ref({})
const auth = useAuthStore()
const createTicket = async () => {
const {data:ticketData,error:ticketError} = await supabase.from("tickets").insert({
const ticketRes = await useEntities("tickets").create({
title: itemInfo.value.title,
created_by: profileStore.activeProfile.id,
tenant: profileStore.currentTenant
}).select().single()
})
if(ticketError) {
console.error(ticketError)
} else {
console.log(ticketData)
const {data:messageData,error:messageError} = await supabase.from("ticketmessages").insert({
ticket: ticketData.id,
profile: profileStore.activeProfile.id,
console.log(ticketRes)
const ticketMsgRes = await useEntities("ticketmessages").create({
ticket: ticketRes.id,
created_by: auth.user.user_id,
content: itemInfo.value.content,
internal: false
})
if(messageError) {
console.log(messageError)
} else {
console.log(ticketData)
//useFunctions().useSendTelegramNotification(`Ticket von ${profileStore.activeProfile.fullName} erstellt : ${itemInfo.value.content}`)
router.push(`/support/${ticketData.id}`)
}
}
await router.push(`/support/${ticketRes.id}`)
}
</script>

View File

@@ -1,6 +1,5 @@
<script setup>
import dayjs from "dayjs";
const supabase = useSupabaseClient()
const profileStore = useProfileStore()
const router = useRouter()
@@ -11,7 +10,7 @@ const showClosedTickets = ref(false)
const selectedTenant = ref(null)
const setup = async () => {
if(profileStore.currentTenant === 5) {
/*if(profileStore.currentTenant === 5) {
tickets.value = (await supabase.from("tickets").select("*,created_by(*), ticketmessages(*), tenant(*)").order("created_at", {ascending: false})).data
} else {
tickets.value = (await supabase.from("tickets").select("*,created_by(*), ticketmessages(*)").eq("tenant",profileStore.currentTenant).order("created_at", {ascending: false})).data
@@ -19,7 +18,8 @@ const setup = async () => {
if(profileStore.currentTenant === 5) {
tenants.value = (await supabase.from("tenants").select().order("id")).data
}
}*/
tickets.value = await useEntities("tickets").select("*,created_by(*), ticketmessages(*)", "created_at", false)

View File

@@ -1,283 +0,0 @@
<script setup>
import dayjs from "dayjs";
import {useSupabaseSelectSingle} from "~/composables/useSupabase.js";
defineShortcuts({
'backspace': () => {
router.push("/trackingTrips")
},
'arrowleft': () => {
if(openTab.value > 0){
openTab.value -= 1
}
},
'arrowright': () => {
if(openTab.value < 3) {
openTab.value += 1
}
},
})
const dataStore = useDataStore()
const route = useRoute()
const router = useRouter()
const toast = useToast()
const id = ref(route.params.id ? route.params.id : null )
const openTab = ref(0)
//Working
const mode = ref(route.params.mode || "show")
const itemInfo = ref({
name: "",
type: "Geschäftlich",
driver: null,
active: true
})
const oldItemInfo = ref({})
const profiles = ref([])
const projects = ref([])
//Functions
const setupPage = async () => {
if(mode.value === "show" ){
itemInfo.value = await useSupabaseSelectSingle("trackingtrips",route.params.id,"*, trackingDevice(*, vehicle(*))")
} else if(mode.value === "edit") {
itemInfo.value = await useSupabaseSelectSingle("trackingtrips",route.params.id,"*")
}
if(itemInfo.value) oldItemInfo.value = JSON.parse(JSON.stringify(itemInfo.value))
profiles.value = await useSupabaseSelect("profiles","*")
projects.value = await useSupabaseSelect("projects","*")
}
const cancelEditorCreate = () => {
if(itemInfo.value) {
router.push(`/trackingTrips/show/${itemInfo.value.id}`)
} else {
router.push(`/trackingTrips`)
}
}
const getRowAmount = (row) => {
let amount = 0
row.accounts.forEach(account => {
amount += account.amountNet
amount += account.amountTax
})
return amount
}
setupPage()
const zoom = ref(6)
</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(`/trackingTrips`)"
>
Fahrten
</UButton>
</template>
<template #center>
<h1
v-if="itemInfo"
:class="['text-xl','font-medium' ]"
><UIcon name="i-heroicons-lock-closed" v-if="itemInfo.fixed"/><UIcon name="i-heroicons-lock-open" v-else/> {{itemInfo ? `Fahrt vom: ${dayjs(itemInfo.startTime).format("DD.MM.YY HH:mm")} über ${(itemInfo.distance/1000).toFixed(2)} km` : '' }} </h1>
</template>
<template #right>
<UButton
@click="cancelEditorCreate"
color="red"
variant="outline"
v-if="mode === 'edit' || mode === 'create'"
>
Abbrechen
</UButton>
<UButton
v-if="mode === 'edit'"
variant="outline"
color="primary"
icon="i-mdi-content-save"
@click="dataStore.updateItem('trackingtrips',itemInfo, oldItemInfo)"
>
Entwurf
</UButton>
<UButton
v-if="mode === 'edit'"
icon="i-mdi-content-save"
@click="dataStore.updateItem('trackingtrips',{...itemInfo, fixed:true}, oldItemInfo)"
>
Festschreiben
</UButton>
<UButton
v-if="mode === 'show' && !itemInfo.fixed"
@click="router.push(`/trackingTrips/edit/${itemInfo.id}`)"
>
Bearbeiten
</UButton>
</template>
</UDashboardNavbar>
<UTabs
:items="[{label: 'Informationen'}]"
v-if="mode === 'show' && itemInfo.id"
class="p-5"
v-model="openTab"
>
<template #item="{item}">
<div v-if="item.label === 'Informationen'" class="flex flex-col mt-5">
<div class="flex flex-row" style="height: 40vh">
<div class="w-1/2 mr-5">
<UCard>
<table class="w-full">
<tr>
<td>Festgeschrieben:</td>
<td>{{itemInfo.fixed ? "Ja" : "Nein"}}</td>
</tr>
<tr>
<td>Fahrzeug:</td>
<td><nuxt-link :to="`/vehicles/show/${itemInfo.trackingDevice.vehicle.id}`">{{itemInfo.trackingDevice.vehicle.licensePlate}}</nuxt-link></td>
</tr>
<tr>
<td>Typ:</td>
<td>{{itemInfo.type ? itemInfo.type : "-"}}</td>
</tr>
<tr>
<td>Fahrer:</td>
<td>{{profiles.find(i => i.id === itemInfo.driver) ? profiles.find(i => i.id === itemInfo.driver).fullName : "-"}}</td>
</tr>
<tr>
<td>Startzeit:</td>
<td>{{dayjs(itemInfo.startTime).format("DD.MM.YY HH:mm")}}</td>
</tr>
<tr>
<td>Endzeit:</td>
<td>{{dayjs(itemInfo.endTime).format("DD.MM.YY HH:mm")}}</td>
</tr>
<tr>
<td>Entfernung:</td>
<td>{{(itemInfo.distance/1000).toFixed(2)}} km</td>
</tr>
<tr>
<td>Beschreibung:</td>
<td>{{itemInfo.description ? itemInfo.description : "-" }}</td>
</tr>
</table>
</UCard>
<UCard class="mt-5">
<Map :markers="[[itemInfo.startLatitude, itemInfo.startLongitude],[itemInfo.endLatitude, itemInfo.endLongitude]]"
:startMarker="[itemInfo.startLatitude, itemInfo.startLongitude]"
:endMarker="[itemInfo.endLatitude, itemInfo.endLongitude]"
/>
</UCard>
</div>
<div class="w-1/2">
<UCard>
<div style="height: 75vh">
<HistoryDisplay
type="trackingtrip"
v-if="itemInfo"
:element-id="itemInfo.id"
render-headline
/>
</div>
</UCard>
</div>
</div>
<div class="h-40 mt-5">
</div>
</div>
</template>
</UTabs>
<UForm
v-else-if="mode === 'edit' || mode === 'create'"
class="p-5"
>
<UFormGroup
label="Typ:"
>
<USelectMenu
v-model="itemInfo.type"
:options="['Privat','Geschäftlich','Arbeitsweg']"
/>
</UFormGroup>
<UFormGroup
label="Fahrer:"
>
<USelectMenu
v-model="itemInfo.driver"
:options="[{id: null, fullName: 'Kein Fahrer'},...profiles]"
option-attribute="fullName"
value-attribute="id"
>
<template #label>
{{profiles.find(profile => profile.id === itemInfo.driver) ? profiles.find(profile => profile.id === itemInfo.driver).fullName : 'Kein Fahrer ausgewählt'}}
</template>
</USelectMenu>
</UFormGroup>
<UFormGroup
label="Projekt:"
>
<USelectMenu
v-model="itemInfo.project"
:options="projects"
option-attribute="name"
value-attribute="id"
/>
</UFormGroup>
<UFormGroup
label="Beschreibung:"
>
<UTextarea
v-model="itemInfo.description"
rows="6"
maxrows="6"
/>
</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,155 +0,0 @@
<template>
<UDashboardNavbar title="Fahrten" :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(`/vehicles/create`)">+ Fahrzeug</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(`/trackingTrips/show/${i.id}`) "
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Fahrten anzuzeigen' }"
>
<template #fixed-data="{row}">
<UIcon name="i-heroicons-lock-closed" v-if="row.fixed"/>
<div v-else></div>
</template>
<template #licensePlate-data="{row}">
<span v-if="row === filteredRows[selectedItem]" class="font-bold text-primary-500">{{row.vehicle.licensePlate}}</span>
<span v-else>{{row.vehicle.licensePlate}}</span>
</template>
<template #startTime-data="{row}">
{{dayjs(row.startTime).format("DD.MM.YY HH:mm")}}
</template>
<template #endTime-data="{row}">
{{dayjs(row.endTime).format("DD.MM.YY HH:mm")}}
</template>
<template #distance-data="{row}">
{{row.distance > 0 ? (row.distance/1000).toFixed(2) : 0 }} km
</template>
</UTable>
</template>
<script setup>
import dayjs from "dayjs";
defineShortcuts({
'/': () => {
//console.log(searchinput)
//searchinput.value.focus()
document.getElementById("searchinput").focus()
},
'Enter': {
usingInput: true,
handler: () => {
router.push(`/trackingTrips/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("trackingtrips","*, trackingDevice(*), vehicle (*)","startTime",false)
}
setupPage()
const templateColumns = [
{
key: 'fixed',
label: ""
},
{
key: 'licensePlate',
label: "Kennzeichen:",
sortable: true
},
{
key: "type",
label: "Typ:",
sortable: true
},
{
key: "startTime",
label: "Startzeit:",
sortable: true
},
{
key: "endTime",
label: "Endzeit:",
sortable: true
},
{
key: "distance",
label: "Entfernung:",
sortable: true
}
]
const selectedColumns = ref(templateColumns)
const columns = computed(() => templateColumns.filter((column) => selectedColumns.value.includes(column)))
const searchString = ref('')
const filteredRows = computed(() => {
return useSearch(searchString.value, items.value)
})
</script>
<style scoped>
</style>

View File

@@ -1,8 +1,10 @@
import {Preferences} from "@capacitor/preferences";
export default defineNuxtPlugin(() => {
const config = useRuntimeConfig()
const api = $fetch.create({
baseURL: /*"http://192.168.1.227:3100"*/ "https://backend.fedeo.io",
baseURL: config.public.apiBase,/*"http://192.168.1.227:3100" "https://backend.fedeo.io"*/
credentials: "include",
async onRequest({options}) {
// Token aus Cookie holen

View File

@@ -3,7 +3,6 @@ import dayjs from "dayjs"
//import {typeOf} from "uri-js/dist/esnext/util";
import {useNumberRange} from "~/composables/useNumberRange.js";
//const supabase = createClient('https://uwppvcxflrcsibuzsbil.supabase.co','eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InV3cHB2Y3hmbHJjc2lidXpzYmlsIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MDA5MzgxOTQsImV4cCI6MjAxNjUxNDE5NH0.CkxYSQH0uLfwx9GVUlO6AYMU2FMLAxGMrwEKvyPv7Oo')
import projecttype from "~/components/columnRenderings/projecttype.vue"
import customer from "~/components/columnRenderings/customer.vue"
@@ -49,7 +48,6 @@ import sepaDate from "~/components/columnRenderings/sepaDate.vue";
// @ts-ignore
export const useDataStore = defineStore('data', () => {
const supabase = useSupabaseClient()
const profileStore = useProfileStore()
const toast = useToast()
const router = useRouter()
@@ -1566,6 +1564,18 @@ export const useDataStore = defineStore('data', () => {
}
]
},
tickets: {
isArchivable: true,
label: "Tickets",
labelSingle: "Ticket",
},
ticketmessages: {
isArchivable: true,
label: "Nachrichten",
labelSingle: "Nachricht",
},
files: {
isArchivable: true,
label: "Dateien",
@@ -2593,136 +2603,7 @@ export const useDataStore = defineStore('data', () => {
})
async function createNewItem (dataType,data,noRedirect=false){
if(typeof(data) === 'object') {
data = {...data, tenant: profileStore.currentTenant}
} else if(typeof(data) === 'array') {
data.map(i => {
return {
...i,
tenant: profileStore.currentTenant
}
})
}
console.log(dataType)
if(dataTypes[dataType].numberRangeHolder) {
if(!data[dataTypes[dataType].numberRangeHolder]) {
data[dataTypes[dataType].numberRangeHolder] = await useFunctions().useNextNumber(dataType)
}
} else if(dataType === "createddocuments" && data.type !== "serialInvoices") {
/*if(data.state !== "Entwurf") {
console.log(data.type)
let type = ""
if(data.type === "advanceInvoices"){
type = "invoices"
} else {
type = data.type
}
const numberRange = useNumberRange(type)
data.documentNumber = await numberRange.useNextNumber()
}*/
}
const {data:supabaseData,error:supabaseError} = await supabase
.from(dataType)
.insert(data)
.select()
if(supabaseError) {
console.log(supabaseError)
toast.add({title: "Es ist ein Fehler bei der Erstellung aufgetreten", color: "rose"})
} else if (supabaseData) {
console.log(supabaseData)
let returnData = supabaseData[0]
await generateHistoryItems(dataType, supabaseData[0])
/*if(!["statementallocations","absencerequests", "productcategories", "servicecategories", "projecttypes", "checks", "profiles","services", "inventoryitems", "inventoryitemgroups", "incominginvoices", "costcentres", "ownaccounts"].includes(dataType) ){
await eval( dataType + '.value.push(' + JSON.stringify(...supabaseData) + ')')
}*/
toast.add({title: `${dataTypes[dataType].labelSingle} hinzugefügt`})
if(dataTypes[dataType].redirect && !noRedirect) {
if(dataTypes[dataType].isStandardEntity) {
await router.push(dataTypes[dataType].redirectToList ? `/standardEntity/${dataType}` : `/standardEntity/${dataType}/show/${returnData.id}`)
} else {
await router.push(dataTypes[dataType].redirectToList ? `/${dataType}` : `/${dataType}/show/${returnData.id}`)
}
}
modal.close()
return supabaseData[0]
}
}
async function updateItem (dataType, data, oldData = null, noRedirect = false) {
//console.log(dataType, data)
//Temporary Fix TODO: Remove and build Solution
data = JSON.parse(JSON.stringify(data))
delete data.users
if(oldData) {
oldData = JSON.parse(JSON.stringify(oldData))
delete oldData.users
}
const {tenants, ...newData} = data
/*if(dataType === "createddocuments" && data.type !== "serialInvoices") {
if(data.state !== "Entwurf") {
console.log(data.type)
let type = ""
if(data.type === "advanceInvoices"){
type = "invoices"
} else {
type = data.type
}
const numberRange = useNumberRange(type)
data.documentNumber = await numberRange.useNextNumber()
}
}*/
await generateHistoryItems(dataType,data,oldData)
const {data:supabaseData,error: supabaseError} = await supabase
.from(dataType)
.update(newData)
.eq('id',newData.id)
.select()
if(supabaseError) {
console.log(supabaseError)
toast.add({title: `Fehler beim Speichern`, color: 'rose'})
} else if(supabaseData) {
//await eval(dataType + '.value[' + dataType + '.value.findIndex(i => i.id === ' + JSON.stringify(data.id) + ')] = ' + JSON.stringify(supabaseData[0]))
//if(dataType === 'profiles') await fetchProfiles()
toast.add({title: `${dataTypes[dataType].labelSingle} gespeichert`})
if(dataTypes[dataType].redirect && !noRedirect) {
if(dataTypes[dataType].isStandardEntity) {
await router.push(dataTypes[dataType].redirectToList ? `/standardEntity/${dataType}` : `/standardEntity/${dataType}/show/${data.id}`)
} else {
await router.push(dataTypes[dataType].redirectToList ? `/${dataType}` : `/${dataType}/show/${data.id}`)
}
}
modal.close()
return supabaseData[0]
}
}
@@ -2732,7 +2613,5 @@ export const useDataStore = defineStore('data', () => {
return {
dataTypes,
documentTypesForCreation,
createNewItem,
updateItem,
}
})