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>
|
||||
|
||||
const props = defineProps({
|
||||
queryStringData: {
|
||||
type: String
|
||||
},
|
||||
item: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
topLevelType: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
platform: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
item: { type: Object, required: true },
|
||||
type: { type: String, required: true },
|
||||
topLevelType: { type: String, required: true },
|
||||
platform: { type: String, required: true }
|
||||
})
|
||||
|
||||
const emit = defineEmits(["updateNeeded"])
|
||||
|
||||
const files = useFiles()
|
||||
|
||||
|
||||
const availableFiles = ref([])
|
||||
const activeFile = ref(null)
|
||||
const showViewer = ref(false)
|
||||
|
||||
const setup = async () => {
|
||||
if(props.item.files) {
|
||||
availableFiles.value = (await files.selectSomeDocuments(props.item.files.map(i => i.id))) || []
|
||||
if (props.item.files?.length > 0) {
|
||||
availableFiles.value =
|
||||
(await files.selectSomeDocuments(props.item.files.map((f) => f.id))) || []
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
<template>
|
||||
<UCard class="mt-5" :style="props.platform !== 'mobile' ? 'height: 80vh' : ''">
|
||||
<template #header v-if="props.platform === 'mobile'">
|
||||
<template #header>
|
||||
<span>Dateien</span>
|
||||
</template>
|
||||
|
||||
<!-- Upload -->
|
||||
<Toolbar>
|
||||
<DocumentUpload
|
||||
:type="props.topLevelType.substring(0,props.topLevelType.length-1)"
|
||||
:type="props.topLevelType.substring(0, props.topLevelType.length - 1)"
|
||||
:element-id="props.item.id"
|
||||
@uploadFinished="emit('updateNeeded')"
|
||||
/>
|
||||
</Toolbar>
|
||||
<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"
|
||||
/>
|
||||
<!-- 📱 MOBILE: File Cards -->
|
||||
<div v-if="props.platform === 'mobile'" class="space-y-3 mt-3">
|
||||
<div
|
||||
v-for="file in availableFiles"
|
||||
:key="file.id"
|
||||
class="p-4 border rounded-xl bg-gray-50 dark:bg-gray-900 flex items-center justify-between active:scale-95 transition cursor-pointer"
|
||||
@click="openFile(file)"
|
||||
>
|
||||
<div>
|
||||
<p class="font-semibold truncate max-w-[200px]">{{ file?.path?.split("/").pop() }}</p>
|
||||
</div>
|
||||
|
||||
<UIcon
|
||||
name="i-heroicons-chevron-right-20-solid"
|
||||
class="w-5 h-5 text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<UAlert
|
||||
v-if="!availableFiles.length"
|
||||
icon="i-heroicons-x-mark"
|
||||
title="Keine Dateien verfügbar"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 🖥️ DESKTOP: Classic List -->
|
||||
<template v-else>
|
||||
<DocumentList
|
||||
: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>
|
||||
|
||||
<!-- 📱 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>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -40,7 +40,8 @@ const links = computed(() => {
|
||||
id: 'historyitems',
|
||||
label: "Logbuch",
|
||||
to: "/historyitems",
|
||||
icon: "i-heroicons-book-open"
|
||||
icon: "i-heroicons-book-open",
|
||||
disabled: true
|
||||
},
|
||||
{
|
||||
label: "Organisation",
|
||||
@@ -52,7 +53,7 @@ const links = computed(() => {
|
||||
to: "/standardEntity/tasks",
|
||||
icon: "i-heroicons-rectangle-stack"
|
||||
}] : [],
|
||||
... true ? [{
|
||||
/*... true ? [{
|
||||
label: "Plantafel",
|
||||
to: "/calendar/timeline",
|
||||
icon: "i-heroicons-calendar-days"
|
||||
@@ -66,7 +67,7 @@ const links = computed(() => {
|
||||
label: "Termine",
|
||||
to: "/standardEntity/events",
|
||||
icon: "i-heroicons-calendar-days"
|
||||
}] : [],
|
||||
}] : [],*/
|
||||
/*{
|
||||
label: "Dateien",
|
||||
to: "/files",
|
||||
@@ -83,10 +84,16 @@ const links = computed(() => {
|
||||
label: "Dateien",
|
||||
to: "/files",
|
||||
icon: "i-heroicons-document"
|
||||
},{
|
||||
label: "Anschreiben",
|
||||
to: "/createdletters",
|
||||
icon: "i-heroicons-document",
|
||||
disabled: true
|
||||
},{
|
||||
label: "Boxen",
|
||||
to: "/standardEntity/documentboxes",
|
||||
icon: "i-heroicons-archive-box"
|
||||
icon: "i-heroicons-archive-box",
|
||||
disabled: true
|
||||
},
|
||||
]
|
||||
},
|
||||
@@ -98,12 +105,14 @@ const links = computed(() => {
|
||||
{
|
||||
label: "Helpdesk",
|
||||
to: "/helpdesk",
|
||||
icon: "i-heroicons-chat-bubble-left-right"
|
||||
icon: "i-heroicons-chat-bubble-left-right",
|
||||
disabled: true
|
||||
},
|
||||
{
|
||||
label: "E-Mail",
|
||||
to: "/email/new",
|
||||
icon: "i-heroicons-envelope"
|
||||
icon: "i-heroicons-envelope",
|
||||
disabled: true
|
||||
}/*, {
|
||||
label: "Logbücher",
|
||||
to: "/communication/historyItems",
|
||||
@@ -145,7 +154,7 @@ const links = computed(() => {
|
||||
... true ? [{
|
||||
label: "Anwesenheiten",
|
||||
to: "/staff/time",
|
||||
icon: "i-heroicons-clock"
|
||||
icon: "i-heroicons-clock",
|
||||
}] : [],
|
||||
/*... has("absencerequests") ? [{
|
||||
label: "Abwesenheiten",
|
||||
@@ -175,7 +184,7 @@ const links = computed(() => {
|
||||
},{
|
||||
label: "Eingangsbelege",
|
||||
to: "/incomingInvoices",
|
||||
icon: "i-heroicons-document-text"
|
||||
icon: "i-heroicons-document-text",
|
||||
},{
|
||||
label: "Kostenstellen",
|
||||
to: "/standardEntity/costcentres",
|
||||
@@ -183,7 +192,7 @@ const links = computed(() => {
|
||||
},{
|
||||
label: "Buchungskonten",
|
||||
to: "/accounts",
|
||||
icon: "i-heroicons-document-text"
|
||||
icon: "i-heroicons-document-text",
|
||||
},{
|
||||
label: "zusätzliche Buchungskonten",
|
||||
to: "/standardEntity/ownaccounts",
|
||||
@@ -192,7 +201,7 @@ const links = computed(() => {
|
||||
{
|
||||
label: "Bank",
|
||||
to: "/banking",
|
||||
icon: "i-heroicons-document-text"
|
||||
icon: "i-heroicons-document-text",
|
||||
},
|
||||
]
|
||||
}],
|
||||
@@ -285,11 +294,11 @@ const links = computed(() => {
|
||||
to: "/standardEntity/plants",
|
||||
icon: "i-heroicons-clipboard-document"
|
||||
},] : [],
|
||||
... has("checks") ? [{
|
||||
/*... has("checks") ? [{
|
||||
label: "Überprüfungen",
|
||||
to: "/standardEntity/checks",
|
||||
icon: "i-heroicons-magnifying-glass"
|
||||
},] : [],
|
||||
},] : [],*/
|
||||
{
|
||||
label: "Einstellungen",
|
||||
defaultOpen: false,
|
||||
@@ -298,7 +307,7 @@ const links = computed(() => {
|
||||
{
|
||||
label: "Nummernkreise",
|
||||
to: "/settings/numberRanges",
|
||||
icon: "i-heroicons-clipboard-document-list"
|
||||
icon: "i-heroicons-clipboard-document-list",
|
||||
},/*{
|
||||
label: "Rollen",
|
||||
to: "/roles",
|
||||
@@ -306,15 +315,15 @@ const links = computed(() => {
|
||||
},*/{
|
||||
label: "E-Mail Konten",
|
||||
to: "/settings/emailaccounts",
|
||||
icon: "i-heroicons-envelope"
|
||||
icon: "i-heroicons-envelope",
|
||||
},{
|
||||
label: "Bankkonten",
|
||||
to: "/settings/banking",
|
||||
icon: "i-heroicons-currency-euro"
|
||||
icon: "i-heroicons-currency-euro",
|
||||
},{
|
||||
label: "Textvorlagen",
|
||||
to: "/settings/texttemplates",
|
||||
icon: "i-heroicons-clipboard-document-list"
|
||||
icon: "i-heroicons-clipboard-document-list",
|
||||
},/*{
|
||||
label: "Eigene Felder",
|
||||
to: "/settings/ownfields",
|
||||
@@ -322,15 +331,16 @@ const links = computed(() => {
|
||||
},*/{
|
||||
label: "Firmeneinstellungen",
|
||||
to: "/settings/tenant",
|
||||
icon: "i-heroicons-building-office"
|
||||
icon: "i-heroicons-building-office",
|
||||
},{
|
||||
label: "Projekttypen",
|
||||
to: "/projecttypes",
|
||||
icon: "i-heroicons-clipboard-document-list"
|
||||
icon: "i-heroicons-clipboard-document-list",
|
||||
},{
|
||||
label: "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: {
|
||||
type: String,
|
||||
|
||||
},
|
||||
noControls: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -123,7 +127,18 @@ const handleKeyPress = (event) => {
|
||||
const downloadControl = computed(() => vpvRef.value?.downloadControl)
|
||||
|
||||
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)
|
||||
if (!downloadCtrl) return
|
||||
@@ -145,7 +160,7 @@ watch(downloadControl, (downloadCtrl) => {
|
||||
</script>
|
||||
|
||||
<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">
|
||||
|
||||
<!-- Zoom out button -->
|
||||
@@ -168,6 +183,7 @@ watch(downloadControl, (downloadCtrl) => {
|
||||
variant="outline"
|
||||
></UButton>
|
||||
<UButton
|
||||
v-if="props.fileId || props.uri"
|
||||
@click="handleDownloadFile"
|
||||
variant="outline"
|
||||
icon="i-heroicons-arrow-down-on-square"
|
||||
|
||||
@@ -3,55 +3,75 @@ import dayjs from "dayjs";
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean;
|
||||
entry?: null;
|
||||
entry?: any | null;
|
||||
users: any[];
|
||||
canSelectUser: boolean;
|
||||
defaultUserId: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "saved"]);
|
||||
|
||||
const { create, update } = useStaffTime();
|
||||
|
||||
// v-model für das Modal
|
||||
const show = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (v: boolean) => emit("update:modelValue", v),
|
||||
});
|
||||
|
||||
// 🧱 Lokale reactive Kopie, die beim Öffnen aus props.entry befüllt wird
|
||||
const local = reactive<{
|
||||
id?: string;
|
||||
description: string;
|
||||
started_at: string;
|
||||
stopped_at: string | null;
|
||||
type: string;
|
||||
}>({
|
||||
// 🌈 Typen
|
||||
const typeOptions = [
|
||||
{ label: "Arbeitszeit", value: "work" },
|
||||
{ label: "Urlaub", value: "vacation" },
|
||||
{ label: "Krankheit", value: "sick" },
|
||||
{ label: "Feiertag", value: "holiday" },
|
||||
];
|
||||
|
||||
// Lokaler State
|
||||
const local = reactive({
|
||||
id: "",
|
||||
user_id: "", // 👈 Mitarbeiter
|
||||
description: "",
|
||||
started_at: dayjs().startOf("hour").format("YYYY-MM-DDTHH:mm"),
|
||||
stopped_at: dayjs().add(1, "hour").format("YYYY-MM-DDTHH:mm"),
|
||||
started_at: "",
|
||||
stopped_at: "",
|
||||
type: "work",
|
||||
vacation_reason: "",
|
||||
sick_reason: "",
|
||||
});
|
||||
|
||||
// 📡 Wenn das Modal geöffnet wird, Entry-Daten übernehmen
|
||||
// 📡 ENTRY —> LOCAL
|
||||
watch(
|
||||
() => props.entry,
|
||||
(val) => {
|
||||
if (val) {
|
||||
Object.assign(local, {
|
||||
id: val.id,
|
||||
user_id: val.user_id, // 👈 Mitarbeiter vorbelegen
|
||||
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",
|
||||
|
||||
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 {
|
||||
Object.assign(local, {
|
||||
id: "",
|
||||
user_id: props.defaultUserId, // 👈 Neuer Eintrag → aktueller Nutzer
|
||||
description: "",
|
||||
started_at: dayjs().startOf("hour").format("YYYY-MM-DDTHH:mm"),
|
||||
stopped_at: dayjs().add(1, "hour").format("YYYY-MM-DDTHH:mm"),
|
||||
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() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const payload = {
|
||||
description: local.description,
|
||||
started_at: dayjs(local.started_at).toISOString(),
|
||||
stopped_at: local.stopped_at ? dayjs(local.stopped_at).toISOString() : null,
|
||||
const payload: any = {
|
||||
user_id: local.user_id, // 👈 immer senden
|
||||
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) {
|
||||
await update(local.id, payload);
|
||||
} else {
|
||||
@@ -84,6 +118,7 @@ async function handleSubmit() {
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<UModal v-model="show" :ui="{ width: 'w-full sm:max-w-md' }" :key="local.id || 'new'">
|
||||
<UCard>
|
||||
@@ -94,18 +129,72 @@ async function handleSubmit() {
|
||||
</template>
|
||||
|
||||
<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 label="Startzeit" name="started_at">
|
||||
<UInput v-model="local.started_at" type="datetime-local" />
|
||||
<!-- TYPE -->
|
||||
<UFormGroup label="Typ">
|
||||
<USelect v-model="local.type" :options="typeOptions" />
|
||||
</UFormGroup>
|
||||
|
||||
<UFormGroup label="Endzeit" name="stopped_at">
|
||||
<UInput v-model="local.stopped_at" type="datetime-local" />
|
||||
</UFormGroup>
|
||||
<!-- VACATION -->
|
||||
<template v-if="local.type === 'vacation'">
|
||||
<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">
|
||||
<UButton color="gray" label="Abbrechen" @click="show = false" />
|
||||
<UButton color="primary" :loading="loading" type="submit" label="Speichern" />
|
||||
@@ -114,3 +203,4 @@ async function handleSubmit() {
|
||||
</UCard>
|
||||
</UModal>
|
||||
</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>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="auth.activeTenant">
|
||||
<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>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -2,46 +2,65 @@
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'solid'
|
||||
default: "solid"
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: 'primary'
|
||||
default: "primary"
|
||||
},
|
||||
pos: {
|
||||
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>
|
||||
|
||||
<template>
|
||||
<UButton
|
||||
id="fab"
|
||||
:icon="props.icon"
|
||||
:label="props.label"
|
||||
:variant="props.variant"
|
||||
:color="props.color"
|
||||
@click="emit('click')"
|
||||
:style="`bottom: ${15 + props.pos * 5}vh;`"
|
||||
class="bg-white dark:bg-gray-950"
|
||||
/>
|
||||
<!-- Wrapper für Position + Animation -->
|
||||
<div
|
||||
class="fixed right-5 z-40 transition-all"
|
||||
:style="{ bottom: `calc(${props.pos}rem + env(safe-area-inset-bottom))` }"
|
||||
>
|
||||
<UButton
|
||||
id="fab"
|
||||
:icon="props.icon"
|
||||
: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>
|
||||
|
||||
<style scoped>
|
||||
#fab {
|
||||
position: fixed;
|
||||
right: 15px;
|
||||
z-index: 5;
|
||||
/* FAB Basis */
|
||||
.fab-base {
|
||||
@apply rounded-full px-5 py-4 text-lg font-semibold;
|
||||
|
||||
/* 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
|
||||
|
||||
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",
|
||||
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)
|
||||
|
||||
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() {
|
||||
const { $api } = useNuxtApp()
|
||||
const auth = useAuthStore()
|
||||
|
||||
|
||||
|
||||
@@ -46,9 +47,17 @@ export function useStaffTime() {
|
||||
}
|
||||
|
||||
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',
|
||||
body: { state: 'approved' },
|
||||
body: {
|
||||
state: 'approved',
|
||||
//@ts-ignore
|
||||
approved_by: auth.user.id,
|
||||
approved_at: now,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -60,5 +60,10 @@
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -11,6 +11,7 @@ install! 'cocoapods', :disable_input_output_paths => true
|
||||
def capacitor_pods
|
||||
pod 'Capacitor', :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 'CapacitorNetwork', :path => '../../node_modules/@capacitor/network'
|
||||
pod 'CapacitorPreferences', :path => '../../node_modules/@capacitor/preferences'
|
||||
@@ -24,7 +25,15 @@ target 'App' do
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
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>
|
||||
import { ref, onMounted, onBeforeUnmount } from "vue"
|
||||
|
||||
|
||||
import MainNav from "~/components/MainNav.vue";
|
||||
import dayjs from "dayjs";
|
||||
import {useProfileStore} from "~/stores/profile.js";
|
||||
import {useCapacitor} from "../composables/useCapacitor.js";
|
||||
import {useAuthStore} from "~/stores/auth.js";
|
||||
|
||||
const dataStore = useDataStore()
|
||||
const profileStore = useProfileStore()
|
||||
const colorMode = useColorMode()
|
||||
const { isHelpSlideoverOpen } = useDashboard()
|
||||
const supabase = useSupabaseClient()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const auth = useAuthStore()
|
||||
|
||||
//profileStore.initializeData((await supabase.auth.getUser()).data.user.id)
|
||||
|
||||
const month = dayjs().format("MM")
|
||||
|
||||
const actions = [
|
||||
{
|
||||
id: 'new-customer',
|
||||
label: 'Kunde hinzufügen',
|
||||
icon: 'i-heroicons-user-group',
|
||||
to: "/customers/create" ,
|
||||
},
|
||||
{
|
||||
id: 'new-vendor',
|
||||
label: 'Lieferant hinzufügen',
|
||||
icon: 'i-heroicons-truck',
|
||||
to: "/vendors/create" ,
|
||||
},
|
||||
{
|
||||
id: 'new-contact',
|
||||
label: 'Ansprechpartner hinzufügen',
|
||||
icon: 'i-heroicons-user-group',
|
||||
to: "/contacts/create" ,
|
||||
},
|
||||
{
|
||||
id: 'new-task',
|
||||
label: 'Aufgabe hinzufügen',
|
||||
icon: 'i-heroicons-rectangle-stack',
|
||||
to: "/tasks/create" ,
|
||||
},
|
||||
{
|
||||
id: 'new-plant',
|
||||
label: 'Objekt hinzufügen',
|
||||
icon: 'i-heroicons-clipboard-document',
|
||||
to: "/plants/create" ,
|
||||
},
|
||||
{
|
||||
id: 'new-product',
|
||||
label: 'Artikel hinzufügen',
|
||||
icon: 'i-heroicons-puzzle-piece',
|
||||
to: "/products/create" ,
|
||||
},
|
||||
{
|
||||
id: 'new-project',
|
||||
label: 'Projekt hinzufügen',
|
||||
icon: 'i-heroicons-clipboard-document-check',
|
||||
to: "/projects/create" ,
|
||||
|
||||
const hideNav = ref(false)
|
||||
let lastScrollY = 0
|
||||
let scrollElement = null
|
||||
let returnTimer = null
|
||||
|
||||
const SHOW_DELAY = 1000 // 1 Sekunden
|
||||
|
||||
function showNavAfterDelay() {
|
||||
clearTimeout(returnTimer)
|
||||
returnTimer = setTimeout(() => {
|
||||
hideNav.value = false
|
||||
}, SHOW_DELAY)
|
||||
}
|
||||
|
||||
const handleScroll = () => {
|
||||
const current = scrollElement.scrollTop
|
||||
|
||||
// Runter scrollen -> verstecken
|
||||
if (current > lastScrollY + 10) {
|
||||
hideNav.value = true
|
||||
showNavAfterDelay()
|
||||
}
|
||||
]
|
||||
|
||||
// Hoch scrollen -> sofort zeigen
|
||||
if (current < lastScrollY - 10) {
|
||||
hideNav.value = false
|
||||
clearTimeout(returnTimer)
|
||||
}
|
||||
|
||||
lastScrollY = current
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
scrollElement = document.querySelector('.mobile-scroll-area')
|
||||
if (scrollElement) {
|
||||
scrollElement.addEventListener('scroll', handleScroll)
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (scrollElement) scrollElement.removeEventListener('scroll', handleScroll)
|
||||
clearTimeout(returnTimer)
|
||||
})
|
||||
|
||||
const footerLinks = [/*{
|
||||
label: 'Invite people',
|
||||
icon: 'i-heroicons-plus',
|
||||
to: '/settings/members'
|
||||
}, */{
|
||||
label: 'Hilfe & Info',
|
||||
icon: 'i-heroicons-question-mark-circle',
|
||||
click: () => isHelpSlideoverOpen.value = true
|
||||
}]
|
||||
|
||||
</script>
|
||||
|
||||
@@ -191,45 +171,49 @@ const footerLinks = [/*{
|
||||
</UDashboardPanel>
|
||||
</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
|
||||
icon="i-heroicons-home"
|
||||
to="/mobile/"
|
||||
variant="ghost"
|
||||
:color="route.fullPath === '/mobile' ? 'primary' : 'gray'"
|
||||
class="nav-btn"
|
||||
/>
|
||||
|
||||
<UButton
|
||||
icon="i-heroicons-clipboard-document-check"
|
||||
to="/standardEntity/tasks"
|
||||
variant="ghost"
|
||||
:color="route.fullPath === '/standardEntity/tasks' ? 'primary' : 'gray'"
|
||||
class="nav-btn"
|
||||
/>
|
||||
|
||||
<UButton
|
||||
icon="i-heroicons-rectangle-stack"
|
||||
to="/standardEntity/projects"
|
||||
variant="ghost"
|
||||
: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
|
||||
icon="i-heroicons-bars-4"
|
||||
to="/mobile/menu"
|
||||
variant="ghost"
|
||||
:color="route.fullPath === '/mobile/menu' ? 'primary' : 'gray'"
|
||||
class="nav-btn"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- ~/components/HelpSlideover.vue -->
|
||||
<HelpSlideover/>
|
||||
<!-- ~/components/NotificationsSlideover.vue -->
|
||||
<NotificationsSlideover />
|
||||
</nav>
|
||||
|
||||
|
||||
</UDashboardLayout>
|
||||
@@ -251,9 +235,9 @@ const footerLinks = [/*{
|
||||
class="w-1/3 mx-auto my-10"
|
||||
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 -->
|
||||
<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">
|
||||
<span class="text-left">{{tenant.name}}</span>
|
||||
<UButton
|
||||
@@ -269,27 +253,23 @@ const footerLinks = [/*{
|
||||
|
||||
</div>
|
||||
<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>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.mobileFooter {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 8vh;
|
||||
width: 100%;
|
||||
border-top: 1px solid grey;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: 1em;
|
||||
.nav-btn {
|
||||
@apply w-12 h-12 flex justify-center items-center rounded-xl active:scale-95 transition;
|
||||
}
|
||||
|
||||
.mobileFooter > a {
|
||||
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,9 @@
|
||||
export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||
const auth = useAuthStore()
|
||||
|
||||
console.log(auth)
|
||||
|
||||
if (auth.loading) return
|
||||
|
||||
|
||||
// Wenn nicht eingeloggt → auf /login (außer er will schon dahin)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
export default defineNuxtRouteMiddleware(async (to, _from) => {
|
||||
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')
|
||||
}
|
||||
})
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@capacitor/android": "^7.0.0",
|
||||
"@capacitor/browser": "^7.0.2",
|
||||
"@capacitor/core": "^7.0.0",
|
||||
"@capacitor/device": "^7.0.0",
|
||||
"@capacitor/ios": "^7.0.0",
|
||||
@@ -66,7 +67,7 @@
|
||||
"maplibre-gl": "^4.7.0",
|
||||
"nuxt-editorjs": "^1.0.4",
|
||||
"nuxt-viewport": "^2.0.6",
|
||||
"onesignal-cordova-plugin": "^5.2.11",
|
||||
"onesignal-cordova-plugin": "^5.2.14",
|
||||
"papaparse": "^5.4.1",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"pinia": "^2.1.7",
|
||||
|
||||
@@ -16,9 +16,15 @@ const router = useRouter()
|
||||
const items = ref([])
|
||||
const dataLoaded = ref(false)
|
||||
|
||||
const statementallocations = ref([])
|
||||
const incominginvoices = ref([])
|
||||
|
||||
const setupPage = async () => {
|
||||
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) => {
|
||||
let renderedAllocationsTemp = await renderedAllocations(i.id)
|
||||
let saldo = getSaldo(renderedAllocationsTemp)
|
||||
@@ -37,22 +43,22 @@ const setupPage = async () => {
|
||||
|
||||
const renderedAllocations = async (account) => {
|
||||
|
||||
let statementallocations = (await useEntities("statementallocations").select("*, bs_id(*)")).filter(i => i.account === account)
|
||||
let incominginvoices = (await useEntities("incominginvoices").select("*, vendor(*)")).filter(i => i.accounts.find(x => x.account === account))
|
||||
let statementallocationslocal = statementallocations.value.filter(i => i.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 {
|
||||
...i,
|
||||
type: "statementallocation",
|
||||
date: i.bs_id.date,
|
||||
partner: i.bs_id ? (i.bs_id.debName ? i.bs_id.debName : (i.bs_id.credName ? i.bs_id.credName : '')) : ''
|
||||
date: i.bankstatement.date,
|
||||
partner: i.bankstatement ? (i.bankstatement.debName ? i.bankstatement.debName : (i.bankstatement.credName ? i.bankstatement.credName : '')) : ''
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
let incominginvoicesallocations = []
|
||||
|
||||
incominginvoices.forEach(i => {
|
||||
incominginvoiceslocal.forEach(i => {
|
||||
|
||||
incominginvoicesallocations.push(...i.accounts.filter(x => x.account === account).map(x => {
|
||||
return {
|
||||
|
||||
@@ -65,8 +65,8 @@ const setup = async () => {
|
||||
|
||||
console.log(openDocuments.value)
|
||||
|
||||
allocatedDocuments.value = documents.filter(i => i.statementallocations.find(x => x.bs_id === itemInfo.value.id))
|
||||
allocatedIncomingInvoices.value = incominginvoices.filter(i => i.statementallocations.find(x => x.bs_id === itemInfo.value.id))
|
||||
allocatedDocuments.value = documents.filter(i => i.statementallocations.find(x => x.bankstatement === itemInfo.value.id))
|
||||
allocatedIncomingInvoices.value = incominginvoices.filter(i => i.statementallocations.find(x => x.bankstatement === itemInfo.value.id))
|
||||
console.log(allocatedDocuments.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))
|
||||
@@ -611,7 +611,7 @@ const archiveStatement = async () => {
|
||||
variant="outline"
|
||||
icon="i-heroicons-check"
|
||||
: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
|
||||
@click="accountToSave = ''"
|
||||
@@ -677,7 +677,7 @@ const archiveStatement = async () => {
|
||||
variant="outline"
|
||||
icon="i-heroicons-check"
|
||||
: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
|
||||
@click="accountToSave = ''"
|
||||
@@ -715,7 +715,7 @@ const archiveStatement = async () => {
|
||||
variant="outline"
|
||||
icon="i-heroicons-check"
|
||||
: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
|
||||
@click="customerAccountToSave = ''"
|
||||
@@ -753,7 +753,7 @@ const archiveStatement = async () => {
|
||||
variant="outline"
|
||||
icon="i-heroicons-check"
|
||||
: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
|
||||
@click="vendorAccountToSave = ''"
|
||||
@@ -811,7 +811,7 @@ const archiveStatement = async () => {
|
||||
variant="outline"
|
||||
class="mr-3"
|
||||
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
|
||||
@@ -836,7 +836,7 @@ const archiveStatement = async () => {
|
||||
variant="outline"
|
||||
class="mr-3"
|
||||
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
|
||||
variant="outline"
|
||||
|
||||
@@ -661,7 +661,7 @@ const findDocumentErrors = computed(() => {
|
||||
if (itemInfo.value.rows.length === 0) {
|
||||
errors.push({message: "Es sind keine Positionen angegeben", type: "breaking"})
|
||||
} else {
|
||||
itemInfo.value.rows.forEach(row => {
|
||||
itemInfo.value.rows.forEach((row,index) => {
|
||||
|
||||
if (itemInfo.value.type !== "quotes" && row.optional) {
|
||||
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}">
|
||||
{{dataStore.documentTypesForCreation[row.type].labelSingle}}
|
||||
<!--
|
||||
<span v-if="row.type === 'cancellationInvoices'"> zu {{row.linkedDocument.documentNumber}}</span>
|
||||
-->
|
||||
</template>
|
||||
<template #state-data="{row}">
|
||||
<span
|
||||
@@ -132,19 +134,25 @@
|
||||
<span v-if="row.documentDate">{{row.documentDate ? dayjs(row.documentDate).format("DD.MM.YY") : ''}}</span>
|
||||
</template>
|
||||
<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>
|
||||
-->
|
||||
</template>
|
||||
<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-else class="text-rose-600">Offen</span>
|
||||
</div>
|
||||
</div>-->
|
||||
</template>
|
||||
<template #amount-data="{row}">
|
||||
<!--
|
||||
<span v-if="row.type !== 'deliveryNotes'">{{displayCurrency(useSum().getCreatedDocumentSum(row,items))}}</span>
|
||||
-->
|
||||
</template>
|
||||
<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>
|
||||
-->
|
||||
</template>
|
||||
</UTable>
|
||||
</div>
|
||||
|
||||
@@ -32,6 +32,8 @@ const costcentres = ref([])
|
||||
const vendors = ref([])
|
||||
const accounts = ref([])
|
||||
|
||||
const mode = ref(route.params.mode)
|
||||
|
||||
const setup = async () => {
|
||||
let filetype = (await useEntities("filetags").select()).find(i=> i.incomingDocumentType === "invoices").id
|
||||
console.log(filetype)
|
||||
@@ -43,6 +45,8 @@ const setup = async () => {
|
||||
itemInfo.value = await useEntities("incominginvoices").selectSingle(route.params.id, "*, files(*)")
|
||||
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()
|
||||
@@ -143,7 +147,7 @@ const findIncomingInvoiceErrors = computed(() => {
|
||||
let errors = []
|
||||
|
||||
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.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"})
|
||||
@@ -168,21 +172,38 @@ const findIncomingInvoiceErrors = computed(() => {
|
||||
</script>
|
||||
|
||||
<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>
|
||||
<ArchiveButton
|
||||
color="rose"
|
||||
variant="outline"
|
||||
type="incominginvoices"
|
||||
@confirmed="useEntities('incominginvoices').archive(route.params.id)"
|
||||
v-if="mode !== 'show'"
|
||||
/>
|
||||
<UButton
|
||||
@click="updateIncomingInvoice(false)"
|
||||
v-if="mode !== 'show'"
|
||||
>
|
||||
Speichern
|
||||
</UButton>
|
||||
<UButton
|
||||
@click="updateIncomingInvoice(true)"
|
||||
v-if="mode !== 'show'"
|
||||
:disabled="findIncomingInvoiceErrors.filter(i => i.type === 'breaking').length > 0"
|
||||
>
|
||||
Speichern & Buchen
|
||||
@@ -192,6 +213,7 @@ const findIncomingInvoiceErrors = computed(() => {
|
||||
<UDashboardPanelContent>
|
||||
<div
|
||||
class="flex justify-between mt-5 workingContainer"
|
||||
v-if="loadedFile"
|
||||
>
|
||||
<object
|
||||
v-if="loadedFile"
|
||||
@@ -200,7 +222,6 @@ const findIncomingInvoiceErrors = computed(() => {
|
||||
class="mx-5 documentPreview"
|
||||
/>
|
||||
<div class="w-3/5 mx-5">
|
||||
|
||||
<UAlert
|
||||
class="mb-5"
|
||||
title="Vorhandene Probleme und Informationen:"
|
||||
@@ -218,17 +239,19 @@ const findIncomingInvoiceErrors = computed(() => {
|
||||
|
||||
</UAlert>
|
||||
|
||||
<div class=" scrollContainer">
|
||||
<div class="scrollContainer">
|
||||
<InputGroup class="mb-3">
|
||||
<UButton
|
||||
:variant="itemInfo.expense ? 'solid' : 'outline'"
|
||||
@click="itemInfo.expense = true"
|
||||
:disabled="mode === 'show'"
|
||||
>
|
||||
Ausgabe
|
||||
</UButton>
|
||||
<UButton
|
||||
:variant="!itemInfo.expense ? 'solid' : 'outline'"
|
||||
@click="itemInfo.expense = false"
|
||||
:disabled="mode === 'show'"
|
||||
>
|
||||
Einnahme
|
||||
</UButton>
|
||||
@@ -237,6 +260,7 @@ const findIncomingInvoiceErrors = computed(() => {
|
||||
<UFormGroup label="Lieferant:" >
|
||||
<InputGroup>
|
||||
<USelectMenu
|
||||
:disabled="mode === 'show'"
|
||||
v-model="itemInfo.vendor"
|
||||
:options="vendors"
|
||||
option-attribute="name"
|
||||
@@ -258,12 +282,15 @@ const findIncomingInvoiceErrors = computed(() => {
|
||||
type="vendors"
|
||||
:id="itemInfo.vendor"
|
||||
@return-data="(data) => itemInfo.vendor = data.id"
|
||||
:button-edit="mode !== 'show'"
|
||||
:button-create="mode !== 'show'"
|
||||
/>
|
||||
<UButton
|
||||
icon="i-heroicons-x-mark"
|
||||
variant="outline"
|
||||
color="rose"
|
||||
@click="itemInfo.vendor = null"
|
||||
v-if="itemInfo.vendor && mode !== 'show'"
|
||||
/>
|
||||
</InputGroup>
|
||||
|
||||
@@ -276,6 +303,7 @@ const findIncomingInvoiceErrors = computed(() => {
|
||||
>
|
||||
<UInput
|
||||
v-model="itemInfo.reference"
|
||||
:disabled="mode === 'show'"
|
||||
/>
|
||||
</UFormGroup>
|
||||
|
||||
@@ -287,6 +315,7 @@ const findIncomingInvoiceErrors = computed(() => {
|
||||
:label="itemInfo.date ? dayjs(itemInfo.date).format('DD.MM.YYYY') : 'Datum auswählen'"
|
||||
variant="outline"
|
||||
:color="!itemInfo.date ? 'rose' : 'primary'"
|
||||
:disabled="mode === 'show'"
|
||||
/>
|
||||
|
||||
<template #panel="{ close }">
|
||||
@@ -301,10 +330,11 @@ const findIncomingInvoiceErrors = computed(() => {
|
||||
icon="i-heroicons-calendar-days-20-solid"
|
||||
:label="itemInfo.dueDate ? dayjs(itemInfo.dueDate).format('DD.MM.YYYY') : 'Datum auswählen'"
|
||||
variant="outline"
|
||||
:disabled="mode === 'show'"
|
||||
/>
|
||||
|
||||
<template #panel="{ close }">
|
||||
<LazyDatePicker v-model="itemInfo.dueDate" @close="close" />
|
||||
<LazyDatePicker v-model="itemInfo.dueDate" @close="close"/>
|
||||
</template>
|
||||
</UPopover>
|
||||
</UFormGroup>
|
||||
@@ -316,12 +346,14 @@ const findIncomingInvoiceErrors = computed(() => {
|
||||
<USelectMenu
|
||||
:options="['Einzug','Kreditkarte','Überweisung','Sonstiges']"
|
||||
v-model="itemInfo.paymentType"
|
||||
:disabled="mode === 'show'"
|
||||
/>
|
||||
</UFormGroup>
|
||||
|
||||
<UFormGroup label="Beschreibung:" >
|
||||
<UTextarea
|
||||
v-model="itemInfo.description"
|
||||
:disabled="mode === 'show'"
|
||||
/>
|
||||
</UFormGroup>
|
||||
|
||||
@@ -329,12 +361,14 @@ const findIncomingInvoiceErrors = computed(() => {
|
||||
<UButton
|
||||
:variant="!useNetMode ? 'solid' : 'outline'"
|
||||
@click="changeNetMode(false)"
|
||||
:disabled="mode === 'show'"
|
||||
>
|
||||
Brutto
|
||||
</UButton>
|
||||
<UButton
|
||||
:variant="useNetMode ? 'solid' : 'outline'"
|
||||
@click="changeNetMode(true)"
|
||||
:disabled="mode === 'show'"
|
||||
>
|
||||
Netto
|
||||
</UButton>
|
||||
@@ -377,6 +411,7 @@ const findIncomingInvoiceErrors = computed(() => {
|
||||
option-attribute="label"
|
||||
value-attribute="id"
|
||||
searchable
|
||||
:disabled="mode === 'show'"
|
||||
:search-attributes="['label']"
|
||||
searchable-placeholder="Suche..."
|
||||
v-model="item.account"
|
||||
@@ -398,6 +433,7 @@ const findIncomingInvoiceErrors = computed(() => {
|
||||
option-attribute="name"
|
||||
value-attribute="id"
|
||||
searchable
|
||||
:disabled="mode === 'show'"
|
||||
:search-attributes="['label']"
|
||||
searchable-placeholder="Suche..."
|
||||
v-model="item.costCentre"
|
||||
@@ -417,7 +453,7 @@ const findIncomingInvoiceErrors = computed(() => {
|
||||
<UButton
|
||||
variant="outline"
|
||||
color="rose"
|
||||
v-if="item.costCentre"
|
||||
v-if="item.costCentre && mode !== 'show'"
|
||||
icon="i-heroicons-x-mark"
|
||||
@click="item.costCentre = null"
|
||||
/>
|
||||
@@ -431,11 +467,12 @@ const findIncomingInvoiceErrors = computed(() => {
|
||||
<UInput
|
||||
v-model="item.description"
|
||||
class="flex-auto"
|
||||
:disabled="mode === 'show'"
|
||||
></UInput>
|
||||
<UButton
|
||||
variant="outline"
|
||||
color="rose"
|
||||
v-if="item.description"
|
||||
v-if="item.description && mode !== 'show'"
|
||||
icon="i-heroicons-x-mark"
|
||||
@click="item.description = null"
|
||||
/>
|
||||
@@ -457,7 +494,7 @@ const findIncomingInvoiceErrors = computed(() => {
|
||||
step="0.01"
|
||||
v-model="item.amountNet"
|
||||
: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)),
|
||||
item.amountGross = Number(item.amountNet) + NUmber(item.amountTax)"
|
||||
>
|
||||
@@ -476,7 +513,7 @@ const findIncomingInvoiceErrors = computed(() => {
|
||||
<UInput
|
||||
type="number"
|
||||
step="0.01"
|
||||
:disabled="item.taxType === null"
|
||||
:disabled="item.taxType === null || mode === 'show'"
|
||||
v-model="item.amountGross"
|
||||
:color="!item.amountGross ? 'rose' : 'primary'"
|
||||
:ui-menu="{ width: 'min-w-max' }"
|
||||
@@ -496,6 +533,8 @@ const findIncomingInvoiceErrors = computed(() => {
|
||||
>
|
||||
<USelectMenu
|
||||
:options="taxOptions"
|
||||
:disabled="mode === 'show'"
|
||||
:color="item.taxType === null || item.taxType === '0' ? 'rose' : 'primary'"
|
||||
v-model="item.taxType"
|
||||
value-attribute="key"
|
||||
:ui-menu="{ width: 'min-w-max' }"
|
||||
@@ -513,12 +552,13 @@ const findIncomingInvoiceErrors = computed(() => {
|
||||
|
||||
<UButton
|
||||
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)]"
|
||||
>
|
||||
Betrag aufteilen
|
||||
</UButton>
|
||||
<UButton
|
||||
v-if="index !== 0"
|
||||
v-if="index !== 0 && mode !== 'show'"
|
||||
class="mt-3"
|
||||
variant="ghost"
|
||||
color="rose"
|
||||
@@ -532,6 +572,7 @@ const findIncomingInvoiceErrors = computed(() => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<UProgress v-else animation="carousel"/>
|
||||
</UDashboardPanelContent>
|
||||
|
||||
|
||||
@@ -549,8 +590,6 @@ const findIncomingInvoiceErrors = computed(() => {
|
||||
|
||||
.scrollContainer {
|
||||
overflow-y: scroll;
|
||||
padding-left: 1em;
|
||||
padding-right: 1em;
|
||||
height: 70vh;
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
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 toast = useToast()
|
||||
const router = useRouter()
|
||||
|
||||
|
||||
const platformIsNative = useCapacitor().getIsNative()
|
||||
|
||||
|
||||
const doLogin = async (data:any) => {
|
||||
@@ -14,10 +15,10 @@ const doLogin = async (data:any) => {
|
||||
await auth.login(data.email, data.password)
|
||||
// Weiterleiten nach erfolgreichem Login
|
||||
toast.add({title:"Einloggen erfolgreich"})
|
||||
if(useCapacitor().getIsNative()) {
|
||||
return navigateTo("/mobile")
|
||||
if(platformIsNative) {
|
||||
await router.push("/mobile")
|
||||
} else {
|
||||
return navigateTo("/")
|
||||
await router.push("/")
|
||||
}
|
||||
} catch (err: any) {
|
||||
toast.add({title:"Zugangsdaten falsch. Bitte überprüfen Sie Ihre Eingaben",color:"rose"})
|
||||
@@ -26,7 +27,7 @@ const doLogin = async (data:any) => {
|
||||
</script>
|
||||
|
||||
<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
|
||||
light="/Logo.png"
|
||||
@@ -67,4 +68,35 @@ const doLogin = async (data:any) => {
|
||||
</template>
|
||||
</UAuthForm>
|
||||
</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>
|
||||
@@ -1,13 +1,33 @@
|
||||
<script setup>
|
||||
|
||||
import DisplayPresentProfiles from "~/components/noAutoLoad/displayPresentProfiles.vue";
|
||||
|
||||
definePageMeta({
|
||||
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>
|
||||
|
||||
<template>
|
||||
@@ -42,6 +62,7 @@ definePageMeta({
|
||||
>
|
||||
<display-projects-in-phases/>
|
||||
</UDashboardCard>
|
||||
<display-pinnend-links :links="pinnedLinks"/>
|
||||
|
||||
</UPageGrid>
|
||||
</UDashboardPanelContent>
|
||||
|
||||
@@ -13,30 +13,19 @@ const auth = useAuthStore()
|
||||
<UDivider class="mb-3">Weiteres</UDivider>
|
||||
<UButton
|
||||
class="w-full my-1"
|
||||
to="/times"
|
||||
to="/staff/time"
|
||||
icon="i-heroicons-clock"
|
||||
>
|
||||
Zeiten
|
||||
</UButton>
|
||||
<UButton
|
||||
<!-- <UButton
|
||||
class="w-full my-1"
|
||||
to="/standardEntity/absencerequests"
|
||||
icon="i-heroicons-document-text"
|
||||
>
|
||||
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
|
||||
class="w-full my-1"
|
||||
to="/standardEntity/customers"
|
||||
|
||||
@@ -3,12 +3,14 @@ const { $dayjs } = useNuxtApp()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const auth = useAuthStore()
|
||||
|
||||
const toast = useToast()
|
||||
// 🔹 State
|
||||
const workingtimes = ref([])
|
||||
const absencerequests = ref([])
|
||||
const workingTimeInfo = ref(null)
|
||||
|
||||
const platformIsNative = ref(useCapacitor().getIsNative())
|
||||
|
||||
const selectedPresetRange = ref("Dieser Monat bis heute")
|
||||
const selectedStartDay = 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)
|
||||
|
||||
console.log(profile.value)
|
||||
setPageLayout(platformIsNative.value ? 'mobile' : 'default')
|
||||
|
||||
}
|
||||
|
||||
@@ -81,10 +84,32 @@ async function generateDocument() {
|
||||
|
||||
uri.value = await useFunctions().useCreatePDF({
|
||||
full_name: profile.value.full_name,
|
||||
employee_number: profile.value.employee_number ? profile.value.employee_number : "-",
|
||||
...workingTimeInfo.value}, path, "timesheet")
|
||||
|
||||
|
||||
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) {
|
||||
@@ -97,29 +122,30 @@ changeRange()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UDashboardNavbar :ui="{ center: 'flex items-stretch gap-1.5 min-w-0' }">
|
||||
<template #left>
|
||||
<UButton
|
||||
icon="i-heroicons-chevron-left"
|
||||
variant="outline"
|
||||
@click="router.push('/staff/time')"
|
||||
>
|
||||
Anwesenheiten
|
||||
</UButton>
|
||||
</template>
|
||||
<template v-if="!platformIsNative">
|
||||
<UDashboardNavbar :ui="{ center: 'flex items-stretch gap-1.5 min-w-0' }">
|
||||
<template #left>
|
||||
<UButton
|
||||
icon="i-heroicons-chevron-left"
|
||||
variant="outline"
|
||||
@click="router.push('/staff/time')"
|
||||
>
|
||||
Anwesenheiten
|
||||
</UButton>
|
||||
</template>
|
||||
|
||||
<template #center>
|
||||
<h1 class="text-xl font-medium truncate">
|
||||
Auswertung Anwesenheiten: {{ profile?.full_name || '' }}
|
||||
</h1>
|
||||
</template>
|
||||
</UDashboardNavbar>
|
||||
<template #center>
|
||||
<h1 class="text-xl font-medium truncate">
|
||||
Auswertung Anwesenheiten: {{ profile?.full_name || '' }}
|
||||
</h1>
|
||||
</template>
|
||||
</UDashboardNavbar>
|
||||
|
||||
<UDashboardToolbar>
|
||||
<template #left>
|
||||
<UFormGroup label="Zeitraum:">
|
||||
<USelectMenu
|
||||
:options="[
|
||||
<UDashboardToolbar>
|
||||
<template #left>
|
||||
<UFormGroup label="Zeitraum:">
|
||||
<USelectMenu
|
||||
:options="[
|
||||
'Dieser Monat bis heute',
|
||||
'Diese Woche',
|
||||
'Dieser Monat',
|
||||
@@ -128,101 +154,265 @@ changeRange()
|
||||
'Letzter Monat',
|
||||
'Letztes Jahr'
|
||||
]"
|
||||
v-model="selectedPresetRange"
|
||||
@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'"
|
||||
v-model="selectedPresetRange"
|
||||
@change="changeRange"
|
||||
/>
|
||||
<template #panel="{ close }">
|
||||
<LazyDatePicker v-model="selectedStartDay" @close="loadWorkingTimeInfo" />
|
||||
</template>
|
||||
</UPopover>
|
||||
</UFormGroup>
|
||||
</UFormGroup>
|
||||
|
||||
<UFormGroup label="Ende:">
|
||||
<UPopover :popper="{ placement: 'bottom-start' }">
|
||||
<UButton
|
||||
icon="i-heroicons-calendar-days-20-solid"
|
||||
:label="selectedEndDay ? $dayjs(selectedEndDay).format('DD.MM.YYYY') : 'Datum wählen'"
|
||||
/>
|
||||
<template #panel="{ close }">
|
||||
<LazyDatePicker v-model="selectedEndDay" @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>
|
||||
<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 }">
|
||||
<LazyDatePicker v-model="selectedStartDay" @close="loadWorkingTimeInfo" />
|
||||
</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>
|
||||
</UPopover>
|
||||
</UFormGroup>
|
||||
|
||||
<UDashboardPanel>
|
||||
<UTable
|
||||
v-if="workingTimeInfo"
|
||||
:rows="workingTimeInfo.times"
|
||||
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Anwesenheiten' }"
|
||||
:columns="[
|
||||
<UFormGroup label="Ende:">
|
||||
<UPopover :popper="{ placement: 'bottom-start' }">
|
||||
<UButton
|
||||
icon="i-heroicons-calendar-days-20-solid"
|
||||
:label="selectedEndDay ? $dayjs(selectedEndDay).format('DD.MM.YYYY') : 'Datum wählen'"
|
||||
/>
|
||||
<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: 'start', label: 'Start' },
|
||||
{ key: 'end', label: 'Ende' },
|
||||
{ key: 'duration', label: 'Dauer' },
|
||||
{ key: 'description', label: 'Beschreibung' }
|
||||
]"
|
||||
@select="(row) => router.push(`/workingtimes/edit/${row.id}`)"
|
||||
>
|
||||
<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>
|
||||
@select="(row) => router.push(`/workingtimes/edit/${row.id}`)"
|
||||
>
|
||||
<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 #start-data="{ row }">
|
||||
{{ $dayjs(row.started_at).format('HH:mm DD.MM.YY') }} Uhr
|
||||
</template>
|
||||
<template #start-data="{ row }">
|
||||
{{ $dayjs(row.started_at).format('HH:mm DD.MM.YY') }} Uhr
|
||||
</template>
|
||||
|
||||
<template #end-data="{ row }">
|
||||
{{ $dayjs(row.stopped_at).format('HH:mm DD.MM.YY') }} Uhr
|
||||
</template>
|
||||
<template #end-data="{ row }">
|
||||
{{ $dayjs(row.stopped_at).format('HH:mm DD.MM.YY') }} Uhr
|
||||
</template>
|
||||
|
||||
<template #duration-data="{ row }">
|
||||
{{ useFormatDuration(row.duration_minutes) }}
|
||||
</template>
|
||||
</UTable>
|
||||
</UDashboardPanel>
|
||||
<template #duration-data="{ row }">
|
||||
{{ useFormatDuration(row.duration_minutes) }}
|
||||
</template>
|
||||
</UTable>
|
||||
</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>
|
||||
<p class="text-xs text-gray-500 mb-1">Ende</p>
|
||||
<UPopover :popper="{ placement: 'bottom-start' }">
|
||||
<UButton
|
||||
icon="i-heroicons-calendar"
|
||||
class="w-full"
|
||||
:label="$dayjs(selectedEndDay).format('DD.MM.YYYY')"
|
||||
/>
|
||||
<template #panel>
|
||||
<LazyDatePicker v-model="selectedEndDay" @close="loadWorkingTimeInfo" />
|
||||
</template>
|
||||
</UPopover>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 📑 Mobile Tabs -->
|
||||
<UTabs
|
||||
:items="[{ label: 'Information' }, { label: 'Bericht' }]"
|
||||
v-model="openTab"
|
||||
@change="onTabChange"
|
||||
class="mt-3 mx-3"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
|
||||
<!-- ====================== -->
|
||||
<!-- TAB 1 — INFORMATION -->
|
||||
<!-- ====================== -->
|
||||
<div v-if="item.label === 'Information'" class="space-y-4">
|
||||
|
||||
<!-- Summary Card -->
|
||||
<UCard v-if="workingTimeInfo" class="mt-3">
|
||||
<template #header>
|
||||
<h3 class="text-base font-semibold">Zusammenfassung</h3>
|
||||
</template>
|
||||
|
||||
<div class="space-y-2 text-sm">
|
||||
<p>Eingereicht: <b>{{ formatMinutesToHHMM(workingTimeInfo.sumWorkingMinutesEingereicht) }}</b></p>
|
||||
<p>Genehmigt: <b>{{ formatMinutesToHHMM(workingTimeInfo.sumWorkingMinutesApproved) }}</b></p>
|
||||
|
||||
<p>
|
||||
Feiertagsausgleich:
|
||||
<b>{{ formatMinutesToHHMM(workingTimeInfo.sumWorkingMinutesRecreationDays) }}</b>
|
||||
/ {{ workingTimeInfo.sumRecreationDays }} Tage
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Urlaubs-/Berufsschule:
|
||||
<b>{{ formatMinutesToHHMM(workingTimeInfo.sumWorkingMinutesVacationDays) }}</b>
|
||||
/ {{ workingTimeInfo.sumVacationDays }} Tage
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Krankheitsausgleich:
|
||||
<b>{{ formatMinutesToHHMM(workingTimeInfo.sumWorkingMinutesSickDays) }}</b>
|
||||
/ {{ workingTimeInfo.sumSickDays }} Tage
|
||||
</p>
|
||||
|
||||
<p>Soll: <b>{{ formatMinutesToHHMM(workingTimeInfo.timeSpanWorkingMinutes) }}</b></p>
|
||||
|
||||
<p>
|
||||
Inoffizielles Saldo:
|
||||
<b>{{ (workingTimeInfo.saldoInOfficial >= 0 ? '+' : '-') + formatMinutesToHHMM(Math.abs(workingTimeInfo.saldoInOfficial)) }}</b>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Saldo:
|
||||
<b>{{ (workingTimeInfo.saldo >= 0 ? '+' : '-') + formatMinutesToHHMM(Math.abs(workingTimeInfo.saldo)) }}</b>
|
||||
</p>
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ====================== -->
|
||||
<!-- TAB 2 — BERICHT -->
|
||||
<!-- ====================== -->
|
||||
<div v-else-if="item.label === 'Bericht'">
|
||||
<UButton
|
||||
v-if="uri && !fileSaved"
|
||||
icon="i-mdi-content-save"
|
||||
color="primary"
|
||||
class="w-full mb-3"
|
||||
@click="saveFile"
|
||||
>
|
||||
Bericht speichern
|
||||
</UButton>
|
||||
|
||||
<PDFViewer
|
||||
v-if="showDocument"
|
||||
:uri="uri"
|
||||
@@ -231,5 +421,7 @@ changeRange()
|
||||
</div>
|
||||
</template>
|
||||
</UTabs>
|
||||
</UDashboardPanelContent>
|
||||
</template>
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
@@ -1,23 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
import { useStaffTime } from '~/composables/useStaffTime'
|
||||
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 router = useRouter()
|
||||
|
||||
// MOBILE DETECTION
|
||||
const platformIsNative = useCapacitor().getIsNative()
|
||||
// LIST + ACTIVE
|
||||
const entries = ref([])
|
||||
const active = computed(() => entries.value.find(e => !e.stopped_at && e.user_id === auth.user.id))
|
||||
|
||||
const loading = ref(false)
|
||||
const showModal = ref(false)
|
||||
const editEntry = ref(null)
|
||||
|
||||
// 👥 Nutzer-Filter (nur für Berechtigte)
|
||||
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 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() {
|
||||
if (!canViewAll.value) return
|
||||
// Beispiel: User aus Supabase holen
|
||||
@@ -25,15 +50,18 @@ async function loadUsers() {
|
||||
users.value = res
|
||||
}
|
||||
|
||||
|
||||
// LOAD ENTRIES (only own entries on mobile)
|
||||
async function load() {
|
||||
entries.value = await list(
|
||||
canViewAll.value && selectedUser.value ? { user_id: selectedUser.value } : undefined
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
async function handleStart() {
|
||||
loading.value = true
|
||||
await start('Arbeitszeit gestartet')
|
||||
await start("Arbeitszeit gestartet")
|
||||
await load()
|
||||
loading.value = false
|
||||
}
|
||||
@@ -61,161 +89,351 @@ async function handleApprove(entry: any) {
|
||||
await load()
|
||||
}
|
||||
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
await loadUsers()
|
||||
await load()
|
||||
await loadUsers()
|
||||
setPageLayout(platformIsNative ? 'mobile' : 'default')
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UDashboardNavbar title="Zeiterfassung" :badge="entries.length" />
|
||||
<!-- ============================= -->
|
||||
<!-- DESKTOP VERSION -->
|
||||
<!-- ============================= -->
|
||||
<template v-if="!platformIsNative">
|
||||
<UDashboardNavbar title="Zeiterfassung" :badge="entries.length" />
|
||||
|
||||
<!-- TOOLBAR -->
|
||||
<UDashboardToolbar>
|
||||
<template #left>
|
||||
<div class="flex items-center gap-2">
|
||||
<UIcon name="i-heroicons-clock" class="text-primary-500" />
|
||||
<span v-if="active" class="text-primary-600 font-medium">
|
||||
Läuft seit {{ useNuxtApp().$dayjs(active.started_at).format('HH:mm') }}
|
||||
</span>
|
||||
<span v-else class="text-gray-500">Keine aktive Zeit</span>
|
||||
</div>
|
||||
</template>
|
||||
<UDashboardToolbar>
|
||||
<template #left>
|
||||
<div class="flex items-center gap-2">
|
||||
<UIcon name="i-heroicons-clock" class="text-primary-500" />
|
||||
<span v-if="active" class="text-primary-600 font-medium">
|
||||
Läuft seit {{ useNuxtApp().$dayjs(active.started_at).format('HH:mm') }}
|
||||
</span>
|
||||
<span v-else class="text-gray-500">Keine aktive Zeit</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #right>
|
||||
<UButton
|
||||
v-if="active"
|
||||
color="red"
|
||||
icon="i-heroicons-stop"
|
||||
:loading="loading"
|
||||
label="Stoppen"
|
||||
@click="handleStop"
|
||||
/>
|
||||
<UButton
|
||||
v-else
|
||||
color="green"
|
||||
icon="i-heroicons-play"
|
||||
:loading="loading"
|
||||
label="Starten"
|
||||
@click="handleStart"
|
||||
/>
|
||||
<UButton
|
||||
color="primary"
|
||||
icon="i-heroicons-plus"
|
||||
label="Zeit"
|
||||
@click="() => { editEntry = null; showModal = true }"
|
||||
/>
|
||||
</template>
|
||||
</UDashboardToolbar>
|
||||
<UDashboardToolbar>
|
||||
<template #left>
|
||||
<!-- 👥 User-Filter (nur bei Berechtigung) -->
|
||||
<div v-if="canViewAll" class="flex items-center gap-2">
|
||||
<USelectMenu
|
||||
v-model="selectedUser"
|
||||
:options="[
|
||||
<template #right>
|
||||
<UButton
|
||||
v-if="active"
|
||||
color="red"
|
||||
icon="i-heroicons-stop"
|
||||
:loading="loading"
|
||||
label="Stoppen"
|
||||
@click="handleStop"
|
||||
/>
|
||||
<UButton
|
||||
v-else
|
||||
color="green"
|
||||
icon="i-heroicons-play"
|
||||
:loading="loading"
|
||||
label="Starten"
|
||||
@click="handleStart"
|
||||
/>
|
||||
<UButton
|
||||
color="primary"
|
||||
icon="i-heroicons-plus"
|
||||
label="Zeit"
|
||||
@click="() => { editEntry = null; showModal = true }"
|
||||
/>
|
||||
</template>
|
||||
</UDashboardToolbar>
|
||||
<UDashboardToolbar>
|
||||
<template #left>
|
||||
<!-- 👥 User-Filter (nur bei Berechtigung) -->
|
||||
<div v-if="canViewAll" class="flex items-center gap-2">
|
||||
<USelectMenu
|
||||
v-model="selectedUser"
|
||||
:options="[
|
||||
{ label: 'Alle Benutzer', value: null },
|
||||
...users.map(u => ({ label: u.full_name || u.email, value: u.user_id }))
|
||||
]"
|
||||
placeholder="Benutzer auswählen"
|
||||
value-attribute="value"
|
||||
option-attribute="label"
|
||||
class="min-w-[220px]"
|
||||
@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`)"
|
||||
placeholder="Benutzer auswählen"
|
||||
value-attribute="value"
|
||||
option-attribute="label"
|
||||
class="min-w-[220px]"
|
||||
@change="load"
|
||||
/>
|
||||
</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>
|
||||
</template>
|
||||
</UDashboardToolbar>
|
||||
|
||||
<!-- TABELLE -->
|
||||
<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: 'description', label: 'Beschreibung' },
|
||||
...(canViewAll ? [{ key: 'user_name', label: 'Benutzer' }] : []),
|
||||
<div class="px-3 mt-3">
|
||||
<UButton
|
||||
color="gray"
|
||||
icon="i-heroicons-chart-bar"
|
||||
label="Eigene Auswertung"
|
||||
class="w-full"
|
||||
variant="soft"
|
||||
@click="router.push(`/staff/time/${auth.user.id}/evaluate`)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
]"
|
||||
>
|
||||
<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 #started_at-data="{ row }">
|
||||
{{ useNuxtApp().$dayjs(row.started_at).format('DD.MM.YY HH:mm') }}
|
||||
</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'"
|
||||
<!-- 📜 SCROLLABLE CONTENT -->
|
||||
<UDashboardPanelContent class="flex-1 overflow-y-auto p-3 space-y-4 pb-24">
|
||||
|
||||
<!-- ZEIT-CARDS -->
|
||||
<UCard
|
||||
v-for="row in entries"
|
||||
:key="row.id"
|
||||
class="p-4 border rounded-xl active:scale-[0.98] transition cursor-pointer"
|
||||
@click="handleEdit(row)"
|
||||
>
|
||||
<UButton
|
||||
variant="ghost"
|
||||
icon="i-heroicons-check-circle"
|
||||
@click="handleApprove(row)"
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="font-semibold flex items-center gap-2">
|
||||
<span>{{ row.description || 'Keine Beschreibung' }}</span>
|
||||
|
||||
/>
|
||||
</UTooltip>
|
||||
<UTooltip
|
||||
text="Zeit einreichen"
|
||||
v-if="row.state === 'draft'"
|
||||
>
|
||||
<UButton
|
||||
variant="ghost"
|
||||
icon="i-heroicons-arrow-right-end-on-rectangle"
|
||||
@click="handleSubmit(row)"
|
||||
<UBadge
|
||||
:color="typeColor[row.type]"
|
||||
class="text-xs"
|
||||
>
|
||||
{{ typeLabel[row.type] }}
|
||||
</UBadge>
|
||||
</div>
|
||||
|
||||
/>
|
||||
</UTooltip>
|
||||
<UTooltip
|
||||
text="Zeit bearbeiten"
|
||||
v-if="row.state === 'draft'"
|
||||
>
|
||||
<UButton
|
||||
variant="ghost"
|
||||
icon="i-heroicons-pencil-square"
|
||||
@click="handleEdit(row)"
|
||||
/>
|
||||
</UTooltip>
|
||||
<UBadge
|
||||
:color="{
|
||||
approved: 'primary',
|
||||
submitted: 'cyan',
|
||||
draft: 'red'
|
||||
}[row.state]"
|
||||
>
|
||||
{{
|
||||
{
|
||||
approved: 'Genehmigt',
|
||||
submitted: 'Eingereicht',
|
||||
draft: 'Entwurf'
|
||||
}[row.state] || row.state
|
||||
}}
|
||||
</UBadge>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
</UTable>
|
||||
</UDashboardPanelContent>
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
Start: {{ useNuxtApp().$dayjs(row.started_at).format('DD.MM.YY HH:mm') }}
|
||||
</p>
|
||||
|
||||
<p class="text-sm text-gray-500">
|
||||
Ende:
|
||||
<span v-if="row.stopped_at">
|
||||
{{ useNuxtApp().$dayjs(row.stopped_at).format('DD.MM.YY HH:mm') }}
|
||||
</span>
|
||||
<span v-else class="text-primary-500 font-medium">läuft...</span>
|
||||
</p>
|
||||
|
||||
<p class="text-sm text-gray-500">
|
||||
Dauer:
|
||||
{{ row.duration_minutes ? useFormatDuration(row.duration_minutes) : '-' }}
|
||||
</p>
|
||||
|
||||
<!-- ACTION-BUTTONS -->
|
||||
<div class="flex gap-2 mt-3">
|
||||
<UButton
|
||||
v-if="row.state === 'draft'"
|
||||
color="gray"
|
||||
icon="i-heroicons-arrow-right-end-on-rectangle"
|
||||
label="Einreichen"
|
||||
variant="soft"
|
||||
@click.stop="handleSubmit(row)"
|
||||
/>
|
||||
|
||||
<!-- <UButton
|
||||
v-if="row.state === 'submitted'"
|
||||
color="primary"
|
||||
icon="i-heroicons-check"
|
||||
label="Genehmigen"
|
||||
variant="soft"
|
||||
@click.stop="handleApprove(row)"
|
||||
/>-->
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
</UDashboardPanelContent>
|
||||
|
||||
<!-- ➕ FLOATING ACTION BUTTON -->
|
||||
<FloatingActionButton
|
||||
icon="i-heroicons-plus"
|
||||
class="!fixed bottom-6 right-6 z-50"
|
||||
color="primary"
|
||||
@click="() => { editEntry = null; showModal = true }"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- MODAL -->
|
||||
<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>
|
||||
|
||||
@@ -9,7 +9,7 @@ const api = useNuxtApp().$api
|
||||
|
||||
|
||||
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]
|
||||
|
||||
@@ -3,9 +3,12 @@ import {useTempStore} from "~/stores/temp.js";
|
||||
import FloatingActionButton from "~/components/mobile/FloatingActionButton.vue";
|
||||
import EntityTable from "~/components/EntityTable.vue";
|
||||
import EntityTableMobile from "~/components/EntityTableMobile.vue";
|
||||
import {setPageLayout} from "#app";
|
||||
|
||||
const { has } = usePermission()
|
||||
|
||||
const platformIsNative = useCapacitor().getIsNative()
|
||||
|
||||
defineShortcuts({
|
||||
'/': () => {
|
||||
//console.log(searchinput)
|
||||
@@ -67,8 +70,31 @@ const sort = ref({
|
||||
|
||||
const columnsToFilter = ref({})
|
||||
|
||||
const showMobileFilter = ref(false)
|
||||
|
||||
|
||||
//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 = () => {
|
||||
tempStore.clearSearchString(type)
|
||||
searchString.value = ''
|
||||
@@ -77,13 +103,14 @@ const clearSearchString = () => {
|
||||
|
||||
const performSearch = async () => {
|
||||
tempStore.modifySearchString(type,searchString)
|
||||
changePage(1,true)
|
||||
setupPage()
|
||||
}
|
||||
|
||||
const changePage = (number) => {
|
||||
const changePage = (number, noSetup = false) => {
|
||||
page.value = number
|
||||
tempStore.modifyPages(type, number)
|
||||
setupPage()
|
||||
if(!noSetup) setupPage()
|
||||
}
|
||||
|
||||
|
||||
@@ -99,10 +126,23 @@ const changeSort = (column) => {
|
||||
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
|
||||
|
||||
const setupPage = async () => {
|
||||
loading.value = true
|
||||
setPageLayout(platformIsNative ? "mobile" : "default")
|
||||
|
||||
|
||||
const filters = {
|
||||
archived:false
|
||||
@@ -160,17 +200,11 @@ const handleFilterChange = async (action,column) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- <FloatingActionButton
|
||||
:label="`+ ${dataType.labelSingle}`"
|
||||
variant="outline"
|
||||
v-if="platform === 'mobile'"
|
||||
@click="router.push(`/standardEntity/${type}/create`)"
|
||||
/>-->
|
||||
<UDashboardNavbar :title="dataType.label" :badge="itemsMeta.total">
|
||||
<template #toggle>
|
||||
<div v-if="platform === 'mobile'"></div>
|
||||
<div v-if="platformIsNative"></div>
|
||||
</template>
|
||||
<template #right>
|
||||
<template #right v-if="!platformIsNative">
|
||||
<UTooltip :text="`${dataType.label} durchsuchen`">
|
||||
<UInput
|
||||
id="searchinput"
|
||||
@@ -211,58 +245,17 @@ const handleFilterChange = async (action,column) => {
|
||||
</UDashboardNavbar>
|
||||
|
||||
|
||||
<UDashboardToolbar>
|
||||
<UDashboardToolbar v-if="!platformIsNative">
|
||||
<template #left>
|
||||
<UTooltip :text="`${dataType.label} pro Seite`">
|
||||
<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"
|
||||
value-attribute="value"
|
||||
option-attribute="value"
|
||||
@change="setupPage"
|
||||
/>
|
||||
</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
|
||||
v-if="initialSetupDone && items.length > 0"
|
||||
:disabled="loading"
|
||||
@@ -299,95 +292,96 @@ const handleFilterChange = async (action,column) => {
|
||||
</template>
|
||||
</UDashboardToolbar>
|
||||
|
||||
<UTable
|
||||
:loading="loading"
|
||||
:loading-state="{ icon: 'i-heroicons-arrow-path-20-solid', label: 'Loading...' }"
|
||||
sort-mode="manual"
|
||||
v-model:sort="sort"
|
||||
@update:sort="setupPage"
|
||||
v-if="dataType && columns && items.length > 0 && !loading"
|
||||
:rows="items"
|
||||
:columns="columns"
|
||||
class="w-full"
|
||||
style="height: 85dvh"
|
||||
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
|
||||
@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)"
|
||||
v-slot:[`${column.key}-header`]="{row}">
|
||||
<InputGroup>
|
||||
<UTooltip v-if="column.sortable">
|
||||
<div v-if="!platformIsNative">
|
||||
<UTable
|
||||
:loading="loading"
|
||||
:loading-state="{ icon: 'i-heroicons-arrow-path-20-solid', label: 'Loading...' }"
|
||||
sort-mode="manual"
|
||||
v-model:sort="sort"
|
||||
@update:sort="setupPage"
|
||||
v-if="dataType && columns && items.length > 0 && !loading"
|
||||
:rows="items"
|
||||
:columns="columns"
|
||||
class="w-full"
|
||||
style="height: 85dvh"
|
||||
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
|
||||
@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)"
|
||||
v-slot:[`${column.key}-header`]="{row}">
|
||||
<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
|
||||
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'"
|
||||
variant="solid"
|
||||
color="white"
|
||||
v-else
|
||||
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>
|
||||
</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>
|
||||
<UButton
|
||||
@click="handleFilterChange('reset',column.key)"
|
||||
variant="outline"
|
||||
color="rose"
|
||||
>
|
||||
X
|
||||
</UButton>
|
||||
</UTooltip>
|
||||
|
||||
|
||||
</USelectMenu>
|
||||
</UTooltip>
|
||||
<UButton
|
||||
variant="solid"
|
||||
color="white"
|
||||
v-else
|
||||
class="mr-2 truncate"
|
||||
>{{column.label}}</UButton>
|
||||
</InputGroup>
|
||||
|
||||
|
||||
<UTooltip
|
||||
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}">
|
||||
</template>
|
||||
<template #name-data="{row}">
|
||||
<span
|
||||
v-if="row.id === items[selectedItem].id"
|
||||
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}}
|
||||
</UTooltip> </span>
|
||||
<span v-else>
|
||||
<span v-else>
|
||||
<UTooltip
|
||||
:text="row.name"
|
||||
>
|
||||
{{dataType.templateColumns.find(i => i.key === "name").maxLength ? (row.name.length > dataType.templateColumns.find(i => i.key === "name").maxLength ? `${row.name.substring(0,dataType.templateColumns.find(i => i.key === "name").maxLength)}...` : row.name ) : row.name}}
|
||||
</UTooltip>
|
||||
</span>
|
||||
</template>
|
||||
<template #fullName-data="{row}">
|
||||
</template>
|
||||
<template #fullName-data="{row}">
|
||||
<span
|
||||
v-if="row.id === items[selectedItem].id"
|
||||
class="text-primary-500 font-bold">{{row.fullName}}
|
||||
</span>
|
||||
<span v-else>
|
||||
<span v-else>
|
||||
{{row.fullName}}
|
||||
</span>
|
||||
</template>
|
||||
<template #licensePlate-data="{row}">
|
||||
</template>
|
||||
<template #licensePlate-data="{row}">
|
||||
<span
|
||||
v-if="row.id === items[selectedItem].id"
|
||||
class="text-primary-500 font-bold">{{row.licensePlate}}
|
||||
</span>
|
||||
<span v-else>
|
||||
<span v-else>
|
||||
{{row.licensePlate}}
|
||||
</span>
|
||||
</template>
|
||||
<template
|
||||
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}">
|
||||
<component v-if="column.component" :is="column.component" :row="row"></component>
|
||||
<span v-else-if="row[column.key]">
|
||||
</template>
|
||||
<template
|
||||
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}">
|
||||
<component v-if="column.component" :is="column.component" :row="row"></component>
|
||||
<span v-else-if="row[column.key]">
|
||||
<UTooltip :text="row[column.key]">
|
||||
{{row[column.key] ? `${column.maxLength ? (row[column.key].length > column.maxLength ? `${row[column.key].substring(0,column.maxLength)}...` : row[column.key]) : row[column.key]} ${column.unit ? column.unit : ''}`: ''}}
|
||||
</UTooltip>
|
||||
</span>
|
||||
|
||||
</template>
|
||||
</UTable>
|
||||
<UCard
|
||||
class="w-1/3 mx-auto mt-10"
|
||||
v-else-if="!loading"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col text-center"
|
||||
</template>
|
||||
</UTable>
|
||||
<UCard
|
||||
class="w-1/3 mx-auto mt-10"
|
||||
v-else-if="!loading"
|
||||
>
|
||||
<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
|
||||
class="flex flex-col text-center"
|
||||
>
|
||||
<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>
|
||||
|
||||
<!-- 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>
|
||||
<UProgress v-else animation="carousel" class="w-3/4 mx-auto mt-5"></UProgress>
|
||||
<UCard
|
||||
v-for="item in items"
|
||||
:key="item.id"
|
||||
class="p-4 rounded-xl shadow-sm border cursor-pointer active:scale-[0.98] transition"
|
||||
@click="router.push(`/standardEntity/${type}/show/${item.id}`)"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<p class="text-base font-semibold truncate text-primary-600">
|
||||
{{
|
||||
dataType.templateColumns.find(i => i.title)?.key
|
||||
? item[dataType.templateColumns.find(i => i.title).key]
|
||||
: null
|
||||
}}
|
||||
</p>
|
||||
|
||||
<UIcon
|
||||
name="i-heroicons-chevron-right-20-solid"
|
||||
class="text-gray-400 w-5 h-5 flex-shrink-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-gray-500 truncate">
|
||||
{{ dataType.numberRangeHolder ? item[dataType.numberRangeHolder] : null }}
|
||||
</p>
|
||||
|
||||
<div
|
||||
v-for="secondInfo in dataType.templateColumns.filter(i => i.secondInfo)"
|
||||
:key="secondInfo.key"
|
||||
class="text-sm text-gray-400 truncate"
|
||||
>
|
||||
{{
|
||||
(secondInfo.secondInfoKey && item[secondInfo.key])
|
||||
? item[secondInfo.key][secondInfo.secondInfoKey]
|
||||
: item[secondInfo.key]
|
||||
}}
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<div
|
||||
v-if="!loading && items.length > 0"
|
||||
class="p-4 bg-white dark:bg-gray-900 border-t flex items-center justify-center mt-4 rounded-xl"
|
||||
>
|
||||
<UPagination
|
||||
v-if="initialSetupDone && items.length > 0"
|
||||
:disabled="loading"
|
||||
v-model="page"
|
||||
:page-count="pageLimit"
|
||||
:total="itemsMeta.total"
|
||||
@update:modelValue="(i) => changePage(i)"
|
||||
show-first
|
||||
show-last
|
||||
:first-button="{ icon: 'i-heroicons-chevron-double-left', color: 'gray' }"
|
||||
:last-button="{ icon: 'i-heroicons-chevron-double-right', trailing: true, color: 'gray' }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Empty -->
|
||||
<UCard
|
||||
v-if="!loading && items.length === 0"
|
||||
class="mx-auto mt-10 p-6 text-center"
|
||||
>
|
||||
<UIcon name="i-heroicons-circle-stack-20-solid" class="mx-auto w-10 h-10 mb-3"/>
|
||||
<p class="font-bold">Keine {{ dataType.label }} gefunden</p>
|
||||
</UCard>
|
||||
|
||||
<div v-if="loading" class="mt-5">
|
||||
<UProgress animation="carousel" class="w-3/4 mx-auto"></UProgress>
|
||||
</div>
|
||||
|
||||
</UDashboardPanelContent>
|
||||
<!-- Mobile Filter Slideover -->
|
||||
<USlideover
|
||||
v-model="showMobileFilter"
|
||||
side="bottom"
|
||||
:ui="{ width: '100%', height: 'auto', maxHeight: '90vh' }"
|
||||
class="pb-[env(safe-area-inset-bottom)]"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="p-4 border-b flex items-center justify-between flex-shrink-0">
|
||||
<h2 class="text-xl font-bold">Filter</h2>
|
||||
<UButton
|
||||
icon="i-heroicons-x-mark"
|
||||
variant="ghost"
|
||||
@click="showMobileFilter = false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Scrollable content -->
|
||||
<div class="flex-1 overflow-y-auto p-4 space-y-6">
|
||||
|
||||
<div
|
||||
v-for="column in dataType.templateColumns.filter(c => c.distinct)"
|
||||
:key="column.key"
|
||||
class="space-y-2"
|
||||
>
|
||||
<p class="font-semibold">{{ column.label }}</p>
|
||||
|
||||
<USelectMenu
|
||||
v-model="columnsToFilter[column.key]"
|
||||
:options="itemsMeta?.distinctValues?.[column.key]"
|
||||
multiple
|
||||
searchable
|
||||
:search-attributes="[column.key]"
|
||||
placeholder="Auswählen…"
|
||||
:ui-menu="{ width: '100%' }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Footer FIXED in card -->
|
||||
<div
|
||||
class="
|
||||
flex justify-between gap-3
|
||||
px-4 py-4 border-t flex-shrink-0
|
||||
bg-white dark:bg-gray-900
|
||||
rounded-b-2xl
|
||||
"
|
||||
>
|
||||
<UButton
|
||||
color="rose"
|
||||
variant="outline"
|
||||
class="flex-1"
|
||||
@click="resetMobileFilters"
|
||||
>
|
||||
Zurücksetzen
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
color="primary"
|
||||
class="flex-1"
|
||||
@click="applyMobileFilters"
|
||||
>
|
||||
Anwenden
|
||||
</UButton>
|
||||
</div>
|
||||
</USlideover>
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -4,12 +4,12 @@ export default defineNuxtPlugin(() => {
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
const api = $fetch.create({
|
||||
baseURL: config.public.apiBase,/*"http://192.168.1.227:3100" "https://backend.fedeo.io"*/
|
||||
baseURL: config.public.apiBase,
|
||||
credentials: "include",
|
||||
async onRequest({options}) {
|
||||
// Token aus Cookie holen
|
||||
let token: string | null | undefined = ""
|
||||
if (await useCapacitor().getIsNative()) {
|
||||
if (useCapacitor().getIsNative()) {
|
||||
const {value} = await Preferences.get({key: 'token'});
|
||||
token = value
|
||||
} else {
|
||||
|
||||
@@ -63,13 +63,18 @@ export const useAuthStore = defineStore("auth", {
|
||||
},
|
||||
|
||||
async login(email: string, password: string) {
|
||||
console.log("Auth login")
|
||||
const { token } = await useNuxtApp().$api("/auth/login", {
|
||||
method: "POST",
|
||||
body: { email, password }
|
||||
})
|
||||
console.log(token)
|
||||
await this.fetchMe(token)
|
||||
try {
|
||||
console.log("Auth login")
|
||||
const { token } = await useNuxtApp().$api("/auth/login", {
|
||||
method: "POST",
|
||||
body: { email, password }
|
||||
})
|
||||
console.log("Token: " + token)
|
||||
await this.fetchMe(token)
|
||||
} catch (e) {
|
||||
console.log("login error:" + e)
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
async logout() {
|
||||
@@ -105,6 +110,7 @@ export const useAuthStore = defineStore("auth", {
|
||||
jwt
|
||||
}}
|
||||
})
|
||||
console.log(me)
|
||||
this.user = me.user
|
||||
this.permissions = me.permissions
|
||||
this.tenants = me.tenants
|
||||
@@ -143,7 +149,7 @@ export const useAuthStore = defineStore("auth", {
|
||||
|
||||
const {token} = res
|
||||
|
||||
if(await useCapacitor().getIsNative()) {
|
||||
if(useCapacitor().getIsNative()) {
|
||||
await Preferences.set({
|
||||
key:"token",
|
||||
value: token,
|
||||
|
||||
@@ -950,7 +950,7 @@ export const useDataStore = defineStore('data', () => {
|
||||
selectSearchAttributes: ['name'],
|
||||
},
|
||||
{
|
||||
key: "purchasePrice",
|
||||
key: "purchase_price",
|
||||
label: "Einkaufspreis",
|
||||
component: purchasePrice,
|
||||
inputType: "number",
|
||||
@@ -963,7 +963,7 @@ export const useDataStore = defineStore('data', () => {
|
||||
}
|
||||
}
|
||||
},{
|
||||
key: "markupPercentage",
|
||||
key: "markup_percentage",
|
||||
label: "Aufschlag",
|
||||
inputType: "number",
|
||||
inputTrailing: "%",
|
||||
@@ -977,7 +977,7 @@ export const useDataStore = defineStore('data', () => {
|
||||
}
|
||||
}
|
||||
},{
|
||||
key: "sellingPrice",
|
||||
key: "selling_price",
|
||||
label: "Verkaufpreispreis",
|
||||
required: true,
|
||||
component: sellingPrice,
|
||||
@@ -991,7 +991,7 @@ export const useDataStore = defineStore('data', () => {
|
||||
}
|
||||
}
|
||||
},{
|
||||
key: "taxPercentage",
|
||||
key: "tax_percentage",
|
||||
label: "Umsatzsteuer",
|
||||
inputType: "select",
|
||||
selectOptionAttribute: "label",
|
||||
@@ -1194,7 +1194,7 @@ export const useDataStore = defineStore('data', () => {
|
||||
inputType: "bool",
|
||||
sortable: true
|
||||
},{
|
||||
key: 'licensePlate',
|
||||
key: 'license_plate',
|
||||
label: "Kennzeichen",
|
||||
required: true,
|
||||
inputType: "text",
|
||||
@@ -1219,18 +1219,18 @@ export const useDataStore = defineStore('data', () => {
|
||||
component: driver
|
||||
},*/
|
||||
{
|
||||
key: "tankSize",
|
||||
key: "tank_size",
|
||||
label: "Tankvolumen",
|
||||
unit: "L",
|
||||
inputType: "number"
|
||||
},
|
||||
{
|
||||
key: "buildYear",
|
||||
key: "build_year",
|
||||
label: "Baujahr",
|
||||
inputType: "number"
|
||||
},
|
||||
{
|
||||
key: "towingCapacity",
|
||||
key: "towing_capacity",
|
||||
label: "Anhängelast",
|
||||
unit: "Kg",
|
||||
inputType: "number",
|
||||
@@ -1242,7 +1242,7 @@ export const useDataStore = defineStore('data', () => {
|
||||
inputType: "text"
|
||||
},
|
||||
{
|
||||
key: "powerInKW",
|
||||
key: "power_in_kw",
|
||||
label: "Leistung",
|
||||
unit: "kW",
|
||||
inputType: "number",
|
||||
@@ -1459,7 +1459,7 @@ export const useDataStore = defineStore('data', () => {
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
key: 'spaceNumber',
|
||||
key: 'space_number',
|
||||
label: "Lagerplatznr.",
|
||||
inputType: "text",
|
||||
inputIsNumberRange: true,
|
||||
@@ -1483,7 +1483,7 @@ export const useDataStore = defineStore('data', () => {
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
key: "parentSpace",
|
||||
key: "parent_space",
|
||||
label: "Übergeordneter Lagerplatz",
|
||||
inputType: "select",
|
||||
selectDataType: "spaces",
|
||||
@@ -1492,21 +1492,21 @@ export const useDataStore = defineStore('data', () => {
|
||||
inputColumn: "Allgemeines"
|
||||
},
|
||||
{
|
||||
key: "infoData.streetNumber",
|
||||
key: "info_data.streetNumber",
|
||||
label: "Straße + Hausnummer",
|
||||
inputType: "text",
|
||||
disabledInTable: true,
|
||||
inputColumn: "Ort"
|
||||
},
|
||||
{
|
||||
key: "infoData.special",
|
||||
key: "info_data.special",
|
||||
label: "Adresszusatz",
|
||||
inputType: "text",
|
||||
disabledInTable: true,
|
||||
inputColumn: "Ort"
|
||||
},
|
||||
{
|
||||
key: "infoData.zip",
|
||||
key: "info_data.zip",
|
||||
label: "Postleitzahl",
|
||||
inputType: "number",
|
||||
disabledInTable: true,
|
||||
@@ -1518,14 +1518,14 @@ export const useDataStore = defineStore('data', () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "infoData.city",
|
||||
key: "info_data.city",
|
||||
label: "Stadt",
|
||||
inputType: "text",
|
||||
disabledInTable: true,
|
||||
inputColumn: "Ort"
|
||||
},
|
||||
{
|
||||
key: "infoData.country",
|
||||
key: "info_data.country",
|
||||
label: "Land",
|
||||
inputType: "select",
|
||||
selectDataType: "countrys",
|
||||
@@ -1574,6 +1574,12 @@ export const useDataStore = defineStore('data', () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
templateColumns: [
|
||||
{
|
||||
key: "customer",
|
||||
distinct: true
|
||||
}
|
||||
]
|
||||
},
|
||||
tickets: {
|
||||
@@ -2192,8 +2198,8 @@ export const useDataStore = defineStore('data', () => {
|
||||
label: "Fahrzeuge",
|
||||
inputType: "select",
|
||||
selectDataType: "vehicles",
|
||||
selectOptionAttribute: "licensePlate",
|
||||
selectSearchAttributes: ['licensePlate'],
|
||||
selectOptionAttribute: "license_plate",
|
||||
selectSearchAttributes: ['license_plate'],
|
||||
selectMultiple: true,
|
||||
component: vehiclesWithLoad,
|
||||
},{
|
||||
|
||||
Reference in New Issue
Block a user