Merge branch 'devCorrected' into 'beta'

fixed archived filtering

See merge request fedeo/software!9
This commit is contained in:
2025-09-26 16:22:38 +00:00
34 changed files with 807 additions and 363 deletions

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

@@ -171,7 +171,7 @@ 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 <object
class="bigPreview" class="bigPreview"
:data="`${props.documentData.url}#toolbar=0&navpanes=0&scrollbar=0`" :data="`${props.documentData.url}#toolbar=0&navpanes=0&scrollbar=0`"
@@ -186,21 +186,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

@@ -254,20 +254,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,7 @@
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 { has } = usePermission()
@@ -145,6 +145,7 @@ const filteredRows = computed(() => {
</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"/>
@@ -181,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"

View File

@@ -220,7 +220,6 @@ const changePinned = async () => {
</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'"
@@ -326,7 +325,7 @@ const changePinned = async () => {
@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'"
@@ -355,7 +354,7 @@ const changePinned = async () => {
:top-level-type="type" :top-level-type="type"
v-else v-else
:platform="platform" :platform="platform"
/> />-->
</div> </div>
</UDashboardPanelContent> </UDashboardPanelContent>

190
components/PDFViewer.vue Normal file
View File

@@ -0,0 +1,190 @@
<script setup>
import { ref, onMounted, watch } from "vue"
import { VPdfViewer } 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,
},
scale: {
type: Number,
default: 1.2,
},
})
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(() => loadPdf(props.fileId))
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) => {
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 = () => {
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="prevPage"
:disabled="isPreviousPageButtonDisable"
icon="i-heroicons-chevron-up"
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">
<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>Lade PDF</div>
</div></template>
<style scoped>
.pdf-container {
width: 100%;
height: 100%;
}
</style>

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}
} }

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

@@ -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

@@ -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

@@ -15,7 +15,6 @@ const route = useRoute()
const auth = useAuthStore() const auth = useAuthStore()
//profileStore.initializeData((await supabase.auth.getUser()).data.user.id)
const month = dayjs().format("MM") const month = dayjs().format("MM")

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>

View File

