Merge branch 'beta' into 'main'
2025.21.0 See merge request fedeo/software!40
This commit is contained in:
@@ -1,70 +1,134 @@
|
|||||||
<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)"
|
||||||
:element-id="props.item.id"
|
:element-id="props.item.id"
|
||||||
@uploadFinished="emit('updateNeeded')"
|
@uploadFinished="emit('updateNeeded')"
|
||||||
/>
|
/>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
<DocumentList
|
|
||||||
:key="props.item.files.length"
|
|
||||||
:documents="availableFiles"
|
|
||||||
v-if="availableFiles.length > 0"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<UAlert
|
<!-- 📱 MOBILE: File Cards -->
|
||||||
v-else
|
<div v-if="props.platform === 'mobile'" class="space-y-3 mt-3">
|
||||||
icon="i-heroicons-x-mark"
|
<div
|
||||||
title="Keine Dateien verfügbar"
|
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
|
||||||
|
:key="props.item.files.length"
|
||||||
|
:documents="availableFiles"
|
||||||
|
v-if="availableFiles.length > 0"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UAlert v-else icon="i-heroicons-x-mark" title="Keine Dateien verfügbar" />
|
||||||
|
</template>
|
||||||
</UCard>
|
</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
|
||||||
|
v-else
|
||||||
|
title="Nicht unterstützter Dateityp"
|
||||||
|
icon="i-heroicons-exclamation-triangle"
|
||||||
|
/>
|
||||||
|
</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,7 @@ const links = computed(() => {
|
|||||||
... true ? [{
|
... true ? [{
|
||||||
label: "Anwesenheiten",
|
label: "Anwesenheiten",
|
||||||
to: "/staff/time",
|
to: "/staff/time",
|
||||||
icon: "i-heroicons-clock"
|
icon: "i-heroicons-clock",
|
||||||
}] : [],
|
}] : [],
|
||||||
/*... has("absencerequests") ? [{
|
/*... has("absencerequests") ? [{
|
||||||
label: "Abwesenheiten",
|
label: "Abwesenheiten",
|
||||||
@@ -175,7 +184,7 @@ const links = computed(() => {
|
|||||||
},{
|
},{
|
||||||
label: "Eingangsbelege",
|
label: "Eingangsbelege",
|
||||||
to: "/incomingInvoices",
|
to: "/incomingInvoices",
|
||||||
icon: "i-heroicons-document-text"
|
icon: "i-heroicons-document-text",
|
||||||
},{
|
},{
|
||||||
label: "Kostenstellen",
|
label: "Kostenstellen",
|
||||||
to: "/standardEntity/costcentres",
|
to: "/standardEntity/costcentres",
|
||||||
@@ -183,7 +192,7 @@ const links = computed(() => {
|
|||||||
},{
|
},{
|
||||||
label: "Buchungskonten",
|
label: "Buchungskonten",
|
||||||
to: "/accounts",
|
to: "/accounts",
|
||||||
icon: "i-heroicons-document-text"
|
icon: "i-heroicons-document-text",
|
||||||
},{
|
},{
|
||||||
label: "zusätzliche Buchungskonten",
|
label: "zusätzliche Buchungskonten",
|
||||||
to: "/standardEntity/ownaccounts",
|
to: "/standardEntity/ownaccounts",
|
||||||
@@ -192,7 +201,7 @@ const links = computed(() => {
|
|||||||
{
|
{
|
||||||
label: "Bank",
|
label: "Bank",
|
||||||
to: "/banking",
|
to: "/banking",
|
||||||
icon: "i-heroicons-document-text"
|
icon: "i-heroicons-document-text",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}],
|
}],
|
||||||
@@ -285,11 +294,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 +307,7 @@ 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",
|
||||||
},/*{
|
},/*{
|
||||||
label: "Rollen",
|
label: "Rollen",
|
||||||
to: "/roles",
|
to: "/roles",
|
||||||
@@ -306,15 +315,15 @@ 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",
|
||||||
},{
|
},{
|
||||||
label: "Bankkonten",
|
label: "Bankkonten",
|
||||||
to: "/settings/banking",
|
to: "/settings/banking",
|
||||||
icon: "i-heroicons-currency-euro"
|
icon: "i-heroicons-currency-euro",
|
||||||
},{
|
},{
|
||||||
label: "Textvorlagen",
|
label: "Textvorlagen",
|
||||||
to: "/settings/texttemplates",
|
to: "/settings/texttemplates",
|
||||||
icon: "i-heroicons-clipboard-document-list"
|
icon: "i-heroicons-clipboard-document-list",
|
||||||
},/*{
|
},/*{
|
||||||
label: "Eigene Felder",
|
label: "Eigene Felder",
|
||||||
to: "/settings/ownfields",
|
to: "/settings/ownfields",
|
||||||
@@ -322,15 +331,16 @@ const links = computed(() => {
|
|||||||
},*/{
|
},*/{
|
||||||
label: "Firmeneinstellungen",
|
label: "Firmeneinstellungen",
|
||||||
to: "/settings/tenant",
|
to: "/settings/tenant",
|
||||||
icon: "i-heroicons-building-office"
|
icon: "i-heroicons-building-office",
|
||||||
},{
|
},{
|
||||||
label: "Projekttypen",
|
label: "Projekttypen",
|
||||||
to: "/projecttypes",
|
to: "/projecttypes",
|
||||||
icon: "i-heroicons-clipboard-document-list"
|
icon: "i-heroicons-clipboard-document-list",
|
||||||
},{
|
},{
|
||||||
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 () => {
|
||||||
await useFiles().downloadFile(props.fileId)
|
if(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,6 +183,7 @@ watch(downloadControl, (downloadCtrl) => {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
></UButton>
|
></UButton>
|
||||||
<UButton
|
<UButton
|
||||||
|
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"
|
||||||
|
|||||||
@@ -3,55 +3,75 @@ import dayjs from "dayjs";
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: boolean;
|
modelValue: boolean;
|
||||||
entry?: null;
|
entry?: any | null;
|
||||||
|
users: any[];
|
||||||
|
canSelectUser: boolean;
|
||||||
|
defaultUserId: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits(["update:modelValue", "saved"]);
|
const emit = defineEmits(["update:modelValue", "saved"]);
|
||||||
|
|
||||||
const { create, update } = useStaffTime();
|
const { create, update } = useStaffTime();
|
||||||
|
|
||||||
// v-model für das Modal
|
|
||||||
const show = computed({
|
const show = computed({
|
||||||
get: () => props.modelValue,
|
get: () => props.modelValue,
|
||||||
set: (v: boolean) => emit("update:modelValue", v),
|
set: (v: boolean) => emit("update:modelValue", v),
|
||||||
});
|
});
|
||||||
|
|
||||||
// 🧱 Lokale reactive Kopie, die beim Öffnen aus props.entry befüllt wird
|
// 🌈 Typen
|
||||||
const local = reactive<{
|
const typeOptions = [
|
||||||
id?: string;
|
{ label: "Arbeitszeit", value: "work" },
|
||||||
description: string;
|
{ label: "Urlaub", value: "vacation" },
|
||||||
started_at: string;
|
{ label: "Krankheit", value: "sick" },
|
||||||
stopped_at: string | null;
|
{ label: "Feiertag", value: "holiday" },
|
||||||
type: string;
|
];
|
||||||
}>({
|
|
||||||
|
// Lokaler State
|
||||||
|
const local = reactive({
|
||||||
id: "",
|
id: "",
|
||||||
|
user_id: "", // 👈 Mitarbeiter
|
||||||
description: "",
|
description: "",
|
||||||
started_at: dayjs().startOf("hour").format("YYYY-MM-DDTHH:mm"),
|
started_at: "",
|
||||||
stopped_at: dayjs().add(1, "hour").format("YYYY-MM-DDTHH:mm"),
|
stopped_at: "",
|
||||||
type: "work",
|
type: "work",
|
||||||
|
vacation_reason: "",
|
||||||
|
sick_reason: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
// 📡 Wenn das Modal geöffnet wird, Entry-Daten übernehmen
|
// 📡 ENTRY —> LOCAL
|
||||||
watch(
|
watch(
|
||||||
() => props.entry,
|
() => props.entry,
|
||||||
(val) => {
|
(val) => {
|
||||||
if (val) {
|
if (val) {
|
||||||
Object.assign(local, {
|
Object.assign(local, {
|
||||||
id: val.id,
|
id: val.id,
|
||||||
|
user_id: val.user_id, // 👈 Mitarbeiter vorbelegen
|
||||||
description: val.description || "",
|
description: val.description || "",
|
||||||
started_at: dayjs(val.started_at).format("YYYY-MM-DDTHH:mm"),
|
|
||||||
stopped_at: val.stopped_at
|
|
||||||
? dayjs(val.stopped_at).format("YYYY-MM-DDTHH:mm")
|
|
||||||
: dayjs(val.started_at).add(1, "hour").format("YYYY-MM-DDTHH:mm"),
|
|
||||||
type: val.type || "work",
|
type: val.type || "work",
|
||||||
|
|
||||||
|
started_at:
|
||||||
|
val.type === "vacation"
|
||||||
|
? dayjs(val.started_at).format("YYYY-MM-DD")
|
||||||
|
: dayjs(val.started_at).format("YYYY-MM-DDTHH:mm"),
|
||||||
|
|
||||||
|
stopped_at:
|
||||||
|
val.type === "vacation"
|
||||||
|
? dayjs(val.stopped_at).format("YYYY-MM-DD")
|
||||||
|
: dayjs(val.stopped_at).format("YYYY-MM-DDTHH:mm"),
|
||||||
|
|
||||||
|
vacation_reason: val.vacation_reason || "",
|
||||||
|
sick_reason: val.sick_reason || "",
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
Object.assign(local, {
|
Object.assign(local, {
|
||||||
id: "",
|
id: "",
|
||||||
|
user_id: props.defaultUserId, // 👈 Neuer Eintrag → aktueller Nutzer
|
||||||
description: "",
|
description: "",
|
||||||
started_at: dayjs().startOf("hour").format("YYYY-MM-DDTHH:mm"),
|
|
||||||
stopped_at: dayjs().add(1, "hour").format("YYYY-MM-DDTHH:mm"),
|
|
||||||
type: "work",
|
type: "work",
|
||||||
|
started_at: dayjs().format("YYYY-MM-DDTHH:mm"),
|
||||||
|
stopped_at: dayjs().add(1, "hour").format("YYYY-MM-DDTHH:mm"),
|
||||||
|
vacation_reason: "",
|
||||||
|
sick_reason: "",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -63,13 +83,27 @@ const loading = ref(false);
|
|||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const payload: any = {
|
||||||
description: local.description,
|
user_id: local.user_id, // 👈 immer senden
|
||||||
started_at: dayjs(local.started_at).toISOString(),
|
|
||||||
stopped_at: local.stopped_at ? dayjs(local.stopped_at).toISOString() : null,
|
|
||||||
type: local.type,
|
type: local.type,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (local.type === "vacation") {
|
||||||
|
payload.started_at = dayjs(local.started_at).startOf("day").toISOString();
|
||||||
|
payload.stopped_at = dayjs(local.stopped_at).endOf("day").toISOString();
|
||||||
|
payload.vacation_reason = local.vacation_reason;
|
||||||
|
} else {
|
||||||
|
payload.started_at = dayjs(local.started_at).toISOString();
|
||||||
|
payload.stopped_at = local.stopped_at
|
||||||
|
? dayjs(local.stopped_at).toISOString()
|
||||||
|
: null;
|
||||||
|
payload.description = local.description;
|
||||||
|
|
||||||
|
if (local.type === "sick") {
|
||||||
|
payload.sick_reason = local.sick_reason;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (local.id) {
|
if (local.id) {
|
||||||
await update(local.id, payload);
|
await update(local.id, payload);
|
||||||
} else {
|
} else {
|
||||||
@@ -84,6 +118,7 @@ async function handleSubmit() {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UModal v-model="show" :ui="{ width: 'w-full sm:max-w-md' }" :key="local.id || 'new'">
|
<UModal v-model="show" :ui="{ width: 'w-full sm:max-w-md' }" :key="local.id || 'new'">
|
||||||
<UCard>
|
<UCard>
|
||||||
@@ -94,18 +129,72 @@ async function handleSubmit() {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<UForm @submit.prevent="handleSubmit" class="space-y-4">
|
<UForm @submit.prevent="handleSubmit" class="space-y-4">
|
||||||
<UFormGroup label="Beschreibung" name="description">
|
|
||||||
<UInput v-model="local.description" placeholder="Was wurde gemacht?" />
|
<!-- 👥 Mitarbeiter-Auswahl -->
|
||||||
|
<UFormGroup label="Mitarbeiter" v-if="props.canSelectUser">
|
||||||
|
<USelectMenu
|
||||||
|
v-model="local.user_id"
|
||||||
|
:options="props.users.map(u => ({
|
||||||
|
label: u.full_name || u.email,
|
||||||
|
value: u.user_id
|
||||||
|
}))"
|
||||||
|
placeholder="Mitarbeiter wählen"
|
||||||
|
option-attribute="label"
|
||||||
|
value-attribute="value"
|
||||||
|
/>
|
||||||
</UFormGroup>
|
</UFormGroup>
|
||||||
|
|
||||||
<UFormGroup label="Startzeit" name="started_at">
|
<!-- TYPE -->
|
||||||
<UInput v-model="local.started_at" type="datetime-local" />
|
<UFormGroup label="Typ">
|
||||||
|
<USelect v-model="local.type" :options="typeOptions" />
|
||||||
</UFormGroup>
|
</UFormGroup>
|
||||||
|
|
||||||
<UFormGroup label="Endzeit" name="stopped_at">
|
<!-- VACATION -->
|
||||||
<UInput v-model="local.stopped_at" type="datetime-local" />
|
<template v-if="local.type === 'vacation'">
|
||||||
</UFormGroup>
|
<UFormGroup label="Urlaubsgrund">
|
||||||
|
<UInput v-model="local.vacation_reason" />
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<UFormGroup label="Start (Tag)">
|
||||||
|
<UInput v-model="local.started_at" type="date" />
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<UFormGroup label="Ende (Tag)">
|
||||||
|
<UInput v-model="local.stopped_at" type="date" />
|
||||||
|
</UFormGroup>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- SICK -->
|
||||||
|
<template v-else-if="local.type === 'sick'">
|
||||||
|
<UFormGroup label="Krankheitsgrund">
|
||||||
|
<UInput v-model="local.sick_reason" />
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<UFormGroup label="Start (Tag)">
|
||||||
|
<UInput v-model="local.started_at" type="date" />
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<UFormGroup label="Ende (Tag)">
|
||||||
|
<UInput v-model="local.stopped_at" type="date" />
|
||||||
|
</UFormGroup>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- WORK / OTHER -->
|
||||||
|
<template v-else>
|
||||||
|
<UFormGroup label="Beschreibung">
|
||||||
|
<UInput v-model="local.description" placeholder="Was wurde gemacht?" />
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<UFormGroup label="Startzeit">
|
||||||
|
<UInput v-model="local.started_at" type="datetime-local" />
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<UFormGroup label="Endzeit">
|
||||||
|
<UInput v-model="local.stopped_at" type="datetime-local" />
|
||||||
|
</UFormGroup>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ACTIONS -->
|
||||||
<div class="flex justify-end gap-2 mt-4">
|
<div class="flex justify-end gap-2 mt-4">
|
||||||
<UButton color="gray" label="Abbrechen" @click="show = false" />
|
<UButton color="gray" label="Abbrechen" @click="show = false" />
|
||||||
<UButton color="primary" :loading="loading" type="submit" label="Speichern" />
|
<UButton color="primary" :loading="loading" type="submit" label="Speichern" />
|
||||||
@@ -114,3 +203,4 @@ async function handleSubmit() {
|
|||||||
</UCard>
|
</UCard>
|
||||||
</UModal>
|
</UModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
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,46 +2,65 @@
|
|||||||
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>
|
||||||
<UButton
|
<!-- Wrapper für Position + Animation -->
|
||||||
id="fab"
|
<div
|
||||||
:icon="props.icon"
|
class="fixed right-5 z-40 transition-all"
|
||||||
:label="props.label"
|
:style="{ bottom: `calc(${props.pos}rem + env(safe-area-inset-bottom))` }"
|
||||||
:variant="props.variant"
|
>
|
||||||
:color="props.color"
|
<UButton
|
||||||
@click="emit('click')"
|
id="fab"
|
||||||
:style="`bottom: ${15 + props.pos * 5}vh;`"
|
:icon="props.icon"
|
||||||
class="bg-white dark:bg-gray-950"
|
:label="props.label"
|
||||||
/>
|
:variant="props.variant"
|
||||||
|
:color="props.color"
|
||||||
|
@click="emit('click')"
|
||||||
|
class="
|
||||||
|
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 */
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
/* Optional: Auto-Kreisen wenn kein Label */
|
||||||
|
#fab:not([label]) {
|
||||||
|
@apply w-14 h-14 p-0 flex items-center justify-center text-2xl;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ export const useEntities = (
|
|||||||
) => {
|
) => {
|
||||||
if (!idToEq) return null
|
if (!idToEq) return null
|
||||||
|
|
||||||
const res = await useNuxtApp().$api(withInformation ? `/api/resource/${relation}/${idToEq}/${withInformation}` : `/api/resource/${relation}/${idToEq}`, {
|
const res = await useNuxtApp().$api(withInformation ? `/api/resource/${relation}/${idToEq}` : `/api/resource/${relation}/${idToEq}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
params: { select }
|
params: { select }
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -124,6 +124,19 @@ export const useFiles = () => {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const dataURLtoFile = (dataurl:string, filename:string) => {
|
||||||
|
let arr = dataurl.split(","),
|
||||||
|
//@ts-ignore
|
||||||
|
mime = arr[0].match(/:(.*?);/)[1],
|
||||||
|
bstr = atob(arr[arr.length - 1]),
|
||||||
|
n = bstr.length,
|
||||||
|
u8arr = new Uint8Array(n);
|
||||||
|
while (n--) {
|
||||||
|
u8arr[n] = bstr.charCodeAt(n);
|
||||||
|
}
|
||||||
|
return new File([u8arr], filename, {type: mime});
|
||||||
|
}
|
||||||
|
|
||||||
return {uploadFiles, selectDocuments, selectSomeDocuments, selectDocument, downloadFile}
|
|
||||||
|
return {uploadFiles, selectDocuments, selectSomeDocuments, selectDocument, downloadFile, dataURLtoFile}
|
||||||
}
|
}
|
||||||
@@ -5,4 +5,21 @@ export const useFormatDuration = (durationInMinutes:number,) => {
|
|||||||
const mins = Math.floor(durationInMinutes % 60)
|
const mins = Math.floor(durationInMinutes % 60)
|
||||||
|
|
||||||
return `${String(hrs).padStart(2, "0")}:${String(mins).padStart(2, "0")}`
|
return `${String(hrs).padStart(2, "0")}:${String(mins).padStart(2, "0")}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useFormatDurationDays = (start,end) => {
|
||||||
|
const startDate = useNuxtApp().$dayjs(start);
|
||||||
|
const endDate = useNuxtApp().$dayjs(end);
|
||||||
|
|
||||||
|
if(startDate.isBefore(endDate)){
|
||||||
|
// inkl. beider Tage → +1
|
||||||
|
const days = endDate.diff(startDate, "day") + 1;
|
||||||
|
|
||||||
|
return days + " Tag" + (days > 1 ? "e" : "");
|
||||||
|
} else {
|
||||||
|
const days = startDate.diff(endDate, "day") + 1;
|
||||||
|
|
||||||
|
return days + " Tag" + (days > 1 ? "e" : "");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,7 @@ interface StaffTimeEntry {
|
|||||||
|
|
||||||
export function useStaffTime() {
|
export function useStaffTime() {
|
||||||
const { $api } = useNuxtApp()
|
const { $api } = useNuxtApp()
|
||||||
|
const auth = useAuthStore()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -46,9 +47,17 @@ export function useStaffTime() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function approve(id: string) {
|
async function approve(id: string) {
|
||||||
return await $api<StaffTimeEntry>(`/api/staff/time/${id}`, {
|
const auth = useAuthStore()
|
||||||
|
const now = useNuxtApp().$dayjs().toISOString()
|
||||||
|
|
||||||
|
return await $api(`/api/staff/time/${id}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: { state: 'approved' },
|
body: {
|
||||||
|
state: 'approved',
|
||||||
|
//@ts-ignore
|
||||||
|
approved_by: auth.user.id,
|
||||||
|
approved_at: now,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
const handleScroll = () => {
|
||||||
icon: 'i-heroicons-user-group',
|
const current = scrollElement.scrollTop
|
||||||
to: "/contacts/create" ,
|
|
||||||
},
|
// Runter scrollen -> verstecken
|
||||||
{
|
if (current > lastScrollY + 10) {
|
||||||
id: 'new-task',
|
hideNav.value = true
|
||||||
label: 'Aufgabe hinzufügen',
|
showNavAfterDelay()
|
||||||
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" ,
|
|
||||||
}
|
}
|
||||||
]
|
|
||||||
|
|
||||||
|
// 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",
|
||||||
|
|||||||
@@ -16,9 +16,15 @@ const router = useRouter()
|
|||||||
const items = ref([])
|
const items = ref([])
|
||||||
const dataLoaded = ref(false)
|
const dataLoaded = ref(false)
|
||||||
|
|
||||||
|
const statementallocations = ref([])
|
||||||
|
const incominginvoices = ref([])
|
||||||
|
|
||||||
const setupPage = async () => {
|
const setupPage = async () => {
|
||||||
items.value = await useEntities("accounts").selectSpecial()
|
items.value = await useEntities("accounts").selectSpecial()
|
||||||
|
|
||||||
|
statementallocations.value = (await useEntities("statementallocations").select("*, bs_id(*)"))
|
||||||
|
incominginvoices.value = (await useEntities("incominginvoices").select("*, vendor(*)"))
|
||||||
|
|
||||||
items.value = await Promise.all(items.value.map(async (i) => {
|
items.value = await Promise.all(items.value.map(async (i) => {
|
||||||
let renderedAllocationsTemp = await renderedAllocations(i.id)
|
let renderedAllocationsTemp = await renderedAllocations(i.id)
|
||||||
let saldo = getSaldo(renderedAllocationsTemp)
|
let saldo = getSaldo(renderedAllocationsTemp)
|
||||||
@@ -37,22 +43,22 @@ const setupPage = async () => {
|
|||||||
|
|
||||||
const renderedAllocations = async (account) => {
|
const renderedAllocations = async (account) => {
|
||||||
|
|
||||||
let statementallocations = (await useEntities("statementallocations").select("*, bs_id(*)")).filter(i => i.account === account)
|
let statementallocationslocal = statementallocations.value.filter(i => i.account === account)
|
||||||
let incominginvoices = (await useEntities("incominginvoices").select("*, vendor(*)")).filter(i => i.accounts.find(x => x.account === account))
|
let incominginvoiceslocal = incominginvoices.value.filter(i => i.accounts.find(x => x.account === account))
|
||||||
|
|
||||||
let tempstatementallocations = statementallocations.map(i => {
|
let tempstatementallocations = statementallocationslocal.map(i => {
|
||||||
return {
|
return {
|
||||||
...i,
|
...i,
|
||||||
type: "statementallocation",
|
type: "statementallocation",
|
||||||
date: i.bs_id.date,
|
date: i.bankstatement.date,
|
||||||
partner: i.bs_id ? (i.bs_id.debName ? i.bs_id.debName : (i.bs_id.credName ? i.bs_id.credName : '')) : ''
|
partner: i.bankstatement ? (i.bankstatement.debName ? i.bankstatement.debName : (i.bankstatement.credName ? i.bankstatement.credName : '')) : ''
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
let incominginvoicesallocations = []
|
let incominginvoicesallocations = []
|
||||||
|
|
||||||
incominginvoices.forEach(i => {
|
incominginvoiceslocal.forEach(i => {
|
||||||
|
|
||||||
incominginvoicesallocations.push(...i.accounts.filter(x => x.account === account).map(x => {
|
incominginvoicesallocations.push(...i.accounts.filter(x => x.account === account).map(x => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -65,8 +65,8 @@ const setup = async () => {
|
|||||||
|
|
||||||
console.log(openDocuments.value)
|
console.log(openDocuments.value)
|
||||||
|
|
||||||
allocatedDocuments.value = documents.filter(i => i.statementallocations.find(x => x.bs_id === itemInfo.value.id))
|
allocatedDocuments.value = documents.filter(i => i.statementallocations.find(x => x.bankstatement === itemInfo.value.id))
|
||||||
allocatedIncomingInvoices.value = incominginvoices.filter(i => i.statementallocations.find(x => x.bs_id === itemInfo.value.id))
|
allocatedIncomingInvoices.value = incominginvoices.filter(i => i.statementallocations.find(x => x.bankstatement === itemInfo.value.id))
|
||||||
console.log(allocatedDocuments.value)
|
console.log(allocatedDocuments.value)
|
||||||
console.log(allocatedIncomingInvoices.value)
|
console.log(allocatedIncomingInvoices.value)
|
||||||
openIncomingInvoices.value = (await useEntities("incominginvoices").select("*, statementallocations(*), vendor(*)")).filter(i => !i.archived && i.statementallocations.reduce((n,{amount}) => n + amount, 0).toFixed(2) !== getInvoiceSum(i,false))
|
openIncomingInvoices.value = (await useEntities("incominginvoices").select("*, statementallocations(*), vendor(*)")).filter(i => !i.archived && i.statementallocations.reduce((n,{amount}) => n + amount, 0).toFixed(2) !== getInvoiceSum(i,false))
|
||||||
@@ -611,7 +611,7 @@ const archiveStatement = async () => {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
icon="i-heroicons-check"
|
icon="i-heroicons-check"
|
||||||
:disabled="!accountToSave"
|
:disabled="!accountToSave"
|
||||||
@click="saveAllocation({bs_id: itemInfo.id, amount: manualAllocationSum, account: accountToSave, description: allocationDescription })"
|
@click="saveAllocation({bankstatement: itemInfo.id, amount: manualAllocationSum, account: accountToSave, description: allocationDescription })"
|
||||||
/>
|
/>
|
||||||
<UButton
|
<UButton
|
||||||
@click="accountToSave = ''"
|
@click="accountToSave = ''"
|
||||||
@@ -677,7 +677,7 @@ const archiveStatement = async () => {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
icon="i-heroicons-check"
|
icon="i-heroicons-check"
|
||||||
:disabled="!ownAccountToSave"
|
:disabled="!ownAccountToSave"
|
||||||
@click="saveAllocation({bs_id: itemInfo.id, amount: manualAllocationSum, ownaccount: ownAccountToSave, description: allocationDescription })"
|
@click="saveAllocation({bankstatement: itemInfo.id, amount: manualAllocationSum, ownaccount: ownAccountToSave, description: allocationDescription })"
|
||||||
/>
|
/>
|
||||||
<UButton
|
<UButton
|
||||||
@click="accountToSave = ''"
|
@click="accountToSave = ''"
|
||||||
@@ -715,7 +715,7 @@ const archiveStatement = async () => {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
icon="i-heroicons-check"
|
icon="i-heroicons-check"
|
||||||
:disabled="!customerAccountToSave"
|
:disabled="!customerAccountToSave"
|
||||||
@click="saveAllocation({bs_id: itemInfo.id, amount: manualAllocationSum, customer: customerAccountToSave, description: allocationDescription })"
|
@click="saveAllocation({bankstatement: itemInfo.id, amount: manualAllocationSum, customer: customerAccountToSave, description: allocationDescription })"
|
||||||
/>
|
/>
|
||||||
<UButton
|
<UButton
|
||||||
@click="customerAccountToSave = ''"
|
@click="customerAccountToSave = ''"
|
||||||
@@ -753,7 +753,7 @@ const archiveStatement = async () => {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
icon="i-heroicons-check"
|
icon="i-heroicons-check"
|
||||||
:disabled="!vendorAccountToSave"
|
:disabled="!vendorAccountToSave"
|
||||||
@click="saveAllocation({bs_id: itemInfo.id, amount: manualAllocationSum, vendor: vendorAccountToSave, description: allocationDescription })"
|
@click="saveAllocation({bankstatement: itemInfo.id, amount: manualAllocationSum, vendor: vendorAccountToSave, description: allocationDescription })"
|
||||||
/>
|
/>
|
||||||
<UButton
|
<UButton
|
||||||
@click="vendorAccountToSave = ''"
|
@click="vendorAccountToSave = ''"
|
||||||
@@ -811,7 +811,7 @@ const archiveStatement = async () => {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
class="mr-3"
|
class="mr-3"
|
||||||
v-if="!itemInfo.statementallocations.find(i => i.cd_id === document.id)"
|
v-if="!itemInfo.statementallocations.find(i => i.cd_id === document.id)"
|
||||||
@click="saveAllocation({cd_id: document.id, bs_id: itemInfo.id, amount: Number(Number(document.openSum) < manualAllocationSum ? document.openSum : manualAllocationSum), description: allocationDescription})"
|
@click="saveAllocation({cd_id: document.id, bankstatement: itemInfo.id, amount: Number(Number(document.openSum) < manualAllocationSum ? document.openSum : manualAllocationSum), description: allocationDescription})"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<UButton
|
<UButton
|
||||||
@@ -836,7 +836,7 @@ const archiveStatement = async () => {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
class="mr-3"
|
class="mr-3"
|
||||||
v-if="!itemInfo.statementallocations.find(i => i.ii_id === item.id)"
|
v-if="!itemInfo.statementallocations.find(i => i.ii_id === item.id)"
|
||||||
@click="saveAllocation({ii_id: item.id, bs_id: itemInfo.id, amount: Number(Math.abs(getInvoiceSum(item,true)) > Math.abs(manualAllocationSum) ? manualAllocationSum : getInvoiceSum(item,true)), description: allocationDescription})"
|
@click="saveAllocation({ii_id: item.id, bankstatement: itemInfo.id, amount: Number(Math.abs(getInvoiceSum(item,true)) > Math.abs(manualAllocationSum) ? manualAllocationSum : getInvoiceSum(item,true)), description: allocationDescription})"
|
||||||
/>
|
/>
|
||||||
<UButton
|
<UButton
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|||||||
@@ -661,7 +661,7 @@ const findDocumentErrors = computed(() => {
|
|||||||
if (itemInfo.value.rows.length === 0) {
|
if (itemInfo.value.rows.length === 0) {
|
||||||
errors.push({message: "Es sind keine Positionen angegeben", type: "breaking"})
|
errors.push({message: "Es sind keine Positionen angegeben", type: "breaking"})
|
||||||
} else {
|
} else {
|
||||||
itemInfo.value.rows.forEach(row => {
|
itemInfo.value.rows.forEach((row,index) => {
|
||||||
|
|
||||||
if (itemInfo.value.type !== "quotes" && row.optional) {
|
if (itemInfo.value.type !== "quotes" && row.optional) {
|
||||||
errors.push({
|
errors.push({
|
||||||
@@ -717,6 +717,10 @@ const findDocumentErrors = computed(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (index === itemInfo.value.rows.length - 1 && row.mode === "pagebreak") {
|
||||||
|
errors.push({message: `Die letze Position darf kein Seitenumbruch sein`, type: "breaking"})
|
||||||
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ const costcentres = ref([])
|
|||||||
const vendors = ref([])
|
const vendors = ref([])
|
||||||
const accounts = ref([])
|
const accounts = ref([])
|
||||||
|
|
||||||
|
const mode = ref(route.params.mode)
|
||||||
|
|
||||||
const setup = async () => {
|
const setup = async () => {
|
||||||
let filetype = (await useEntities("filetags").select()).find(i=> i.incomingDocumentType === "invoices").id
|
let filetype = (await useEntities("filetags").select()).find(i=> i.incomingDocumentType === "invoices").id
|
||||||
console.log(filetype)
|
console.log(filetype)
|
||||||
@@ -43,6 +45,8 @@ const setup = async () => {
|
|||||||
itemInfo.value = await useEntities("incominginvoices").selectSingle(route.params.id, "*, files(*)")
|
itemInfo.value = await useEntities("incominginvoices").selectSingle(route.params.id, "*, files(*)")
|
||||||
await loadFile(itemInfo.value.files[itemInfo.value.files.length-1].id)
|
await loadFile(itemInfo.value.files[itemInfo.value.files.length-1].id)
|
||||||
|
|
||||||
|
if(itemInfo.value.date && !itemInfo.value.dueDate) itemInfo.value.dueDate = itemInfo.value.date
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setup()
|
setup()
|
||||||
@@ -143,7 +147,7 @@ const findIncomingInvoiceErrors = computed(() => {
|
|||||||
let errors = []
|
let errors = []
|
||||||
|
|
||||||
if(itemInfo.value.vendor === null) errors.push({message: "Es ist kein Lieferant ausgewählt", type: "breaking"})
|
if(itemInfo.value.vendor === null) errors.push({message: "Es ist kein Lieferant ausgewählt", type: "breaking"})
|
||||||
if(itemInfo.value.reference === null) errors.push({message: "Es ist keine Referenz angegeben", type: "breaking"})
|
if(itemInfo.value.reference === null || itemInfo.value.reference.length === 0) errors.push({message: "Es ist keine Referenz angegeben", type: "breaking"})
|
||||||
if(itemInfo.value.date === null) errors.push({message: "Es ist kein Datum ausgewählt", type: "breaking"})
|
if(itemInfo.value.date === null) errors.push({message: "Es ist kein Datum ausgewählt", type: "breaking"})
|
||||||
if(itemInfo.value.dueDate === null) errors.push({message: "Es ist kein Fälligkeitsdatum ausgewählt", type: "breaking"})
|
if(itemInfo.value.dueDate === null) errors.push({message: "Es ist kein Fälligkeitsdatum ausgewählt", type: "breaking"})
|
||||||
if(itemInfo.value.paymentType === null) errors.push({message: "Es ist keine Zahlart ausgewählt", type: "breaking"})
|
if(itemInfo.value.paymentType === null) errors.push({message: "Es ist keine Zahlart ausgewählt", type: "breaking"})
|
||||||
@@ -168,21 +172,38 @@ const findIncomingInvoiceErrors = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UDashboardNavbar :title="'Eingangsbeleg erstellen'">
|
<UDashboardNavbar>
|
||||||
|
<template #left>
|
||||||
|
<UButton
|
||||||
|
icon="i-heroicons-chevron-left"
|
||||||
|
@click="navigateTo(`/incomingInvoices`)"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
Eingangsbelege
|
||||||
|
</UButton>
|
||||||
|
</template>
|
||||||
|
<template #center>
|
||||||
|
<h1
|
||||||
|
class="text-xl font-medium"
|
||||||
|
>{{`Eingangsbeleg ${mode === 'show' ? 'anzeigen' : 'bearbeiten'}`}}</h1>
|
||||||
|
</template>
|
||||||
<template #right>
|
<template #right>
|
||||||
<ArchiveButton
|
<ArchiveButton
|
||||||
color="rose"
|
color="rose"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
type="incominginvoices"
|
type="incominginvoices"
|
||||||
@confirmed="useEntities('incominginvoices').archive(route.params.id)"
|
@confirmed="useEntities('incominginvoices').archive(route.params.id)"
|
||||||
|
v-if="mode !== 'show'"
|
||||||
/>
|
/>
|
||||||
<UButton
|
<UButton
|
||||||
@click="updateIncomingInvoice(false)"
|
@click="updateIncomingInvoice(false)"
|
||||||
|
v-if="mode !== 'show'"
|
||||||
>
|
>
|
||||||
Speichern
|
Speichern
|
||||||
</UButton>
|
</UButton>
|
||||||
<UButton
|
<UButton
|
||||||
@click="updateIncomingInvoice(true)"
|
@click="updateIncomingInvoice(true)"
|
||||||
|
v-if="mode !== 'show'"
|
||||||
:disabled="findIncomingInvoiceErrors.filter(i => i.type === 'breaking').length > 0"
|
:disabled="findIncomingInvoiceErrors.filter(i => i.type === 'breaking').length > 0"
|
||||||
>
|
>
|
||||||
Speichern & Buchen
|
Speichern & Buchen
|
||||||
@@ -192,6 +213,7 @@ const findIncomingInvoiceErrors = computed(() => {
|
|||||||
<UDashboardPanelContent>
|
<UDashboardPanelContent>
|
||||||
<div
|
<div
|
||||||
class="flex justify-between mt-5 workingContainer"
|
class="flex justify-between mt-5 workingContainer"
|
||||||
|
v-if="loadedFile"
|
||||||
>
|
>
|
||||||
<object
|
<object
|
||||||
v-if="loadedFile"
|
v-if="loadedFile"
|
||||||
@@ -200,7 +222,6 @@ const findIncomingInvoiceErrors = computed(() => {
|
|||||||
class="mx-5 documentPreview"
|
class="mx-5 documentPreview"
|
||||||
/>
|
/>
|
||||||
<div class="w-3/5 mx-5">
|
<div class="w-3/5 mx-5">
|
||||||
|
|
||||||
<UAlert
|
<UAlert
|
||||||
class="mb-5"
|
class="mb-5"
|
||||||
title="Vorhandene Probleme und Informationen:"
|
title="Vorhandene Probleme und Informationen:"
|
||||||
@@ -218,17 +239,19 @@ const findIncomingInvoiceErrors = computed(() => {
|
|||||||
|
|
||||||
</UAlert>
|
</UAlert>
|
||||||
|
|
||||||
<div class=" scrollContainer">
|
<div class="scrollContainer">
|
||||||
<InputGroup class="mb-3">
|
<InputGroup class="mb-3">
|
||||||
<UButton
|
<UButton
|
||||||
:variant="itemInfo.expense ? 'solid' : 'outline'"
|
:variant="itemInfo.expense ? 'solid' : 'outline'"
|
||||||
@click="itemInfo.expense = true"
|
@click="itemInfo.expense = true"
|
||||||
|
:disabled="mode === 'show'"
|
||||||
>
|
>
|
||||||
Ausgabe
|
Ausgabe
|
||||||
</UButton>
|
</UButton>
|
||||||
<UButton
|
<UButton
|
||||||
:variant="!itemInfo.expense ? 'solid' : 'outline'"
|
:variant="!itemInfo.expense ? 'solid' : 'outline'"
|
||||||
@click="itemInfo.expense = false"
|
@click="itemInfo.expense = false"
|
||||||
|
:disabled="mode === 'show'"
|
||||||
>
|
>
|
||||||
Einnahme
|
Einnahme
|
||||||
</UButton>
|
</UButton>
|
||||||
@@ -237,6 +260,7 @@ const findIncomingInvoiceErrors = computed(() => {
|
|||||||
<UFormGroup label="Lieferant:" >
|
<UFormGroup label="Lieferant:" >
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<USelectMenu
|
<USelectMenu
|
||||||
|
:disabled="mode === 'show'"
|
||||||
v-model="itemInfo.vendor"
|
v-model="itemInfo.vendor"
|
||||||
:options="vendors"
|
:options="vendors"
|
||||||
option-attribute="name"
|
option-attribute="name"
|
||||||
@@ -258,12 +282,15 @@ const findIncomingInvoiceErrors = computed(() => {
|
|||||||
type="vendors"
|
type="vendors"
|
||||||
:id="itemInfo.vendor"
|
:id="itemInfo.vendor"
|
||||||
@return-data="(data) => itemInfo.vendor = data.id"
|
@return-data="(data) => itemInfo.vendor = data.id"
|
||||||
|
:button-edit="mode !== 'show'"
|
||||||
|
:button-create="mode !== 'show'"
|
||||||
/>
|
/>
|
||||||
<UButton
|
<UButton
|
||||||
icon="i-heroicons-x-mark"
|
icon="i-heroicons-x-mark"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
color="rose"
|
color="rose"
|
||||||
@click="itemInfo.vendor = null"
|
@click="itemInfo.vendor = null"
|
||||||
|
v-if="itemInfo.vendor && mode !== 'show'"
|
||||||
/>
|
/>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
|
|
||||||
@@ -276,6 +303,7 @@ const findIncomingInvoiceErrors = computed(() => {
|
|||||||
>
|
>
|
||||||
<UInput
|
<UInput
|
||||||
v-model="itemInfo.reference"
|
v-model="itemInfo.reference"
|
||||||
|
:disabled="mode === 'show'"
|
||||||
/>
|
/>
|
||||||
</UFormGroup>
|
</UFormGroup>
|
||||||
|
|
||||||
@@ -287,6 +315,7 @@ const findIncomingInvoiceErrors = computed(() => {
|
|||||||
:label="itemInfo.date ? dayjs(itemInfo.date).format('DD.MM.YYYY') : 'Datum auswählen'"
|
:label="itemInfo.date ? dayjs(itemInfo.date).format('DD.MM.YYYY') : 'Datum auswählen'"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
:color="!itemInfo.date ? 'rose' : 'primary'"
|
:color="!itemInfo.date ? 'rose' : 'primary'"
|
||||||
|
:disabled="mode === 'show'"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<template #panel="{ close }">
|
<template #panel="{ close }">
|
||||||
@@ -301,10 +330,11 @@ const findIncomingInvoiceErrors = computed(() => {
|
|||||||
icon="i-heroicons-calendar-days-20-solid"
|
icon="i-heroicons-calendar-days-20-solid"
|
||||||
:label="itemInfo.dueDate ? dayjs(itemInfo.dueDate).format('DD.MM.YYYY') : 'Datum auswählen'"
|
:label="itemInfo.dueDate ? dayjs(itemInfo.dueDate).format('DD.MM.YYYY') : 'Datum auswählen'"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
:disabled="mode === 'show'"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<template #panel="{ close }">
|
<template #panel="{ close }">
|
||||||
<LazyDatePicker v-model="itemInfo.dueDate" @close="close" />
|
<LazyDatePicker v-model="itemInfo.dueDate" @close="close"/>
|
||||||
</template>
|
</template>
|
||||||
</UPopover>
|
</UPopover>
|
||||||
</UFormGroup>
|
</UFormGroup>
|
||||||
@@ -316,12 +346,14 @@ const findIncomingInvoiceErrors = computed(() => {
|
|||||||
<USelectMenu
|
<USelectMenu
|
||||||
:options="['Einzug','Kreditkarte','Überweisung','Sonstiges']"
|
:options="['Einzug','Kreditkarte','Überweisung','Sonstiges']"
|
||||||
v-model="itemInfo.paymentType"
|
v-model="itemInfo.paymentType"
|
||||||
|
:disabled="mode === 'show'"
|
||||||
/>
|
/>
|
||||||
</UFormGroup>
|
</UFormGroup>
|
||||||
|
|
||||||
<UFormGroup label="Beschreibung:" >
|
<UFormGroup label="Beschreibung:" >
|
||||||
<UTextarea
|
<UTextarea
|
||||||
v-model="itemInfo.description"
|
v-model="itemInfo.description"
|
||||||
|
:disabled="mode === 'show'"
|
||||||
/>
|
/>
|
||||||
</UFormGroup>
|
</UFormGroup>
|
||||||
|
|
||||||
@@ -329,12 +361,14 @@ const findIncomingInvoiceErrors = computed(() => {
|
|||||||
<UButton
|
<UButton
|
||||||
:variant="!useNetMode ? 'solid' : 'outline'"
|
:variant="!useNetMode ? 'solid' : 'outline'"
|
||||||
@click="changeNetMode(false)"
|
@click="changeNetMode(false)"
|
||||||
|
:disabled="mode === 'show'"
|
||||||
>
|
>
|
||||||
Brutto
|
Brutto
|
||||||
</UButton>
|
</UButton>
|
||||||
<UButton
|
<UButton
|
||||||
:variant="useNetMode ? 'solid' : 'outline'"
|
:variant="useNetMode ? 'solid' : 'outline'"
|
||||||
@click="changeNetMode(true)"
|
@click="changeNetMode(true)"
|
||||||
|
:disabled="mode === 'show'"
|
||||||
>
|
>
|
||||||
Netto
|
Netto
|
||||||
</UButton>
|
</UButton>
|
||||||
@@ -377,6 +411,7 @@ const findIncomingInvoiceErrors = computed(() => {
|
|||||||
option-attribute="label"
|
option-attribute="label"
|
||||||
value-attribute="id"
|
value-attribute="id"
|
||||||
searchable
|
searchable
|
||||||
|
:disabled="mode === 'show'"
|
||||||
:search-attributes="['label']"
|
:search-attributes="['label']"
|
||||||
searchable-placeholder="Suche..."
|
searchable-placeholder="Suche..."
|
||||||
v-model="item.account"
|
v-model="item.account"
|
||||||
@@ -398,6 +433,7 @@ const findIncomingInvoiceErrors = computed(() => {
|
|||||||
option-attribute="name"
|
option-attribute="name"
|
||||||
value-attribute="id"
|
value-attribute="id"
|
||||||
searchable
|
searchable
|
||||||
|
:disabled="mode === 'show'"
|
||||||
:search-attributes="['label']"
|
:search-attributes="['label']"
|
||||||
searchable-placeholder="Suche..."
|
searchable-placeholder="Suche..."
|
||||||
v-model="item.costCentre"
|
v-model="item.costCentre"
|
||||||
@@ -417,7 +453,7 @@ const findIncomingInvoiceErrors = computed(() => {
|
|||||||
<UButton
|
<UButton
|
||||||
variant="outline"
|
variant="outline"
|
||||||
color="rose"
|
color="rose"
|
||||||
v-if="item.costCentre"
|
v-if="item.costCentre && mode !== 'show'"
|
||||||
icon="i-heroicons-x-mark"
|
icon="i-heroicons-x-mark"
|
||||||
@click="item.costCentre = null"
|
@click="item.costCentre = null"
|
||||||
/>
|
/>
|
||||||
@@ -431,11 +467,12 @@ const findIncomingInvoiceErrors = computed(() => {
|
|||||||
<UInput
|
<UInput
|
||||||
v-model="item.description"
|
v-model="item.description"
|
||||||
class="flex-auto"
|
class="flex-auto"
|
||||||
|
:disabled="mode === 'show'"
|
||||||
></UInput>
|
></UInput>
|
||||||
<UButton
|
<UButton
|
||||||
variant="outline"
|
variant="outline"
|
||||||
color="rose"
|
color="rose"
|
||||||
v-if="item.description"
|
v-if="item.description && mode !== 'show'"
|
||||||
icon="i-heroicons-x-mark"
|
icon="i-heroicons-x-mark"
|
||||||
@click="item.description = null"
|
@click="item.description = null"
|
||||||
/>
|
/>
|
||||||
@@ -457,7 +494,7 @@ const findIncomingInvoiceErrors = computed(() => {
|
|||||||
step="0.01"
|
step="0.01"
|
||||||
v-model="item.amountNet"
|
v-model="item.amountNet"
|
||||||
:color="!item.amountNet ? 'rose' : 'primary'"
|
:color="!item.amountNet ? 'rose' : 'primary'"
|
||||||
:disabled="item.taxType === null"
|
:disabled="item.taxType === null || mode === 'show'"
|
||||||
@keyup="item.amountTax = Number((item.amountNet * (Number(taxOptions.find(i => i.key === item.taxType).percentage)/100)).toFixed(2)),
|
@keyup="item.amountTax = Number((item.amountNet * (Number(taxOptions.find(i => i.key === item.taxType).percentage)/100)).toFixed(2)),
|
||||||
item.amountGross = Number(item.amountNet) + NUmber(item.amountTax)"
|
item.amountGross = Number(item.amountNet) + NUmber(item.amountTax)"
|
||||||
>
|
>
|
||||||
@@ -476,7 +513,7 @@ const findIncomingInvoiceErrors = computed(() => {
|
|||||||
<UInput
|
<UInput
|
||||||
type="number"
|
type="number"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
:disabled="item.taxType === null"
|
:disabled="item.taxType === null || mode === 'show'"
|
||||||
v-model="item.amountGross"
|
v-model="item.amountGross"
|
||||||
:color="!item.amountGross ? 'rose' : 'primary'"
|
:color="!item.amountGross ? 'rose' : 'primary'"
|
||||||
:ui-menu="{ width: 'min-w-max' }"
|
:ui-menu="{ width: 'min-w-max' }"
|
||||||
@@ -496,6 +533,8 @@ const findIncomingInvoiceErrors = computed(() => {
|
|||||||
>
|
>
|
||||||
<USelectMenu
|
<USelectMenu
|
||||||
:options="taxOptions"
|
:options="taxOptions"
|
||||||
|
:disabled="mode === 'show'"
|
||||||
|
:color="item.taxType === null || item.taxType === '0' ? 'rose' : 'primary'"
|
||||||
v-model="item.taxType"
|
v-model="item.taxType"
|
||||||
value-attribute="key"
|
value-attribute="key"
|
||||||
:ui-menu="{ width: 'min-w-max' }"
|
:ui-menu="{ width: 'min-w-max' }"
|
||||||
@@ -513,12 +552,13 @@ const findIncomingInvoiceErrors = computed(() => {
|
|||||||
|
|
||||||
<UButton
|
<UButton
|
||||||
class="mt-3"
|
class="mt-3"
|
||||||
|
v-if="mode !== 'show'"
|
||||||
@click="itemInfo.accounts = [...itemInfo.accounts.slice(0,index+1),{account:null, amountNet: null, amountTax:null, taxType: '19'} , ...itemInfo.accounts.slice(index+1)]"
|
@click="itemInfo.accounts = [...itemInfo.accounts.slice(0,index+1),{account:null, amountNet: null, amountTax:null, taxType: '19'} , ...itemInfo.accounts.slice(index+1)]"
|
||||||
>
|
>
|
||||||
Betrag aufteilen
|
Betrag aufteilen
|
||||||
</UButton>
|
</UButton>
|
||||||
<UButton
|
<UButton
|
||||||
v-if="index !== 0"
|
v-if="index !== 0 && mode !== 'show'"
|
||||||
class="mt-3"
|
class="mt-3"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
color="rose"
|
color="rose"
|
||||||
@@ -532,6 +572,7 @@ const findIncomingInvoiceErrors = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<UProgress v-else animation="carousel"/>
|
||||||
</UDashboardPanelContent>
|
</UDashboardPanelContent>
|
||||||
|
|
||||||
|
|
||||||
@@ -549,8 +590,6 @@ const findIncomingInvoiceErrors = computed(() => {
|
|||||||
|
|
||||||
.scrollContainer {
|
.scrollContainer {
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
padding-left: 1em;
|
|
||||||
padding-right: 1em;
|
|
||||||
height: 70vh;
|
height: 70vh;
|
||||||
-ms-overflow-style: none; /* IE and Edge */
|
-ms-overflow-style: none; /* IE and Edge */
|
||||||
scrollbar-width: none; /* Firefox */
|
scrollbar-width: none; /* Firefox */
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const dataStore = useDataStore()
|
|
||||||
const profileStore = useProfileStore()
|
|
||||||
const route = useRoute()
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
//Working
|
|
||||||
const mode = ref(route.params.mode || "show")
|
|
||||||
|
|
||||||
const itemInfo = ref({
|
|
||||||
vendor: 0,
|
|
||||||
expense: true,
|
|
||||||
reference: "",
|
|
||||||
date: null,
|
|
||||||
dueDate: null,
|
|
||||||
paymentType: "Überweisung",
|
|
||||||
description: "",
|
|
||||||
state: "Entwurf",
|
|
||||||
accounts: [
|
|
||||||
{
|
|
||||||
account: null,
|
|
||||||
amountNet: null,
|
|
||||||
amountTax: null,
|
|
||||||
taxType: "19",
|
|
||||||
costCentre: null
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//Functions
|
|
||||||
|
|
||||||
const currentDocument = ref(null)
|
|
||||||
|
|
||||||
const loading = ref(true)
|
|
||||||
|
|
||||||
const setupPage = async () => {
|
|
||||||
if((mode.value === "show") && route.params.id){
|
|
||||||
itemInfo.value = await useEntities("incominginvoices").selectSingle(route.params.id,"*, files(*), vendor(*)")
|
|
||||||
if(process.dev) console.log(itemInfo.value)
|
|
||||||
currentDocument.value = await useFiles().selectDocument(itemInfo.value.files[0].id)
|
|
||||||
}
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const setState = async (newState) => {
|
|
||||||
|
|
||||||
let item = itemInfo.value
|
|
||||||
delete item.files
|
|
||||||
if(item.vendor.id) item.vendor = item.vendor.id
|
|
||||||
item.state = newState
|
|
||||||
|
|
||||||
await useEntities('incominginvoices').update(route.params.id,item)
|
|
||||||
|
|
||||||
await router.push("/incomingInvoices")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
setupPage()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<UDashboardNavbar :title="'Eingangsbeleg anzeigen'">
|
|
||||||
<template #left>
|
|
||||||
<UButton
|
|
||||||
to="/incominginvoices"
|
|
||||||
icon="i-heroicons-chevron-left"
|
|
||||||
variant="outline"
|
|
||||||
>
|
|
||||||
Übersicht
|
|
||||||
</UButton>
|
|
||||||
</template>
|
|
||||||
<template #right>
|
|
||||||
<UButton
|
|
||||||
@click="router.push(`/incomingInvoices/edit/${itemInfo.id}`)"
|
|
||||||
v-if="itemInfo.state !== 'Gebucht'"
|
|
||||||
>
|
|
||||||
Bearbeiten
|
|
||||||
</UButton>
|
|
||||||
<UButton
|
|
||||||
@click="setState('Gebucht')"
|
|
||||||
v-if="itemInfo.state !== 'Gebucht'"
|
|
||||||
color="rose"
|
|
||||||
>
|
|
||||||
Status auf Gebucht
|
|
||||||
</UButton>
|
|
||||||
</template>
|
|
||||||
</UDashboardNavbar>
|
|
||||||
<UDashboardPanelContent v-if="!loading">
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="flex justify-between mt-5"
|
|
||||||
>
|
|
||||||
<object
|
|
||||||
v-if="currentDocument ? currentDocument.url : false"
|
|
||||||
:data="currentDocument.url + '#toolbar=0&navpanes=0&scrollbar=0&statusbar=0&messages=0&scrollbar=0'"
|
|
||||||
type="application/pdf"
|
|
||||||
class="mx-5 w-2/5 documentPreview"
|
|
||||||
/>
|
|
||||||
<div class="w-1/2 mx-5">
|
|
||||||
<UCard class="truncate mb-5">
|
|
||||||
<p>Status: {{itemInfo.state}}</p>
|
|
||||||
<p>Datum: {{dayjs(itemInfo.date).format('DD.MM.YYYY')}}</p>
|
|
||||||
<p>Fälligkeitsdatum: {{dayjs(itemInfo.dueDate).format('DD.MM.YYYY')}}</p>
|
|
||||||
<p v-if="itemInfo.vendor">Lieferant: <nuxt-link :to="`/standardEntity/vendors/show/${itemInfo.vendor.id}`">{{itemInfo.vendor.name}}</nuxt-link></p>
|
|
||||||
<p>Bezahlt: {{itemInfo.paid ? "Ja" : "Nein"}}</p>
|
|
||||||
<p>Beschreibung: {{itemInfo.description}}</p>
|
|
||||||
|
|
||||||
<!-- TODO: Buchungszeilen darstellen -->
|
|
||||||
</UCard>
|
|
||||||
|
|
||||||
<UCard class="scrollContainer">
|
|
||||||
<HistoryDisplay
|
|
||||||
type="incomingInvoice"
|
|
||||||
v-if="itemInfo"
|
|
||||||
:element-id="itemInfo.id"
|
|
||||||
render-headline
|
|
||||||
/>
|
|
||||||
</UCard>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</UDashboardPanelContent>
|
|
||||||
<UProgress class="mt-3 mx-3" v-else animation="carousel"/>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.documentPreview {
|
|
||||||
aspect-ratio: 1 / 1.414;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.scrollContainer {
|
|
||||||
overflow-y: scroll;
|
|
||||||
padding-left: 1em;
|
|
||||||
padding-right: 1em;
|
|
||||||
height: 75vh;
|
|
||||||
-ms-overflow-style: none; /* IE and Edge */
|
|
||||||
scrollbar-width: none; /* Firefox */
|
|
||||||
}
|
|
||||||
|
|
||||||
.scrollContainer::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.lineItemRow {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ const { $dayjs } = useNuxtApp()
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
|
const toast = useToast()
|
||||||
// 🔹 State
|
// 🔹 State
|
||||||
const workingtimes = ref([])
|
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')
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,10 +84,32 @@ async function generateDocument() {
|
|||||||
|
|
||||||
uri.value = await useFunctions().useCreatePDF({
|
uri.value = await useFunctions().useCreatePDF({
|
||||||
full_name: profile.value.full_name,
|
full_name: profile.value.full_name,
|
||||||
|
employee_number: profile.value.employee_number ? profile.value.employee_number : "-",
|
||||||
...workingTimeInfo.value}, path, "timesheet")
|
...workingTimeInfo.value}, path, "timesheet")
|
||||||
|
|
||||||
|
|
||||||
showDocument.value = true
|
showDocument.value = true
|
||||||
|
}
|
||||||
|
const fileSaved = ref(false)
|
||||||
|
async function saveFile() {
|
||||||
|
try {
|
||||||
|
let fileData = {
|
||||||
|
auth_profile: profile.value.id,
|
||||||
|
tenant: auth.activeTenant
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = useFiles().dataURLtoFile(uri.value, `${profile.value.full_name}-${$dayjs(selectedStartDay.value).format("YYYY-MM-DD")}-${$dayjs(selectedEndDay.value).format("YYYY-MM-DD")}.pdf`)
|
||||||
|
|
||||||
|
await useFiles().uploadFiles(fileData, [file])
|
||||||
|
|
||||||
|
toast.add({title:"Auswertung erfolgreich gespeichert"})
|
||||||
|
fileSaved.value = true
|
||||||
|
} catch (error) {
|
||||||
|
toast.add({title:"Fehler beim Speichern der Auswertung", color: "rose"})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onTabChange(index: number) {
|
async function onTabChange(index: number) {
|
||||||
@@ -97,29 +122,30 @@ changeRange()
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UDashboardNavbar :ui="{ center: 'flex items-stretch gap-1.5 min-w-0' }">
|
<template v-if="!platformIsNative">
|
||||||
<template #left>
|
<UDashboardNavbar :ui="{ center: 'flex items-stretch gap-1.5 min-w-0' }">
|
||||||
<UButton
|
<template #left>
|
||||||
icon="i-heroicons-chevron-left"
|
<UButton
|
||||||
variant="outline"
|
icon="i-heroicons-chevron-left"
|
||||||
@click="router.push('/staff/time')"
|
variant="outline"
|
||||||
>
|
@click="router.push('/staff/time')"
|
||||||
Anwesenheiten
|
>
|
||||||
</UButton>
|
Anwesenheiten
|
||||||
</template>
|
</UButton>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template #center>
|
<template #center>
|
||||||
<h1 class="text-xl font-medium truncate">
|
<h1 class="text-xl font-medium truncate">
|
||||||
Auswertung Anwesenheiten: {{ profile?.full_name || '' }}
|
Auswertung Anwesenheiten: {{ profile?.full_name || '' }}
|
||||||
</h1>
|
</h1>
|
||||||
</template>
|
</template>
|
||||||
</UDashboardNavbar>
|
</UDashboardNavbar>
|
||||||
|
|
||||||
<UDashboardToolbar>
|
<UDashboardToolbar>
|
||||||
<template #left>
|
<template #left>
|
||||||
<UFormGroup label="Zeitraum:">
|
<UFormGroup label="Zeitraum:">
|
||||||
<USelectMenu
|
<USelectMenu
|
||||||
:options="[
|
:options="[
|
||||||
'Dieser Monat bis heute',
|
'Dieser Monat bis heute',
|
||||||
'Diese Woche',
|
'Diese Woche',
|
||||||
'Dieser Monat',
|
'Dieser Monat',
|
||||||
@@ -128,101 +154,265 @@ changeRange()
|
|||||||
'Letzter Monat',
|
'Letzter Monat',
|
||||||
'Letztes Jahr'
|
'Letztes Jahr'
|
||||||
]"
|
]"
|
||||||
v-model="selectedPresetRange"
|
v-model="selectedPresetRange"
|
||||||
@change="changeRange"
|
@change="changeRange"
|
||||||
/>
|
|
||||||
</UFormGroup>
|
|
||||||
|
|
||||||
<UFormGroup label="Start:">
|
|
||||||
<UPopover :popper="{ placement: 'bottom-start' }">
|
|
||||||
<UButton
|
|
||||||
icon="i-heroicons-calendar-days-20-solid"
|
|
||||||
:label="selectedStartDay ? $dayjs(selectedStartDay).format('DD.MM.YYYY') : 'Datum wählen'"
|
|
||||||
/>
|
/>
|
||||||
<template #panel="{ close }">
|
</UFormGroup>
|
||||||
<LazyDatePicker v-model="selectedStartDay" @close="loadWorkingTimeInfo" />
|
|
||||||
</template>
|
|
||||||
</UPopover>
|
|
||||||
</UFormGroup>
|
|
||||||
|
|
||||||
<UFormGroup label="Ende:">
|
<UFormGroup label="Start:">
|
||||||
<UPopover :popper="{ placement: 'bottom-start' }">
|
<UPopover :popper="{ placement: 'bottom-start' }">
|
||||||
<UButton
|
<UButton
|
||||||
icon="i-heroicons-calendar-days-20-solid"
|
icon="i-heroicons-calendar-days-20-solid"
|
||||||
:label="selectedEndDay ? $dayjs(selectedEndDay).format('DD.MM.YYYY') : 'Datum wählen'"
|
:label="selectedStartDay ? $dayjs(selectedStartDay).format('DD.MM.YYYY') : 'Datum wählen'"
|
||||||
/>
|
/>
|
||||||
<template #panel="{ close }">
|
<template #panel="{ close }">
|
||||||
<LazyDatePicker v-model="selectedEndDay" @close="loadWorkingTimeInfo" />
|
<LazyDatePicker v-model="selectedStartDay" @close="loadWorkingTimeInfo" />
|
||||||
</template>
|
|
||||||
</UPopover>
|
|
||||||
</UFormGroup>
|
|
||||||
</template>
|
|
||||||
</UDashboardToolbar>
|
|
||||||
|
|
||||||
<UDashboardPanelContent>
|
|
||||||
<UTabs
|
|
||||||
:items="[{ label: 'Information' }, { label: 'Bericht' }]"
|
|
||||||
v-model="openTab"
|
|
||||||
@change="onTabChange"
|
|
||||||
>
|
|
||||||
<template #item="{ item }">
|
|
||||||
<div v-if="item.label === 'Information'">
|
|
||||||
<UCard v-if="workingTimeInfo" class="my-5">
|
|
||||||
<template #header>
|
|
||||||
<h3 class="text-base font-semibold">Zusammenfassung</h3>
|
|
||||||
</template>
|
</template>
|
||||||
<div class="grid grid-cols-2 gap-3 text-sm">
|
</UPopover>
|
||||||
<p>Eingereicht: <b>{{ formatMinutesToHHMM(workingTimeInfo.sumWorkingMinutesEingereicht) }}</b></p>
|
</UFormGroup>
|
||||||
<p>Genehmigt: <b>{{ formatMinutesToHHMM(workingTimeInfo.sumWorkingMinutesApproved) }}</b></p>
|
|
||||||
<p>Feiertagsausgleich: <b>{{ formatMinutesToHHMM(workingTimeInfo.sumWorkingMinutesRecreationDays) }}</b> / {{ workingTimeInfo.sumRecreationDays }} Tage</p>
|
|
||||||
<p>Urlaubs-/Berufsschulausgleich: <b>{{ formatMinutesToHHMM(workingTimeInfo.sumWorkingMinutesVacationDays) }}</b> / {{ workingTimeInfo.sumVacationDays }} Tage</p>
|
|
||||||
<p>Krankheitsausgleich: <b>{{ formatMinutesToHHMM(workingTimeInfo.sumWorkingMinutesSickDays) }}</b> / {{ workingTimeInfo.sumSickDays }} Tage</p>
|
|
||||||
<p>Soll-Stunden: <b>{{ formatMinutesToHHMM(workingTimeInfo.timeSpanWorkingMinutes) }}</b></p>
|
|
||||||
<p class="col-span-2">
|
|
||||||
Inoffizielles Saldo: <b>{{ (workingTimeInfo.saldoInOfficial >= 0 ? '+' : '-') + formatMinutesToHHMM(Math.abs(workingTimeInfo.saldoInOfficial)) }}</b>
|
|
||||||
</p>
|
|
||||||
<p class="col-span-2">
|
|
||||||
Saldo: <b>{{ (workingTimeInfo.saldo >= 0 ? '+' : '-') + formatMinutesToHHMM(Math.abs(workingTimeInfo.saldo)) }}</b>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</UCard>
|
|
||||||
|
|
||||||
<UDashboardPanel>
|
<UFormGroup label="Ende:">
|
||||||
<UTable
|
<UPopover :popper="{ placement: 'bottom-start' }">
|
||||||
v-if="workingTimeInfo"
|
<UButton
|
||||||
:rows="workingTimeInfo.times"
|
icon="i-heroicons-calendar-days-20-solid"
|
||||||
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Anwesenheiten' }"
|
:label="selectedEndDay ? $dayjs(selectedEndDay).format('DD.MM.YYYY') : 'Datum wählen'"
|
||||||
:columns="[
|
/>
|
||||||
|
<template #panel="{ close }">
|
||||||
|
<LazyDatePicker v-model="selectedEndDay" @close="loadWorkingTimeInfo" />
|
||||||
|
</template>
|
||||||
|
</UPopover>
|
||||||
|
</UFormGroup>
|
||||||
|
</template>
|
||||||
|
<template #right>
|
||||||
|
<UTooltip
|
||||||
|
:text="fileSaved ? 'Bericht bereits gespeichert' : 'Bericht speichern'"
|
||||||
|
v-if="openTab === 1 && uri"
|
||||||
|
>
|
||||||
|
<UButton
|
||||||
|
icon="i-mdi-content-save"
|
||||||
|
:disabled="fileSaved"
|
||||||
|
@click="saveFile"
|
||||||
|
>Bericht</UButton>
|
||||||
|
</UTooltip>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
</UDashboardToolbar>
|
||||||
|
|
||||||
|
<UDashboardPanelContent>
|
||||||
|
<UTabs
|
||||||
|
:items="[{ label: 'Information' }, { label: 'Bericht' }]"
|
||||||
|
v-model="openTab"
|
||||||
|
@change="onTabChange"
|
||||||
|
>
|
||||||
|
<template #item="{ item }">
|
||||||
|
<div v-if="item.label === 'Information'">
|
||||||
|
<UCard v-if="workingTimeInfo" class="my-5">
|
||||||
|
<template #header>
|
||||||
|
<h3 class="text-base font-semibold">Zusammenfassung</h3>
|
||||||
|
</template>
|
||||||
|
<div class="grid grid-cols-2 gap-3 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-/Berufsschulausgleich: <b>{{ formatMinutesToHHMM(workingTimeInfo.sumWorkingMinutesVacationDays) }}</b> / {{ workingTimeInfo.sumVacationDays }} Tage</p>
|
||||||
|
<p>Krankheitsausgleich: <b>{{ formatMinutesToHHMM(workingTimeInfo.sumWorkingMinutesSickDays) }}</b> / {{ workingTimeInfo.sumSickDays }} Tage</p>
|
||||||
|
<p>Soll-Stunden: <b>{{ formatMinutesToHHMM(workingTimeInfo.timeSpanWorkingMinutes) }}</b></p>
|
||||||
|
<p class="col-span-2">
|
||||||
|
Inoffizielles Saldo: <b>{{ (workingTimeInfo.saldoInOfficial >= 0 ? '+' : '-') + formatMinutesToHHMM(Math.abs(workingTimeInfo.saldoInOfficial)) }}</b>
|
||||||
|
</p>
|
||||||
|
<p class="col-span-2">
|
||||||
|
Saldo: <b>{{ (workingTimeInfo.saldo >= 0 ? '+' : '-') + formatMinutesToHHMM(Math.abs(workingTimeInfo.saldo)) }}</b>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
<UDashboardPanel>
|
||||||
|
<UTable
|
||||||
|
v-if="workingTimeInfo"
|
||||||
|
:rows="workingTimeInfo.times"
|
||||||
|
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Anwesenheiten' }"
|
||||||
|
:columns="[
|
||||||
{ key: 'state', label: 'Status' },
|
{ key: 'state', label: 'Status' },
|
||||||
{ key: 'start', label: 'Start' },
|
{ key: 'start', label: 'Start' },
|
||||||
{ key: 'end', label: 'Ende' },
|
{ key: 'end', label: 'Ende' },
|
||||||
{ key: 'duration', label: 'Dauer' },
|
{ key: 'duration', label: 'Dauer' },
|
||||||
{ key: 'description', label: 'Beschreibung' }
|
{ key: 'description', label: 'Beschreibung' }
|
||||||
]"
|
]"
|
||||||
@select="(row) => router.push(`/workingtimes/edit/${row.id}`)"
|
@select="(row) => router.push(`/workingtimes/edit/${row.id}`)"
|
||||||
>
|
>
|
||||||
<template #state-data="{row}">
|
<template #state-data="{row}">
|
||||||
<span v-if="row.state === 'approved'" class="text-primary-500">Genehmigt</span>
|
<span v-if="row.state === 'approved'" class="text-primary-500">Genehmigt</span>
|
||||||
<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 #start-data="{ row }">
|
<template #start-data="{ row }">
|
||||||
{{ $dayjs(row.started_at).format('HH:mm DD.MM.YY') }} Uhr
|
{{ $dayjs(row.started_at).format('HH:mm DD.MM.YY') }} Uhr
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #end-data="{ row }">
|
<template #end-data="{ row }">
|
||||||
{{ $dayjs(row.stopped_at).format('HH:mm DD.MM.YY') }} Uhr
|
{{ $dayjs(row.stopped_at).format('HH:mm DD.MM.YY') }} Uhr
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #duration-data="{ row }">
|
<template #duration-data="{ row }">
|
||||||
{{ useFormatDuration(row.duration_minutes) }}
|
{{ useFormatDuration(row.duration_minutes) }}
|
||||||
</template>
|
</template>
|
||||||
</UTable>
|
</UTable>
|
||||||
</UDashboardPanel>
|
</UDashboardPanel>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="item.label === 'Bericht'">
|
||||||
|
<PDFViewer
|
||||||
|
v-if="showDocument"
|
||||||
|
:uri="uri"
|
||||||
|
location="show_time_evaluation"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UTabs>
|
||||||
|
</UDashboardPanelContent>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<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'">
|
<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
|
<PDFViewer
|
||||||
v-if="showDocument"
|
v-if="showDocument"
|
||||||
:uri="uri"
|
:uri="uri"
|
||||||
@@ -231,5 +421,7 @@ changeRange()
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</UTabs>
|
</UTabs>
|
||||||
</UDashboardPanelContent>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,23 +1,48 @@
|
|||||||
<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";
|
||||||
|
|
||||||
const { list, start, stop, submit,approve } = useStaffTime()
|
definePageMeta({
|
||||||
|
layout: "default",
|
||||||
|
})
|
||||||
|
|
||||||
|
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'))
|
||||||
|
|
||||||
|
const typeLabel = {
|
||||||
|
work: "Arbeitszeit",
|
||||||
|
vacation: "Urlaub",
|
||||||
|
sick: "Krankheit",
|
||||||
|
holiday: "Feiertag",
|
||||||
|
other: "Sonstiges"
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeColor = {
|
||||||
|
work: "gray",
|
||||||
|
vacation: "yellow",
|
||||||
|
sick: "rose",
|
||||||
|
holiday: "blue",
|
||||||
|
other: "gray"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async function loadUsers() {
|
async function loadUsers() {
|
||||||
if (!canViewAll.value) return
|
if (!canViewAll.value) return
|
||||||
// Beispiel: User aus Supabase holen
|
// Beispiel: User aus Supabase holen
|
||||||
@@ -25,15 +50,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,161 +89,351 @@ 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>
|
||||||
<UDashboardNavbar title="Zeiterfassung" :badge="entries.length" />
|
<!-- ============================= -->
|
||||||
|
<!-- DESKTOP VERSION -->
|
||||||
|
<!-- ============================= -->
|
||||||
|
<template v-if="!platformIsNative">
|
||||||
|
<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">
|
<UIcon name="i-heroicons-clock" class="text-primary-500" />
|
||||||
<UIcon name="i-heroicons-clock" class="text-primary-500" />
|
<span v-if="active" class="text-primary-600 font-medium">
|
||||||
<span v-if="active" class="text-primary-600 font-medium">
|
Läuft seit {{ useNuxtApp().$dayjs(active.started_at).format('HH:mm') }}
|
||||||
Läuft seit {{ useNuxtApp().$dayjs(active.started_at).format('HH:mm') }}
|
</span>
|
||||||
</span>
|
<span v-else class="text-gray-500">Keine aktive Zeit</span>
|
||||||
<span v-else class="text-gray-500">Keine aktive Zeit</span>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #right>
|
<template #right>
|
||||||
<UButton
|
<UButton
|
||||||
v-if="active"
|
v-if="active"
|
||||||
color="red"
|
color="red"
|
||||||
icon="i-heroicons-stop"
|
icon="i-heroicons-stop"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
label="Stoppen"
|
label="Stoppen"
|
||||||
@click="handleStop"
|
@click="handleStop"
|
||||||
/>
|
/>
|
||||||
<UButton
|
<UButton
|
||||||
v-else
|
v-else
|
||||||
color="green"
|
color="green"
|
||||||
icon="i-heroicons-play"
|
icon="i-heroicons-play"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
label="Starten"
|
label="Starten"
|
||||||
@click="handleStart"
|
@click="handleStart"
|
||||||
/>
|
/>
|
||||||
<UButton
|
<UButton
|
||||||
color="primary"
|
color="primary"
|
||||||
icon="i-heroicons-plus"
|
icon="i-heroicons-plus"
|
||||||
label="Zeit"
|
label="Zeit"
|
||||||
@click="() => { editEntry = null; showModal = true }"
|
@click="() => { editEntry = null; showModal = true }"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</UDashboardToolbar>
|
</UDashboardToolbar>
|
||||||
<UDashboardToolbar>
|
<UDashboardToolbar>
|
||||||
<template #left>
|
<template #left>
|
||||||
<!-- 👥 User-Filter (nur bei Berechtigung) -->
|
<!-- 👥 User-Filter (nur bei Berechtigung) -->
|
||||||
<div v-if="canViewAll" class="flex items-center gap-2">
|
<div v-if="canViewAll" class="flex items-center gap-2">
|
||||||
<USelectMenu
|
<USelectMenu
|
||||||
v-model="selectedUser"
|
v-model="selectedUser"
|
||||||
:options="[
|
:options="[
|
||||||
{ label: 'Alle Benutzer', value: null },
|
{ label: 'Alle Benutzer', value: null },
|
||||||
...users.map(u => ({ label: u.full_name || u.email, value: u.user_id }))
|
...users.map(u => ({ label: u.full_name || u.email, value: u.user_id }))
|
||||||
]"
|
]"
|
||||||
placeholder="Benutzer auswählen"
|
placeholder="Benutzer auswählen"
|
||||||
value-attribute="value"
|
value-attribute="value"
|
||||||
option-attribute="label"
|
option-attribute="label"
|
||||||
class="min-w-[220px]"
|
class="min-w-[220px]"
|
||||||
@change="load"
|
@change="load"
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 🔹 Button zur Auswertung -->
|
|
||||||
<UTooltip
|
|
||||||
:text="selectedUser ? 'Anwesenheiten des Mitarbeiters auswerten' : 'Mitarbeiter für die Auswertung auswählen'"
|
|
||||||
>
|
|
||||||
<UButton
|
|
||||||
:disabled="!selectedUser"
|
|
||||||
color="gray"
|
|
||||||
icon="i-heroicons-chart-bar"
|
|
||||||
label="Auswertung"
|
|
||||||
variant="soft"
|
|
||||||
@click="router.push(`/staff/time/${selectedUser}/evaluate`)"
|
|
||||||
/>
|
/>
|
||||||
</UTooltip>
|
|
||||||
|
|
||||||
|
<!-- 🔹 Button zur Auswertung -->
|
||||||
|
<UTooltip
|
||||||
|
:text="selectedUser ? 'Anwesenheiten des Mitarbeiters auswerten' : 'Mitarbeiter für die Auswertung auswählen'"
|
||||||
|
>
|
||||||
|
<UButton
|
||||||
|
:disabled="!selectedUser"
|
||||||
|
color="gray"
|
||||||
|
icon="i-heroicons-chart-bar"
|
||||||
|
label="Auswertung"
|
||||||
|
variant="soft"
|
||||||
|
@click="router.push(`/staff/time/${selectedUser}/evaluate`)"
|
||||||
|
/>
|
||||||
|
</UTooltip>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UDashboardToolbar>
|
||||||
|
|
||||||
|
|
||||||
|
<UDashboardPanelContent>
|
||||||
|
<UTable
|
||||||
|
:rows="entries"
|
||||||
|
:columns="[
|
||||||
|
{ key: 'actions', label: '' },
|
||||||
|
{ key: 'state', label: 'Status' },
|
||||||
|
{ key: 'started_at', label: 'Start' },
|
||||||
|
{ key: 'stopped_at', label: 'Ende' },
|
||||||
|
{ key: 'duration_minutes', label: 'Dauer' },
|
||||||
|
{ key: 'user', label: 'Mitarbeiter' },
|
||||||
|
{ key: 'type', label: 'Typ' },
|
||||||
|
{ key: 'description', label: 'Beschreibung' },
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<template #state-data="{ row }">
|
||||||
|
<span v-if="row.state === 'approved'" class="text-primary-500">Genehmigt</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>
|
||||||
|
</template>
|
||||||
|
<template #type-data="{ row }">
|
||||||
|
<UBadge :color="typeColor[row.type] || 'gray'">
|
||||||
|
{{ typeLabel[row.type] || row.type }}
|
||||||
|
</UBadge>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- START -->
|
||||||
|
<template #started_at-data="{ row }">
|
||||||
|
<!-- Urlaub / Krankheit → nur Tag -->
|
||||||
|
<span v-if="row.type === 'vacation' || row.type === 'sick'">
|
||||||
|
{{ useNuxtApp().$dayjs(row.started_at).format("DD.MM.YY") }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Arbeitszeit / andere → Datum + Uhrzeit -->
|
||||||
|
<span v-else>
|
||||||
|
{{ useNuxtApp().$dayjs(row.started_at).format("DD.MM.YY HH:mm") }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- ENDE -->
|
||||||
|
<template #stopped_at-data="{ row }">
|
||||||
|
<span v-if="!row.stopped_at" class="text-primary-500 font-medium">
|
||||||
|
läuft...
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Urlaub / Krankheit → nur Tag -->
|
||||||
|
<span v-else-if="row.type === 'vacation' || row.type === 'sick'">
|
||||||
|
{{ useNuxtApp().$dayjs(row.stopped_at).format("DD.MM.YY") }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Arbeitszeit / andere → Datum + Uhrzeit -->
|
||||||
|
<span v-else>
|
||||||
|
{{ useNuxtApp().$dayjs(row.stopped_at).format("DD.MM.YY HH:mm") }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #duration_minutes-data="{ row }">
|
||||||
|
|
||||||
|
<!-- Urlaub / Krankheit → Tage anzeigen -->
|
||||||
|
<span v-if="row.type === 'vacation' || row.type === 'sick'">
|
||||||
|
<!-- {{ useFormatDurationDays(row.startet_at, row.stopped_at) }}-->--
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Arbeitszeit / andere → Minutenformat -->
|
||||||
|
<span v-else>
|
||||||
|
{{ row.duration_minutes ? useFormatDuration(row.duration_minutes) : "-" }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #actions-data="{ row }">
|
||||||
|
<UTooltip text="Zeit genehmigen" v-if="row.state === 'submitted'">
|
||||||
|
<UButton
|
||||||
|
variant="ghost"
|
||||||
|
icon="i-heroicons-check-circle"
|
||||||
|
@click="handleApprove(row)"
|
||||||
|
/>
|
||||||
|
</UTooltip>
|
||||||
|
<UTooltip text="Zeit einreichen" v-if="row.state === 'draft'">
|
||||||
|
<UButton
|
||||||
|
variant="ghost"
|
||||||
|
icon="i-heroicons-arrow-right-end-on-rectangle"
|
||||||
|
@click="handleSubmit(row)"
|
||||||
|
/>
|
||||||
|
</UTooltip>
|
||||||
|
<UTooltip text="Zeit bearbeiten" v-if="row.state === 'draft'">
|
||||||
|
<UButton
|
||||||
|
variant="ghost"
|
||||||
|
icon="i-heroicons-pencil-square"
|
||||||
|
@click="handleEdit(row)"
|
||||||
|
/>
|
||||||
|
</UTooltip>
|
||||||
|
</template>
|
||||||
|
<template #user-data="{ row }">
|
||||||
|
{{users.find(i => i.user_id === row.user_id) ? users.find(i => i.user_id === row.user_id).full_name : ""}}
|
||||||
|
</template>
|
||||||
|
<template #description-data="{ row }">
|
||||||
|
<span v-if="row.type === 'vacation'">{{row.vacation_reason}}</span>
|
||||||
|
<span v-else-if="row.type === 'sick'">{{row.sick_reason}}</span>
|
||||||
|
<span v-else>{{row.description}}</span>
|
||||||
|
</template>
|
||||||
|
</UTable>
|
||||||
|
</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>
|
||||||
</template>
|
|
||||||
</UDashboardToolbar>
|
|
||||||
|
|
||||||
<!-- TABELLE -->
|
<div class="px-3 mt-3">
|
||||||
<UDashboardPanelContent>
|
<UButton
|
||||||
<UTable
|
color="gray"
|
||||||
:rows="entries"
|
icon="i-heroicons-chart-bar"
|
||||||
:columns="[
|
label="Eigene Auswertung"
|
||||||
{ key: 'actions', label: '' },
|
class="w-full"
|
||||||
{ key: 'state', label: 'Status' },
|
variant="soft"
|
||||||
{ key: 'started_at', label: 'Start' },
|
@click="router.push(`/staff/time/${auth.user.id}/evaluate`)"
|
||||||
{ key: 'stopped_at', label: 'Ende' },
|
/>
|
||||||
{ key: 'duration_minutes', label: 'Dauer' },
|
</div>
|
||||||
{ key: 'description', label: 'Beschreibung' },
|
|
||||||
...(canViewAll ? [{ key: 'user_name', label: 'Benutzer' }] : []),
|
|
||||||
|
|
||||||
]"
|
<!-- 📜 SCROLLABLE CONTENT -->
|
||||||
>
|
<UDashboardPanelContent class="flex-1 overflow-y-auto p-3 space-y-4 pb-24">
|
||||||
<template #state-data="{row}">
|
|
||||||
<span v-if="row.state === 'approved'" class="text-primary-500">Genehmigt</span>
|
<!-- ZEIT-CARDS -->
|
||||||
<span v-else-if="row.state === 'submitted'" class="text-cyan-500">Eingereicht</span>
|
<UCard
|
||||||
<span v-else-if="row.state === 'draft'" class="text-red-500">Entwurf</span>
|
v-for="row in entries"
|
||||||
</template>
|
:key="row.id"
|
||||||
<template #started_at-data="{ row }">
|
class="p-4 border rounded-xl active:scale-[0.98] transition cursor-pointer"
|
||||||
{{ useNuxtApp().$dayjs(row.started_at).format('DD.MM.YY HH:mm') }}
|
@click="handleEdit(row)"
|
||||||
</template>
|
|
||||||
<template #stopped_at-data="{ row }">
|
|
||||||
<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>
|
|
||||||
</template>
|
|
||||||
<template #duration_minutes-data="{ row }">
|
|
||||||
{{ 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 #actions-data="{ row }">
|
|
||||||
<UTooltip
|
|
||||||
text="Zeit genehmigen"
|
|
||||||
v-if="row.state === 'submitted'"
|
|
||||||
>
|
>
|
||||||
<UButton
|
<div class="flex justify-between items-center">
|
||||||
variant="ghost"
|
<div class="font-semibold flex items-center gap-2">
|
||||||
icon="i-heroicons-check-circle"
|
<span>{{ row.description || 'Keine Beschreibung' }}</span>
|
||||||
@click="handleApprove(row)"
|
|
||||||
|
|
||||||
/>
|
<UBadge
|
||||||
</UTooltip>
|
:color="typeColor[row.type]"
|
||||||
<UTooltip
|
class="text-xs"
|
||||||
text="Zeit einreichen"
|
>
|
||||||
v-if="row.state === 'draft'"
|
{{ typeLabel[row.type] }}
|
||||||
>
|
</UBadge>
|
||||||
<UButton
|
</div>
|
||||||
variant="ghost"
|
|
||||||
icon="i-heroicons-arrow-right-end-on-rectangle"
|
|
||||||
@click="handleSubmit(row)"
|
|
||||||
|
|
||||||
/>
|
<UBadge
|
||||||
</UTooltip>
|
:color="{
|
||||||
<UTooltip
|
approved: 'primary',
|
||||||
text="Zeit bearbeiten"
|
submitted: 'cyan',
|
||||||
v-if="row.state === 'draft'"
|
draft: 'red'
|
||||||
>
|
}[row.state]"
|
||||||
<UButton
|
>
|
||||||
variant="ghost"
|
{{
|
||||||
icon="i-heroicons-pencil-square"
|
{
|
||||||
@click="handleEdit(row)"
|
approved: 'Genehmigt',
|
||||||
/>
|
submitted: 'Eingereicht',
|
||||||
</UTooltip>
|
draft: 'Entwurf'
|
||||||
|
}[row.state] || row.state
|
||||||
|
}}
|
||||||
|
</UBadge>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
</template>
|
<p class="text-sm text-gray-500 mt-1">
|
||||||
</UTable>
|
Start: {{ useNuxtApp().$dayjs(row.started_at).format('DD.MM.YY HH:mm') }}
|
||||||
</UDashboardPanelContent>
|
</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"
|
||||||
|
:users="users"
|
||||||
|
:can-select-user="canViewAll"
|
||||||
|
:default-user-id="selectedUser"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -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,95 +292,96 @@ const handleFilterChange = async (action,column) => {
|
|||||||
</template>
|
</template>
|
||||||
</UDashboardToolbar>
|
</UDashboardToolbar>
|
||||||
|
|
||||||
<UTable
|
<div v-if="!platformIsNative">
|
||||||
:loading="loading"
|
<UTable
|
||||||
:loading-state="{ icon: 'i-heroicons-arrow-path-20-solid', label: 'Loading...' }"
|
:loading="loading"
|
||||||
sort-mode="manual"
|
:loading-state="{ icon: 'i-heroicons-arrow-path-20-solid', label: 'Loading...' }"
|
||||||
v-model:sort="sort"
|
sort-mode="manual"
|
||||||
@update:sort="setupPage"
|
v-model:sort="sort"
|
||||||
v-if="dataType && columns && items.length > 0 && !loading"
|
@update:sort="setupPage"
|
||||||
:rows="items"
|
v-if="dataType && columns && items.length > 0 && !loading"
|
||||||
:columns="columns"
|
:rows="items"
|
||||||
class="w-full"
|
:columns="columns"
|
||||||
style="height: 85dvh"
|
class="w-full"
|
||||||
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
|
style="height: 85dvh"
|
||||||
@select="(i) => router.push(`/standardEntity/${type}/show/${i.id}`) "
|
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
|
||||||
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: `Keine ${dataType.label} anzuzeigen` }"
|
@select="(i) => router.push(`/standardEntity/${type}/show/${i.id}`) "
|
||||||
>
|
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: `Keine ${dataType.label} anzuzeigen` }"
|
||||||
<template
|
>
|
||||||
v-for="column in dataType.templateColumns.filter(i => !i.disabledInTable)"
|
<template
|
||||||
v-slot:[`${column.key}-header`]="{row}">
|
v-for="column in dataType.templateColumns.filter(i => !i.disabledInTable)"
|
||||||
<InputGroup>
|
v-slot:[`${column.key}-header`]="{row}">
|
||||||
<UTooltip v-if="column.sortable">
|
<InputGroup>
|
||||||
|
<UTooltip v-if="column.sortable">
|
||||||
|
<UButton
|
||||||
|
variant="outline"
|
||||||
|
@click="changeSort(column.key)"
|
||||||
|
:color="sort.column === column.key ? 'primary' : 'white'"
|
||||||
|
:icon="sort.column === column.key ? (sort.direction === 'asc' ? 'i-heroicons-arrow-up' : 'i-heroicons-arrow-down') : 'i-heroicons-arrows-up-down'"
|
||||||
|
>
|
||||||
|
|
||||||
|
</UButton>
|
||||||
|
</UTooltip>
|
||||||
|
<UTooltip
|
||||||
|
v-if="column.distinct"
|
||||||
|
:text="!columnsToFilter[column.key]?.length > 0 ? `Keine Einträge für ${column.label} verfügbar` : `${column.label} Spalte nach Einträgen filtern`"
|
||||||
|
>
|
||||||
|
<USelectMenu
|
||||||
|
:options="itemsMeta?.distinctValues?.[column.key]"
|
||||||
|
v-model="columnsToFilter[column.key]"
|
||||||
|
multiple
|
||||||
|
@change="handleFilterChange('change', column.key)"
|
||||||
|
searchable
|
||||||
|
searchable-placeholder="Suche..."
|
||||||
|
:search-attributes="[column.key]"
|
||||||
|
:ui-menu="{ width: 'min-w-max' }"
|
||||||
|
clear-search-on-close
|
||||||
|
>
|
||||||
|
|
||||||
|
<template #empty>
|
||||||
|
Keine Einträge in der Spalte {{column.label}}
|
||||||
|
</template>
|
||||||
|
<template #default="{open}">
|
||||||
|
<UButton
|
||||||
|
:disabled="!columnsToFilter[column.key]?.length > 0"
|
||||||
|
:variant="columnsToFilter[column.key]?.length !== itemsMeta.distinctValues?.[column.key]?.length ? 'outline' : 'solid'"
|
||||||
|
:color="columnsToFilter[column.key]?.length !== itemsMeta.distinctValues?.[column.key]?.length ? 'primary' : 'white'"
|
||||||
|
>
|
||||||
|
<span class="truncate">{{ column.label }}</span>
|
||||||
|
|
||||||
|
<UIcon name="i-heroicons-chevron-right-20-solid" class="w-5 h-5 transition-transform text-gray-400 dark:text-gray-500" :class="[open && 'transform rotate-90']" />
|
||||||
|
</UButton>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
</USelectMenu>
|
||||||
|
</UTooltip>
|
||||||
<UButton
|
<UButton
|
||||||
variant="outline"
|
variant="solid"
|
||||||
@click="changeSort(column.key)"
|
color="white"
|
||||||
:color="sort.column === column.key ? 'primary' : 'white'"
|
v-else
|
||||||
:icon="sort.column === column.key ? (sort.direction === 'asc' ? 'i-heroicons-arrow-up' : 'i-heroicons-arrow-down') : 'i-heroicons-arrows-up-down'"
|
class="mr-2 truncate"
|
||||||
|
>{{column.label}}</UButton>
|
||||||
|
|
||||||
|
|
||||||
|
<UTooltip
|
||||||
|
text="Filter zurücksetzen"
|
||||||
|
v-if="columnsToFilter[column.key]?.length !== itemsMeta.distinctValues?.[column.key]?.length && column.distinct"
|
||||||
>
|
>
|
||||||
|
<UButton
|
||||||
</UButton>
|
@click="handleFilterChange('reset',column.key)"
|
||||||
</UTooltip>
|
variant="outline"
|
||||||
<UTooltip
|
color="rose"
|
||||||
v-if="column.distinct"
|
>
|
||||||
:text="!columnsToFilter[column.key]?.length > 0 ? `Keine Einträge für ${column.label} verfügbar` : `${column.label} Spalte nach Einträgen filtern`"
|
X
|
||||||
>
|
</UButton>
|
||||||
<USelectMenu
|
</UTooltip>
|
||||||
:options="itemsMeta?.distinctValues?.[column.key]"
|
|
||||||
v-model="columnsToFilter[column.key]"
|
|
||||||
multiple
|
|
||||||
@change="handleFilterChange('change', column.key)"
|
|
||||||
searchable
|
|
||||||
searchable-placeholder="Suche..."
|
|
||||||
:search-attributes="[column.key]"
|
|
||||||
:ui-menu="{ width: 'min-w-max' }"
|
|
||||||
clear-search-on-close
|
|
||||||
>
|
|
||||||
|
|
||||||
<template #empty>
|
|
||||||
Keine Einträge in der Spalte {{column.label}}
|
|
||||||
</template>
|
|
||||||
<template #default="{open}">
|
|
||||||
<UButton
|
|
||||||
:disabled="!columnsToFilter[column.key]?.length > 0"
|
|
||||||
:variant="columnsToFilter[column.key]?.length !== itemsMeta.distinctValues?.[column.key]?.length ? 'outline' : 'solid'"
|
|
||||||
:color="columnsToFilter[column.key]?.length !== itemsMeta.distinctValues?.[column.key]?.length ? 'primary' : 'white'"
|
|
||||||
>
|
|
||||||
<span class="truncate">{{ column.label }}</span>
|
|
||||||
|
|
||||||
<UIcon name="i-heroicons-chevron-right-20-solid" class="w-5 h-5 transition-transform text-gray-400 dark:text-gray-500" :class="[open && 'transform rotate-90']" />
|
|
||||||
</UButton>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
|
|
||||||
</USelectMenu>
|
</InputGroup>
|
||||||
</UTooltip>
|
|
||||||
<UButton
|
|
||||||
variant="solid"
|
|
||||||
color="white"
|
|
||||||
v-else
|
|
||||||
class="mr-2 truncate"
|
|
||||||
>{{column.label}}</UButton>
|
|
||||||
|
|
||||||
|
</template>
|
||||||
<UTooltip
|
<template #name-data="{row}">
|
||||||
text="Filter zurücksetzen"
|
|
||||||
v-if="columnsToFilter[column.key]?.length !== itemsMeta.distinctValues?.[column.key]?.length && column.distinct"
|
|
||||||
>
|
|
||||||
<UButton
|
|
||||||
@click="handleFilterChange('reset',column.key)"
|
|
||||||
variant="outline"
|
|
||||||
color="rose"
|
|
||||||
>
|
|
||||||
X
|
|
||||||
</UButton>
|
|
||||||
</UTooltip>
|
|
||||||
|
|
||||||
|
|
||||||
</InputGroup>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
<template #name-data="{row}">
|
|
||||||
<span
|
<span
|
||||||
v-if="row.id === items[selectedItem].id"
|
v-if="row.id === items[selectedItem].id"
|
||||||
class="text-primary-500 font-bold">
|
class="text-primary-500 font-bold">
|
||||||
@@ -396,61 +390,236 @@ const handleFilterChange = async (action,column) => {
|
|||||||
>
|
>
|
||||||
{{dataType.templateColumns.find(i => i.key === "name").maxLength ? (row.name.length > dataType.templateColumns.find(i => i.key === "name").maxLength ? `${row.name.substring(0,dataType.templateColumns.find(i => i.key === "name").maxLength)}...` : row.name ) : row.name}}
|
{{dataType.templateColumns.find(i => i.key === "name").maxLength ? (row.name.length > dataType.templateColumns.find(i => i.key === "name").maxLength ? `${row.name.substring(0,dataType.templateColumns.find(i => i.key === "name").maxLength)}...` : row.name ) : row.name}}
|
||||||
</UTooltip> </span>
|
</UTooltip> </span>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
<UTooltip
|
<UTooltip
|
||||||
:text="row.name"
|
:text="row.name"
|
||||||
>
|
>
|
||||||
{{dataType.templateColumns.find(i => i.key === "name").maxLength ? (row.name.length > dataType.templateColumns.find(i => i.key === "name").maxLength ? `${row.name.substring(0,dataType.templateColumns.find(i => i.key === "name").maxLength)}...` : row.name ) : row.name}}
|
{{dataType.templateColumns.find(i => i.key === "name").maxLength ? (row.name.length > dataType.templateColumns.find(i => i.key === "name").maxLength ? `${row.name.substring(0,dataType.templateColumns.find(i => i.key === "name").maxLength)}...` : row.name ) : row.name}}
|
||||||
</UTooltip>
|
</UTooltip>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<template #fullName-data="{row}">
|
<template #fullName-data="{row}">
|
||||||
<span
|
<span
|
||||||
v-if="row.id === items[selectedItem].id"
|
v-if="row.id === items[selectedItem].id"
|
||||||
class="text-primary-500 font-bold">{{row.fullName}}
|
class="text-primary-500 font-bold">{{row.fullName}}
|
||||||
</span>
|
</span>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
{{row.fullName}}
|
{{row.fullName}}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<template #licensePlate-data="{row}">
|
<template #licensePlate-data="{row}">
|
||||||
<span
|
<span
|
||||||
v-if="row.id === items[selectedItem].id"
|
v-if="row.id === items[selectedItem].id"
|
||||||
class="text-primary-500 font-bold">{{row.licensePlate}}
|
class="text-primary-500 font-bold">{{row.licensePlate}}
|
||||||
</span>
|
</span>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
{{row.licensePlate}}
|
{{row.licensePlate}}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<template
|
<template
|
||||||
v-for="column in dataType.templateColumns.filter(i => i.key !== 'name' && i.key !== 'fullName' && i.key !== 'licensePlate' && !i.disabledInTable)"
|
v-for="column in dataType.templateColumns.filter(i => i.key !== 'name' && i.key !== 'fullName' && i.key !== 'licensePlate' && !i.disabledInTable)"
|
||||||
v-slot:[`${column.key}-data`]="{row}">
|
v-slot:[`${column.key}-data`]="{row}">
|
||||||
<component v-if="column.component" :is="column.component" :row="row"></component>
|
<component v-if="column.component" :is="column.component" :row="row"></component>
|
||||||
<span v-else-if="row[column.key]">
|
<span v-else-if="row[column.key]">
|
||||||
<UTooltip :text="row[column.key]">
|
<UTooltip :text="row[column.key]">
|
||||||
{{row[column.key] ? `${column.maxLength ? (row[column.key].length > column.maxLength ? `${row[column.key].substring(0,column.maxLength)}...` : row[column.key]) : row[column.key]} ${column.unit ? column.unit : ''}`: ''}}
|
{{row[column.key] ? `${column.maxLength ? (row[column.key].length > column.maxLength ? `${row[column.key].substring(0,column.maxLength)}...` : row[column.key]) : row[column.key]} ${column.unit ? column.unit : ''}`: ''}}
|
||||||
</UTooltip>
|
</UTooltip>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
</UTable>
|
</UTable>
|
||||||
<UCard
|
<UCard
|
||||||
class="w-1/3 mx-auto mt-10"
|
class="w-1/3 mx-auto mt-10"
|
||||||
v-else-if="!loading"
|
v-else-if="!loading"
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="flex flex-col text-center"
|
|
||||||
>
|
>
|
||||||
<UIcon
|
<div
|
||||||
class="mx-auto w-10 h-10 mb-5"
|
class="flex flex-col text-center"
|
||||||
name="i-heroicons-circle-stack-20-solid"/>
|
>
|
||||||
<span class="font-bold">Keine {{dataType.label}} anzuzeigen</span>
|
<UIcon
|
||||||
|
class="mx-auto w-10 h-10 mb-5"
|
||||||
|
name="i-heroicons-circle-stack-20-solid"/>
|
||||||
|
<span class="font-bold">Keine {{dataType.label}} anzuzeigen</span>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</UCard>
|
||||||
|
<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>
|
</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>
|
<UCard
|
||||||
<UProgress v-else animation="carousel" class="w-3/4 mx-auto mt-5"></UProgress>
|
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) {
|
||||||
console.log("Auth login")
|
try {
|
||||||
const { token } = await useNuxtApp().$api("/auth/login", {
|
console.log("Auth login")
|
||||||
method: "POST",
|
const { token } = await useNuxtApp().$api("/auth/login", {
|
||||||
body: { email, password }
|
method: "POST",
|
||||||
})
|
body: { email, password }
|
||||||
console.log(token)
|
})
|
||||||
await this.fetchMe(token)
|
console.log("Token: " + 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