Merge branch 'devCorrected' into 'beta'
Dev corrected See merge request fedeo/software!38
This commit is contained in:
@@ -1,49 +1,53 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
queryStringData: {
|
item: { type: Object, required: true },
|
||||||
type: String
|
type: { type: String, required: true },
|
||||||
},
|
topLevelType: { type: String, required: true },
|
||||||
item: {
|
platform: { type: String, required: true }
|
||||||
type: Object,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
type: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
topLevelType: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
platform: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(["updateNeeded"])
|
const emit = defineEmits(["updateNeeded"])
|
||||||
|
|
||||||
const files = useFiles()
|
const files = useFiles()
|
||||||
|
|
||||||
|
|
||||||
const availableFiles = ref([])
|
const availableFiles = ref([])
|
||||||
|
const activeFile = ref(null)
|
||||||
|
const showViewer = ref(false)
|
||||||
|
|
||||||
const setup = async () => {
|
const setup = async () => {
|
||||||
if(props.item.files) {
|
if (props.item.files?.length > 0) {
|
||||||
availableFiles.value = (await files.selectSomeDocuments(props.item.files.map(i => i.id))) || []
|
availableFiles.value =
|
||||||
|
(await files.selectSomeDocuments(props.item.files.map((f) => f.id))) || []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setup()
|
setup()
|
||||||
|
|
||||||
|
// Datei öffnen (Mobile/Tablet)
|
||||||
|
function openFile(file) {
|
||||||
|
activeFile.value = file
|
||||||
|
showViewer.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeViewer() {
|
||||||
|
showViewer.value = false
|
||||||
|
activeFile.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// PDF oder Bild?
|
||||||
|
function isPdf(file) {
|
||||||
|
return file.path.includes("pdf")
|
||||||
|
}
|
||||||
|
function isImage(file) {
|
||||||
|
return file.mimetype?.startsWith("image/")
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UCard class="mt-5" :style="props.platform !== 'mobile' ? 'height: 80vh' : ''">
|
<UCard class="mt-5" :style="props.platform !== 'mobile' ? 'height: 80vh' : ''">
|
||||||
<template #header v-if="props.platform === 'mobile'">
|
<template #header>
|
||||||
<span>Dateien</span>
|
<span>Dateien</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- Upload -->
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<DocumentUpload
|
<DocumentUpload
|
||||||
:type="props.topLevelType.substring(0, props.topLevelType.length - 1)"
|
:type="props.topLevelType.substring(0, props.topLevelType.length - 1)"
|
||||||
@@ -51,20 +55,80 @@ setup()
|
|||||||
@uploadFinished="emit('updateNeeded')"
|
@uploadFinished="emit('updateNeeded')"
|
||||||
/>
|
/>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
|
|
||||||
|
<!-- 📱 MOBILE: File Cards -->
|
||||||
|
<div v-if="props.platform === 'mobile'" class="space-y-3 mt-3">
|
||||||
|
<div
|
||||||
|
v-for="file in availableFiles"
|
||||||
|
:key="file.id"
|
||||||
|
class="p-4 border rounded-xl bg-gray-50 dark:bg-gray-900 flex items-center justify-between active:scale-95 transition cursor-pointer"
|
||||||
|
@click="openFile(file)"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p class="font-semibold truncate max-w-[200px]">{{ file?.path?.split("/").pop() }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UIcon
|
||||||
|
name="i-heroicons-chevron-right-20-solid"
|
||||||
|
class="w-5 h-5 text-gray-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UAlert
|
||||||
|
v-if="!availableFiles.length"
|
||||||
|
icon="i-heroicons-x-mark"
|
||||||
|
title="Keine Dateien verfügbar"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 🖥️ DESKTOP: Classic List -->
|
||||||
|
<template v-else>
|
||||||
<DocumentList
|
<DocumentList
|
||||||
:key="props.item.files.length"
|
:key="props.item.files.length"
|
||||||
:documents="availableFiles"
|
:documents="availableFiles"
|
||||||
v-if="availableFiles.length > 0"
|
v-if="availableFiles.length > 0"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<UAlert v-else icon="i-heroicons-x-mark" title="Keine Dateien verfügbar" />
|
||||||
|
</template>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
<!-- 📱 PDF / IMG Viewer Slideover -->
|
||||||
|
<UModal v-model="showViewer" side="bottom" class="h-[100dvh]" fullscreen>
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="p-4 border-b flex justify-between items-center flex-shrink-0">
|
||||||
|
<h3 class="font-bold truncate max-w-[70vw]">{{ activeFile?.path?.split("/").pop() }}</h3>
|
||||||
|
<UButton icon="i-heroicons-x-mark" variant="ghost" @click="closeViewer" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<div class="flex-1 overflow-y-auto m-2">
|
||||||
|
<!-- PDF -->
|
||||||
|
<div v-if="activeFile && isPdf(activeFile)" class="h-full">
|
||||||
|
<PDFViewer
|
||||||
|
:no-controls="true"
|
||||||
|
:file-id="activeFile.id"
|
||||||
|
location="fileviewer-mobile"
|
||||||
|
class="h-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- IMAGE -->
|
||||||
|
<div
|
||||||
|
v-else-if="activeFile && isImage(activeFile)"
|
||||||
|
class="p-4 flex justify-center"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="activeFile.url"
|
||||||
|
class="max-w-full max-h-[80vh] rounded-lg shadow"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<UAlert
|
<UAlert
|
||||||
v-else
|
v-else
|
||||||
icon="i-heroicons-x-mark"
|
title="Nicht unterstützter Dateityp"
|
||||||
title="Keine Dateien verfügbar"
|
icon="i-heroicons-exclamation-triangle"
|
||||||
/>
|
/>
|
||||||
</UCard>
|
</div>
|
||||||
|
</UModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
@@ -40,7 +40,8 @@ const links = computed(() => {
|
|||||||
id: 'historyitems',
|
id: 'historyitems',
|
||||||
label: "Logbuch",
|
label: "Logbuch",
|
||||||
to: "/historyitems",
|
to: "/historyitems",
|
||||||
icon: "i-heroicons-book-open"
|
icon: "i-heroicons-book-open",
|
||||||
|
disabled: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Organisation",
|
label: "Organisation",
|
||||||
@@ -52,7 +53,7 @@ const links = computed(() => {
|
|||||||
to: "/standardEntity/tasks",
|
to: "/standardEntity/tasks",
|
||||||
icon: "i-heroicons-rectangle-stack"
|
icon: "i-heroicons-rectangle-stack"
|
||||||
}] : [],
|
}] : [],
|
||||||
... true ? [{
|
/*... true ? [{
|
||||||
label: "Plantafel",
|
label: "Plantafel",
|
||||||
to: "/calendar/timeline",
|
to: "/calendar/timeline",
|
||||||
icon: "i-heroicons-calendar-days"
|
icon: "i-heroicons-calendar-days"
|
||||||
@@ -66,7 +67,7 @@ const links = computed(() => {
|
|||||||
label: "Termine",
|
label: "Termine",
|
||||||
to: "/standardEntity/events",
|
to: "/standardEntity/events",
|
||||||
icon: "i-heroicons-calendar-days"
|
icon: "i-heroicons-calendar-days"
|
||||||
}] : [],
|
}] : [],*/
|
||||||
/*{
|
/*{
|
||||||
label: "Dateien",
|
label: "Dateien",
|
||||||
to: "/files",
|
to: "/files",
|
||||||
@@ -83,10 +84,16 @@ const links = computed(() => {
|
|||||||
label: "Dateien",
|
label: "Dateien",
|
||||||
to: "/files",
|
to: "/files",
|
||||||
icon: "i-heroicons-document"
|
icon: "i-heroicons-document"
|
||||||
|
},{
|
||||||
|
label: "Anschreiben",
|
||||||
|
to: "/createdletters",
|
||||||
|
icon: "i-heroicons-document",
|
||||||
|
disabled: true
|
||||||
},{
|
},{
|
||||||
label: "Boxen",
|
label: "Boxen",
|
||||||
to: "/standardEntity/documentboxes",
|
to: "/standardEntity/documentboxes",
|
||||||
icon: "i-heroicons-archive-box"
|
icon: "i-heroicons-archive-box",
|
||||||
|
disabled: true
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -98,12 +105,14 @@ const links = computed(() => {
|
|||||||
{
|
{
|
||||||
label: "Helpdesk",
|
label: "Helpdesk",
|
||||||
to: "/helpdesk",
|
to: "/helpdesk",
|
||||||
icon: "i-heroicons-chat-bubble-left-right"
|
icon: "i-heroicons-chat-bubble-left-right",
|
||||||
|
disabled: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "E-Mail",
|
label: "E-Mail",
|
||||||
to: "/email/new",
|
to: "/email/new",
|
||||||
icon: "i-heroicons-envelope"
|
icon: "i-heroicons-envelope",
|
||||||
|
disabled: true
|
||||||
}/*, {
|
}/*, {
|
||||||
label: "Logbücher",
|
label: "Logbücher",
|
||||||
to: "/communication/historyItems",
|
to: "/communication/historyItems",
|
||||||
@@ -145,7 +154,8 @@ const links = computed(() => {
|
|||||||
... true ? [{
|
... true ? [{
|
||||||
label: "Anwesenheiten",
|
label: "Anwesenheiten",
|
||||||
to: "/staff/time",
|
to: "/staff/time",
|
||||||
icon: "i-heroicons-clock"
|
icon: "i-heroicons-clock",
|
||||||
|
disabled: true
|
||||||
}] : [],
|
}] : [],
|
||||||
/*... has("absencerequests") ? [{
|
/*... has("absencerequests") ? [{
|
||||||
label: "Abwesenheiten",
|
label: "Abwesenheiten",
|
||||||
@@ -175,7 +185,8 @@ const links = computed(() => {
|
|||||||
},{
|
},{
|
||||||
label: "Eingangsbelege",
|
label: "Eingangsbelege",
|
||||||
to: "/incomingInvoices",
|
to: "/incomingInvoices",
|
||||||
icon: "i-heroicons-document-text"
|
icon: "i-heroicons-document-text",
|
||||||
|
disabled: true
|
||||||
},{
|
},{
|
||||||
label: "Kostenstellen",
|
label: "Kostenstellen",
|
||||||
to: "/standardEntity/costcentres",
|
to: "/standardEntity/costcentres",
|
||||||
@@ -183,7 +194,8 @@ const links = computed(() => {
|
|||||||
},{
|
},{
|
||||||
label: "Buchungskonten",
|
label: "Buchungskonten",
|
||||||
to: "/accounts",
|
to: "/accounts",
|
||||||
icon: "i-heroicons-document-text"
|
icon: "i-heroicons-document-text",
|
||||||
|
disabled: true
|
||||||
},{
|
},{
|
||||||
label: "zusätzliche Buchungskonten",
|
label: "zusätzliche Buchungskonten",
|
||||||
to: "/standardEntity/ownaccounts",
|
to: "/standardEntity/ownaccounts",
|
||||||
@@ -192,7 +204,8 @@ const links = computed(() => {
|
|||||||
{
|
{
|
||||||
label: "Bank",
|
label: "Bank",
|
||||||
to: "/banking",
|
to: "/banking",
|
||||||
icon: "i-heroicons-document-text"
|
icon: "i-heroicons-document-text",
|
||||||
|
disabled: true
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}],
|
}],
|
||||||
@@ -285,11 +298,11 @@ const links = computed(() => {
|
|||||||
to: "/standardEntity/plants",
|
to: "/standardEntity/plants",
|
||||||
icon: "i-heroicons-clipboard-document"
|
icon: "i-heroicons-clipboard-document"
|
||||||
},] : [],
|
},] : [],
|
||||||
... has("checks") ? [{
|
/*... has("checks") ? [{
|
||||||
label: "Überprüfungen",
|
label: "Überprüfungen",
|
||||||
to: "/standardEntity/checks",
|
to: "/standardEntity/checks",
|
||||||
icon: "i-heroicons-magnifying-glass"
|
icon: "i-heroicons-magnifying-glass"
|
||||||
},] : [],
|
},] : [],*/
|
||||||
{
|
{
|
||||||
label: "Einstellungen",
|
label: "Einstellungen",
|
||||||
defaultOpen: false,
|
defaultOpen: false,
|
||||||
@@ -298,7 +311,8 @@ const links = computed(() => {
|
|||||||
{
|
{
|
||||||
label: "Nummernkreise",
|
label: "Nummernkreise",
|
||||||
to: "/settings/numberRanges",
|
to: "/settings/numberRanges",
|
||||||
icon: "i-heroicons-clipboard-document-list"
|
icon: "i-heroicons-clipboard-document-list",
|
||||||
|
disabled: true
|
||||||
},/*{
|
},/*{
|
||||||
label: "Rollen",
|
label: "Rollen",
|
||||||
to: "/roles",
|
to: "/roles",
|
||||||
@@ -306,15 +320,18 @@ const links = computed(() => {
|
|||||||
},*/{
|
},*/{
|
||||||
label: "E-Mail Konten",
|
label: "E-Mail Konten",
|
||||||
to: "/settings/emailaccounts",
|
to: "/settings/emailaccounts",
|
||||||
icon: "i-heroicons-envelope"
|
icon: "i-heroicons-envelope",
|
||||||
|
disabled: true
|
||||||
},{
|
},{
|
||||||
label: "Bankkonten",
|
label: "Bankkonten",
|
||||||
to: "/settings/banking",
|
to: "/settings/banking",
|
||||||
icon: "i-heroicons-currency-euro"
|
icon: "i-heroicons-currency-euro",
|
||||||
|
disabled: true
|
||||||
},{
|
},{
|
||||||
label: "Textvorlagen",
|
label: "Textvorlagen",
|
||||||
to: "/settings/texttemplates",
|
to: "/settings/texttemplates",
|
||||||
icon: "i-heroicons-clipboard-document-list"
|
icon: "i-heroicons-clipboard-document-list",
|
||||||
|
disabled: true
|
||||||
},/*{
|
},/*{
|
||||||
label: "Eigene Felder",
|
label: "Eigene Felder",
|
||||||
to: "/settings/ownfields",
|
to: "/settings/ownfields",
|
||||||
@@ -322,15 +339,18 @@ const links = computed(() => {
|
|||||||
},*/{
|
},*/{
|
||||||
label: "Firmeneinstellungen",
|
label: "Firmeneinstellungen",
|
||||||
to: "/settings/tenant",
|
to: "/settings/tenant",
|
||||||
icon: "i-heroicons-building-office"
|
icon: "i-heroicons-building-office",
|
||||||
|
disabled: true
|
||||||
},{
|
},{
|
||||||
label: "Projekttypen",
|
label: "Projekttypen",
|
||||||
to: "/projecttypes",
|
to: "/projecttypes",
|
||||||
icon: "i-heroicons-clipboard-document-list"
|
icon: "i-heroicons-clipboard-document-list",
|
||||||
|
disabled: true
|
||||||
},{
|
},{
|
||||||
label: "Export",
|
label: "Export",
|
||||||
to: "/export",
|
to: "/export",
|
||||||
icon: "i-heroicons-clipboard-document-list"
|
icon: "i-heroicons-clipboard-document-list",
|
||||||
|
disabled: true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ const props = defineProps({
|
|||||||
location: {
|
location: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|
||||||
|
},
|
||||||
|
noControls: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -123,7 +127,18 @@ const handleKeyPress = (event) => {
|
|||||||
const downloadControl = computed(() => vpvRef.value?.downloadControl)
|
const downloadControl = computed(() => vpvRef.value?.downloadControl)
|
||||||
|
|
||||||
const handleDownloadFile = async () => {
|
const handleDownloadFile = async () => {
|
||||||
|
if(props.fileId){
|
||||||
await useFiles().downloadFile(props.fileId)
|
await useFiles().downloadFile(props.fileId)
|
||||||
|
} else if(props.uri){
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = props.uri;
|
||||||
|
a.download = "entwurf.pdf";
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*const downloadCtrl = unref(downloadControl)
|
/*const downloadCtrl = unref(downloadControl)
|
||||||
if (!downloadCtrl) return
|
if (!downloadCtrl) return
|
||||||
@@ -145,7 +160,7 @@ watch(downloadControl, (downloadCtrl) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-4 justify-self-center">
|
<div class="flex flex-col gap-4 justify-self-center" v-if="!noControls">
|
||||||
<div class="flex items-center gap-4 text-[#7862FF] bg-pale-blue border-[#D7D1FB] rounded-lg p-2 justify-center">
|
<div class="flex items-center gap-4 text-[#7862FF] bg-pale-blue border-[#D7D1FB] rounded-lg p-2 justify-center">
|
||||||
|
|
||||||
<!-- Zoom out button -->
|
<!-- Zoom out button -->
|
||||||
@@ -168,7 +183,7 @@ watch(downloadControl, (downloadCtrl) => {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
></UButton>
|
></UButton>
|
||||||
<UButton
|
<UButton
|
||||||
v-if="props.fileId"
|
v-if="props.fileId || props.uri"
|
||||||
@click="handleDownloadFile"
|
@click="handleDownloadFile"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
icon="i-heroicons-arrow-down-on-square"
|
icon="i-heroicons-arrow-down-on-square"
|
||||||
|
|||||||
67
components/displayPinnendLinks.vue
Normal file
67
components/displayPinnendLinks.vue
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<script setup>
|
||||||
|
import { Browser } from "@capacitor/browser"
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
links: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Öffnet externen Link in iOS/Android über InApp Browser.
|
||||||
|
* Öffnet externen Link im Web über window.open.
|
||||||
|
* Interne Links → navigateTo
|
||||||
|
*/
|
||||||
|
async function openLink(link) {
|
||||||
|
if (link.external) {
|
||||||
|
if (useCapacitor().getIsNative()) {
|
||||||
|
await Browser.open({
|
||||||
|
url: link.to,
|
||||||
|
presentationStyle: "popover",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
window.open(link.to, "_blank")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return navigateTo(link.to)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UDashboardCard
|
||||||
|
v-if="links.length > 0"
|
||||||
|
title="Schnellzugriffe"
|
||||||
|
>
|
||||||
|
<div class="space-y-2">
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-for="(link, index) in links"
|
||||||
|
:key="index"
|
||||||
|
class="
|
||||||
|
p-3 bg-gray-50 dark:bg-gray-900
|
||||||
|
rounded-xl border flex items-center justify-between
|
||||||
|
active:scale-95 transition cursor-pointer
|
||||||
|
"
|
||||||
|
@click="openLink(link)"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<UIcon
|
||||||
|
:name="link.icon || 'i-heroicons-link'"
|
||||||
|
class="w-6 h-6 text-primary-500"
|
||||||
|
/>
|
||||||
|
<span class="font-medium truncate max-w-[60vw]">
|
||||||
|
{{ link.label }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UIcon
|
||||||
|
name="i-heroicons-chevron-right-20-solid"
|
||||||
|
class="w-5 h-5 text-gray-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</UDashboardCard>
|
||||||
|
</template>
|
||||||
@@ -6,9 +6,9 @@ const auth = useAuthStore()
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div v-if="auth.activeTenant">
|
||||||
<h1 class="font-bold text-xl">Willkommen zurück {{auth.profile.full_name}}</h1>
|
<h1 class="font-bold text-xl">Willkommen zurück {{auth.profile.full_name}}</h1>
|
||||||
<span v-if="auth.activeTenant">bei {{auth.activeTenantData.name}}</span>
|
<span>bei {{auth.activeTenantData.name}}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -2,30 +2,36 @@
|
|||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
label: {
|
label: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: false,
|
||||||
|
default: null
|
||||||
},
|
},
|
||||||
icon: {
|
icon: {
|
||||||
type: String,
|
type: String,
|
||||||
|
required: false
|
||||||
},
|
},
|
||||||
variant: {
|
variant: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'solid'
|
default: "solid"
|
||||||
},
|
},
|
||||||
color: {
|
color: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'primary'
|
default: "primary"
|
||||||
},
|
},
|
||||||
pos: {
|
pos: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0
|
default: 6 // Abstand von unten in Rem (6 = 1.5rem * 6 = 9rem)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['click'])
|
const emit = defineEmits(["click"])
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<!-- Wrapper für Position + Animation -->
|
||||||
|
<div
|
||||||
|
class="fixed right-5 z-40 transition-all"
|
||||||
|
:style="{ bottom: `calc(${props.pos}rem + env(safe-area-inset-bottom))` }"
|
||||||
|
>
|
||||||
<UButton
|
<UButton
|
||||||
id="fab"
|
id="fab"
|
||||||
:icon="props.icon"
|
:icon="props.icon"
|
||||||
@@ -33,15 +39,28 @@ const emit = defineEmits(['click'])
|
|||||||
:variant="props.variant"
|
:variant="props.variant"
|
||||||
:color="props.color"
|
:color="props.color"
|
||||||
@click="emit('click')"
|
@click="emit('click')"
|
||||||
:style="`bottom: ${15 + props.pos * 5}vh;`"
|
class="
|
||||||
class="bg-white dark:bg-gray-950"
|
fab-base
|
||||||
|
shadow-xl
|
||||||
|
hover:shadow-2xl
|
||||||
|
active:scale-95
|
||||||
|
transition
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
#fab {
|
/* FAB Basis */
|
||||||
position: fixed;
|
.fab-base {
|
||||||
right: 15px;
|
@apply rounded-full px-5 py-4 text-lg font-semibold;
|
||||||
z-index: 5;
|
|
||||||
|
/* Wenn nur ein Icon vorhanden ist → runder Kreis */
|
||||||
|
/* Wenn Label + Icon → Extended FAB */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Optional: Auto-Kreisen wenn kein Label */
|
||||||
|
#fab:not([label]) {
|
||||||
|
@apply w-14 h-14 p-0 flex items-center justify-center text-2xl;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -60,5 +60,10 @@
|
|||||||
</array>
|
</array>
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>NSAppTransportSecurity</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ install! 'cocoapods', :disable_input_output_paths => true
|
|||||||
def capacitor_pods
|
def capacitor_pods
|
||||||
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
||||||
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
||||||
|
pod 'CapacitorBrowser', :path => '../../node_modules/@capacitor/browser'
|
||||||
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 'CapacitorPreferences', :path => '../../node_modules/@capacitor/preferences'
|
||||||
@@ -24,7 +25,15 @@ target 'App' do
|
|||||||
end
|
end
|
||||||
|
|
||||||
post_install do |installer|
|
post_install do |installer|
|
||||||
assertDeploymentTarget(installer)
|
installer.pods_project.targets.each do |target|
|
||||||
|
target.build_configurations.each do |config|
|
||||||
|
# iOS Deployment Target erzwingen
|
||||||
|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '14.0'
|
||||||
|
|
||||||
|
# Alle Warnungen auf inherited setzen, falls Pods Dinge überschreiben
|
||||||
|
config.build_settings['CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER'] = '$(inherited)'
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
target 'OneSignalNotificationServiceExtension' do
|
target 'OneSignalNotificationServiceExtension' do
|
||||||
|
|||||||
@@ -1,105 +0,0 @@
|
|||||||
PODS:
|
|
||||||
- Capacitor (7.1.0):
|
|
||||||
- CapacitorCordova
|
|
||||||
- CapacitorCordova (7.1.0)
|
|
||||||
- CapacitorDevice (7.0.0):
|
|
||||||
- Capacitor
|
|
||||||
- CapacitorNetwork (7.0.0):
|
|
||||||
- Capacitor
|
|
||||||
- CapacitorPluginSafeArea (4.0.0):
|
|
||||||
- Capacitor
|
|
||||||
- CapacitorPreferences (6.0.3):
|
|
||||||
- Capacitor
|
|
||||||
- CordovaPluginsStatic (7.1.0):
|
|
||||||
- CapacitorCordova
|
|
||||||
- OneSignalXCFramework (= 5.2.10)
|
|
||||||
- OneSignalXCFramework (5.2.10):
|
|
||||||
- OneSignalXCFramework/OneSignalComplete (= 5.2.10)
|
|
||||||
- OneSignalXCFramework/OneSignal (5.2.10):
|
|
||||||
- OneSignalXCFramework/OneSignalCore
|
|
||||||
- OneSignalXCFramework/OneSignalExtension
|
|
||||||
- OneSignalXCFramework/OneSignalLiveActivities
|
|
||||||
- OneSignalXCFramework/OneSignalNotifications
|
|
||||||
- OneSignalXCFramework/OneSignalOSCore
|
|
||||||
- OneSignalXCFramework/OneSignalOutcomes
|
|
||||||
- OneSignalXCFramework/OneSignalUser
|
|
||||||
- OneSignalXCFramework/OneSignalComplete (5.2.10):
|
|
||||||
- OneSignalXCFramework/OneSignal
|
|
||||||
- OneSignalXCFramework/OneSignalInAppMessages
|
|
||||||
- OneSignalXCFramework/OneSignalLocation
|
|
||||||
- OneSignalXCFramework/OneSignalCore (5.2.10)
|
|
||||||
- OneSignalXCFramework/OneSignalExtension (5.2.10):
|
|
||||||
- OneSignalXCFramework/OneSignalCore
|
|
||||||
- OneSignalXCFramework/OneSignalOutcomes
|
|
||||||
- OneSignalXCFramework/OneSignalInAppMessages (5.2.10):
|
|
||||||
- OneSignalXCFramework/OneSignalCore
|
|
||||||
- OneSignalXCFramework/OneSignalNotifications
|
|
||||||
- OneSignalXCFramework/OneSignalOSCore
|
|
||||||
- OneSignalXCFramework/OneSignalOutcomes
|
|
||||||
- OneSignalXCFramework/OneSignalUser
|
|
||||||
- OneSignalXCFramework/OneSignalLiveActivities (5.2.10):
|
|
||||||
- OneSignalXCFramework/OneSignalCore
|
|
||||||
- OneSignalXCFramework/OneSignalOSCore
|
|
||||||
- OneSignalXCFramework/OneSignalUser
|
|
||||||
- OneSignalXCFramework/OneSignalLocation (5.2.10):
|
|
||||||
- OneSignalXCFramework/OneSignalCore
|
|
||||||
- OneSignalXCFramework/OneSignalNotifications
|
|
||||||
- OneSignalXCFramework/OneSignalOSCore
|
|
||||||
- OneSignalXCFramework/OneSignalUser
|
|
||||||
- OneSignalXCFramework/OneSignalNotifications (5.2.10):
|
|
||||||
- OneSignalXCFramework/OneSignalCore
|
|
||||||
- OneSignalXCFramework/OneSignalExtension
|
|
||||||
- OneSignalXCFramework/OneSignalOutcomes
|
|
||||||
- OneSignalXCFramework/OneSignalOSCore (5.2.10):
|
|
||||||
- OneSignalXCFramework/OneSignalCore
|
|
||||||
- OneSignalXCFramework/OneSignalOutcomes (5.2.10):
|
|
||||||
- OneSignalXCFramework/OneSignalCore
|
|
||||||
- OneSignalXCFramework/OneSignalUser (5.2.10):
|
|
||||||
- OneSignalXCFramework/OneSignalCore
|
|
||||||
- OneSignalXCFramework/OneSignalNotifications
|
|
||||||
- OneSignalXCFramework/OneSignalOSCore
|
|
||||||
- OneSignalXCFramework/OneSignalOutcomes
|
|
||||||
|
|
||||||
DEPENDENCIES:
|
|
||||||
- "Capacitor (from `../../node_modules/@capacitor/ios`)"
|
|
||||||
- "CapacitorCordova (from `../../node_modules/@capacitor/ios`)"
|
|
||||||
- "CapacitorDevice (from `../../node_modules/@capacitor/device`)"
|
|
||||||
- "CapacitorNetwork (from `../../node_modules/@capacitor/network`)"
|
|
||||||
- CapacitorPluginSafeArea (from `../../node_modules/capacitor-plugin-safe-area`)
|
|
||||||
- "CapacitorPreferences (from `../../node_modules/@capacitor/preferences`)"
|
|
||||||
- CordovaPluginsStatic (from `../capacitor-cordova-ios-plugins`)
|
|
||||||
- OneSignalXCFramework (< 6.0, >= 5.0)
|
|
||||||
|
|
||||||
SPEC REPOS:
|
|
||||||
trunk:
|
|
||||||
- OneSignalXCFramework
|
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
|
||||||
Capacitor:
|
|
||||||
:path: "../../node_modules/@capacitor/ios"
|
|
||||||
CapacitorCordova:
|
|
||||||
:path: "../../node_modules/@capacitor/ios"
|
|
||||||
CapacitorDevice:
|
|
||||||
:path: "../../node_modules/@capacitor/device"
|
|
||||||
CapacitorNetwork:
|
|
||||||
:path: "../../node_modules/@capacitor/network"
|
|
||||||
CapacitorPluginSafeArea:
|
|
||||||
:path: "../../node_modules/capacitor-plugin-safe-area"
|
|
||||||
CapacitorPreferences:
|
|
||||||
:path: "../../node_modules/@capacitor/preferences"
|
|
||||||
CordovaPluginsStatic:
|
|
||||||
:path: "../capacitor-cordova-ios-plugins"
|
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
|
||||||
Capacitor: bceb785fb78f5e81e4a9e37843bc1c24bd9c7194
|
|
||||||
CapacitorCordova: 866217f32c1d25b326c568a10ea3ed0c36b13e29
|
|
||||||
CapacitorDevice: 069faf433b3a99c3d5f0e500fbe634f60a8c6a84
|
|
||||||
CapacitorNetwork: 30c2e78a0ed32530656cb426c8ee6c2caec10dbf
|
|
||||||
CapacitorPluginSafeArea: 22031c3436269ca80fac90ec2c94bc7c1e59a81d
|
|
||||||
CapacitorPreferences: f3eadae2369ac3ab8e21743a2959145b0d1286a3
|
|
||||||
CordovaPluginsStatic: f722d4ff434f50099581e690d579b7c108f490e6
|
|
||||||
OneSignalXCFramework: 1a3b28dfbff23aabce585796d23c1bef37772774
|
|
||||||
|
|
||||||
PODFILE CHECKSUM: d76fcd3d35c3f8c3708303de70ef45a76cc6e2b5
|
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
|
||||||
@@ -1,79 +1,59 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { ref, onMounted, onBeforeUnmount } from "vue"
|
||||||
|
|
||||||
|
|
||||||
import MainNav from "~/components/MainNav.vue";
|
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import {useProfileStore} from "~/stores/profile.js";
|
import {useAuthStore} from "~/stores/auth.js";
|
||||||
import {useCapacitor} from "../composables/useCapacitor.js";
|
|
||||||
|
|
||||||
const dataStore = useDataStore()
|
|
||||||
const profileStore = useProfileStore()
|
|
||||||
const colorMode = useColorMode()
|
|
||||||
const { isHelpSlideoverOpen } = useDashboard()
|
|
||||||
const supabase = useSupabaseClient()
|
|
||||||
const router = useRouter()
|
|
||||||
const route = useRoute()
|
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")
|
||||||
|
|
||||||
const actions = [
|
|
||||||
{
|
const hideNav = ref(false)
|
||||||
id: 'new-customer',
|
let lastScrollY = 0
|
||||||
label: 'Kunde hinzufügen',
|
let scrollElement = null
|
||||||
icon: 'i-heroicons-user-group',
|
let returnTimer = null
|
||||||
to: "/customers/create" ,
|
|
||||||
},
|
const SHOW_DELAY = 1000 // 1 Sekunden
|
||||||
{
|
|
||||||
id: 'new-vendor',
|
function showNavAfterDelay() {
|
||||||
label: 'Lieferant hinzufügen',
|
clearTimeout(returnTimer)
|
||||||
icon: 'i-heroicons-truck',
|
returnTimer = setTimeout(() => {
|
||||||
to: "/vendors/create" ,
|
hideNav.value = false
|
||||||
},
|
}, SHOW_DELAY)
|
||||||
{
|
|
||||||
id: 'new-contact',
|
|
||||||
label: 'Ansprechpartner hinzufügen',
|
|
||||||
icon: 'i-heroicons-user-group',
|
|
||||||
to: "/contacts/create" ,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'new-task',
|
|
||||||
label: 'Aufgabe hinzufügen',
|
|
||||||
icon: 'i-heroicons-rectangle-stack',
|
|
||||||
to: "/tasks/create" ,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'new-plant',
|
|
||||||
label: 'Objekt hinzufügen',
|
|
||||||
icon: 'i-heroicons-clipboard-document',
|
|
||||||
to: "/plants/create" ,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'new-product',
|
|
||||||
label: 'Artikel hinzufügen',
|
|
||||||
icon: 'i-heroicons-puzzle-piece',
|
|
||||||
to: "/products/create" ,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'new-project',
|
|
||||||
label: 'Projekt hinzufügen',
|
|
||||||
icon: 'i-heroicons-clipboard-document-check',
|
|
||||||
to: "/projects/create" ,
|
|
||||||
}
|
}
|
||||||
]
|
|
||||||
|
|
||||||
|
const handleScroll = () => {
|
||||||
|
const current = scrollElement.scrollTop
|
||||||
|
|
||||||
|
// Runter scrollen -> verstecken
|
||||||
|
if (current > lastScrollY + 10) {
|
||||||
|
hideNav.value = true
|
||||||
|
showNavAfterDelay()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hoch scrollen -> sofort zeigen
|
||||||
|
if (current < lastScrollY - 10) {
|
||||||
|
hideNav.value = false
|
||||||
|
clearTimeout(returnTimer)
|
||||||
|
}
|
||||||
|
|
||||||
|
lastScrollY = current
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
scrollElement = document.querySelector('.mobile-scroll-area')
|
||||||
|
if (scrollElement) {
|
||||||
|
scrollElement.addEventListener('scroll', handleScroll)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (scrollElement) scrollElement.removeEventListener('scroll', handleScroll)
|
||||||
|
clearTimeout(returnTimer)
|
||||||
|
})
|
||||||
|
|
||||||
const footerLinks = [/*{
|
|
||||||
label: 'Invite people',
|
|
||||||
icon: 'i-heroicons-plus',
|
|
||||||
to: '/settings/members'
|
|
||||||
}, */{
|
|
||||||
label: 'Hilfe & Info',
|
|
||||||
icon: 'i-heroicons-question-mark-circle',
|
|
||||||
click: () => isHelpSlideoverOpen.value = true
|
|
||||||
}]
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -191,45 +171,49 @@ const footerLinks = [/*{
|
|||||||
</UDashboardPanel>
|
</UDashboardPanel>
|
||||||
</UDashboardPage>
|
</UDashboardPage>
|
||||||
|
|
||||||
<div class="mobileFooter bg-white dark:bg-gray-950">
|
<!-- Modernisierte Mobile Navigation -->
|
||||||
|
<nav
|
||||||
|
:class="[
|
||||||
|
'fixed bottom-0 left-0 right-0 z-50', // ← bottom-0 hinzugefügt!
|
||||||
|
'h-[70px] bg-white/80 dark:bg-gray-950/80 backdrop-blur-xl',
|
||||||
|
'border-t border-gray-200 dark:border-gray-800',
|
||||||
|
'flex justify-around items-center pt-2 pb-[max(env(safe-area-inset-bottom),0.5rem)]',
|
||||||
|
'transition-transform duration-300 ease-in-out',
|
||||||
|
hideNav ? 'translate-y-full' : 'translate-y-0'
|
||||||
|
]"
|
||||||
|
>
|
||||||
<UButton
|
<UButton
|
||||||
icon="i-heroicons-home"
|
icon="i-heroicons-home"
|
||||||
to="/mobile/"
|
to="/mobile/"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
:color="route.fullPath === '/mobile' ? 'primary' : 'gray'"
|
:color="route.fullPath === '/mobile' ? 'primary' : 'gray'"
|
||||||
|
class="nav-btn"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<UButton
|
<UButton
|
||||||
icon="i-heroicons-clipboard-document-check"
|
icon="i-heroicons-clipboard-document-check"
|
||||||
to="/standardEntity/tasks"
|
to="/standardEntity/tasks"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
:color="route.fullPath === '/standardEntity/tasks' ? 'primary' : 'gray'"
|
:color="route.fullPath === '/standardEntity/tasks' ? 'primary' : 'gray'"
|
||||||
|
class="nav-btn"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<UButton
|
<UButton
|
||||||
icon="i-heroicons-rectangle-stack"
|
icon="i-heroicons-rectangle-stack"
|
||||||
to="/standardEntity/projects"
|
to="/standardEntity/projects"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
:color="route.fullPath === '/standardEntity/projects' ? 'primary' : 'gray'"
|
:color="route.fullPath === '/standardEntity/projects' ? 'primary' : 'gray'"
|
||||||
|
class="nav-btn"
|
||||||
/>
|
/>
|
||||||
<!-- <UButton
|
|
||||||
icon="i-heroicons-clock"
|
|
||||||
to="/workingtimes"
|
|
||||||
variant="ghost"
|
|
||||||
:color="route.fullPath === '/workingtimes' ? 'primary' : 'gray'"
|
|
||||||
/>-->
|
|
||||||
<UButton
|
<UButton
|
||||||
icon="i-heroicons-bars-4"
|
icon="i-heroicons-bars-4"
|
||||||
to="/mobile/menu"
|
to="/mobile/menu"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
:color="route.fullPath === '/mobile/menu' ? 'primary' : 'gray'"
|
:color="route.fullPath === '/mobile/menu' ? 'primary' : 'gray'"
|
||||||
|
class="nav-btn"
|
||||||
/>
|
/>
|
||||||
</div>
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- ~/components/HelpSlideover.vue -->
|
|
||||||
<HelpSlideover/>
|
|
||||||
<!-- ~/components/NotificationsSlideover.vue -->
|
|
||||||
<NotificationsSlideover />
|
|
||||||
|
|
||||||
|
|
||||||
</UDashboardLayout>
|
</UDashboardLayout>
|
||||||
@@ -251,9 +235,9 @@ 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="!auth.activeTenant" class="w-full mx-auto text-center">
|
<div v-if="!auth.activeTenant && auth.tenants?.length > 0 " class="w-full mx-auto text-center">
|
||||||
<!-- Tenant Selection -->
|
<!-- Tenant Selection -->
|
||||||
<h3 class="text-center font-bold text-2xl mb-5">Kein Aktiver Mandant. Bitte wählen Sie ein Mandant.</h3>
|
<h3 class="text-center font-bold text-2xl mb-5">Kein Aktiver Mandant. <br>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">
|
<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>
|
<span class="text-left">{{tenant.name}}</span>
|
||||||
<UButton
|
<UButton
|
||||||
@@ -269,27 +253,23 @@ const footerLinks = [/*{
|
|||||||
|
|
||||||
</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" />Test
|
||||||
|
{{auth.tenants}}
|
||||||
|
|
||||||
|
<UButton
|
||||||
|
variant="outline"
|
||||||
|
color="rose"
|
||||||
|
@click="auth.logout()"
|
||||||
|
>Abmelden</UButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.mobileFooter {
|
.nav-btn {
|
||||||
position: absolute;
|
@apply w-12 h-12 flex justify-center items-center rounded-xl active:scale-95 transition;
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
height: 8vh;
|
|
||||||
width: 100%;
|
|
||||||
border-top: 1px solid grey;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 1em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobileFooter > a {
|
|
||||||
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
export default defineNuxtRouteMiddleware(async (to, from) => {
|
export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
|
|
||||||
|
console.log(auth)
|
||||||
|
|
||||||
|
if (auth.loading) return
|
||||||
|
|
||||||
|
|
||||||
// Wenn nicht eingeloggt → auf /login (außer er will schon dahin)
|
// Wenn nicht eingeloggt → auf /login (außer er will schon dahin)
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
export default defineNuxtRouteMiddleware(async (to, _from) => {
|
export default defineNuxtRouteMiddleware(async (to, _from) => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
console.log(await useCapacitor().getIsPhone())
|
console.log(useCapacitor().getIsNative())
|
||||||
|
|
||||||
if(await useCapacitor().getIsPhone() && _from.path !== '/mobile') {
|
if(useCapacitor().getIsNative() && _from.path !== '/mobile') {
|
||||||
return router.push('/mobile')
|
return router.push('/mobile')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capacitor/android": "^7.0.0",
|
"@capacitor/android": "^7.0.0",
|
||||||
|
"@capacitor/browser": "^7.0.2",
|
||||||
"@capacitor/core": "^7.0.0",
|
"@capacitor/core": "^7.0.0",
|
||||||
"@capacitor/device": "^7.0.0",
|
"@capacitor/device": "^7.0.0",
|
||||||
"@capacitor/ios": "^7.0.0",
|
"@capacitor/ios": "^7.0.0",
|
||||||
@@ -66,7 +67,7 @@
|
|||||||
"maplibre-gl": "^4.7.0",
|
"maplibre-gl": "^4.7.0",
|
||||||
"nuxt-editorjs": "^1.0.4",
|
"nuxt-editorjs": "^1.0.4",
|
||||||
"nuxt-viewport": "^2.0.6",
|
"nuxt-viewport": "^2.0.6",
|
||||||
"onesignal-cordova-plugin": "^5.2.11",
|
"onesignal-cordova-plugin": "^5.2.14",
|
||||||
"papaparse": "^5.4.1",
|
"papaparse": "^5.4.1",
|
||||||
"pdf-lib": "^1.17.1",
|
"pdf-lib": "^1.17.1",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
|
|||||||
@@ -83,7 +83,9 @@
|
|||||||
>
|
>
|
||||||
<template #type-data="{row}">
|
<template #type-data="{row}">
|
||||||
{{dataStore.documentTypesForCreation[row.type].labelSingle}}
|
{{dataStore.documentTypesForCreation[row.type].labelSingle}}
|
||||||
|
<!--
|
||||||
<span v-if="row.type === 'cancellationInvoices'"> zu {{row.linkedDocument.documentNumber}}</span>
|
<span v-if="row.type === 'cancellationInvoices'"> zu {{row.linkedDocument.documentNumber}}</span>
|
||||||
|
-->
|
||||||
</template>
|
</template>
|
||||||
<template #state-data="{row}">
|
<template #state-data="{row}">
|
||||||
<span
|
<span
|
||||||
@@ -132,19 +134,25 @@
|
|||||||
<span v-if="row.documentDate">{{row.documentDate ? dayjs(row.documentDate).format("DD.MM.YY") : ''}}</span>
|
<span v-if="row.documentDate">{{row.documentDate ? dayjs(row.documentDate).format("DD.MM.YY") : ''}}</span>
|
||||||
</template>
|
</template>
|
||||||
<template #dueDate-data="{row}">
|
<template #dueDate-data="{row}">
|
||||||
|
<!--
|
||||||
<span v-if="row.state === 'Gebucht' && row.paymentDays && ['invoices','advanceInvoices'].includes(row.type) && !items.find(i => i.linkedDocument && i.linkedDocument.id === row.id)" :class="dayjs(row.documentDate).add(row.paymentDays,'day').diff(dayjs()) <= 0 && !isPaid(row) ? ['text-rose-500'] : '' ">{{row.documentDate ? dayjs(row.documentDate).add(row.paymentDays,'day').format("DD.MM.YY") : ''}}</span>
|
<span v-if="row.state === 'Gebucht' && row.paymentDays && ['invoices','advanceInvoices'].includes(row.type) && !items.find(i => i.linkedDocument && i.linkedDocument.id === row.id)" :class="dayjs(row.documentDate).add(row.paymentDays,'day').diff(dayjs()) <= 0 && !isPaid(row) ? ['text-rose-500'] : '' ">{{row.documentDate ? dayjs(row.documentDate).add(row.paymentDays,'day').format("DD.MM.YY") : ''}}</span>
|
||||||
|
-->
|
||||||
</template>
|
</template>
|
||||||
<template #paid-data="{row}">
|
<template #paid-data="{row}">
|
||||||
<div v-if="(row.type === 'invoices' ||row.type === 'advanceInvoices') && row.state === 'Gebucht' && !items.find(i => i.linkedDocument && i.linkedDocument.id === row.id)">
|
<!-- <div v-if="(row.type === 'invoices' ||row.type === 'advanceInvoices') && row.state === 'Gebucht' && !items.find(i => i.linkedDocument && i.linkedDocument.id === row.id)">
|
||||||
<span v-if="useSum().getIsPaid(row,items)" class="text-primary-500">Bezahlt</span>
|
<span v-if="useSum().getIsPaid(row,items)" class="text-primary-500">Bezahlt</span>
|
||||||
<span v-else class="text-rose-600">Offen</span>
|
<span v-else class="text-rose-600">Offen</span>
|
||||||
</div>
|
</div>-->
|
||||||
</template>
|
</template>
|
||||||
<template #amount-data="{row}">
|
<template #amount-data="{row}">
|
||||||
|
<!--
|
||||||
<span v-if="row.type !== 'deliveryNotes'">{{displayCurrency(useSum().getCreatedDocumentSum(row,items))}}</span>
|
<span v-if="row.type !== 'deliveryNotes'">{{displayCurrency(useSum().getCreatedDocumentSum(row,items))}}</span>
|
||||||
|
-->
|
||||||
</template>
|
</template>
|
||||||
<template #amountOpen-data="{row}">
|
<template #amountOpen-data="{row}">
|
||||||
|
<!--
|
||||||
<span v-if="!['deliveryNotes','cancellationInvoices','quotes','confirmationOrders'].includes(row.type) && row.state !== 'Entwurf' && !useSum().getIsPaid(row,items) && !items.find(i => i.linkedDocument && i.linkedDocument.id === row.id) ">{{displayCurrency(useSum().getCreatedDocumentSum(row, items) - row.statementallocations.reduce((n,{amount}) => n + amount, 0))}}</span>
|
<span v-if="!['deliveryNotes','cancellationInvoices','quotes','confirmationOrders'].includes(row.type) && row.state !== 'Entwurf' && !useSum().getIsPaid(row,items) && !items.find(i => i.linkedDocument && i.linkedDocument.id === row.id) ">{{displayCurrency(useSum().getCreatedDocumentSum(row, items) - row.statementallocations.reduce((n,{amount}) => n + amount, 0))}}</span>
|
||||||
|
-->
|
||||||
</template>
|
</template>
|
||||||
</UTable>
|
</UTable>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ definePageMeta({
|
|||||||
|
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const platformIsNative = useCapacitor().getIsNative()
|
||||||
|
|
||||||
|
|
||||||
const doLogin = async (data:any) => {
|
const doLogin = async (data:any) => {
|
||||||
@@ -14,10 +15,10 @@ 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"})
|
||||||
if(useCapacitor().getIsNative()) {
|
if(platformIsNative) {
|
||||||
return navigateTo("/mobile")
|
await router.push("/mobile")
|
||||||
} else {
|
} else {
|
||||||
return navigateTo("/")
|
await router.push("/")
|
||||||
}
|
}
|
||||||
} 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"})
|
||||||
@@ -26,7 +27,7 @@ const doLogin = async (data:any) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UCard class="max-w-sm w-full mx-auto mt-5">
|
<UCard class="max-w-sm w-full mx-auto mt-5" v-if="!platformIsNative">
|
||||||
|
|
||||||
<UColorModeImage
|
<UColorModeImage
|
||||||
light="/Logo.png"
|
light="/Logo.png"
|
||||||
@@ -67,4 +68,35 @@ const doLogin = async (data:any) => {
|
|||||||
</template>
|
</template>
|
||||||
</UAuthForm>
|
</UAuthForm>
|
||||||
</UCard>
|
</UCard>
|
||||||
|
<div v-else class="mt-20 m-2 p-2">
|
||||||
|
<UColorModeImage
|
||||||
|
light="/Logo.png"
|
||||||
|
dark="/Logo_Dark.png"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UAuthForm
|
||||||
|
title="Login"
|
||||||
|
description="Geben Sie Ihre Anmeldedaten ein um Zugriff auf Ihren Account zu erhalten."
|
||||||
|
align="bottom"
|
||||||
|
:fields="[{
|
||||||
|
name: 'email',
|
||||||
|
type: 'text',
|
||||||
|
label: 'Email',
|
||||||
|
placeholder: 'Deine E-Mail Adresse'
|
||||||
|
}, {
|
||||||
|
name: 'password',
|
||||||
|
label: 'Passwort',
|
||||||
|
type: 'password',
|
||||||
|
placeholder: 'Dein Passwort'
|
||||||
|
}]"
|
||||||
|
:loading="false"
|
||||||
|
@submit="doLogin"
|
||||||
|
:submit-button="{label: 'Weiter'}"
|
||||||
|
divider="oder"
|
||||||
|
>
|
||||||
|
<template #password-hint>
|
||||||
|
<NuxtLink to="/password-reset" class="text-primary font-medium">Passwort vergessen?</NuxtLink>
|
||||||
|
</template>
|
||||||
|
</UAuthForm>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -1,13 +1,33 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
|
||||||
import DisplayPresentProfiles from "~/components/noAutoLoad/displayPresentProfiles.vue";
|
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: 'mobile'
|
layout: 'mobile'
|
||||||
})
|
})
|
||||||
|
|
||||||
//const profileStore = useProfileStore()
|
const auth = useAuthStore()
|
||||||
|
|
||||||
|
const pinnedLinks = computed(() => {
|
||||||
|
return (auth.profile?.pinned_on_navigation || [])
|
||||||
|
.map((pin) => {
|
||||||
|
if (pin.type === "external") {
|
||||||
|
return {
|
||||||
|
label: pin.label,
|
||||||
|
to: pin.link,
|
||||||
|
icon: pin.icon,
|
||||||
|
external: true,
|
||||||
|
}
|
||||||
|
} else if (pin.type === "standardEntity") {
|
||||||
|
return {
|
||||||
|
label: pin.label,
|
||||||
|
to: `/standardEntity/${pin.datatype}/show/${pin.id}`,
|
||||||
|
icon: pin.icon,
|
||||||
|
external: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -42,6 +62,7 @@ definePageMeta({
|
|||||||
>
|
>
|
||||||
<display-projects-in-phases/>
|
<display-projects-in-phases/>
|
||||||
</UDashboardCard>
|
</UDashboardCard>
|
||||||
|
<display-pinnend-links :links="pinnedLinks"/>
|
||||||
|
|
||||||
</UPageGrid>
|
</UPageGrid>
|
||||||
</UDashboardPanelContent>
|
</UDashboardPanelContent>
|
||||||
|
|||||||
@@ -13,30 +13,19 @@ const auth = useAuthStore()
|
|||||||
<UDivider class="mb-3">Weiteres</UDivider>
|
<UDivider class="mb-3">Weiteres</UDivider>
|
||||||
<UButton
|
<UButton
|
||||||
class="w-full my-1"
|
class="w-full my-1"
|
||||||
to="/times"
|
to="/staff/time"
|
||||||
icon="i-heroicons-clock"
|
icon="i-heroicons-clock"
|
||||||
>
|
>
|
||||||
Zeiten
|
Zeiten
|
||||||
</UButton>
|
</UButton>
|
||||||
<UButton
|
<!-- <UButton
|
||||||
class="w-full my-1"
|
class="w-full my-1"
|
||||||
to="/standardEntity/absencerequests"
|
to="/standardEntity/absencerequests"
|
||||||
icon="i-heroicons-document-text"
|
icon="i-heroicons-document-text"
|
||||||
>
|
>
|
||||||
Abwesenheiten
|
Abwesenheiten
|
||||||
</UButton>
|
|
||||||
|
|
||||||
<UButton
|
|
||||||
class="w-full my-1"
|
|
||||||
to="/workingtimes"
|
|
||||||
icon="i-heroicons-clock"
|
|
||||||
>
|
|
||||||
Anwesenheiten
|
|
||||||
</UButton>
|
|
||||||
<!-- <UButton
|
|
||||||
class="w-full my-1">
|
|
||||||
Kalender
|
|
||||||
</UButton>-->
|
</UButton>-->
|
||||||
|
|
||||||
<UButton
|
<UButton
|
||||||
class="w-full my-1"
|
class="w-full my-1"
|
||||||
to="/standardEntity/customers"
|
to="/standardEntity/customers"
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ const workingtimes = ref([])
|
|||||||
const absencerequests = ref([])
|
const absencerequests = ref([])
|
||||||
const workingTimeInfo = ref(null)
|
const workingTimeInfo = ref(null)
|
||||||
|
|
||||||
|
const platformIsNative = ref(useCapacitor().getIsNative())
|
||||||
|
|
||||||
const selectedPresetRange = ref("Dieser Monat bis heute")
|
const selectedPresetRange = ref("Dieser Monat bis heute")
|
||||||
const selectedStartDay = ref("")
|
const selectedStartDay = ref("")
|
||||||
const selectedEndDay = ref("")
|
const selectedEndDay = ref("")
|
||||||
@@ -63,6 +65,7 @@ async function setupPage() {
|
|||||||
profile.value = (await useNuxtApp().$api("/api/tenant/profiles")).data.find(i => i.user_id === route.params.id)
|
profile.value = (await useNuxtApp().$api("/api/tenant/profiles")).data.find(i => i.user_id === route.params.id)
|
||||||
|
|
||||||
console.log(profile.value)
|
console.log(profile.value)
|
||||||
|
setPageLayout(platformIsNative.value ? 'mobile' : 'default')
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,6 +122,7 @@ changeRange()
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<template v-if="!platformIsNative">
|
||||||
<UDashboardNavbar :ui="{ center: 'flex items-stretch gap-1.5 min-w-0' }">
|
<UDashboardNavbar :ui="{ center: 'flex items-stretch gap-1.5 min-w-0' }">
|
||||||
<template #left>
|
<template #left>
|
||||||
<UButton
|
<UButton
|
||||||
@@ -268,3 +272,156 @@ changeRange()
|
|||||||
</UTabs>
|
</UTabs>
|
||||||
</UDashboardPanelContent>
|
</UDashboardPanelContent>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- ====================== -->
|
||||||
|
<!-- 📱 MOBILE ANSICHT -->
|
||||||
|
<!-- ====================== -->
|
||||||
|
<template v-else>
|
||||||
|
|
||||||
|
<!-- 🔙 Navigation -->
|
||||||
|
<UDashboardNavbar title="Auswertung">
|
||||||
|
<template #toggle><div></div></template>
|
||||||
|
<template #left>
|
||||||
|
<UButton
|
||||||
|
icon="i-heroicons-chevron-left"
|
||||||
|
variant="ghost"
|
||||||
|
@click="router.push('/staff/time')"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</UDashboardNavbar>
|
||||||
|
|
||||||
|
<!-- 📌 Mobile Zeitraumwahl -->
|
||||||
|
<div class="p-4 space-y-4 border-b bg-white dark:bg-gray-900">
|
||||||
|
<!-- Predefined Ranges -->
|
||||||
|
<USelectMenu
|
||||||
|
v-model="selectedPresetRange"
|
||||||
|
:options="[
|
||||||
|
'Dieser Monat bis heute',
|
||||||
|
'Diese Woche',
|
||||||
|
'Dieser Monat',
|
||||||
|
'Dieses Jahr',
|
||||||
|
'Letzte Woche',
|
||||||
|
'Letzter Monat',
|
||||||
|
'Letztes Jahr'
|
||||||
|
]"
|
||||||
|
@change="changeRange"
|
||||||
|
placeholder="Zeitraum wählen"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Start/End Datum -->
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<p class="text-xs text-gray-500 mb-1">Start</p>
|
||||||
|
<UPopover :popper="{ placement: 'bottom-start' }">
|
||||||
|
<UButton
|
||||||
|
icon="i-heroicons-calendar"
|
||||||
|
class="w-full"
|
||||||
|
:label="$dayjs(selectedStartDay).format('DD.MM.YYYY')"
|
||||||
|
/>
|
||||||
|
<template #panel>
|
||||||
|
<LazyDatePicker v-model="selectedStartDay" @close="loadWorkingTimeInfo" />
|
||||||
|
</template>
|
||||||
|
</UPopover>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p class="text-xs text-gray-500 mb-1">Ende</p>
|
||||||
|
<UPopover :popper="{ placement: 'bottom-start' }">
|
||||||
|
<UButton
|
||||||
|
icon="i-heroicons-calendar"
|
||||||
|
class="w-full"
|
||||||
|
:label="$dayjs(selectedEndDay).format('DD.MM.YYYY')"
|
||||||
|
/>
|
||||||
|
<template #panel>
|
||||||
|
<LazyDatePicker v-model="selectedEndDay" @close="loadWorkingTimeInfo" />
|
||||||
|
</template>
|
||||||
|
</UPopover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 📑 Mobile Tabs -->
|
||||||
|
<UTabs
|
||||||
|
:items="[{ label: 'Information' }, { label: 'Bericht' }]"
|
||||||
|
v-model="openTab"
|
||||||
|
@change="onTabChange"
|
||||||
|
class="mt-3 mx-3"
|
||||||
|
>
|
||||||
|
<template #item="{ item }">
|
||||||
|
|
||||||
|
<!-- ====================== -->
|
||||||
|
<!-- TAB 1 — INFORMATION -->
|
||||||
|
<!-- ====================== -->
|
||||||
|
<div v-if="item.label === 'Information'" class="space-y-4">
|
||||||
|
|
||||||
|
<!-- Summary Card -->
|
||||||
|
<UCard v-if="workingTimeInfo" class="mt-3">
|
||||||
|
<template #header>
|
||||||
|
<h3 class="text-base font-semibold">Zusammenfassung</h3>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="space-y-2 text-sm">
|
||||||
|
<p>Eingereicht: <b>{{ formatMinutesToHHMM(workingTimeInfo.sumWorkingMinutesEingereicht) }}</b></p>
|
||||||
|
<p>Genehmigt: <b>{{ formatMinutesToHHMM(workingTimeInfo.sumWorkingMinutesApproved) }}</b></p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Feiertagsausgleich:
|
||||||
|
<b>{{ formatMinutesToHHMM(workingTimeInfo.sumWorkingMinutesRecreationDays) }}</b>
|
||||||
|
/ {{ workingTimeInfo.sumRecreationDays }} Tage
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Urlaubs-/Berufsschule:
|
||||||
|
<b>{{ formatMinutesToHHMM(workingTimeInfo.sumWorkingMinutesVacationDays) }}</b>
|
||||||
|
/ {{ workingTimeInfo.sumVacationDays }} Tage
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Krankheitsausgleich:
|
||||||
|
<b>{{ formatMinutesToHHMM(workingTimeInfo.sumWorkingMinutesSickDays) }}</b>
|
||||||
|
/ {{ workingTimeInfo.sumSickDays }} Tage
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>Soll: <b>{{ formatMinutesToHHMM(workingTimeInfo.timeSpanWorkingMinutes) }}</b></p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Inoffizielles Saldo:
|
||||||
|
<b>{{ (workingTimeInfo.saldoInOfficial >= 0 ? '+' : '-') + formatMinutesToHHMM(Math.abs(workingTimeInfo.saldoInOfficial)) }}</b>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Saldo:
|
||||||
|
<b>{{ (workingTimeInfo.saldo >= 0 ? '+' : '-') + formatMinutesToHHMM(Math.abs(workingTimeInfo.saldo)) }}</b>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ====================== -->
|
||||||
|
<!-- TAB 2 — BERICHT -->
|
||||||
|
<!-- ====================== -->
|
||||||
|
<div v-else-if="item.label === 'Bericht'">
|
||||||
|
<UButton
|
||||||
|
v-if="uri && !fileSaved"
|
||||||
|
icon="i-mdi-content-save"
|
||||||
|
color="primary"
|
||||||
|
class="w-full mb-3"
|
||||||
|
@click="saveFile"
|
||||||
|
>
|
||||||
|
Bericht speichern
|
||||||
|
</UButton>
|
||||||
|
|
||||||
|
<PDFViewer
|
||||||
|
v-if="showDocument"
|
||||||
|
:uri="uri"
|
||||||
|
location="show_time_evaluation"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UTabs>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|||||||
@@ -1,20 +1,28 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useStaffTime } from '~/composables/useStaffTime'
|
import { useStaffTime } from '~/composables/useStaffTime'
|
||||||
import { useAuthStore } from '~/stores/auth'
|
import { useAuthStore } from '~/stores/auth'
|
||||||
|
import FloatingActionButton from "~/components/mobile/FloatingActionButton.vue";
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
layout: "default",
|
||||||
|
})
|
||||||
|
|
||||||
const { list, start, stop, submit, approve } = useStaffTime()
|
const { list, start, stop, submit, approve } = useStaffTime()
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
// MOBILE DETECTION
|
||||||
|
const platformIsNative = useCapacitor().getIsNative()
|
||||||
|
// LIST + ACTIVE
|
||||||
const entries = ref([])
|
const entries = ref([])
|
||||||
const active = computed(() => entries.value.find(e => !e.stopped_at && e.user_id === auth.user.id))
|
const active = computed(() => entries.value.find(e => !e.stopped_at && e.user_id === auth.user.id))
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const showModal = ref(false)
|
const showModal = ref(false)
|
||||||
const editEntry = ref(null)
|
const editEntry = ref(null)
|
||||||
|
|
||||||
// 👥 Nutzer-Filter (nur für Berechtigte)
|
// 👥 Nutzer-Filter (nur für Berechtigte)
|
||||||
const users = ref([])
|
const users = ref([])
|
||||||
const selectedUser = ref<string | null>(null)
|
const selectedUser = ref(platformIsNative ? auth.user.id : null)
|
||||||
|
|
||||||
const canViewAll = computed(() => auth.permissions.includes('staff.time.read_all'))
|
const canViewAll = computed(() => auth.permissions.includes('staff.time.read_all'))
|
||||||
|
|
||||||
@@ -25,15 +33,18 @@ async function loadUsers() {
|
|||||||
users.value = res
|
users.value = res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// LOAD ENTRIES (only own entries on mobile)
|
||||||
async function load() {
|
async function load() {
|
||||||
entries.value = await list(
|
entries.value = await list(
|
||||||
canViewAll.value && selectedUser.value ? { user_id: selectedUser.value } : undefined
|
canViewAll.value && selectedUser.value ? { user_id: selectedUser.value } : undefined
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleStart() {
|
async function handleStart() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
await start('Arbeitszeit gestartet')
|
await start("Arbeitszeit gestartet")
|
||||||
await load()
|
await load()
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@@ -61,16 +72,24 @@ async function handleApprove(entry: any) {
|
|||||||
await load()
|
await load()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadUsers()
|
|
||||||
await load()
|
await load()
|
||||||
|
await loadUsers()
|
||||||
|
setPageLayout(platformIsNative ? 'mobile' : 'default')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<!-- ============================= -->
|
||||||
|
<!-- DESKTOP VERSION -->
|
||||||
|
<!-- ============================= -->
|
||||||
|
<template v-if="!platformIsNative">
|
||||||
<UDashboardNavbar title="Zeiterfassung" :badge="entries.length" />
|
<UDashboardNavbar title="Zeiterfassung" :badge="entries.length" />
|
||||||
|
|
||||||
<!-- TOOLBAR -->
|
|
||||||
<UDashboardToolbar>
|
<UDashboardToolbar>
|
||||||
<template #left>
|
<template #left>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
@@ -142,7 +161,7 @@ onMounted(async () => {
|
|||||||
</template>
|
</template>
|
||||||
</UDashboardToolbar>
|
</UDashboardToolbar>
|
||||||
|
|
||||||
<!-- TABELLE -->
|
|
||||||
<UDashboardPanelContent>
|
<UDashboardPanelContent>
|
||||||
<UTable
|
<UTable
|
||||||
:rows="entries"
|
:rows="entries"
|
||||||
@@ -153,8 +172,6 @@ onMounted(async () => {
|
|||||||
{ key: 'stopped_at', label: 'Ende' },
|
{ key: 'stopped_at', label: 'Ende' },
|
||||||
{ key: 'duration_minutes', label: 'Dauer' },
|
{ key: 'duration_minutes', label: 'Dauer' },
|
||||||
{ key: 'description', label: 'Beschreibung' },
|
{ key: 'description', label: 'Beschreibung' },
|
||||||
...(canViewAll ? [{ key: 'user_name', label: 'Benutzer' }] : []),
|
|
||||||
|
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<template #state-data="{ row }">
|
<template #state-data="{ row }">
|
||||||
@@ -162,59 +179,184 @@ onMounted(async () => {
|
|||||||
<span v-else-if="row.state === 'submitted'" class="text-cyan-500">Eingereicht</span>
|
<span v-else-if="row.state === 'submitted'" class="text-cyan-500">Eingereicht</span>
|
||||||
<span v-else-if="row.state === 'draft'" class="text-red-500">Entwurf</span>
|
<span v-else-if="row.state === 'draft'" class="text-red-500">Entwurf</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #started_at-data="{ row }">
|
<template #started_at-data="{ row }">
|
||||||
{{ useNuxtApp().$dayjs(row.started_at).format('DD.MM.YY HH:mm') }}
|
{{ useNuxtApp().$dayjs(row.started_at).format("DD.MM.YY HH:mm") }}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #stopped_at-data="{ row }">
|
<template #stopped_at-data="{ row }">
|
||||||
<span v-if="row.stopped_at">
|
<span v-if="row.stopped_at">
|
||||||
{{ useNuxtApp().$dayjs(row.stopped_at).format('DD.MM.YY HH:mm') }}
|
{{ useNuxtApp().$dayjs(row.stopped_at).format("DD.MM.YY HH:mm") }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else class="text-primary-500 font-medium">läuft...</span>
|
<span v-else class="text-primary-500 font-medium">läuft...</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #duration_minutes-data="{ row }">
|
<template #duration_minutes-data="{ row }">
|
||||||
{{ row.duration_minutes ? useFormatDuration(row.duration_minutes) : '-' }}
|
{{ row.duration_minutes ? useFormatDuration(row.duration_minutes) : "-" }}
|
||||||
</template>
|
|
||||||
<template #user_name-data="{ row }">
|
|
||||||
{{ row.user_id ? users.find(i => i.user_id === row.user_id).full_name : '-' }}
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #actions-data="{ row }">
|
<template #actions-data="{ row }">
|
||||||
<UTooltip
|
<UTooltip text="Zeit genehmigen" v-if="row.state === 'submitted'">
|
||||||
text="Zeit genehmigen"
|
|
||||||
v-if="row.state === 'submitted'"
|
|
||||||
>
|
|
||||||
<UButton
|
<UButton
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
icon="i-heroicons-check-circle"
|
icon="i-heroicons-check-circle"
|
||||||
@click="handleApprove(row)"
|
@click="handleApprove(row)"
|
||||||
|
|
||||||
/>
|
/>
|
||||||
</UTooltip>
|
</UTooltip>
|
||||||
<UTooltip
|
<UTooltip text="Zeit einreichen" v-if="row.state === 'draft'">
|
||||||
text="Zeit einreichen"
|
|
||||||
v-if="row.state === 'draft'"
|
|
||||||
>
|
|
||||||
<UButton
|
<UButton
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
icon="i-heroicons-arrow-right-end-on-rectangle"
|
icon="i-heroicons-arrow-right-end-on-rectangle"
|
||||||
@click="handleSubmit(row)"
|
@click="handleSubmit(row)"
|
||||||
|
|
||||||
/>
|
/>
|
||||||
</UTooltip>
|
</UTooltip>
|
||||||
<UTooltip
|
<UTooltip text="Zeit bearbeiten" v-if="row.state === 'draft'">
|
||||||
text="Zeit bearbeiten"
|
|
||||||
v-if="row.state === 'draft'"
|
|
||||||
>
|
|
||||||
<UButton
|
<UButton
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
icon="i-heroicons-pencil-square"
|
icon="i-heroicons-pencil-square"
|
||||||
@click="handleEdit(row)"
|
@click="handleEdit(row)"
|
||||||
/>
|
/>
|
||||||
</UTooltip>
|
</UTooltip>
|
||||||
|
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
</UTable>
|
</UTable>
|
||||||
</UDashboardPanelContent>
|
</UDashboardPanelContent>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ============================= -->
|
||||||
|
<!-- MOBILE VERSION -->
|
||||||
|
<!-- ============================= -->
|
||||||
|
<template v-else>
|
||||||
|
<UDashboardNavbar title="Zeiterfassung" />
|
||||||
|
|
||||||
|
<div class="relative flex flex-col h-[100dvh] overflow-hidden">
|
||||||
|
|
||||||
|
<!-- 🔥 FIXED ACTIVE TIMER -->
|
||||||
|
<div class="p-4 bg-white dark:bg-gray-900 border-b sticky top-0 z-20 shadow-sm">
|
||||||
|
<UCard class="p-3">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p class="text-gray-500 text-sm">Aktive Zeit</p>
|
||||||
|
|
||||||
|
<p v-if="active" class="text-primary-600 font-semibold">
|
||||||
|
Läuft seit {{ useNuxtApp().$dayjs(active.started_at).format('HH:mm') }}
|
||||||
|
</p>
|
||||||
|
<p v-else class="text-gray-600">Keine aktive Zeit</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UButton
|
||||||
|
v-if="active"
|
||||||
|
color="red"
|
||||||
|
icon="i-heroicons-stop"
|
||||||
|
:loading="loading"
|
||||||
|
@click="handleStop"
|
||||||
|
/>
|
||||||
|
<UButton
|
||||||
|
v-else
|
||||||
|
color="green"
|
||||||
|
icon="i-heroicons-play"
|
||||||
|
:loading="loading"
|
||||||
|
@click="handleStart"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="px-3 mt-3">
|
||||||
|
<UButton
|
||||||
|
color="gray"
|
||||||
|
icon="i-heroicons-chart-bar"
|
||||||
|
label="Eigene Auswertung"
|
||||||
|
class="w-full"
|
||||||
|
variant="soft"
|
||||||
|
@click="router.push(`/staff/time/${auth.user.id}/evaluate`)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 📜 SCROLLABLE CONTENT -->
|
||||||
|
<UDashboardPanelContent class="flex-1 overflow-y-auto p-3 space-y-4 pb-24">
|
||||||
|
|
||||||
|
<!-- ZEIT-CARDS -->
|
||||||
|
<UCard
|
||||||
|
v-for="row in entries"
|
||||||
|
:key="row.id"
|
||||||
|
class="p-4 border rounded-xl active:scale-[0.98] transition cursor-pointer"
|
||||||
|
@click="handleEdit(row)"
|
||||||
|
>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<p class="font-semibold">
|
||||||
|
{{ row.description || 'Keine Beschreibung' }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<UBadge
|
||||||
|
:color="{
|
||||||
|
approved: 'primary',
|
||||||
|
submitted: 'cyan',
|
||||||
|
draft: 'red'
|
||||||
|
}[row.state]"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
{
|
||||||
|
approved: 'Genehmigt',
|
||||||
|
submitted: 'Eingereicht',
|
||||||
|
draft: 'Entwurf'
|
||||||
|
}[row.state] || row.state
|
||||||
|
}}
|
||||||
|
</UBadge>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-sm text-gray-500 mt-1">
|
||||||
|
Start: {{ useNuxtApp().$dayjs(row.started_at).format('DD.MM.YY HH:mm') }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="text-sm text-gray-500">
|
||||||
|
Ende:
|
||||||
|
<span v-if="row.stopped_at">
|
||||||
|
{{ useNuxtApp().$dayjs(row.stopped_at).format('DD.MM.YY HH:mm') }}
|
||||||
|
</span>
|
||||||
|
<span v-else class="text-primary-500 font-medium">läuft...</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="text-sm text-gray-500">
|
||||||
|
Dauer:
|
||||||
|
{{ row.duration_minutes ? useFormatDuration(row.duration_minutes) : '-' }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- ACTION-BUTTONS -->
|
||||||
|
<div class="flex gap-2 mt-3">
|
||||||
|
<UButton
|
||||||
|
v-if="row.state === 'draft'"
|
||||||
|
color="gray"
|
||||||
|
icon="i-heroicons-arrow-right-end-on-rectangle"
|
||||||
|
label="Einreichen"
|
||||||
|
variant="soft"
|
||||||
|
@click.stop="handleSubmit(row)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- <UButton
|
||||||
|
v-if="row.state === 'submitted'"
|
||||||
|
color="primary"
|
||||||
|
icon="i-heroicons-check"
|
||||||
|
label="Genehmigen"
|
||||||
|
variant="soft"
|
||||||
|
@click.stop="handleApprove(row)"
|
||||||
|
/>-->
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
</UDashboardPanelContent>
|
||||||
|
|
||||||
|
<!-- ➕ FLOATING ACTION BUTTON -->
|
||||||
|
<FloatingActionButton
|
||||||
|
icon="i-heroicons-plus"
|
||||||
|
class="!fixed bottom-6 right-6 z-50"
|
||||||
|
color="primary"
|
||||||
|
@click="() => { editEntry = null; showModal = true }"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- MODAL -->
|
<!-- MODAL -->
|
||||||
<StaffTimeEntryModal v-model="showModal" :entry="editEntry" @saved="load" />
|
<StaffTimeEntryModal v-model="showModal" :entry="editEntry" @saved="load" />
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ 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().getIsNative() ? "mobile" : "default"
|
||||||
|
|
||||||
|
|
||||||
const dataType = dataStore.dataTypes[route.params.type]
|
const dataType = dataStore.dataTypes[route.params.type]
|
||||||
|
|||||||
@@ -3,9 +3,12 @@ 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 EntityTableMobile from "~/components/EntityTableMobile.vue";
|
import EntityTableMobile from "~/components/EntityTableMobile.vue";
|
||||||
|
import {setPageLayout} from "#app";
|
||||||
|
|
||||||
const { has } = usePermission()
|
const { has } = usePermission()
|
||||||
|
|
||||||
|
const platformIsNative = useCapacitor().getIsNative()
|
||||||
|
|
||||||
defineShortcuts({
|
defineShortcuts({
|
||||||
'/': () => {
|
'/': () => {
|
||||||
//console.log(searchinput)
|
//console.log(searchinput)
|
||||||
@@ -67,8 +70,31 @@ const sort = ref({
|
|||||||
|
|
||||||
const columnsToFilter = ref({})
|
const columnsToFilter = ref({})
|
||||||
|
|
||||||
|
const showMobileFilter = ref(false)
|
||||||
|
|
||||||
|
|
||||||
//Functions
|
//Functions
|
||||||
|
|
||||||
|
function resetMobileFilters() {
|
||||||
|
if (!itemsMeta.value?.distinctValues) return
|
||||||
|
|
||||||
|
Object.keys(itemsMeta.value.distinctValues).forEach(key => {
|
||||||
|
columnsToFilter.value[key] = [...itemsMeta.value.distinctValues[key]]
|
||||||
|
})
|
||||||
|
|
||||||
|
showMobileFilter.value = false
|
||||||
|
setupPage()
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyMobileFilters() {
|
||||||
|
Object.keys(columnsToFilter.value).forEach(key => {
|
||||||
|
tempStore.modifyFilter(type, key, columnsToFilter.value[key])
|
||||||
|
})
|
||||||
|
|
||||||
|
showMobileFilter.value = false
|
||||||
|
setupPage()
|
||||||
|
}
|
||||||
|
|
||||||
const clearSearchString = () => {
|
const clearSearchString = () => {
|
||||||
tempStore.clearSearchString(type)
|
tempStore.clearSearchString(type)
|
||||||
searchString.value = ''
|
searchString.value = ''
|
||||||
@@ -77,13 +103,14 @@ const clearSearchString = () => {
|
|||||||
|
|
||||||
const performSearch = async () => {
|
const performSearch = async () => {
|
||||||
tempStore.modifySearchString(type,searchString)
|
tempStore.modifySearchString(type,searchString)
|
||||||
|
changePage(1,true)
|
||||||
setupPage()
|
setupPage()
|
||||||
}
|
}
|
||||||
|
|
||||||
const changePage = (number) => {
|
const changePage = (number, noSetup = false) => {
|
||||||
page.value = number
|
page.value = number
|
||||||
tempStore.modifyPages(type, number)
|
tempStore.modifyPages(type, number)
|
||||||
setupPage()
|
if(!noSetup) setupPage()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -99,10 +126,23 @@ const changeSort = (column) => {
|
|||||||
changePage(1)
|
changePage(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isFiltered = computed(() => {
|
||||||
|
if (!itemsMeta.value?.distinctValues) return false
|
||||||
|
|
||||||
|
return Object.keys(columnsToFilter.value).some(key => {
|
||||||
|
const allValues = itemsMeta.value.distinctValues[key]
|
||||||
|
const selected = columnsToFilter.value[key]
|
||||||
|
if (!allValues || !selected) return false
|
||||||
|
return selected.length !== allValues.length
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
//SETUP
|
//SETUP
|
||||||
|
|
||||||
const setupPage = async () => {
|
const setupPage = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
setPageLayout(platformIsNative ? "mobile" : "default")
|
||||||
|
|
||||||
|
|
||||||
const filters = {
|
const filters = {
|
||||||
archived:false
|
archived:false
|
||||||
@@ -160,17 +200,11 @@ const handleFilterChange = async (action,column) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- <FloatingActionButton
|
|
||||||
:label="`+ ${dataType.labelSingle}`"
|
|
||||||
variant="outline"
|
|
||||||
v-if="platform === 'mobile'"
|
|
||||||
@click="router.push(`/standardEntity/${type}/create`)"
|
|
||||||
/>-->
|
|
||||||
<UDashboardNavbar :title="dataType.label" :badge="itemsMeta.total">
|
<UDashboardNavbar :title="dataType.label" :badge="itemsMeta.total">
|
||||||
<template #toggle>
|
<template #toggle>
|
||||||
<div v-if="platform === 'mobile'"></div>
|
<div v-if="platformIsNative"></div>
|
||||||
</template>
|
</template>
|
||||||
<template #right>
|
<template #right v-if="!platformIsNative">
|
||||||
<UTooltip :text="`${dataType.label} durchsuchen`">
|
<UTooltip :text="`${dataType.label} durchsuchen`">
|
||||||
<UInput
|
<UInput
|
||||||
id="searchinput"
|
id="searchinput"
|
||||||
@@ -211,58 +245,17 @@ const handleFilterChange = async (action,column) => {
|
|||||||
</UDashboardNavbar>
|
</UDashboardNavbar>
|
||||||
|
|
||||||
|
|
||||||
<UDashboardToolbar>
|
<UDashboardToolbar v-if="!platformIsNative">
|
||||||
<template #left>
|
<template #left>
|
||||||
<UTooltip :text="`${dataType.label} pro Seite`">
|
<UTooltip :text="`${dataType.label} pro Seite`">
|
||||||
<USelectMenu
|
<USelectMenu
|
||||||
:options="[10,15,25,50,100,250]"
|
:options="[{value:10},{value:15, disabled: itemsMeta.total < 15},{value:25, disabled: itemsMeta.total < 25},{value:50, disabled: itemsMeta.total < 50},{value:100, disabled: itemsMeta.total < 100},{value:250, disabled: itemsMeta.total < 250}]"
|
||||||
v-model="pageLimit"
|
v-model="pageLimit"
|
||||||
|
value-attribute="value"
|
||||||
|
option-attribute="value"
|
||||||
@change="setupPage"
|
@change="setupPage"
|
||||||
/>
|
/>
|
||||||
</UTooltip>
|
</UTooltip>
|
||||||
<!-- <UTooltip text="Erste Seite">
|
|
||||||
<UButton
|
|
||||||
variant="outline"
|
|
||||||
@click="changePage(1)"
|
|
||||||
icon="i-heroicons-chevron-double-left"
|
|
||||||
:disabled="page <= 1"
|
|
||||||
/>
|
|
||||||
</UTooltip>
|
|
||||||
<UTooltip text="Eine Seite nach vorne">
|
|
||||||
<UButton
|
|
||||||
variant="outline"
|
|
||||||
@click="changePage(page-1)"
|
|
||||||
icon="i-heroicons-chevron-left"
|
|
||||||
:disabled="page <= 1"
|
|
||||||
/>
|
|
||||||
</UTooltip>
|
|
||||||
<UTooltip
|
|
||||||
v-for="pageNumber in itemsMeta.totalPages"
|
|
||||||
:text="`Zu Seite ${pageNumber} wechseln`"
|
|
||||||
>
|
|
||||||
<UButton
|
|
||||||
:variant="page === pageNumber ? 'solid' : 'outline'"
|
|
||||||
@click="changePage(pageNumber)"
|
|
||||||
>
|
|
||||||
{{pageNumber}}
|
|
||||||
</UButton>
|
|
||||||
</UTooltip>
|
|
||||||
<UTooltip text="Eine Seite nach hinten">
|
|
||||||
<UButton
|
|
||||||
variant="outline"
|
|
||||||
@click="changePage(page+1)"
|
|
||||||
icon="i-heroicons-chevron-right"
|
|
||||||
:disabled="page >= itemsMeta.totalPages"
|
|
||||||
/>
|
|
||||||
</UTooltip>
|
|
||||||
<UTooltip text="Letzte Seite">
|
|
||||||
<UButton
|
|
||||||
variant="outline"
|
|
||||||
@click="changePage(itemsMeta.totalPages)"
|
|
||||||
icon="i-heroicons-chevron-double-right"
|
|
||||||
:disabled="page >= itemsMeta.totalPages"
|
|
||||||
/>
|
|
||||||
</UTooltip>-->
|
|
||||||
<UPagination
|
<UPagination
|
||||||
v-if="initialSetupDone && items.length > 0"
|
v-if="initialSetupDone && items.length > 0"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
@@ -299,6 +292,7 @@ const handleFilterChange = async (action,column) => {
|
|||||||
</template>
|
</template>
|
||||||
</UDashboardToolbar>
|
</UDashboardToolbar>
|
||||||
|
|
||||||
|
<div v-if="!platformIsNative">
|
||||||
<UTable
|
<UTable
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:loading-state="{ icon: 'i-heroicons-arrow-path-20-solid', label: 'Loading...' }"
|
:loading-state="{ icon: 'i-heroicons-arrow-path-20-solid', label: 'Loading...' }"
|
||||||
@@ -451,6 +445,181 @@ const handleFilterChange = async (action,column) => {
|
|||||||
|
|
||||||
</UCard>
|
</UCard>
|
||||||
<UProgress v-else animation="carousel" class="w-3/4 mx-auto mt-5"></UProgress>
|
<UProgress v-else animation="carousel" class="w-3/4 mx-auto mt-5"></UProgress>
|
||||||
|
</div>
|
||||||
|
<div v-else class="relative flex flex-col h-[calc(100dvh-80px)]">
|
||||||
|
|
||||||
|
<!-- Mobile Searchbar (sticky top) -->
|
||||||
|
<div class="p-2 bg-white dark:bg-gray-900 border-b sticky top-0 z-20">
|
||||||
|
<InputGroup>
|
||||||
|
<UInput
|
||||||
|
v-model="searchString"
|
||||||
|
icon="i-heroicons-magnifying-glass"
|
||||||
|
placeholder="Suche..."
|
||||||
|
@keyup="performSearch"
|
||||||
|
@change="performSearch"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
<UButton
|
||||||
|
v-if="searchString.length > 0"
|
||||||
|
icon="i-heroicons-x-mark"
|
||||||
|
variant="ghost"
|
||||||
|
color="rose"
|
||||||
|
@click="clearSearchString()"
|
||||||
|
|
||||||
|
/>
|
||||||
|
<UButton
|
||||||
|
icon="i-heroicons-funnel"
|
||||||
|
variant="ghost"
|
||||||
|
:color="isFiltered ? 'primary' : 'gray'"
|
||||||
|
@click="showMobileFilter = true"
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Scroll Area -->
|
||||||
|
<UDashboardPanelContent class="flex-1 overflow-y-auto px-2 py-3 space-y-3 pb-[calc(8vh+env(safe-area-inset-bottom))] mobile-scroll-area">
|
||||||
|
|
||||||
|
<UCard
|
||||||
|
v-for="item in items"
|
||||||
|
:key="item.id"
|
||||||
|
class="p-4 rounded-xl shadow-sm border cursor-pointer active:scale-[0.98] transition"
|
||||||
|
@click="router.push(`/standardEntity/${type}/show/${item.id}`)"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between mb-1">
|
||||||
|
<p class="text-base font-semibold truncate text-primary-600">
|
||||||
|
{{
|
||||||
|
dataType.templateColumns.find(i => i.title)?.key
|
||||||
|
? item[dataType.templateColumns.find(i => i.title).key]
|
||||||
|
: null
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<UIcon
|
||||||
|
name="i-heroicons-chevron-right-20-solid"
|
||||||
|
class="text-gray-400 w-5 h-5 flex-shrink-0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-sm text-gray-500 truncate">
|
||||||
|
{{ dataType.numberRangeHolder ? item[dataType.numberRangeHolder] : null }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-for="secondInfo in dataType.templateColumns.filter(i => i.secondInfo)"
|
||||||
|
:key="secondInfo.key"
|
||||||
|
class="text-sm text-gray-400 truncate"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
(secondInfo.secondInfoKey && item[secondInfo.key])
|
||||||
|
? item[secondInfo.key][secondInfo.secondInfoKey]
|
||||||
|
: item[secondInfo.key]
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="!loading && items.length > 0"
|
||||||
|
class="p-4 bg-white dark:bg-gray-900 border-t flex items-center justify-center mt-4 rounded-xl"
|
||||||
|
>
|
||||||
|
<UPagination
|
||||||
|
v-if="initialSetupDone && items.length > 0"
|
||||||
|
:disabled="loading"
|
||||||
|
v-model="page"
|
||||||
|
:page-count="pageLimit"
|
||||||
|
:total="itemsMeta.total"
|
||||||
|
@update:modelValue="(i) => changePage(i)"
|
||||||
|
show-first
|
||||||
|
show-last
|
||||||
|
:first-button="{ icon: 'i-heroicons-chevron-double-left', color: 'gray' }"
|
||||||
|
:last-button="{ icon: 'i-heroicons-chevron-double-right', trailing: true, color: 'gray' }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty -->
|
||||||
|
<UCard
|
||||||
|
v-if="!loading && items.length === 0"
|
||||||
|
class="mx-auto mt-10 p-6 text-center"
|
||||||
|
>
|
||||||
|
<UIcon name="i-heroicons-circle-stack-20-solid" class="mx-auto w-10 h-10 mb-3"/>
|
||||||
|
<p class="font-bold">Keine {{ dataType.label }} gefunden</p>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
<div v-if="loading" class="mt-5">
|
||||||
|
<UProgress animation="carousel" class="w-3/4 mx-auto"></UProgress>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</UDashboardPanelContent>
|
||||||
|
<!-- Mobile Filter Slideover -->
|
||||||
|
<USlideover
|
||||||
|
v-model="showMobileFilter"
|
||||||
|
side="bottom"
|
||||||
|
:ui="{ width: '100%', height: 'auto', maxHeight: '90vh' }"
|
||||||
|
class="pb-[env(safe-area-inset-bottom)]"
|
||||||
|
>
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="p-4 border-b flex items-center justify-between flex-shrink-0">
|
||||||
|
<h2 class="text-xl font-bold">Filter</h2>
|
||||||
|
<UButton
|
||||||
|
icon="i-heroicons-x-mark"
|
||||||
|
variant="ghost"
|
||||||
|
@click="showMobileFilter = false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Scrollable content -->
|
||||||
|
<div class="flex-1 overflow-y-auto p-4 space-y-6">
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-for="column in dataType.templateColumns.filter(c => c.distinct)"
|
||||||
|
:key="column.key"
|
||||||
|
class="space-y-2"
|
||||||
|
>
|
||||||
|
<p class="font-semibold">{{ column.label }}</p>
|
||||||
|
|
||||||
|
<USelectMenu
|
||||||
|
v-model="columnsToFilter[column.key]"
|
||||||
|
:options="itemsMeta?.distinctValues?.[column.key]"
|
||||||
|
multiple
|
||||||
|
searchable
|
||||||
|
:search-attributes="[column.key]"
|
||||||
|
placeholder="Auswählen…"
|
||||||
|
:ui-menu="{ width: '100%' }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer FIXED in card -->
|
||||||
|
<div
|
||||||
|
class="
|
||||||
|
flex justify-between gap-3
|
||||||
|
px-4 py-4 border-t flex-shrink-0
|
||||||
|
bg-white dark:bg-gray-900
|
||||||
|
rounded-b-2xl
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<UButton
|
||||||
|
color="rose"
|
||||||
|
variant="outline"
|
||||||
|
class="flex-1"
|
||||||
|
@click="resetMobileFilters"
|
||||||
|
>
|
||||||
|
Zurücksetzen
|
||||||
|
</UButton>
|
||||||
|
|
||||||
|
<UButton
|
||||||
|
color="primary"
|
||||||
|
class="flex-1"
|
||||||
|
@click="applyMobileFilters"
|
||||||
|
>
|
||||||
|
Anwenden
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</USlideover>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ export default defineNuxtPlugin(() => {
|
|||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
|
|
||||||
const api = $fetch.create({
|
const api = $fetch.create({
|
||||||
baseURL: config.public.apiBase,/*"http://192.168.1.227:3100" "https://backend.fedeo.io"*/
|
baseURL: config.public.apiBase,
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
async onRequest({options}) {
|
async onRequest({options}) {
|
||||||
// Token aus Cookie holen
|
// Token aus Cookie holen
|
||||||
let token: string | null | undefined = ""
|
let token: string | null | undefined = ""
|
||||||
if (await useCapacitor().getIsNative()) {
|
if (useCapacitor().getIsNative()) {
|
||||||
const {value} = await Preferences.get({key: 'token'});
|
const {value} = await Preferences.get({key: 'token'});
|
||||||
token = value
|
token = value
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -63,13 +63,18 @@ export const useAuthStore = defineStore("auth", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async login(email: string, password: string) {
|
async login(email: string, password: string) {
|
||||||
|
try {
|
||||||
console.log("Auth login")
|
console.log("Auth login")
|
||||||
const { token } = await useNuxtApp().$api("/auth/login", {
|
const { token } = await useNuxtApp().$api("/auth/login", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: { email, password }
|
body: { email, password }
|
||||||
})
|
})
|
||||||
console.log(token)
|
console.log("Token: " + token)
|
||||||
await this.fetchMe(token)
|
await this.fetchMe(token)
|
||||||
|
} catch (e) {
|
||||||
|
console.log("login error:" + e)
|
||||||
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async logout() {
|
async logout() {
|
||||||
@@ -105,6 +110,7 @@ export const useAuthStore = defineStore("auth", {
|
|||||||
jwt
|
jwt
|
||||||
}}
|
}}
|
||||||
})
|
})
|
||||||
|
console.log(me)
|
||||||
this.user = me.user
|
this.user = me.user
|
||||||
this.permissions = me.permissions
|
this.permissions = me.permissions
|
||||||
this.tenants = me.tenants
|
this.tenants = me.tenants
|
||||||
@@ -143,7 +149,7 @@ export const useAuthStore = defineStore("auth", {
|
|||||||
|
|
||||||
const {token} = res
|
const {token} = res
|
||||||
|
|
||||||
if(await useCapacitor().getIsNative()) {
|
if(useCapacitor().getIsNative()) {
|
||||||
await Preferences.set({
|
await Preferences.set({
|
||||||
key:"token",
|
key:"token",
|
||||||
value: token,
|
value: token,
|
||||||
|
|||||||
@@ -950,7 +950,7 @@ export const useDataStore = defineStore('data', () => {
|
|||||||
selectSearchAttributes: ['name'],
|
selectSearchAttributes: ['name'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "purchasePrice",
|
key: "purchase_price",
|
||||||
label: "Einkaufspreis",
|
label: "Einkaufspreis",
|
||||||
component: purchasePrice,
|
component: purchasePrice,
|
||||||
inputType: "number",
|
inputType: "number",
|
||||||
@@ -963,7 +963,7 @@ export const useDataStore = defineStore('data', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},{
|
},{
|
||||||
key: "markupPercentage",
|
key: "markup_percentage",
|
||||||
label: "Aufschlag",
|
label: "Aufschlag",
|
||||||
inputType: "number",
|
inputType: "number",
|
||||||
inputTrailing: "%",
|
inputTrailing: "%",
|
||||||
@@ -977,7 +977,7 @@ export const useDataStore = defineStore('data', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},{
|
},{
|
||||||
key: "sellingPrice",
|
key: "selling_price",
|
||||||
label: "Verkaufpreispreis",
|
label: "Verkaufpreispreis",
|
||||||
required: true,
|
required: true,
|
||||||
component: sellingPrice,
|
component: sellingPrice,
|
||||||
@@ -991,7 +991,7 @@ export const useDataStore = defineStore('data', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},{
|
},{
|
||||||
key: "taxPercentage",
|
key: "tax_percentage",
|
||||||
label: "Umsatzsteuer",
|
label: "Umsatzsteuer",
|
||||||
inputType: "select",
|
inputType: "select",
|
||||||
selectOptionAttribute: "label",
|
selectOptionAttribute: "label",
|
||||||
@@ -1194,7 +1194,7 @@ export const useDataStore = defineStore('data', () => {
|
|||||||
inputType: "bool",
|
inputType: "bool",
|
||||||
sortable: true
|
sortable: true
|
||||||
},{
|
},{
|
||||||
key: 'licensePlate',
|
key: 'license_plate',
|
||||||
label: "Kennzeichen",
|
label: "Kennzeichen",
|
||||||
required: true,
|
required: true,
|
||||||
inputType: "text",
|
inputType: "text",
|
||||||
@@ -1219,18 +1219,18 @@ export const useDataStore = defineStore('data', () => {
|
|||||||
component: driver
|
component: driver
|
||||||
},*/
|
},*/
|
||||||
{
|
{
|
||||||
key: "tankSize",
|
key: "tank_size",
|
||||||
label: "Tankvolumen",
|
label: "Tankvolumen",
|
||||||
unit: "L",
|
unit: "L",
|
||||||
inputType: "number"
|
inputType: "number"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "buildYear",
|
key: "build_year",
|
||||||
label: "Baujahr",
|
label: "Baujahr",
|
||||||
inputType: "number"
|
inputType: "number"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "towingCapacity",
|
key: "towing_capacity",
|
||||||
label: "Anhängelast",
|
label: "Anhängelast",
|
||||||
unit: "Kg",
|
unit: "Kg",
|
||||||
inputType: "number",
|
inputType: "number",
|
||||||
@@ -1242,7 +1242,7 @@ export const useDataStore = defineStore('data', () => {
|
|||||||
inputType: "text"
|
inputType: "text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "powerInKW",
|
key: "power_in_kw",
|
||||||
label: "Leistung",
|
label: "Leistung",
|
||||||
unit: "kW",
|
unit: "kW",
|
||||||
inputType: "number",
|
inputType: "number",
|
||||||
@@ -1459,7 +1459,7 @@ export const useDataStore = defineStore('data', () => {
|
|||||||
sortable: true
|
sortable: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'spaceNumber',
|
key: 'space_number',
|
||||||
label: "Lagerplatznr.",
|
label: "Lagerplatznr.",
|
||||||
inputType: "text",
|
inputType: "text",
|
||||||
inputIsNumberRange: true,
|
inputIsNumberRange: true,
|
||||||
@@ -1483,7 +1483,7 @@ export const useDataStore = defineStore('data', () => {
|
|||||||
sortable: true
|
sortable: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "parentSpace",
|
key: "parent_space",
|
||||||
label: "Übergeordneter Lagerplatz",
|
label: "Übergeordneter Lagerplatz",
|
||||||
inputType: "select",
|
inputType: "select",
|
||||||
selectDataType: "spaces",
|
selectDataType: "spaces",
|
||||||
@@ -1492,21 +1492,21 @@ export const useDataStore = defineStore('data', () => {
|
|||||||
inputColumn: "Allgemeines"
|
inputColumn: "Allgemeines"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "infoData.streetNumber",
|
key: "info_data.streetNumber",
|
||||||
label: "Straße + Hausnummer",
|
label: "Straße + Hausnummer",
|
||||||
inputType: "text",
|
inputType: "text",
|
||||||
disabledInTable: true,
|
disabledInTable: true,
|
||||||
inputColumn: "Ort"
|
inputColumn: "Ort"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "infoData.special",
|
key: "info_data.special",
|
||||||
label: "Adresszusatz",
|
label: "Adresszusatz",
|
||||||
inputType: "text",
|
inputType: "text",
|
||||||
disabledInTable: true,
|
disabledInTable: true,
|
||||||
inputColumn: "Ort"
|
inputColumn: "Ort"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "infoData.zip",
|
key: "info_data.zip",
|
||||||
label: "Postleitzahl",
|
label: "Postleitzahl",
|
||||||
inputType: "number",
|
inputType: "number",
|
||||||
disabledInTable: true,
|
disabledInTable: true,
|
||||||
@@ -1518,14 +1518,14 @@ export const useDataStore = defineStore('data', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "infoData.city",
|
key: "info_data.city",
|
||||||
label: "Stadt",
|
label: "Stadt",
|
||||||
inputType: "text",
|
inputType: "text",
|
||||||
disabledInTable: true,
|
disabledInTable: true,
|
||||||
inputColumn: "Ort"
|
inputColumn: "Ort"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "infoData.country",
|
key: "info_data.country",
|
||||||
label: "Land",
|
label: "Land",
|
||||||
inputType: "select",
|
inputType: "select",
|
||||||
selectDataType: "countrys",
|
selectDataType: "countrys",
|
||||||
@@ -1574,6 +1574,12 @@ export const useDataStore = defineStore('data', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
templateColumns: [
|
||||||
|
{
|
||||||
|
key: "customer",
|
||||||
|
distinct: true
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
tickets: {
|
tickets: {
|
||||||
@@ -2192,8 +2198,8 @@ export const useDataStore = defineStore('data', () => {
|
|||||||
label: "Fahrzeuge",
|
label: "Fahrzeuge",
|
||||||
inputType: "select",
|
inputType: "select",
|
||||||
selectDataType: "vehicles",
|
selectDataType: "vehicles",
|
||||||
selectOptionAttribute: "licensePlate",
|
selectOptionAttribute: "license_plate",
|
||||||
selectSearchAttributes: ['licensePlate'],
|
selectSearchAttributes: ['license_plate'],
|
||||||
selectMultiple: true,
|
selectMultiple: true,
|
||||||
component: vehiclesWithLoad,
|
component: vehiclesWithLoad,
|
||||||
},{
|
},{
|
||||||
|
|||||||
Reference in New Issue
Block a user