@@ -0,0 +1,9 @@
export default defineNuxtRouteMiddleware(async (to, _from) => {
const router = useRouter()
console.log(await useCapacitor().getIsPhone())
if(await useCapacitor().getIsPhone() && _from.path !== '/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}

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

@@ -16,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({})
@@ -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>

View File

@@ -9,7 +9,6 @@ const dataStore = useDataStore()
const profileStore = useProfileStore() const profileStore = useProfileStore()
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const supabase = useSupabaseClient()
const modal = useModal() const modal = useModal()
const auth = useAuthStore() const auth = useAuthStore()
@@ -134,7 +133,8 @@ const setupPage = async () => {
console.log(route.query.loadMode) console.log(route.query.loadMode)
if (route.query.loadMode === "deliveryNotes") { if (route.query.loadMode === "deliveryNotes") {
let linkedDocuments = (await supabase.from("createddocuments").select().in("id", JSON.parse(route.query.linkedDocuments))).data 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 //TODO: Implement Checking for Same Customer, Contact and Project
@@ -1523,21 +1523,14 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
</UButton> </UButton>
</template> </template>
<template #right> <template #right>
<ButtonWithConfirm <ArchiveButton
v-if="itemInfo.state === 'Entwurf' || itemInfo.type === 'serialInvoices'"
color="rose" color="rose"
type="createddocuments"
v-if="itemInfo.state === 'Entwurf' || itemInfo.type === 'serialInvoices'"
variant="outline" variant="outline"
@confirmed="useEntities('createddocuments').update(itemInfo.id,{archived: true}), @confirmed="useEntities('createddocuments').update(itemInfo.id,{archived: true}),
router.push('/')" router.push('/')"
> />
<template #button>
Archivieren
</template>
<template #header>
<span class="text-md dark:text-whitetext-black font-bold">Archivieren bestätigen</span>
</template>
Möchten Sie diesen Ausgangsbeleg wirklich archivieren?
</ButtonWithConfirm>
<UButton <UButton
icon="i-mdi-content-save" icon="i-mdi-content-save"
@click="saveDocument('Entwurf',true)" @click="saveDocument('Entwurf',true)"

View File

@@ -47,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' }"
> >
@@ -61,6 +62,15 @@
</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
@@ -182,12 +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 useEntities("createddocuments").select("*, customer(id,name), statementallocations(id,amount),linkedDocument(*)","documentNumber",true)).filter(i => !i.archived) items.value = (await useEntities("createddocuments").select("*, customer(id,name), statementallocations(id,amount),linkedDocument(*)","documentNumber",true, true))
} }
setupPage() setupPage()
@@ -279,24 +292,34 @@ 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

@@ -13,12 +13,13 @@ const modal = useModal()
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const auth = useAuthStore() const auth = useAuthStore()
const dataStore = useDataStore()
const itemInfo = ref({}) const itemInfo = ref({})
const linkedDocument =ref({}) const linkedDocument =ref({})
const setupPage = async () => { const setupPage = async () => {
if(route.params) { if(route.params) {
if(route.params.id) itemInfo.value = await useEntities("createddocuments").selectSingle(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)
@@ -95,14 +96,27 @@ const openEmail = () => {
> >
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

@@ -170,19 +170,12 @@ const findIncomingInvoiceErrors = computed(() => {
<template> <template>
<UDashboardNavbar :title="'Eingangsbeleg erstellen'"> <UDashboardNavbar :title="'Eingangsbeleg erstellen'">
<template #right> <template #right>
<ButtonWithConfirm <ArchiveButton
color="rose" color="rose"
variant="outline" variant="outline"
type="incominginvoices"
@confirmed="useEntities('incominginvoices').archive(route.params.id)" @confirmed="useEntities('incominginvoices').archive(route.params.id)"
> />
<template #button>
Archivieren
</template>
<template #header>
<span class="text-md text-black font-bold">Archivieren bestätigen</span>
</template>
Möchten Sie den Eingangsbeleg wirklich archivieren?
</ButtonWithConfirm>
<UButton <UButton
@click="updateIncomingInvoice(false)" @click="updateIncomingInvoice(false)"
> >

View File

@@ -47,69 +47,49 @@ const sort = ref({
direction: 'desc' 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 useSupabaseSelect("incominginvoices","*, vendor(id,name), statementallocations(id,amount)","created_at",false)
items.value = await useEntities("incominginvoices").select("*, vendor(id,name), statementallocations(id,amount)",sort.value.column,sort.value.direction === "asc") 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:",
sortable: true,
}, {
key: 'state',
label: "Status:"
},
{
key: "date",
label: "Datum",
sortable: true,
},
{
key: "vendor",
label: "Lieferant",
},
{
key: "amount",
label: "Betrag",
},
{
key: "dueDate",
label: "Fälligkeitsdatum",
sortable: true,
},
{
key: "paid",
label: "Bezahlt"
},
{
key: "paymentType",
label: "Zahlart",
sortable: true,
},
{
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[type] ||'')
const searchString = ref(tempStore.searchStrings['incominginvoices'] ||'')
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")]
}) })
@@ -133,7 +113,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}`)
@@ -157,7 +137,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="/" />
@@ -179,57 +159,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"
v-model:sort="sort" :items="[{label: 'In Bearbeitung'},{label: 'Gebucht'}]"
sort-mode="manual" >
@update:sort="setupPage" <template #default="{item}">
:rows="filteredRows" {{item.label}}
:columns="columns" <UBadge
class="w-full" variant="outline"
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }" class="ml-2"
@select="(i) => selectIncomingInvoice(i) " >
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Belege anzuzeigen' }" {{filteredRows.filter(i => item.label === 'Gebucht' ? i.state === 'Gebucht' : i.state !== 'Gebucht' ).length}}
> </UBadge>
<template #reference-data="{row}"> </template>
<span v-if="row === filteredRows[selectedItem]" class="text-primary-500 font-bold">{{row.reference}}</span> <template #item="{item}">
<span v-else>{{row.reference}}</span> <div style="height: 80dvh; overflow-y: scroll">
</template> <UTable
<template #state-data="{row}"> v-model:sort="sort"
<span v-if="row.state === 'Vorbereitet'" class="text-cyan-500">{{row.state}}</span> sort-mode="manual"
<span v-else-if="row.state === 'Entwurf'" class="text-red-500">{{row.state}}</span> @update:sort="setupPage"
<span v-else-if="row.state === 'Gebucht'" class="text-primary-500">{{row.state}}</span> :rows="filteredRows.filter(i => item.label === 'Gebucht' ? i.state === 'Gebucht' : i.state !== 'Gebucht' )"
</template> :columns="columns"
<template #date-data="{row}"> class="w-full"
{{dayjs(row.date).format("DD.MM.YYYY")}} :ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
</template> @select="(i) => selectIncomingInvoice(i) "
<template #vendor-data="{row}"> :empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Belege anzuzeigen' }"
{{row.vendor ? row.vendor.name : ""}} >
</template> <template #reference-data="{row}">
<template #amount-data="{row}"> <span v-if="row === filteredRows[selectedItem]" class="text-primary-500 font-bold">{{row.reference}}</span>
{{displayCurrency(sum.getIncomingInvoiceSum(row))}} <span v-else>{{row.reference}}</span>
</template> </template>
<template #dueDate-data="{row}"> <template #state-data="{row}">
<span v-if="row.dueDate">{{dayjs(row.dueDate).format("DD.MM.YYYY")}}</span> <span v-if="row.state === 'Vorbereitet'" class="text-cyan-500">{{row.state}}</span>
</template> <span v-else-if="row.state === 'Entwurf'" class="text-red-500">{{row.state}}</span>
<template #paid-data="{row}"> <span v-else-if="row.state === 'Gebucht'" class="text-primary-500">{{row.state}}</span>
<span v-if="isPaid(row)" class="text-primary-500">Bezahlt</span> </template>
<span v-else class="text-rose-600">Offen</span> <template #date-data="{row}">
</template> {{dayjs(row.date).format("DD.MM.YYYY")}}
</UTable> </template>
</UDashboardPanelContent> <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

@@ -62,6 +62,11 @@
<script setup> <script setup>
definePageMeta({
middleware: 'redirect-to-mobile-index'
})
import DisplayPresentProfiles from "~/components/noAutoLoad/displayPresentProfiles.vue"; import DisplayPresentProfiles from "~/components/noAutoLoad/displayPresentProfiles.vue";

View File

@@ -14,7 +14,11 @@ const doLogin = async (data:any) => {
await auth.login(data.email, data.password) await auth.login(data.email, data.password)
// Weiterleiten nach erfolgreichem Login // Weiterleiten nach erfolgreichem Login
toast.add({title:"Einloggen erfolgreich"}) toast.add({title:"Einloggen erfolgreich"})
return navigateTo("/") if(useCapacitor().getIsNative()) {
return navigateTo("/mobile")
} else {
return navigateTo("/")
}
} catch (err: any) { } catch (err: any) {
toast.add({title:"Zugangsdaten falsch. Bitte überprüfen Sie Ihre Eingaben",color:"rose"}) toast.add({title:"Zugangsdaten falsch. Bitte überprüfen Sie Ihre Eingaben",color:"rose"})
} }

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>

View File

@@ -2,9 +2,7 @@
import {setPageLayout} from "#app"; import {setPageLayout} from "#app";
import {useCapacitor} from "~/composables/useCapacitor.js"; import {useCapacitor} from "~/composables/useCapacitor.js";
definePageMeta({
layout: "default",
})
const route = useRoute() const route = useRoute()
const dataStore = useDataStore() const dataStore = useDataStore()
const api = useNuxtApp().$api const api = useNuxtApp().$api
@@ -12,7 +10,6 @@ const api = useNuxtApp().$api
const type = route.params.type const type = route.params.type
const platform = await useCapacitor().getIsPhone() ? "mobile" : "default" const platform = await useCapacitor().getIsPhone() ? "mobile" : "default"
console.log(platform)
const dataType = dataStore.dataTypes[route.params.type] const dataType = dataStore.dataTypes[route.params.type]
@@ -25,10 +22,9 @@ const item = ref({})
const setupPage = async (sort_column = null, sort_direction = null) => { const setupPage = async (sort_column = null, sort_direction = null) => {
loaded.value = false loaded.value = false
setPageLayout(platform)
if (await useCapacitor().getIsPhone()) {
setPageLayout("mobile")
}
if (route.params.mode) mode.value = route.params.mode if (route.params.mode) mode.value = route.params.mode

View File

@@ -1,10 +1,19 @@
import {Preferences} from "@capacitor/preferences";
export default defineNuxtPlugin(() => { export default defineNuxtPlugin(() => {
const api = $fetch.create({ const api = $fetch.create({
baseURL: /*"http://localhost:3100"*/ "https://backend.fedeo.io", baseURL: /*"http://192.168.1.227:3100"*/ "https://backend.fedeo.io",
credentials: "include", credentials: "include",
onRequest({ options }) { async onRequest({options}) {
// Token aus Cookie holen // Token aus Cookie holen
let token = useCookie("token").value let token: string | null | undefined = ""
if (await useCapacitor().getIsNative()) {
const {value} = await Preferences.get({key: 'token'});
token = value
} else {
token = useCookie("token").value
}
// Falls im Request explizit ein anderer JWT übergeben wird → diesen verwenden // Falls im Request explizit ein anderer JWT übergeben wird → diesen verwenden
if (options.context && (options.context as any).jwt) { if (options.context && (options.context as any).jwt) {

View File

@@ -1,5 +1,6 @@
import { defineStore } from "pinia" import { defineStore } from "pinia"
import router from "#app/plugins/router"; import router from "#app/plugins/router";
import {Preferences} from "@capacitor/preferences";
export const useAuthStore = defineStore("auth", { export const useAuthStore = defineStore("auth", {
state: () => ({ state: () => ({
@@ -15,7 +16,12 @@ export const useAuthStore = defineStore("auth", {
actions: { actions: {
async init(token) { async init(token) {
await this.fetchMe(token) await this.fetchMe(token)
navigateTo("/")
if(useCapacitor().getIsNative()) {
navigateTo("/mobile")
} else {
navigateTo("/")
}
}, },
async login(email: string, password: string) { async login(email: string, password: string) {
@@ -23,7 +29,15 @@ export const useAuthStore = defineStore("auth", {
method: "POST", method: "POST",
body: { email, password } body: { email, password }
}) })
useCookie("token").value = token // persistieren if(useCapacitor().getIsNative()) {
await Preferences.set({
key:"token",
value: token,
})
} else {
useCookie("token").value = token // persistieren
}
await this.fetchMe(token) await this.fetchMe(token)
}, },
@@ -38,7 +52,13 @@ export const useAuthStore = defineStore("auth", {
this.profile = null this.profile = null
this.activeTenant = null this.activeTenant = null
this.tenants = [] this.tenants = []
useCookie("token").value = null if(useCapacitor().getIsNative()) {
await Preferences.remove({ key: 'token' });
useCookie("token").value = null
} else {
useCookie("token").value = null
}
navigateTo("/login") navigateTo("/login")
}, },
@@ -62,7 +82,7 @@ export const useAuthStore = defineStore("auth", {
this.profile = me.profile this.profile = me.profile
if(me.activeTenant) { if(me.activeTenant > 0) {
this.activeTenant = me.activeTenant this.activeTenant = me.activeTenant
this.activeTenantData = me.tenants.find(i => i.id === me.activeTenant) this.activeTenantData = me.tenants.find(i => i.id === me.activeTenant)
this.loading = false this.loading = false
@@ -79,13 +99,29 @@ export const useAuthStore = defineStore("auth", {
async switchTenant(tenant_id: string) { async switchTenant(tenant_id: string) {
this.loading = true this.loading = true
const { token } = await useNuxtApp().$api("/api/tenant/switch", { const res = await useNuxtApp().$api("/api/tenant/switch", {
method: "POST", method: "POST",
body: { tenant_id } body: { tenant_id }
}) })
useCookie("token").value = token console.log(res)
const {token} = res
if(await useCapacitor().getIsNative()) {
await Preferences.set({
key:"token",
value: token,
})
} else {
useCookie("token").value = token // persistieren
}
await this.init(token) await this.init(token)
navigateTo("/") if(useCapacitor().getIsNative()) {
navigateTo("/mobile")
} else {
navigateTo("/")
}
}, },
hasPermission(key: string) { hasPermission(key: string) {

View File

@@ -1552,6 +1552,19 @@ export const useDataStore = defineStore('data', () => {
label: "Dokumente", label: "Dokumente",
labelSingle: "Dokument", labelSingle: "Dokument",
supabaseSelectWithInformation: "*, files(*), statementallocations(*)", supabaseSelectWithInformation: "*, files(*), statementallocations(*)",
filters: [
{
name: "Archivierte ausblenden",
default: true,
"filterFunction": function (row) {
if(!row.archived) {
return true
} else {
return false
}
}
}
]
}, },
files: { files: {
isArchivable: true, isArchivable: true,
@@ -1568,7 +1581,61 @@ export const useDataStore = defineStore('data', () => {
incominginvoices: { incominginvoices: {
label: "Eingangsrechnungen", label: "Eingangsrechnungen",
labelSingle: "Eingangsrechnung", labelSingle: "Eingangsrechnung",
redirect:true redirect:true,
filters: [
{
name: "Archivierte ausblenden",
default: true,
"filterFunction": function (row) {
if(!row.archived) {
return true
} else {
return false
}
}
}
],
templateColumns: [
{
key: 'reference',
label: "Referenz:",
sortable: true,
}, {
key: 'state',
label: "Status:"
},
{
key: "date",
label: "Datum",
sortable: true,
},
{
key: "vendor",
label: "Lieferant",
},
{
key: "amount",
label: "Betrag",
},
{
key: "dueDate",
label: "Fälligkeitsdatum",
sortable: true,
},
{
key: "paid",
label: "Bezahlt"
},
{
key: "paymentType",
label: "Zahlart",
sortable: true,
},
{
key: "description",
label: "Beschreibung"
}
]
}, },
inventoryitems: { inventoryitems: {
isArchivable: true, isArchivable: true,