Compare commits
5 Commits
cfd84b773f
...
11a242d70d
| Author | SHA1 | Date | |
|---|---|---|---|
| 11a242d70d | |||
| 9f665fc3b8 | |||
| 03bcc1a939 | |||
| 68b2cbb0ee | |||
| b009ac845f |
@@ -1,7 +1,9 @@
|
||||
export default defineAppConfig({
|
||||
ui: {
|
||||
primary: 'green',
|
||||
gray: 'slate',
|
||||
colors: {
|
||||
primary: 'green',
|
||||
neutral: 'slate'
|
||||
},
|
||||
tooltip: {
|
||||
background: '!bg-background'
|
||||
},
|
||||
@@ -35,4 +37,4 @@ export default defineAppConfig({
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup>
|
||||
import * as Sentry from "@sentry/browser"
|
||||
import { de as germanLocale } from "@nuxt/ui/locale"
|
||||
|
||||
|
||||
|
||||
@@ -47,14 +48,16 @@ useSeoMeta({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="safearea">
|
||||
<NuxtLayout>
|
||||
<NuxtPage/>
|
||||
</NuxtLayout>
|
||||
<UNotifications/>
|
||||
<USlideovers />
|
||||
<UModals/>
|
||||
</div>
|
||||
<UApp :locale="germanLocale">
|
||||
<div class="safearea">
|
||||
<NuxtLayout>
|
||||
<NuxtPage/>
|
||||
</NuxtLayout>
|
||||
<UNotifications/>
|
||||
<USlideovers />
|
||||
<UModals/>
|
||||
</div>
|
||||
</UApp>
|
||||
|
||||
|
||||
|
||||
@@ -136,4 +139,4 @@ useSeoMeta({
|
||||
.scroll {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
26
frontend/assets/css/main.css
Normal file
26
frontend/assets/css/main.css
Normal file
@@ -0,0 +1,26 @@
|
||||
@import "tailwindcss";
|
||||
@import "@nuxt/ui-pro";
|
||||
|
||||
@theme static {
|
||||
--font-sans: "SF Pro Text", "SF Pro Display", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
--font-mono: "SF Mono", "Cascadia Code", "JetBrains Mono", Consolas, "Liberation Mono", Menlo, monospace;
|
||||
--color-green-50: #f4fbf2;
|
||||
--color-green-100: #e7f7e1;
|
||||
--color-green-200: #cdeec4;
|
||||
--color-green-300: #a6e095;
|
||||
--color-green-400: #69c350;
|
||||
--color-green-500: #53ad3a;
|
||||
--color-green-600: #418e2b;
|
||||
--color-green-700: #357025;
|
||||
--color-green-800: #2d5922;
|
||||
--color-green-900: #254a1d;
|
||||
--color-green-950: #10280b;
|
||||
}
|
||||
|
||||
:root {
|
||||
--ui-container: 90rem;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
@@ -38,37 +38,39 @@ const emitConfirm = () => {
|
||||
>
|
||||
Archivieren
|
||||
</UButton>
|
||||
<UModal v-model="showModal">
|
||||
<UCard>
|
||||
<template #header>
|
||||
<span class="text-md font-bold">Archivieren bestätigen</span>
|
||||
</template>
|
||||
Möchten Sie diese/-s/-n {{dataType.labelSingle}} wirklich archivieren?
|
||||
<UModal v-model:open="showModal">
|
||||
<template #content>
|
||||
<UCard>
|
||||
<template #header>
|
||||
<span class="text-md font-bold">Archivieren bestätigen</span>
|
||||
</template>
|
||||
Möchten Sie diese/-s/-n {{dataType.labelSingle}} wirklich archivieren?
|
||||
|
||||
<template #footer>
|
||||
<div class="text-right">
|
||||
<UButtonGroup>
|
||||
<UButton
|
||||
variant="outline"
|
||||
@click="showModal = false"
|
||||
>
|
||||
Abbrechen
|
||||
</UButton>
|
||||
<UButton
|
||||
@click="emitConfirm"
|
||||
class="ml-2"
|
||||
color="rose"
|
||||
>
|
||||
Archivieren
|
||||
</UButton>
|
||||
</UButtonGroup>
|
||||
<template #footer>
|
||||
<div class="text-right">
|
||||
<UButtonGroup>
|
||||
<UButton
|
||||
variant="outline"
|
||||
@click="showModal = false"
|
||||
>
|
||||
Abbrechen
|
||||
</UButton>
|
||||
<UButton
|
||||
@click="emitConfirm"
|
||||
class="ml-2"
|
||||
color="error"
|
||||
>
|
||||
Archivieren
|
||||
</UButton>
|
||||
</UButtonGroup>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
</template>
|
||||
</UModal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -49,7 +49,7 @@ const assignByIban = async () => {
|
||||
|
||||
const match = accounts.value.find((a) => normalizeIban(a.iban) === search)
|
||||
if (!match) {
|
||||
toast.add({ title: "Kein Bankkonto mit dieser IBAN gefunden.", color: "rose" })
|
||||
toast.add({ title: "Kein Bankkonto mit dieser IBAN gefunden.", color: "error" })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ const removeAssigned = (id) => {
|
||||
|
||||
const createAndAssign = async () => {
|
||||
if (!createPayload.value.iban || !createPayload.value.bic || !createPayload.value.bankName) {
|
||||
toast.add({ title: "IBAN, BIC und Bankinstitut sind Pflichtfelder.", color: "rose" })
|
||||
toast.add({ title: "IBAN, BIC und Bankinstitut sind Pflichtfelder.", color: "error" })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -140,43 +140,45 @@ loadAccounts()
|
||||
</InputGroup>
|
||||
</div>
|
||||
|
||||
<UModal v-model="showCreate">
|
||||
<UCard>
|
||||
<template #header>Neue Bankverbindung erstellen</template>
|
||||
<div class="space-y-3">
|
||||
<UFormGroup label="IBAN">
|
||||
<InputGroup>
|
||||
<UInput
|
||||
v-model="createPayload.iban"
|
||||
@blur="resolveCreatePayloadFromIban"
|
||||
@keydown.enter.prevent="resolveCreatePayloadFromIban"
|
||||
/>
|
||||
<UButton
|
||||
color="gray"
|
||||
variant="outline"
|
||||
:loading="resolvingIban"
|
||||
@click="resolveCreatePayloadFromIban"
|
||||
>
|
||||
Ermitteln
|
||||
</UButton>
|
||||
</InputGroup>
|
||||
</UFormGroup>
|
||||
<UFormGroup label="BIC">
|
||||
<UInput v-model="createPayload.bic" />
|
||||
</UFormGroup>
|
||||
<UFormGroup label="Bankinstitut">
|
||||
<UInput v-model="createPayload.bankName" />
|
||||
</UFormGroup>
|
||||
<UFormGroup label="Beschreibung (optional)">
|
||||
<UInput v-model="createPayload.description" />
|
||||
</UFormGroup>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<UButton color="gray" variant="outline" @click="showCreate = false">Abbrechen</UButton>
|
||||
<UButton @click="createAndAssign">Erstellen und zuweisen</UButton>
|
||||
<UModal v-model:open="showCreate">
|
||||
<template #content>
|
||||
<UCard>
|
||||
<template #header>Neue Bankverbindung erstellen</template>
|
||||
<div class="space-y-3">
|
||||
<UFormField label="IBAN">
|
||||
<InputGroup>
|
||||
<UInput
|
||||
v-model="createPayload.iban"
|
||||
@blur="resolveCreatePayloadFromIban"
|
||||
@keydown.enter.prevent="resolveCreatePayloadFromIban"
|
||||
/>
|
||||
<UButton
|
||||
color="gray"
|
||||
variant="outline"
|
||||
:loading="resolvingIban"
|
||||
@click="resolveCreatePayloadFromIban"
|
||||
>
|
||||
Ermitteln
|
||||
</UButton>
|
||||
</InputGroup>
|
||||
</UFormField>
|
||||
<UFormField label="BIC">
|
||||
<UInput v-model="createPayload.bic" />
|
||||
</UFormField>
|
||||
<UFormField label="Bankinstitut">
|
||||
<UInput v-model="createPayload.bankName" />
|
||||
</UFormField>
|
||||
<UFormField label="Beschreibung (optional)">
|
||||
<UInput v-model="createPayload.description" />
|
||||
</UFormField>
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<UButton color="gray" variant="outline" @click="showCreate = false">Abbrechen</UButton>
|
||||
<UButton @click="createAndAssign">Erstellen und zuweisen</UButton>
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
</template>
|
||||
</UModal>
|
||||
</template>
|
||||
|
||||
@@ -31,36 +31,38 @@ const emitConfirm = () => {
|
||||
>
|
||||
<slot name="button"></slot>
|
||||
</UButton>
|
||||
<UModal v-model="showModal">
|
||||
<UCard>
|
||||
<template #header>
|
||||
<slot name="header"></slot>
|
||||
</template>
|
||||
<slot/>
|
||||
<template #footer>
|
||||
<div class="text-right">
|
||||
<UButtonGroup>
|
||||
<UButton
|
||||
variant="outline"
|
||||
@click="showModal = false"
|
||||
>
|
||||
Abbrechen
|
||||
</UButton>
|
||||
<UButton
|
||||
@click="emitConfirm"
|
||||
class="ml-2"
|
||||
color="rose"
|
||||
>
|
||||
Archivieren
|
||||
</UButton>
|
||||
</UButtonGroup>
|
||||
<UModal v-model:open="showModal">
|
||||
<template #content>
|
||||
<UCard>
|
||||
<template #header>
|
||||
<slot name="header"></slot>
|
||||
</template>
|
||||
<slot/>
|
||||
<template #footer>
|
||||
<div class="text-right">
|
||||
<UButtonGroup>
|
||||
<UButton
|
||||
variant="outline"
|
||||
@click="showModal = false"
|
||||
>
|
||||
Abbrechen
|
||||
</UButton>
|
||||
<UButton
|
||||
@click="emitConfirm"
|
||||
class="ml-2"
|
||||
color="error"
|
||||
>
|
||||
Archivieren
|
||||
</UButton>
|
||||
</UButtonGroup>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
</template>
|
||||
</UModal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -51,8 +51,8 @@
|
||||
<div class="grid grid-cols-4 gap-2">
|
||||
<UTooltip text="Brutto (+19%)"><UButton color="green" variant="soft" block size="xs" @click="applyTax(19)">+19%</UButton></UTooltip>
|
||||
<UTooltip text="Brutto (+7%)"><UButton color="green" variant="soft" block size="xs" @click="applyTax(7)">+7%</UButton></UTooltip>
|
||||
<UTooltip text="Netto (-19%)"><UButton color="rose" variant="soft" block size="xs" @click="removeTax(19)">-19%</UButton></UTooltip>
|
||||
<UTooltip text="Netto (-7%)"><UButton color="rose" variant="soft" block size="xs" @click="removeTax(7)">-7%</UButton></UTooltip>
|
||||
<UTooltip text="Netto (-19%)"><UButton color="error" variant="soft" block size="xs" @click="removeTax(19)">-19%</UButton></UTooltip>
|
||||
<UTooltip text="Netto (-7%)"><UButton color="error" variant="soft" block size="xs" @click="removeTax(7)">-7%</UButton></UTooltip>
|
||||
|
||||
<UTooltip text="Löschen"><UButton color="gray" variant="ghost" block @click="clear">C</UButton></UTooltip>
|
||||
<UTooltip text="Speicher +"><UButton color="gray" variant="ghost" block @click="addToSum">M+</UButton></UTooltip>
|
||||
@@ -227,9 +227,14 @@ defineShortcuts({
|
||||
width: 4px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
@apply bg-transparent;
|
||||
background: transparent;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
@apply bg-gray-200 dark:bg-gray-700 rounded-full;
|
||||
background: #e5e7eb;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
</style>
|
||||
|
||||
:global(.dark) .custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: #374151;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -156,7 +156,8 @@ const moveFile = async () => {
|
||||
|
||||
<template>
|
||||
<UModal fullscreen >
|
||||
<UCard :ui="{ body: { base: 'flex-1' }, ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }" class="h-full">
|
||||
<template #content>
|
||||
<UCard :ui="{ body: { base: 'flex-1' }, ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }" class="h-full">
|
||||
<template #header>
|
||||
<div class="flex flex-row justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -186,7 +187,7 @@ const moveFile = async () => {
|
||||
<div class="w-2/3 p-5" v-if="!false">
|
||||
<UButtonGroup>
|
||||
<ArchiveButton
|
||||
color="rose"
|
||||
color="error"
|
||||
variant="outline"
|
||||
type="files"
|
||||
@confirmed="archiveDocument"
|
||||
@@ -202,7 +203,7 @@ const moveFile = async () => {
|
||||
</UButton>
|
||||
</UButtonGroup>
|
||||
|
||||
<UDivider>Zuweisungen</UDivider>
|
||||
<USeparator label="Zuweisungen"/>
|
||||
<table class="w-full">
|
||||
<tr v-if="props.documentData.project">
|
||||
<td>Projekt</td>
|
||||
@@ -278,44 +279,44 @@ const moveFile = async () => {
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<UDivider class="my-3">Datei zuweisen</UDivider>
|
||||
<USeparator class="my-3" label="Datei zuweisen"/>
|
||||
|
||||
<UFormGroup
|
||||
<UFormField
|
||||
label="Resource auswählen"
|
||||
>
|
||||
<USelectMenu
|
||||
:options="resourceOptions"
|
||||
:items="resourceOptions"
|
||||
v-model="resourceToAssign"
|
||||
value-attribute="value"
|
||||
option-attribute="label"
|
||||
value-key="value"
|
||||
label-key="label"
|
||||
@change="getItemsBySelectedResource"
|
||||
>
|
||||
|
||||
</USelectMenu>
|
||||
</UFormGroup>
|
||||
<UFormGroup
|
||||
</UFormField>
|
||||
<UFormField
|
||||
label="Eintrag auswählen:"
|
||||
>
|
||||
<USelectMenu
|
||||
:options="itemOptions"
|
||||
:items="itemOptions"
|
||||
v-model="idToAssign"
|
||||
:option-attribute="resourceOptions.find(i => i.value === resourceToAssign)? resourceOptions.find(i => i.value === resourceToAssign).optionAttr : 'name'"
|
||||
value-attribute="id"
|
||||
:label-key="resourceOptions.find(i => i.value === resourceToAssign)? resourceOptions.find(i => i.value === resourceToAssign).optionAttr : 'name'"
|
||||
value-key="id"
|
||||
@change="updateDocumentAssignment"
|
||||
></USelectMenu>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
|
||||
|
||||
<UDivider class="my-5">Datei verschieben</UDivider>
|
||||
<USeparator class="my-5" label="Datei verschieben"/>
|
||||
|
||||
<InputGroup class="w-full">
|
||||
<USelectMenu
|
||||
class="flex-auto"
|
||||
v-model="folderToMoveTo"
|
||||
value-attribute="id"
|
||||
option-attribute="name"
|
||||
:options="folders"
|
||||
value-key="id"
|
||||
label-key="name"
|
||||
:items="folders"
|
||||
/>
|
||||
<UButton
|
||||
@click="moveFile"
|
||||
@@ -324,34 +325,35 @@ const moveFile = async () => {
|
||||
>Verschieben</UButton>
|
||||
</InputGroup>
|
||||
|
||||
<UDivider class="my-5">Dateityp</UDivider>
|
||||
<USeparator class="my-5" label="Dateityp"/>
|
||||
|
||||
<InputGroup class="w-full">
|
||||
<USelectMenu
|
||||
class="flex-auto"
|
||||
v-model="props.documentData.type"
|
||||
value-attribute="id"
|
||||
option-attribute="name"
|
||||
:options="filetypes"
|
||||
value-key="id"
|
||||
label-key="name"
|
||||
:items="filetypes"
|
||||
@change="updateDocument"
|
||||
/>
|
||||
</InputGroup>
|
||||
<UDivider class="my-5">Dokumentenbox</UDivider>
|
||||
<USeparator class="my-5" label="Dokumentenbox" />
|
||||
|
||||
<InputGroup class="w-full">
|
||||
<USelectMenu
|
||||
class="flex-auto"
|
||||
v-model="props.documentData.documentbox"
|
||||
value-attribute="id"
|
||||
option-attribute="key"
|
||||
:options="documentboxes"
|
||||
value-key="id"
|
||||
label-key="key"
|
||||
:items="documentboxes"
|
||||
@change="updateDocument"
|
||||
/>
|
||||
</InputGroup>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
</UCard>
|
||||
</template>
|
||||
</UModal>
|
||||
</template>
|
||||
|
||||
@@ -362,4 +364,4 @@ const moveFile = async () => {
|
||||
aspect-ratio: 1/ 1.414;
|
||||
}
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -78,84 +78,86 @@ const fileNames = computed(() => {
|
||||
|
||||
<template>
|
||||
<UModal>
|
||||
<div ref="dropZoneRef" class="relative h-full flex flex-col">
|
||||
<template #content>
|
||||
<div ref="dropZoneRef" class="relative h-full flex flex-col">
|
||||
|
||||
<div
|
||||
v-if="isOverDropZone"
|
||||
class="absolute inset-0 z-50 flex items-center justify-center bg-primary-500/10 border-2 border-primary-500 border-dashed rounded-lg backdrop-blur-sm transition-all"
|
||||
>
|
||||
<span class="text-xl font-bold text-primary-600 bg-white/80 px-4 py-2 rounded shadow-sm">
|
||||
Dateien hier ablegen
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<UCard :ui="{ body: { base: 'flex-1' }, ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
||||
Datei hochladen
|
||||
</h3>
|
||||
<UButton
|
||||
color="gray"
|
||||
variant="ghost"
|
||||
icon="i-heroicons-x-mark-20-solid"
|
||||
class="-my-1"
|
||||
@click="modal.close()"
|
||||
:disabled="uploadInProgress"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<UFormGroup
|
||||
label="Datei:"
|
||||
:help="selectedFiles.length > 0 ? `${selectedFiles.length} Datei(en) ausgewählt` : 'Ziehen Sie Dateien hierher oder klicken Sie'"
|
||||
<div
|
||||
v-if="isOverDropZone"
|
||||
class="absolute inset-0 z-50 flex items-center justify-center bg-primary-500/10 border-2 border-primary-500 border-dashed rounded-lg backdrop-blur-sm transition-all"
|
||||
>
|
||||
<UInput
|
||||
v-if="selectedFiles.length === 0"
|
||||
type="file"
|
||||
id="fileUploadInput"
|
||||
multiple
|
||||
accept="image/jpeg, image/png, image/gif, application/pdf"
|
||||
@change="onFileInputChange"
|
||||
/>
|
||||
<span class="text-xl font-bold text-primary-600 bg-white/80 px-4 py-2 rounded shadow-sm">
|
||||
Dateien hier ablegen
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedFiles.length > 0" class="mt-2 text-sm text-gray-500">
|
||||
Ausgewählt: <span class="font-medium text-gray-700 dark:text-gray-300">{{ fileNames }}</span>
|
||||
</div>
|
||||
</UFormGroup>
|
||||
<UCard :ui="{ body: { base: 'flex-1' }, ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
||||
Datei hochladen
|
||||
</h3>
|
||||
<UButton
|
||||
color="gray"
|
||||
variant="ghost"
|
||||
icon="i-heroicons-x-mark-20-solid"
|
||||
class="-my-1"
|
||||
@click="modal.close()"
|
||||
:disabled="uploadInProgress"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<UFormGroup
|
||||
label="Typ:"
|
||||
class="mt-3"
|
||||
>
|
||||
<USelectMenu
|
||||
option-attribute="name"
|
||||
value-attribute="id"
|
||||
searchable
|
||||
searchable-placeholder="Suchen..."
|
||||
:options="availableFiletypes"
|
||||
v-model="props.fileData.type"
|
||||
:disabled="!props.fileData.typeEnabled"
|
||||
<UFormField
|
||||
label="Datei:"
|
||||
:help="selectedFiles.length > 0 ? `${selectedFiles.length} Datei(en) ausgewählt` : 'Ziehen Sie Dateien hierher oder klicken Sie'"
|
||||
>
|
||||
<template #label>
|
||||
<span v-if="availableFiletypes.find(x => x.id === props.fileData.type)">{{availableFiletypes.find(x => x.id === props.fileData.type).name}}</span>
|
||||
<span v-else>Kein Typ ausgewählt</span>
|
||||
</template>
|
||||
</USelectMenu>
|
||||
</UFormGroup>
|
||||
<UInput
|
||||
v-if="selectedFiles.length === 0"
|
||||
type="file"
|
||||
id="fileUploadInput"
|
||||
multiple
|
||||
accept="image/jpeg, image/png, image/gif, application/pdf"
|
||||
@change="onFileInputChange"
|
||||
/>
|
||||
|
||||
<template #footer>
|
||||
<UButton
|
||||
@click="uploadFiles"
|
||||
:loading="uploadInProgress"
|
||||
:disabled="uploadInProgress || selectedFiles.length === 0"
|
||||
>Hochladen</UButton>
|
||||
</template>
|
||||
</UCard>
|
||||
</div>
|
||||
<div v-if="selectedFiles.length > 0" class="mt-2 text-sm text-gray-500">
|
||||
Ausgewählt: <span class="font-medium text-gray-700 dark:text-gray-300">{{ fileNames }}</span>
|
||||
</div>
|
||||
</UFormField>
|
||||
|
||||
<UFormField
|
||||
label="Typ:"
|
||||
class="mt-3"
|
||||
>
|
||||
<USelectMenu
|
||||
option-attribute="name"
|
||||
value-attribute="id"
|
||||
searchable
|
||||
searchable-placeholder="Suchen..."
|
||||
:options="availableFiletypes"
|
||||
v-model="props.fileData.type"
|
||||
:disabled="!props.fileData.typeEnabled"
|
||||
>
|
||||
<template #label>
|
||||
<span v-if="availableFiletypes.find(x => x.id === props.fileData.type)">{{availableFiletypes.find(x => x.id === props.fileData.type).name}}</span>
|
||||
<span v-else>Kein Typ ausgewählt</span>
|
||||
</template>
|
||||
</USelectMenu>
|
||||
</UFormField>
|
||||
|
||||
<template #footer>
|
||||
<UButton
|
||||
@click="uploadFiles"
|
||||
:loading="uploadInProgress"
|
||||
:disabled="uploadInProgress || selectedFiles.length === 0"
|
||||
>Hochladen</UButton>
|
||||
</template>
|
||||
</UCard>
|
||||
</div>
|
||||
</template>
|
||||
</UModal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Optional: Animationen für das Overlay */
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -211,6 +211,22 @@ const contentChanged = (content, datapoint) => {
|
||||
}
|
||||
}
|
||||
|
||||
const getSelectItems = (datapoint) => {
|
||||
return datapoint.selectManualOptions || loadedOptions.value[datapoint.selectDataType] || []
|
||||
}
|
||||
|
||||
const getSelectValueKey = (datapoint) => {
|
||||
return datapoint.selectValueAttribute || 'id'
|
||||
}
|
||||
|
||||
const getSelectLabelKey = (datapoint) => {
|
||||
return datapoint.selectOptionAttribute || 'label'
|
||||
}
|
||||
|
||||
const getSelectSearchInput = (datapoint) => {
|
||||
return datapoint.selectSearchAttributes ? { placeholder: 'Suche...' } : false
|
||||
}
|
||||
|
||||
|
||||
const createItem = async () => {
|
||||
let ret = null
|
||||
@@ -264,7 +280,7 @@ const updateItem = async () => {
|
||||
</template>
|
||||
<template #right>
|
||||
<ArchiveButton
|
||||
color="rose"
|
||||
color="error"
|
||||
v-if="platform !== 'mobile'"
|
||||
variant="outline"
|
||||
:type="type"
|
||||
@@ -336,12 +352,12 @@ const updateItem = async () => {
|
||||
v-for="(columnName,index) in dataType.inputColumns"
|
||||
:class="platform === 'mobile' ? ['w-full'] : [`w-1/${dataType.inputColumns.length}`, ... index < dataType.inputColumns.length -1 ? ['mr-5'] : []]"
|
||||
>
|
||||
<UDivider>{{ columnName }}</UDivider>
|
||||
<USeparator :label="columnName"/>
|
||||
|
||||
<div
|
||||
v-for="datapoint in dataType.templateColumns.filter(i => i.inputType && i.inputColumn === columnName)"
|
||||
>
|
||||
<UFormGroup
|
||||
<UFormField
|
||||
v-if="(datapoint.showFunction ? datapoint.showFunction(item) : true)"
|
||||
:label="datapoint.label"
|
||||
>
|
||||
@@ -354,7 +370,7 @@ const updateItem = async () => {
|
||||
</template>
|
||||
<InputGroup class="w-full" v-if="datapoint.key.includes('.')">
|
||||
<UInput
|
||||
:color="datapoint.required ? (item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]] ? 'primary' : 'rose') : 'white'"
|
||||
:color="datapoint.required ? (item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]] ? 'primary' : 'error') : 'white'"
|
||||
class="flex-auto"
|
||||
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
|
||||
v-if="['text','number'].includes(datapoint.inputType)"
|
||||
@@ -367,25 +383,25 @@ const updateItem = async () => {
|
||||
<span class="text-gray-500 dark:text-gray-400 text-xs">{{ datapoint.inputTrailing }}</span>
|
||||
</template>
|
||||
</UInput>
|
||||
<UToggle
|
||||
:color="datapoint.required ? (item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]] ? 'primary' : 'rose') : 'primary'"
|
||||
<USwitch
|
||||
:color="datapoint.required ? (item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]] ? 'primary' : 'error') : 'primary'"
|
||||
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
|
||||
v-else-if="datapoint.inputType === 'bool'"
|
||||
v-model="item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]]"
|
||||
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
|
||||
/>
|
||||
<USelectMenu
|
||||
:color="datapoint.required ? (item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]] ? 'primary' : 'rose') : 'white'"
|
||||
:color="datapoint.required ? (item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]] ? 'primary' : 'error') : 'white'"
|
||||
class="flex-auto"
|
||||
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
|
||||
v-else-if="datapoint.inputType === 'select'"
|
||||
v-model="item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]]"
|
||||
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
|
||||
:option-attribute="datapoint.selectOptionAttribute"
|
||||
:value-attribute="datapoint.selectValueAttribute || 'id'"
|
||||
:options="datapoint.selectManualOptions || loadedOptions[datapoint.selectDataType]"
|
||||
:searchable="datapoint.selectSearchAttributes"
|
||||
:search-attributes="datapoint.selectSearchAttributes"
|
||||
:items="getSelectItems(datapoint)"
|
||||
:label-key="getSelectLabelKey(datapoint)"
|
||||
:value-key="getSelectValueKey(datapoint)"
|
||||
:search-input="getSelectSearchInput(datapoint)"
|
||||
:filter-fields="datapoint.selectSearchAttributes"
|
||||
:multiple="datapoint.selectMultiple"
|
||||
>
|
||||
<template #empty>
|
||||
@@ -393,7 +409,7 @@ const updateItem = async () => {
|
||||
</template>
|
||||
</USelectMenu>
|
||||
<UTextarea
|
||||
:color="datapoint.required ? (item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]] ? 'primary' : 'rose') : 'white'"
|
||||
:color="datapoint.required ? (item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]] ? 'primary' : 'error') : 'white'"
|
||||
class="flex-auto"
|
||||
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
|
||||
v-else-if="datapoint.inputType === 'textarea'"
|
||||
@@ -401,9 +417,9 @@ const updateItem = async () => {
|
||||
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
|
||||
rows="4"
|
||||
/>
|
||||
<UPopover :popper="{ placement: 'bottom-start' }" v-else-if="datapoint.inputType === 'date'">
|
||||
<UPopover :content="{ side: 'bottom', align: 'start' }" v-else-if="datapoint.inputType === 'date'">
|
||||
<UButton
|
||||
:color="datapoint.required ? (item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]] ? 'primary' : 'rose') : 'white'"
|
||||
:color="datapoint.required ? (item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]] ? 'primary' : 'error') : 'white'"
|
||||
icon="i-heroicons-calendar-days-20-solid"
|
||||
:label="item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]] ? dayjs(item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]]).format('DD.MM.YYYY') : 'Datum auswählen'"
|
||||
variant="outline"
|
||||
@@ -411,17 +427,17 @@ const updateItem = async () => {
|
||||
|
||||
/>
|
||||
|
||||
<template #panel="{ close }">
|
||||
<template #content>
|
||||
<LazyDatePicker
|
||||
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
|
||||
v-model="item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]]" @close="close"
|
||||
v-model="item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]]"
|
||||
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
|
||||
/>
|
||||
</template>
|
||||
</UPopover>
|
||||
<UPopover :popper="{ placement: 'bottom-start' }" v-else-if="datapoint.inputType === 'datetime'">
|
||||
<UPopover :content="{ side: 'bottom', align: 'start' }" v-else-if="datapoint.inputType === 'datetime'">
|
||||
<UButton
|
||||
:color="datapoint.required ? (item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]] ? 'primary' : 'rose') : 'white'"
|
||||
:color="datapoint.required ? (item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]] ? 'primary' : 'error') : 'white'"
|
||||
icon="i-heroicons-calendar-days-20-solid"
|
||||
:label="item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]] ? dayjs(item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]]).format('DD.MM.YY HH:mm') : 'Datum auswählen'"
|
||||
variant="outline"
|
||||
@@ -429,10 +445,10 @@ const updateItem = async () => {
|
||||
|
||||
/>
|
||||
|
||||
<template #panel="{ close }">
|
||||
<template #content>
|
||||
<LazyDatePicker
|
||||
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
|
||||
v-model="item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]]" @close="close"
|
||||
v-model="item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]]"
|
||||
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
|
||||
mode="datetime"
|
||||
/>
|
||||
@@ -460,7 +476,7 @@ const updateItem = async () => {
|
||||
<InputGroup class="w-full" v-else>
|
||||
<UInput
|
||||
class="flex-auto"
|
||||
:color="datapoint.required ? (item[datapoint.key] ? 'primary' : 'rose') : 'white'"
|
||||
:color="datapoint.required ? (item[datapoint.key] ? 'primary' : 'error') : 'white'"
|
||||
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
|
||||
v-if="['text','number'].includes(datapoint.inputType)"
|
||||
v-model="item[datapoint.key]"
|
||||
@@ -472,34 +488,33 @@ const updateItem = async () => {
|
||||
{{ datapoint.inputTrailing }}
|
||||
</template>
|
||||
</UInput>
|
||||
<UToggle
|
||||
:color="datapoint.required ? (item[datapoint.key] ? 'primary' : 'rose') : 'primary'"
|
||||
<USwitch
|
||||
:color="datapoint.required ? (item[datapoint.key] ? 'primary' : 'error') : 'primary'"
|
||||
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
|
||||
v-else-if="datapoint.inputType === 'bool'"
|
||||
v-model="item[datapoint.key]"
|
||||
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
|
||||
/>
|
||||
<USelectMenu
|
||||
:color="datapoint.required ? (item[datapoint.key] ? 'primary' : 'rose') : 'white'"
|
||||
:color="datapoint.required ? (item[datapoint.key] ? 'primary' : 'error') : 'white'"
|
||||
class="flex-auto"
|
||||
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
|
||||
v-else-if="datapoint.inputType === 'select'"
|
||||
v-model="item[datapoint.key]"
|
||||
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
|
||||
:option-attribute="datapoint.selectOptionAttribute"
|
||||
:value-attribute="datapoint.selectValueAttribute || 'id'"
|
||||
:options="datapoint.selectManualOptions || loadedOptions[datapoint.selectDataType]"
|
||||
:searchable="datapoint.selectSearchAttributes"
|
||||
:search-attributes="datapoint.selectSearchAttributes"
|
||||
:items="getSelectItems(datapoint)"
|
||||
:label-key="getSelectLabelKey(datapoint)"
|
||||
:value-key="getSelectValueKey(datapoint)"
|
||||
:search-input="getSelectSearchInput(datapoint)"
|
||||
:filter-fields="datapoint.selectSearchAttributes"
|
||||
:multiple="datapoint.selectMultiple"
|
||||
searchable-placeholder="Suche..."
|
||||
>
|
||||
<template #empty>
|
||||
Keine Optionen verfügbar
|
||||
</template>
|
||||
</USelectMenu>
|
||||
<UTextarea
|
||||
:color="datapoint.required ? (item[datapoint.key] ? 'primary' : 'rose') : 'white'"
|
||||
:color="datapoint.required ? (item[datapoint.key] ? 'primary' : 'error') : 'white'"
|
||||
class="flex-auto"
|
||||
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
|
||||
v-else-if="datapoint.inputType === 'textarea'"
|
||||
@@ -507,37 +522,36 @@ const updateItem = async () => {
|
||||
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
|
||||
rows="4"
|
||||
/>
|
||||
<UPopover :popper="{ placement: 'bottom-start' }" v-else-if="datapoint.inputType === 'date'">
|
||||
<UPopover :content="{ side: 'bottom', align: 'start' }" v-else-if="datapoint.inputType === 'date'">
|
||||
<UButton
|
||||
:color="datapoint.required ? (item[datapoint.key] ? 'primary' : 'rose') : 'white'"
|
||||
:color="datapoint.required ? (item[datapoint.key] ? 'primary' : 'error') : 'white'"
|
||||
icon="i-heroicons-calendar-days-20-solid"
|
||||
:label="item[datapoint.key] ? dayjs(item[datapoint.key]).format('DD.MM.YYYY') : 'Datum auswählen'"
|
||||
variant="outline"
|
||||
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
|
||||
/>
|
||||
|
||||
<template #panel="{ close }">
|
||||
<template #content>
|
||||
<LazyDatePicker
|
||||
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
|
||||
v-model="item[datapoint.key]" @close="close"
|
||||
v-model="item[datapoint.key]"
|
||||
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
|
||||
/>
|
||||
</template>
|
||||
</UPopover>
|
||||
<UPopover :popper="{ placement: 'bottom-start' }" v-else-if="datapoint.inputType === 'datetime'">
|
||||
<UPopover :content="{ side: 'bottom', align: 'start' }" v-else-if="datapoint.inputType === 'datetime'">
|
||||
<UButton
|
||||
:color="datapoint.required ? (item[datapoint.key] ? 'primary' : 'rose') : 'white'"
|
||||
:color="datapoint.required ? (item[datapoint.key] ? 'primary' : 'error') : 'white'"
|
||||
icon="i-heroicons-calendar-days-20-solid"
|
||||
:label="item[datapoint.key] ? dayjs(item[datapoint.key]).format('DD.MM.YY HH:mm') : 'Datum auswählen'"
|
||||
variant="outline"
|
||||
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
|
||||
/>
|
||||
|
||||
<template #panel="{ close }">
|
||||
<template #content>
|
||||
<LazyDatePicker
|
||||
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
|
||||
v-model="item[datapoint.key]"
|
||||
@close="close"
|
||||
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
|
||||
mode="datetime"
|
||||
/>
|
||||
@@ -572,11 +586,11 @@ const updateItem = async () => {
|
||||
icon="i-heroicons-x-mark"
|
||||
/>
|
||||
</InputGroup>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<UFormGroup
|
||||
<UFormField
|
||||
v-for="datapoint in dataType.templateColumns.filter(i => i.inputType && !i.inputColumn)"
|
||||
:label="datapoint.label"
|
||||
>
|
||||
@@ -589,7 +603,7 @@ const updateItem = async () => {
|
||||
</template>
|
||||
<InputGroup class="w-full" v-if="datapoint.key.includes('.')">
|
||||
<UInput
|
||||
:color="datapoint.required ? (item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]] ? 'primary' : 'rose') : 'white'"
|
||||
:color="datapoint.required ? (item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]] ? 'primary' : 'error') : 'white'"
|
||||
class="flex-auto"
|
||||
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
|
||||
v-if="['text','number'].includes(datapoint.inputType)"
|
||||
@@ -602,25 +616,25 @@ const updateItem = async () => {
|
||||
<span class="text-gray-500 dark:text-gray-400 text-xs">{{ datapoint.inputTrailing }}</span>
|
||||
</template>
|
||||
</UInput>
|
||||
<UToggle
|
||||
:color="datapoint.required ? (item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]] ? 'primary' : 'rose') : 'primary'"
|
||||
<USwitch
|
||||
:color="datapoint.required ? (item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]] ? 'primary' : 'error') : 'primary'"
|
||||
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
|
||||
v-else-if="datapoint.inputType === 'bool'"
|
||||
v-model="item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]]"
|
||||
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
|
||||
/>
|
||||
<USelectMenu
|
||||
:color="datapoint.required ? (item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]] ? 'primary' : 'rose') : 'white'"
|
||||
:color="datapoint.required ? (item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]] ? 'primary' : 'error') : 'white'"
|
||||
class="flex-auto"
|
||||
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
|
||||
v-else-if="datapoint.inputType === 'select'"
|
||||
v-model="item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]]"
|
||||
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
|
||||
:option-attribute="datapoint.selectOptionAttribute"
|
||||
:value-attribute="datapoint.selectValueAttribute || 'id'"
|
||||
:options="datapoint.selectManualOptions || loadedOptions[datapoint.selectDataType]"
|
||||
:searchable="datapoint.selectSearchAttributes"
|
||||
:search-attributes="datapoint.selectSearchAttributes"
|
||||
:items="getSelectItems(datapoint)"
|
||||
:label-key="getSelectLabelKey(datapoint)"
|
||||
:value-key="getSelectValueKey(datapoint)"
|
||||
:search-input="getSelectSearchInput(datapoint)"
|
||||
:filter-fields="datapoint.selectSearchAttributes"
|
||||
:multiple="datapoint.selectMultiple"
|
||||
>
|
||||
<template #empty>
|
||||
@@ -628,7 +642,7 @@ const updateItem = async () => {
|
||||
</template>
|
||||
</USelectMenu>
|
||||
<UTextarea
|
||||
:color="datapoint.required ? (item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]] ? 'primary' : 'rose') : 'white'"
|
||||
:color="datapoint.required ? (item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]] ? 'primary' : 'error') : 'white'"
|
||||
class="flex-auto"
|
||||
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
|
||||
v-else-if="datapoint.inputType === 'textarea'"
|
||||
@@ -636,9 +650,9 @@ const updateItem = async () => {
|
||||
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
|
||||
rows="4"
|
||||
/>
|
||||
<UPopover :popper="{ placement: 'bottom-start' }" v-else-if="datapoint.inputType === 'date'">
|
||||
<UPopover :content="{ side: 'bottom', align: 'start' }" v-else-if="datapoint.inputType === 'date'">
|
||||
<UButton
|
||||
:color="datapoint.required ? (item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]] ? 'primary' : 'rose') : 'white'"
|
||||
:color="datapoint.required ? (item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]] ? 'primary' : 'error') : 'white'"
|
||||
icon="i-heroicons-calendar-days-20-solid"
|
||||
:label="item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]] ? dayjs(item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]]).format('DD.MM.YYYY') : 'Datum auswählen'"
|
||||
variant="outline"
|
||||
@@ -646,17 +660,17 @@ const updateItem = async () => {
|
||||
|
||||
/>
|
||||
|
||||
<template #panel="{ close }">
|
||||
<template #content>
|
||||
<LazyDatePicker
|
||||
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
|
||||
v-model="item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]]" @close="close"
|
||||
v-model="item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]]"
|
||||
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
|
||||
/>
|
||||
</template>
|
||||
</UPopover>
|
||||
<UPopover :popper="{ placement: 'bottom-start' }" v-else-if="datapoint.inputType === 'datetime'">
|
||||
<UPopover :content="{ side: 'bottom', align: 'start' }" v-else-if="datapoint.inputType === 'datetime'">
|
||||
<UButton
|
||||
:color="datapoint.required ? (item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]] ? 'primary' : 'rose') : 'white'"
|
||||
:color="datapoint.required ? (item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]] ? 'primary' : 'error') : 'white'"
|
||||
icon="i-heroicons-calendar-days-20-solid"
|
||||
:label="item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]] ? dayjs(item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]]).format('DD.MM.YY HH:mm') : 'Datum auswählen'"
|
||||
variant="outline"
|
||||
@@ -664,10 +678,10 @@ const updateItem = async () => {
|
||||
|
||||
/>
|
||||
|
||||
<template #panel="{ close }">
|
||||
<template #content>
|
||||
<LazyDatePicker
|
||||
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
|
||||
v-model="item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]]" @close="close"
|
||||
v-model="item[datapoint.key.split('.')[0]][datapoint.key.split('.')[1]]"
|
||||
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
|
||||
mode="datetime"
|
||||
/>
|
||||
@@ -695,7 +709,7 @@ const updateItem = async () => {
|
||||
<InputGroup class="w-full" v-else>
|
||||
<UInput
|
||||
class="flex-auto"
|
||||
:color="datapoint.required ? (item[datapoint.key] ? 'primary' : 'rose') : 'white'"
|
||||
:color="datapoint.required ? (item[datapoint.key] ? 'primary' : 'error') : 'white'"
|
||||
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
|
||||
v-if="['text','number'].includes(datapoint.inputType)"
|
||||
v-model="item[datapoint.key]"
|
||||
@@ -707,34 +721,33 @@ const updateItem = async () => {
|
||||
{{ datapoint.inputTrailing }}
|
||||
</template>
|
||||
</UInput>
|
||||
<UToggle
|
||||
:color="datapoint.required ? (item[datapoint.key] ? 'primary' : 'rose') : 'primary'"
|
||||
<USwitch
|
||||
:color="datapoint.required ? (item[datapoint.key] ? 'primary' : 'error') : 'primary'"
|
||||
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
|
||||
v-else-if="datapoint.inputType === 'bool'"
|
||||
v-model="item[datapoint.key]"
|
||||
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
|
||||
/>
|
||||
<USelectMenu
|
||||
:color="datapoint.required ? (item[datapoint.key] ? 'primary' : 'rose') : 'white'"
|
||||
:color="datapoint.required ? (item[datapoint.key] ? 'primary' : 'error') : 'white'"
|
||||
class="flex-auto"
|
||||
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
|
||||
v-else-if="datapoint.inputType === 'select'"
|
||||
v-model="item[datapoint.key]"
|
||||
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
|
||||
:option-attribute="datapoint.selectOptionAttribute"
|
||||
:value-attribute="datapoint.selectValueAttribute || 'id'"
|
||||
:options="datapoint.selectManualOptions || loadedOptions[datapoint.selectDataType]"
|
||||
:searchable="datapoint.selectSearchAttributes"
|
||||
:search-attributes="datapoint.selectSearchAttributes"
|
||||
:items="getSelectItems(datapoint)"
|
||||
:label-key="getSelectLabelKey(datapoint)"
|
||||
:value-key="getSelectValueKey(datapoint)"
|
||||
:search-input="getSelectSearchInput(datapoint)"
|
||||
:filter-fields="datapoint.selectSearchAttributes"
|
||||
:multiple="datapoint.selectMultiple"
|
||||
searchable-placeholder="Suche..."
|
||||
>
|
||||
<template #empty>
|
||||
Keine Optionen verfügbar
|
||||
</template>
|
||||
</USelectMenu>
|
||||
<UTextarea
|
||||
:color="datapoint.required ? (item[datapoint.key] ? 'primary' : 'rose') : 'white'"
|
||||
:color="datapoint.required ? (item[datapoint.key] ? 'primary' : 'error') : 'white'"
|
||||
class="flex-auto"
|
||||
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
|
||||
v-else-if="datapoint.inputType === 'textarea'"
|
||||
@@ -742,37 +755,36 @@ const updateItem = async () => {
|
||||
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
|
||||
rows="4"
|
||||
/>
|
||||
<UPopover :popper="{ placement: 'bottom-start' }" v-else-if="datapoint.inputType === 'date'">
|
||||
<UPopover :content="{ side: 'bottom', align: 'start' }" v-else-if="datapoint.inputType === 'date'">
|
||||
<UButton
|
||||
:color="datapoint.required ? (item[datapoint.key] ? 'primary' : 'rose') : 'white'"
|
||||
:color="datapoint.required ? (item[datapoint.key] ? 'primary' : 'error') : 'white'"
|
||||
icon="i-heroicons-calendar-days-20-solid"
|
||||
:label="item[datapoint.key] ? dayjs(item[datapoint.key]).format('DD.MM.YYYY') : 'Datum auswählen'"
|
||||
variant="outline"
|
||||
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
|
||||
/>
|
||||
|
||||
<template #panel="{ close }">
|
||||
<template #content>
|
||||
<LazyDatePicker
|
||||
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
|
||||
v-model="item[datapoint.key]" @close="close"
|
||||
v-model="item[datapoint.key]"
|
||||
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
|
||||
/>
|
||||
</template>
|
||||
</UPopover>
|
||||
<UPopover :popper="{ placement: 'bottom-start' }" v-else-if="datapoint.inputType === 'datetime'">
|
||||
<UPopover :content="{ side: 'bottom', align: 'start' }" v-else-if="datapoint.inputType === 'datetime'">
|
||||
<UButton
|
||||
:color="datapoint.required ? (item[datapoint.key] ? 'primary' : 'rose') : 'white'"
|
||||
:color="datapoint.required ? (item[datapoint.key] ? 'primary' : 'error') : 'white'"
|
||||
icon="i-heroicons-calendar-days-20-solid"
|
||||
:label="item[datapoint.key] ? dayjs(item[datapoint.key]).format('DD.MM.YY HH:mm') : 'Datum auswählen'"
|
||||
variant="outline"
|
||||
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
|
||||
/>
|
||||
|
||||
<template #panel="{ close }">
|
||||
<template #content>
|
||||
<LazyDatePicker
|
||||
@change="datapoint.inputChangeFunction ? datapoint.inputChangeFunction(item,loadedOptions) : null"
|
||||
v-model="item[datapoint.key]"
|
||||
@close="close"
|
||||
:disabled="datapoint.disabledFunction ? datapoint.disabledFunction(item) : false"
|
||||
mode="datetime"
|
||||
/>
|
||||
@@ -807,7 +819,7 @@ const updateItem = async () => {
|
||||
icon="i-heroicons-x-mark"
|
||||
/>
|
||||
</InputGroup>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
</UForm>
|
||||
</UDashboardPanelContent>
|
||||
</template>
|
||||
|
||||
@@ -110,12 +110,6 @@ const filteredRows = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FloatingActionButton
|
||||
:label="`+ ${dataType.labelSingle}`"
|
||||
variant="outline"
|
||||
v-if="platform === 'mobile'"
|
||||
@click="router.push(`/standardEntity/${type}/create`)"
|
||||
/>
|
||||
<UDashboardNavbar :title="dataType.label" :badge="filteredRows.length">
|
||||
<template #toggle>
|
||||
<div v-if="platform === 'mobile'"></div>
|
||||
@@ -138,7 +132,7 @@ const filteredRows = computed(() => {
|
||||
<UButton
|
||||
icon="i-heroicons-x-mark"
|
||||
variant="outline"
|
||||
color="rose"
|
||||
color="error"
|
||||
@click="clearSearchString()"
|
||||
v-if="searchString.length > 0"
|
||||
/>
|
||||
@@ -161,15 +155,15 @@ const filteredRows = computed(() => {
|
||||
<USelectMenu
|
||||
v-model="selectedColumns"
|
||||
icon="i-heroicons-adjustments-horizontal-solid"
|
||||
:options="dataType.templateColumns.filter(i => !i.disabledInTable)"
|
||||
:items="dataType.templateColumns.filter(i => !i.disabledInTable)"
|
||||
multiple
|
||||
class="hidden lg:block"
|
||||
by="key"
|
||||
:color="selectedColumns.length !== dataType.templateColumns.filter(i => !i.disabledInTable).length ? 'primary' : 'white'"
|
||||
:ui-menu="{ width: 'min-w-max' }"
|
||||
:content="{ width: 'min-w-max' }"
|
||||
@change="tempStore.modifyColumns(type,selectedColumns)"
|
||||
>
|
||||
<template #label>
|
||||
<template #default>
|
||||
Spalten
|
||||
</template>
|
||||
</USelectMenu>
|
||||
@@ -178,11 +172,11 @@ const filteredRows = computed(() => {
|
||||
icon="i-heroicons-adjustments-horizontal-solid"
|
||||
multiple
|
||||
v-model="selectedFilters"
|
||||
:options="selectableFilters"
|
||||
:items="selectableFilters"
|
||||
:color="selectedFilters.length > 0 ? 'primary' : 'white'"
|
||||
:ui-menu="{ width: 'min-w-max' }"
|
||||
:content="{ width: 'min-w-max' }"
|
||||
>
|
||||
<template #label>
|
||||
<template #default>
|
||||
Filter
|
||||
</template>
|
||||
</USelectMenu>
|
||||
@@ -191,14 +185,14 @@ const filteredRows = computed(() => {
|
||||
<EntityTableMobile
|
||||
v-if="platform === 'mobile'"
|
||||
:type="props.type"
|
||||
:columns="columns"
|
||||
:columns="normalizeTableColumns(columns)"
|
||||
:rows="filteredRows"
|
||||
/>
|
||||
<EntityTable
|
||||
v-else
|
||||
@sort="(i) => emit('sort',i)"
|
||||
:type="props.type"
|
||||
:columns="columns"
|
||||
:columns="normalizeTableColumns(columns)"
|
||||
:rows="filteredRows"
|
||||
:loading="props.loading"
|
||||
/>
|
||||
|
||||
@@ -39,7 +39,9 @@ const modal = useModal()
|
||||
<template>
|
||||
<UButton
|
||||
variant="outline"
|
||||
class="w-25 ml-2"
|
||||
size="sm"
|
||||
square
|
||||
class="ml-2 shrink-0"
|
||||
v-if="props.id && props.buttonShow"
|
||||
icon="i-heroicons-eye"
|
||||
@click="modal.open(StandardEntityModal, {
|
||||
@@ -50,7 +52,9 @@ const modal = useModal()
|
||||
/>
|
||||
<UButton
|
||||
variant="outline"
|
||||
class="w-25 ml-2"
|
||||
size="sm"
|
||||
square
|
||||
class="ml-2 shrink-0"
|
||||
v-if="props.id && props.buttonEdit"
|
||||
icon="i-heroicons-pencil-solid"
|
||||
@click="modal.open(StandardEntityModal, {
|
||||
@@ -64,7 +68,9 @@ const modal = useModal()
|
||||
/>
|
||||
<UButton
|
||||
variant="outline"
|
||||
class="w-25 ml-2"
|
||||
size="sm"
|
||||
square
|
||||
class="ml-2 shrink-0"
|
||||
v-if="!props.id && props.buttonCreate"
|
||||
icon="i-heroicons-plus"
|
||||
@click="modal.open(StandardEntityModal, {
|
||||
@@ -80,4 +86,4 @@ const modal = useModal()
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -28,14 +28,16 @@ defineShortcuts({
|
||||
router.back()
|
||||
},
|
||||
'arrowleft': () => {
|
||||
if(openTab.value > 0){
|
||||
openTab.value -= 1
|
||||
const currentIndex = Number(openTab.value)
|
||||
if(currentIndex > 0){
|
||||
openTab.value = String(currentIndex - 1)
|
||||
router.push(`${router.currentRoute.value.path}?tabIndex=${openTab.value}`)
|
||||
}
|
||||
},
|
||||
'arrowright': () => {
|
||||
if(openTab.value < dataType.showTabs.length - 1) {
|
||||
openTab.value += 1
|
||||
const currentIndex = Number(openTab.value)
|
||||
if(currentIndex < dataType.showTabs.length - 1) {
|
||||
openTab.value = String(currentIndex + 1)
|
||||
router.push(`${router.currentRoute.value.path}?tabIndex=${openTab.value}`)
|
||||
}
|
||||
},
|
||||
@@ -51,7 +53,7 @@ const auth = useAuthStore()
|
||||
|
||||
const dataType = dataStore.dataTypes[type]
|
||||
|
||||
const openTab = ref(route.query.tabIndex || 0)
|
||||
const openTab = ref(String(route.query.tabIndex || 0))
|
||||
|
||||
|
||||
|
||||
@@ -97,7 +99,8 @@ const getAvailableQueryStringData = (keys) => {
|
||||
}
|
||||
|
||||
const onTabChange = (index) => {
|
||||
router.push(`${router.currentRoute.value.path}?tabIndex=${index}`)
|
||||
openTab.value = String(index)
|
||||
router.push(`${router.currentRoute.value.path}?tabIndex=${openTab.value}`)
|
||||
}
|
||||
|
||||
const changePinned = async () => {
|
||||
@@ -255,9 +258,9 @@ const openCustomerInventoryLabelPrint = () => {
|
||||
v-if="props.item.id && platform !== 'mobile'"
|
||||
class="p-5"
|
||||
v-model="openTab"
|
||||
@change="onTabChange"
|
||||
@update:model-value="onTabChange"
|
||||
>
|
||||
<template #item="{item:tab}">
|
||||
<template #content="{item:tab}">
|
||||
<div v-if="tab.label === 'Informationen'" class="flex flex-row">
|
||||
|
||||
<EntityShowSubInformation
|
||||
|
||||
@@ -96,15 +96,15 @@ setup()
|
||||
<USelectMenu
|
||||
v-model="selectedColumns"
|
||||
icon="i-heroicons-adjustments-horizontal-solid"
|
||||
:options="dataType.templateColumns.filter(i => !i.disabledInTable)"
|
||||
:items="dataType.templateColumns.filter(i => !i.disabledInTable)"
|
||||
multiple
|
||||
class="hidden lg:block"
|
||||
by="key"
|
||||
:color="selectedColumns.length !== dataType.templateColumns.filter(i => !i.disabledInTable).length ? 'primary' : 'white'"
|
||||
:ui-menu="{ width: 'min-w-max' }"
|
||||
:content="{ width: 'min-w-max' }"
|
||||
@change="tempStore.modifyColumns(type,selectedColumns)"
|
||||
>
|
||||
<template #label>
|
||||
<template #default>
|
||||
Spalten
|
||||
</template>
|
||||
</USelectMenu>
|
||||
@@ -114,7 +114,7 @@ setup()
|
||||
<div class="scroll" style="height: 70vh">
|
||||
<EntityTable
|
||||
:type="type"
|
||||
:columns="columns"
|
||||
:columns="normalizeTableColumns(columns)"
|
||||
:rows="props.item[type]"
|
||||
style
|
||||
/>
|
||||
|
||||
@@ -181,49 +181,51 @@ const selectItem = (item) => {
|
||||
</UButton>
|
||||
<UModal
|
||||
prevent-close
|
||||
v-model="showFinalInvoiceConfig"
|
||||
v-model:open="showFinalInvoiceConfig"
|
||||
>
|
||||
<UCard>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
||||
Schlussrechnung konfigurieren
|
||||
</h3>
|
||||
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="isOpen = false" />
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<UCard>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
||||
Schlussrechnung konfigurieren
|
||||
</h3>
|
||||
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="showFinalInvoiceConfig = false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<UFormGroup
|
||||
label="Rechnungsvorlage"
|
||||
>
|
||||
<USelectMenu
|
||||
:options="props.item.createddocuments.filter(i => ['confirmationOrders','quotes'].includes(i.type))"
|
||||
value-attribute="id"
|
||||
option-attribute="documentNumber"
|
||||
v-model="referenceDocument"
|
||||
/>
|
||||
</UFormGroup>
|
||||
|
||||
<UFormGroup
|
||||
label="Abschlagsrechnungen"
|
||||
>
|
||||
<USelectMenu
|
||||
:options="props.item.createddocuments.filter(i => ['advanceInvoices'].includes(i.type))"
|
||||
multiple
|
||||
value-attribute="id"
|
||||
option-attribute="documentNumber"
|
||||
v-model="advanceInvoicesToAdd"
|
||||
/>
|
||||
</UFormGroup>
|
||||
|
||||
<template #footer>
|
||||
<UButton
|
||||
@click="invoiceAdvanceInvoices"
|
||||
<UFormField
|
||||
label="Rechnungsvorlage"
|
||||
>
|
||||
Weiter
|
||||
</UButton>
|
||||
</template>
|
||||
</UCard>
|
||||
<USelectMenu
|
||||
:items="props.item.createddocuments.filter(i => ['confirmationOrders','quotes'].includes(i.type))"
|
||||
value-key="id"
|
||||
label-key="documentNumber"
|
||||
v-model="referenceDocument"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<UFormField
|
||||
label="Abschlagsrechnungen"
|
||||
>
|
||||
<USelectMenu
|
||||
:items="props.item.createddocuments.filter(i => ['advanceInvoices'].includes(i.type))"
|
||||
multiple
|
||||
value-key="id"
|
||||
label-key="documentNumber"
|
||||
v-model="advanceInvoicesToAdd"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<template #footer>
|
||||
<UButton
|
||||
@click="invoiceAdvanceInvoices"
|
||||
>
|
||||
Weiter
|
||||
</UButton>
|
||||
</template>
|
||||
</UCard>
|
||||
</template>
|
||||
</UModal>
|
||||
<UButton
|
||||
@click="router.push(`/createDocument/edit/?${getAvailableQueryStringData({type: 'invoices'})}`)"
|
||||
@@ -235,48 +237,53 @@ const selectItem = (item) => {
|
||||
<USelectMenu
|
||||
v-model="selectedColumns"
|
||||
icon="i-heroicons-adjustments-horizontal-solid"
|
||||
:options="templateColumns"
|
||||
:items="templateColumns"
|
||||
multiple
|
||||
class="hidden lg:block"
|
||||
by="key"
|
||||
@change="tempStore.modifyColumns('createddocuments',selectedColumns)"
|
||||
>
|
||||
<template #label>
|
||||
<template #default>
|
||||
Spalten
|
||||
</template>
|
||||
</USelectMenu>
|
||||
</template>
|
||||
</Toolbar>
|
||||
<UTable
|
||||
:rows="props.item.createddocuments.filter(i => !i.archived)"
|
||||
:columns="columns"
|
||||
:data="props.item.createddocuments.filter(i => !i.archived)"
|
||||
:columns="normalizeTableColumns(columns)"
|
||||
class="w-full"
|
||||
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
|
||||
@select="selectItem"
|
||||
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Belege anzuzeigen' }"
|
||||
:on-select="(row) => selectItem(row.original)"
|
||||
style="height: 70vh"
|
||||
>
|
||||
<template #type-data="{row}">
|
||||
{{dataStore.documentTypesForCreation[row.type].labelSingle}}
|
||||
<template #empty>
|
||||
<div class="flex flex-col items-center justify-center py-8 text-center text-sm text-muted">
|
||||
<UIcon name="i-heroicons-circle-stack-20-solid" class="mb-2 size-5" />
|
||||
<span>Keine Belege anzuzeigen</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #state-data="{row}">
|
||||
<template #type-cell="{ row }">
|
||||
{{ dataStore.documentTypesForCreation[row.original.type].labelSingle }}
|
||||
</template>
|
||||
<template #state-cell="{ row }">
|
||||
<span
|
||||
v-if="row.state === 'Entwurf'"
|
||||
class="text-rose-500"
|
||||
v-if="row.original.state === 'Entwurf'"
|
||||
class="text-error-500"
|
||||
>
|
||||
{{row.state}}
|
||||
{{ row.original.state }}
|
||||
</span>
|
||||
<span
|
||||
v-if="row.state === 'Gebucht'"
|
||||
v-if="row.original.state === 'Gebucht'"
|
||||
class="text-cyan-500"
|
||||
>
|
||||
{{row.state}}
|
||||
{{ row.original.state }}
|
||||
</span>
|
||||
<span
|
||||
v-if="row.state === 'Abgeschlossen'"
|
||||
v-if="row.original.state === 'Abgeschlossen'"
|
||||
class="text-primary-500"
|
||||
>
|
||||
{{row.state}}
|
||||
{{ row.original.state }}
|
||||
</span>
|
||||
</template>
|
||||
<!-- <template #paid-data="{row}">
|
||||
@@ -285,19 +292,19 @@ const selectItem = (item) => {
|
||||
<span v-else class="text-rose-600">Offen</span>
|
||||
</div>
|
||||
</template>-->
|
||||
<template #reference-data="{row}">
|
||||
<span v-if="row === props.item.createddocuments[selectedItem]" class="text-primary-500 font-bold">{{row.documentNumber}}</span>
|
||||
<span v-else>{{row.documentNumber}}</span>
|
||||
<template #reference-cell="{ row }">
|
||||
<span v-if="row.original === props.item.createddocuments[selectedItem]" class="text-primary-500 font-bold">{{ row.original.documentNumber }}</span>
|
||||
<span v-else>{{ row.original.documentNumber }}</span>
|
||||
</template>
|
||||
<template #date-data="{row}">
|
||||
<span v-if="row.date">{{row.date ? dayjs(row.date).format("DD.MM.YY") : ''}}</span>
|
||||
<span v-if="row.documentDate">{{row.documentDate ? dayjs(row.documentDate).format("DD.MM.YY") : ''}}</span>
|
||||
<template #date-cell="{ row }">
|
||||
<span v-if="row.original.date">{{ row.original.date ? dayjs(row.original.date).format("DD.MM.YY") : '' }}</span>
|
||||
<span v-if="row.original.documentDate">{{ row.original.documentDate ? dayjs(row.original.documentDate).format("DD.MM.YY") : '' }}</span>
|
||||
</template>
|
||||
<template #dueDate-data="{row}">
|
||||
<span v-if="row.paymentDays && ['invoices','advanceInvoices'].includes(row.type)" >{{row.documentDate ? dayjs(row.documentDate).add(row.paymentDays,'day').format("DD.MM.YY") : ''}}</span>
|
||||
<template #dueDate-cell="{ row }">
|
||||
<span v-if="row.original.paymentDays && ['invoices','advanceInvoices'].includes(row.original.type)">{{ row.original.documentDate ? dayjs(row.original.documentDate).add(row.original.paymentDays,'day').format("DD.MM.YY") : '' }}</span>
|
||||
</template>
|
||||
<template #amount-data="{row}">
|
||||
<span v-if="row.type !== 'deliveryNotes'">{{useCurrency(useSum().getCreatedDocumentSum(row, createddocuments))}}</span>
|
||||
<template #amount-cell="{ row }">
|
||||
<span v-if="row.original.type !== 'deliveryNotes'">{{ useCurrency(useSum().getCreatedDocumentSum(row.original, createddocuments)) }}</span>
|
||||
</template>
|
||||
</UTable>
|
||||
|
||||
|
||||
@@ -94,41 +94,43 @@ function isImage(file) {
|
||||
</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"
|
||||
/>
|
||||
<UModal v-model:open="showViewer" side="bottom" class="h-[100dvh]" fullscreen>
|
||||
<template #content>
|
||||
<!-- 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>
|
||||
|
||||
<!-- 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"
|
||||
<!-- 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>
|
||||
|
||||
<UAlert
|
||||
v-else
|
||||
title="Nicht unterstützter Dateityp"
|
||||
icon="i-heroicons-exclamation-triangle"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</UModal>
|
||||
</template>
|
||||
|
||||
@@ -65,7 +65,7 @@ const renderDatapointValue = (datapoint) => {
|
||||
</template>
|
||||
<UAlert
|
||||
v-if="props.item.archived"
|
||||
color="rose"
|
||||
color="error"
|
||||
variant="outline"
|
||||
:title="`${dataType.labelSingle} archiviert`"
|
||||
icon="i-heroicons-archive-box"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup>
|
||||
import dayjs from "dayjs";
|
||||
const router = useRouter()
|
||||
import dayjs from "dayjs"
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const props = defineProps({
|
||||
queryStringData: {
|
||||
@@ -21,83 +21,260 @@ const props = defineProps({
|
||||
}
|
||||
})
|
||||
|
||||
const statementallocations = ref([])
|
||||
const incominginvoices = ref([])
|
||||
const loading = ref(true)
|
||||
const incomingInvoices = ref([])
|
||||
const statementAllocations = ref([])
|
||||
const selectedYear = ref(String(dayjs().year()))
|
||||
const selectedMonth = ref("all")
|
||||
|
||||
const currency = (value) => `${Number(value || 0).toFixed(2).replace(".", ",")} EUR`
|
||||
const currentAccountId = computed(() => String(props.item?.id ?? ""))
|
||||
const sameAccount = (value) => String(value ?? "") === currentAccountId.value
|
||||
const getStatementId = (allocation) => allocation?.bankstatement?.id || allocation?.bs_id?.id || allocation?.bs_id || null
|
||||
const getStatementLike = (allocation) => allocation?.bankstatement || (typeof allocation?.bs_id === "object" ? allocation.bs_id : null)
|
||||
const getAllocationDate = (allocation) => {
|
||||
const statement = getStatementLike(allocation)
|
||||
|
||||
return statement?.date || statement?.valueDate || allocation?.bs_id?.date || allocation?.bs_id?.valueDate || null
|
||||
}
|
||||
const getAllocationPartner = (allocation) => {
|
||||
const statement = getStatementLike(allocation)
|
||||
|
||||
return statement?.debName || statement?.credName || statement?.partner || allocation?.bs_id?.debName || allocation?.bs_id?.credName || ""
|
||||
}
|
||||
const getAllocationDescription = (allocation) => {
|
||||
const statement = getStatementLike(allocation)
|
||||
|
||||
return allocation?.description || statement?.purpose || statement?.text || statement?.description || allocation?.bs_id?.purpose || allocation?.bs_id?.text || ""
|
||||
}
|
||||
const hasContent = (value) => value !== null && value !== undefined && String(value).trim() !== ""
|
||||
|
||||
const monthItems = [
|
||||
{ label: "Ganzes Jahr", value: "all" },
|
||||
{ label: "Januar", value: "1" },
|
||||
{ label: "Februar", value: "2" },
|
||||
{ label: "Maerz", value: "3" },
|
||||
{ label: "April", value: "4" },
|
||||
{ label: "Mai", value: "5" },
|
||||
{ label: "Juni", value: "6" },
|
||||
{ label: "Juli", value: "7" },
|
||||
{ label: "August", value: "8" },
|
||||
{ label: "September", value: "9" },
|
||||
{ label: "Oktober", value: "10" },
|
||||
{ label: "November", value: "11" },
|
||||
{ label: "Dezember", value: "12" }
|
||||
]
|
||||
|
||||
const allAllocations = computed(() => {
|
||||
const statementRows = statementAllocations.value.map((allocation) => ({
|
||||
...allocation,
|
||||
type: "statementallocation",
|
||||
bankstatement: allocation.bankstatement || getStatementLike(allocation),
|
||||
date: getAllocationDate(allocation),
|
||||
partner: getAllocationPartner(allocation),
|
||||
description: getAllocationDescription(allocation),
|
||||
amount: Number(allocation.amount || 0)
|
||||
}))
|
||||
|
||||
const incomingInvoiceRows = incomingInvoices.value.flatMap((invoice) => {
|
||||
return (invoice.accounts || [])
|
||||
.filter((account) => sameAccount(account.account?.id || account.account))
|
||||
.map((account, index) => ({
|
||||
id: `${invoice.id}-${index}`,
|
||||
incominginvoiceid: invoice.id,
|
||||
type: "incominginvoice",
|
||||
amount: Number(account.amountGross || account.amountNet || 0),
|
||||
date: invoice.date,
|
||||
partner: invoice.vendor?.name || "",
|
||||
description: account.description || invoice.description || "",
|
||||
color: invoice.expense ? "red" : "green",
|
||||
expense: invoice.expense,
|
||||
reference: invoice.reference || "-"
|
||||
}))
|
||||
})
|
||||
|
||||
return [...statementRows, ...incomingInvoiceRows]
|
||||
})
|
||||
|
||||
const yearItems = computed(() => {
|
||||
const years = [...new Set(
|
||||
allAllocations.value
|
||||
.map((allocation) => allocation.bankstatement?.date || allocation.date)
|
||||
.filter(Boolean)
|
||||
.map((date) => String(dayjs(date).year()))
|
||||
)].sort((a, b) => Number(b) - Number(a))
|
||||
|
||||
return years.length > 0
|
||||
? years.map((year) => ({ label: year, value: year }))
|
||||
: [{ label: String(dayjs().year()), value: String(dayjs().year()) }]
|
||||
})
|
||||
|
||||
const renderedAllocations = computed(() => {
|
||||
return allAllocations.value.filter((allocation) => {
|
||||
const allocationDateValue = allocation.bankstatement?.date || allocation.date
|
||||
const allocationDate = allocationDateValue ? dayjs(allocationDateValue) : null
|
||||
|
||||
if (allocationDate && allocationDate.year().toString() !== selectedYear.value) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (allocationDate && selectedMonth.value !== "all" && allocationDate.month() + 1 !== Number(selectedMonth.value)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
})
|
||||
|
||||
const totals = computed(() => {
|
||||
return renderedAllocations.value.reduce((acc, allocation) => {
|
||||
const amount = Number(allocation.amount || 0)
|
||||
|
||||
if (allocation.incominginvoiceid) {
|
||||
if (allocation.expense) {
|
||||
acc.expenses += amount
|
||||
acc.balance -= amount
|
||||
} else {
|
||||
acc.income += amount
|
||||
acc.balance += amount
|
||||
}
|
||||
} else {
|
||||
if (amount < 0) {
|
||||
acc.expenses += Math.abs(amount)
|
||||
} else {
|
||||
acc.income += amount
|
||||
}
|
||||
acc.balance += amount
|
||||
}
|
||||
|
||||
return acc
|
||||
}, { income: 0, expenses: 0, balance: 0 })
|
||||
})
|
||||
|
||||
const columns = [
|
||||
{ accessorKey: "amount", header: "Betrag" },
|
||||
{ accessorKey: "date", header: "Datum" },
|
||||
{ accessorKey: "partner", header: "Partner" },
|
||||
{ accessorKey: "description", header: "Beschreibung" }
|
||||
]
|
||||
|
||||
const setup = async () => {
|
||||
loading.value = true
|
||||
|
||||
statementAllocations.value = (await useEntities("statementallocations").select("*, bankstatement(*), createddocument(*), incominginvoice(*)"))
|
||||
.filter((allocation) => sameAccount(allocation.account?.id || allocation.account) || sameAccount(allocation.ownaccount?.id || allocation.ownaccount))
|
||||
|
||||
incomingInvoices.value = (await useEntities("incominginvoices").select("*, vendor(*)"))
|
||||
.filter((invoice) => (invoice.accounts || []).some((account) => sameAccount(account.account?.id || account.account)))
|
||||
|
||||
const firstYear = yearItems.value[0]?.value
|
||||
if (firstYear && !yearItems.value.some((item) => item.value === selectedYear.value)) {
|
||||
selectedYear.value = firstYear
|
||||
}
|
||||
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
setup()
|
||||
|
||||
const selectAllocation = (allocation) => {
|
||||
if(allocation.type === "statementallocation") {
|
||||
router.push(`/banking/statements/edit/${allocation.bs_id.id}`)
|
||||
} else if(allocation.type === "incominginvoice") {
|
||||
router.push(`/incominginvoices/show/${allocation.incominginvoiceid}`)
|
||||
const unwrapAllocationRow = (allocationLike) => allocationLike?.original || allocationLike
|
||||
|
||||
const selectAllocation = (allocationLike) => {
|
||||
const allocation = unwrapAllocationRow(allocationLike)
|
||||
|
||||
if (!allocation) {
|
||||
return
|
||||
}
|
||||
|
||||
const statementId = getStatementId(allocation)
|
||||
|
||||
if (allocation.type === "statementallocation" && statementId) {
|
||||
router.push(`/banking/statements/edit/${statementId}`)
|
||||
} else if (allocation.type === "incominginvoice" && allocation.incominginvoiceid) {
|
||||
router.push(`/incomingInvoices/show/${allocation.incominginvoiceid}`)
|
||||
}
|
||||
}
|
||||
|
||||
const renderedAllocations = computed(() => {
|
||||
|
||||
let tempstatementallocations = props.item.statementallocations.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 : '')) : ''
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
/*let incominginvoicesallocations = []
|
||||
|
||||
incominginvoices.value.forEach(i => {
|
||||
|
||||
incominginvoicesallocations.push(...i.accounts.filter(x => x.account == route.params.id).map(x => {
|
||||
return {
|
||||
...x,
|
||||
incominginvoiceid: i.id,
|
||||
type: "incominginvoice",
|
||||
amount: x.amountGross ? x.amountGross : x.amountNet,
|
||||
date: i.date,
|
||||
partner: i.vendor.name,
|
||||
description: i.description,
|
||||
color: i.expense ? "red" : "green"
|
||||
}
|
||||
}))
|
||||
})*/
|
||||
|
||||
return [...tempstatementallocations/*, ... incominginvoicesallocations*/]
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UCard class="mt-5">
|
||||
<UTable
|
||||
v-if="props.item.statementallocations"
|
||||
:rows="renderedAllocations"
|
||||
:columns="[{key:'amount', label:'Betrag'},{key:'date', label:'Datum'},{key:'partner', label:'Partner'},{key:'description', label:'Beschreibung'}]"
|
||||
@select="(i) => selectAllocation(i)"
|
||||
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Buchungen anzuzeigen' }"
|
||||
>
|
||||
<template #amount-data="{row}">
|
||||
<span class="text-right text-rose-600" v-if="row.amount < 0 || row.color === 'red'">{{useCurrency(row.amount)}}</span>
|
||||
<span class="text-right text-primary-500" v-else-if="row.amount > 0 || row.color === 'green'">{{useCurrency(row.amount)}}</span>
|
||||
<span v-else>{{useCurrency(row.amount)}}</span>
|
||||
</template>
|
||||
<template #date-data="{row}">
|
||||
{{row.date ? dayjs(row.date).format('DD.MM.YYYY') : ''}}
|
||||
</template>
|
||||
<template #description-data="{row}">
|
||||
{{row.description ? row.description : ''}}
|
||||
</template>
|
||||
</UTable>
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-col gap-3 md:flex-row md:items-end">
|
||||
<UFormField label="Jahr" class="w-full md:w-48">
|
||||
<USelectMenu
|
||||
v-model="selectedYear"
|
||||
:items="yearItems"
|
||||
value-key="value"
|
||||
label-key="label"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<UFormField label="Monat" class="w-full md:w-56">
|
||||
<USelectMenu
|
||||
v-model="selectedMonth"
|
||||
:items="monthItems"
|
||||
value-key="value"
|
||||
label-key="label"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 md:grid-cols-3">
|
||||
<UCard>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">Einnahmen</div>
|
||||
<div class="mt-1 text-xl font-semibold">{{ currency(totals.income) }}</div>
|
||||
</UCard>
|
||||
|
||||
<UCard>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">Ausgaben</div>
|
||||
<div class="mt-1 text-xl font-semibold">{{ currency(totals.expenses) }}</div>
|
||||
</UCard>
|
||||
|
||||
<UCard>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">Saldo</div>
|
||||
<div class="mt-1 text-xl font-semibold">{{ currency(totals.balance) }}</div>
|
||||
</UCard>
|
||||
</div>
|
||||
|
||||
<div v-if="!loading" class="overflow-auto max-h-[60vh]">
|
||||
<UTable
|
||||
:data="renderedAllocations"
|
||||
:columns="normalizeTableColumns(columns)"
|
||||
:on-select="selectAllocation"
|
||||
class="w-full"
|
||||
>
|
||||
<template #empty>
|
||||
<div class="flex flex-col items-center justify-center py-10 text-center">
|
||||
<UIcon name="i-heroicons-circle-stack-20-solid" class="mb-2 h-10 w-10 text-gray-400" />
|
||||
<p class="font-medium">Keine Buchungen im ausgewaehlten Zeitraum</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #amount-cell="{ row }">
|
||||
<span class="text-right text-error" v-if="row.original.amount < 0 || row.original.color === 'red'">{{ useCurrency(row.original.amount) }}</span>
|
||||
<span class="text-right text-primary-500" v-else-if="row.original.amount > 0 || row.original.color === 'green'">{{ useCurrency(row.original.amount) }}</span>
|
||||
<span v-else>{{ useCurrency(row.original.amount) }}</span>
|
||||
</template>
|
||||
|
||||
<template #date-cell="{ row }">
|
||||
{{ row.original.bankstatement.date && dayjs(row.original.bankstatement.date).isValid() ? dayjs(row.original.bankstatement.date).format('DD.MM.YYYY') : '-' }}
|
||||
</template>
|
||||
|
||||
<template #partner-cell="{ row }">
|
||||
<div class="truncate">{{ hasContent(row.original.partner) ? row.original.partner : '-' }}</div>
|
||||
</template>
|
||||
|
||||
<template #description-cell="{ row }">
|
||||
<UTooltip :text="hasContent(row.original.description) ? row.original.description : '-'">
|
||||
<div class="max-w-[22rem] truncate">{{ hasContent(row.original.description) ? row.original.description : '-' }}</div>
|
||||
</UTooltip>
|
||||
</template>
|
||||
</UTable>
|
||||
</div>
|
||||
|
||||
<UProgress v-else animation="carousel" class="w-3/4 mx-auto" />
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
||||
@@ -95,26 +95,26 @@ const changeActivePhase = async (key) => {
|
||||
<UAccordion
|
||||
:items="renderedPhases"
|
||||
>
|
||||
<template #default="{item,index,open}">
|
||||
<template #default="slotProps">
|
||||
<UButton
|
||||
variant="ghost"
|
||||
:color="item.active ? 'primary' : 'white'"
|
||||
:color="slotProps.item.active ? 'primary' : 'white'"
|
||||
class="mb-1"
|
||||
:disabled="true"
|
||||
>
|
||||
<template #leading>
|
||||
<div class="w-6 h-6 flex items-center justify-center -my-1">
|
||||
<UIcon :name="item.icon" class="w-4 h-4 " />
|
||||
<UIcon :name="slotProps.item.icon" class="w-4 h-4 " />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<span class="truncate"> {{item.label}}</span>
|
||||
<span class="truncate"> {{ slotProps.item.label }}</span>
|
||||
|
||||
<template #trailing>
|
||||
<UIcon
|
||||
name="i-heroicons-chevron-right-20-solid"
|
||||
class="w-5 h-5 ms-auto transform transition-transform duration-200"
|
||||
:class="[open && 'rotate-90']"
|
||||
:class="[slotProps?.open && 'rotate-90']"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -67,40 +67,45 @@ const columns = [
|
||||
<UCard class="mt-5">
|
||||
<UTable
|
||||
class="mt-3"
|
||||
:columns="columns"
|
||||
:rows="props.item.times"
|
||||
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Noch keine Einträge' }"
|
||||
:columns="normalizeTableColumns(columns)"
|
||||
:data="props.item.times"
|
||||
>
|
||||
<template #state-data="{row}">
|
||||
<template #empty>
|
||||
<div class="flex flex-col items-center justify-center py-8 text-center text-sm text-muted">
|
||||
<UIcon name="i-heroicons-circle-stack-20-solid" class="mb-2 size-5" />
|
||||
<span>Noch keine Einträge</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #state-cell="{ row }">
|
||||
<span
|
||||
v-if="row.state === 'Entwurf'"
|
||||
class="text-rose-500"
|
||||
>{{row.state}}</span>
|
||||
v-if="row.original.state === 'Entwurf'"
|
||||
class="text-error-500"
|
||||
>{{ row.original.state }}</span>
|
||||
<span
|
||||
v-if="row.state === 'Eingereicht'"
|
||||
v-if="row.original.state === 'Eingereicht'"
|
||||
class="text-cyan-500"
|
||||
>{{row.state}}</span>
|
||||
>{{ row.original.state }}</span>
|
||||
<span
|
||||
v-if="row.state === 'Bestätigt'"
|
||||
v-if="row.original.state === 'Bestätigt'"
|
||||
class="text-primary-500"
|
||||
>{{row.state}}</span>
|
||||
>{{ row.original.state }}</span>
|
||||
</template>
|
||||
<template #user-data="{row}">
|
||||
{{row.profile ? row.profile.fullName : "" }}
|
||||
<template #user-cell="{ row }">
|
||||
{{ row.original.profile ? row.original.profile.fullName : "" }}
|
||||
</template>
|
||||
|
||||
<template #startDate-data="{row}">
|
||||
{{dayjs(row.startDate).format("DD.MM.YY HH:mm")}}
|
||||
<template #startDate-cell="{ row }">
|
||||
{{ dayjs(row.original.startDate).format("DD.MM.YY HH:mm") }}
|
||||
</template>
|
||||
<template #endDate-data="{row}">
|
||||
{{dayjs(row.endDate).format("DD.MM.YY HH:mm")}}
|
||||
<template #endDate-cell="{ row }">
|
||||
{{ dayjs(row.original.endDate).format("DD.MM.YY HH:mm") }}
|
||||
</template>
|
||||
<template #duration-data="{row}">
|
||||
{{Math.floor(dayjs(row.endDate).diff(row.startDate, "minutes")/60)}}:{{String(dayjs(row.endDate).diff(row.startDate, "minutes") % 60).padStart(2,"0")}} h
|
||||
<template #duration-cell="{ row }">
|
||||
{{ Math.floor(dayjs(row.original.endDate).diff(row.original.startDate, "minutes") / 60) }}:{{ String(dayjs(row.original.endDate).diff(row.original.startDate, "minutes") % 60).padStart(2,"0") }} h
|
||||
|
||||
</template>
|
||||
<template #project-data="{row}">
|
||||
{{row.project ? row.project.name : "" }}
|
||||
<template #project-cell="{ row }">
|
||||
{{ row.original.project ? row.original.project.name : "" }}
|
||||
</template>
|
||||
</UTable>
|
||||
</UCard>
|
||||
|
||||
@@ -58,76 +58,101 @@
|
||||
const dataType = dataStore.dataTypes[props.type]
|
||||
|
||||
const selectedItem = ref(0)
|
||||
const sort = ref({
|
||||
column: dataType.sortColumn || "date",
|
||||
direction: 'desc'
|
||||
})
|
||||
const sorting = ref([{
|
||||
id: dataType.sortColumn || "date",
|
||||
desc: true
|
||||
}])
|
||||
const normalizedColumns = computed(() => normalizeTableColumns(props.columns))
|
||||
const truncateValue = (value, maxLength) => {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return '\u00A0'
|
||||
}
|
||||
|
||||
const stringValue = String(value)
|
||||
if (!maxLength || stringValue.length <= maxLength) {
|
||||
return stringValue
|
||||
}
|
||||
|
||||
return `${stringValue.substring(0, maxLength)}...`
|
||||
}
|
||||
const handleSortChange = (value) => {
|
||||
const nextSort = Array.isArray(value) ? value[0] : undefined
|
||||
|
||||
if (!nextSort?.id) {
|
||||
return
|
||||
}
|
||||
|
||||
emit('sort', {
|
||||
sort_column: nextSort.id,
|
||||
sort_direction: nextSort.desc ? 'desc' : 'asc'
|
||||
})
|
||||
}
|
||||
const handleSelect = (row) => {
|
||||
router.push(getShowRoute(props.type, row.original.id))
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UTable
|
||||
:loading="props.loading"
|
||||
:loading-state="{ icon: 'i-heroicons-arrow-path-20-solid', label: 'Loading...' }"
|
||||
sort-mode="manual"
|
||||
v-model:sort="sort"
|
||||
@update:sort="emit('sort',{sort_column: sort.column, sort_direction: sort.direction})"
|
||||
v-model:sorting="sorting"
|
||||
@update:sorting="handleSortChange"
|
||||
v-if="dataType && columns"
|
||||
:rows="props.rows"
|
||||
:columns="props.columns"
|
||||
:data="props.rows"
|
||||
:columns="normalizedColumns"
|
||||
class="w-full"
|
||||
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
|
||||
@select="(i) => router.push(getShowRoute(type, i.id))"
|
||||
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: `Keine ${dataType.label} anzuzeigen` }"
|
||||
:on-select="handleSelect"
|
||||
:empty="`Keine ${dataType.label} anzuzeigen`"
|
||||
>
|
||||
<!-- <template
|
||||
v-for="column in dataType.templateColumns.filter(i => !i.disabledInTable)"
|
||||
v-slot:[`${column.key}-header`]="{row}">
|
||||
<span class="text-nowrap">{{column.label}}</span>
|
||||
</template>-->
|
||||
<template #name-data="{row}">
|
||||
<template #name-cell="{ row }">
|
||||
<span
|
||||
v-if="row.id === props.rows[selectedItem].id"
|
||||
class="text-primary-500 font-bold">
|
||||
<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}}
|
||||
v-if="row.original.id === props.rows[selectedItem]?.id"
|
||||
class="block truncate text-primary-500 font-bold"
|
||||
>
|
||||
<UTooltip :text="row.original.name">
|
||||
<span class="block truncate">
|
||||
{{ truncateValue(row.original.name, dataType.templateColumns.find(i => i.key === "name")?.maxLength) }}
|
||||
</span>
|
||||
</UTooltip> </span>
|
||||
<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}}
|
||||
<span v-else class="block truncate">
|
||||
<UTooltip :text="row.original.name">
|
||||
<span class="block truncate">
|
||||
{{ truncateValue(row.original.name, dataType.templateColumns.find(i => i.key === "name")?.maxLength) }}
|
||||
</span>
|
||||
</UTooltip>
|
||||
</span>
|
||||
</template>
|
||||
<template #fullName-data="{row}">
|
||||
<template #fullName-cell="{ row }">
|
||||
<span
|
||||
v-if="row.id === props.rows[selectedItem].id"
|
||||
class="text-primary-500 font-bold">{{row.fullName}}
|
||||
v-if="row.original.id === props.rows[selectedItem]?.id"
|
||||
class="block truncate text-primary-500 font-bold">{{ row.original.fullName }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{row.fullName}}
|
||||
<span v-else class="block truncate">
|
||||
{{ row.original.fullName }}
|
||||
</span>
|
||||
</template>
|
||||
<template #licensePlate-data="{row}">
|
||||
<template #licensePlate-cell="{ row }">
|
||||
<span
|
||||
v-if="row.id === props.rows[selectedItem].id"
|
||||
class="text-primary-500 font-bold">{{row.licensePlate}}
|
||||
v-if="row.original.id === props.rows[selectedItem]?.id"
|
||||
class="block truncate text-primary-500 font-bold">{{ row.original.licensePlate }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{row.licensePlate}}
|
||||
<span v-else class="block truncate">
|
||||
{{ row.original.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]">
|
||||
<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>
|
||||
v-slot:[`${column.key}-cell`]="{ row }">
|
||||
<component v-if="column.component" :is="column.component" :row="row.original"></component>
|
||||
<span v-else-if="row.original[column.key]" class="block truncate">
|
||||
<UTooltip :text="String(row.original[column.key])">
|
||||
<span class="block truncate">
|
||||
{{ `${truncateValue(row.original[column.key], column.maxLength)}${column.unit ? ` ${column.unit}` : ''}` }}
|
||||
</span>
|
||||
</UTooltip>
|
||||
</span>
|
||||
|
||||
</template>
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
|
||||
<!-- <UTable
|
||||
v-if="dataType && columns"
|
||||
:rows="props.rows"
|
||||
:data="props.rows"
|
||||
:columns="props.columns"
|
||||
class="w-full"
|
||||
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
|
||||
|
||||
@@ -55,17 +55,19 @@ setup()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UModal v-model="showMessageModal" prevent-close>
|
||||
<UCard>
|
||||
<template #header>
|
||||
<span class="font-bold">{{messageToShow.title}}</span>
|
||||
</template>
|
||||
<p class=" my-2" v-html="messageToShow.description"></p>
|
||||
<UButton
|
||||
variant="outline"
|
||||
@click="markMessageAsRead"
|
||||
>Gelesen</UButton>
|
||||
</UCard>
|
||||
<UModal v-model:open="showMessageModal" prevent-close>
|
||||
<template #content>
|
||||
<UCard>
|
||||
<template #header>
|
||||
<span class="font-bold">{{messageToShow.title}}</span>
|
||||
</template>
|
||||
<p class=" my-2" v-html="messageToShow.description"></p>
|
||||
<UButton
|
||||
variant="outline"
|
||||
@click="markMessageAsRead"
|
||||
>Gelesen</UButton>
|
||||
</UCard>
|
||||
</template>
|
||||
</UModal>
|
||||
|
||||
<!-- <UCard
|
||||
@@ -79,7 +81,7 @@ setup()
|
||||
variant="ghost"
|
||||
@click="showMessage(globalMessages[0])"
|
||||
/>
|
||||
<UModal v-model="showMessageModal">
|
||||
<UModal v-model:open="showMessageModal">
|
||||
<UCard>
|
||||
<template #header>
|
||||
<span class="font-bold">{{messageToShow.title}}</span>
|
||||
|
||||
@@ -123,18 +123,20 @@ function onSelect (option) {
|
||||
/>
|
||||
|
||||
<UModal
|
||||
v-model="showCommandPalette"
|
||||
v-model:open="showCommandPalette"
|
||||
>
|
||||
<UCommandPalette
|
||||
v-model="selectedCommand"
|
||||
:groups="groups"
|
||||
:autoselect="false"
|
||||
@update:model-value="onSelect"
|
||||
ref="commandPaletteRef"
|
||||
/>
|
||||
<template #content>
|
||||
<UCommandPalette
|
||||
v-model="selectedCommand"
|
||||
:groups="groups"
|
||||
:autoselect="false"
|
||||
@update:model-value="onSelect"
|
||||
ref="commandPaletteRef"
|
||||
/>
|
||||
</template>
|
||||
</UModal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -2,9 +2,16 @@
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const { isHelpSlideoverOpen } = useDashboard()
|
||||
const { metaSymbol } = useShortcuts()
|
||||
const { entries, pending, error, seenState, refresh, markAsSeen } = useChangelog()
|
||||
|
||||
const metaSymbol = computed(() => {
|
||||
if (import.meta.server) {
|
||||
return 'Ctrl'
|
||||
}
|
||||
|
||||
return /Mac|iPhone|iPad|iPod/i.test(navigator.platform) ? '⌘' : 'Ctrl'
|
||||
})
|
||||
|
||||
const shortcuts = ref(false)
|
||||
const query = ref('')
|
||||
const toast = useToast()
|
||||
@@ -125,7 +132,7 @@ const addContactRequest = async () => {
|
||||
toast.add({title: "Anfrage erfolgreich erstellt"})
|
||||
resetContactRequest()
|
||||
} else {
|
||||
toast.add({title: "Anfrage konnte nicht erstellt werden",color:"rose"})
|
||||
toast.add({title: "Anfrage konnte nicht erstellt werden",color:"error"})
|
||||
}
|
||||
loadingContactRequest.value = false
|
||||
}
|
||||
@@ -154,7 +161,7 @@ watch(isHelpSlideoverOpen, async (isOpen) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UDashboardSlideover v-model="isHelpSlideoverOpen">
|
||||
<USlideover v-model:open="isHelpSlideoverOpen" side="right">
|
||||
<template #title>
|
||||
<UButton
|
||||
v-if="shortcuts"
|
||||
@@ -168,93 +175,94 @@ watch(isHelpSlideoverOpen, async (isOpen) => {
|
||||
{{ shortcuts ? 'Shortcuts' : 'Hilfe & Information' }}
|
||||
</template>
|
||||
|
||||
<div v-if="shortcuts" class="space-y-6">
|
||||
<UInput v-model="query" icon="i-heroicons-magnifying-glass" placeholder="Search..." autofocus color="gray" />
|
||||
<template #body>
|
||||
<div v-if="shortcuts" class="space-y-6">
|
||||
<UInput v-model="query" icon="i-heroicons-magnifying-glass" placeholder="Search..." autofocus color="gray" />
|
||||
|
||||
<div v-for="(category, index) in filteredCategories" :key="index">
|
||||
<p class="mb-3 text-sm text-gray-900 dark:text-white font-semibold">
|
||||
{{ category.title }}
|
||||
</p>
|
||||
<div v-for="(category, index) in filteredCategories" :key="index">
|
||||
<p class="mb-3 text-sm text-gray-900 dark:text-white font-semibold">
|
||||
{{ category.title }}
|
||||
</p>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div v-for="(item, i) in category.items" :key="i" class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400">{{ item.name }}</span>
|
||||
<div class="space-y-2">
|
||||
<div v-for="(item, i) in category.items" :key="i" class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400">{{ item.name }}</span>
|
||||
|
||||
<div class="flex items-center justify-end flex-shrink-0 gap-0.5">
|
||||
<UKbd v-for="(shortcut, j) in item.shortcuts" :key="j">
|
||||
{{ shortcut }}
|
||||
</UKbd>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="flex flex-col gap-y-6">
|
||||
<div class="flex flex-col gap-y-3">
|
||||
<UButton v-for="(link, index) in links" :key="index" color="white" v-bind="link" />
|
||||
</div>
|
||||
|
||||
<UCard>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p class="text-base font-semibold text-gray-900 dark:text-white">
|
||||
Changelog
|
||||
</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Zuletzt geöffnet: {{ lastOpenedLabel }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<UButton
|
||||
icon="i-heroicons-arrow-path"
|
||||
color="gray"
|
||||
variant="ghost"
|
||||
:loading="pending"
|
||||
@click="refresh(true)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<UAlert
|
||||
v-if="error"
|
||||
class="mt-4"
|
||||
color="red"
|
||||
variant="soft"
|
||||
title="Changelog konnte nicht geladen werden"
|
||||
:description="error"
|
||||
/>
|
||||
|
||||
<div v-else-if="pending && !changelogEntries.length" class="mt-4">
|
||||
<UProgress animation="carousel"/>
|
||||
</div>
|
||||
|
||||
<div v-else-if="changelogEntries.length" class="mt-4 flex flex-col gap-3">
|
||||
<div
|
||||
v-for="entry in changelogEntries"
|
||||
:key="entry.hash"
|
||||
class="rounded-lg border border-gray-200 dark:border-gray-800 p-3"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="min-w-0">
|
||||
<p class="font-medium text-gray-900 dark:text-white break-words">
|
||||
{{ entry.subject }}
|
||||
</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
{{ entry.authorName }} · {{ dayjs(entry.committedAt).format('DD.MM.YYYY HH:mm') }}
|
||||
</p>
|
||||
<div class="flex items-center justify-end flex-shrink-0 gap-0.5">
|
||||
<UKbd v-for="(shortcut, j) in item.shortcuts" :key="j">
|
||||
{{ shortcut }}
|
||||
</UKbd>
|
||||
</div>
|
||||
|
||||
<UBadge color="gray" variant="subtle">
|
||||
{{ entry.shortHash }}
|
||||
</UBadge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="flex flex-col gap-y-6">
|
||||
<div class="flex flex-col gap-y-3">
|
||||
<UButton v-for="(link, index) in links" :key="index" color="white" v-bind="link" />
|
||||
</div>
|
||||
|
||||
<p v-else class="mt-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
Es sind noch keine Changelog-Einträge verfügbar.
|
||||
</p>
|
||||
</UCard>
|
||||
</div>
|
||||
<UCard>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p class="text-base font-semibold text-gray-900 dark:text-white">
|
||||
Changelog
|
||||
</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Zuletzt geöffnet: {{ lastOpenedLabel }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<UButton
|
||||
icon="i-heroicons-arrow-path"
|
||||
color="gray"
|
||||
variant="ghost"
|
||||
:loading="pending"
|
||||
@click="refresh(true)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<UAlert
|
||||
v-if="error"
|
||||
class="mt-4"
|
||||
color="red"
|
||||
variant="soft"
|
||||
title="Changelog konnte nicht geladen werden"
|
||||
:description="error"
|
||||
/>
|
||||
|
||||
<div v-else-if="pending && !changelogEntries.length" class="mt-4">
|
||||
<UProgress animation="carousel"/>
|
||||
</div>
|
||||
|
||||
<div v-else-if="changelogEntries.length" class="mt-4 flex flex-col gap-3">
|
||||
<div
|
||||
v-for="entry in changelogEntries"
|
||||
:key="entry.hash"
|
||||
class="rounded-lg border border-gray-200 dark:border-gray-800 p-3"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="min-w-0">
|
||||
<p class="font-medium text-gray-900 dark:text-white break-words">
|
||||
{{ entry.subject }}
|
||||
</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
{{ entry.authorName }} · {{ dayjs(entry.committedAt).format('DD.MM.YYYY HH:mm') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<UBadge color="gray" variant="subtle">
|
||||
{{ entry.shortHash }}
|
||||
</UBadge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-else class="mt-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
Es sind noch keine Changelog-Einträge verfügbar.
|
||||
</p>
|
||||
</UCard>
|
||||
</div>
|
||||
<!-- <div class="mt-5" v-if="!loadingContactRequest">
|
||||
<h1 class="font-semibold">Kontaktanfrage:</h1>
|
||||
<UForm
|
||||
@@ -262,29 +270,29 @@ watch(isHelpSlideoverOpen, async (isOpen) => {
|
||||
@submit="addContactRequest"
|
||||
@reset="resetContactRequest"
|
||||
>
|
||||
<!– <UFormGroup
|
||||
<!– <UFormField
|
||||
label="Art:"
|
||||
>
|
||||
<USelectMenu
|
||||
:options="['Hilfe','Software Problem / Bug','Funktionsanfrage','Kontakt','Sonstiges']"
|
||||
v-model="contactRequestData.contactType"
|
||||
/>
|
||||
</UFormGroup>–>
|
||||
<UFormGroup
|
||||
</UFormField>–>
|
||||
<UFormField
|
||||
label="Titel:"
|
||||
>
|
||||
<UInput
|
||||
v-model="contactRequestData.title"
|
||||
/>
|
||||
</UFormGroup>
|
||||
<UFormGroup
|
||||
</UFormField>
|
||||
<UFormField
|
||||
label="Nachricht:"
|
||||
>
|
||||
<UTextarea
|
||||
v-model="contactRequestData.message"
|
||||
rows="6"
|
||||
/>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
<InputGroup class="mt-3">
|
||||
<UButton
|
||||
type="submit"
|
||||
@@ -294,7 +302,7 @@ watch(isHelpSlideoverOpen, async (isOpen) => {
|
||||
</UButton>
|
||||
<UButton
|
||||
type="reset"
|
||||
color="rose"
|
||||
color="error"
|
||||
variant="outline"
|
||||
:disabled="!contactRequestData.title && !contactRequestData.message"
|
||||
>
|
||||
@@ -305,5 +313,6 @@ watch(isHelpSlideoverOpen, async (isOpen) => {
|
||||
</UForm>
|
||||
</div>
|
||||
<UProgress class="mt-5" animation="carousel" v-else/>-->
|
||||
</UDashboardSlideover>
|
||||
</template>
|
||||
</USlideover>
|
||||
</template>
|
||||
|
||||
@@ -76,38 +76,40 @@ const renderText = (text) => {
|
||||
|
||||
<template>
|
||||
<UModal
|
||||
v-model="showAddHistoryItemModal"
|
||||
v-model:open="showAddHistoryItemModal"
|
||||
|
||||
>
|
||||
<UCard class="h-full">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
||||
Eintrag hinzufügen
|
||||
</h3>
|
||||
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="showAddHistoryItemModal = false" />
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<UCard class="h-full">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
||||
Eintrag hinzufügen
|
||||
</h3>
|
||||
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="showAddHistoryItemModal = false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<UFormGroup
|
||||
label="Text:"
|
||||
>
|
||||
<UTextarea
|
||||
v-model="addHistoryItemData.text"
|
||||
@keyup.meta.enter="addHistoryItem"
|
||||
/>
|
||||
<!-- TODO: Add Dropdown and Checking for Usernames -->
|
||||
<!-- <template #help>
|
||||
<UKbd>{{metaSymbol}}</UKbd> <UKbd>Enter</UKbd> Speichern
|
||||
</template>-->
|
||||
<UFormField
|
||||
label="Text:"
|
||||
>
|
||||
<UTextarea
|
||||
v-model="addHistoryItemData.text"
|
||||
@keyup.meta.enter="addHistoryItem"
|
||||
/>
|
||||
<!-- TODO: Add Dropdown and Checking for Usernames -->
|
||||
<!-- <template #help>
|
||||
<UKbd>{{metaSymbol}}</UKbd> <UKbd>Enter</UKbd> Speichern
|
||||
</template>-->
|
||||
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
|
||||
<template #footer>
|
||||
<UButton @click="addHistoryItem">Speichern</UButton>
|
||||
</template>
|
||||
</UCard>
|
||||
<template #footer>
|
||||
<UButton @click="addHistoryItem">Speichern</UButton>
|
||||
</template>
|
||||
</UCard>
|
||||
</template>
|
||||
</UModal>
|
||||
<Toolbar
|
||||
v-if="!props.renderHeadline && props.elementId && props.type"
|
||||
@@ -127,7 +129,7 @@ const renderText = (text) => {
|
||||
+ Eintrag
|
||||
</UButton>
|
||||
</div>
|
||||
<UDivider class="my-3"/>
|
||||
<USeparator class="my-3"/>
|
||||
</div>
|
||||
|
||||
<!-- ITEM LIST -->
|
||||
@@ -136,7 +138,7 @@ const renderText = (text) => {
|
||||
v-if="items.length > 0"
|
||||
v-for="(item,index) in items.slice().reverse()"
|
||||
>
|
||||
<UDivider
|
||||
<USeparator
|
||||
class="my-3"
|
||||
v-if="index !== 0"
|
||||
/>
|
||||
|
||||
@@ -86,7 +86,7 @@ defineShortcuts({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<UDivider />
|
||||
<USeparator />
|
||||
</div>
|
||||
</UDashboardPanelContent>
|
||||
</template>
|
||||
@@ -34,7 +34,7 @@ defineProps({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<UDivider class="my-5" />
|
||||
<USeparator class="my-5" />
|
||||
|
||||
<div class="flex-1">
|
||||
<p class="text-lg">
|
||||
@@ -42,7 +42,7 @@ defineProps({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<UDivider class="my-5" />
|
||||
<USeparator class="my-5" />
|
||||
|
||||
<form @submit.prevent>
|
||||
<UTextarea color="gray" required size="xl" :rows="5" :placeholder="`Reply to ${mail.from.name}`">
|
||||
|
||||
@@ -90,7 +90,8 @@ watch(() => labelPrinter.connected, (connected) => {
|
||||
|
||||
<template>
|
||||
<UModal :ui="{ width: 'sm:max-w-5xl' }">
|
||||
<UCard class="w-[92vw] max-w-5xl">
|
||||
<template #content>
|
||||
<UCard class="w-[92vw] max-w-5xl">
|
||||
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
@@ -133,6 +134,7 @@ watch(() => labelPrinter.connected, (connected) => {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</UCard>
|
||||
</UCard>
|
||||
</template>
|
||||
</UModal>
|
||||
</template>
|
||||
|
||||
@@ -18,28 +18,30 @@ const handleClick = async () => {
|
||||
<template>
|
||||
<!-- Printer Button -->
|
||||
|
||||
<UModal v-model="showPrinterInfo">
|
||||
<UCard>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg font-semibold">Drucker Informationen</h3>
|
||||
<UButton icon="i-heroicons-x-mark" variant="ghost" @click="showPrinterInfo = false" />
|
||||
</div>
|
||||
</template>
|
||||
<p>Seriennummer: {{labelPrinter.info.serial}}</p>
|
||||
<p>MAC: {{labelPrinter.info.mac}}</p>
|
||||
<p>Modell: {{labelPrinter.info.modelId}}</p>
|
||||
<p>Charge: {{labelPrinter.info.charge}}</p>
|
||||
<p>Hardware Version: {{labelPrinter.info.hardwareVersion}}</p>
|
||||
<p>Software Version: {{labelPrinter.info.softwareVersion}}</p>
|
||||
</UCard>
|
||||
<UModal v-model:open="showPrinterInfo">
|
||||
<template #content>
|
||||
<UCard>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg font-semibold">Drucker Informationen</h3>
|
||||
<UButton icon="i-heroicons-x-mark" variant="ghost" @click="showPrinterInfo = false" />
|
||||
</div>
|
||||
</template>
|
||||
<p>Seriennummer: {{labelPrinter.info.serial}}</p>
|
||||
<p>MAC: {{labelPrinter.info.mac}}</p>
|
||||
<p>Modell: {{labelPrinter.info.modelId}}</p>
|
||||
<p>Charge: {{labelPrinter.info.charge}}</p>
|
||||
<p>Hardware Version: {{labelPrinter.info.hardwareVersion}}</p>
|
||||
<p>Software Version: {{labelPrinter.info.softwareVersion}}</p>
|
||||
</UCard>
|
||||
</template>
|
||||
</UModal>
|
||||
|
||||
<UButton
|
||||
:icon="labelPrinter.connected ? 'i-heroicons-printer' : 'i-heroicons-printer'"
|
||||
:color="labelPrinter.connected ? 'green' : ''"
|
||||
variant="soft"
|
||||
class="w-full justify-start"
|
||||
class="w-full justify-start rounded-lg px-2.5 py-2 transition-colors hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
:loading="labelPrinter.connectLoading"
|
||||
@click="handleClick"
|
||||
>
|
||||
@@ -50,4 +52,4 @@ const handleClick = async () => {
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
collapsed: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const auth = useAuthStore()
|
||||
const { has } = usePermission()
|
||||
|
||||
// Lokaler State für den Taschenrechner
|
||||
const showCalculator = ref(false)
|
||||
const tenantExtraModules = computed(() => {
|
||||
const modules = auth.activeTenantData?.extraModules
|
||||
return Array.isArray(modules) ? modules : []
|
||||
@@ -19,6 +23,17 @@ const isAdmin = computed(() => Boolean(auth.user?.is_admin))
|
||||
const tenantFeatures = computed(() => auth.activeTenantData?.features || {})
|
||||
const featureEnabled = (key) => tenantFeatures.value?.[key] !== false
|
||||
const visibleItems = (items) => items.filter(item => item && !item.disabled)
|
||||
const isRouteActive = (to) => {
|
||||
if (!to) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (to === '/') {
|
||||
return route.path === '/'
|
||||
}
|
||||
|
||||
return route.path === to || route.path.startsWith(`${to}/`)
|
||||
}
|
||||
|
||||
const links = computed(() => {
|
||||
const organisationChildren = [
|
||||
@@ -121,25 +136,37 @@ const links = computed(() => {
|
||||
to: "/incomingInvoices",
|
||||
icon: "i-heroicons-document-text",
|
||||
} : null,
|
||||
(featureEnabled("createDocument") || featureEnabled("incomingInvoices")) ? {
|
||||
label: "USt-Auswertung",
|
||||
to: "/accounting/tax",
|
||||
icon: "i-heroicons-calculator",
|
||||
} : null,
|
||||
featureEnabled("costcentres") ? {
|
||||
label: "Kostenstellen",
|
||||
to: "/standardEntity/costcentres",
|
||||
icon: "i-heroicons-document-currency-euro"
|
||||
} : null,
|
||||
featureEnabled("accounts") ? {
|
||||
label: "Buchungskonten",
|
||||
to: "/accounts",
|
||||
icon: "i-heroicons-document-text",
|
||||
} : null,
|
||||
featureEnabled("ownaccounts") ? {
|
||||
label: "zusätzliche Buchungskonten",
|
||||
to: "/standardEntity/ownaccounts",
|
||||
icon: "i-heroicons-document-text"
|
||||
((featureEnabled("createDocument") || featureEnabled("incomingInvoices")) || featureEnabled("accounts") || featureEnabled("ownaccounts") || featureEnabled("costcentres")) ? {
|
||||
label: "Auswertungen",
|
||||
icon: "i-heroicons-chart-pie",
|
||||
defaultOpen: false,
|
||||
children: visibleItems([
|
||||
(featureEnabled("createDocument") || featureEnabled("incomingInvoices")) ? {
|
||||
label: "USt",
|
||||
to: "/accounting/tax",
|
||||
icon: "i-heroicons-calculator",
|
||||
} : null,
|
||||
(featureEnabled("createDocument") || featureEnabled("incomingInvoices") || featureEnabled("accounts") || featureEnabled("ownaccounts")) ? {
|
||||
label: "BWA",
|
||||
to: "/accounting/bwa",
|
||||
icon: "i-heroicons-chart-bar-square",
|
||||
} : null,
|
||||
featureEnabled("costcentres") ? {
|
||||
label: "Kostenstellen",
|
||||
to: "/standardEntity/costcentres",
|
||||
icon: "i-heroicons-document-currency-euro"
|
||||
} : null,
|
||||
featureEnabled("accounts") ? {
|
||||
label: "Buchungskonten",
|
||||
to: "/accounts",
|
||||
icon: "i-heroicons-document-text",
|
||||
} : null,
|
||||
featureEnabled("ownaccounts") ? {
|
||||
label: "Zusätzliche Buchungskonten",
|
||||
to: "/standardEntity/ownaccounts",
|
||||
icon: "i-heroicons-document-text"
|
||||
} : null,
|
||||
])
|
||||
} : null,
|
||||
featureEnabled("banking") ? {
|
||||
label: "Bank",
|
||||
@@ -284,15 +311,13 @@ const links = computed(() => {
|
||||
label: pin.label,
|
||||
to: pin.link,
|
||||
icon: pin.icon,
|
||||
target: "_blank",
|
||||
pinned: true
|
||||
target: "_blank"
|
||||
}
|
||||
} else if (pin.type === "standardEntity") {
|
||||
return {
|
||||
label: pin.label,
|
||||
to: pin.datatype === "tasks" ? `/tasks/show/${pin.id}` : `/standardEntity/${pin.datatype}/show/${pin.id}`,
|
||||
icon: pin.icon,
|
||||
pinned: true
|
||||
icon: pin.icon
|
||||
}
|
||||
}
|
||||
}),
|
||||
@@ -309,6 +334,21 @@ const links = computed(() => {
|
||||
to: "/historyitems",
|
||||
icon: "i-heroicons-book-open"
|
||||
} : null,
|
||||
...(has("projects") && featureEnabled("projects")) ? [{
|
||||
label: "Projekte",
|
||||
to: "/standardEntity/projects",
|
||||
icon: "i-heroicons-clipboard-document-check"
|
||||
}] : [],
|
||||
...(has("contracts") && featureEnabled("contracts")) ? [{
|
||||
label: "Verträge",
|
||||
to: "/standardEntity/contracts",
|
||||
icon: "i-heroicons-clipboard-document"
|
||||
}] : [],
|
||||
...(has("plants") && featureEnabled("plants")) ? [{
|
||||
label: "Objekte",
|
||||
to: "/standardEntity/plants",
|
||||
icon: "i-heroicons-clipboard-document"
|
||||
}] : [],
|
||||
...(visibleOrganisationChildren.length > 0 ? [{
|
||||
label: "Organisation",
|
||||
icon: "i-heroicons-rectangle-stack",
|
||||
@@ -358,21 +398,7 @@ const links = computed(() => {
|
||||
children: visibleMasterDataChildren
|
||||
}] : []),
|
||||
|
||||
...(has("projects") && featureEnabled("projects")) ? [{
|
||||
label: "Projekte",
|
||||
to: "/standardEntity/projects",
|
||||
icon: "i-heroicons-clipboard-document-check"
|
||||
}] : [],
|
||||
...(has("contracts") && featureEnabled("contracts")) ? [{
|
||||
label: "Verträge",
|
||||
to: "/standardEntity/contracts",
|
||||
icon: "i-heroicons-clipboard-document"
|
||||
}] : [],
|
||||
...(has("plants") && featureEnabled("plants")) ? [{
|
||||
label: "Objekte",
|
||||
to: "/standardEntity/plants",
|
||||
icon: "i-heroicons-clipboard-document"
|
||||
}] : [],
|
||||
|
||||
...(visibleSettingsChildren.length > 0 ? [{
|
||||
label: "Einstellungen",
|
||||
defaultOpen: false,
|
||||
@@ -382,81 +408,80 @@ const links = computed(() => {
|
||||
])
|
||||
})
|
||||
|
||||
const accordionItems = computed(() =>
|
||||
links.value.filter(item => Array.isArray(item.children) && item.children.length > 0)
|
||||
)
|
||||
const mapNavItem = (item, valuePrefix = "item") => {
|
||||
const children = Array.isArray(item.children)
|
||||
? item.children
|
||||
.filter(Boolean)
|
||||
.map((child, index) => mapNavItem(child, `${valuePrefix}-${index}`))
|
||||
: undefined
|
||||
|
||||
const buttonItems = computed(() =>
|
||||
links.value.filter(item => !item.children || item.children.length === 0)
|
||||
const active = item.active || isRouteActive(item.to) || Boolean(children?.some(child => child.active))
|
||||
|
||||
return {
|
||||
...item,
|
||||
children,
|
||||
value: item.id || item.label || valuePrefix,
|
||||
defaultOpen: item.defaultOpen || active,
|
||||
active,
|
||||
tooltip: true,
|
||||
popover: true,
|
||||
trailingIcon: children?.length ? undefined : ''
|
||||
}
|
||||
}
|
||||
|
||||
const navItems = computed(() =>
|
||||
links.value
|
||||
.filter(Boolean)
|
||||
.map((item, index) => mapNavItem(item, String(index)))
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-1">
|
||||
<UButton
|
||||
v-for="item in buttonItems"
|
||||
:key="item.label"
|
||||
variant="ghost"
|
||||
:color="(item.to && route.path === item.to) ? 'primary' : (item.pinned ? 'amber' : 'gray')"
|
||||
:icon="item.pinned ? 'i-heroicons-star' : item.icon"
|
||||
class="w-full"
|
||||
:to="item.to"
|
||||
:target="item.target"
|
||||
@click="item.click ? item.click() : null"
|
||||
>
|
||||
<UIcon
|
||||
v-if="item.pinned"
|
||||
:name="item.icon"
|
||||
class="w-5 h-5 me-2"
|
||||
/>
|
||||
{{ item.label }}
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<UDivider class="my-2"/>
|
||||
|
||||
<UAccordion
|
||||
:items="accordionItems"
|
||||
:multiple="false"
|
||||
class="mt-2"
|
||||
<UNavigationMenu
|
||||
:items="navItems"
|
||||
orientation="vertical"
|
||||
:collapsed="props.collapsed"
|
||||
tooltip
|
||||
popover
|
||||
color="neutral"
|
||||
highlight
|
||||
highlight-color="primary"
|
||||
class="w-full"
|
||||
:ui="{
|
||||
root: 'w-full',
|
||||
list: 'space-y-1',
|
||||
link: 'min-w-0 rounded-lg px-2.5 py-2',
|
||||
linkLeadingIcon: 'size-5 shrink-0',
|
||||
linkLabel: 'truncate',
|
||||
childList: 'ms-0 space-y-1 border-l border-default ps-3',
|
||||
childLink: 'min-w-0 rounded-lg px-2 py-1.5',
|
||||
childLinkLabel: 'truncate'
|
||||
}"
|
||||
>
|
||||
<template #default="{ item, open }">
|
||||
<UButton
|
||||
variant="ghost"
|
||||
:color="(item.children?.some(c => route.path.includes(c.to))) ? 'primary' : 'gray'"
|
||||
:icon="item.icon"
|
||||
class="w-full"
|
||||
<template #item-leading="{ item, active }">
|
||||
<UIcon
|
||||
v-if="item.icon"
|
||||
:name="item.icon"
|
||||
class="size-5 shrink-0"
|
||||
:class="active ? 'text-primary' : 'text-muted'"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #item-trailing="{ item, active }">
|
||||
<UBadge
|
||||
v-if="item.badge && !props.collapsed"
|
||||
color="primary"
|
||||
variant="soft"
|
||||
size="xs"
|
||||
>
|
||||
{{ item.label }}
|
||||
<template #trailing>
|
||||
<UIcon
|
||||
name="i-heroicons-chevron-right-20-solid"
|
||||
class="w-5 h-5 ms-auto transform transition-transform duration-200"
|
||||
:class="[open && 'rotate-90']"
|
||||
/>
|
||||
</template>
|
||||
</UButton>
|
||||
{{ item.badge }}
|
||||
</UBadge>
|
||||
<UIcon
|
||||
v-else-if="item.children?.length"
|
||||
name="i-heroicons-chevron-down-20-solid"
|
||||
class="size-4 shrink-0 transition-transform"
|
||||
:class="active ? 'text-primary' : 'text-muted'"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #item="{ item }">
|
||||
<div class="flex flex-col">
|
||||
<UButton
|
||||
v-for="child in item.children"
|
||||
:key="child.label"
|
||||
variant="ghost"
|
||||
:color="child.to === route.path ? 'primary' : 'gray'"
|
||||
:icon="child.icon"
|
||||
class="ml-4"
|
||||
:to="child.to"
|
||||
:target="child.target"
|
||||
:disabled="child.disabled"
|
||||
@click="child.click ? child.click() : null"
|
||||
>
|
||||
{{ child.label }}
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
</UAccordion>
|
||||
|
||||
<Calculator v-if="showCalculator" v-model="showCalculator"/>
|
||||
</UNavigationMenu>
|
||||
</template>
|
||||
|
||||
@@ -36,28 +36,30 @@ const setNotificationAsRead = async (notification) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UDashboardSlideover v-model="isNotificationsSlideoverOpen" title="Benachrichtigungen">
|
||||
<NuxtLink
|
||||
v-for="notification in notifications"
|
||||
:key="notification.id"
|
||||
:to="notification.link"
|
||||
class="p-3 rounded-md hover:bg-gray-50 dark:hover:bg-gray-800/50 cursor-pointer flex items-center gap-3 relative"
|
||||
@click="setNotificationAsRead(notification)"
|
||||
>
|
||||
<UChip color="primary" :show="!notification.read && !notification.readAt" inset>
|
||||
<UAvatar alt="FEDEO" size="md" />
|
||||
</UChip>
|
||||
<USlideover v-model:open="isNotificationsSlideoverOpen" title="Benachrichtigungen" side="right">
|
||||
<template #body>
|
||||
<NuxtLink
|
||||
v-for="notification in notifications"
|
||||
:key="notification.id"
|
||||
:to="notification.link"
|
||||
class="p-3 rounded-md hover:bg-gray-50 dark:hover:bg-gray-800/50 cursor-pointer flex items-center gap-3 relative"
|
||||
@click="setNotificationAsRead(notification)"
|
||||
>
|
||||
<UChip color="primary" :show="!notification.read && !notification.readAt" inset>
|
||||
<UAvatar alt="FEDEO" size="md" />
|
||||
</UChip>
|
||||
|
||||
<div class="text-sm flex-1">
|
||||
<p class="flex items-center justify-between">
|
||||
<span class="text-gray-900 dark:text-white font-medium">{{notification.title}}</span>
|
||||
<div class="text-sm flex-1">
|
||||
<p class="flex items-center justify-between">
|
||||
<span class="text-gray-900 dark:text-white font-medium">{{notification.title}}</span>
|
||||
|
||||
<time :datetime="notification.date || notification.createdAt || notification.created_at" class="text-gray-500 dark:text-gray-400 text-xs" v-text="formatTimeAgo(new Date(notification.createdAt || notification.created_at))" />
|
||||
</p>
|
||||
<p class="text-gray-500 dark:text-gray-400">
|
||||
{{ notification.message }}
|
||||
</p>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</UDashboardSlideover>
|
||||
<time :datetime="notification.date || notification.createdAt || notification.created_at" class="text-gray-500 dark:text-gray-400 text-xs" v-text="formatTimeAgo(new Date(notification.createdAt || notification.created_at))" />
|
||||
</p>
|
||||
<p class="text-gray-500 dark:text-gray-400">
|
||||
{{ notification.message }}
|
||||
</p>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
</USlideover>
|
||||
</template>
|
||||
|
||||
@@ -119,7 +119,7 @@ const setDeliveryDateToToday = () => {
|
||||
|
||||
<div class="space-y-5">
|
||||
|
||||
<UFormGroup
|
||||
<UFormField
|
||||
label="Datum der Ausführung"
|
||||
:error="errors.deliveryDate"
|
||||
required
|
||||
@@ -134,9 +134,9 @@ const setDeliveryDateToToday = () => {
|
||||
/>
|
||||
<UButton color="gray" variant="soft" size="lg" label="Heute" @click="setDeliveryDateToToday" />
|
||||
</div>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup
|
||||
<UFormField
|
||||
v-if="!context?.meta?.defaultProfileId && data?.profiles?.length > 0"
|
||||
label="Mitarbeiter"
|
||||
:error="errors.profile"
|
||||
@@ -144,16 +144,16 @@ const setDeliveryDateToToday = () => {
|
||||
>
|
||||
<USelectMenu
|
||||
v-model="form.profile"
|
||||
:options="data.profiles"
|
||||
option-attribute="fullName"
|
||||
value-attribute="id"
|
||||
:items="data.profiles"
|
||||
label-key="fullName"
|
||||
value-key="id"
|
||||
placeholder="Name auswählen..."
|
||||
searchable
|
||||
size="lg"
|
||||
/>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup
|
||||
<UFormField
|
||||
v-if="data?.projects?.length > 0"
|
||||
:label="config.ui?.labels?.project || 'Projekt / Auftrag'"
|
||||
:error="errors.project"
|
||||
@@ -161,16 +161,16 @@ const setDeliveryDateToToday = () => {
|
||||
>
|
||||
<USelectMenu
|
||||
v-model="form.project"
|
||||
:options="data.projects"
|
||||
option-attribute="name"
|
||||
value-attribute="id"
|
||||
:items="data.projects"
|
||||
label-key="name"
|
||||
value-key="id"
|
||||
placeholder="Wählen..."
|
||||
searchable
|
||||
size="lg"
|
||||
/>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup
|
||||
<UFormField
|
||||
v-if="data?.services?.length > 0"
|
||||
:label="config?.ui?.labels?.service || 'Tätigkeit'"
|
||||
:error="errors.service"
|
||||
@@ -178,16 +178,16 @@ const setDeliveryDateToToday = () => {
|
||||
>
|
||||
<USelectMenu
|
||||
v-model="form.service"
|
||||
:options="data.services"
|
||||
option-attribute="name"
|
||||
value-attribute="id"
|
||||
:items="data.services"
|
||||
label-key="name"
|
||||
value-key="id"
|
||||
placeholder="Wählen..."
|
||||
searchable
|
||||
size="lg"
|
||||
/>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup
|
||||
<UFormField
|
||||
label="Menge / Dauer"
|
||||
:error="errors.quantity"
|
||||
required
|
||||
@@ -203,9 +203,9 @@ const setDeliveryDateToToday = () => {
|
||||
<span class="text-gray-500 text-sm pr-2">{{ currentUnit }}</span>
|
||||
</template>
|
||||
</UInput>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup
|
||||
<UFormField
|
||||
v-if="config?.features?.agriculture?.showDieselUsage"
|
||||
label="Dieselverbrauch"
|
||||
:error="errors.diesel"
|
||||
@@ -216,11 +216,11 @@ const setDeliveryDateToToday = () => {
|
||||
<span class="text-gray-500 text-xs">Liter</span>
|
||||
</template>
|
||||
</UInput>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup :label="config?.ui?.labels?.description || 'Notiz / Vorkommnisse'">
|
||||
<UFormField :label="config?.ui?.labels?.description || 'Notiz / Vorkommnisse'">
|
||||
<UTextarea v-model="form.description" :rows="3" placeholder="Optional..." />
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -16,28 +16,30 @@ const onLogout = async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UModal v-model="auth.sessionWarningVisible" prevent-close>
|
||||
<UCard>
|
||||
<template #header>
|
||||
<h3 class="text-lg font-semibold">Sitzung läuft bald ab</h3>
|
||||
</template>
|
||||
<UModal v-model:open="auth.sessionWarningVisible" prevent-close>
|
||||
<template #content>
|
||||
<UCard>
|
||||
<template #header>
|
||||
<h3 class="text-lg font-semibold">Sitzung läuft bald ab</h3>
|
||||
</template>
|
||||
|
||||
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||
Deine Sitzung endet in
|
||||
<span class="font-semibold">{{ remainingTimeLabel }}</span>.
|
||||
Bitte bestätige, um eingeloggt zu bleiben.
|
||||
</p>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||
Deine Sitzung endet in
|
||||
<span class="font-semibold">{{ remainingTimeLabel }}</span>.
|
||||
Bitte bestätige, um eingeloggt zu bleiben.
|
||||
</p>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<UButton variant="outline" color="gray" @click="onLogout">
|
||||
Abmelden
|
||||
</UButton>
|
||||
<UButton color="primary" @click="onRefresh">
|
||||
Eingeloggt bleiben
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<UButton variant="outline" color="gray" @click="onLogout">
|
||||
Abmelden
|
||||
</UButton>
|
||||
<UButton color="primary" @click="onRefresh">
|
||||
Eingeloggt bleiben
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
</template>
|
||||
</UModal>
|
||||
</template>
|
||||
|
||||
@@ -116,7 +116,7 @@ async function onSubmit(event: FormSubmitEvent<any>) {
|
||||
emit('saved')
|
||||
isOpen.value = false
|
||||
} catch (error: any) {
|
||||
toast.add({ title: 'Fehler', description: error.message, color: 'red' })
|
||||
toast.add({ title: 'Fehler', description: error.message, color: 'error' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -124,57 +124,63 @@ async function onSubmit(event: FormSubmitEvent<any>) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UModal v-model="isOpen">
|
||||
<UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
||||
{{ entry ? 'Eintrag bearbeiten' : 'Neue Zeit erfassen' }}
|
||||
</h3>
|
||||
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="isOpen = false" />
|
||||
</div>
|
||||
</template>
|
||||
<UModal v-model:open="isOpen">
|
||||
<template #content>
|
||||
<UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
||||
{{ entry ? 'Eintrag bearbeiten' : 'Neue Zeit erfassen' }}
|
||||
</h3>
|
||||
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="isOpen = false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<UForm :schema="schema" :state="state" class="space-y-4" @submit="onSubmit">
|
||||
<UForm :schema="schema" :state="state" class="space-y-4" @submit="onSubmit">
|
||||
<UFormField label="Typ" name="type">
|
||||
<USelectMenu
|
||||
v-model="state.type"
|
||||
:items="types"
|
||||
value-key="value"
|
||||
label-key="label"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup label="Typ" name="type">
|
||||
<USelectMenu v-model="state.type" :options="types" value-attribute="value" option-attribute="label" />
|
||||
</UFormGroup>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<UFormField label="Start Datum" name="start_date">
|
||||
<div class="flex items-center gap-2">
|
||||
<UInput type="date" v-model="state.start_date" class="flex-1" />
|
||||
<UButton color="gray" variant="soft" label="Heute" @click="setDateFieldToToday('start_date')" />
|
||||
</div>
|
||||
</UFormField>
|
||||
<UFormField label="Start Zeit" name="start_time">
|
||||
<UInput type="time" v-model="state.start_time" />
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<UFormGroup label="Start Datum" name="start_date">
|
||||
<div class="flex items-center gap-2">
|
||||
<UInput type="date" v-model="state.start_date" class="flex-1" />
|
||||
<UButton color="gray" variant="soft" label="Heute" @click="setDateFieldToToday('start_date')" />
|
||||
</div>
|
||||
</UFormGroup>
|
||||
<UFormGroup label="Start Zeit" name="start_time">
|
||||
<UInput type="time" v-model="state.start_time" />
|
||||
</UFormGroup>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<UFormField label="Ende Datum" name="end_date">
|
||||
<div class="flex items-center gap-2">
|
||||
<UInput type="date" v-model="state.end_date" class="flex-1" />
|
||||
<UButton color="gray" variant="soft" label="Heute" @click="setDateFieldToToday('end_date')" />
|
||||
</div>
|
||||
</UFormField>
|
||||
<UFormField label="Ende Zeit" name="end_time">
|
||||
<UInput type="time" v-model="state.end_time" />
|
||||
</UFormField>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 -mt-2">Leer lassen, wenn die Zeit noch läuft.</p>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<UFormGroup label="Ende Datum" name="end_date">
|
||||
<div class="flex items-center gap-2">
|
||||
<UInput type="date" v-model="state.end_date" class="flex-1" />
|
||||
<UButton color="gray" variant="soft" label="Heute" @click="setDateFieldToToday('end_date')" />
|
||||
</div>
|
||||
</UFormGroup>
|
||||
<UFormGroup label="Ende Zeit" name="end_time">
|
||||
<UInput type="time" v-model="state.end_time" />
|
||||
</UFormGroup>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 -mt-2">Leer lassen, wenn die Zeit noch läuft.</p>
|
||||
<UFormField label="Beschreibung / Notiz" name="description">
|
||||
<UTextarea v-model="state.description" placeholder="Was wurde gemacht?" />
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup label="Beschreibung / Notiz" name="description">
|
||||
<UTextarea v-model="state.description" placeholder="Was wurde gemacht?" />
|
||||
</UFormGroup>
|
||||
|
||||
<div class="flex justify-end gap-2 pt-4">
|
||||
<UButton label="Abbrechen" color="gray" variant="ghost" @click="isOpen = false" />
|
||||
<UButton type="submit" label="Speichern" color="primary" :loading="loading" />
|
||||
</div>
|
||||
</UForm>
|
||||
</UCard>
|
||||
<div class="flex justify-end gap-2 pt-4">
|
||||
<UButton label="Abbrechen" color="gray" variant="ghost" @click="isOpen = false" />
|
||||
<UButton type="submit" label="Speichern" color="primary" :loading="loading" />
|
||||
</div>
|
||||
</UForm>
|
||||
</UCard>
|
||||
</template>
|
||||
</UModal>
|
||||
</template>
|
||||
|
||||
@@ -61,33 +61,35 @@ setupPage()
|
||||
|
||||
<template>
|
||||
<UModal :fullscreen="props.mode === 'show'">
|
||||
<EntityShow
|
||||
v-if="loaded && props.mode === 'show'"
|
||||
:type="props.type"
|
||||
:item="item"
|
||||
@updateNeeded="setupPage"
|
||||
:key="item"
|
||||
:in-modal="true"
|
||||
/>
|
||||
<EntityEdit
|
||||
v-else-if="loaded && (props.mode === 'edit' || props.mode === 'create')"
|
||||
:type="props.type"
|
||||
:item="item"
|
||||
:inModal="true"
|
||||
@return-data="(data) => emit('return-data',data)"
|
||||
:createQuery="props.createQuery"
|
||||
:mode="props.mode"
|
||||
/>
|
||||
<!-- <EntityList
|
||||
v-else-if="loaded && props.mode === 'list'"
|
||||
:type="props.type"
|
||||
:items="items"
|
||||
/>-->
|
||||
<UProgress
|
||||
v-else
|
||||
animation="carousel"
|
||||
class="p-5 mt-10"
|
||||
/>
|
||||
<template #content>
|
||||
<EntityShow
|
||||
v-if="loaded && props.mode === 'show'"
|
||||
:type="props.type"
|
||||
:item="item"
|
||||
@updateNeeded="setupPage"
|
||||
:key="item"
|
||||
:in-modal="true"
|
||||
/>
|
||||
<EntityEdit
|
||||
v-else-if="loaded && (props.mode === 'edit' || props.mode === 'create')"
|
||||
:type="props.type"
|
||||
:item="item"
|
||||
:inModal="true"
|
||||
@return-data="(data) => emit('return-data',data)"
|
||||
:createQuery="props.createQuery"
|
||||
:mode="props.mode"
|
||||
/>
|
||||
<!-- <EntityList
|
||||
v-else-if="loaded && props.mode === 'list'"
|
||||
:type="props.type"
|
||||
:items="items"
|
||||
/>-->
|
||||
<UProgress
|
||||
v-else
|
||||
animation="carousel"
|
||||
class="p-5 mt-10"
|
||||
/>
|
||||
</template>
|
||||
</UModal>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,27 +1,59 @@
|
||||
<script setup>
|
||||
const auth = useAuthStore()
|
||||
|
||||
const selectedTenant = ref(auth.user.tenant_id)
|
||||
const activeTenantName = computed(() => {
|
||||
return auth.activeTenantData?.name || auth.tenants?.find((tenant) => tenant.id === auth.activeTenant)?.name || 'Mandant waehlen'
|
||||
})
|
||||
|
||||
const tenantInitials = computed(() => {
|
||||
return activeTenantName.value
|
||||
.split(' ')
|
||||
.filter(Boolean)
|
||||
.slice(0, 2)
|
||||
.map((part) => part[0]?.toUpperCase() || '')
|
||||
.join('') || 'M'
|
||||
})
|
||||
|
||||
const tenantItems = computed(() => [
|
||||
auth.tenants.map((tenant) => ({
|
||||
label: tenant.name,
|
||||
icon: tenant.id === auth.activeTenant ? 'i-heroicons-check' : undefined,
|
||||
disabled: Boolean(tenant.locked),
|
||||
onSelect: async (event) => {
|
||||
if (tenant.locked || tenant.id === auth.activeTenant) {
|
||||
event?.preventDefault?.()
|
||||
return
|
||||
}
|
||||
|
||||
await auth.switchTenant(tenant.id)
|
||||
}
|
||||
}))
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<USelectMenu
|
||||
:options="auth.tenants"
|
||||
value-attribute="id"
|
||||
class="w-40"
|
||||
@change="auth.switchTenant(selectedTenant)"
|
||||
v-model="selectedTenant"
|
||||
:items="tenantItems"
|
||||
:content="{ align: 'start', side: 'bottom', sideOffset: 6 }"
|
||||
:ui="{ content: 'w-[var(--reka-dropdown-menu-trigger-width)] max-w-[var(--reka-dropdown-menu-trigger-width)]' }"
|
||||
class="block w-40"
|
||||
:avatar="{
|
||||
alt: activeTenantName,
|
||||
text: tenantInitials,
|
||||
loading: 'lazy'
|
||||
}"
|
||||
>
|
||||
<UButton color="gray" variant="ghost" :class="[open && 'bg-gray-50 dark:bg-gray-800']" class="w-full">
|
||||
<UAvatar :alt="auth.activeTenantData?.name" size="md" />
|
||||
|
||||
<span class="truncate text-gray-900 dark:text-white font-semibold">{{auth.tenants.find(i => auth.activeTenant === i.id).name}}</span>
|
||||
</UButton>
|
||||
|
||||
<template #option="{option}">
|
||||
{{option.name}}
|
||||
<template #default="{ open }">
|
||||
<UButton
|
||||
color="gray"
|
||||
variant="ghost"
|
||||
class="w-full min-w-0 max-w-full justify-start gap-2 rounded-lg px-2.5 py-2 text-left"
|
||||
:class="[open && 'bg-gray-100 dark:bg-gray-800']"
|
||||
>
|
||||
<span class="min-w-0 flex-1 truncate font-medium text-gray-900 dark:text-white">
|
||||
{{ activeTenantName }}
|
||||
</span>
|
||||
</UButton>
|
||||
</template>
|
||||
</USelectMenu>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<slot name="right"/>
|
||||
</InputGroup>
|
||||
</div>
|
||||
<UDivider class="my-3"/>
|
||||
<USeparator class="my-3"/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
166
frontend/components/UCalendar.vue
Normal file
166
frontend/components/UCalendar.vue
Normal file
@@ -0,0 +1,166 @@
|
||||
<script>
|
||||
import theme from "#build/ui/calendar";
|
||||
</script>
|
||||
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { useForwardPropsEmits } from "reka-ui";
|
||||
import { Calendar as SingleCalendar, RangeCalendar } from "reka-ui/namespaced";
|
||||
import { reactiveOmit } from "@vueuse/core";
|
||||
import { useAppConfig } from "#imports";
|
||||
import { useLocale } from "@nuxt/ui/composables/useLocale";
|
||||
import { tv } from "@nuxt/ui/utils/tv";
|
||||
import UButton from "@nuxt/ui/components/Button.vue";
|
||||
|
||||
const props = defineProps({
|
||||
as: { type: null, required: false },
|
||||
nextYearIcon: { type: String, required: false },
|
||||
nextYear: { type: Object, required: false },
|
||||
nextMonthIcon: { type: String, required: false },
|
||||
nextMonth: { type: Object, required: false },
|
||||
prevYearIcon: { type: String, required: false },
|
||||
prevYear: { type: Object, required: false },
|
||||
prevMonthIcon: { type: String, required: false },
|
||||
prevMonth: { type: Object, required: false },
|
||||
color: { type: null, required: false },
|
||||
size: { type: null, required: false },
|
||||
range: { type: Boolean, required: false },
|
||||
multiple: { type: Boolean, required: false },
|
||||
monthControls: { type: Boolean, required: false, default: true },
|
||||
yearControls: { type: Boolean, required: false, default: true },
|
||||
defaultValue: { type: null, required: false },
|
||||
modelValue: { type: null, required: false },
|
||||
class: { type: null, required: false },
|
||||
ui: { type: null, required: false },
|
||||
defaultPlaceholder: { type: null, required: false },
|
||||
placeholder: { type: null, required: false },
|
||||
allowNonContiguousRanges: { type: Boolean, required: false },
|
||||
pagedNavigation: { type: Boolean, required: false },
|
||||
preventDeselect: { type: Boolean, required: false },
|
||||
maximumDays: { type: Number, required: false },
|
||||
weekStartsOn: { type: Number, required: false, default: 1 },
|
||||
weekdayFormat: { type: String, required: false },
|
||||
fixedWeeks: { type: Boolean, required: false, default: true },
|
||||
maxValue: { type: null, required: false },
|
||||
minValue: { type: null, required: false },
|
||||
numberOfMonths: { type: Number, required: false },
|
||||
disabled: { type: Boolean, required: false },
|
||||
readonly: { type: Boolean, required: false },
|
||||
initialFocus: { type: Boolean, required: false },
|
||||
isDateDisabled: { type: Function, required: false },
|
||||
isDateUnavailable: { type: Function, required: false },
|
||||
isDateHighlightable: { type: Function, required: false },
|
||||
nextPage: { type: Function, required: false },
|
||||
prevPage: { type: Function, required: false },
|
||||
disableDaysOutsideCurrentView: { type: Boolean, required: false },
|
||||
fixedDate: { type: String, required: false }
|
||||
});
|
||||
|
||||
const emits = defineEmits(["update:modelValue", "update:placeholder", "update:validModelValue", "update:startValue"]);
|
||||
|
||||
defineSlots();
|
||||
|
||||
const { code: locale, dir, t } = useLocale();
|
||||
const appConfig = useAppConfig();
|
||||
const rootProps = useForwardPropsEmits(
|
||||
reactiveOmit(props, "range", "modelValue", "defaultValue", "color", "size", "monthControls", "yearControls", "class", "ui"),
|
||||
emits
|
||||
);
|
||||
|
||||
const nextYearIcon = computed(() => props.nextYearIcon || (dir.value === "rtl" ? appConfig.ui.icons.chevronDoubleLeft : appConfig.ui.icons.chevronDoubleRight));
|
||||
const nextMonthIcon = computed(() => props.nextMonthIcon || (dir.value === "rtl" ? appConfig.ui.icons.chevronLeft : appConfig.ui.icons.chevronRight));
|
||||
const prevYearIcon = computed(() => props.prevYearIcon || (dir.value === "rtl" ? appConfig.ui.icons.chevronDoubleRight : appConfig.ui.icons.chevronDoubleLeft));
|
||||
const prevMonthIcon = computed(() => props.prevMonthIcon || (dir.value === "rtl" ? appConfig.ui.icons.chevronRight : appConfig.ui.icons.chevronLeft));
|
||||
const ui = computed(() => tv({ extend: tv(theme), ...appConfig.ui?.calendar || {} })({
|
||||
color: props.color,
|
||||
size: props.size
|
||||
}));
|
||||
const Calendar = computed(() => props.range ? RangeCalendar : SingleCalendar);
|
||||
|
||||
function paginateYear(date, sign) {
|
||||
if (sign === -1) {
|
||||
return date.subtract({ years: 1 });
|
||||
}
|
||||
|
||||
return date.add({ years: 1 });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Calendar.Root
|
||||
v-slot="{ weekDays, grid }"
|
||||
v-bind="rootProps"
|
||||
:model-value="modelValue"
|
||||
:default-value="defaultValue"
|
||||
:locale="locale"
|
||||
:dir="dir"
|
||||
:class="ui.root({ class: [props.ui?.root, props.class] })"
|
||||
>
|
||||
<Calendar.Header :class="ui.header({ class: props.ui?.header })">
|
||||
<Calendar.Prev v-if="props.yearControls" :prev-page="(date) => paginateYear(date, -1)" :aria-label="t('calendar.prevYear')" as-child>
|
||||
<UButton :icon="prevYearIcon" :size="props.size" color="neutral" variant="ghost" v-bind="props.prevYear" />
|
||||
</Calendar.Prev>
|
||||
<Calendar.Prev v-if="props.monthControls" :aria-label="t('calendar.prevMonth')" as-child>
|
||||
<UButton :icon="prevMonthIcon" :size="props.size" color="neutral" variant="ghost" v-bind="props.prevMonth" />
|
||||
</Calendar.Prev>
|
||||
<Calendar.Heading v-slot="{ headingValue }" :class="ui.heading({ class: props.ui?.heading })">
|
||||
<slot name="heading" :value="headingValue">
|
||||
{{ headingValue }}
|
||||
</slot>
|
||||
</Calendar.Heading>
|
||||
<Calendar.Next v-if="props.monthControls" :aria-label="t('calendar.nextMonth')" as-child>
|
||||
<UButton :icon="nextMonthIcon" :size="props.size" color="neutral" variant="ghost" v-bind="props.nextMonth" />
|
||||
</Calendar.Next>
|
||||
<Calendar.Next v-if="props.yearControls" :next-page="(date) => paginateYear(date, 1)" :aria-label="t('calendar.nextYear')" as-child>
|
||||
<UButton :icon="nextYearIcon" :size="props.size" color="neutral" variant="ghost" v-bind="props.nextYear" />
|
||||
</Calendar.Next>
|
||||
</Calendar.Header>
|
||||
|
||||
<div :class="ui.body({ class: props.ui?.body })">
|
||||
<Calendar.Grid
|
||||
v-for="month in grid"
|
||||
:key="month.value.toString()"
|
||||
:class="ui.grid({ class: props.ui?.grid })"
|
||||
>
|
||||
<Calendar.GridHead>
|
||||
<Calendar.GridRow :class="ui.gridWeekDaysRow({ class: props.ui?.gridWeekDaysRow })">
|
||||
<Calendar.HeadCell
|
||||
v-for="day in weekDays"
|
||||
:key="day"
|
||||
:class="ui.headCell({ class: props.ui?.headCell })"
|
||||
>
|
||||
<slot name="week-day" :day="day">
|
||||
{{ day }}
|
||||
</slot>
|
||||
</Calendar.HeadCell>
|
||||
</Calendar.GridRow>
|
||||
</Calendar.GridHead>
|
||||
|
||||
<Calendar.GridBody :class="ui.gridBody({ class: props.ui?.gridBody })">
|
||||
<Calendar.GridRow
|
||||
v-for="(weekDates, index) in month.rows"
|
||||
:key="`weekDate-${index}`"
|
||||
:class="ui.gridRow({ class: props.ui?.gridRow })"
|
||||
>
|
||||
<Calendar.Cell
|
||||
v-for="weekDate in weekDates"
|
||||
:key="weekDate.toString()"
|
||||
:date="weekDate"
|
||||
:class="ui.cell({ class: props.ui?.cell })"
|
||||
>
|
||||
<Calendar.CellTrigger
|
||||
:day="weekDate"
|
||||
:month="month.value"
|
||||
:class="ui.cellTrigger({ class: props.ui?.cellTrigger })"
|
||||
>
|
||||
<slot name="day" :day="weekDate">
|
||||
{{ weekDate.day }}
|
||||
</slot>
|
||||
</Calendar.CellTrigger>
|
||||
</Calendar.Cell>
|
||||
</Calendar.GridRow>
|
||||
</Calendar.GridBody>
|
||||
</Calendar.Grid>
|
||||
</div>
|
||||
</Calendar.Root>
|
||||
</template>
|
||||
94
frontend/components/UDashboardNavbar.vue
Normal file
94
frontend/components/UDashboardNavbar.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<script setup>
|
||||
import DashboardNavbarBase from "@nuxt/ui-pro/runtime/components/DashboardNavbar.vue"
|
||||
import UBadge from "@nuxt/ui/components/Badge.vue"
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
const props = defineProps({
|
||||
as: {
|
||||
type: null,
|
||||
required: false
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
toggle: {
|
||||
type: [Boolean, Object],
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
toggleSide: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "left"
|
||||
},
|
||||
badge: {
|
||||
type: [String, Number],
|
||||
required: false
|
||||
},
|
||||
class: {
|
||||
type: null,
|
||||
required: false
|
||||
},
|
||||
ui: {
|
||||
type: null,
|
||||
required: false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DashboardNavbarBase
|
||||
:as="as"
|
||||
:icon="icon"
|
||||
:title="title"
|
||||
:toggle="toggle"
|
||||
:toggle-side="toggleSide"
|
||||
:class="props.class"
|
||||
:ui="ui"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<template v-if="$slots.toggle" #toggle="slotProps">
|
||||
<slot name="toggle" v-bind="slotProps" />
|
||||
</template>
|
||||
|
||||
<template v-if="$slots.left" #left="slotProps">
|
||||
<slot name="left" v-bind="slotProps" />
|
||||
</template>
|
||||
|
||||
<template v-if="$slots.leading" #leading="slotProps">
|
||||
<slot name="leading" v-bind="slotProps" />
|
||||
</template>
|
||||
|
||||
<template #title>
|
||||
<slot name="title">
|
||||
<span class="inline-flex min-w-0 items-center gap-2">
|
||||
<span class="truncate">{{ title }}</span>
|
||||
<UBadge
|
||||
v-if="badge !== undefined && badge !== null && badge !== ''"
|
||||
size="sm"
|
||||
color="neutral"
|
||||
variant="subtle"
|
||||
>
|
||||
{{ badge }}
|
||||
</UBadge>
|
||||
</span>
|
||||
</slot>
|
||||
</template>
|
||||
|
||||
<template v-if="$slots.trailing" #trailing="slotProps">
|
||||
<slot name="trailing" v-bind="slotProps" />
|
||||
</template>
|
||||
|
||||
<slot />
|
||||
|
||||
<template v-if="$slots.right" #right="slotProps">
|
||||
<slot name="right" v-bind="slotProps" />
|
||||
</template>
|
||||
</DashboardNavbarBase>
|
||||
</template>
|
||||
27
frontend/components/UDashboardPanelContent.vue
Normal file
27
frontend/components/UDashboardPanelContent.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<script setup>
|
||||
defineOptions({
|
||||
inheritAttrs: false
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
as: {
|
||||
type: [String, Object],
|
||||
default: 'div'
|
||||
}
|
||||
})
|
||||
|
||||
const attrs = useAttrs()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="props.as"
|
||||
v-bind="attrs"
|
||||
:class="[
|
||||
'min-h-0 flex-1 overflow-y-auto px-4 py-4 sm:px-6 sm:py-5',
|
||||
attrs.class
|
||||
]"
|
||||
>
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
||||
@@ -1,57 +1,44 @@
|
||||
<script setup>
|
||||
|
||||
const { isHelpSlideoverOpen } = useDashboard()
|
||||
const { isDashboardSearchModalOpen } = useUIState()
|
||||
const { metaSymbol } = useShortcuts()
|
||||
const auth = useAuthStore()
|
||||
|
||||
const items = computed(() => [
|
||||
[{
|
||||
slot: 'account',
|
||||
label: '',
|
||||
disabled: true
|
||||
}], [/*{
|
||||
label: 'Mein Profil',
|
||||
icon: 'i-heroicons-user',
|
||||
to: `/profiles/show/${profileStore.activeProfile.id}`
|
||||
},*/{
|
||||
label: 'Passwort ändern',
|
||||
const userItems = computed(() => [[
|
||||
{
|
||||
label: 'Passwort aendern',
|
||||
icon: 'i-heroicons-shield-check',
|
||||
to: `/password-change`
|
||||
},{
|
||||
to: '/password-change'
|
||||
},
|
||||
{
|
||||
label: 'Abmelden',
|
||||
icon: 'i-heroicons-arrow-left-on-rectangle',
|
||||
click: async () => {
|
||||
onSelect: async () => {
|
||||
await auth.logout()
|
||||
|
||||
}
|
||||
}]
|
||||
])
|
||||
}
|
||||
]])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UDropdown mode="hover" :items="items" :ui="{ width: 'w-full', item: { disabled: 'cursor-text select-text' } }" :popper="{ strategy: 'absolute', placement: 'top' }" class="w-full">
|
||||
<UDropdownMenu
|
||||
:items="userItems"
|
||||
:content="{ align: 'start', side: 'top', sideOffset: 8 }"
|
||||
:ui="{ content: 'w-[var(--reka-dropdown-menu-trigger-width)] max-w-[var(--reka-dropdown-menu-trigger-width)]' }"
|
||||
class="block w-full"
|
||||
>
|
||||
<template #default="{ open }">
|
||||
<UButton color="gray" variant="ghost" class="w-full" :label="auth.user.email" :class="[open && 'bg-gray-50 dark:bg-gray-800']">
|
||||
<!-- <template #leading>
|
||||
<UAvatar :alt="auth.user.email" size="xs" />
|
||||
</template>-->
|
||||
<UButton
|
||||
color="gray"
|
||||
variant="ghost"
|
||||
class="w-full min-w-0 justify-start gap-2 rounded-lg px-2.5 py-2 text-left transition-colors hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
:class="[open && 'bg-gray-100 dark:bg-gray-800']"
|
||||
>
|
||||
<div class="flex items-space gap-2">
|
||||
<span class="min-w-0 flex-1 truncate font-medium text-gray-900 dark:text-white">
|
||||
{{ auth.user.email }}
|
||||
</span>
|
||||
<UIcon name="i-heroicons-ellipsis-vertical" class="h-5 w-5 shrink-0" />
|
||||
</div>
|
||||
|
||||
<template #trailing>
|
||||
<UIcon name="i-heroicons-ellipsis-vertical" class="w-5 h-5 ml-auto" />
|
||||
</template>
|
||||
</UButton>
|
||||
</template>
|
||||
|
||||
<template #account>
|
||||
<div class="text-left">
|
||||
<p>
|
||||
Angemeldet als
|
||||
</p>
|
||||
<p class="truncate font-medium text-gray-900 dark:text-white">
|
||||
{{auth.user.email}}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</UDropdown>
|
||||
</UDropdownMenu>
|
||||
</template>
|
||||
|
||||
@@ -67,12 +67,13 @@ const startImport = () => {
|
||||
|
||||
<template>
|
||||
<UModal :fullscreen="false">
|
||||
<template #content>
|
||||
<UCard>
|
||||
<template #header>
|
||||
Erstelltes Dokument Kopieren
|
||||
</template>
|
||||
|
||||
<UFormGroup
|
||||
<UFormField
|
||||
label="Dokumententyp:"
|
||||
class="mb-3"
|
||||
>
|
||||
@@ -84,7 +85,7 @@ const startImport = () => {
|
||||
>
|
||||
|
||||
</USelectMenu>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
<UCheckbox
|
||||
v-for="key in Object.keys(optionsToImport).filter(i => documentTypeToUse !== props.type ? !['startText','endText'].includes(i) : true)"
|
||||
v-model="optionsToImport[key]"
|
||||
@@ -101,9 +102,10 @@ const startImport = () => {
|
||||
</template>
|
||||
|
||||
</UCard>
|
||||
</template>
|
||||
</UModal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,26 +1,205 @@
|
||||
<script setup>
|
||||
import dayjs from "dayjs"
|
||||
|
||||
const props = defineProps({
|
||||
item: {
|
||||
required: true,
|
||||
type: String
|
||||
type: Object
|
||||
}
|
||||
})
|
||||
|
||||
const incomingInvoices = ref({})
|
||||
const loading = ref(true)
|
||||
const incomingInvoices = ref([])
|
||||
const selectedYear = ref(String(dayjs().year()))
|
||||
const selectedMonth = ref("all")
|
||||
|
||||
const currency = (value) => `${Number(value || 0).toFixed(2).replace(".", ",")} EUR`
|
||||
|
||||
const yearItems = computed(() => {
|
||||
const years = [...new Set(
|
||||
incomingInvoices.value
|
||||
.map((invoice) => invoice.date ? String(dayjs(invoice.date).year()) : null)
|
||||
.filter(Boolean)
|
||||
)].sort((a, b) => Number(b) - Number(a))
|
||||
|
||||
return years.length > 0 ? years.map((year) => ({ label: year, value: year })) : [{ label: String(dayjs().year()), value: String(dayjs().year()) }]
|
||||
})
|
||||
|
||||
const monthItems = [
|
||||
{ label: "Ganzes Jahr", value: "all" },
|
||||
{ label: "Januar", value: "1" },
|
||||
{ label: "Februar", value: "2" },
|
||||
{ label: "Maerz", value: "3" },
|
||||
{ label: "April", value: "4" },
|
||||
{ label: "Mai", value: "5" },
|
||||
{ label: "Juni", value: "6" },
|
||||
{ label: "Juli", value: "7" },
|
||||
{ label: "August", value: "8" },
|
||||
{ label: "September", value: "9" },
|
||||
{ label: "Oktober", value: "10" },
|
||||
{ label: "November", value: "11" },
|
||||
{ label: "Dezember", value: "12" }
|
||||
]
|
||||
|
||||
const reportRows = computed(() => {
|
||||
return incomingInvoices.value.flatMap((invoice) => {
|
||||
const invoiceDate = invoice.date ? dayjs(invoice.date) : null
|
||||
|
||||
if (invoiceDate && invoiceDate.year().toString() !== selectedYear.value) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (invoiceDate && selectedMonth.value !== "all" && invoiceDate.month() + 1 !== Number(selectedMonth.value)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const matchingAccounts = (invoice.accounts || []).filter((account) => account.costCentre === props.item.id)
|
||||
|
||||
return matchingAccounts.map((account, index) => {
|
||||
const amountNet = Number(account.amountNet || 0)
|
||||
const amountTax = Number(account.amountTax || 0)
|
||||
const amountGross = Number(account.amountGross || amountNet + amountTax || 0)
|
||||
|
||||
return {
|
||||
id: `${invoice.id}-${index}`,
|
||||
invoiceId: invoice.id,
|
||||
reference: invoice.reference || "-",
|
||||
date: invoice.date,
|
||||
state: invoice.state || "-",
|
||||
vendorName: invoice.vendor?.name || "-",
|
||||
accountLabel: account.account?.label || account.accountLabel || "-",
|
||||
description: account.description || invoice.description || "-",
|
||||
amountNet,
|
||||
amountTax,
|
||||
amountGross
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const totals = computed(() => {
|
||||
return reportRows.value.reduce((acc, row) => {
|
||||
acc.net += row.amountNet
|
||||
acc.tax += row.amountTax
|
||||
acc.gross += row.amountGross
|
||||
return acc
|
||||
}, { net: 0, tax: 0, gross: 0 })
|
||||
})
|
||||
|
||||
const columns = [
|
||||
{ accessorKey: "reference", header: "Beleg" },
|
||||
{ accessorKey: "date", header: "Datum" },
|
||||
{ accessorKey: "vendorName", header: "Lieferant" },
|
||||
{ accessorKey: "accountLabel", header: "Konto" },
|
||||
{ accessorKey: "description", header: "Beschreibung" },
|
||||
{ accessorKey: "amountNet", header: "Netto" },
|
||||
{ accessorKey: "amountTax", header: "Steuer" },
|
||||
{ accessorKey: "amountGross", header: "Brutto" }
|
||||
]
|
||||
|
||||
const setupPage = async () => {
|
||||
incomingInvoices.value = (await useEntities("incominginvoices").select()).filter(i => i.accounts.find(x => x.costCentre === props.item.id))
|
||||
loading.value = true
|
||||
|
||||
const invoices = await useEntities("incominginvoices").select("*, vendor(id,name)")
|
||||
|
||||
incomingInvoices.value = invoices.filter((invoice) =>
|
||||
(invoice.accounts || []).some((account) => account.costCentre === props.item.id)
|
||||
)
|
||||
|
||||
const firstYear = yearItems.value[0]?.value
|
||||
if (firstYear && !yearItems.value.some((item) => item.value === selectedYear.value)) {
|
||||
selectedYear.value = firstYear
|
||||
}
|
||||
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
setupPage()
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
{{props.item}}
|
||||
{{incomingInvoices}}
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-col gap-3 md:flex-row md:items-end">
|
||||
<UFormField label="Jahr" class="w-full md:w-48">
|
||||
<USelectMenu
|
||||
v-model="selectedYear"
|
||||
:items="yearItems"
|
||||
value-key="value"
|
||||
label-key="label"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<UFormField label="Monat" class="w-full md:w-56">
|
||||
<USelectMenu
|
||||
v-model="selectedMonth"
|
||||
:items="monthItems"
|
||||
value-key="value"
|
||||
label-key="label"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 md:grid-cols-3">
|
||||
<UCard>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">Netto gesamt</div>
|
||||
<div class="mt-1 text-xl font-semibold">{{ currency(totals.net) }}</div>
|
||||
</UCard>
|
||||
|
||||
<UCard>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">Steuer gesamt</div>
|
||||
<div class="mt-1 text-xl font-semibold">{{ currency(totals.tax) }}</div>
|
||||
</UCard>
|
||||
|
||||
<UCard>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">Brutto gesamt</div>
|
||||
<div class="mt-1 text-xl font-semibold">{{ currency(totals.gross) }}</div>
|
||||
</UCard>
|
||||
</div>
|
||||
|
||||
<UTable
|
||||
v-if="!loading"
|
||||
:data="reportRows"
|
||||
:columns="columns"
|
||||
:empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Eingangsbelege mit dieser Kostenstelle gefunden' }"
|
||||
class="w-full"
|
||||
>
|
||||
<template #reference-cell="{ row }">
|
||||
<div class="truncate font-medium">{{ row.original.reference }}</div>
|
||||
</template>
|
||||
|
||||
<template #date-cell="{ row }">
|
||||
<div class="truncate">{{ row.original.date ? dayjs(row.original.date).format("DD.MM.YYYY") : "-" }}</div>
|
||||
</template>
|
||||
|
||||
<template #vendorName-cell="{ row }">
|
||||
<div class="truncate">{{ row.original.vendorName }}</div>
|
||||
</template>
|
||||
|
||||
<template #accountLabel-cell="{ row }">
|
||||
<div class="truncate">{{ row.original.accountLabel }}</div>
|
||||
</template>
|
||||
|
||||
<template #description-cell="{ row }">
|
||||
<UTooltip :text="row.original.description">
|
||||
<div class="max-w-[18rem] truncate">{{ row.original.description }}</div>
|
||||
</UTooltip>
|
||||
</template>
|
||||
|
||||
<template #amountNet-cell="{ row }">
|
||||
<div class="text-right tabular-nums">{{ currency(row.original.amountNet) }}</div>
|
||||
</template>
|
||||
|
||||
<template #amountTax-cell="{ row }">
|
||||
<div class="text-right tabular-nums">{{ currency(row.original.amountTax) }}</div>
|
||||
</template>
|
||||
|
||||
<template #amountGross-cell="{ row }">
|
||||
<div class="text-right font-medium tabular-nums">{{ currency(row.original.amountGross) }}</div>
|
||||
</template>
|
||||
</UTable>
|
||||
|
||||
<UProgress v-else animation="carousel" class="w-3/4 mx-auto" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
||||
221
frontend/components/displayBWASummary.vue
Normal file
221
frontend/components/displayBWASummary.vue
Normal file
@@ -0,0 +1,221 @@
|
||||
<script setup lang="ts">
|
||||
import dayjs from "dayjs"
|
||||
import {
|
||||
getCreatedDocumentTaxBreakdown,
|
||||
getIncomingInvoiceTaxBreakdown
|
||||
} from "~/composables/useTaxEvaluation"
|
||||
|
||||
const loading = ref(true)
|
||||
const summary = ref({
|
||||
label: "",
|
||||
income: 0,
|
||||
expenses: 0,
|
||||
result: 0,
|
||||
taxBalance: 0,
|
||||
incomeCount: 0,
|
||||
expenseCount: 0
|
||||
})
|
||||
|
||||
const formatCurrency = (value: number) => {
|
||||
return new Intl.NumberFormat("de-DE", {
|
||||
style: "currency",
|
||||
currency: "EUR"
|
||||
}).format(Number(value || 0))
|
||||
}
|
||||
|
||||
const isRelevantOutputDocument = (doc: any) => {
|
||||
return doc?.state === "Gebucht" && ["invoices", "advanceInvoices", "cancellationInvoices"].includes(doc?.type)
|
||||
}
|
||||
|
||||
const isRelevantInputInvoice = (invoice: any) => {
|
||||
return invoice?.state === "Gebucht" && !!invoice?.date
|
||||
}
|
||||
|
||||
const loadSummary = async () => {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const bounds = {
|
||||
start: dayjs().startOf("month"),
|
||||
end: dayjs().endOf("month")
|
||||
}
|
||||
|
||||
const [docs, incoming, allocations] = await Promise.all([
|
||||
useEntities("createddocuments").select(),
|
||||
useEntities("incominginvoices").select(),
|
||||
useEntities("statementallocations").select("*, bankstatement(*)")
|
||||
])
|
||||
|
||||
const outputDocs = (docs || []).filter((doc: any) => {
|
||||
if (!isRelevantOutputDocument(doc)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const date = dayjs(doc.documentDate)
|
||||
return date.isValid() && !date.isBefore(bounds.start, "day") && !date.isAfter(bounds.end, "day")
|
||||
})
|
||||
|
||||
const inputDocs = (incoming || []).filter((invoice: any) => {
|
||||
if (!isRelevantInputInvoice(invoice)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const date = dayjs(invoice.date)
|
||||
return date.isValid() && !date.isBefore(bounds.start, "day") && !date.isAfter(bounds.end, "day")
|
||||
})
|
||||
|
||||
const directExpenses = (allocations || []).filter((allocation: any) => {
|
||||
if (allocation?.account === null || typeof allocation?.account === "undefined") {
|
||||
return false
|
||||
}
|
||||
|
||||
const statementDate = allocation?.bankstatement?.date || allocation?.bankstatement?.valueDate || allocation?.date || allocation?.created_at
|
||||
const date = dayjs(statementDate)
|
||||
const amount = Number(allocation?.amount || 0)
|
||||
|
||||
return amount < 0 && date.isValid() && !date.isBefore(bounds.start, "day") && !date.isAfter(bounds.end, "day")
|
||||
})
|
||||
|
||||
const income = outputDocs.reduce((sum: number, doc: any) => {
|
||||
return sum + (doc.rows || []).reduce((rowSum: number, row: any) => {
|
||||
if (!row || ["pagebreak", "title", "text"].includes(row.mode)) {
|
||||
return rowSum
|
||||
}
|
||||
|
||||
const quantity = Number(row.quantity || 0)
|
||||
const price = Number(row.price || 0)
|
||||
const discountPercent = Number(row.discountPercent || 0)
|
||||
|
||||
return rowSum + (quantity * price * (1 - discountPercent / 100))
|
||||
}, 0)
|
||||
}, 0)
|
||||
|
||||
const invoiceExpenses = inputDocs.reduce((sum: number, invoice: any) => {
|
||||
return sum + (invoice.accounts || []).reduce((accountSum: number, account: any) => accountSum + Number(account.amountNet || 0), 0)
|
||||
}, 0)
|
||||
|
||||
const directAccountExpenses = directExpenses.reduce((sum: number, allocation: any) => {
|
||||
return sum + Math.abs(Number(allocation.amount || 0))
|
||||
}, 0)
|
||||
|
||||
const outputTax = outputDocs.reduce((sum: number, doc: any) => {
|
||||
const breakdown = getCreatedDocumentTaxBreakdown(doc)
|
||||
return sum + breakdown.tax19 + breakdown.tax7
|
||||
}, 0)
|
||||
|
||||
const inputTax = inputDocs.reduce((sum: number, invoice: any) => {
|
||||
const breakdown = getIncomingInvoiceTaxBreakdown(invoice)
|
||||
return sum + breakdown.tax19 + breakdown.tax7
|
||||
}, 0)
|
||||
|
||||
const expenses = invoiceExpenses + directAccountExpenses
|
||||
|
||||
summary.value = {
|
||||
label: dayjs().format("MMMM YYYY"),
|
||||
income: Number(income.toFixed(2)),
|
||||
expenses: Number(expenses.toFixed(2)),
|
||||
result: Number((income - expenses).toFixed(2)),
|
||||
taxBalance: Number((outputTax - inputTax).toFixed(2)),
|
||||
incomeCount: outputDocs.length,
|
||||
expenseCount: inputDocs.length + directExpenses.length
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadSummary)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-3">
|
||||
<div class="bwa-summary-top">
|
||||
<div>
|
||||
<p class="bwa-summary-period">{{ summary.label }}</p>
|
||||
<p class="bwa-summary-range">Aktueller Monat</p>
|
||||
</div>
|
||||
<UButton
|
||||
size="xs"
|
||||
variant="soft"
|
||||
color="gray"
|
||||
icon="i-heroicons-arrow-top-right-on-square"
|
||||
@click="navigateTo('/accounting/bwa')"
|
||||
>
|
||||
Details
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<div class="bwa-summary-row">
|
||||
<span class="bwa-summary-label">Einnahmen</span>
|
||||
<span class="bwa-summary-value text-primary-500">
|
||||
{{ loading ? "..." : formatCurrency(summary.income) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="bwa-summary-row">
|
||||
<span class="bwa-summary-label">Ausgaben</span>
|
||||
<span class="bwa-summary-value text-error">
|
||||
{{ loading ? "..." : formatCurrency(summary.expenses) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="bwa-summary-row">
|
||||
<span class="bwa-summary-label">Ergebnis</span>
|
||||
<span class="bwa-summary-value" :class="summary.result >= 0 ? 'text-primary-500' : 'text-error'">
|
||||
{{ loading ? "..." : formatCurrency(summary.result) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="bwa-summary-meta">
|
||||
{{ summary.incomeCount }} Einnahmenbelege | {{ summary.expenseCount }} Ausgabenbelege | USt-Saldo {{ formatCurrency(summary.taxBalance) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.bwa-summary-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.bwa-summary-period {
|
||||
margin: 0;
|
||||
font-weight: 700;
|
||||
color: rgb(17 24 39);
|
||||
}
|
||||
|
||||
.bwa-summary-range,
|
||||
.bwa-summary-meta {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
color: rgb(107 114 128);
|
||||
}
|
||||
|
||||
.bwa-summary-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.bwa-summary-label {
|
||||
color: rgb(55 65 81);
|
||||
}
|
||||
|
||||
.bwa-summary-value {
|
||||
font-weight: 700;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
:deep(.dark) .bwa-summary-period {
|
||||
color: rgb(243 244 246);
|
||||
}
|
||||
|
||||
:deep(.dark) .bwa-summary-range,
|
||||
:deep(.dark) .bwa-summary-meta,
|
||||
:deep(.dark) .bwa-summary-label {
|
||||
color: rgb(156 163 175);
|
||||
}
|
||||
</style>
|
||||
@@ -243,26 +243,26 @@ loadData()
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<USelectMenu
|
||||
v-model="granularity"
|
||||
:options="granularityOptions"
|
||||
value-attribute="value"
|
||||
option-attribute="label"
|
||||
:items="granularityOptions"
|
||||
value-key="value"
|
||||
label-key="label"
|
||||
class="w-28"
|
||||
/>
|
||||
|
||||
<USelectMenu
|
||||
v-model="selectedYear"
|
||||
:options="yearOptions"
|
||||
value-attribute="value"
|
||||
option-attribute="label"
|
||||
:items="yearOptions"
|
||||
value-key="value"
|
||||
label-key="label"
|
||||
class="w-24"
|
||||
/>
|
||||
|
||||
<USelectMenu
|
||||
v-if="granularity === 'month'"
|
||||
v-model="selectedMonth"
|
||||
:options="monthOptions"
|
||||
value-attribute="value"
|
||||
option-attribute="label"
|
||||
:items="monthOptions"
|
||||
value-key="value"
|
||||
label-key="label"
|
||||
class="w-36"
|
||||
/>
|
||||
</div>
|
||||
@@ -288,26 +288,26 @@ loadData()
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<USelectMenu
|
||||
v-model="granularity"
|
||||
:options="granularityOptions"
|
||||
value-attribute="value"
|
||||
option-attribute="label"
|
||||
:items="granularityOptions"
|
||||
value-key="value"
|
||||
label-key="label"
|
||||
class="w-28"
|
||||
/>
|
||||
|
||||
<USelectMenu
|
||||
v-model="selectedYear"
|
||||
:options="yearOptions"
|
||||
value-attribute="value"
|
||||
option-attribute="label"
|
||||
:items="yearOptions"
|
||||
value-key="value"
|
||||
label-key="label"
|
||||
class="w-24"
|
||||
/>
|
||||
|
||||
<USelectMenu
|
||||
v-if="granularity === 'month'"
|
||||
v-model="selectedMonth"
|
||||
:options="monthOptions"
|
||||
value-attribute="value"
|
||||
option-attribute="label"
|
||||
:items="monthOptions"
|
||||
value-key="value"
|
||||
label-key="label"
|
||||
class="w-36"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -23,9 +23,9 @@ setupPage()
|
||||
<template>
|
||||
<UTable
|
||||
v-if="openTasks.length > 0"
|
||||
:rows="openTasks"
|
||||
:columns="[{key:'name',label:'Name'},{key:'categorie',label:'Kategorie'}]"
|
||||
@select="(i) => router.push(`/tasks/show/${i.id}`)"
|
||||
:data="openTasks"
|
||||
:columns="normalizeTableColumns([{key:'name',label:'Name'},{key:'categorie',label:'Kategorie'}])"
|
||||
:on-select="(i) => router.push(`/tasks/show/${i.id}`)"
|
||||
/>
|
||||
<div v-else>
|
||||
<p class="text-center font-bold">Keine offenen Aufgaben</p>
|
||||
|
||||
@@ -29,7 +29,7 @@ const startTime = async () => {
|
||||
await setupPage()
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
toast.add({title: "Fehler beim starten der Projektzeit",color:"rose"})
|
||||
toast.add({title: "Fehler beim starten der Projektzeit",color:"error"})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ const stopStartedTime = async () => {
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
let errorId = await useError().logError(`${JSON.stringify(error)}`)
|
||||
toast.add({title: errorId ? `Fehler beim stoppen der Projektzeit (Fehler ID: ${errorId})` : `Fehler beim stoppen der Projektzeit`,color:"rose"})
|
||||
toast.add({title: errorId ? `Fehler beim stoppen der Projektzeit (Fehler ID: ${errorId})` : `Fehler beim stoppen der Projektzeit`,color:"error"})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,15 +53,15 @@ const stopStartedTime = async () => {
|
||||
<p>Start: {{dayjs(runningTimeInfo.started_at).format("HH:mm")}}</p>
|
||||
<p>Dauer: {{dayjs().diff(dayjs(runningTimeInfo.started_at),'minutes') > 59 ? `${Math.floor(dayjs().diff(dayjs(runningTimeInfo.started_at),'minutes') / 60)}:${dayjs().diff(dayjs(runningTimeInfo.started_at),'minutes') % 60} h` : dayjs().diff(dayjs(runningTimeInfo.started_at),'minutes') + ' min' }}</p>
|
||||
|
||||
<UFormGroup
|
||||
<UFormField
|
||||
class="mt-2"
|
||||
label="Notizen:"
|
||||
>
|
||||
<UTextarea
|
||||
v-model="runningTimeInfo.notes"
|
||||
/>
|
||||
</UFormGroup>
|
||||
<UFormGroup
|
||||
</UFormField>
|
||||
<UFormField
|
||||
class="mt-2"
|
||||
label="Projekt:"
|
||||
>
|
||||
@@ -74,7 +74,7 @@ const stopStartedTime = async () => {
|
||||
value-attribute="id"
|
||||
option-attribute="name"
|
||||
/>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
<UButton
|
||||
class="mt-3"
|
||||
@click="stopStartedTime"
|
||||
|
||||
@@ -25,7 +25,7 @@ const startTime = async () => {
|
||||
await setupPage()
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
toast.add({title: "Fehler beim starten der Zeit",color:"rose"})
|
||||
toast.add({title: "Fehler beim starten der Zeit",color:"error"})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ const stopStartedTime = async () => {
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
let errorId = await useError().logError(`${JSON.stringify(error)}`)
|
||||
toast.add({title: errorId ? `Fehler beim stoppen der Anwesenheit (Fehler ID: ${errorId})` : `Fehler beim stoppen der Anwesenheit`,color:"rose"})
|
||||
toast.add({title: errorId ? `Fehler beim stoppen der Anwesenheit (Fehler ID: ${errorId})` : `Fehler beim stoppen der Anwesenheit`,color:"error"})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,14 +49,14 @@ const stopStartedTime = async () => {
|
||||
<p>Start: {{dayjs(runningTimeInfo.started_at).format("HH:mm")}}</p>
|
||||
<p>Dauer: {{dayjs().diff(dayjs(runningTimeInfo.started_at),'minutes') > 59 ? `${Math.floor(dayjs().diff(dayjs(runningTimeInfo.started_at),'minutes') / 60)}:${dayjs().diff(dayjs(runningTimeInfo.started_at),'minutes') % 60} h` : dayjs().diff(dayjs(runningTimeInfo.started_at),'minutes') + ' min' }}</p>
|
||||
|
||||
<UFormGroup
|
||||
<UFormField
|
||||
class="mt-2"
|
||||
label="Notizen:"
|
||||
>
|
||||
<UTextarea
|
||||
v-model="runningTimeInfo.notes"
|
||||
/>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
<UButton
|
||||
class="mt-3"
|
||||
@click="stopStartedTime"
|
||||
|
||||
@@ -12,6 +12,10 @@ const props = defineProps({
|
||||
const products = ref([])
|
||||
const units = ref([])
|
||||
|
||||
const productSearchInput = {
|
||||
placeholder: 'Artikel suchen...'
|
||||
}
|
||||
|
||||
const setup = async () => {
|
||||
products.value = await useEntities("products").select()
|
||||
units.value = await useEntities("units").selectSpecial()
|
||||
@@ -68,66 +72,83 @@ const setRowData = (row) => {
|
||||
+ Artikel
|
||||
</UButton>
|
||||
|
||||
<table class="w-full mt-3">
|
||||
<tr>
|
||||
<th>Artikel</th>
|
||||
<th>Menge</th>
|
||||
<th>Einheit</th>
|
||||
<th>Verkaufspreis</th>
|
||||
</tr>
|
||||
<tr
|
||||
v-for="product in props.item.materialComposition"
|
||||
>
|
||||
<td>
|
||||
<USelectMenu
|
||||
searchable
|
||||
:search-attributes="['name']"
|
||||
:options="products"
|
||||
value-attribute="id"
|
||||
option-attribute="name"
|
||||
v-model="product.product"
|
||||
:color="product.product ? 'primary' : 'rose'"
|
||||
@change="setRowData(product)"
|
||||
<div class="mt-3 overflow-x-auto">
|
||||
<table class="w-full min-w-[44rem] table-fixed">
|
||||
<thead>
|
||||
<tr class="border-b border-gray-200 dark:border-gray-800">
|
||||
<th class="px-2 py-2 text-left font-medium">Artikel</th>
|
||||
<th class="px-2 py-2 text-left font-medium">Menge</th>
|
||||
<th class="px-2 py-2 text-left font-medium">Einheit</th>
|
||||
<th class="px-2 py-2 text-left font-medium">Verkaufspreis</th>
|
||||
<th class="w-12 px-2 py-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="product in props.item.materialComposition"
|
||||
:key="product.id"
|
||||
class="border-b border-gray-100 align-top dark:border-gray-800"
|
||||
>
|
||||
<template #label>
|
||||
{{products.find(i => i.id === product.product) ? products.find(i => i.id === product.product).name : 'Kein Artikel ausgewählt'}}
|
||||
<td class="px-2 py-2">
|
||||
<USelectMenu
|
||||
class="w-full"
|
||||
:items="products"
|
||||
label-key="name"
|
||||
value-key="id"
|
||||
:search-input="productSearchInput"
|
||||
:filter-fields="['name']"
|
||||
v-model="product.product"
|
||||
:color="product.product ? 'primary' : 'error'"
|
||||
@update:model-value="setRowData(product)"
|
||||
>
|
||||
<template #default>
|
||||
{{ products.find(i => i.id === product.product)?.name || 'Kein Artikel ausgewählt' }}
|
||||
</template>
|
||||
</USelectMenu>
|
||||
</td>
|
||||
<td>
|
||||
<td class="px-2 py-2">
|
||||
<UInput
|
||||
class="w-full"
|
||||
type="number"
|
||||
v-model="product.quantity"
|
||||
:step="units.find(i => i.id === product.unit) ? units.find(i => i.id === product.unit).step : 1"
|
||||
@change="calculateTotalMaterialPrice"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<td class="px-2 py-2">
|
||||
<USelectMenu
|
||||
:options="units"
|
||||
value-attribute="id"
|
||||
option-attribute="name"
|
||||
class="w-full"
|
||||
:items="units"
|
||||
label-key="name"
|
||||
value-key="id"
|
||||
v-model="product.unit"
|
||||
></USelectMenu>
|
||||
>
|
||||
<template #default>
|
||||
{{ units.find(i => i.id === product.unit)?.name || 'Einheit wählen' }}
|
||||
</template>
|
||||
</USelectMenu>
|
||||
</td>
|
||||
<td>
|
||||
<td class="px-2 py-2">
|
||||
<UInput
|
||||
class="w-full"
|
||||
type="number"
|
||||
v-model="product.price"
|
||||
step="0.01"
|
||||
@change="calculateTotalMaterialPrice"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<td class="px-2 py-2">
|
||||
<UButton
|
||||
icon="i-heroicons-x-mark"
|
||||
@click="removeProductFromMaterialComposition(product.id)"
|
||||
variant="outline"
|
||||
color="rose"
|
||||
color="error"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</UCard>
|
||||
</template>
|
||||
|
||||
@@ -135,4 +156,4 @@ const setRowData = (row) => {
|
||||
td {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -53,7 +53,10 @@ const emit = defineEmits(["click"])
|
||||
<style scoped>
|
||||
/* FAB Basis */
|
||||
.fab-base {
|
||||
@apply rounded-full px-5 py-4 text-lg font-semibold;
|
||||
border-radius: 9999px;
|
||||
padding: 1rem 1.25rem;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
|
||||
/* Wenn nur ein Icon vorhanden ist → runder Kreis */
|
||||
/* Wenn Label + Icon → Extended FAB */
|
||||
@@ -61,6 +64,12 @@ const emit = defineEmits(["click"])
|
||||
|
||||
/* Optional: Auto-Kreisen wenn kein Label */
|
||||
#fab:not([label]) {
|
||||
@apply w-14 h-14 p-0 flex items-center justify-center text-2xl;
|
||||
width: 3.5rem;
|
||||
height: 3.5rem;
|
||||
padding: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -47,14 +47,14 @@ async function handlePrint() {
|
||||
|
||||
{{labelPrinter.printProgress}}
|
||||
|
||||
<UFormGroup label="Breite">
|
||||
<UFormField label="Breite">
|
||||
<UInput v-model="labelWidth"><template #trailing>mm</template></UInput>
|
||||
</UFormGroup>
|
||||
<UFormGroup label="Höhe">
|
||||
</UFormField>
|
||||
<UFormField label="Höhe">
|
||||
<UInput v-model="labelHeight"><template #trailing>mm</template></UInput>
|
||||
</UFormGroup>
|
||||
<UFormGroup label="ZPL">
|
||||
</UFormField>
|
||||
<UFormField label="ZPL">
|
||||
<UTextarea v-model="zpl" rows="6" />
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
</UCard>
|
||||
</template>
|
||||
|
||||
@@ -12,6 +12,10 @@ const props = defineProps({
|
||||
const hourrates = ref([])
|
||||
const units = ref([])
|
||||
|
||||
const hourrateSearchInput = {
|
||||
placeholder: 'Stundensatz suchen...'
|
||||
}
|
||||
|
||||
const setup = async () => {
|
||||
hourrates.value = await useEntities("hourrates").select()
|
||||
units.value = await useEntities("units").selectSpecial()
|
||||
@@ -69,76 +73,94 @@ const setRowData = (row) => {
|
||||
+ Stundensatz
|
||||
</UButton>
|
||||
|
||||
<table class="w-full mt-3">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Menge</th>
|
||||
<th>Einheit</th>
|
||||
<th>Einkaufpreis</th>
|
||||
<th>Verkaufspreis</th>
|
||||
</tr>
|
||||
<tr
|
||||
v-for="row in props.item.personalComposition"
|
||||
>
|
||||
<td>
|
||||
<USelectMenu
|
||||
searchable
|
||||
:search-attributes="['name']"
|
||||
:options="hourrates"
|
||||
value-attribute="id"
|
||||
option-attribute="name"
|
||||
v-model="row.hourrate"
|
||||
:color="row.hourrate ? 'primary' : 'rose'"
|
||||
@change="setRowData(row)"
|
||||
<div class="mt-3 overflow-x-auto">
|
||||
<table class="w-full min-w-[52rem] table-fixed">
|
||||
<thead>
|
||||
<tr class="border-b border-gray-200 dark:border-gray-800">
|
||||
<th class="px-2 py-2 text-left font-medium">Name</th>
|
||||
<th class="px-2 py-2 text-left font-medium">Menge</th>
|
||||
<th class="px-2 py-2 text-left font-medium">Einheit</th>
|
||||
<th class="px-2 py-2 text-left font-medium">Einkaufspreis</th>
|
||||
<th class="px-2 py-2 text-left font-medium">Verkaufspreis</th>
|
||||
<th class="w-12 px-2 py-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="row in props.item.personalComposition"
|
||||
:key="row.id"
|
||||
class="border-b border-gray-100 align-top dark:border-gray-800"
|
||||
>
|
||||
<!-- <template #label>
|
||||
{{products.find(i => i.id === product.product) ? products.find(i => i.id === product.product).name : 'Kein Artikel ausgewählt'}}
|
||||
</template>-->
|
||||
<td class="px-2 py-2">
|
||||
<USelectMenu
|
||||
class="w-full"
|
||||
:items="hourrates"
|
||||
label-key="name"
|
||||
value-key="id"
|
||||
:search-input="hourrateSearchInput"
|
||||
:filter-fields="['name']"
|
||||
v-model="row.hourrate"
|
||||
:color="row.hourrate ? 'primary' : 'error'"
|
||||
@update:model-value="setRowData(row)"
|
||||
>
|
||||
<template #default>
|
||||
{{ hourrates.find(i => i.id === row.hourrate)?.name || 'Kein Stundensatz ausgewählt' }}
|
||||
</template>
|
||||
</USelectMenu>
|
||||
</td>
|
||||
<td>
|
||||
<td class="px-2 py-2">
|
||||
<UInput
|
||||
class="w-full"
|
||||
type="number"
|
||||
v-model="row.quantity"
|
||||
:step="units.find(i => i.id === row.unit) ? units.find(i => i.id === row.unit).step : 1"
|
||||
@change="calculateTotalPersonalPrice"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<td class="px-2 py-2">
|
||||
<USelectMenu
|
||||
:options="units"
|
||||
class="w-full"
|
||||
:items="units"
|
||||
disabled
|
||||
value-attribute="id"
|
||||
option-attribute="name"
|
||||
label-key="name"
|
||||
value-key="id"
|
||||
v-model="row.unit"
|
||||
></USelectMenu>
|
||||
>
|
||||
<template #default>
|
||||
{{ units.find(i => i.id === row.unit)?.name || 'Einheit' }}
|
||||
</template>
|
||||
</USelectMenu>
|
||||
</td>
|
||||
<td>
|
||||
<td class="px-2 py-2">
|
||||
<UInput
|
||||
class="w-full"
|
||||
type="number"
|
||||
v-model="row.purchasePrice"
|
||||
step="0.01"
|
||||
@change="calculateTotalPersonalPrice"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<td class="px-2 py-2">
|
||||
<UInput
|
||||
class="w-full"
|
||||
type="number"
|
||||
v-model="row.price"
|
||||
step="0.01"
|
||||
@change="calculateTotalPersonalPrice"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<td class="px-2 py-2">
|
||||
<UButton
|
||||
icon="i-heroicons-x-mark"
|
||||
@click="removeRowFromPersonalComposition(row.id)"
|
||||
variant="outline"
|
||||
color="rose"
|
||||
color="error"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</UCard>
|
||||
</template>
|
||||
|
||||
@@ -146,4 +168,4 @@ const setRowData = (row) => {
|
||||
td {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -206,16 +206,62 @@ const addVideo = () => {
|
||||
<style scoped>
|
||||
/* Toolbar & Buttons */
|
||||
.toolbar-btn {
|
||||
@apply p-1.5 rounded text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors text-sm font-medium min-w-[28px] h-[28px] flex items-center justify-center;
|
||||
padding: 0.375rem;
|
||||
border-radius: 0.25rem;
|
||||
color: #4b5563;
|
||||
transition: background-color 0.2s ease, color 0.2s ease;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
min-width: 28px;
|
||||
height: 28px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.toolbar-btn.is-active {
|
||||
@apply bg-gray-200 dark:bg-gray-600 text-black dark:text-white shadow-inner;
|
||||
background: #e5e7eb;
|
||||
color: #000;
|
||||
box-shadow: inset 0 2px 4px 0 rgb(0 0 0 / 0.05);
|
||||
}
|
||||
.bubble-btn {
|
||||
@apply px-2 py-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors text-sm min-w-[24px] h-[24px] flex items-center justify-center text-gray-700 dark:text-gray-200;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
transition: background-color 0.2s ease, color 0.2s ease;
|
||||
font-size: 0.875rem;
|
||||
min-width: 24px;
|
||||
height: 24px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #374151;
|
||||
}
|
||||
.bubble-btn.is-active {
|
||||
@apply bg-gray-200 dark:bg-gray-600 text-black dark:text-white;
|
||||
background: #e5e7eb;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.toolbar-btn:hover,
|
||||
.bubble-btn:hover {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
:global(.dark) .toolbar-btn {
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
:global(.dark) .toolbar-btn:hover,
|
||||
:global(.dark) .bubble-btn:hover {
|
||||
background: #374151;
|
||||
}
|
||||
|
||||
:global(.dark) .toolbar-btn.is-active,
|
||||
:global(.dark) .bubble-btn.is-active {
|
||||
background: #4b5563;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
:global(.dark) .bubble-btn {
|
||||
color: #e5e7eb;
|
||||
}
|
||||
|
||||
/* GLOBAL EDITOR STYLES */
|
||||
@@ -235,20 +281,48 @@ const addVideo = () => {
|
||||
/* MENTION */
|
||||
.wiki-mention {
|
||||
/* Pill-Shape, grau/neutral statt knallig blau */
|
||||
@apply bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-200 px-1.5 py-0.5 rounded-md text-sm font-medium no-underline inline-block mx-0.5 align-middle border border-gray-200 dark:border-gray-700;
|
||||
|
||||
background: #f3f4f6;
|
||||
color: #374151;
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
margin-inline: 0.125rem;
|
||||
vertical-align: middle;
|
||||
border: 1px solid #e5e7eb;
|
||||
box-decoration-break: clone;
|
||||
}
|
||||
|
||||
.wiki-mention::before {
|
||||
@apply text-gray-400 dark:text-gray-500 mr-0.5;
|
||||
color: #9ca3af;
|
||||
margin-right: 0.125rem;
|
||||
}
|
||||
|
||||
.wiki-mention:hover {
|
||||
@apply bg-primary-50 dark:bg-primary-900/30 border-primary-200 dark:border-primary-800 text-primary-700 dark:text-primary-400;
|
||||
background: #eefbf0;
|
||||
border-color: #bbf7d0;
|
||||
color: #15803d;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:global(.dark) .wiki-mention {
|
||||
background: #1f2937;
|
||||
color: #e5e7eb;
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
:global(.dark) .wiki-mention::before {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
:global(.dark) .wiki-mention:hover {
|
||||
background: rgb(20 83 45 / 0.3);
|
||||
border-color: #166534;
|
||||
color: #4ade80;
|
||||
}
|
||||
|
||||
/* TABLE */
|
||||
table { width: 100% !important; border-collapse: collapse; table-layout: fixed; margin: 1rem 0; }
|
||||
th, td { border: 1px solid #d1d5db; padding: 0.5rem; vertical-align: top; box-sizing: border-box; min-width: 1em; }
|
||||
@@ -258,7 +332,7 @@ const addVideo = () => {
|
||||
.column-resize-handle { background-color: #3b82f6; width: 4px; }
|
||||
|
||||
/* CODE */
|
||||
pre { background: #0d1117; color: #c9d1d9; font-family: 'JetBrains Mono', monospace; padding: 0.75rem 1rem; border-radius: 0.5rem; margin: 1rem 0; overflow-x: auto; }
|
||||
pre { background: #0d1117; color: #c9d1d9; font-family: var(--font-mono); padding: 0.75rem 1rem; border-radius: 0.5rem; margin: 1rem 0; overflow-x: auto; }
|
||||
code { color: inherit; padding: 0; background: none; font-size: 0.9rem; }
|
||||
|
||||
/* IMG */
|
||||
@@ -269,4 +343,4 @@ const addVideo = () => {
|
||||
mark { background-color: #fef08a; padding: 0.1rem 0.2rem; border-radius: 0.2rem; }
|
||||
.ProseMirror-selectednode { outline: 2px solid #3b82f6; }
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -98,17 +98,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UModal v-model="isCreateModalOpen">
|
||||
<div class="p-5">
|
||||
<h3 class="font-bold mb-4">Neue Seite</h3>
|
||||
<form @submit.prevent="createPage">
|
||||
<UInput v-model="newTitle" placeholder="Titel..." autofocus />
|
||||
<div class="mt-4 flex justify-end gap-2">
|
||||
<UButton color="gray" variant="ghost" @click="isCreateModalOpen = false">Abbrechen</UButton>
|
||||
<UButton type="submit" color="primary" :loading="isCreating">Erstellen</UButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<UModal v-model:open="isCreateModalOpen">
|
||||
<template #content>
|
||||
<div class="p-5">
|
||||
<h3 class="font-bold mb-4">Neue Seite</h3>
|
||||
<form @submit.prevent="createPage">
|
||||
<UInput v-model="newTitle" placeholder="Titel..." autofocus />
|
||||
<div class="mt-4 flex justify-end gap-2">
|
||||
<UButton color="gray" variant="ghost" @click="isCreateModalOpen = false">Abbrechen</UButton>
|
||||
<UButton type="submit" color="primary" :loading="isCreating">Erstellen</UButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
</UModal>
|
||||
|
||||
</div>
|
||||
@@ -163,7 +165,7 @@ async function selectPage(id: string) {
|
||||
const data = await $api(`/api/wiki/${id}`, { method: 'GET' })
|
||||
selectedPage.value = data
|
||||
} catch (e) {
|
||||
toast.add({ title: 'Fehler beim Laden', color: 'red' })
|
||||
toast.add({ title: 'Fehler beim Laden', color: 'error' })
|
||||
} finally {
|
||||
loadingContent.value = false
|
||||
}
|
||||
@@ -233,4 +235,4 @@ watch(() => [props.entityId, props.entityUuid], fetchList)
|
||||
<style scoped>
|
||||
.custom-scrollbar::-webkit-scrollbar { width: 4px; }
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb { background-color: #ddd; border-radius: 4px; }
|
||||
</style>
|
||||
</style>
|
||||
|
||||
35
frontend/composables/useModal.ts
Normal file
35
frontend/composables/useModal.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
type ModalComponent = any
|
||||
type ModalProps = Record<string, any> | undefined
|
||||
|
||||
const modalStack = useState<any[]>('__fed_modal_stack__', () => [])
|
||||
|
||||
export const useModal = () => {
|
||||
const overlay = useOverlay()
|
||||
|
||||
const open = (component: ModalComponent, props?: ModalProps) => {
|
||||
const instance = overlay.create(component, { props, destroyOnClose: true })
|
||||
modalStack.value.push(instance)
|
||||
|
||||
const result = instance.open(props)
|
||||
result.finally(() => {
|
||||
modalStack.value = modalStack.value.filter((entry) => entry.id !== instance.id)
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const close = (value?: any) => {
|
||||
const current = modalStack.value[modalStack.value.length - 1]
|
||||
|
||||
if (!current) {
|
||||
return
|
||||
}
|
||||
|
||||
current.close(value)
|
||||
}
|
||||
|
||||
return {
|
||||
open,
|
||||
close
|
||||
}
|
||||
}
|
||||
28
frontend/composables/useTableColumns.ts
Normal file
28
frontend/composables/useTableColumns.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
type LegacyTableColumn = {
|
||||
id?: string
|
||||
key?: string
|
||||
label?: unknown
|
||||
header?: unknown
|
||||
accessorKey?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export const normalizeTableColumns = (columns: LegacyTableColumn[] = []) => {
|
||||
return columns.map((column, index) => {
|
||||
const accessorKey = typeof column.accessorKey === 'string'
|
||||
? column.accessorKey
|
||||
: typeof column.key === 'string'
|
||||
? column.key
|
||||
: undefined
|
||||
|
||||
const header = column.header ?? column.label ?? accessorKey ?? `column_${index}`
|
||||
const id = column.id ?? accessorKey ?? (typeof header === 'string' ? header : `column_${index}`)
|
||||
|
||||
return {
|
||||
...column,
|
||||
id,
|
||||
accessorKey,
|
||||
header
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -240,26 +240,42 @@ onMounted(() => {
|
||||
</UCard>
|
||||
</UContainer>
|
||||
</div>
|
||||
<UDashboardLayout class="safearea" v-else>
|
||||
<UDashboardPanel :width="250" :resizable="{ min: 200, max: 300 }" collapsible>
|
||||
<UDashboardNavbar style="margin-top: env(safe-area-inset-top, 10px) !important;"
|
||||
:class="['!border-transparent']" :ui="{ left: 'flex-1' }">
|
||||
<template #left>
|
||||
<div class="safearea flex min-h-screen w-full flex-col overflow-hidden" v-else>
|
||||
<!-- <div
|
||||
class="border-b border-default bg-default px-3 py-2"
|
||||
style="padding-top: max(env(safe-area-inset-top, 0px), 0.5rem);"
|
||||
>
|
||||
<TenantDropdown class="min-w-0 w-full max-w-sm" />
|
||||
</div>-->
|
||||
|
||||
<UDashboardGroup class="flex min-h-0 flex-1 overflow-hidden">
|
||||
|
||||
|
||||
<UDashboardSidebar
|
||||
id="sidebar"
|
||||
collapsible
|
||||
resizable
|
||||
:default-size="18"
|
||||
:min-size="14"
|
||||
:max-size="24"
|
||||
class="shrink-0 border-r border-default bg-default"
|
||||
>
|
||||
<template #header>
|
||||
<TenantDropdown class="w-full"/>
|
||||
|
||||
</template>
|
||||
</UDashboardNavbar>
|
||||
|
||||
<UDashboardSidebar id="sidebar">
|
||||
<template #default="{ collapsed }">
|
||||
<MainNav :collapsed="collapsed" />
|
||||
</template>
|
||||
|
||||
<MainNav/>
|
||||
<template #footer="{ collapsed }">
|
||||
<div class="flex flex-col gap-3 w-full">
|
||||
<div class="flex items-center gap-2">
|
||||
<UColorModeSwitch :class="[collapsed ? 'mx-auto' : 'ml-3']"/>
|
||||
<UDashboardSidebarCollapse v-if="collapsed" class="mx-auto" />
|
||||
</div>
|
||||
|
||||
<div class="flex-1"/>
|
||||
|
||||
<template #footer>
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
|
||||
<UColorModeToggle class="ml-3"/>
|
||||
<LabelPrinterButton class="w-full"/>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
@@ -268,38 +284,37 @@ onMounted(() => {
|
||||
:key="item.label"
|
||||
color="gray"
|
||||
variant="ghost"
|
||||
class="w-full"
|
||||
class="w-full min-w-0 justify-start rounded-lg px-2.5 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
:icon="item.icon"
|
||||
@click="item.click ? item.click() : null"
|
||||
>
|
||||
{{ item.label }}
|
||||
<span v-if="!collapsed">{{ item.label }}</span>
|
||||
|
||||
<template #trailing>
|
||||
<UBadge v-if="item.badge" color="primary" variant="solid" size="xs">
|
||||
<UBadge v-if="!collapsed && item.badge" color="primary" variant="solid" size="xs">
|
||||
{{ item.badge }}
|
||||
</UBadge>
|
||||
</template>
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<UDivider class="sticky bottom-0 w-full"/>
|
||||
<USeparator class="sticky bottom-0 w-full"/>
|
||||
<UserDropdown style="margin-bottom: env(safe-area-inset-bottom, 10px) !important;" class="w-full"/>
|
||||
</div>
|
||||
</template>
|
||||
</UDashboardSidebar>
|
||||
</UDashboardPanel>
|
||||
|
||||
<UDashboardPage>
|
||||
<UDashboardPanel grow>
|
||||
|
||||
<UDashboardPanel class="flex min-w-0 flex-1 flex-col overflow-hidden">
|
||||
<slot/>
|
||||
|
||||
</UDashboardPanel>
|
||||
</UDashboardPage>
|
||||
</UDashboardGroup>
|
||||
|
||||
<HelpSlideover/>
|
||||
|
||||
<Calculator v-if="calculatorStore.isOpen"/>
|
||||
|
||||
</UDashboardLayout>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -330,7 +345,7 @@ onMounted(() => {
|
||||
</div>
|
||||
<UButton
|
||||
variant="outline"
|
||||
color="rose"
|
||||
color="error"
|
||||
@click="auth.logout()"
|
||||
>Abmelden
|
||||
</UButton>
|
||||
|
||||
@@ -7,7 +7,7 @@ export default defineNuxtConfig({
|
||||
}
|
||||
},
|
||||
|
||||
modules: ['@vite-pwa/nuxt','@pinia/nuxt', '@nuxt/ui', "nuxt-editorjs", '@nuxtjs/fontaine', 'nuxt-viewport', '@nuxtjs/leaflet', '@vueuse/nuxt'],
|
||||
modules: ['@vite-pwa/nuxt','@pinia/nuxt', '@nuxt/ui-pro', "nuxt-editorjs", '@nuxtjs/fontaine', 'nuxt-viewport', '@nuxtjs/leaflet', '@vueuse/nuxt'],
|
||||
|
||||
ssr: false,
|
||||
|
||||
@@ -15,14 +15,12 @@ export default defineNuxtConfig({
|
||||
dirs: ['stores']
|
||||
},
|
||||
|
||||
extends: [
|
||||
'@nuxt/ui-pro'
|
||||
],
|
||||
|
||||
components: [{
|
||||
path: '~/components'
|
||||
}],
|
||||
|
||||
css: ['~/assets/css/main.css'],
|
||||
|
||||
build: {
|
||||
transpile: ['@vuepic/vue-datepicker','@tiptap/vue-3','@tiptap/extension-code-block-lowlight',
|
||||
'lowlight',]
|
||||
@@ -74,10 +72,6 @@ export default defineNuxtConfig({
|
||||
},
|
||||
},
|
||||
|
||||
ui: {
|
||||
icons: ['heroicons', 'mdi', 'simple-icons']
|
||||
},
|
||||
|
||||
colorMode: {
|
||||
preference: 'system'
|
||||
},
|
||||
|
||||
3506
frontend/package-lock.json
generated
3506
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -37,7 +37,8 @@
|
||||
"@fullcalendar/vue3": "^6.1.10",
|
||||
"@iconify/json": "^2.2.171",
|
||||
"@mmote/niimbluelib": "^0.0.1-alpha.29",
|
||||
"@nuxt/ui-pro": "^1.6.0",
|
||||
"@nuxt/ui": "^3.3.7",
|
||||
"@nuxt/ui-pro": "^3.3.7",
|
||||
"@nuxtjs/fontaine": "^0.4.1",
|
||||
"@nuxtjs/google-fonts": "^3.1.0",
|
||||
"@nuxtjs/strapi": "^1.9.3",
|
||||
|
||||
600
frontend/pages/accounting/bwa.vue
Normal file
600
frontend/pages/accounting/bwa.vue
Normal file
@@ -0,0 +1,600 @@
|
||||
<script setup lang="ts">
|
||||
import dayjs from "dayjs"
|
||||
import {
|
||||
getCreatedDocumentTaxBreakdown,
|
||||
getIncomingInvoiceTaxBreakdown
|
||||
} from "~/composables/useTaxEvaluation"
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const loading = ref(true)
|
||||
const createdDocuments = ref<any[]>([])
|
||||
const incomingInvoices = ref<any[]>([])
|
||||
const accounts = ref<any[]>([])
|
||||
const ownAccounts = ref<any[]>([])
|
||||
const statementAllocations = ref<any[]>([])
|
||||
|
||||
const selectedYear = ref(String(dayjs().year()))
|
||||
const selectedMonth = ref("all")
|
||||
|
||||
const monthItems = [
|
||||
{ label: "Ganzes Jahr", value: "all" },
|
||||
{ label: "Januar", value: "1" },
|
||||
{ label: "Februar", value: "2" },
|
||||
{ label: "Maerz", value: "3" },
|
||||
{ label: "April", value: "4" },
|
||||
{ label: "Mai", value: "5" },
|
||||
{ label: "Juni", value: "6" },
|
||||
{ label: "Juli", value: "7" },
|
||||
{ label: "August", value: "8" },
|
||||
{ label: "September", value: "9" },
|
||||
{ label: "Oktober", value: "10" },
|
||||
{ label: "November", value: "11" },
|
||||
{ label: "Dezember", value: "12" }
|
||||
]
|
||||
|
||||
const accountColumns = [
|
||||
{ accessorKey: "gross", header: "Brutto" },
|
||||
{ accessorKey: "net", header: "Netto" },
|
||||
{ accessorKey: "tax", header: "Steuer" },
|
||||
{ accessorKey: "number", header: "Nummer" },
|
||||
{ accessorKey: "label", header: "Konto" },
|
||||
{ accessorKey: "bookings", header: "Buchungen" }
|
||||
]
|
||||
|
||||
const ownAccountColumns = [
|
||||
{ accessorKey: "balance", header: "Saldo" },
|
||||
{ accessorKey: "expenses", header: "Ausgaben" },
|
||||
{ accessorKey: "income", header: "Einnahmen" },
|
||||
{ accessorKey: "number", header: "Nummer" },
|
||||
{ accessorKey: "label", header: "Konto" },
|
||||
{ accessorKey: "bookings", header: "Buchungen" }
|
||||
]
|
||||
|
||||
const isRelevantOutputDocument = (doc: any) => {
|
||||
return doc?.state === "Gebucht" && ["invoices", "advanceInvoices", "cancellationInvoices"].includes(doc?.type)
|
||||
}
|
||||
|
||||
const isRelevantInputInvoice = (invoice: any) => {
|
||||
return invoice?.state === "Gebucht" && !!invoice?.date
|
||||
}
|
||||
|
||||
const sameId = (left: any, right: any) => String(left ?? "") === String(right ?? "")
|
||||
|
||||
const getStatementDate = (allocation: any) => {
|
||||
return allocation?.bankstatement?.date || allocation?.bankstatement?.valueDate || allocation?.date || allocation?.created_at || null
|
||||
}
|
||||
|
||||
const matchesSelectedPeriod = (dateValue: any) => {
|
||||
const parsed = dayjs(dateValue)
|
||||
|
||||
if (!parsed.isValid()) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (String(parsed.year()) !== selectedYear.value) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (selectedMonth.value !== "all" && parsed.month() + 1 !== Number(selectedMonth.value)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const computeDocumentNet = (doc: any) => {
|
||||
return Number((doc?.rows || []).reduce((sum: number, row: any) => {
|
||||
if (!row || ["pagebreak", "title", "text"].includes(row.mode)) {
|
||||
return sum
|
||||
}
|
||||
|
||||
const quantity = Number(row.quantity || 0)
|
||||
const price = Number(row.price || 0)
|
||||
const discountPercent = Number(row.discountPercent || 0)
|
||||
|
||||
return sum + (quantity * price * (1 - discountPercent / 100))
|
||||
}, 0).toFixed(2))
|
||||
}
|
||||
|
||||
const computeIncomingInvoiceGross = (invoice: any) => {
|
||||
return Number((invoice?.accounts || []).reduce((sum: number, account: any) => {
|
||||
const amountNet = Number(account?.amountNet || 0)
|
||||
const amountTax = Number(account?.amountTax || 0)
|
||||
const amountGross = Number(account?.amountGross)
|
||||
|
||||
return sum + (Number.isFinite(amountGross) ? amountGross : amountNet + amountTax)
|
||||
}, 0).toFixed(2))
|
||||
}
|
||||
|
||||
const yearItems = computed(() => {
|
||||
const years = new Set<string>([String(dayjs().year())])
|
||||
|
||||
createdDocuments.value.forEach((doc) => {
|
||||
const parsed = dayjs(doc.documentDate)
|
||||
if (parsed.isValid()) {
|
||||
years.add(String(parsed.year()))
|
||||
}
|
||||
})
|
||||
|
||||
incomingInvoices.value.forEach((invoice) => {
|
||||
const parsed = dayjs(invoice.date)
|
||||
if (parsed.isValid()) {
|
||||
years.add(String(parsed.year()))
|
||||
}
|
||||
})
|
||||
|
||||
statementAllocations.value.forEach((allocation) => {
|
||||
const parsed = dayjs(getStatementDate(allocation))
|
||||
if (parsed.isValid()) {
|
||||
years.add(String(parsed.year()))
|
||||
}
|
||||
})
|
||||
|
||||
return Array.from(years)
|
||||
.sort((a, b) => Number(b) - Number(a))
|
||||
.map((year) => ({ label: year, value: year }))
|
||||
})
|
||||
|
||||
const filteredDocuments = computed(() => {
|
||||
return createdDocuments.value.filter((doc) => matchesSelectedPeriod(doc.documentDate))
|
||||
})
|
||||
|
||||
const filteredIncomingInvoices = computed(() => {
|
||||
return incomingInvoices.value.filter((invoice) => matchesSelectedPeriod(invoice.date))
|
||||
})
|
||||
|
||||
const filteredStatementAllocations = computed(() => {
|
||||
return statementAllocations.value.filter((allocation) => matchesSelectedPeriod(getStatementDate(allocation)))
|
||||
})
|
||||
|
||||
const filteredAccountStatementAllocations = computed(() => {
|
||||
return filteredStatementAllocations.value.filter((allocation) => allocation.account !== null && allocation.account !== undefined)
|
||||
})
|
||||
|
||||
const incomeTotal = computed(() => {
|
||||
return Number(filteredDocuments.value.reduce((sum, doc) => sum + computeDocumentNet(doc), 0).toFixed(2))
|
||||
})
|
||||
|
||||
const expenseNetTotal = computed(() => {
|
||||
const invoiceExpenses = filteredIncomingInvoices.value.reduce((sum, invoice) => {
|
||||
return sum + (invoice.accounts || []).reduce((accountSum: number, account: any) => accountSum + Number(account.amountNet || 0), 0)
|
||||
}, 0)
|
||||
|
||||
const directAccountExpenses = filteredAccountStatementAllocations.value.reduce((sum, allocation) => {
|
||||
const amount = Number(allocation.amount || 0)
|
||||
return amount < 0 ? sum + Math.abs(amount) : sum
|
||||
}, 0)
|
||||
|
||||
return Number((invoiceExpenses + directAccountExpenses).toFixed(2))
|
||||
})
|
||||
|
||||
const expenseGrossTotal = computed(() => {
|
||||
const invoiceExpenses = filteredIncomingInvoices.value.reduce((sum, invoice) => sum + computeIncomingInvoiceGross(invoice), 0)
|
||||
|
||||
const directAccountExpenses = filteredAccountStatementAllocations.value.reduce((sum, allocation) => {
|
||||
const amount = Number(allocation.amount || 0)
|
||||
return amount < 0 ? sum + Math.abs(amount) : sum
|
||||
}, 0)
|
||||
|
||||
return Number((invoiceExpenses + directAccountExpenses).toFixed(2))
|
||||
})
|
||||
|
||||
const taxSummary = computed(() => {
|
||||
const output = filteredDocuments.value.reduce((sum, doc) => {
|
||||
const breakdown = getCreatedDocumentTaxBreakdown(doc)
|
||||
return {
|
||||
net19: sum.net19 + breakdown.net19,
|
||||
tax19: sum.tax19 + breakdown.tax19,
|
||||
net7: sum.net7 + breakdown.net7,
|
||||
tax7: sum.tax7 + breakdown.tax7,
|
||||
net0: sum.net0 + breakdown.net0
|
||||
}
|
||||
}, { net19: 0, tax19: 0, net7: 0, tax7: 0, net0: 0 })
|
||||
|
||||
const input = filteredIncomingInvoices.value.reduce((sum, invoice) => {
|
||||
const breakdown = getIncomingInvoiceTaxBreakdown(invoice)
|
||||
return {
|
||||
net19: sum.net19 + breakdown.net19,
|
||||
tax19: sum.tax19 + breakdown.tax19,
|
||||
net7: sum.net7 + breakdown.net7,
|
||||
tax7: sum.tax7 + breakdown.tax7,
|
||||
net0: sum.net0 + breakdown.net0
|
||||
}
|
||||
}, { net19: 0, tax19: 0, net7: 0, tax7: 0, net0: 0 })
|
||||
|
||||
const outputTax = Number((output.tax19 + output.tax7).toFixed(2))
|
||||
const inputTax = Number((input.tax19 + input.tax7).toFixed(2))
|
||||
|
||||
return {
|
||||
output,
|
||||
input,
|
||||
outputTax,
|
||||
inputTax,
|
||||
balance: Number((outputTax - inputTax).toFixed(2))
|
||||
}
|
||||
})
|
||||
|
||||
const operatingResult = computed(() => {
|
||||
return Number((incomeTotal.value - expenseNetTotal.value).toFixed(2))
|
||||
})
|
||||
|
||||
const incomeDocumentCount = computed(() => filteredDocuments.value.length)
|
||||
const expenseDocumentCount = computed(() => {
|
||||
return filteredIncomingInvoices.value.length + filteredAccountStatementAllocations.value.length
|
||||
})
|
||||
|
||||
const accountRows = computed(() => {
|
||||
return accounts.value
|
||||
.map((account) => {
|
||||
const invoiceBookings = filteredIncomingInvoices.value.flatMap((invoice) => {
|
||||
return (invoice.accounts || [])
|
||||
.filter((invoiceAccount: any) => sameId(invoiceAccount.account?.id || invoiceAccount.account, account.id))
|
||||
.map((invoiceAccount: any) => ({
|
||||
type: "incominginvoice",
|
||||
amountNet: Number(invoiceAccount.amountNet || 0),
|
||||
amountTax: Number(invoiceAccount.amountTax || 0),
|
||||
amountGross: Number.isFinite(Number(invoiceAccount.amountGross))
|
||||
? Number(invoiceAccount.amountGross)
|
||||
: Number(invoiceAccount.amountNet || 0) + Number(invoiceAccount.amountTax || 0)
|
||||
}))
|
||||
})
|
||||
|
||||
const directBookings = filteredAccountStatementAllocations.value
|
||||
.filter((allocation) => sameId(allocation.account?.id || allocation.account, account.id))
|
||||
.map((allocation) => {
|
||||
const amount = Number(allocation.amount || 0)
|
||||
|
||||
return {
|
||||
type: "statementallocation",
|
||||
amountNet: amount,
|
||||
amountTax: 0,
|
||||
amountGross: amount
|
||||
}
|
||||
})
|
||||
|
||||
const bookings = [...invoiceBookings, ...directBookings]
|
||||
|
||||
if (bookings.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const net = bookings.reduce((sum, booking: any) => sum + Number(booking.amountNet || 0), 0)
|
||||
const tax = bookings.reduce((sum, booking: any) => sum + Number(booking.amountTax || 0), 0)
|
||||
const gross = bookings.reduce((sum, booking: any) => {
|
||||
const amountGross = Number(booking.amountGross)
|
||||
return sum + (Number.isFinite(amountGross) ? amountGross : Number(booking.amountNet || 0) + Number(booking.amountTax || 0))
|
||||
}, 0)
|
||||
|
||||
return {
|
||||
id: account.id,
|
||||
number: account.number || "-",
|
||||
label: account.label || account.name || "-",
|
||||
bookings: bookings.length,
|
||||
net: Number(net.toFixed(2)),
|
||||
tax: Number(tax.toFixed(2)),
|
||||
gross: Number(gross.toFixed(2))
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort((left: any, right: any) => Math.abs(Number(right.gross)) - Math.abs(Number(left.gross)))
|
||||
})
|
||||
|
||||
const ownAccountRows = computed(() => {
|
||||
return ownAccounts.value
|
||||
.map((account) => {
|
||||
const bookings = filteredStatementAllocations.value.filter((allocation) => sameId(allocation.ownaccount?.id || allocation.ownaccount, account.id))
|
||||
|
||||
if (bookings.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const income = bookings.reduce((sum, booking) => {
|
||||
const amount = Number(booking.amount || 0)
|
||||
return amount > 0 ? sum + amount : sum
|
||||
}, 0)
|
||||
|
||||
const expenses = bookings.reduce((sum, booking) => {
|
||||
const amount = Number(booking.amount || 0)
|
||||
return amount < 0 ? sum + Math.abs(amount) : sum
|
||||
}, 0)
|
||||
|
||||
const balance = bookings.reduce((sum, booking) => sum + Number(booking.amount || 0), 0)
|
||||
|
||||
return {
|
||||
id: account.id,
|
||||
number: account.number || "-",
|
||||
label: account.name || account.label || "-",
|
||||
bookings: bookings.length,
|
||||
income: Number(income.toFixed(2)),
|
||||
expenses: Number(expenses.toFixed(2)),
|
||||
balance: Number(balance.toFixed(2))
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort((left: any, right: any) => Math.abs(Number(right.balance)) - Math.abs(Number(left.balance)))
|
||||
})
|
||||
|
||||
const setupPage = async () => {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const [docs, invoices, accountItems, ownAccountItems, allocationItems] = await Promise.all([
|
||||
useEntities("createddocuments").select(),
|
||||
useEntities("incominginvoices").select("*, vendor(*)"),
|
||||
useEntities("accounts").selectSpecial(),
|
||||
useEntities("ownaccounts").select(),
|
||||
useEntities("statementallocations").select("*, bankstatement(*), createddocument(*), incominginvoice(*)")
|
||||
])
|
||||
|
||||
createdDocuments.value = (docs || []).filter(isRelevantOutputDocument)
|
||||
incomingInvoices.value = (invoices || []).filter(isRelevantInputInvoice)
|
||||
accounts.value = accountItems || []
|
||||
ownAccounts.value = ownAccountItems || []
|
||||
statementAllocations.value = allocationItems || []
|
||||
|
||||
const firstYear = yearItems.value[0]?.value
|
||||
if (firstYear && !yearItems.value.some((item) => item.value === selectedYear.value)) {
|
||||
selectedYear.value = firstYear
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const openAccount = (rowLike: any) => {
|
||||
const row = rowLike?.original || rowLike
|
||||
if (row?.id) {
|
||||
router.push(`/accounts/show/${row.id}`)
|
||||
}
|
||||
}
|
||||
|
||||
const openOwnAccount = (rowLike: any) => {
|
||||
const row = rowLike?.original || rowLike
|
||||
if (row?.id) {
|
||||
router.push(`/standardEntity/ownaccounts/show/${row.id}`)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(setupPage)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UDashboardNavbar title="BWA">
|
||||
|
||||
</UDashboardNavbar>
|
||||
|
||||
<UDashboardPanelContent class="min-w-0 space-y-6 overflow-x-hidden overflow-y-auto p-4 md:p-6">
|
||||
<div class="flex flex-col gap-3 md:flex-row md:items-end">
|
||||
<UFormField label="Jahr" class="w-full md:w-48">
|
||||
<USelectMenu
|
||||
v-model="selectedYear"
|
||||
:items="yearItems"
|
||||
value-key="value"
|
||||
label-key="label"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<UFormField label="Monat" class="w-full md:w-56">
|
||||
<USelectMenu
|
||||
v-model="selectedMonth"
|
||||
:items="monthItems"
|
||||
value-key="value"
|
||||
label-key="label"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<div class="grid min-w-0 gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<UCard class="min-w-0">
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">Einnahmen netto</div>
|
||||
<div class="mt-2 text-2xl font-semibold">{{ useCurrency(incomeTotal) }}</div>
|
||||
<div class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ incomeDocumentCount }} gebuchte Ausgangsbelege
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<UCard class="min-w-0">
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">Ausgaben netto</div>
|
||||
<div class="mt-2 text-2xl font-semibold">{{ useCurrency(expenseNetTotal) }}</div>
|
||||
<div class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Brutto: {{ useCurrency(expenseGrossTotal) }}
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<UCard class="min-w-0">
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">Einnahmen Belege</div>
|
||||
<div class="mt-2 text-2xl font-semibold">{{ incomeDocumentCount }}</div>
|
||||
<div class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Ausgangsbelege im Zeitraum
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<UCard class="min-w-0">
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">Ausgaben Belege</div>
|
||||
<div class="mt-2 text-2xl font-semibold">{{ expenseDocumentCount }}</div>
|
||||
<div class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Eingangsbelege plus direkte Buchungen
|
||||
</div>
|
||||
</UCard>
|
||||
</div>
|
||||
|
||||
<div class="grid min-w-0 gap-4 md:grid-cols-2">
|
||||
<UCard class="min-w-0">
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">Betriebsergebnis</div>
|
||||
<div class="mt-2 text-2xl font-semibold" :class="operatingResult >= 0 ? 'text-primary-500' : 'text-error'">
|
||||
{{ useCurrency(operatingResult) }}
|
||||
</div>
|
||||
<div class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Einnahmen minus Ausgaben netto
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<UCard class="min-w-0">
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">USt-Saldo</div>
|
||||
<div class="mt-2 text-2xl font-semibold" :class="taxSummary.balance >= 0 ? 'text-amber-600 dark:text-amber-400' : 'text-primary-500'">
|
||||
{{ useCurrency(taxSummary.balance) }}
|
||||
</div>
|
||||
<div class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
USt {{ useCurrency(taxSummary.outputTax) }} | Vorsteuer {{ useCurrency(taxSummary.inputTax) }}
|
||||
</div>
|
||||
</UCard>
|
||||
</div>
|
||||
|
||||
<div class="grid min-w-0 gap-4 xl:grid-cols-2">
|
||||
<UCard class="min-w-0">
|
||||
<template #header>
|
||||
<div class="font-semibold">USt-Details</div>
|
||||
</template>
|
||||
|
||||
<div class="space-y-3 text-sm">
|
||||
<div class="flex items-center justify-between">
|
||||
<span>Netto 19% Ausgangsbelege</span>
|
||||
<span>{{ useCurrency(taxSummary.output.net19) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>USt 19%</span>
|
||||
<span>{{ useCurrency(taxSummary.output.tax19) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>Netto 7% Ausgangsbelege</span>
|
||||
<span>{{ useCurrency(taxSummary.output.net7) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>USt 7%</span>
|
||||
<span>{{ useCurrency(taxSummary.output.tax7) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>Steuerfrei</span>
|
||||
<span>{{ useCurrency(taxSummary.output.net0 + taxSummary.input.net0) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<UCard class="min-w-0">
|
||||
<template #header>
|
||||
<div class="font-semibold">Vorsteuer-Details</div>
|
||||
</template>
|
||||
|
||||
<div class="space-y-3 text-sm">
|
||||
<div class="flex items-center justify-between">
|
||||
<span>Netto 19% Eingangsbelege</span>
|
||||
<span>{{ useCurrency(taxSummary.input.net19) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>Vorsteuer 19%</span>
|
||||
<span>{{ useCurrency(taxSummary.input.tax19) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>Netto 7% Eingangsbelege</span>
|
||||
<span>{{ useCurrency(taxSummary.input.net7) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>Vorsteuer 7%</span>
|
||||
<span>{{ useCurrency(taxSummary.input.tax7) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>Steuerfrei</span>
|
||||
<span>{{ useCurrency(taxSummary.input.net0) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
</div>
|
||||
|
||||
<div class="grid min-w-0 gap-4 xl:grid-cols-2">
|
||||
<UCard class="min-w-0">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<span class="font-semibold">Buchungskonten</span>
|
||||
<UBadge color="neutral" variant="soft">{{ accountRows.length }}</UBadge>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="min-w-0">
|
||||
<UTable
|
||||
:data="accountRows"
|
||||
:columns="normalizeTableColumns(accountColumns)"
|
||||
:loading="loading"
|
||||
:on-select="openAccount"
|
||||
>
|
||||
<template #empty>
|
||||
<div class="flex flex-col items-center justify-center py-10 text-center">
|
||||
<UIcon name="i-heroicons-circle-stack-20-solid" class="mb-2 h-10 w-10 text-gray-400" />
|
||||
<p class="font-medium">Keine Buchungskonten im ausgewaehlten Zeitraum</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #label-cell="{ row }">
|
||||
<div class="truncate font-medium">{{ row.original.label }}</div>
|
||||
</template>
|
||||
|
||||
<template #bookings-cell="{ row }">
|
||||
<div class="text-right">{{ row.original.bookings }}</div>
|
||||
</template>
|
||||
|
||||
<template #net-cell="{ row }">
|
||||
<div class="text-right tabular-nums">{{ useCurrency(row.original.net) }}</div>
|
||||
</template>
|
||||
|
||||
<template #tax-cell="{ row }">
|
||||
<div class="text-right tabular-nums">{{ useCurrency(row.original.tax) }}</div>
|
||||
</template>
|
||||
|
||||
<template #gross-cell="{ row }">
|
||||
<div class="text-right font-medium tabular-nums">{{ useCurrency(row.original.gross) }}</div>
|
||||
</template>
|
||||
</UTable>
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<UCard class="min-w-0">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<span class="font-semibold">Eigene Buchungskonten</span>
|
||||
<UBadge color="neutral" variant="soft">{{ ownAccountRows.length }}</UBadge>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="min-w-0">
|
||||
<UTable
|
||||
:data="ownAccountRows"
|
||||
:columns="normalizeTableColumns(ownAccountColumns)"
|
||||
:loading="loading"
|
||||
:on-select="openOwnAccount"
|
||||
>
|
||||
<template #empty>
|
||||
<div class="flex flex-col items-center justify-center py-10 text-center">
|
||||
<UIcon name="i-heroicons-circle-stack-20-solid" class="mb-2 h-10 w-10 text-gray-400" />
|
||||
<p class="font-medium">Keine eigenen Buchungen im ausgewaehlten Zeitraum</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #label-cell="{ row }">
|
||||
<div class="truncate font-medium">{{ row.original.label }}</div>
|
||||
</template>
|
||||
|
||||
<template #bookings-cell="{ row }">
|
||||
<div class="text-right">{{ row.original.bookings }}</div>
|
||||
</template>
|
||||
|
||||
<template #income-cell="{ row }">
|
||||
<div class="text-right text-primary-500 tabular-nums">{{ useCurrency(row.original.income) }}</div>
|
||||
</template>
|
||||
|
||||
<template #expenses-cell="{ row }">
|
||||
<div class="text-right text-error tabular-nums">{{ useCurrency(row.original.expenses) }}</div>
|
||||
</template>
|
||||
|
||||
<template #balance-cell="{ row }">
|
||||
<div class="text-right font-medium tabular-nums" :class="row.original.balance >= 0 ? 'text-primary-500' : 'text-error'">
|
||||
{{ useCurrency(row.original.balance) }}
|
||||
</div>
|
||||
</template>
|
||||
</UTable>
|
||||
</div>
|
||||
</UCard>
|
||||
</div>
|
||||
</UDashboardPanelContent>
|
||||
</template>
|
||||
@@ -126,164 +126,162 @@ onMounted(loadData)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<UDashboardNavbar title="USt-Auswertung">
|
||||
<template #right>
|
||||
<UButton
|
||||
icon="i-heroicons-arrow-path"
|
||||
variant="outline"
|
||||
@click="loadData"
|
||||
:loading="loading"
|
||||
<UDashboardNavbar title="USt-Auswertung">
|
||||
<template #right>
|
||||
<UButton
|
||||
icon="i-heroicons-arrow-path"
|
||||
variant="outline"
|
||||
@click="loadData"
|
||||
:loading="loading"
|
||||
>
|
||||
Aktualisieren
|
||||
</UButton>
|
||||
</template>
|
||||
</UDashboardNavbar>
|
||||
|
||||
<UDashboardPanelContent class="p-4 md:p-6">
|
||||
<div class="mb-6 flex flex-col gap-2">
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Aktueller Zeitraum: {{ currentPeriod?.label }}
|
||||
</h2>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Intervall: {{ periodType === "monthly" ? "monatlich" : periodType === "quarterly" ? "quartalsweise" : "jährlich" }}.
|
||||
Berücksichtigt werden gebuchte Ausgangsrechnungen, Abschlags- und Stornorechnungen sowie gebuchte Eingangsbelege mit Datum.
|
||||
</p>
|
||||
<p v-if="currentPeriod" class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ currentPeriod.range }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="currentPeriod" class="grid gap-4 md:grid-cols-3">
|
||||
<UCard>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">Berechnete USt</div>
|
||||
<div class="mt-2 text-2xl font-semibold text-gray-900 dark:text-white">
|
||||
{{ formatCurrency(currentPeriod.outputTax) }}
|
||||
</div>
|
||||
<div class="mt-3 text-sm text-gray-500 dark:text-gray-400">
|
||||
19%: {{ formatCurrency(currentPeriod.output.tax19) }} | 7%: {{ formatCurrency(currentPeriod.output.tax7) }}
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<UCard>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">Vorsteuer</div>
|
||||
<div class="mt-2 text-2xl font-semibold text-gray-900 dark:text-white">
|
||||
{{ formatCurrency(currentPeriod.inputTax) }}
|
||||
</div>
|
||||
<div class="mt-3 text-sm text-gray-500 dark:text-gray-400">
|
||||
19%: {{ formatCurrency(currentPeriod.input.tax19) }} | 7%: {{ formatCurrency(currentPeriod.input.tax7) }}
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<UCard>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">Verrechnetes Ergebnis</div>
|
||||
<div
|
||||
class="mt-2 text-2xl font-semibold"
|
||||
:class="currentPeriod.balance >= 0 ? 'text-amber-600 dark:text-amber-400' : 'text-emerald-600 dark:text-emerald-400'"
|
||||
>
|
||||
Aktualisieren
|
||||
</UButton>
|
||||
</template>
|
||||
</UDashboardNavbar>
|
||||
{{ formatCurrency(currentPeriod.balance) }}
|
||||
</div>
|
||||
<div class="mt-3 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ currentPeriod.outputCount }} Ausgangsbelege | {{ currentPeriod.inputCount }} Eingangsbelege
|
||||
</div>
|
||||
</UCard>
|
||||
</div>
|
||||
|
||||
<UDashboardPanelContent class="p-4 md:p-6">
|
||||
<div class="mb-6 flex flex-col gap-2">
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Aktueller Zeitraum: {{ currentPeriod?.label }}
|
||||
</h2>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Intervall: {{ periodType === "monthly" ? "monatlich" : periodType === "quarterly" ? "quartalsweise" : "jährlich" }}.
|
||||
Berücksichtigt werden gebuchte Ausgangsrechnungen, Abschlags- und Stornorechnungen sowie gebuchte Eingangsbelege mit Datum.
|
||||
</p>
|
||||
<p v-if="currentPeriod" class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ currentPeriod.range }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="currentPeriod" class="grid gap-4 md:grid-cols-3">
|
||||
<UCard>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">Berechnete USt</div>
|
||||
<div class="mt-2 text-2xl font-semibold text-gray-900 dark:text-white">
|
||||
{{ formatCurrency(currentPeriod.outputTax) }}
|
||||
</div>
|
||||
<div class="mt-3 text-sm text-gray-500 dark:text-gray-400">
|
||||
19%: {{ formatCurrency(currentPeriod.output.tax19) }} | 7%: {{ formatCurrency(currentPeriod.output.tax7) }}
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<UCard>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">Vorsteuer</div>
|
||||
<div class="mt-2 text-2xl font-semibold text-gray-900 dark:text-white">
|
||||
{{ formatCurrency(currentPeriod.inputTax) }}
|
||||
</div>
|
||||
<div class="mt-3 text-sm text-gray-500 dark:text-gray-400">
|
||||
19%: {{ formatCurrency(currentPeriod.input.tax19) }} | 7%: {{ formatCurrency(currentPeriod.input.tax7) }}
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<UCard>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">Verrechnetes Ergebnis</div>
|
||||
<div
|
||||
class="mt-2 text-2xl font-semibold"
|
||||
:class="currentPeriod.balance >= 0 ? 'text-amber-600 dark:text-amber-400' : 'text-emerald-600 dark:text-emerald-400'"
|
||||
>
|
||||
{{ formatCurrency(currentPeriod.balance) }}
|
||||
</div>
|
||||
<div class="mt-3 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ currentPeriod.outputCount }} Ausgangsbelege | {{ currentPeriod.inputCount }} Eingangsbelege
|
||||
</div>
|
||||
</UCard>
|
||||
</div>
|
||||
|
||||
<div v-if="currentPeriod" class="mt-6 grid gap-4 xl:grid-cols-2">
|
||||
<UCard>
|
||||
<template #header>
|
||||
<div class="font-semibold">Ausgangsrechnungen</div>
|
||||
</template>
|
||||
<div class="space-y-3 text-sm">
|
||||
<div class="flex items-center justify-between">
|
||||
<span>Netto 19%</span>
|
||||
<span>{{ formatCurrency(currentPeriod.output.net19) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>USt 19%</span>
|
||||
<span>{{ formatCurrency(currentPeriod.output.tax19) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>Netto 7%</span>
|
||||
<span>{{ formatCurrency(currentPeriod.output.net7) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>USt 7%</span>
|
||||
<span>{{ formatCurrency(currentPeriod.output.tax7) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>Netto 0%</span>
|
||||
<span>{{ formatCurrency(currentPeriod.output.net0) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<UCard>
|
||||
<template #header>
|
||||
<div class="font-semibold">Eingangsbelege</div>
|
||||
</template>
|
||||
<div class="space-y-3 text-sm">
|
||||
<div class="flex items-center justify-between">
|
||||
<span>Netto 19%</span>
|
||||
<span>{{ formatCurrency(currentPeriod.input.net19) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>Vorsteuer 19%</span>
|
||||
<span>{{ formatCurrency(currentPeriod.input.tax19) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>Netto 7%</span>
|
||||
<span>{{ formatCurrency(currentPeriod.input.net7) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>Vorsteuer 7%</span>
|
||||
<span>{{ formatCurrency(currentPeriod.input.tax7) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>Netto 0%</span>
|
||||
<span>{{ formatCurrency(currentPeriod.input.net0) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
</div>
|
||||
|
||||
<UCard class="mt-6">
|
||||
<div v-if="currentPeriod" class="mt-6 grid gap-4 xl:grid-cols-2">
|
||||
<UCard>
|
||||
<template #header>
|
||||
<div class="font-semibold">Aktueller und vorherige Zeiträume</div>
|
||||
<div class="font-semibold">Ausgangsrechnungen</div>
|
||||
</template>
|
||||
<div class="space-y-3 text-sm">
|
||||
<div class="flex items-center justify-between">
|
||||
<span>Netto 19%</span>
|
||||
<span>{{ formatCurrency(currentPeriod.output.net19) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>USt 19%</span>
|
||||
<span>{{ formatCurrency(currentPeriod.output.tax19) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>Netto 7%</span>
|
||||
<span>{{ formatCurrency(currentPeriod.output.net7) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>USt 7%</span>
|
||||
<span>{{ formatCurrency(currentPeriod.output.tax7) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>Netto 0%</span>
|
||||
<span>{{ formatCurrency(currentPeriod.output.net0) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<UCard>
|
||||
<template #header>
|
||||
<div class="font-semibold">Eingangsbelege</div>
|
||||
</template>
|
||||
<div class="space-y-3 text-sm">
|
||||
<div class="flex items-center justify-between">
|
||||
<span>Netto 19%</span>
|
||||
<span>{{ formatCurrency(currentPeriod.input.net19) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>Vorsteuer 19%</span>
|
||||
<span>{{ formatCurrency(currentPeriod.input.tax19) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>Netto 7%</span>
|
||||
<span>{{ formatCurrency(currentPeriod.input.net7) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>Vorsteuer 7%</span>
|
||||
<span>{{ formatCurrency(currentPeriod.input.tax7) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>Netto 0%</span>
|
||||
<span>{{ formatCurrency(currentPeriod.input.net0) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
</div>
|
||||
|
||||
<UCard class="mt-6">
|
||||
<template #header>
|
||||
<div class="font-semibold">Aktueller und vorherige Zeiträume</div>
|
||||
</template>
|
||||
|
||||
<UTable
|
||||
:columns="normalizeTableColumns(columns)"
|
||||
:data="periods"
|
||||
:loading="loading"
|
||||
:empty="{ icon: 'i-heroicons-calculator', label: 'Keine Daten für die USt-Auswertung vorhanden' }"
|
||||
>
|
||||
<template #label-cell="{ row }">
|
||||
<div class="flex items-center gap-2">
|
||||
<span>{{ row.original.label }}</span>
|
||||
<UBadge v-if="row.original.isCurrent" color="primary" variant="soft">Aktuell</UBadge>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<UTable
|
||||
:columns="columns"
|
||||
:rows="periods"
|
||||
:loading="loading"
|
||||
:empty-state="{ icon: 'i-heroicons-calculator', label: 'Keine Daten für die USt-Auswertung vorhanden' }"
|
||||
>
|
||||
<template #label-data="{ row }">
|
||||
<div class="flex items-center gap-2">
|
||||
<span>{{ row.label }}</span>
|
||||
<UBadge v-if="row.isCurrent" color="primary" variant="soft">Aktuell</UBadge>
|
||||
</div>
|
||||
</template>
|
||||
<template #outputTax-cell="{ row }">
|
||||
{{ formatCurrency(row.original.outputTax) }}
|
||||
</template>
|
||||
|
||||
<template #outputTax-data="{ row }">
|
||||
{{ formatCurrency(row.outputTax) }}
|
||||
</template>
|
||||
<template #inputTax-cell="{ row }">
|
||||
{{ formatCurrency(row.original.inputTax) }}
|
||||
</template>
|
||||
|
||||
<template #inputTax-data="{ row }">
|
||||
{{ formatCurrency(row.inputTax) }}
|
||||
</template>
|
||||
|
||||
<template #balance-data="{ row }">
|
||||
<span :class="row.balance >= 0 ? 'text-amber-600 dark:text-amber-400 font-medium' : 'text-emerald-600 dark:text-emerald-400 font-medium'">
|
||||
{{ formatCurrency(row.balance) }}
|
||||
<template #balance-cell="{ row }">
|
||||
<span :class="row.original.balance >= 0 ? 'text-amber-600 dark:text-amber-400 font-medium' : 'text-emerald-600 dark:text-emerald-400 font-medium'">
|
||||
{{ formatCurrency(row.original.balance) }}
|
||||
</span>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template #documents-data="{ row }">
|
||||
{{ row.outputCount }} / {{ row.inputCount }}
|
||||
</template>
|
||||
</UTable>
|
||||
</UCard>
|
||||
</UDashboardPanelContent>
|
||||
</div>
|
||||
<template #documents-cell="{ row }">
|
||||
{{ row.original.outputCount }} / {{ row.original.inputCount }}
|
||||
</template>
|
||||
</UTable>
|
||||
</UCard>
|
||||
</UDashboardPanelContent>
|
||||
</template>
|
||||
|
||||
@@ -158,7 +158,7 @@ setupPage()
|
||||
<UButton
|
||||
icon="i-heroicons-x-mark"
|
||||
variant="outline"
|
||||
color="rose"
|
||||
color="error"
|
||||
@click="clearSearchString()"
|
||||
v-if="searchString.length > 0"
|
||||
/>
|
||||
@@ -194,20 +194,20 @@ setupPage()
|
||||
</template>
|
||||
</UDashboardToolbar>
|
||||
<UTable
|
||||
:rows="filteredRows"
|
||||
:columns="columns"
|
||||
:data="filteredRows"
|
||||
:columns="normalizeTableColumns(columns)"
|
||||
class="w-full"
|
||||
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
|
||||
@select="(i) => router.push(`/accounts/show/${i.id}`)"
|
||||
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Buchungen anzuzeigen' }"
|
||||
:on-select="(row) => router.push(`/accounts/show/${row.original.id}`)"
|
||||
:empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Buchungen anzuzeigen' }"
|
||||
>
|
||||
<template #allocations-data="{row}">
|
||||
<span v-if="dataLoaded">{{row.allocations ? row.allocations : null}}</span>
|
||||
<template #allocations-cell="{row}">
|
||||
<span v-if="dataLoaded">{{row.original.allocations ? row.original.allocations : null}}</span>
|
||||
<USkeleton v-else class="h-4 w-[250px]" />
|
||||
|
||||
</template>
|
||||
<template #saldo-data="{row}">
|
||||
<span v-if="dataLoaded">{{row.allocations ? useCurrency(row.saldo) : null}}</span>
|
||||
<template #saldo-cell="{row}">
|
||||
<span v-if="dataLoaded">{{row.original.allocations ? useCurrency(row.original.saldo) : null}}</span>
|
||||
<USkeleton v-else class="h-4 w-[250px]" />
|
||||
</template>
|
||||
</UTable>
|
||||
@@ -219,4 +219,4 @@ setupPage()
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,62 +1,44 @@
|
||||
<script setup>
|
||||
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const itemInfo = ref(null)
|
||||
const statementallocations = ref([])
|
||||
const incominginvoices = ref([])
|
||||
const currentAccountId = computed(() => String(route.params.id))
|
||||
const sameAccount = (value) => String(value ?? "") === currentAccountId.value
|
||||
|
||||
const setup = async () => {
|
||||
itemInfo.value = (await useEntities("accounts").selectSpecial("*")).find(i => i.id === Number(route.params.id))
|
||||
statementallocations.value = (await useEntities("statementallocations").select("*, bs_id(*)")).filter(i => i.account === Number(route.params.id))
|
||||
incominginvoices.value = (await useEntities("incominginvoices").select("*, vendor(*)")).filter(i => i.accounts.find(x => x.account === Number(route.params.id)))
|
||||
statementallocations.value = (await useEntities("statementallocations").select("*, bankstatement(*)"))
|
||||
.filter((allocation) => sameAccount(allocation.account?.id || allocation.account))
|
||||
incominginvoices.value = (await useEntities("incominginvoices").select("*, vendor(*)"))
|
||||
.filter((invoice) => (invoice.accounts || []).some((account) => sameAccount(account.account?.id || account.account)))
|
||||
}
|
||||
|
||||
setup()
|
||||
|
||||
const selectAllocation = (allocation) => {
|
||||
if(allocation.type === "statementallocation") {
|
||||
router.push(`/banking/statements/edit/${allocation.bs_id.id}`)
|
||||
} else if(allocation.type === "incominginvoice") {
|
||||
router.push(`/incominginvoices/show/${allocation.incominginvoiceid}`)
|
||||
}
|
||||
}
|
||||
|
||||
const renderedAllocations = computed(() => {
|
||||
const statementRows = statementallocations.value.map((allocation) => ({
|
||||
...allocation,
|
||||
type: "statementallocation",
|
||||
amount: Number(allocation.amount || 0)
|
||||
}))
|
||||
|
||||
let tempstatementallocations = statementallocations.value.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 : '')) : ''
|
||||
}
|
||||
const incomingInvoiceRows = incominginvoices.value.flatMap((invoice) => {
|
||||
return (invoice.accounts || [])
|
||||
.filter((account) => sameAccount(account.account?.id || account.account))
|
||||
.map((account, index) => ({
|
||||
id: `${invoice.id}-${index}`,
|
||||
incominginvoiceid: invoice.id,
|
||||
type: "incominginvoice",
|
||||
amount: Number(account.amountGross || account.amountNet || 0),
|
||||
expense: invoice.expense
|
||||
}))
|
||||
})
|
||||
|
||||
|
||||
let incominginvoicesallocations = []
|
||||
|
||||
incominginvoices.value.forEach(i => {
|
||||
|
||||
incominginvoicesallocations.push(...i.accounts.filter(x => x.account == route.params.id).map(x => {
|
||||
return {
|
||||
...x,
|
||||
incominginvoiceid: i.id,
|
||||
type: "incominginvoice",
|
||||
amount: x.amountGross ? x.amountGross : x.amountNet,
|
||||
date: i.date,
|
||||
partner: i.vendor.name,
|
||||
description: i.description,
|
||||
color: i.expense ? "red" : "green",
|
||||
expense: i.expense
|
||||
}
|
||||
}))
|
||||
})
|
||||
|
||||
return [...tempstatementallocations, ... incominginvoicesallocations]
|
||||
return [...statementRows, ...incomingInvoiceRows]
|
||||
})
|
||||
|
||||
const saldo = computed(() => {
|
||||
@@ -106,7 +88,7 @@ const saldo = computed(() => {
|
||||
</UDashboardNavbar>
|
||||
<UDashboardPanelContent>
|
||||
<UTabs :items="[{label: 'Information'},{label: 'Buchungen'}]">
|
||||
<template #item="{item}">
|
||||
<template #content="{item}">
|
||||
<UCard class="mt-5" v-if="item.label === 'Information'">
|
||||
<div class="text-wrap">
|
||||
<table class="w-full" v-if="itemInfo">
|
||||
@@ -135,25 +117,12 @@ const saldo = computed(() => {
|
||||
</div>
|
||||
</UCard>
|
||||
<UCard class="mt-5" v-if="item.label === 'Buchungen'">
|
||||
<UTable
|
||||
v-if="statementallocations"
|
||||
:rows="renderedAllocations"
|
||||
:columns="[{key:'amount', label:'Betrag'},{key:'date', label:'Datum'},{key:'partner', label:'Partner'},{key:'description', label:'Beschreibung'}]"
|
||||
@select="(i) => selectAllocation(i)"
|
||||
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Buchungen anzuzeigen' }"
|
||||
>
|
||||
<template #amount-data="{row}">
|
||||
<span class="text-right text-rose-600" v-if="row.amount < 0 || row.color === 'red'">{{useCurrency(row.amount)}}</span>
|
||||
<span class="text-right text-primary-500" v-else-if="row.amount > 0 || row.color === 'green'">{{useCurrency(row.amount)}}</span>
|
||||
<span v-else>{{useCurrency(row.amount)}}</span>
|
||||
</template>
|
||||
<template #date-data="{row}">
|
||||
{{row.date ? dayjs(row.date).format('DD.MM.YYYY') : ''}}
|
||||
</template>
|
||||
<template #description-data="{row}">
|
||||
{{row.description ? row.description : ''}}
|
||||
</template>
|
||||
</UTable>
|
||||
<EntityShowSubOwnAccountsStatements
|
||||
v-if="itemInfo"
|
||||
:item="itemInfo"
|
||||
top-level-type="accounts"
|
||||
platform="desktop"
|
||||
/>
|
||||
</UCard>
|
||||
</template>
|
||||
</UTabs>
|
||||
@@ -167,4 +136,4 @@ td {
|
||||
padding-bottom: 0.15em;
|
||||
padding-top: 0.15em;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -504,7 +504,7 @@ onMounted(() => {
|
||||
placeholder="Konten"
|
||||
class="w-48"
|
||||
/>
|
||||
<UDivider orientation="vertical" class="h-6"/>
|
||||
<USeparator orientation="vertical" class="h-6"/>
|
||||
<div class="flex items-center gap-2">
|
||||
<USelectMenu
|
||||
v-model="selectedPeriod"
|
||||
@@ -601,17 +601,18 @@ onMounted(() => {
|
||||
</div>
|
||||
<PageLeaveGuard :when="isSyncing"/>
|
||||
|
||||
<UModal v-model="suggestionsModalOpen" :ui="{ width: 'sm:max-w-6xl' }">
|
||||
<UCard>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-lg font-semibold">Vorschlaege fuer Bankbuchungen</div>
|
||||
<div class="text-sm text-gray-500">Direkte Zuweisung von passenden Rechnungen und Eingangsbelegen</div>
|
||||
<UModal v-model:open="suggestionsModalOpen" :ui="{ width: 'sm:max-w-6xl' }">
|
||||
<template #content>
|
||||
<UCard>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-lg font-semibold">Vorschlaege fuer Bankbuchungen</div>
|
||||
<div class="text-sm text-gray-500">Direkte Zuweisung von passenden Rechnungen und Eingangsbelegen</div>
|
||||
</div>
|
||||
<UBadge color="primary" variant="subtle">{{ suggestionCount }}</UBadge>
|
||||
</div>
|
||||
<UBadge color="primary" variant="subtle">{{ suggestionCount }}</UBadge>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<div v-if="rowsWithSuggestions.length > 0" class="grid grid-cols-1 lg:grid-cols-3 gap-4 min-h-[520px]">
|
||||
<div class="lg:col-span-1 border rounded-lg overflow-hidden dark:border-gray-800">
|
||||
@@ -633,7 +634,7 @@ onMounted(() => {
|
||||
</div>
|
||||
<div class="mt-2 flex flex-wrap gap-1">
|
||||
<UBadge v-if="entry.suggestions?.topDocument" size="xs" color="emerald" variant="subtle">Rechnung</UBadge>
|
||||
<UBadge v-if="entry.suggestions?.topIncomingInvoice" size="xs" color="rose" variant="subtle">Eingangsbeleg</UBadge>
|
||||
<UBadge v-if="entry.suggestions?.topIncomingInvoice" size="xs" color="error" variant="subtle">Eingangsbeleg</UBadge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -690,7 +691,7 @@ onMounted(() => {
|
||||
</UBadge>
|
||||
</div>
|
||||
</div>
|
||||
<UButton size="sm" color="rose" @click="handleAssignIncomingInvoice(selectedSuggestionRow.row, selectedSuggestionRow.suggestions.topIncomingInvoice, $event)">
|
||||
<UButton size="sm" color="error" @click="handleAssignIncomingInvoice(selectedSuggestionRow.row, selectedSuggestionRow.suggestions.topIncomingInvoice, $event)">
|
||||
Zuweisen
|
||||
</UButton>
|
||||
<UButton size="sm" color="gray" variant="ghost" @click="dismissSuggestion(selectedSuggestionRow.row, selectedSuggestionRow.suggestions.topIncomingInvoice.suggestionKey, $event)">
|
||||
@@ -701,10 +702,11 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="py-10 text-center text-gray-400">
|
||||
<UIcon name="i-heroicons-sparkles" class="w-10 h-10 mx-auto mb-2 opacity-30"/>
|
||||
<p>Keine Vorschlaege fuer die aktuelle Filterung vorhanden.</p>
|
||||
</div>
|
||||
</UCard>
|
||||
<div v-else class="py-10 text-center text-gray-400">
|
||||
<UIcon name="i-heroicons-sparkles" class="w-10 h-10 mx-auto mb-2 opacity-30"/>
|
||||
<p>Keine Vorschlaege fuer die aktuelle Filterung vorhanden.</p>
|
||||
</div>
|
||||
</UCard>
|
||||
</template>
|
||||
</UModal>
|
||||
</template>
|
||||
|
||||
@@ -438,7 +438,7 @@ setup()
|
||||
<UBadge v-else color="amber" variant="subtle">Offen</UBadge>
|
||||
</template>
|
||||
<template #right>
|
||||
<ArchiveButton color="rose" variant="outline" type="bankstatements" @confirmed="archiveStatement"/>
|
||||
<ArchiveButton color="error" variant="outline" type="bankstatements" @confirmed="archiveStatement"/>
|
||||
</template>
|
||||
</UDashboardNavbar>
|
||||
|
||||
@@ -552,7 +552,7 @@ setup()
|
||||
<div class="font-mono text-sm font-semibold">{{ displayCurrency(item.amount) }}</div>
|
||||
<UButton
|
||||
icon="i-heroicons-trash"
|
||||
color="rose"
|
||||
color="error"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
class="opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
@@ -571,14 +571,14 @@ setup()
|
||||
class="p-4 bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800 shadow-sm shrink-0 z-10">
|
||||
<div class="grid grid-cols-12 gap-4 items-end">
|
||||
<div class="col-span-12 md:col-span-3">
|
||||
<UFormGroup label="Betrag" size="sm">
|
||||
<UFormField label="Betrag" size="sm">
|
||||
<UInput v-model="manualAllocationSum" type="number" step="0.01">
|
||||
<template #trailing><span class="text-gray-500 text-xs">EUR</span></template>
|
||||
</UInput>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
</div>
|
||||
<div class="col-span-12 md:col-span-5">
|
||||
<UFormGroup label="Konto / Manuelle Buchung" size="sm">
|
||||
<UFormField label="Konto / Manuelle Buchung" size="sm">
|
||||
<div class="flex gap-1">
|
||||
<USelectMenu
|
||||
class="w-full"
|
||||
@@ -607,7 +607,7 @@ setup()
|
||||
/>
|
||||
</UTooltip>
|
||||
</div>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
</div>
|
||||
<div class="col-span-12 md:col-span-4 flex justify-end gap-2 pb-0.5">
|
||||
<UButton variant="soft" color="gray" icon="i-heroicons-adjustments-horizontal"
|
||||
@@ -747,7 +747,7 @@ setup()
|
||||
</div>
|
||||
<UButton
|
||||
size="xs"
|
||||
color="rose"
|
||||
color="error"
|
||||
@click="saveAllocation({incominginvoice: invoice.id, bankstatement: itemInfo.id, amount: Number(Math.abs(getInvoiceSum(invoice,true)) > Math.abs(manualAllocationSum) ? manualAllocationSum : getInvoiceSum(invoice,true)), description: allocationDescription || 'Automatischer Vorschlag'})"
|
||||
>
|
||||
Beleg zuweisen
|
||||
@@ -867,7 +867,7 @@ setup()
|
||||
v-if="!itemInfo.statementallocations.find(i => i.incominginvoice === item.id)"
|
||||
icon="i-heroicons-check"
|
||||
size="sm"
|
||||
color="rose"
|
||||
color="error"
|
||||
variant="soft"
|
||||
@click="saveAllocation({incominginvoice: item.id, bankstatement: itemInfo.id, amount: Number(Math.abs(getInvoiceSum(item,true)) > Math.abs(manualAllocationSum) ? manualAllocationSum : getInvoiceSum(item,true)), description: allocationDescription})"
|
||||
/>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,7 +16,7 @@
|
||||
</UInput>
|
||||
<UButton
|
||||
v-if="searchString.length > 0"
|
||||
color="rose"
|
||||
color="error"
|
||||
icon="i-heroicons-x-mark"
|
||||
variant="outline"
|
||||
@click="clearSearchString()"
|
||||
@@ -55,82 +55,82 @@
|
||||
{{ getRowsForTab(item.key).length }}
|
||||
</UBadge>
|
||||
</template>
|
||||
<template #item="{item}">
|
||||
<template #content="{item}">
|
||||
<div style="height: 80vh; overflow-y: scroll">
|
||||
<UTable
|
||||
:columns="getColumnsForTab(item.key)"
|
||||
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Belege anzuzeigen' }"
|
||||
:rows="getRowsForTab(item.key)"
|
||||
:columns="normalizeTableColumns(getColumnsForTab(item.key))"
|
||||
:empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Belege anzuzeigen' }"
|
||||
:data="getRowsForTab(item.key)"
|
||||
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
|
||||
class="w-full"
|
||||
@select="selectItem"
|
||||
:on-select="selectItem"
|
||||
>
|
||||
<template #type-data="{row}">
|
||||
<span v-if="row.type === 'cancellationInvoices'" class="text-cyan-500">{{
|
||||
dataStore.documentTypesForCreation[row.type].labelSingle
|
||||
}} für {{ filteredRows.find(i => row.createddocument?.id === i.id)?.documentNumber }}</span>
|
||||
<span v-else>{{ dataStore.documentTypesForCreation[row.type].labelSingle }}</span>
|
||||
<template #type-cell="{row}">
|
||||
<span v-if="row.original.type === 'cancellationInvoices'" class="text-cyan-500">{{
|
||||
dataStore.documentTypesForCreation[row.original.type].labelSingle
|
||||
}} für {{ filteredRows.find(i => row.original.createddocument?.id === i.id)?.documentNumber }}</span>
|
||||
<span v-else>{{ dataStore.documentTypesForCreation[row.original.type].labelSingle }}</span>
|
||||
</template>
|
||||
|
||||
<template #state-data="{row}">
|
||||
<span v-if="row.state === 'Entwurf'" class="text-rose-500">{{ row.state }}</span>
|
||||
<template #state-cell="{row}">
|
||||
<span v-if="row.original.state === 'Entwurf'" class="text-rose-500">{{ row.original.state }}</span>
|
||||
<span
|
||||
v-if="row.state === 'Gebucht' && !hasCancellationInvoice(row)"
|
||||
v-if="row.original.state === 'Gebucht' && !hasCancellationInvoice(row.original)"
|
||||
class="text-primary-500"
|
||||
>
|
||||
{{ row.state }}
|
||||
{{ row.original.state }}
|
||||
</span>
|
||||
<span
|
||||
v-else-if="row.state === 'Gebucht' && hasCancellationInvoice(row) && ['invoices','advanceInvoices'].includes(row.type)"
|
||||
v-else-if="row.original.state === 'Gebucht' && hasCancellationInvoice(row.original) && ['invoices','advanceInvoices'].includes(row.original.type)"
|
||||
class="text-cyan-500"
|
||||
>
|
||||
Storniert mit {{ getCancellationInvoice(row)?.documentNumber }}
|
||||
Storniert mit {{ getCancellationInvoice(row.original)?.documentNumber }}
|
||||
</span>
|
||||
<span v-else-if="row.state === 'Gebucht'" class="text-primary-500">{{ row.state }}</span>
|
||||
<span v-else-if="row.original.state === 'Gebucht'" class="text-primary-500">{{ row.original.state }}</span>
|
||||
</template>
|
||||
|
||||
<template #partner-data="{row}">
|
||||
<span v-if="row.customer && row.customer.name.length < 21">{{ row.customer ? row.customer.name : "" }}</span>
|
||||
<UTooltip v-else-if="row.customer && row.customer.name.length > 20" :text="row.customer.name">
|
||||
{{ row.customer.name.substring(0, 20) }}...
|
||||
<template #partner-cell="{row}">
|
||||
<span v-if="row.original.customer && row.original.customer.name.length < 21">{{ row.original.customer ? row.original.customer.name : "" }}</span>
|
||||
<UTooltip v-else-if="row.original.customer && row.original.customer.name.length > 20" :text="row.original.customer.name">
|
||||
{{ row.original.customer.name.substring(0, 20) }}...
|
||||
</UTooltip>
|
||||
</template>
|
||||
|
||||
<template #reference-data="{row}">
|
||||
<span v-if="row === filteredRows[selectedItem]" class="text-primary-500 font-bold">{{ row.documentNumber }}</span>
|
||||
<span v-else>{{ row.documentNumber }}</span>
|
||||
<template #reference-cell="{row}">
|
||||
<span v-if="row.original === filteredRows[selectedItem]" class="text-primary-500 font-bold">{{ row.original.documentNumber }}</span>
|
||||
<span v-else>{{ row.original.documentNumber }}</span>
|
||||
</template>
|
||||
|
||||
<template #date-data="{row}">
|
||||
<span v-if="row.date">{{ row.date ? dayjs(row.date).format("DD.MM.YY") : '' }}</span>
|
||||
<span v-if="row.documentDate">{{ row.documentDate ? dayjs(row.documentDate).format("DD.MM.YY") : '' }}</span>
|
||||
<template #date-cell="{row}">
|
||||
<span v-if="row.original.date">{{ row.original.date ? dayjs(row.original.date).format("DD.MM.YY") : '' }}</span>
|
||||
<span v-if="row.original.documentDate">{{ row.original.documentDate ? dayjs(row.original.documentDate).format("DD.MM.YY") : '' }}</span>
|
||||
</template>
|
||||
|
||||
<template #dueDate-data="{row}">
|
||||
<template #dueDate-cell="{row}">
|
||||
<span
|
||||
v-if="row.state === 'Gebucht' && row.paymentDays && ['invoices','advanceInvoices'].includes(row.type) && !hasCancellationInvoice(row)"
|
||||
:class="dayjs(row.documentDate).add(row.paymentDays,'day').diff(dayjs()) <= 0 && !isPaid(row) ? ['text-rose-500'] : '' "
|
||||
v-if="row.original.state === 'Gebucht' && row.original.paymentDays && ['invoices','advanceInvoices'].includes(row.original.type) && !hasCancellationInvoice(row.original)"
|
||||
:class="dayjs(row.original.documentDate).add(row.original.paymentDays,'day').diff(dayjs()) <= 0 && !isPaid(row.original) ? ['text-rose-500'] : '' "
|
||||
>
|
||||
{{ row.documentDate ? dayjs(row.documentDate).add(row.paymentDays, 'day').format("DD.MM.YY") : '' }}
|
||||
{{ row.original.documentDate ? dayjs(row.original.documentDate).add(row.original.paymentDays, 'day').format("DD.MM.YY") : '' }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #paid-data="{row}">
|
||||
<template #paid-cell="{row}">
|
||||
<div
|
||||
v-if="(row.type === 'invoices' ||row.type === 'advanceInvoices') && row.state === 'Gebucht' && !hasCancellationInvoice(row)">
|
||||
<span v-if="useSum().getIsPaid(row,items)" class="text-primary-500">Bezahlt</span>
|
||||
v-if="(row.original.type === 'invoices' ||row.original.type === 'advanceInvoices') && row.original.state === 'Gebucht' && !hasCancellationInvoice(row.original)">
|
||||
<span v-if="useSum().getIsPaid(row.original,items)" class="text-primary-500">Bezahlt</span>
|
||||
<span v-else class="text-rose-600">Offen</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #amount-data="{row}">
|
||||
<span v-if="row.type !== 'deliveryNotes'">{{ displayCurrency(useSum().getCreatedDocumentSum(row, items)) }}</span>
|
||||
<template #amount-cell="{row}">
|
||||
<span v-if="row.original.type !== 'deliveryNotes'">{{ displayCurrency(useSum().getCreatedDocumentSum(row.original, items)) }}</span>
|
||||
</template>
|
||||
|
||||
<template #amountOpen-data="{row}">
|
||||
<template #amountOpen-cell="{row}">
|
||||
<span
|
||||
v-if="!['deliveryNotes','cancellationInvoices','quotes','confirmationOrders'].includes(row.type) && row.state !== 'Entwurf' && !hasCancellationInvoice(row) && !useSum().getIsPaid(row,items) ">
|
||||
{{ displayCurrency(useSum().getCreatedDocumentOpenAmount(row, items)) }}
|
||||
v-if="!['deliveryNotes','cancellationInvoices','quotes','confirmationOrders'].includes(row.original.type) && row.original.state !== 'Entwurf' && !hasCancellationInvoice(row.original) && !useSum().getIsPaid(row.original,items) ">
|
||||
{{ displayCurrency(useSum().getCreatedDocumentOpenAmount(row.original, items)) }}
|
||||
</span>
|
||||
</template>
|
||||
</UTable>
|
||||
@@ -246,7 +246,15 @@ const types = computed(() => {
|
||||
return templateTypes.filter((type) => selectedTypes.value.find(i => i.key === type.key))
|
||||
})
|
||||
|
||||
const selectItem = (item) => {
|
||||
const unwrapSelectedRow = (itemLike) => itemLike?.original || itemLike
|
||||
|
||||
const selectItem = (itemLike) => {
|
||||
const item = unwrapSelectedRow(itemLike)
|
||||
|
||||
if (!item?.id) {
|
||||
return
|
||||
}
|
||||
|
||||
if (item.state === "Entwurf") {
|
||||
router.push(`/createDocument/edit/${item.id}`)
|
||||
} else {
|
||||
|
||||
@@ -44,15 +44,15 @@
|
||||
<USelectMenu
|
||||
v-model="selectedFilters"
|
||||
icon="i-heroicons-adjustments-horizontal-solid"
|
||||
:options="filterOptions"
|
||||
option-attribute="name"
|
||||
value-attribute="name"
|
||||
:items="filterOptions"
|
||||
label-key="name"
|
||||
value-key="name"
|
||||
multiple
|
||||
class="hidden lg:block"
|
||||
:color="selectedFilters.length > 0 ? 'primary' : 'white'"
|
||||
:ui-menu="{ width: 'min-w-max' }"
|
||||
:content="{ width: 'min-w-max' }"
|
||||
>
|
||||
<template #label>
|
||||
<template #default>
|
||||
Filter
|
||||
</template>
|
||||
</USelectMenu>
|
||||
@@ -93,16 +93,16 @@
|
||||
</div>
|
||||
|
||||
<UTable
|
||||
:rows="filteredRows"
|
||||
:columns="columns"
|
||||
:data="filteredRows"
|
||||
:columns="normalizeTableColumns(columns)"
|
||||
class="w-full"
|
||||
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
|
||||
@select="(row) => router.push(`/createDocument/edit/${row.id}`)"
|
||||
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Belege anzuzeigen' }"
|
||||
:on-select="(row) => router.push(`/createDocument/edit/${row.id}`)"
|
||||
empty="Keine Belege anzuzeigen"
|
||||
>
|
||||
<template #actions-data="{ row }">
|
||||
<template #actions-cell="{ row }">
|
||||
<div @click.stop>
|
||||
<UDropdown :items="getActionItems(row)" :popper="{ placement: 'bottom-end' }">
|
||||
<UDropdown :items="getActionItems(row.original)" :popper="{ placement: 'bottom-end' }">
|
||||
<UButton
|
||||
color="gray"
|
||||
variant="ghost"
|
||||
@@ -112,53 +112,54 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #type-data="{row}">
|
||||
{{dataStore.documentTypesForCreation[row.type].labelSingle}}
|
||||
<template #type-cell="{row}">
|
||||
{{dataStore.documentTypesForCreation[row.original.type].labelSingle}}
|
||||
</template>
|
||||
<template #partner-data="{row}">
|
||||
<span v-if="row.customer">{{row.customer ? row.customer.name : ""}}</span>
|
||||
<template #partner-cell="{row}">
|
||||
<span v-if="row.original.customer">{{row.original.customer ? row.original.customer.name : ""}}</span>
|
||||
|
||||
</template>
|
||||
<template #amount-data="{row}">
|
||||
{{displayCurrency(calculateDocSum(row))}}
|
||||
<template #amount-cell="{row}">
|
||||
{{displayCurrency(calculateDocSum(row.original))}}
|
||||
</template>
|
||||
<template #serialConfig.active-data="{row}">
|
||||
<span v-if="row.serialConfig.active" class="text-primary">Ja</span>
|
||||
<template #serialConfig.active-cell="{row}">
|
||||
<span v-if="row.original.serialConfig.active" class="text-primary">Ja</span>
|
||||
<span v-else class="text-rose-600">Nein</span>
|
||||
</template>
|
||||
<template #contract-data="{row}">
|
||||
<span v-if="row.contract">{{row.contract.contractNumber}} - {{row.contract.name}}</span>
|
||||
<template #contract-cell="{row}">
|
||||
<span v-if="row.original.contract">{{row.original.contract.contractNumber}} - {{row.original.contract.name}}</span>
|
||||
</template>
|
||||
<template #serialConfig.intervall-data="{row}">
|
||||
<span v-if="row.serialConfig?.intervall === 'monatlich'">Monatlich</span>
|
||||
<span v-if="row.serialConfig?.intervall === 'vierteljährlich'">Quartalsweise</span>
|
||||
<template #serialConfig.intervall-cell="{row}">
|
||||
<span v-if="row.original.serialConfig?.intervall === 'monatlich'">Monatlich</span>
|
||||
<span v-if="row.original.serialConfig?.intervall === 'vierteljährlich'">Quartalsweise</span>
|
||||
</template>
|
||||
<template #payment_type-data="{row}">
|
||||
<span v-if="row.payment_type === 'transfer'">Überweisung</span>
|
||||
<span v-else-if="row.payment_type === 'direct-debit'">SEPA - Einzug</span>
|
||||
<template #payment_type-cell="{row}">
|
||||
<span v-if="row.original.payment_type === 'transfer'">Überweisung</span>
|
||||
<span v-else-if="row.original.payment_type === 'direct-debit'">SEPA - Einzug</span>
|
||||
</template>
|
||||
</UTable>
|
||||
|
||||
<UModal v-model="showExecutionModal" :ui="{ width: 'sm:max-w-4xl' }">
|
||||
<UCard>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
||||
Serienrechnungen manuell ausführen
|
||||
</h3>
|
||||
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="showExecutionModal = false" />
|
||||
</div>
|
||||
</template>
|
||||
<UModal v-model:open="showExecutionModal" :ui="{ width: 'sm:max-w-4xl' }">
|
||||
<template #content>
|
||||
<UCard>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
||||
Serienrechnungen manuell ausführen
|
||||
</h3>
|
||||
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="showExecutionModal = false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="space-y-4">
|
||||
<UFormGroup label="Ausführungsdatum (Belegdatum)" help="Dieses Datum steuert auch den Leistungszeitraum (z.B. Vormonat bei 'Rückwirkend').">
|
||||
<UFormField label="Ausführungsdatum (Belegdatum)" help="Dieses Datum steuert auch den Leistungszeitraum (z.B. Vormonat bei 'Rückwirkend').">
|
||||
<div class="flex items-center gap-2">
|
||||
<UInput type="date" v-model="executionDate" class="flex-1" />
|
||||
<UButton color="gray" variant="soft" label="Heute" @click="setExecutionDateToToday" />
|
||||
</div>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UDivider label="Vorlagen auswählen" />
|
||||
<USeparator label="Vorlagen auswählen" />
|
||||
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
||||
<div class="flex flex-col sm:flex-row gap-2 w-full sm:w-auto">
|
||||
@@ -172,9 +173,9 @@
|
||||
|
||||
<USelectMenu
|
||||
v-model="selectedExecutionIntervall"
|
||||
:options="executionIntervallOptions"
|
||||
option-attribute="label"
|
||||
value-attribute="value"
|
||||
:items="executionIntervallOptions"
|
||||
label-key="label"
|
||||
value-key="value"
|
||||
size="sm"
|
||||
class="w-full sm:w-52"
|
||||
/>
|
||||
@@ -204,24 +205,24 @@
|
||||
<div class="max-h-96 overflow-y-auto border border-gray-200 dark:border-gray-800 rounded-md">
|
||||
<UTable
|
||||
v-model="selectedExecutionRows"
|
||||
:rows="filteredExecutionList"
|
||||
:columns="executionColumns"
|
||||
:data="filteredExecutionList"
|
||||
:columns="normalizeTableColumns(executionColumns)"
|
||||
:ui="{ th: { base: 'whitespace-nowrap' } }"
|
||||
>
|
||||
<template #partner-data="{row}">
|
||||
{{row.customer ? row.customer.name : "-"}}
|
||||
<template #partner-cell="{row}">
|
||||
{{row.original.customer ? row.original.customer.name : "-"}}
|
||||
</template>
|
||||
<template #amount-data="{row}">
|
||||
{{displayCurrency(calculateDocSum(row))}}
|
||||
<template #amount-cell="{row}">
|
||||
{{displayCurrency(calculateDocSum(row.original))}}
|
||||
</template>
|
||||
<template #serialConfig.intervall-data="{row}">
|
||||
{{ getIntervallLabel(row.serialConfig?.intervall) }}
|
||||
<template #serialConfig.intervall-cell="{row}">
|
||||
{{ getIntervallLabel(row.original.serialConfig?.intervall) }}
|
||||
</template>
|
||||
<template #contract-data="{row}">
|
||||
{{row.contract?.contractNumber}} - {{row.contract?.name}}
|
||||
<template #contract-cell="{row}">
|
||||
{{row.original.contract?.contractNumber}} - {{row.original.contract?.name}}
|
||||
</template>
|
||||
<template #plant-data="{row}">
|
||||
{{ row.plant?.name || "-" }}
|
||||
<template #plant-cell="{row}">
|
||||
{{ row.original.plant?.name || "-" }}
|
||||
</template>
|
||||
</UTable>
|
||||
</div>
|
||||
@@ -231,58 +232,61 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<UButton color="white" @click="showExecutionModal = false">Abbrechen</UButton>
|
||||
<UButton
|
||||
color="primary"
|
||||
:loading="isExecuting"
|
||||
:disabled="selectedExecutionRows.length === 0"
|
||||
@click="executeSerialInvoices"
|
||||
>
|
||||
Jetzt ausführen
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<UButton color="white" @click="showExecutionModal = false">Abbrechen</UButton>
|
||||
<UButton
|
||||
color="primary"
|
||||
:loading="isExecuting"
|
||||
:disabled="selectedExecutionRows.length === 0"
|
||||
@click="executeSerialInvoices"
|
||||
>
|
||||
Jetzt ausführen
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
</template>
|
||||
</UModal>
|
||||
|
||||
<USlideover v-model="showExecutionsSlideover" :ui="{ width: 'w-screen max-w-md' }">
|
||||
<UCard class="flex flex-col flex-1" :ui="{ body: { base: 'flex-1' }, ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
||||
Alle Ausführungen
|
||||
</h3>
|
||||
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="showExecutionsSlideover = false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="space-y-4 overflow-y-auto h-full p-1">
|
||||
<div v-if="executionsLoading" class="flex justify-center py-4">
|
||||
<UIcon name="i-heroicons-arrow-path" class="animate-spin text-gray-400 w-6 h-6" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="completedExecutions.length === 0" class="text-center text-gray-500 py-8">
|
||||
Keine abgeschlossenen Ausführungen gefunden.
|
||||
</div>
|
||||
|
||||
<div v-for="exec in completedExecutions" :key="exec.id" class="border border-gray-200 dark:border-gray-700 rounded-lg p-3">
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<span class="text-sm font-semibold">{{ dayjs(exec.createdAt).format('DD.MM.YYYY HH:mm') }}</span>
|
||||
<UBadge :color="getStatusColor(exec.status)" variant="subtle" size="xs">
|
||||
{{ getStatusLabel(exec.status) }}
|
||||
</UBadge>
|
||||
<USlideover v-model:open="showExecutionsSlideover" :ui="{ width: 'w-screen max-w-md' }">
|
||||
<template #body>
|
||||
<UCard class="flex flex-col flex-1" :ui="{ body: { base: 'flex-1' }, ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
||||
Alle Ausführungen
|
||||
</h3>
|
||||
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="showExecutionsSlideover = false" />
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 grid grid-cols-2 gap-2 mt-2">
|
||||
<div>
|
||||
<UIcon name="i-heroicons-check-circle" class="text-green-500 w-3 h-3 align-text-bottom" />
|
||||
{{exec.summary}}
|
||||
</template>
|
||||
|
||||
<div class="space-y-4 overflow-y-auto h-full p-1">
|
||||
<div v-if="executionsLoading" class="flex justify-center py-4">
|
||||
<UIcon name="i-heroicons-arrow-path" class="animate-spin text-gray-400 w-6 h-6" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="completedExecutions.length === 0" class="text-center text-gray-500 py-8">
|
||||
Keine abgeschlossenen Ausführungen gefunden.
|
||||
</div>
|
||||
|
||||
<div v-for="exec in completedExecutions" :key="exec.id" class="border border-gray-200 dark:border-gray-700 rounded-lg p-3">
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<span class="text-sm font-semibold">{{ dayjs(exec.createdAt).format('DD.MM.YYYY HH:mm') }}</span>
|
||||
<UBadge :color="getStatusColor(exec.status)" variant="subtle" size="xs">
|
||||
{{ getStatusLabel(exec.status) }}
|
||||
</UBadge>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 grid grid-cols-2 gap-2 mt-2">
|
||||
<div>
|
||||
<UIcon name="i-heroicons-check-circle" class="text-green-500 w-3 h-3 align-text-bottom" />
|
||||
{{exec.summary}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
</UCard>
|
||||
</template>
|
||||
</USlideover>
|
||||
|
||||
</template>
|
||||
|
||||
@@ -94,7 +94,7 @@ const openBankstatements = () => {
|
||||
<UButton
|
||||
@click="router.push(`/createDocument/edit/?createddocument=${itemInfo.id}&loadMode=storno`)"
|
||||
variant="outline"
|
||||
color="rose"
|
||||
color="error"
|
||||
:disabled="links.find(i => i.type === 'cancellationInvoices')"
|
||||
>
|
||||
Stornieren
|
||||
|
||||
@@ -13,6 +13,7 @@ const itemInfo = ref({
|
||||
})
|
||||
const showDocument = ref(false)
|
||||
const uri = ref("")
|
||||
const openTab = ref("0")
|
||||
|
||||
const setupPage = async () => {
|
||||
letterheads.value = await useEntities("letterheads").select("*")
|
||||
@@ -23,7 +24,9 @@ const setupPage = async () => {
|
||||
setupPage()
|
||||
|
||||
const onChangeTab = (index) => {
|
||||
if(index === 1) {
|
||||
openTab.value = String(index)
|
||||
|
||||
if(String(index) === "1") {
|
||||
generateDocument()
|
||||
}
|
||||
}
|
||||
@@ -78,8 +81,8 @@ const contentChanged = (content) => {
|
||||
<UDashboardNavbar title="Anschreiben bearbeiten"/>
|
||||
{{itemInfo}}
|
||||
<UDashboardPanelContent>
|
||||
<UTabs @change="onChangeTab" :items="[{label: 'Editor'},{label: 'Vorschau'}]">
|
||||
<template #item="{item}">
|
||||
<UTabs v-model="openTab" @update:model-value="onChangeTab" :items="[{label: 'Editor'},{label: 'Vorschau'}]">
|
||||
<template #content="{item}">
|
||||
<div v-if="item.label === 'Editor'">
|
||||
<Tiptap
|
||||
class="mt-3"
|
||||
|
||||
@@ -162,7 +162,7 @@ const sendEmail = async () => {
|
||||
|
||||
|
||||
if(!res.success) {
|
||||
toast.add({title: "Fehler beim Absenden der E-Mail", color: "rose"})
|
||||
toast.add({title: "Fehler beim Absenden der E-Mail", color: "error"})
|
||||
|
||||
} else {
|
||||
navigateTo("/")
|
||||
@@ -210,34 +210,34 @@ const sendEmail = async () => {
|
||||
|
||||
<div class="scrollContainer mt-3">
|
||||
<div class="flex-col flex w-full">
|
||||
<UFormGroup
|
||||
<UFormField
|
||||
label="Absender"
|
||||
>
|
||||
<USelectMenu
|
||||
:options="emailAccounts"
|
||||
option-attribute="email"
|
||||
value-attribute="id"
|
||||
:items="emailAccounts"
|
||||
label-key="email"
|
||||
value-key="id"
|
||||
v-model="emailData.account"
|
||||
/>
|
||||
</UFormGroup>
|
||||
<UDivider class="my-3"/>
|
||||
<UFormGroup
|
||||
</UFormField>
|
||||
<USeparator class="my-3"/>
|
||||
<UFormField
|
||||
label="Empfänger"
|
||||
>
|
||||
<UInput
|
||||
class="w-full my-1"
|
||||
v-model="emailData.to"
|
||||
/>
|
||||
</UFormGroup>
|
||||
<UFormGroup
|
||||
</UFormField>
|
||||
<UFormField
|
||||
label="Kopie"
|
||||
>
|
||||
<UInput
|
||||
class="w-full my-1"
|
||||
v-model="emailData.cc"
|
||||
/>
|
||||
</UFormGroup>
|
||||
<UFormGroup
|
||||
</UFormField>
|
||||
<UFormField
|
||||
label="Blindkopie"
|
||||
>
|
||||
<UInput
|
||||
@@ -245,17 +245,17 @@ const sendEmail = async () => {
|
||||
placeholder=""
|
||||
v-model="emailData.bcc"
|
||||
/>
|
||||
</UFormGroup>
|
||||
<UFormGroup
|
||||
</UFormField>
|
||||
<UFormField
|
||||
label="Betreff"
|
||||
>
|
||||
<UInput
|
||||
class="w-full my-1"
|
||||
v-model="emailData.subject"
|
||||
/>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
</div>
|
||||
<UDivider class="my-3"/>
|
||||
<USeparator class="my-3"/>
|
||||
<div id="parentAttachments" class="flex flex-col justify-center mt-3">
|
||||
<span class="font-medium mb-2 text-xl">Anhänge</span>
|
||||
<!-- <UIcon
|
||||
|
||||
@@ -39,7 +39,7 @@ const createExport = async () => {
|
||||
:loading="true"
|
||||
v-model="selected"
|
||||
:loading-state="{ icon: 'i-heroicons-arrow-path-20-solid', label: 'Loading...' }"
|
||||
:rows="createddocuments" />
|
||||
:data="createddocuments" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -89,7 +89,7 @@ const createExport = async () => {
|
||||
if(res.success) {
|
||||
toast.add({title: "Export wird erstellt. Sie erhalten eine Benachrichtigung sobald es soweit ist."})
|
||||
} else {
|
||||
toast.add({title: "Es gab einen Fehler beim erstellen", color: "rose"})
|
||||
toast.add({title: "Es gab einen Fehler beim erstellen", color: "error"})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -110,38 +110,39 @@ const createExport = async () => {
|
||||
</UDashboardNavbar>
|
||||
|
||||
<UTable
|
||||
:rows="filteredExports"
|
||||
:columns="[
|
||||
:data="filteredExports"
|
||||
:columns="normalizeTableColumns([
|
||||
{ key: 'created_at', label: 'Erstellt am' },
|
||||
{ key: 'start_date', label: 'Start' },
|
||||
{ key: 'end_date', label: 'Ende' },
|
||||
{ key: 'valid_until', label: 'Gültig bis' },
|
||||
{ key: 'type', label: 'Typ' },
|
||||
{ key: 'download', label: 'Download' },
|
||||
]"
|
||||
])"
|
||||
>
|
||||
<template #created_at-data="{row}">
|
||||
{{dayjs(row.created_at).format("DD.MM.YYYY HH:mm")}}
|
||||
<template #created_at-cell="{row}">
|
||||
{{dayjs(row.original.created_at).format("DD.MM.YYYY HH:mm")}}
|
||||
</template>
|
||||
<template #start_date-data="{row}">
|
||||
{{dayjs(row.start_date).format("DD.MM.YYYY HH:mm")}}
|
||||
<template #start_date-cell="{row}">
|
||||
{{dayjs(row.original.start_date).format("DD.MM.YYYY HH:mm")}}
|
||||
</template>
|
||||
<template #end_date-data="{row}">
|
||||
{{dayjs(row.end_date).format("DD.MM.YYYY HH:mm")}}
|
||||
<template #end_date-cell="{row}">
|
||||
{{dayjs(row.original.end_date).format("DD.MM.YYYY HH:mm")}}
|
||||
</template>
|
||||
<template #valid_until-data="{row}">
|
||||
{{dayjs(row.valid_until).format("DD.MM.YYYY HH:mm")}}
|
||||
<template #valid_until-cell="{row}">
|
||||
{{dayjs(row.original.valid_until).format("DD.MM.YYYY HH:mm")}}
|
||||
</template>
|
||||
<template #download-data="{row}">
|
||||
<UButton @click="downloadFile(row)">Download</UButton>
|
||||
<template #download-cell="{row}">
|
||||
<UButton @click="downloadFile(row.original)">Download</UButton>
|
||||
</template>
|
||||
</UTable>
|
||||
|
||||
<UModal v-model="showCreateExportModal">
|
||||
<UCard>
|
||||
<template #header>
|
||||
Export erstellen
|
||||
</template>
|
||||
<UModal v-model:open="showCreateExportModal">
|
||||
<template #content>
|
||||
<UCard>
|
||||
<template #header>
|
||||
Export erstellen
|
||||
</template>
|
||||
|
||||
<div class="mb-6 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-100 dark:border-gray-700">
|
||||
<div class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">Schnellauswahl</div>
|
||||
@@ -180,7 +181,7 @@ const createExport = async () => {
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<UFormGroup label="Start:" class="flex-1">
|
||||
<UFormField label="Start:" class="flex-1">
|
||||
<UPopover :popper="{ placement: 'bottom-start' }">
|
||||
<UButton
|
||||
icon="i-heroicons-calendar-days-20-solid"
|
||||
@@ -192,9 +193,9 @@ const createExport = async () => {
|
||||
<LazyDatePicker v-model="createExportData.start_date" @close="close" />
|
||||
</template>
|
||||
</UPopover>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup label="Ende:" class="flex-1">
|
||||
<UFormField label="Ende:" class="flex-1">
|
||||
<UPopover :popper="{ placement: 'bottom-start' }">
|
||||
<UButton
|
||||
icon="i-heroicons-calendar-days-20-solid"
|
||||
@@ -206,21 +207,22 @@ const createExport = async () => {
|
||||
<LazyDatePicker v-model="createExportData.end_date" @close="close" />
|
||||
</template>
|
||||
</UPopover>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end">
|
||||
<UButton @click="createExport">
|
||||
Erstellen
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="flex justify-end">
|
||||
<UButton @click="createExport">
|
||||
Erstellen
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</UCard>
|
||||
</UCard>
|
||||
</template>
|
||||
</UModal>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -350,8 +350,8 @@ const syncdokubox = async () => {
|
||||
<UBreadcrumb :links="breadcrumbLinks"/>
|
||||
</template>
|
||||
<template #right>
|
||||
<USelectMenu v-model="displayMode" :options="displayModes" value-attribute="key" class="w-32" :ui-menu="{ zIndex: 'z-50' }">
|
||||
<template #label>
|
||||
<USelectMenu v-model="displayMode" :items="displayModes" value-key="key" class="w-32" :content="{ zIndex: 'z-50' }">
|
||||
<template #default>
|
||||
<UIcon :name="displayModes.find(i => i.key === displayMode).icon" class="w-4 h-4"/>
|
||||
<span>{{ displayModes.find(i => i.key === displayMode).label }}</span>
|
||||
</template>
|
||||
@@ -453,30 +453,31 @@ const syncdokubox = async () => {
|
||||
</div>
|
||||
</UDashboardPanelContent>
|
||||
|
||||
<UModal v-model="createFolderModalOpen">
|
||||
<UCard>
|
||||
<template #header><h3 class="font-bold">Ordner erstellen</h3></template>
|
||||
<UModal v-model:open="createFolderModalOpen">
|
||||
<template #content>
|
||||
<UCard>
|
||||
<template #header><h3 class="font-bold">Ordner erstellen</h3></template>
|
||||
|
||||
<div class="space-y-4">
|
||||
<UFormGroup label="Name" required>
|
||||
<UFormField label="Name" required>
|
||||
<UInput v-model="createFolderData.name" autofocus @keyup.enter="createFolder"/>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup
|
||||
<UFormField
|
||||
label="Standard Dateityp (Tag)"
|
||||
:help="isParentTypeMandatory ? 'Vom übergeordneten Ordner vorgegeben' : ''"
|
||||
>
|
||||
<USelectMenu
|
||||
v-model="createFolderData.standardFiletype"
|
||||
:options="filetags"
|
||||
value-attribute="id"
|
||||
option-attribute="name"
|
||||
:items="filetags"
|
||||
value-key="id"
|
||||
label-key="name"
|
||||
placeholder="Kein Standardtyp"
|
||||
searchable
|
||||
clear-search-on-close
|
||||
:disabled="isParentTypeMandatory"
|
||||
/>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UCheckbox
|
||||
v-model="createFolderData.standardFiletypeIsOptional"
|
||||
@@ -485,27 +486,30 @@ const syncdokubox = async () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<UButton color="gray" @click="createFolderModalOpen = false">Abbrechen</UButton>
|
||||
<UButton color="primary" @click="createFolder">Erstellen</UButton>
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<UButton color="gray" @click="createFolderModalOpen = false">Abbrechen</UButton>
|
||||
<UButton color="primary" @click="createFolder">Erstellen</UButton>
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
</template>
|
||||
</UModal>
|
||||
|
||||
<UModal v-model="renameModalOpen">
|
||||
<UCard>
|
||||
<template #header><h3 class="font-bold">Umbenennen</h3></template>
|
||||
<UFormGroup label="Neuer Name">
|
||||
<UInput v-model="renameData.name" autofocus @keyup.enter="updateName"/>
|
||||
</UFormGroup>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<UButton color="gray" @click="renameModalOpen = false">Abbrechen</UButton>
|
||||
<UButton color="primary" @click="updateName">Speichern</UButton>
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
<UModal v-model:open="renameModalOpen">
|
||||
<template #content>
|
||||
<UCard>
|
||||
<template #header><h3 class="font-bold">Umbenennen</h3></template>
|
||||
<UFormField label="Neuer Name">
|
||||
<UInput v-model="renameData.name" autofocus @keyup.enter="updateName"/>
|
||||
</UFormField>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<UButton color="gray" @click="renameModalOpen = false">Abbrechen</UButton>
|
||||
<UButton color="primary" @click="updateName">Speichern</UButton>
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
</template>
|
||||
</UModal>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
import InputGroup from "~/components/InputGroup.vue";
|
||||
import dayjs from "dayjs";
|
||||
import { parseDate } from "@internationalized/date"
|
||||
import { useDraggable } from '@vueuse/core'
|
||||
|
||||
// --- Standard Setup & Data ---
|
||||
@@ -44,6 +45,9 @@ const costcentres = ref([])
|
||||
const vendors = ref([])
|
||||
const accounts = ref([])
|
||||
const loadedFileId = ref(null)
|
||||
const invoiceFiles = ref([])
|
||||
const paymentTypeItems = ['Überweisung', 'Lastschrift', 'Kreditkarte', 'PayPal', 'Bar', 'Sonstiges']
|
||||
const files = useFiles()
|
||||
|
||||
const setup = async () => {
|
||||
// 1. Daten laden
|
||||
@@ -67,7 +71,9 @@ const setup = async () => {
|
||||
|
||||
// Datei laden
|
||||
if (itemInfo.value.files && itemInfo.value.files.length > 0) {
|
||||
loadedFileId.value = itemInfo.value.files[itemInfo.value.files.length-1].id
|
||||
invoiceFiles.value = await files.selectSomeDocuments(itemInfo.value.files.map((file) => file.id))
|
||||
const latestPdf = [...invoiceFiles.value].reverse().find((file) => file?.path?.toLowerCase().includes('.pdf'))
|
||||
loadedFileId.value = latestPdf?.id || null
|
||||
}
|
||||
|
||||
if(itemInfo.value.date && !itemInfo.value.dueDate) itemInfo.value.dueDate = itemInfo.value.date
|
||||
@@ -98,6 +104,23 @@ const taxOptions = ref([
|
||||
{ label: "Keine USt", percentage: 0, key: "null" },
|
||||
])
|
||||
|
||||
const getCalendarValue = (value) => {
|
||||
if (!value) return undefined
|
||||
|
||||
const formatted = dayjs(value).format('YYYY-MM-DD')
|
||||
return formatted ? parseDate(formatted) : undefined
|
||||
}
|
||||
|
||||
const setDateField = (field, value) => {
|
||||
itemInfo.value[field] = value ? dayjs(value.toString()).toDate() : null
|
||||
}
|
||||
|
||||
const setDateFieldToToday = (field) => {
|
||||
itemInfo.value[field] = dayjs().toDate()
|
||||
}
|
||||
|
||||
const getDateButtonLabel = (value, emptyLabel = "Kein Datum") => value ? dayjs(value).format('DD.MM.YYYY') : emptyLabel
|
||||
|
||||
const totalCalculated = computed(() => {
|
||||
let totalNet = 0
|
||||
let totalAmount19Tax = 0
|
||||
@@ -157,7 +180,7 @@ const updateIncomingInvoice = async (setBooked = false) => {
|
||||
toast.add({
|
||||
title: "Buchen nicht möglich",
|
||||
description: "Bitte beheben Sie zuerst die rot markierten Pflichtfehler.",
|
||||
color: "rose"
|
||||
color: "error"
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -220,7 +243,7 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
|
||||
<template #right>
|
||||
<ArchiveButton
|
||||
v-if="mode !== 'show'"
|
||||
color="rose"
|
||||
color="error"
|
||||
variant="outline"
|
||||
type="incominginvoices"
|
||||
@confirmed="useEntities('incominginvoices').archive(route.params.id)"
|
||||
@@ -286,7 +309,7 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
|
||||
<UAlert
|
||||
v-if="findIncomingInvoiceErrors.length > 0"
|
||||
title="Prüfung erforderlich"
|
||||
:color="hasBlockingIncomingInvoiceErrors ? 'rose' : 'orange'"
|
||||
:color="hasBlockingIncomingInvoiceErrors ? 'error' : 'orange'"
|
||||
variant="soft"
|
||||
icon="i-heroicons-exclamation-triangle"
|
||||
>
|
||||
@@ -323,30 +346,30 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="font-semibold">Stammdaten</h3>
|
||||
<div class="flex bg-gray-100 dark:bg-gray-800 p-1 rounded-lg">
|
||||
<UButton size="xs" :variant="itemInfo.expense ? 'solid' : 'ghost'" color="rose" @click="itemInfo.expense = true" :disabled="mode === 'show'">Ausgabe</UButton>
|
||||
<UButton size="xs" :variant="itemInfo.expense ? 'solid' : 'ghost'" color="error" @click="itemInfo.expense = true" :disabled="mode === 'show'">Ausgabe</UButton>
|
||||
<UButton size="xs" :variant="!itemInfo.expense ? 'solid' : 'ghost'" color="emerald" @click="itemInfo.expense = false" :disabled="mode === 'show'">Einnahme</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<UFormGroup label="Lieferant / Partner" class="md:col-span-2">
|
||||
<UFormField label="Lieferant / Partner" class="md:col-span-2">
|
||||
<div class="flex gap-2">
|
||||
<USelectMenu
|
||||
class="w-full"
|
||||
v-model="itemInfo.vendor"
|
||||
:options="vendors"
|
||||
option-attribute="name"
|
||||
value-attribute="id"
|
||||
searchable
|
||||
:items="vendors"
|
||||
label-key="name"
|
||||
value-key="id"
|
||||
:search-input="{ placeholder: 'Lieferant suchen...' }"
|
||||
:disabled="mode === 'show'"
|
||||
:search-attributes="['name', 'vendorNumber']"
|
||||
placeholder="Lieferant suchen..."
|
||||
:filter-fields="['name', 'vendorNumber']"
|
||||
:color="itemInfo.vendor ? 'primary' : 'error'"
|
||||
>
|
||||
<template #label>
|
||||
<template #default>
|
||||
{{ vendors.find(v => v.id === itemInfo.vendor)?.name || 'Bitte wählen' }}
|
||||
</template>
|
||||
<template #option="{ option }">
|
||||
<template #item="{ item: option }">
|
||||
<span class="font-mono text-xs opacity-75 mr-2">{{ option.vendorNumber }}</span> {{ option.name }}
|
||||
</template>
|
||||
</USelectMenu>
|
||||
@@ -354,7 +377,7 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
|
||||
v-if="mode !== 'show'"
|
||||
icon="i-heroicons-x-mark"
|
||||
variant="outline"
|
||||
color="rose"
|
||||
color="error"
|
||||
:disabled="!itemInfo.vendor"
|
||||
@click="itemInfo.vendor = null"
|
||||
/>
|
||||
@@ -365,37 +388,85 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
|
||||
@return-data="(data) => itemInfo.vendor = data.id"
|
||||
/>
|
||||
</div>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup label="Rechnungsnummer">
|
||||
<UInput v-model="itemInfo.reference" icon="i-heroicons-hashtag" :disabled="mode === 'show'" />
|
||||
</UFormGroup>
|
||||
<UFormField label="Rechnungsnummer">
|
||||
<UInput class="w-full" v-model="itemInfo.reference" icon="i-heroicons-hashtag" :disabled="mode === 'show'" :color="itemInfo.reference ? 'primary' : 'error'" />
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup label="Zahlart">
|
||||
<USelectMenu v-model="itemInfo.paymentType" :options="['Überweisung', 'Lastschrift', 'Kreditkarte', 'PayPal', 'Bar', 'Sonstiges']" :disabled="mode === 'show'" />
|
||||
</UFormGroup>
|
||||
<UFormField label="Zahlart">
|
||||
<USelectMenu v-model="itemInfo.paymentType" :items="paymentTypeItems" :disabled="mode === 'show'" class="w-full" />
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup label="Rechnungsdatum">
|
||||
<UPopover :popper="{ placement: 'bottom-start' }">
|
||||
<UButton block color="white" icon="i-heroicons-calendar" :label="itemInfo.date ? dayjs(itemInfo.date).format('DD.MM.YYYY') : '-'" :disabled="mode === 'show'" />
|
||||
<template #panel="{ close }">
|
||||
<LazyDatePicker v-model="itemInfo.date" @close="() => { if(!itemInfo.dueDate) itemInfo.dueDate = itemInfo.date; close() }" />
|
||||
<UFormField label="Rechnungsdatum">
|
||||
<UPopover class="w-full" :content="{ side: 'bottom', align: 'start' }">
|
||||
<UButton
|
||||
block
|
||||
icon="i-heroicons-calendar"
|
||||
:label="getDateButtonLabel(itemInfo.date)"
|
||||
:disabled="mode === 'show'"
|
||||
:color="itemInfo.date ? 'neutral' : 'error'"
|
||||
variant="outline"
|
||||
class="w-full justify-start"
|
||||
/>
|
||||
<template #content>
|
||||
<div class="p-2">
|
||||
<UCalendar
|
||||
:model-value="getCalendarValue(itemInfo.date)"
|
||||
@update:model-value="(value) => { setDateField('date', value); if (!itemInfo.dueDate) itemInfo.dueDate = itemInfo.date }"
|
||||
:week-starts-on="1"
|
||||
/>
|
||||
<div class="flex justify-end px-2 pb-2">
|
||||
<UButton
|
||||
size="xs"
|
||||
color="gray"
|
||||
variant="soft"
|
||||
icon="i-heroicons-calendar-days"
|
||||
label="Heute"
|
||||
@click="() => { setDateFieldToToday('date'); if (!itemInfo.dueDate) itemInfo.dueDate = itemInfo.date }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</UPopover>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup label="Fälligkeitsdatum">
|
||||
<UPopover :popper="{ placement: 'bottom-start' }">
|
||||
<UButton block color="white" icon="i-heroicons-calendar" :label="itemInfo.dueDate ? dayjs(itemInfo.dueDate).format('DD.MM.YYYY') : '-'" :disabled="mode === 'show'" />
|
||||
<template #panel="{ close }">
|
||||
<LazyDatePicker v-model="itemInfo.dueDate" @close="close" />
|
||||
<UFormField label="Fälligkeitsdatum">
|
||||
<UPopover class="w-full" :content="{ side: 'bottom', align: 'start' }">
|
||||
<UButton
|
||||
block
|
||||
icon="i-heroicons-calendar"
|
||||
:label="getDateButtonLabel(itemInfo.dueDate)"
|
||||
:disabled="mode === 'show'"
|
||||
:color="itemInfo.dueDate ? 'neutral' : 'error'"
|
||||
variant="outline"
|
||||
class="w-full justify-start"
|
||||
/>
|
||||
<template #content>
|
||||
<div class="p-2">
|
||||
<UCalendar
|
||||
:model-value="getCalendarValue(itemInfo.dueDate)"
|
||||
@update:model-value="(value) => setDateField('dueDate', value)"
|
||||
:week-starts-on="1"
|
||||
/>
|
||||
<div class="flex justify-end px-2 pb-2">
|
||||
<UButton
|
||||
size="xs"
|
||||
color="gray"
|
||||
variant="soft"
|
||||
icon="i-heroicons-calendar-days"
|
||||
label="Heute"
|
||||
@click="setDateFieldToToday('dueDate')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</UPopover>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup label="Beschreibung / Notiz" class="md:col-span-2">
|
||||
<UTextarea v-model="itemInfo.description" :rows="2" autoresize :disabled="mode === 'show'" />
|
||||
</UFormGroup>
|
||||
<UFormField label="Beschreibung / Notiz" class="md:col-span-2">
|
||||
<UTextarea class="w-full" v-model="itemInfo.description" :rows="2" autoresize :disabled="mode === 'show'" />
|
||||
</UFormField>
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
@@ -404,7 +475,7 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
|
||||
<h3 class="font-semibold text-lg">Positionen</h3>
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<span :class="{'font-bold': !useNetMode, 'opacity-50': useNetMode}">Brutto</span>
|
||||
<UToggle v-model="useNetMode" color="primary" :disabled="mode === 'show'" />
|
||||
<USwitch v-model="useNetMode" color="primary" :disabled="mode === 'show'" />
|
||||
<span :class="{'font-bold': useNetMode, 'opacity-50': !useNetMode}">Netto Eingabe</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -419,7 +490,7 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
|
||||
<UButton
|
||||
v-if="itemInfo.accounts.length > 1 && mode !== 'show'"
|
||||
icon="i-heroicons-trash"
|
||||
color="rose"
|
||||
color="error"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
class="absolute top-2 right-2"
|
||||
@@ -428,48 +499,50 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
|
||||
|
||||
<div class="grid grid-cols-12 gap-3">
|
||||
<div class="col-span-12 md:col-span-6">
|
||||
<UFormGroup label="Konto / Kategorie">
|
||||
<UFormField label="Konto / Kategorie">
|
||||
<USelectMenu
|
||||
class="w-full"
|
||||
v-model="item.account"
|
||||
:options="accounts"
|
||||
searchable
|
||||
placeholder="Kategorie wählen"
|
||||
option-attribute="label"
|
||||
value-attribute="id"
|
||||
:items="accounts"
|
||||
:search-input="{ placeholder: 'Kategorie wählen' }"
|
||||
label-key="label"
|
||||
value-key="id"
|
||||
:disabled="mode === 'show'"
|
||||
:search-attributes="['label', 'number']"
|
||||
:filter-fields="['label', 'number']"
|
||||
:color="item.account ? 'primary' : 'error'"
|
||||
>
|
||||
<template #option="{ option }">
|
||||
<template #item="{ item: option }">
|
||||
<span class="font-mono text-xs text-gray-500 mr-2">{{ option.number }}</span> {{ option.label }}
|
||||
</template>
|
||||
<template #label>
|
||||
<template #default>
|
||||
{{ accounts.find(a => a.id === item.account)?.label || 'Auswählen' }}
|
||||
</template>
|
||||
</USelectMenu>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<div class="col-span-12 md:col-span-6">
|
||||
<UFormGroup label="Kostenstelle">
|
||||
<UFormField label="Kostenstelle">
|
||||
<USelectMenu
|
||||
class="w-full"
|
||||
v-model="item.costCentre"
|
||||
:options="costcentres"
|
||||
searchable
|
||||
option-attribute="name"
|
||||
value-attribute="id"
|
||||
placeholder="Optional"
|
||||
:items="costcentres"
|
||||
:search-input="{ placeholder: 'Optional' }"
|
||||
label-key="name"
|
||||
value-key="id"
|
||||
:disabled="mode === 'show'"
|
||||
>
|
||||
<template #label>
|
||||
<template #default>
|
||||
{{ costcentres.find(c => c.id === item.costCentre)?.name || 'Keine' }}
|
||||
</template>
|
||||
</USelectMenu>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<div class="col-span-12 md:col-span-3">
|
||||
<UFormGroup label="Betrag (Netto)">
|
||||
<UFormField label="Betrag (Netto)">
|
||||
<UInput
|
||||
class="w-full"
|
||||
type="number"
|
||||
step="0.01"
|
||||
:disabled="mode === 'show' || !useNetMode"
|
||||
@@ -478,12 +551,13 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
|
||||
>
|
||||
<template #trailing>€</template>
|
||||
</UInput>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<div class="col-span-12 md:col-span-3">
|
||||
<UFormGroup label="Betrag (Brutto)">
|
||||
<UFormField label="Betrag (Brutto)">
|
||||
<UInput
|
||||
class="w-full"
|
||||
type="number"
|
||||
step="0.01"
|
||||
:disabled="mode === 'show' || useNetMode"
|
||||
@@ -492,28 +566,30 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
|
||||
>
|
||||
<template #trailing>€</template>
|
||||
</UInput>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<div class="col-span-6 md:col-span-3">
|
||||
<UFormGroup label="Steuerschlüssel">
|
||||
<UFormField label="Steuerschlüssel">
|
||||
<USelectMenu
|
||||
class="w-full"
|
||||
v-model="item.taxType"
|
||||
:options="taxOptions"
|
||||
value-attribute="key"
|
||||
option-attribute="label"
|
||||
:items="taxOptions"
|
||||
value-key="key"
|
||||
label-key="label"
|
||||
:disabled="mode === 'show'"
|
||||
@change="recalculateItem(item, 'taxType')"
|
||||
@update:model-value="recalculateItem(item, 'taxType')"
|
||||
:color="item.taxType ? 'primary' : 'error'"
|
||||
/>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<div class="col-span-6 md:col-span-3">
|
||||
<UFormGroup label="Steuerbetrag" help="Automatisch berechnet">
|
||||
<UInput :model-value="item.amountTax" disabled color="gray" >
|
||||
<UFormField label="Steuerbetrag" help="Automatisch berechnet">
|
||||
<UInput class="w-full" :model-value="item.amountTax" disabled color="gray" >
|
||||
<template #trailing>€</template>
|
||||
</UInput>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<div class="col-span-12 flex justify-end gap-2">
|
||||
@@ -538,7 +614,7 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
|
||||
</div>
|
||||
|
||||
<div class="col-span-12">
|
||||
<UInput v-model="item.description" :disabled="mode === 'show'" placeholder="Positionstext (optional)" icon="i-heroicons-bars-3-bottom-left" variant="none" class="border-b border-gray-100 dark:border-gray-800" />
|
||||
<UInput v-model="item.description" :disabled="mode === 'show'" placeholder="Positionstext (optional)" icon="i-heroicons-bars-3-bottom-left" variant="none" class="w-full border-b border-gray-100 dark:border-gray-800" />
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
@@ -148,7 +148,15 @@ const isPaid = (item) => {
|
||||
return Math.abs(amountPaid) === Math.abs(Number(getInvoiceSum(item)))
|
||||
}
|
||||
|
||||
const selectIncomingInvoice = (invoice) => {
|
||||
const unwrapInvoiceRow = (invoiceLike) => invoiceLike?.original || invoiceLike
|
||||
|
||||
const selectIncomingInvoice = (invoiceLike) => {
|
||||
const invoice = unwrapInvoiceRow(invoiceLike)
|
||||
|
||||
if (!invoice?.id) {
|
||||
return
|
||||
}
|
||||
|
||||
if (invoice.state === "Gebucht") {
|
||||
router.push(`/incomingInvoices/show/${invoice.id}`)
|
||||
} else {
|
||||
@@ -191,7 +199,7 @@ const selectIncomingInvoice = (invoice) => {
|
||||
<UButton
|
||||
icon="i-heroicons-x-mark"
|
||||
variant="outline"
|
||||
color="rose"
|
||||
color="error"
|
||||
@click="clearSearchString()"
|
||||
v-if="searchString.length > 0"
|
||||
/>
|
||||
@@ -203,15 +211,15 @@ const selectIncomingInvoice = (invoice) => {
|
||||
<USelectMenu
|
||||
v-model="selectedColumns"
|
||||
icon="i-heroicons-adjustments-horizontal-solid"
|
||||
:options="dataType.templateColumns.filter(i => !i.disabledInTable)"
|
||||
:items="dataType.templateColumns.filter(i => !i.disabledInTable)"
|
||||
multiple
|
||||
class="hidden lg:block"
|
||||
by="key"
|
||||
:color="selectedColumns.length !== dataType.templateColumns.filter(i => !i.disabledInTable).length ? 'primary' : 'white'"
|
||||
:ui-menu="{ width: 'min-w-max' }"
|
||||
:content="{ width: 'min-w-max' }"
|
||||
@change="tempStore.modifyColumns(type,selectedColumns)"
|
||||
>
|
||||
<template #label>
|
||||
<template #default>
|
||||
Spalten
|
||||
</template>
|
||||
</USelectMenu>
|
||||
@@ -220,11 +228,11 @@ const selectIncomingInvoice = (invoice) => {
|
||||
icon="i-heroicons-adjustments-horizontal-solid"
|
||||
multiple
|
||||
v-model="selectedFilters"
|
||||
:options="selectableFilters"
|
||||
:items="selectableFilters"
|
||||
:color="selectedFilters.length > 0 ? 'primary' : 'white'"
|
||||
:ui-menu="{ width: 'min-w-max' }"
|
||||
:content="{ width: 'min-w-max' }"
|
||||
>
|
||||
<template #label>
|
||||
<template #default>
|
||||
Filter
|
||||
</template>
|
||||
</USelectMenu>
|
||||
@@ -244,42 +252,42 @@ const selectIncomingInvoice = (invoice) => {
|
||||
{{filteredRows.filter(i => item.label === 'Gebucht' ? i.state === 'Gebucht' : i.state !== 'Gebucht' ).length}}
|
||||
</UBadge>
|
||||
</template>
|
||||
<template #item="{item}">
|
||||
<template #content="{item}">
|
||||
<div style="height: 80dvh; overflow-y: scroll">
|
||||
<UTable
|
||||
v-model:sort="sort"
|
||||
v-model:sorting="sort"
|
||||
sort-mode="manual"
|
||||
@update:sort="setupPage"
|
||||
:rows="filteredRows.filter(i => item.label === 'Gebucht' ? i.state === 'Gebucht' : i.state !== 'Gebucht' )"
|
||||
:columns="columns"
|
||||
@update:sorting="setupPage"
|
||||
:data="filteredRows.filter(i => item.label === 'Gebucht' ? i.state === 'Gebucht' : i.state !== 'Gebucht' )"
|
||||
:columns="normalizeTableColumns(columns)"
|
||||
class="w-full"
|
||||
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
|
||||
@select="(i) => selectIncomingInvoice(i) "
|
||||
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Belege anzuzeigen' }"
|
||||
:on-select="selectIncomingInvoice"
|
||||
:empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Belege anzuzeigen' }"
|
||||
>
|
||||
<template #reference-data="{row}">
|
||||
<span v-if="row === filteredRows[selectedItem]" class="text-primary-500 font-bold">{{row.reference}}</span>
|
||||
<span v-else>{{row.reference}}</span>
|
||||
<template #reference-cell="{row}">
|
||||
<span v-if="row.original === filteredRows[selectedItem]" class="text-primary-500 font-bold">{{row.original.reference}}</span>
|
||||
<span v-else>{{row.original.reference}}</span>
|
||||
</template>
|
||||
<template #state-data="{row}">
|
||||
<span v-if="row.state === 'Vorbereitet'" class="text-cyan-500">{{row.state}}</span>
|
||||
<span v-else-if="row.state === 'Entwurf'" class="text-red-500">{{row.state}}</span>
|
||||
<span v-else-if="row.state === 'Gebucht'" class="text-primary-500">{{row.state}}</span>
|
||||
<template #state-cell="{row}">
|
||||
<span v-if="row.original.state === 'Vorbereitet'" class="text-cyan-500">{{row.original.state}}</span>
|
||||
<span v-else-if="row.original.state === 'Entwurf'" class="text-red-500">{{row.original.state}}</span>
|
||||
<span v-else-if="row.original.state === 'Gebucht'" class="text-primary-500">{{row.original.state}}</span>
|
||||
</template>
|
||||
<template #date-data="{row}">
|
||||
{{dayjs(row.date).format("DD.MM.YYYY")}}
|
||||
<template #date-cell="{row}">
|
||||
{{dayjs(row.original.date).format("DD.MM.YYYY")}}
|
||||
</template>
|
||||
<template #vendor-data="{row}">
|
||||
{{row.vendor ? row.vendor.name : ""}}
|
||||
<template #vendor-cell="{row}">
|
||||
{{row.original.vendor ? row.original.vendor.name : ""}}
|
||||
</template>
|
||||
<template #amount-data="{row}">
|
||||
{{displayCurrency(sum.getIncomingInvoiceSum(row))}}
|
||||
<template #amount-cell="{row}">
|
||||
{{displayCurrency(sum.getIncomingInvoiceSum(row.original))}}
|
||||
</template>
|
||||
<template #dueDate-data="{row}">
|
||||
<span v-if="row.dueDate">{{dayjs(row.dueDate).format("DD.MM.YYYY")}}</span>
|
||||
<template #dueDate-cell="{row}">
|
||||
<span v-if="row.original.dueDate">{{dayjs(row.original.dueDate).format("DD.MM.YYYY")}}</span>
|
||||
</template>
|
||||
<template #paid-data="{row}">
|
||||
<span v-if="isPaid(row)" class="text-primary-500">Bezahlt</span>
|
||||
<template #paid-cell="{row}">
|
||||
<span v-if="isPaid(row.original)" class="text-primary-500">Bezahlt</span>
|
||||
<span v-else class="text-rose-600">Offen</span>
|
||||
</template>
|
||||
</UTable>
|
||||
|
||||
@@ -8,6 +8,7 @@ import DisplayBankaccounts from "~/components/displayBankaccounts.vue"
|
||||
import DisplayProjectsInPhases from "~/components/displayProjectsInPhases.vue"
|
||||
import DisplayOpenTasks from "~/components/displayOpenTasks.vue"
|
||||
import DisplayTaxSummary from "~/components/displayTaxSummary.vue"
|
||||
import DisplayBWASummary from "~/components/displayBWASummary.vue"
|
||||
|
||||
setPageLayout("default")
|
||||
|
||||
@@ -78,6 +79,15 @@ const DASHBOARD_WIDGETS = [
|
||||
defaultLayout: { x: 4, y: 7, w: 4, h: 3 },
|
||||
minW: 3,
|
||||
minH: 3
|
||||
},
|
||||
{
|
||||
id: "bwa-summary",
|
||||
title: "BWA aktuell",
|
||||
description: "Einnahmen, Ausgaben und Ergebnis des aktuellen Monats",
|
||||
component: markRaw(DisplayBWASummary),
|
||||
defaultLayout: { x: 8, y: 7, w: 4, h: 3 },
|
||||
minW: 3,
|
||||
minH: 3
|
||||
}
|
||||
]
|
||||
|
||||
@@ -348,110 +358,108 @@ onBeforeUnmount(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<UDashboardNavbar title="Home">
|
||||
<template #right>
|
||||
<div class="flex items-center gap-2">
|
||||
<UButton
|
||||
<UDashboardNavbar title="Home">
|
||||
<template #right>
|
||||
<div class="flex items-center gap-2">
|
||||
<UButton
|
||||
:icon="isEditMode ? 'i-heroicons-check' : 'i-heroicons-pencil-square'"
|
||||
:color="isEditMode ? 'primary' : 'gray'"
|
||||
:variant="isEditMode ? 'solid' : 'ghost'"
|
||||
@click="toggleEditMode"
|
||||
>
|
||||
{{ isEditMode ? "Bearbeitung beenden" : "Dashboard bearbeiten" }}
|
||||
</UButton>
|
||||
<UButton
|
||||
>
|
||||
{{ isEditMode ? "Bearbeitung beenden" : "Dashboard bearbeiten" }}
|
||||
</UButton>
|
||||
<UButton
|
||||
v-if="isEditMode && hiddenWidgets.length > 0"
|
||||
icon="i-heroicons-plus"
|
||||
color="white"
|
||||
variant="soft"
|
||||
@click="manageCardsOpen = true"
|
||||
>
|
||||
Karte hinzufügen
|
||||
</UButton>
|
||||
<UButton
|
||||
>
|
||||
Karte hinzufügen
|
||||
</UButton>
|
||||
<UButton
|
||||
v-if="isEditMode"
|
||||
icon="i-heroicons-squares-2x2"
|
||||
color="gray"
|
||||
variant="ghost"
|
||||
@click="manageCardsOpen = true"
|
||||
>
|
||||
Karten verwalten
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
</UDashboardNavbar>
|
||||
>
|
||||
Karten verwalten
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
</UDashboardNavbar>
|
||||
|
||||
<UDashboardPanelContent>
|
||||
<div v-if="visibleWidgets.length > 0" ref="gridElement" class="grid-stack dashboard-grid">
|
||||
<div
|
||||
v-for="widget in visibleWidgets"
|
||||
:key="widget.id"
|
||||
:class="['grid-stack-item', isEditMode ? 'dashboard-widget-editing' : '']"
|
||||
:data-widget-id="widget.id"
|
||||
:gs-x="widget.x"
|
||||
:gs-y="widget.y"
|
||||
:gs-w="widget.w"
|
||||
:gs-h="widget.h"
|
||||
:gs-min-w="widget.minW"
|
||||
:gs-min-h="widget.minH"
|
||||
>
|
||||
<div class="grid-stack-item-content dashboard-grid-item">
|
||||
<div class="dashboard-widget-card border border-gray-200 dark:border-gray-800">
|
||||
<div class="dashboard-widget-header border-b border-gray-200 dark:border-gray-800">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="min-w-0">
|
||||
<div :class="['dashboard-widget-drag-handle font-semibold', isEditMode ? 'cursor-move' : 'cursor-default']">
|
||||
{{ widget.title }}
|
||||
</div>
|
||||
<p class="mt-1 text-sm">
|
||||
{{ widget.description }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="dashboard-widget-header-actions">
|
||||
<div :id="`dashboard-widget-header-actions-${widget.id}`" class="dashboard-widget-header-target" />
|
||||
<UButtonGroup v-if="isEditMode" size="xs">
|
||||
<UButton
|
||||
color="gray"
|
||||
variant="soft"
|
||||
icon="i-heroicons-arrows-pointing-out"
|
||||
class="dashboard-widget-drag-handle"
|
||||
/>
|
||||
<UButton
|
||||
color="gray"
|
||||
variant="soft"
|
||||
icon="i-heroicons-x-mark"
|
||||
:disabled="visibleWidgets.length <= 1"
|
||||
@click="removeWidget(widget.id)"
|
||||
/>
|
||||
</UButtonGroup>
|
||||
<div v-if="visibleWidgets.length > 0" ref="gridElement" class="grid-stack dashboard-grid overflow-y-auto">
|
||||
<div
|
||||
v-for="widget in visibleWidgets"
|
||||
:key="widget.id"
|
||||
:class="['grid-stack-item', isEditMode ? 'dashboard-widget-editing' : '']"
|
||||
:data-widget-id="widget.id"
|
||||
:gs-x="widget.x"
|
||||
:gs-y="widget.y"
|
||||
:gs-w="widget.w"
|
||||
:gs-h="widget.h"
|
||||
:gs-min-w="widget.minW"
|
||||
:gs-min-h="widget.minH"
|
||||
>
|
||||
<div class="grid-stack-item-content dashboard-grid-item">
|
||||
<div class="dashboard-widget-card border border-gray-200 dark:border-gray-800">
|
||||
<div class="dashboard-widget-header border-b border-gray-200 dark:border-gray-800">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="min-w-0">
|
||||
<div :class="['dashboard-widget-drag-handle font-semibold', isEditMode ? 'cursor-move' : 'cursor-default']">
|
||||
{{ widget.title }}
|
||||
</div>
|
||||
<p class="mt-1 text-sm">
|
||||
{{ widget.description }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="dashboard-widget-header-actions">
|
||||
<div :id="`dashboard-widget-header-actions-${widget.id}`" class="dashboard-widget-header-target" />
|
||||
<UButtonGroup v-if="isEditMode" size="xs">
|
||||
<UButton
|
||||
color="gray"
|
||||
variant="soft"
|
||||
icon="i-heroicons-arrows-pointing-out"
|
||||
class="dashboard-widget-drag-handle"
|
||||
/>
|
||||
<UButton
|
||||
color="gray"
|
||||
variant="soft"
|
||||
icon="i-heroicons-x-mark"
|
||||
:disabled="visibleWidgets.length <= 1"
|
||||
@click="removeWidget(widget.id)"
|
||||
/>
|
||||
</UButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-widget-body">
|
||||
<component
|
||||
:is="widget.component"
|
||||
v-bind="widget.id === 'income-expense'
|
||||
</div>
|
||||
<div class="dashboard-widget-body">
|
||||
<component
|
||||
:is="widget.component"
|
||||
v-bind="widget.id === 'income-expense'
|
||||
? { headerTarget: `#dashboard-widget-header-actions-${widget.id}` }
|
||||
: {}"
|
||||
/>
|
||||
</div>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="rounded-xl border border-dashed border-gray-300 dark:border-gray-700 p-10 text-center">
|
||||
<p class="text-sm">
|
||||
Es sind aktuell keine Dashboard-Karten sichtbar.
|
||||
</p>
|
||||
<UButton v-if="isEditMode" class="mt-4" icon="i-heroicons-plus" @click="manageCardsOpen = true">
|
||||
Karte hinzufügen
|
||||
</UButton>
|
||||
</div>
|
||||
</UDashboardPanelContent>
|
||||
<div v-else class="rounded-xl border border-dashed border-gray-300 dark:border-gray-700 p-10 text-center">
|
||||
<p class="text-sm">
|
||||
Es sind aktuell keine Dashboard-Karten sichtbar.
|
||||
</p>
|
||||
<UButton v-if="isEditMode" class="mt-4" icon="i-heroicons-plus" @click="manageCardsOpen = true">
|
||||
Karte hinzufügen
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<UModal v-model="manageCardsOpen">
|
||||
<UModal v-model:open="manageCardsOpen">
|
||||
<template #content>
|
||||
<UCard>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
@@ -469,9 +477,9 @@ onBeforeUnmount(() => {
|
||||
|
||||
<div class="space-y-3">
|
||||
<div
|
||||
v-for="definition in DASHBOARD_WIDGETS"
|
||||
:key="definition.id"
|
||||
class="flex items-center justify-between gap-3 rounded-lg border border-gray-200 dark:border-gray-800 px-4 py-3"
|
||||
v-for="definition in DASHBOARD_WIDGETS"
|
||||
:key="definition.id"
|
||||
class="flex items-center justify-between gap-3 rounded-lg border border-gray-200 dark:border-gray-800 px-4 py-3"
|
||||
>
|
||||
<div>
|
||||
<p class="font-medium">{{ definition.title }}</p>
|
||||
@@ -479,29 +487,29 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
|
||||
<UButton
|
||||
v-if="getWidgetLayout(definition.id)?.visible"
|
||||
color="gray"
|
||||
variant="soft"
|
||||
icon="i-heroicons-minus"
|
||||
:disabled="visibleWidgets.length <= 1"
|
||||
@click="removeWidget(definition.id)"
|
||||
v-if="getWidgetLayout(definition.id)?.visible"
|
||||
color="gray"
|
||||
variant="soft"
|
||||
icon="i-heroicons-minus"
|
||||
:disabled="visibleWidgets.length <= 1"
|
||||
@click="removeWidget(definition.id)"
|
||||
>
|
||||
Entfernen
|
||||
</UButton>
|
||||
<UButton
|
||||
v-else
|
||||
color="primary"
|
||||
variant="soft"
|
||||
icon="i-heroicons-plus"
|
||||
@click="addWidget(definition.id)"
|
||||
v-else
|
||||
color="primary"
|
||||
variant="soft"
|
||||
icon="i-heroicons-plus"
|
||||
@click="addWidget(definition.id)"
|
||||
>
|
||||
Hinzufügen
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
</UModal>
|
||||
</div>
|
||||
</template>
|
||||
</UModal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormSubmitEvent } from '#ui/types'
|
||||
|
||||
definePageMeta({
|
||||
layout: "notLoggedIn"
|
||||
})
|
||||
@@ -7,16 +9,23 @@ const auth = useAuthStore()
|
||||
const toast = useToast()
|
||||
const router = useRouter()
|
||||
|
||||
const doLogin = async (data:any) => {
|
||||
const state = reactive({
|
||||
email: '',
|
||||
password: ''
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const doLogin = async (event: FormSubmitEvent<typeof state>) => {
|
||||
loading.value = true
|
||||
try {
|
||||
await auth.login(data.email, data.password)
|
||||
// Weiterleiten nach erfolgreichem Login
|
||||
await auth.login(event.data.email, event.data.password)
|
||||
toast.add({title:"Einloggen erfolgreich"})
|
||||
|
||||
await router.push("/")
|
||||
|
||||
} catch (err: any) {
|
||||
toast.add({title:"Zugangsdaten falsch. Bitte überprüfen Sie Ihre Eingaben",color:"rose"})
|
||||
toast.add({title:"Zugangsdaten falsch. Bitte überprüfen Sie Ihre Eingaben",color:"error"})
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -29,60 +38,42 @@ const doLogin = async (data:any) => {
|
||||
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>
|
||||
</UCard>
|
||||
<!-- <div v-else class="mt-20 m-2 p-2">
|
||||
<UColorModeImage
|
||||
light="/Logo.png"
|
||||
dark="/Logo_Dark.png"
|
||||
/>
|
||||
<div class="mt-6 space-y-5">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-xl font-semibold">Login</h1>
|
||||
<p class="text-sm text-muted">
|
||||
Geben Sie Ihre Anmeldedaten ein um Zugriff auf Ihren Account zu erhalten.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<UForm :state="state" class="space-y-4" @submit="doLogin">
|
||||
<UFormField label="E-Mail" name="email">
|
||||
<UInput
|
||||
v-model="state.email"
|
||||
type="email"
|
||||
class="w-full"
|
||||
placeholder="Deine E-Mail Adresse"
|
||||
autocomplete="email"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<UFormField label="Passwort" name="password">
|
||||
<template #hint>
|
||||
<NuxtLink to="/password-reset" class="text-primary font-medium">Passwort vergessen?</NuxtLink>
|
||||
</template>
|
||||
<UInput
|
||||
v-model="state.password"
|
||||
type="password"
|
||||
class="w-full"
|
||||
placeholder="Dein Passwort"
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<UButton type="submit" block class="w-full" :loading="loading">
|
||||
Weiter
|
||||
</UButton>
|
||||
</UForm>
|
||||
</div>
|
||||
</UCard>
|
||||
</template>
|
||||
|
||||
@@ -423,7 +423,7 @@ onMounted(() => {
|
||||
Urlaub
|
||||
</UButton>
|
||||
<UButton
|
||||
color="rose"
|
||||
color="error"
|
||||
variant="soft"
|
||||
icon="i-heroicons-heart"
|
||||
@click="openAbsenceModal('sick')"
|
||||
@@ -453,25 +453,26 @@ onMounted(() => {
|
||||
/>
|
||||
</UDashboardPanelContent>
|
||||
|
||||
<UModal v-model="isAbsenceModalOpen">
|
||||
<UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
||||
{{ absenceModalTitle }}
|
||||
</h3>
|
||||
<UButton
|
||||
color="gray"
|
||||
variant="ghost"
|
||||
icon="i-heroicons-x-mark-20-solid"
|
||||
class="-my-1"
|
||||
@click="isAbsenceModalOpen = false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<UModal v-model:open="isAbsenceModalOpen">
|
||||
<template #content>
|
||||
<UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
||||
{{ absenceModalTitle }}
|
||||
</h3>
|
||||
<UButton
|
||||
color="gray"
|
||||
variant="ghost"
|
||||
icon="i-heroicons-x-mark-20-solid"
|
||||
class="-my-1"
|
||||
@click="isAbsenceModalOpen = false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="space-y-4">
|
||||
<UFormGroup label="Profil">
|
||||
<UFormField label="Profil">
|
||||
<USelectMenu
|
||||
v-model="absenceForm.userId"
|
||||
:options="profileOptions"
|
||||
@@ -479,51 +480,52 @@ onMounted(() => {
|
||||
option-attribute="label"
|
||||
searchable
|
||||
/>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup label="Typ">
|
||||
<UFormField label="Typ">
|
||||
<USelectMenu
|
||||
v-model="absenceForm.type"
|
||||
:options="absenceTypeOptions"
|
||||
value-attribute="value"
|
||||
option-attribute="label"
|
||||
/>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<UFormGroup label="Start">
|
||||
<UFormField label="Start">
|
||||
<div class="flex items-center gap-2">
|
||||
<UInput v-model="absenceForm.startDate" type="date" class="flex-1" />
|
||||
<UButton color="gray" variant="soft" label="Heute" @click="setAbsenceDateToToday('startDate')" />
|
||||
</div>
|
||||
</UFormGroup>
|
||||
<UFormGroup label="Ende">
|
||||
</UFormField>
|
||||
<UFormField label="Ende">
|
||||
<div class="flex items-center gap-2">
|
||||
<UInput v-model="absenceForm.endDate" type="date" class="flex-1" />
|
||||
<UButton color="gray" variant="soft" label="Heute" @click="setAbsenceDateToToday('endDate')" />
|
||||
</div>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<UFormGroup label="Notiz">
|
||||
<UFormField label="Notiz">
|
||||
<UTextarea
|
||||
v-model="absenceForm.description"
|
||||
:placeholder="absenceForm.type === 'sick' ? 'z. B. Krankmeldung eingegangen' : 'z. B. Sommerurlaub'"
|
||||
/>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<UButton color="gray" variant="soft" @click="isAbsenceModalOpen = false">
|
||||
Abbrechen
|
||||
</UButton>
|
||||
<UButton color="primary" :loading="savingAbsence" @click="saveAbsence">
|
||||
Speichern
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<UButton color="gray" variant="soft" @click="isAbsenceModalOpen = false">
|
||||
Abbrechen
|
||||
</UButton>
|
||||
<UButton color="primary" :loading="savingAbsence" @click="saveAbsence">
|
||||
Speichern
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
</template>
|
||||
</UModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormSubmitEvent } from '#ui/types'
|
||||
|
||||
definePageMeta({
|
||||
layout: "notLoggedIn"
|
||||
})
|
||||
@@ -6,25 +8,31 @@ definePageMeta({
|
||||
const auth = useAuthStore()
|
||||
const toast = useToast()
|
||||
|
||||
const state = reactive({
|
||||
oldPassword: '',
|
||||
newPassword: ''
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
|
||||
const doChange = async (data:any) => {
|
||||
const doChange = async (event: FormSubmitEvent<typeof state>) => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await useNuxtApp().$api("/api/auth/password/change", {
|
||||
await useNuxtApp().$api("/api/auth/password/change", {
|
||||
method: "POST",
|
||||
body: {
|
||||
old_password: data.oldPassword,
|
||||
new_password: data.newPassword,
|
||||
old_password: event.data.oldPassword,
|
||||
new_password: event.data.newPassword,
|
||||
}
|
||||
})
|
||||
|
||||
// Weiterleiten nach erfolgreichem Login
|
||||
toast.add({title:"Ändern erfolgreich"})
|
||||
await auth.logout()
|
||||
return navigateTo("/login")
|
||||
} catch (err: any) {
|
||||
toast.add({title:"Es gab ein Problem beim ändern",color:"rose"})
|
||||
toast.add({title:"Es gab ein Problem beim ändern",color:"error"})
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -37,26 +45,39 @@ const doChange = async (data:any) => {
|
||||
dark="/Logo_Dark.png"
|
||||
/>
|
||||
|
||||
<UAuthForm
|
||||
title="Passwort zurücksetzen"
|
||||
description="Geben Sie Ihre E-Mail ein um ein neues Passwort per E-Mail zu erhalten."
|
||||
align="bottom"
|
||||
:fields="[{
|
||||
name: 'oldPassword',
|
||||
label: 'Altes Passwort',
|
||||
type: 'password',
|
||||
placeholder: 'Dein altes Passwort'
|
||||
},{
|
||||
name: 'newPassword',
|
||||
label: 'Neues Passwort',
|
||||
type: 'password',
|
||||
placeholder: 'Dein neues Passwort'
|
||||
}]"
|
||||
:loading="false"
|
||||
@submit="doChange"
|
||||
:submit-button="{label: 'Ändern'}"
|
||||
divider="oder"
|
||||
>
|
||||
</UAuthForm>
|
||||
<div class="mt-6 space-y-5">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-xl font-semibold">Passwort ändern</h1>
|
||||
<p class="text-sm text-muted">
|
||||
Geben Sie Ihr aktuelles und Ihr neues Passwort ein.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<UForm :state="state" class="space-y-4" @submit="doChange">
|
||||
<UFormField label="Altes Passwort" name="oldPassword">
|
||||
<UInput
|
||||
v-model="state.oldPassword"
|
||||
type="password"
|
||||
class="w-full"
|
||||
placeholder="Dein altes Passwort"
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<UFormField label="Neues Passwort" name="newPassword">
|
||||
<UInput
|
||||
v-model="state.newPassword"
|
||||
type="password"
|
||||
class="w-full"
|
||||
placeholder="Dein neues Passwort"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<UButton type="submit" block class="w-full" :loading="loading">
|
||||
Ändern
|
||||
</UButton>
|
||||
</UForm>
|
||||
</div>
|
||||
</UCard>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -1,28 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormSubmitEvent } from '#ui/types'
|
||||
|
||||
definePageMeta({
|
||||
layout: "notLoggedIn"
|
||||
})
|
||||
|
||||
const auth = useAuthStore()
|
||||
const toast = useToast()
|
||||
|
||||
const state = reactive({
|
||||
email: ''
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
|
||||
const doReset = async (data:any) => {
|
||||
const doReset = async (event: FormSubmitEvent<typeof state>) => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await useNuxtApp().$api("/auth/password/reset", {
|
||||
await useNuxtApp().$api("/auth/password/reset", {
|
||||
method: "POST",
|
||||
body: {
|
||||
email: data.email
|
||||
email: event.data.email
|
||||
}
|
||||
})
|
||||
|
||||
// Weiterleiten nach erfolgreichem Login
|
||||
toast.add({title:"Zurücksetzen erfolgreich"})
|
||||
return navigateTo("/login")
|
||||
} catch (err: any) {
|
||||
toast.add({title:"Problem beim zurücksetzen",color:"rose"})
|
||||
toast.add({title:"Problem beim zurücksetzen",color:"error"})
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -35,21 +41,29 @@ const doReset = async (data:any) => {
|
||||
dark="/Logo_Dark.png"
|
||||
/>
|
||||
|
||||
<UAuthForm
|
||||
title="Passwort zurücksetzen"
|
||||
description="Geben Sie Ihre E-Mail ein um ein neues Passwort per E-Mail zu erhalten."
|
||||
align="bottom"
|
||||
:fields="[{
|
||||
name: 'email',
|
||||
type: 'text',
|
||||
label: 'Email',
|
||||
placeholder: 'Deine E-Mail Adresse'
|
||||
}]"
|
||||
:loading="false"
|
||||
@submit="doReset"
|
||||
:submit-button="{label: 'Zurücksetzen'}"
|
||||
divider="oder"
|
||||
>
|
||||
</UAuthForm>
|
||||
<div class="mt-6 space-y-5">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-xl font-semibold">Passwort zurücksetzen</h1>
|
||||
<p class="text-sm text-muted">
|
||||
Geben Sie Ihre E-Mail ein um ein neues Passwort per E-Mail zu erhalten.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<UForm :state="state" class="space-y-4" @submit="doReset">
|
||||
<UFormField label="E-Mail" name="email">
|
||||
<UInput
|
||||
v-model="state.email"
|
||||
type="email"
|
||||
class="w-full"
|
||||
placeholder="Deine E-Mail Adresse"
|
||||
autocomplete="email"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<UButton type="submit" block class="w-full" :loading="loading">
|
||||
Zurücksetzen
|
||||
</UButton>
|
||||
</UForm>
|
||||
</div>
|
||||
</UCard>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -9,18 +9,20 @@ defineShortcuts({
|
||||
router.push("/projecttypes")
|
||||
},
|
||||
'arrowleft': () => {
|
||||
if(openTab.value > 0){
|
||||
openTab.value -= 1
|
||||
const currentIndex = Number(openTab.value)
|
||||
if(currentIndex > 0){
|
||||
openTab.value = String(currentIndex - 1)
|
||||
}
|
||||
},
|
||||
'arrowright': () => {
|
||||
if(openTab.value < 3) {
|
||||
openTab.value += 1
|
||||
const currentIndex = Number(openTab.value)
|
||||
if(currentIndex < 3) {
|
||||
openTab.value = String(currentIndex + 1)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const openTab = ref(0)
|
||||
const openTab = ref("0")
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const toast = useToast()
|
||||
@@ -129,7 +131,7 @@ const addPhase = () => {
|
||||
v-if="itemInfo.id && mode == 'show'"
|
||||
v-model="openTab"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
<template #content="{ item }">
|
||||
<div v-if="item.label === 'Informationen'" class="flex flex-row">
|
||||
<div class="w-1/2 mr-3">
|
||||
<UCard class="mt-5">
|
||||
@@ -152,24 +154,24 @@ const addPhase = () => {
|
||||
|
||||
<UForm v-else-if="mode === 'edit' || mode === 'create'">
|
||||
<UAlert
|
||||
color="rose"
|
||||
color="error"
|
||||
variant="outline"
|
||||
class="mb-5"
|
||||
v-if="mode === 'edit'"
|
||||
description="Achtung Änderungen an diesem Projekttypen betreffen nur Projekte die damit neu erstellt werden. Bestehende Projekte bleiben unverändert."
|
||||
/>
|
||||
|
||||
<UFormGroup
|
||||
<UFormField
|
||||
label="Name:"
|
||||
>
|
||||
<UInput
|
||||
v-model="itemInfo.name"
|
||||
/>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UDivider class="mt-5">
|
||||
<USeparator class="mt-5">
|
||||
Initiale Phasen
|
||||
</UDivider>
|
||||
</USeparator>
|
||||
<UButton
|
||||
class="mt-3"
|
||||
@click="addPhase"
|
||||
@@ -247,44 +249,46 @@ const addPhase = () => {
|
||||
{{ button.label }}
|
||||
</UButton>
|
||||
|
||||
<UModal v-model="openQuickActionModal">
|
||||
<UCard>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
||||
Schnellaktion hinzufügen
|
||||
</h3>
|
||||
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="openQuickActionModal = false" />
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<UButton
|
||||
class="my-1"
|
||||
@click="itemInfo.initialPhases[itemInfo.initialPhases.findIndex(i=> i.key === selectedKeyForQuickAction)].quickactions.push({label:'+ Angebot',link:'/createDocument/edit/?type=quotes'})">Angebot Erstellen</UButton>
|
||||
<UButton
|
||||
class="my-1"
|
||||
@click="itemInfo.initialPhases[itemInfo.initialPhases.findIndex(i=> i.key === selectedKeyForQuickAction)].quickactions.push({label:'+ Auftrag',link:'/createDocument/edit/?type=confirmationOrders'})">Auftrag Erstellen</UButton>
|
||||
<UButton
|
||||
class="my-1"
|
||||
@click="itemInfo.initialPhases[itemInfo.initialPhases.findIndex(i=> i.key === selectedKeyForQuickAction)].quickactions.push({label:'+ Lieferschein',link:'/createDocument/edit/?type=deliveryNotes'})">Lieferschein Erstellen</UButton>
|
||||
<UButton
|
||||
class="my-1"
|
||||
@click="itemInfo.initialPhases[itemInfo.initialPhases.findIndex(i=> i.key === selectedKeyForQuickAction)].quickactions.push({label:'+ Rechnung',link:'/createDocument/edit/?type=invoices'})">Rechnung Erstellen</UButton>
|
||||
<UButton
|
||||
class="my-1"
|
||||
@click="itemInfo.initialPhases[itemInfo.initialPhases.findIndex(i=> i.key === selectedKeyForQuickAction)].quickactions.push({label:'+ Aufgabe',link:'/tasks/create'})">Aufgabe Erstellen</UButton>
|
||||
<UButton
|
||||
class="my-1"
|
||||
@click="itemInfo.initialPhases[itemInfo.initialPhases.findIndex(i=> i.key === selectedKeyForQuickAction)].quickactions.push({label:'+ Termin',link:'/events/edit'})">Termin Erstellen</UButton>
|
||||
<UModal v-model:open="openQuickActionModal">
|
||||
<template #content>
|
||||
<UCard>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
||||
Schnellaktion hinzufügen
|
||||
</h3>
|
||||
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="openQuickActionModal = false" />
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<UButton
|
||||
class="my-1"
|
||||
@click="itemInfo.initialPhases[itemInfo.initialPhases.findIndex(i=> i.key === selectedKeyForQuickAction)].quickactions.push({label:'+ Angebot',link:'/createDocument/edit/?type=quotes'})">Angebot Erstellen</UButton>
|
||||
<UButton
|
||||
class="my-1"
|
||||
@click="itemInfo.initialPhases[itemInfo.initialPhases.findIndex(i=> i.key === selectedKeyForQuickAction)].quickactions.push({label:'+ Auftrag',link:'/createDocument/edit/?type=confirmationOrders'})">Auftrag Erstellen</UButton>
|
||||
<UButton
|
||||
class="my-1"
|
||||
@click="itemInfo.initialPhases[itemInfo.initialPhases.findIndex(i=> i.key === selectedKeyForQuickAction)].quickactions.push({label:'+ Lieferschein',link:'/createDocument/edit/?type=deliveryNotes'})">Lieferschein Erstellen</UButton>
|
||||
<UButton
|
||||
class="my-1"
|
||||
@click="itemInfo.initialPhases[itemInfo.initialPhases.findIndex(i=> i.key === selectedKeyForQuickAction)].quickactions.push({label:'+ Rechnung',link:'/createDocument/edit/?type=invoices'})">Rechnung Erstellen</UButton>
|
||||
<UButton
|
||||
class="my-1"
|
||||
@click="itemInfo.initialPhases[itemInfo.initialPhases.findIndex(i=> i.key === selectedKeyForQuickAction)].quickactions.push({label:'+ Aufgabe',link:'/tasks/create'})">Aufgabe Erstellen</UButton>
|
||||
<UButton
|
||||
class="my-1"
|
||||
@click="itemInfo.initialPhases[itemInfo.initialPhases.findIndex(i=> i.key === selectedKeyForQuickAction)].quickactions.push({label:'+ Termin',link:'/events/edit'})">Termin Erstellen</UButton>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</UCard>
|
||||
</UCard>
|
||||
</template>
|
||||
</UModal>
|
||||
</td>
|
||||
<td>
|
||||
<UButton
|
||||
class="my-2 ml-2"
|
||||
variant="outline"
|
||||
color="rose"
|
||||
color="error"
|
||||
@click="itemInfo.initialPhases = itemInfo.initialPhases.filter(i => i !== phase)"
|
||||
>X</UButton>
|
||||
</td>
|
||||
@@ -301,4 +305,4 @@ const addPhase = () => {
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -87,16 +87,16 @@ const filteredRows = computed(() => {
|
||||
</UDashboardNavbar>
|
||||
|
||||
<UTable
|
||||
:rows="filteredRows"
|
||||
:columns="columns"
|
||||
:data="filteredRows"
|
||||
:columns="normalizeTableColumns(columns)"
|
||||
class="w-full"
|
||||
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
|
||||
@select="(i) => router.push(`/projecttypes/show/${i.id}`) "
|
||||
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Projekttypen anzuzeigen' }"
|
||||
:on-select="(i) => router.push(`/projecttypes/show/${i.id}`) "
|
||||
:empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Projekttypen anzuzeigen' }"
|
||||
>
|
||||
<template #name-data="{row}">
|
||||
<span class="text-primary-500 font-bold" v-if="row === filteredRows[selectedItem]">{{ row.name }}</span>
|
||||
<span v-else>{{ row.name }}</span>
|
||||
<template #name-cell="{row}">
|
||||
<span class="text-primary-500 font-bold" v-if="row.original === filteredRows[selectedItem]">{{ row.original.name }}</span>
|
||||
<span v-else>{{ row.original.name }}</span>
|
||||
</template>
|
||||
|
||||
</UTable>
|
||||
@@ -104,4 +104,4 @@ const filteredRows = computed(() => {
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -8,13 +8,15 @@ defineShortcuts({
|
||||
router.push("/roles")
|
||||
},
|
||||
'arrowleft': () => {
|
||||
if(openTab.value > 0){
|
||||
openTab.value -= 1
|
||||
const currentIndex = Number(openTab.value)
|
||||
if(currentIndex > 0){
|
||||
openTab.value = String(currentIndex - 1)
|
||||
}
|
||||
},
|
||||
'arrowright': () => {
|
||||
if(openTab.value < 3) {
|
||||
openTab.value += 1
|
||||
const currentIndex = Number(openTab.value)
|
||||
if(currentIndex < 3) {
|
||||
openTab.value = String(currentIndex + 1)
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -32,7 +34,7 @@ const mode = ref(route.params.mode || "show")
|
||||
const itemInfo = ref({
|
||||
rights: []
|
||||
})
|
||||
const openTab = ref(0)
|
||||
const openTab = ref("0")
|
||||
|
||||
//Functions
|
||||
const setupPage = async () => {
|
||||
@@ -120,7 +122,7 @@ setupPage()
|
||||
class="p-5"
|
||||
v-model="openTab"
|
||||
>
|
||||
<template #item="{item}">
|
||||
<template #content="{item}">
|
||||
<div v-if="item.label === 'Informationen'" class="mt-5 flex flex-row">
|
||||
<div class="w-1/2 mr-5">
|
||||
<UCard>
|
||||
@@ -157,15 +159,15 @@ setupPage()
|
||||
v-else-if="mode == 'edit' || mode == 'create'"
|
||||
class="p-5"
|
||||
>
|
||||
<UFormGroup
|
||||
<UFormField
|
||||
label="Name:"
|
||||
>
|
||||
<UInput
|
||||
v-model="itemInfo.name"
|
||||
autofocus
|
||||
/>
|
||||
</UFormGroup>
|
||||
<UFormGroup
|
||||
</UFormField>
|
||||
<UFormField
|
||||
label="Rechte:"
|
||||
>
|
||||
<USelectMenu
|
||||
@@ -176,14 +178,14 @@ setupPage()
|
||||
multiple
|
||||
>
|
||||
</USelectMenu>
|
||||
</UFormGroup>
|
||||
<UFormGroup
|
||||
</UFormField>
|
||||
<UFormField
|
||||
label="Beschreibung:"
|
||||
>
|
||||
<UTextarea
|
||||
v-model="itemInfo.description"
|
||||
/>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
</UForm>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ const savingUser = ref(false)
|
||||
const savingTenant = ref(false)
|
||||
const creatingUser = ref(false)
|
||||
const creatingTenant = ref(false)
|
||||
const activeTab = ref(0)
|
||||
const activeTab = ref("0")
|
||||
const createUserModalOpen = ref(false)
|
||||
const createTenantModalOpen = ref(false)
|
||||
const createdUserPassword = ref("")
|
||||
@@ -105,12 +105,14 @@ const userTableColumns = [
|
||||
{ key: "tenant_count", label: "Tenants" },
|
||||
{ key: "is_admin", label: "Admin" },
|
||||
]
|
||||
const normalizedUserTableColumns = normalizeTableColumns(userTableColumns)
|
||||
|
||||
const tenantTableColumns = [
|
||||
{ key: "name", label: "Tenant" },
|
||||
{ key: "short", label: "Kürzel" },
|
||||
{ key: "user_count", label: "Benutzer" },
|
||||
]
|
||||
const normalizedTenantTableColumns = normalizeTableColumns(tenantTableColumns)
|
||||
|
||||
const userTableRows = computed(() =>
|
||||
sortedUsers.value.map((user) => ({
|
||||
@@ -470,7 +472,7 @@ onMounted(async () => {
|
||||
:items="tabItems"
|
||||
class="admin-tabs h-full"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
<template #content="{ item }">
|
||||
<div v-if="item.label === 'Benutzer'" class="admin-grid mt-5 grid grid-cols-1 xl:grid-cols-3 gap-5">
|
||||
<UCard class="admin-card xl:col-span-1">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
@@ -490,9 +492,9 @@ onMounted(async () => {
|
||||
<div class="admin-scroll">
|
||||
<UTable
|
||||
v-if="!loading"
|
||||
:rows="userTableRows"
|
||||
:columns="userTableColumns"
|
||||
@select="selectUser"
|
||||
:data="userTableRows"
|
||||
:columns="normalizedUserTableColumns"
|
||||
:on-select="selectUser"
|
||||
/>
|
||||
|
||||
<USkeleton v-else class="h-80" />
|
||||
@@ -517,53 +519,53 @@ onMounted(async () => {
|
||||
</div>
|
||||
|
||||
<UForm :state="userForm" class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<UFormGroup label="E-Mail">
|
||||
<UFormField label="E-Mail">
|
||||
<UInput v-model="userForm.email" />
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup label="Profil Vorname">
|
||||
<UFormField label="Profil Vorname">
|
||||
<UInput v-model="userForm.profile_defaults.first_name" />
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup label="Profil Nachname">
|
||||
<UFormField label="Profil Nachname">
|
||||
<UInput v-model="userForm.profile_defaults.last_name" />
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup label="Tenants">
|
||||
<UFormField label="Tenants">
|
||||
<USelectMenu
|
||||
:model-value="userForm.tenant_ids"
|
||||
:options="tenantOptions"
|
||||
value-attribute="value"
|
||||
option-attribute="label"
|
||||
:items="tenantOptions"
|
||||
value-key="value"
|
||||
label-key="label"
|
||||
multiple
|
||||
@update:model-value="updateUserTenants"
|
||||
/>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup label="Administrative Freigabe">
|
||||
<UFormField label="Administrative Freigabe">
|
||||
<div class="flex items-center gap-3 h-10">
|
||||
<UToggle v-model="userForm.is_admin" />
|
||||
<USwitch v-model="userForm.is_admin" />
|
||||
<span class="text-sm text-gray-600">Darf Administrationsseite und Admin-API nutzen</span>
|
||||
</div>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup label="Multi-Tenant">
|
||||
<UFormField label="Multi-Tenant">
|
||||
<div class="flex items-center gap-3 h-10">
|
||||
<UToggle v-model="userForm.multiTenant" />
|
||||
<USwitch v-model="userForm.multiTenant" />
|
||||
<span class="text-sm text-gray-600">Benutzer darf mehreren Tenants zugeordnet sein</span>
|
||||
</div>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup label="Passwortwechsel erzwingen">
|
||||
<UFormField label="Passwortwechsel erzwingen">
|
||||
<div class="flex items-center gap-3 h-10">
|
||||
<UToggle v-model="userForm.must_change_password" />
|
||||
<USwitch v-model="userForm.must_change_password" />
|
||||
<span class="text-sm text-gray-600">Beim nächsten Login muss das Passwort geändert werden</span>
|
||||
</div>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
</UForm>
|
||||
|
||||
<div>
|
||||
<UDivider label="Rollen pro Tenant" class="mb-4" />
|
||||
<USeparator label="Rollen pro Tenant" class="mb-4" />
|
||||
|
||||
<div
|
||||
v-if="userForm.tenant_ids.length"
|
||||
@@ -584,33 +586,33 @@ onMounted(async () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UFormGroup label="Rolle">
|
||||
<UFormField label="Rolle">
|
||||
<USelectMenu
|
||||
:model-value="getRoleForTenant(tenantId)"
|
||||
:options="getRoleOptionsForTenant(tenantId)"
|
||||
value-attribute="value"
|
||||
option-attribute="label"
|
||||
:items="getRoleOptionsForTenant(tenantId)"
|
||||
value-key="value"
|
||||
label-key="label"
|
||||
placeholder="Rolle auswählen"
|
||||
@update:model-value="(value) => setRoleForTenant(tenantId, value)"
|
||||
/>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup label="Freies Profil">
|
||||
<UFormField label="Freies Profil">
|
||||
<USelectMenu
|
||||
:model-value="getProfileAssignmentForTenant(tenantId)"
|
||||
:options="[
|
||||
:items="[
|
||||
{ label: 'Neues Profil erzeugen', value: null },
|
||||
...getFreeProfilesForTenant(tenantId).map((profile) => ({
|
||||
label: profile.full_name || `${profile.first_name} ${profile.last_name}`,
|
||||
value: profile.id,
|
||||
}))
|
||||
]"
|
||||
value-attribute="value"
|
||||
option-attribute="label"
|
||||
value-key="value"
|
||||
label-key="label"
|
||||
placeholder="Profil auswählen"
|
||||
@update:model-value="(value) => setProfileAssignmentForTenant(tenantId, value)"
|
||||
/>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
</div>
|
||||
</UCard>
|
||||
</div>
|
||||
@@ -625,7 +627,7 @@ onMounted(async () => {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<UDivider label="Profile im System" class="mb-4" />
|
||||
<USeparator label="Profile im System" class="mb-4" />
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<UBadge
|
||||
v-for="profile in userForm.profiles"
|
||||
@@ -672,9 +674,9 @@ onMounted(async () => {
|
||||
<div class="admin-scroll">
|
||||
<UTable
|
||||
v-if="!loading"
|
||||
:rows="tenantTableRows"
|
||||
:columns="tenantTableColumns"
|
||||
@select="selectTenant"
|
||||
:data="tenantTableRows"
|
||||
:columns="normalizedTenantTableColumns"
|
||||
:on-select="selectTenant"
|
||||
/>
|
||||
|
||||
<USkeleton v-else class="h-80" />
|
||||
@@ -699,17 +701,17 @@ onMounted(async () => {
|
||||
</div>
|
||||
|
||||
<UForm :state="tenantForm" class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<UFormGroup label="Name">
|
||||
<UFormField label="Name">
|
||||
<UInput v-model="tenantForm.name" />
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup label="Kürzel">
|
||||
<UFormField label="Kürzel">
|
||||
<UInput v-model="tenantForm.short" />
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
</UForm>
|
||||
|
||||
<div>
|
||||
<UDivider label="Zugeordnete Benutzer" class="mb-4" />
|
||||
<USeparator label="Zugeordnete Benutzer" class="mb-4" />
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<UBadge
|
||||
v-for="user in getUsersForTenant(tenantForm.id)"
|
||||
@@ -740,50 +742,51 @@ onMounted(async () => {
|
||||
</UTabs>
|
||||
</UDashboardPanelContent>
|
||||
|
||||
<UModal v-model="createUserModalOpen">
|
||||
<UCard>
|
||||
<template #header>
|
||||
<div class="text-lg font-semibold">Benutzer anlegen</div>
|
||||
</template>
|
||||
<UModal v-model:open="createUserModalOpen">
|
||||
<template #content>
|
||||
<UCard>
|
||||
<template #header>
|
||||
<div class="text-lg font-semibold">Benutzer anlegen</div>
|
||||
</template>
|
||||
|
||||
<UForm
|
||||
:state="createUserForm"
|
||||
class="space-y-4"
|
||||
@submit.prevent="createUser"
|
||||
>
|
||||
<UFormGroup label="E-Mail">
|
||||
<UFormField label="E-Mail">
|
||||
<UInput v-model="createUserForm.email" type="email" />
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup label="Initialpasswort">
|
||||
<UFormField label="Initialpasswort">
|
||||
<UInput
|
||||
v-model="createUserForm.password"
|
||||
type="text"
|
||||
placeholder="Leer lassen für automatisches Passwort"
|
||||
/>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup label="Vorname für neues Profil">
|
||||
<UFormField label="Vorname für neues Profil">
|
||||
<UInput v-model="createUserForm.first_name" />
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup label="Nachname für neues Profil">
|
||||
<UFormField label="Nachname für neues Profil">
|
||||
<UInput v-model="createUserForm.last_name" />
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup label="Administrative Freigabe">
|
||||
<UFormField label="Administrative Freigabe">
|
||||
<div class="flex items-center gap-3 h-10">
|
||||
<UToggle v-model="createUserForm.is_admin" />
|
||||
<USwitch v-model="createUserForm.is_admin" />
|
||||
<span class="text-sm text-gray-600">Benutzer darf die Administration öffnen</span>
|
||||
</div>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup label="Multi-Tenant">
|
||||
<UFormField label="Multi-Tenant">
|
||||
<div class="flex items-center gap-3 h-10">
|
||||
<UToggle v-model="createUserForm.multiTenant" />
|
||||
<USwitch v-model="createUserForm.multiTenant" />
|
||||
<span class="text-sm text-gray-600">Mehrere Tenant-Zuordnungen erlauben</span>
|
||||
</div>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<div class="flex justify-end gap-3 pt-2">
|
||||
<UButton color="gray" variant="soft" @click="createUserModalOpen = false">
|
||||
@@ -793,28 +796,30 @@ onMounted(async () => {
|
||||
Benutzer anlegen
|
||||
</UButton>
|
||||
</div>
|
||||
</UForm>
|
||||
</UCard>
|
||||
</UForm>
|
||||
</UCard>
|
||||
</template>
|
||||
</UModal>
|
||||
|
||||
<UModal v-model="createTenantModalOpen">
|
||||
<UCard>
|
||||
<template #header>
|
||||
<div class="text-lg font-semibold">Tenant anlegen</div>
|
||||
</template>
|
||||
<UModal v-model:open="createTenantModalOpen">
|
||||
<template #content>
|
||||
<UCard>
|
||||
<template #header>
|
||||
<div class="text-lg font-semibold">Tenant anlegen</div>
|
||||
</template>
|
||||
|
||||
<UForm
|
||||
:state="createTenantForm"
|
||||
class="space-y-4"
|
||||
@submit.prevent="createTenant"
|
||||
>
|
||||
<UFormGroup label="Name">
|
||||
<UFormField label="Name">
|
||||
<UInput v-model="createTenantForm.name" />
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup label="Kürzel">
|
||||
<UFormField label="Kürzel">
|
||||
<UInput v-model="createTenantForm.short" />
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UAlert
|
||||
title="Seed-Daten"
|
||||
@@ -831,8 +836,9 @@ onMounted(async () => {
|
||||
Tenant anlegen
|
||||
</UButton>
|
||||
</div>
|
||||
</UForm>
|
||||
</UCard>
|
||||
</UForm>
|
||||
</UCard>
|
||||
</template>
|
||||
</UModal>
|
||||
|
||||
<div class="mx-5 mb-5">
|
||||
|
||||
@@ -68,7 +68,7 @@ const addAccount = async (account) => {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
toast.add({title: "Es gab einen Fehler beim Hinzufügen des Accounts", color:"rose"})
|
||||
toast.add({title: "Es gab einen Fehler beim Hinzufügen des Accounts", color:"error"})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ const updateAccount = async (account) => {
|
||||
setupPage()
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
toast.add({title: "Es gab einen Fehler beim Aktualisieren des Accounts", color:"rose"})
|
||||
toast.add({title: "Es gab einen Fehler beim Aktualisieren des Accounts", color:"error"})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,16 +101,17 @@ setupPage()
|
||||
+ Bankverbindung
|
||||
</UButton>
|
||||
<USlideover
|
||||
v-model="showAddBankRequisition"
|
||||
v-model:open="showAddBankRequisition"
|
||||
>
|
||||
<UCard
|
||||
class="h-full"
|
||||
>
|
||||
<template #header>
|
||||
<p>Bankverbindung hinzufügen</p>
|
||||
</template>
|
||||
<template #body>
|
||||
<UCard
|
||||
class="h-full"
|
||||
>
|
||||
<template #header>
|
||||
<p>Bankverbindung hinzufügen</p>
|
||||
</template>
|
||||
|
||||
<UFormGroup
|
||||
<UFormField
|
||||
label="BIC:"
|
||||
class="flex-auto"
|
||||
>
|
||||
@@ -127,7 +128,7 @@ setupPage()
|
||||
</UButton>
|
||||
</InputGroup>
|
||||
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
<UAlert
|
||||
v-if="showAlert && bankData.id && bankData.countries.includes('DE')"
|
||||
title="Bank gefunden"
|
||||
@@ -137,51 +138,54 @@ setupPage()
|
||||
class="mt-3"
|
||||
:actions="[{ variant: 'solid', color: 'primary', label: 'Verbinden',click: generateLink }]"
|
||||
/>
|
||||
<UAlert
|
||||
v-else-if="showAlert && !bankData.id"
|
||||
title="Bank nicht gefunden"
|
||||
icon="i-heroicons-x-circle"
|
||||
color="rose"
|
||||
variant="outline"
|
||||
class="mt-3"
|
||||
/>
|
||||
<UAlert
|
||||
v-else-if="showAlert && !bankData.id"
|
||||
title="Bank nicht gefunden"
|
||||
icon="i-heroicons-x-circle"
|
||||
color="error"
|
||||
variant="outline"
|
||||
class="mt-3"
|
||||
/>
|
||||
|
||||
</UCard>
|
||||
</UCard>
|
||||
</template>
|
||||
</USlideover>
|
||||
</template>
|
||||
</UDashboardNavbar>
|
||||
|
||||
<UModal v-model="showReqData">
|
||||
<UCard>
|
||||
<template #header>
|
||||
Verfügbare Bankkonten
|
||||
</template>
|
||||
<div
|
||||
v-for="account in reqData.accounts"
|
||||
:key="account.id"
|
||||
class="p-2 m-3 flex justify-between"
|
||||
>
|
||||
{{account.iban}} - {{account.owner_name}}
|
||||
<UModal v-model:open="showReqData">
|
||||
<template #content>
|
||||
<UCard>
|
||||
<template #header>
|
||||
Verfügbare Bankkonten
|
||||
</template>
|
||||
<div
|
||||
v-for="account in reqData.accounts"
|
||||
:key="account.id"
|
||||
class="p-2 m-3 flex justify-between"
|
||||
>
|
||||
{{account.iban}} - {{account.owner_name}}
|
||||
|
||||
<UButton
|
||||
@click="addAccount(account)"
|
||||
v-if="!bankaccounts.find(i => i.iban === account.iban)"
|
||||
>
|
||||
Hinzufügen
|
||||
</UButton>
|
||||
<UButton
|
||||
@click="updateAccount(account)"
|
||||
v-else
|
||||
>
|
||||
Aktualisieren
|
||||
</UButton>
|
||||
</div>
|
||||
</UCard>
|
||||
<UButton
|
||||
@click="addAccount(account)"
|
||||
v-if="!bankaccounts.find(i => i.iban === account.iban)"
|
||||
>
|
||||
Hinzufügen
|
||||
</UButton>
|
||||
<UButton
|
||||
@click="updateAccount(account)"
|
||||
v-else
|
||||
>
|
||||
Aktualisieren
|
||||
</UButton>
|
||||
</div>
|
||||
</UCard>
|
||||
</template>
|
||||
</UModal>
|
||||
|
||||
<UTable
|
||||
:rows="bankaccounts"
|
||||
:columns="[
|
||||
:data="bankaccounts"
|
||||
:columns="normalizeTableColumns([
|
||||
{
|
||||
key: 'expired',
|
||||
label: 'Aktiv'
|
||||
@@ -198,23 +202,23 @@ setupPage()
|
||||
key: 'balance',
|
||||
label: 'Saldo'
|
||||
},
|
||||
]"
|
||||
])"
|
||||
>
|
||||
<template #expired-data="{row}">
|
||||
<span v-if="row.expired" class="text-rose-600">Ausgelaufen</span>
|
||||
<template #expired-cell="{ row }">
|
||||
<span v-if="row.original.expired" class="text-error-600">Ausgelaufen</span>
|
||||
<span v-else class="text-primary">Aktiv</span>
|
||||
<UButton
|
||||
v-if="row.expired"
|
||||
v-if="row.original.expired"
|
||||
variant="outline"
|
||||
class="ml-2"
|
||||
@click="generateLink(row.bankId)"
|
||||
@click="generateLink(row.original.bankId)"
|
||||
>Aktualisieren</UButton>
|
||||
</template>
|
||||
<template #balance-data="{row}">
|
||||
{{row.balance ? row.balance.toFixed(2).replace(".",",") + ' €' : '-'}}
|
||||
<template #balance-cell="{ row }">
|
||||
{{ row.original.balance ? row.original.balance.toFixed(2).replace(".",",") + ' €' : '-' }}
|
||||
</template>
|
||||
<template #iban-data="{row}">
|
||||
{{row.iban.match(/.{1,5}/g).join(" ")}}
|
||||
<template #iban-cell="{ row }">
|
||||
{{ row.original.iban.match(/.{1,5}/g).join(" ") }}
|
||||
</template>
|
||||
</UTable>
|
||||
|
||||
|
||||
@@ -57,15 +57,15 @@ const saveAccount = async () => {
|
||||
</template>
|
||||
</UDashboardNavbar>
|
||||
<UForm class="w-2/3 mx-auto mt-5">
|
||||
<UFormGroup
|
||||
<UFormField
|
||||
label="E-Mail Adresse"
|
||||
>
|
||||
<UInput
|
||||
v-model="itemInfo.email"
|
||||
/>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup
|
||||
<UFormField
|
||||
label="Passwort"
|
||||
>
|
||||
<UInput
|
||||
@@ -73,61 +73,61 @@ const saveAccount = async () => {
|
||||
v-model="itemInfo.password"
|
||||
placeholder="********"
|
||||
/>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UDivider> IMAP </UDivider>
|
||||
<USeparator label="IMAP"/>
|
||||
|
||||
<UFormGroup
|
||||
<UFormField
|
||||
label="IMAP Host"
|
||||
>
|
||||
<UInput
|
||||
v-model="itemInfo.imap_host"
|
||||
/>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup
|
||||
<UFormField
|
||||
label="IMAP Port"
|
||||
>
|
||||
<UInput
|
||||
type="number"
|
||||
v-model="itemInfo.imap_port"
|
||||
/>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup
|
||||
<UFormField
|
||||
label="IMAP SSL"
|
||||
>
|
||||
<UToggle
|
||||
<USwitch
|
||||
v-model="itemInfo.imap_ssl"
|
||||
/>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UDivider> SMTP </UDivider>
|
||||
<USeparator label="SMTP"/>
|
||||
|
||||
<UFormGroup
|
||||
<UFormField
|
||||
label="SMTP Host"
|
||||
>
|
||||
<UInput
|
||||
v-model="itemInfo.smtp_host"
|
||||
/>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup
|
||||
<UFormField
|
||||
label="SMTP Port"
|
||||
>
|
||||
<UInput
|
||||
type="number"
|
||||
v-model="itemInfo.smtp_port"
|
||||
/>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup
|
||||
<UFormField
|
||||
label="SMTP SSL"
|
||||
>
|
||||
<UToggle
|
||||
<USwitch
|
||||
v-model="itemInfo.smtp_ssl"
|
||||
/>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
</UForm>
|
||||
</template>
|
||||
|
||||
@@ -32,22 +32,23 @@ const columns = computed(() => templateColumns.filter((column) => selectedColumn
|
||||
|
||||
<template>
|
||||
<UModal
|
||||
v-model="showEmailAddressModal"
|
||||
v-model:open="showEmailAddressModal"
|
||||
>
|
||||
<UCard>
|
||||
<template #header>
|
||||
E-Mail Adresse
|
||||
</template>
|
||||
<!-- <UFormGroup
|
||||
<template #content>
|
||||
<UCard>
|
||||
<template #header>
|
||||
E-Mail Adresse
|
||||
</template>
|
||||
<!-- <UFormField
|
||||
label="E-Mail Adresse:"
|
||||
>
|
||||
|
||||
</UFormGroup>-->
|
||||
</UFormField>-->
|
||||
|
||||
<UInput
|
||||
v-model="createEMailAddress"
|
||||
/>
|
||||
<!-- <UFormGroup
|
||||
<!-- <UFormField
|
||||
label="Account Typ:"
|
||||
>
|
||||
<USelectMenu
|
||||
@@ -56,15 +57,16 @@ const columns = computed(() => templateColumns.filter((column) => selectedColumn
|
||||
value-attribute="key"
|
||||
v-model="createEMailType"
|
||||
/>
|
||||
</UFormGroup>-->
|
||||
<template #footer>
|
||||
<UButton
|
||||
@click="createAccount"
|
||||
>
|
||||
Erstellen
|
||||
</UButton>
|
||||
</template>
|
||||
</UCard>
|
||||
</UFormField>-->
|
||||
<template #footer>
|
||||
<UButton
|
||||
@click="createAccount"
|
||||
>
|
||||
Erstellen
|
||||
</UButton>
|
||||
</template>
|
||||
</UCard>
|
||||
</template>
|
||||
|
||||
</UModal>
|
||||
<UDashboardNavbar title="E-Mail Konten">
|
||||
@@ -80,12 +82,12 @@ const columns = computed(() => templateColumns.filter((column) => selectedColumn
|
||||
</template>
|
||||
</UDashboardNavbar>
|
||||
<UTable
|
||||
:rows="items"
|
||||
:columns="columns"
|
||||
:data="items"
|
||||
:columns="normalizeTableColumns(columns)"
|
||||
class="w-full"
|
||||
@select="(i) => navigateTo(`/settings/emailaccounts/edit/${i.id}`)"
|
||||
:on-select="(i) => navigateTo(`/settings/emailaccounts/edit/${i.id}`)"
|
||||
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
|
||||
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine E-Mail Konten anzuzeigen' }"
|
||||
:empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine E-Mail Konten anzuzeigen' }"
|
||||
>
|
||||
</UTable>
|
||||
</template>
|
||||
|
||||
@@ -33,13 +33,13 @@ const labelPrinterURI = ref("")
|
||||
Etikettendrucker
|
||||
</template>
|
||||
|
||||
<UFormGroup
|
||||
<UFormField
|
||||
label="IP-Adresse:"
|
||||
>
|
||||
<UInput
|
||||
v-model="labelPrinterURI"
|
||||
/>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UButton
|
||||
@click="setupPrinter"
|
||||
|
||||
@@ -37,11 +37,11 @@ const isLight = computed({
|
||||
:items="items"
|
||||
class="h-100 p-5"
|
||||
>
|
||||
<template #item="{item}">
|
||||
<template #content="{item}">
|
||||
<UCard class="mt-5">
|
||||
<div v-if="item.label === 'Profil'">
|
||||
|
||||
<UDivider
|
||||
<USeparator
|
||||
class="my-3"
|
||||
label="Profil"
|
||||
/>
|
||||
@@ -58,12 +58,12 @@ const isLight = computed({
|
||||
|
||||
</div>
|
||||
<div v-else-if="item.label === 'Projekte'">
|
||||
<UDivider
|
||||
<USeparator
|
||||
label="Phasenvorlagen"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="item.label === 'Dokumente'">
|
||||
<UDivider
|
||||
<USeparator
|
||||
label="Tags"
|
||||
class="mb-3"
|
||||
/>
|
||||
@@ -86,4 +86,4 @@ const isLight = computed({
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -80,7 +80,7 @@ const updateNumberRanges = async (range) => {
|
||||
<UDashboardToolbar>
|
||||
<UAlert
|
||||
title="Änderungen an diesen Werten betreffen nur neu Erstellte Einträge."
|
||||
color="rose"
|
||||
color="error"
|
||||
variant="outline"
|
||||
icon="i-heroicons-exclamation-triangle"
|
||||
/>
|
||||
|
||||
@@ -168,11 +168,13 @@ setupPage()
|
||||
}
|
||||
]"
|
||||
>
|
||||
<template #item="{item}">
|
||||
<template #content="{item}">
|
||||
<div v-if="item.label === 'Dokubox'">
|
||||
<UAlert
|
||||
class="mt-5"
|
||||
title="DOKUBOX"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
>
|
||||
<template #description>
|
||||
<p>Die Dokubox ist eine E-Mail Inbox um deine Anhänge direkt als Dokumente zu importieren. Leite Deine E-Mails einfach an die folgende E-Mail Adresse weiter, diese ist für dein Unternehmen einzigartig. Die E-Mails werden dann alle 5 min abgerufen.</p>
|
||||
@@ -190,17 +192,17 @@ setupPage()
|
||||
<div v-if="item.label === 'Rechnung & Kontakt'">
|
||||
<UCard class="mt-5">
|
||||
<UForm class="w-1/2">
|
||||
<UFormGroup
|
||||
<UFormField
|
||||
label="Firmenname:"
|
||||
>
|
||||
<UInput v-model="businessInfo.name"/>
|
||||
</UFormGroup>
|
||||
<UFormGroup
|
||||
</UFormField>
|
||||
<UFormField
|
||||
label="Straße + Hausnummer:"
|
||||
>
|
||||
<UInput v-model="businessInfo.street"/>
|
||||
</UFormGroup>
|
||||
<UFormGroup
|
||||
</UFormField>
|
||||
<UFormField
|
||||
label="PLZ + Ort"
|
||||
class="w-full"
|
||||
>
|
||||
@@ -208,41 +210,41 @@ setupPage()
|
||||
<UInput v-model="businessInfo.zip"/>
|
||||
<UInput v-model="businessInfo.city" class="flex-auto"/>
|
||||
</InputGroup>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
<UButton
|
||||
class="mt-3"
|
||||
@click="updateTenant({businessInfo: businessInfo})"
|
||||
>
|
||||
Speichern
|
||||
</UButton>
|
||||
<UFormGroup
|
||||
<UFormField
|
||||
label="Kontenrahmen:"
|
||||
class="mt-6"
|
||||
>
|
||||
<USelectMenu
|
||||
v-model="accountChart"
|
||||
:options="accountChartOptions"
|
||||
option-attribute="label"
|
||||
value-attribute="value"
|
||||
:items="accountChartOptions"
|
||||
label-key="label"
|
||||
value-key="value"
|
||||
/>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
<UButton
|
||||
class="mt-3"
|
||||
@click="updateTenant({accountChart: accountChart})"
|
||||
>
|
||||
Kontenrahmen speichern
|
||||
</UButton>
|
||||
<UFormGroup
|
||||
<UFormField
|
||||
label="USt-Auswertung:"
|
||||
class="mt-6"
|
||||
>
|
||||
<USelectMenu
|
||||
v-model="taxEvaluationPeriod"
|
||||
:options="TAX_EVALUATION_PERIOD_OPTIONS"
|
||||
option-attribute="label"
|
||||
value-attribute="value"
|
||||
:items="TAX_EVALUATION_PERIOD_OPTIONS"
|
||||
label-key="label"
|
||||
value-key="value"
|
||||
/>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
<UButton
|
||||
class="mt-3"
|
||||
@click="updateTenant({taxEvaluationPeriod: taxEvaluationPeriod})"
|
||||
@@ -258,7 +260,7 @@ setupPage()
|
||||
<UAlert
|
||||
title="Funktionen ausblenden"
|
||||
description="Nur Funktionen mit gesetztem Haken sind im Unternehmen verfügbar. Diese Einstellungen gelten für alle Mitarbeiter und sind unabhängig von Berechtigungen."
|
||||
color="rose"
|
||||
color="error"
|
||||
variant="outline"
|
||||
class="mb-5"
|
||||
/>
|
||||
|
||||
@@ -49,7 +49,7 @@ const refreshData = async () => {
|
||||
// select() filtert bereits archivierte Einträge, wenn dataType.isArchivable true ist
|
||||
texttemplates.value = await select()
|
||||
} catch (e) {
|
||||
toast.add({ title: 'Fehler beim Laden', description: e.message, color: 'rose' })
|
||||
toast.add({ title: 'Fehler beim Laden', description: e.message, color: 'error' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -93,7 +93,7 @@ const handleCreate = async () => {
|
||||
editTemplateModalOpen.value = false
|
||||
await refreshData()
|
||||
} catch (e) {
|
||||
toast.add({ title: 'Fehler', description: 'Konnte nicht erstellt werden.', color: 'rose' })
|
||||
toast.add({ title: 'Fehler', description: 'Konnte nicht erstellt werden.', color: 'error' })
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
}
|
||||
@@ -109,7 +109,7 @@ const handleUpdate = async () => {
|
||||
editTemplateModalOpen.value = false
|
||||
await refreshData()
|
||||
} catch (e) {
|
||||
toast.add({title: 'Fehler', description: 'Konnte nicht gespeichert werden.', color: 'rose'})
|
||||
toast.add({title: 'Fehler', description: 'Konnte nicht gespeichert werden.', color: 'error'})
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
}
|
||||
@@ -122,7 +122,7 @@ const handleArchive = async (row) => {
|
||||
|
||||
await refreshData()
|
||||
} catch (e) {
|
||||
toast.add({title: 'Fehler', description: 'Konnte nicht archiviert werden.', color: 'rose'})
|
||||
toast.add({title: 'Fehler', description: 'Konnte nicht archiviert werden.', color: 'error'})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,45 +160,45 @@ const getDocLabel = (type) => {
|
||||
|
||||
<UTable
|
||||
class="mt-3"
|
||||
:rows="texttemplates"
|
||||
:data="texttemplates"
|
||||
:loading="loading"
|
||||
v-model:expand="expand"
|
||||
:empty-state="{ icon: 'i-heroicons-document-text', label: 'Keine Textvorlagen gefunden' }"
|
||||
:columns="[
|
||||
:empty="{ icon: 'i-heroicons-document-text', label: 'Keine Textvorlagen gefunden' }"
|
||||
:columns="normalizeTableColumns([
|
||||
{ key: 'name', label: 'Bezeichnung' },
|
||||
{ key: 'documentType', label: 'Verwendung' },
|
||||
{ key: 'pos', label: 'Position' },
|
||||
{ key: 'default', label: 'Standard' },
|
||||
{ key: 'actions', label: '' }
|
||||
]"
|
||||
])"
|
||||
>
|
||||
<template #name-data="{ row }">
|
||||
<span class="font-medium text-gray-900 dark:text-white">{{ row.name }}</span>
|
||||
<template #name-cell="{ row }">
|
||||
<span class="font-medium text-gray-900 dark:text-white">{{ row.original.name }}</span>
|
||||
</template>
|
||||
|
||||
<template #documentType-data="{ row }">
|
||||
<template #documentType-cell="{ row }">
|
||||
<UBadge color="gray" variant="soft">
|
||||
{{ getDocLabel(row.documentType) }}
|
||||
{{ getDocLabel(row.original.documentType) }}
|
||||
</UBadge>
|
||||
</template>
|
||||
|
||||
<template #pos-data="{ row }">
|
||||
<template #pos-cell="{ row }">
|
||||
<div class="flex items-center gap-2">
|
||||
<UIcon
|
||||
:name="row.pos === 'startText' ? 'i-heroicons-bars-arrow-down' : 'i-heroicons-bars-arrow-up'"
|
||||
:name="row.original.pos === 'startText' ? 'i-heroicons-bars-arrow-down' : 'i-heroicons-bars-arrow-up'"
|
||||
class="w-4 h-4 text-gray-500"
|
||||
/>
|
||||
<span>{{ row.pos === 'startText' ? 'Einleitung' : 'Endtext' }}</span>
|
||||
<span>{{ row.original.pos === 'startText' ? 'Einleitung' : 'Endtext' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #default-data="{ row }">
|
||||
<UIcon v-if="row.default" name="i-heroicons-check-circle-20-solid" class="text-green-500"/>
|
||||
<template #default-cell="{ row }">
|
||||
<UIcon v-if="row.original.default" name="i-heroicons-check-circle-20-solid" class="text-green-500"/>
|
||||
<span v-else class="text-gray-400">-</span>
|
||||
</template>
|
||||
|
||||
<template #actions-data="{ row }">
|
||||
<UButton color="gray" variant="ghost" icon="i-heroicons-pencil-square" @click="openModal(row)"/>
|
||||
<template #actions-cell="{ row }">
|
||||
<UButton color="gray" variant="ghost" icon="i-heroicons-pencil-square" @click="openModal(row.original)"/>
|
||||
</template>
|
||||
|
||||
<template #expand="{ row }">
|
||||
@@ -206,13 +206,13 @@ const getDocLabel = (type) => {
|
||||
<div class="mb-4">
|
||||
<h4 class="text-sm font-bold uppercase text-gray-500 mb-1">Vorschau</h4>
|
||||
<p class="text-gray-800 dark:text-gray-200 whitespace-pre-line p-3 bg-white dark:bg-gray-900 rounded border border-gray-200 dark:border-gray-700 text-sm">
|
||||
{{ row.text }}
|
||||
{{ row.original.text }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-3">
|
||||
<ButtonWithConfirm
|
||||
color="rose"
|
||||
color="error"
|
||||
variant="soft"
|
||||
icon="i-heroicons-archive-box"
|
||||
@confirmed="handleArchive(row)"
|
||||
@@ -236,26 +236,27 @@ const getDocLabel = (type) => {
|
||||
</UTable>
|
||||
</UDashboardPanelContent>
|
||||
|
||||
<UModal v-model="editTemplateModalOpen" :ui="{ width: 'sm:max-w-4xl' }">
|
||||
<UCard>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="text-lg font-semibold">
|
||||
{{ itemInfo.id ? 'Vorlage bearbeiten' : 'Neue Vorlage erstellen' }}
|
||||
</h3>
|
||||
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark" @click="editTemplateModalOpen = false"/>
|
||||
</div>
|
||||
</template>
|
||||
<UModal v-model:open="editTemplateModalOpen" :ui="{ width: 'sm:max-w-4xl' }">
|
||||
<template #content>
|
||||
<UCard>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="text-lg font-semibold">
|
||||
{{ itemInfo.id ? 'Vorlage bearbeiten' : 'Neue Vorlage erstellen' }}
|
||||
</h3>
|
||||
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark" @click="editTemplateModalOpen = false"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
|
||||
<div class="lg:col-span-2 space-y-4">
|
||||
<UFormGroup label="Bezeichnung" required>
|
||||
<UFormField label="Bezeichnung" required>
|
||||
<UInput v-model="itemInfo.name" placeholder="z.B. Standard Angebotstext" icon="i-heroicons-tag"/>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<UFormGroup label="Dokumententyp" required>
|
||||
<UFormField label="Dokumententyp" required>
|
||||
<USelectMenu
|
||||
v-model="itemInfo.documentType"
|
||||
:options="Object.keys(dataStore.documentTypesForCreation || {})
|
||||
@@ -264,9 +265,9 @@ const getDocLabel = (type) => {
|
||||
option-attribute="label"
|
||||
value-attribute="key"
|
||||
/>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup label="Position" required>
|
||||
<UFormField label="Position" required>
|
||||
<USelectMenu
|
||||
v-model="itemInfo.pos"
|
||||
:options="[
|
||||
@@ -276,10 +277,10 @@ const getDocLabel = (type) => {
|
||||
option-attribute="label"
|
||||
value-attribute="key"
|
||||
/>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<UFormGroup label="Text Inhalt" required help="Klicken Sie rechts auf eine Variable, um sie einzufügen.">
|
||||
<UFormField label="Text Inhalt" required help="Klicken Sie rechts auf eine Variable, um sie einzufügen.">
|
||||
<UTextarea
|
||||
ref="textareaRef"
|
||||
v-model="itemInfo.text"
|
||||
@@ -287,7 +288,7 @@ const getDocLabel = (type) => {
|
||||
placeholder="Sehr geehrte Damen und Herren..."
|
||||
class="font-mono text-sm"
|
||||
/>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UCheckbox v-model="itemInfo.default" label="Als Standard für diesen Typ verwenden"/>
|
||||
</div>
|
||||
@@ -342,34 +343,35 @@ const getDocLabel = (type) => {
|
||||
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-3">
|
||||
<UButton color="gray" variant="ghost" @click="editTemplateModalOpen = false">
|
||||
Abbrechen
|
||||
</UButton>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-3">
|
||||
<UButton color="gray" variant="ghost" @click="editTemplateModalOpen = false">
|
||||
Abbrechen
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
v-if="!itemInfo.id"
|
||||
color="primary"
|
||||
:loading="isSaving"
|
||||
@click="handleCreate"
|
||||
icon="i-heroicons-plus"
|
||||
>
|
||||
Erstellen
|
||||
</UButton>
|
||||
<UButton
|
||||
v-if="!itemInfo.id"
|
||||
color="primary"
|
||||
:loading="isSaving"
|
||||
@click="handleCreate"
|
||||
icon="i-heroicons-plus"
|
||||
>
|
||||
Erstellen
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
v-else
|
||||
color="primary"
|
||||
:loading="isSaving"
|
||||
@click="handleUpdate"
|
||||
icon="i-heroicons-check"
|
||||
>
|
||||
Speichern
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
<UButton
|
||||
v-else
|
||||
color="primary"
|
||||
:loading="isSaving"
|
||||
@click="handleUpdate"
|
||||
icon="i-heroicons-check"
|
||||
>
|
||||
Speichern
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
</template>
|
||||
</UModal>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -179,99 +179,99 @@ onMounted(fetchProfile)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UDivider label="Persönliche Daten" />
|
||||
<USeparator label="Persönliche Daten" />
|
||||
|
||||
<UForm :state="profile" @submit.prevent="saveProfile" class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-4">
|
||||
<UFormGroup label="Vorname">
|
||||
<UFormField label="Vorname">
|
||||
<UInput v-model="profile.first_name" />
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup label="Nachname">
|
||||
<UFormField label="Nachname">
|
||||
<UInput v-model="profile.last_name" />
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup label="E-Mail">
|
||||
<UFormField label="E-Mail">
|
||||
<UInput v-model="profile.email" />
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup label="Telefon (Mobil)">
|
||||
<UFormField label="Telefon (Mobil)">
|
||||
<UInput v-model="profile.mobile_tel" />
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup label="Telefon (Festnetz)">
|
||||
<UFormField label="Telefon (Festnetz)">
|
||||
<UInput v-model="profile.fixed_tel" />
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup label="Geburtstag">
|
||||
<UFormField label="Geburtstag">
|
||||
<div class="flex items-center gap-2">
|
||||
<UInput type="date" v-model="profile.birthday" class="flex-1" />
|
||||
<UButton color="gray" variant="soft" label="Heute" @click="setProfileDate('birthday')" />
|
||||
</div>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
</UForm>
|
||||
</UCard>
|
||||
<UCard v-if="!pending && profile" class="mt-3">
|
||||
<UDivider label="Vertragsinformationen" />
|
||||
<USeparator label="Vertragsinformationen" />
|
||||
|
||||
<UForm :state="profile" @submit.prevent="saveProfile" class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-4">
|
||||
<UFormGroup label="Vertragsart">
|
||||
<UFormField label="Vertragsart">
|
||||
<UInput v-model="profile.contract_type"/>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup label="Status">
|
||||
<UFormField label="Status">
|
||||
<UInput v-model="profile.status"/>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup label="Position">
|
||||
<UFormField label="Position">
|
||||
<UInput v-model="profile.position"/>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup label="Qualifikation">
|
||||
<UFormField label="Qualifikation">
|
||||
<UInput v-model="profile.qualification"/>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup label="Eintrittsdatum">
|
||||
<UFormField label="Eintrittsdatum">
|
||||
<div class="flex items-center gap-2">
|
||||
<UInput type="date" v-model="profile.entry_date" class="flex-1" />
|
||||
<UButton color="gray" variant="soft" label="Heute" @click="setProfileDate('entry_date')" />
|
||||
</div>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup label="Wöchentliche Arbeitszeit (Std)">
|
||||
<UFormField label="Wöchentliche Arbeitszeit (Std)">
|
||||
<UInput type="number" v-model="profile.weekly_working_hours" />
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup label="Bezahlte Urlaubstage (Jahr)">
|
||||
<UFormField label="Bezahlte Urlaubstage (Jahr)">
|
||||
<UInput type="number" v-model="profile.annual_paid_leave_days" />
|
||||
</UFormGroup>
|
||||
<UFormGroup label="Aktiv">
|
||||
</UFormField>
|
||||
<UFormField label="Aktiv">
|
||||
<div class="flex items-center gap-3">
|
||||
<UToggle v-model="profile.active" color="primary" />
|
||||
<USwitch v-model="profile.active" color="primary" />
|
||||
<span class="text-sm text-gray-600">
|
||||
</span>
|
||||
</div>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
</UForm>
|
||||
|
||||
</UCard>
|
||||
|
||||
<UCard v-if="!pending && profile" class="mt-3">
|
||||
<UDivider label="Adresse & Standort" />
|
||||
<USeparator label="Adresse & Standort" />
|
||||
|
||||
<UForm :state="profile" @submit.prevent="saveProfile" class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-4">
|
||||
<UFormGroup label="Straße und Hausnummer">
|
||||
<UFormField label="Straße und Hausnummer">
|
||||
<UInput v-model="profile.address_street"/>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup label="PLZ">
|
||||
<UFormField label="PLZ">
|
||||
<UInput type="text" v-model="profile.address_zip" @focusout="checkZip"/>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup label="Ort">
|
||||
<UFormField label="Ort">
|
||||
<UInput v-model="profile.address_city"/>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup label="Bundesland">
|
||||
<UFormField label="Bundesland">
|
||||
<USelectMenu
|
||||
v-model="profile.state_code"
|
||||
:options="bundeslaender"
|
||||
@@ -279,13 +279,13 @@ onMounted(fetchProfile)
|
||||
option-attribute="name"
|
||||
placeholder="Bundesland auswählen"
|
||||
/>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
</UForm>
|
||||
</UCard>
|
||||
|
||||
|
||||
<UCard v-if="!pending && profile" class="mt-3">
|
||||
<UDivider label="Wöchentliche Arbeitsstunden" />
|
||||
<USeparator label="Wöchentliche Arbeitsstunden" />
|
||||
|
||||
<div class="mt-4 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div
|
||||
@@ -313,23 +313,23 @@ onMounted(fetchProfile)
|
||||
</UCard>
|
||||
|
||||
<UCard v-if="!pending && profile" class="mt-3">
|
||||
<UDivider label="Sonstiges" />
|
||||
<USeparator label="Sonstiges" />
|
||||
<UForm :state="profile" @submit.prevent="saveProfile" class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-4">
|
||||
<UFormGroup label="Kleidergröße (Oberteil)">
|
||||
<UFormField label="Kleidergröße (Oberteil)">
|
||||
<UInput v-model="profile.clothing_size_top" />
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup label="Kleidergröße (Hose)">
|
||||
<UFormField label="Kleidergröße (Hose)">
|
||||
<UInput v-model="profile.clothing_size_bottom" />
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup label="Schuhgröße">
|
||||
<UFormField label="Schuhgröße">
|
||||
<UInput v-model="profile.clothing_size_shoe" />
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup label="Token-ID">
|
||||
<UFormField label="Token-ID">
|
||||
<UInput v-model="profile.token_id" />
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
</UForm>
|
||||
</UCard>
|
||||
|
||||
|
||||
@@ -39,9 +39,9 @@
|
||||
</template>
|
||||
</UDashboardNavbar>
|
||||
<UTable
|
||||
:rows="items"
|
||||
:columns="columns"
|
||||
@select="(i) => navigateTo(`/staff/profiles/${i.id}`)"
|
||||
:data="items"
|
||||
:columns="normalizeTableColumns(columns)"
|
||||
:on-select="(i) => navigateTo(`/staff/profiles/${i.id}`)"
|
||||
>
|
||||
|
||||
</UTable>
|
||||
@@ -49,4 +49,4 @@
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -29,7 +29,16 @@ const platformIsNative = ref(false)
|
||||
const selectedPresetRange = ref("Dieser Monat bis heute")
|
||||
const selectedStartDay = ref("")
|
||||
const selectedEndDay = ref("")
|
||||
const openTab = ref(0)
|
||||
const openTab = ref("0")
|
||||
const presetRangeItems = [
|
||||
'Dieser Monat bis heute',
|
||||
'Diese Woche',
|
||||
'Dieser Monat',
|
||||
'Dieses Jahr',
|
||||
'Letzte Woche',
|
||||
'Letzter Monat',
|
||||
'Letztes Jahr'
|
||||
]
|
||||
|
||||
const showDocument = ref(false)
|
||||
const uri = ref("")
|
||||
@@ -131,7 +140,7 @@ async function loadWorkingTimeInfo() {
|
||||
|
||||
workingTimeInfo.value = data;
|
||||
|
||||
openTab.value = 0
|
||||
openTab.value = "0"
|
||||
}
|
||||
|
||||
// 📄 PDF generieren
|
||||
@@ -172,12 +181,12 @@ async function saveFile() {
|
||||
toast.add({title:"Auswertung erfolgreich gespeichert"})
|
||||
fileSaved.value = true
|
||||
} catch (error) {
|
||||
toast.add({title:"Fehler beim Speichern der Auswertung", color: "rose"})
|
||||
toast.add({title:"Fehler beim Speichern der Auswertung", color: "error"})
|
||||
}
|
||||
}
|
||||
|
||||
async function onTabChange(index: number) {
|
||||
if (index === 1) await generateDocument()
|
||||
async function onTabChange(index: string | number) {
|
||||
if (String(index) === "1") await generateDocument()
|
||||
}
|
||||
|
||||
// Initialisierung
|
||||
@@ -204,52 +213,44 @@ await setupPage()
|
||||
</template>
|
||||
</UDashboardNavbar>
|
||||
|
||||
<UDashboardToolbar>
|
||||
<UDashboardToolbar class="py-3">
|
||||
<template #left>
|
||||
<UFormGroup label="Zeitraum:">
|
||||
<UFormField label="Zeitraum:">
|
||||
<USelectMenu
|
||||
:options="[
|
||||
'Dieser Monat bis heute',
|
||||
'Diese Woche',
|
||||
'Dieser Monat',
|
||||
'Dieses Jahr',
|
||||
'Letzte Woche',
|
||||
'Letzter Monat',
|
||||
'Letztes Jahr'
|
||||
]"
|
||||
:items="presetRangeItems"
|
||||
v-model="selectedPresetRange"
|
||||
@change="changeRange"
|
||||
/>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup label="Start:">
|
||||
<UPopover :popper="{ placement: 'bottom-start' }">
|
||||
<UFormField label="Start:">
|
||||
<UPopover :content="{ side: 'bottom', align: 'start' }">
|
||||
<UButton
|
||||
icon="i-heroicons-calendar-days-20-solid"
|
||||
:label="selectedStartDay ? $dayjs(selectedStartDay).format('DD.MM.YYYY') : 'Datum wählen'"
|
||||
/>
|
||||
<template #panel="{ close }">
|
||||
<template #content>
|
||||
<LazyDatePicker v-model="selectedStartDay" @close="loadWorkingTimeInfo" />
|
||||
</template>
|
||||
</UPopover>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup label="Ende:">
|
||||
<UPopover :popper="{ placement: 'bottom-start' }">
|
||||
<UFormField label="Ende:">
|
||||
<UPopover :content="{ side: 'bottom', align: 'start' }">
|
||||
<UButton
|
||||
icon="i-heroicons-calendar-days-20-solid"
|
||||
:label="selectedEndDay ? $dayjs(selectedEndDay).format('DD.MM.YYYY') : 'Datum wählen'"
|
||||
/>
|
||||
<template #panel="{ close }">
|
||||
<template #content>
|
||||
<LazyDatePicker v-model="selectedEndDay" @close="loadWorkingTimeInfo" />
|
||||
</template>
|
||||
</UPopover>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
</template>
|
||||
<template #right>
|
||||
<UTooltip
|
||||
:text="fileSaved ? 'Bericht bereits gespeichert' : 'Bericht speichern'"
|
||||
v-if="openTab === 1 && uri"
|
||||
v-if="openTab === '1' && uri"
|
||||
>
|
||||
<UButton
|
||||
icon="i-mdi-content-save"
|
||||
@@ -265,9 +266,9 @@ await setupPage()
|
||||
<UTabs
|
||||
:items="[{ label: 'Information' }, { label: 'Bericht' }]"
|
||||
v-model="openTab"
|
||||
@change="onTabChange"
|
||||
@update:model-value="onTabChange"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
<template #content="{ item }">
|
||||
<div v-if="item.label === 'Information'">
|
||||
|
||||
<UCard v-if="workingTimeInfo && workingTimeInfo.summary" class="my-5">
|
||||
@@ -294,39 +295,39 @@ await setupPage()
|
||||
<UDashboardPanel>
|
||||
<UTable
|
||||
v-if="workingTimeInfo"
|
||||
:rows="workingTimeInfo.spans"
|
||||
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Anwesenheiten' }"
|
||||
:columns="[
|
||||
:data="workingTimeInfo.spans"
|
||||
:empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Anwesenheiten' }"
|
||||
:columns="normalizeTableColumns([
|
||||
{ key: 'status', label: 'Status' },
|
||||
{ key: 'startedAt', label: 'Start' },
|
||||
{ key: 'endedAt', label: 'Ende' },
|
||||
{ key: 'duration', label: 'Dauer' },
|
||||
{ key: 'type', label: 'Typ' }
|
||||
]"
|
||||
@select="(row) => router.push(`/workingtimes/edit/${row.sourceEventIds[0]}`)"
|
||||
])"
|
||||
:on-select="(row) => router.push(`/workingtimes/edit/${row.original.sourceEventIds[0]}`)"
|
||||
>
|
||||
<template #status-data="{row}">
|
||||
<span v-if="row.status === 'approved'" class="text-primary-500">Genehmigt</span>
|
||||
<span v-else-if="row.status === 'submitted'" class="text-cyan-500">Eingereicht</span>
|
||||
<span v-else-if="row.status === 'factual'" class="text-gray-500">Faktisch</span>
|
||||
<span v-else-if="row.status === 'draft'" class="text-red-500">Entwurf</span>
|
||||
<span v-else>{{ row.status }}</span>
|
||||
<template #status-cell="{ row }">
|
||||
<span v-if="row.original.status === 'approved'" class="text-primary-500">Genehmigt</span>
|
||||
<span v-else-if="row.original.status === 'submitted'" class="text-cyan-500">Eingereicht</span>
|
||||
<span v-else-if="row.original.status === 'factual'" class="text-gray-500">Faktisch</span>
|
||||
<span v-else-if="row.original.status === 'draft'" class="text-error-500">Entwurf</span>
|
||||
<span v-else>{{ row.original.status }}</span>
|
||||
</template>
|
||||
|
||||
<template #startedAt-data="{ row }">
|
||||
{{ $dayjs(row.startedAt).format('HH:mm DD.MM.YY') }} Uhr
|
||||
<template #startedAt-cell="{ row }">
|
||||
{{ $dayjs(row.original.startedAt).format('HH:mm DD.MM.YY') }} Uhr
|
||||
</template>
|
||||
|
||||
<template #endedAt-data="{ row }">
|
||||
{{ $dayjs(row.endedAt).format('HH:mm DD.MM.YY') }} Uhr
|
||||
<template #endedAt-cell="{ row }">
|
||||
{{ $dayjs(row.original.endedAt).format('HH:mm DD.MM.YY') }} Uhr
|
||||
</template>
|
||||
|
||||
<template #duration-data="{ row }">
|
||||
{{ formatSpanDuration(row.startedAt, row.endedAt) }}
|
||||
<template #duration-cell="{ row }">
|
||||
{{ formatSpanDuration(row.original.startedAt, row.original.endedAt) }}
|
||||
</template>
|
||||
|
||||
<template #type-data="{ row }">
|
||||
{{ row.type.charAt(0).toUpperCase() + row.type.slice(1).replace('_', ' ') }}
|
||||
<template #type-cell="{ row }">
|
||||
{{ row.original.type.charAt(0).toUpperCase() + row.original.type.slice(1).replace('_', ' ') }}
|
||||
</template>
|
||||
</UTable>
|
||||
</UDashboardPanel>
|
||||
@@ -360,15 +361,7 @@ await setupPage()
|
||||
<div class="p-4 space-y-4 border-b bg-white dark:bg-gray-900">
|
||||
<USelectMenu
|
||||
v-model="selectedPresetRange"
|
||||
:options="[
|
||||
'Dieser Monat bis heute',
|
||||
'Diese Woche',
|
||||
'Dieser Monat',
|
||||
'Dieses Jahr',
|
||||
'Letzte Woche',
|
||||
'Letzter Monat',
|
||||
'Letztes Jahr'
|
||||
]"
|
||||
:items="presetRangeItems"
|
||||
@change="changeRange"
|
||||
placeholder="Zeitraum wählen"
|
||||
class="w-full"
|
||||
@@ -377,13 +370,13 @@ await setupPage()
|
||||
<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' }">
|
||||
<UPopover :content="{ side: 'bottom', align: 'start' }">
|
||||
<UButton
|
||||
icon="i-heroicons-calendar"
|
||||
class="w-full"
|
||||
:label="$dayjs(selectedStartDay).format('DD.MM.YYYY')"
|
||||
/>
|
||||
<template #panel>
|
||||
<template #content>
|
||||
<LazyDatePicker v-model="selectedStartDay" @close="loadWorkingTimeInfo" />
|
||||
</template>
|
||||
</UPopover>
|
||||
@@ -391,13 +384,13 @@ await setupPage()
|
||||
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 mb-1">Ende</p>
|
||||
<UPopover :popper="{ placement: 'bottom-start' }">
|
||||
<UPopover :content="{ side: 'bottom', align: 'start' }">
|
||||
<UButton
|
||||
icon="i-heroicons-calendar"
|
||||
class="w-full"
|
||||
:label="$dayjs(selectedEndDay).format('DD.MM.YYYY')"
|
||||
/>
|
||||
<template #panel>
|
||||
<template #content>
|
||||
<LazyDatePicker v-model="selectedEndDay" @close="loadWorkingTimeInfo" />
|
||||
</template>
|
||||
</UPopover>
|
||||
@@ -408,12 +401,11 @@ await setupPage()
|
||||
<UTabs
|
||||
:items="[{ label: 'Information' }, { label: 'Bericht' }]"
|
||||
v-model="openTab"
|
||||
@change="onTabChange"
|
||||
@update:model-value="onTabChange"
|
||||
class="mt-3 mx-3"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
|
||||
<div v-if="item.label === 'Information'" class="space-y-4">
|
||||
<template #content="{ item }">
|
||||
<div v-if="item.label === 'Information'" class="space-y-4">
|
||||
|
||||
<UCard v-if="workingTimeInfo && workingTimeInfo.summary" class="mt-3">
|
||||
<template #header>
|
||||
@@ -459,7 +451,7 @@ await setupPage()
|
||||
|
||||
<div v-else-if="item.label === 'Bericht'">
|
||||
<UButton
|
||||
v-if="uri && !fileSaved"
|
||||
v-if="openTab === '1' && uri && !fileSaved"
|
||||
icon="i-mdi-content-save"
|
||||
color="primary"
|
||||
class="w-full mb-3"
|
||||
@@ -479,4 +471,4 @@ await setupPage()
|
||||
</template>
|
||||
|
||||
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -32,6 +32,10 @@ const rejectReason = ref("")
|
||||
const users = ref([])
|
||||
const selectedUser = ref(auth.user.id)
|
||||
const canViewAll = computed(() => auth.permissions.includes('staff.time.read_all'))
|
||||
const userItems = computed(() => users.value.map(u => ({
|
||||
label: u.full_name || u.email,
|
||||
value: u.user_id
|
||||
})))
|
||||
|
||||
// DATA
|
||||
const entries = ref([])
|
||||
@@ -61,7 +65,7 @@ const typeLabel = {
|
||||
const typeColor = {
|
||||
work: "gray",
|
||||
vacation: "yellow",
|
||||
sick: "rose",
|
||||
sick: "error",
|
||||
holiday: "blue",
|
||||
other: "gray"
|
||||
}
|
||||
@@ -130,7 +134,7 @@ async function confirmReject() {
|
||||
showRejectModal.value = false
|
||||
await load()
|
||||
} catch (e) {
|
||||
toast.add({ title: 'Fehler beim Ablehnen', description: e.message, color: 'red' })
|
||||
toast.add({ title: 'Fehler beim Ablehnen', description: e.message, color: 'error' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
entryToReject.value = null
|
||||
@@ -167,10 +171,10 @@ onMounted(async () => {
|
||||
<div v-if="canViewAll" class="flex items-center gap-2">
|
||||
<USelectMenu
|
||||
v-model="selectedUser"
|
||||
:options="users.map(u => ({ label: u.full_name || u.email, value: u.user_id }))"
|
||||
:items="userItems"
|
||||
placeholder="Benutzer auswählen"
|
||||
value-attribute="value"
|
||||
option-attribute="label"
|
||||
value-key="value"
|
||||
label-key="label"
|
||||
class="min-w-[220px]"
|
||||
:clearable="false"
|
||||
/>
|
||||
@@ -211,11 +215,11 @@ onMounted(async () => {
|
||||
</div>
|
||||
|
||||
<template v-if="isViewingSelf">
|
||||
<UButton v-if="active" color="red" icon="i-heroicons-stop" :loading="loading" label="Stoppen" @click="handleStop" />
|
||||
<UButton v-if="active" color="error" 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" />
|
||||
</template>
|
||||
<template v-else-if="active && canViewAll">
|
||||
<UButton color="red" variant="soft" icon="i-heroicons-stop" :loading="loading" label="Mitarbeiter stoppen" @click="handleStop" />
|
||||
<UButton color="error" variant="soft" icon="i-heroicons-stop" :loading="loading" label="Mitarbeiter stoppen" @click="handleStop" />
|
||||
</template>
|
||||
|
||||
<UButton color="gray" variant="solid" icon="i-heroicons-plus" label="Erfassen" @click="() => { entryToEdit = null; showEditModal = true }" />
|
||||
@@ -227,8 +231,8 @@ onMounted(async () => {
|
||||
|
||||
<UCard v-if="view === 'list'" :ui="{ body: { padding: 'p-0 sm:p-0' } }">
|
||||
<UTable
|
||||
:rows="entries"
|
||||
:columns="[
|
||||
:data="entries"
|
||||
:columns="normalizeTableColumns([
|
||||
{ key: 'actions', label: 'Aktionen', class: 'w-32' },
|
||||
{ key: 'state', label: 'Status' },
|
||||
{ key: 'started_at', label: 'Start' },
|
||||
@@ -236,50 +240,50 @@ onMounted(async () => {
|
||||
{ key: 'duration_minutes', label: 'Dauer' },
|
||||
{ key: 'type', label: 'Typ' },
|
||||
{ key: 'description', label: 'Beschreibung' },
|
||||
]"
|
||||
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Zeiten anzuzeigen' }"
|
||||
])"
|
||||
:empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Zeiten anzuzeigen' }"
|
||||
>
|
||||
<template #state-data="{ row }">
|
||||
<UBadge v-if="row.state === 'approved'" color="green" variant="subtle">Genehmigt</UBadge>
|
||||
<UBadge v-else-if="row.state === 'submitted'" color="cyan" variant="subtle">Eingereicht</UBadge>
|
||||
<UBadge v-else-if="row.state === 'rejected'" color="red" variant="subtle">Abgelehnt</UBadge>
|
||||
<template #state-cell="{ row }">
|
||||
<UBadge v-if="row.original.state === 'approved'" color="green" variant="subtle">Genehmigt</UBadge>
|
||||
<UBadge v-else-if="row.original.state === 'submitted'" color="cyan" variant="subtle">Eingereicht</UBadge>
|
||||
<UBadge v-else-if="row.original.state === 'rejected'" color="error" variant="subtle">Abgelehnt</UBadge>
|
||||
<UBadge v-else color="gray" variant="subtle">Entwurf</UBadge>
|
||||
</template>
|
||||
<template #type-data="{ row }">
|
||||
<UBadge :color="typeColor[row.type] || 'gray'" variant="soft">{{ typeLabel[row.type] || row.type }}</UBadge>
|
||||
<template #type-cell="{ row }">
|
||||
<UBadge :color="typeColor[row.original.type] || 'gray'" variant="soft">{{ typeLabel[row.original.type] || row.original.type }}</UBadge>
|
||||
</template>
|
||||
<template #started_at-data="{ row }">
|
||||
<span v-if="['vacation','sick'].includes(row.type)">{{ useNuxtApp().$dayjs(row.started_at).format("DD.MM.YY") }}</span>
|
||||
<span v-else>{{ useNuxtApp().$dayjs(row.started_at).format("DD.MM.YY HH:mm") }}</span>
|
||||
<template #started_at-cell="{ row }">
|
||||
<span v-if="['vacation','sick'].includes(row.original.type)">{{ useNuxtApp().$dayjs(row.original.started_at).format("DD.MM.YY") }}</span>
|
||||
<span v-else>{{ useNuxtApp().$dayjs(row.original.started_at).format("DD.MM.YY HH:mm") }}</span>
|
||||
</template>
|
||||
<template #stopped_at-data="{ row }">
|
||||
<span v-if="!row.stopped_at" class="text-primary-500 font-medium animate-pulse">läuft...</span>
|
||||
<span v-else-if="['vacation','sick'].includes(row.type)">{{ useNuxtApp().$dayjs(row.stopped_at).format("DD.MM.YY") }}</span>
|
||||
<span v-else>{{ useNuxtApp().$dayjs(row.stopped_at).format("DD.MM.YY HH:mm") }}</span>
|
||||
<template #stopped_at-cell="{ row }">
|
||||
<span v-if="!row.original.stopped_at" class="text-primary-500 font-medium animate-pulse">läuft...</span>
|
||||
<span v-else-if="['vacation','sick'].includes(row.original.type)">{{ useNuxtApp().$dayjs(row.original.stopped_at).format("DD.MM.YY") }}</span>
|
||||
<span v-else>{{ useNuxtApp().$dayjs(row.original.stopped_at).format("DD.MM.YY HH:mm") }}</span>
|
||||
</template>
|
||||
<template #duration_minutes-data="{ row }">
|
||||
{{ row.duration_minutes ? useFormatDuration(row.duration_minutes) : "-" }}
|
||||
<template #duration_minutes-cell="{ row }">
|
||||
{{ row.original.duration_minutes ? useFormatDuration(row.original.duration_minutes) : "-" }}
|
||||
</template>
|
||||
<template #actions-data="{ row }">
|
||||
<template #actions-cell="{ row }">
|
||||
<div class="flex items-center gap-1">
|
||||
<UTooltip text="Einreichen" v-if="(row.state === 'draft' || row.state === 'factual') && row.stopped_at">
|
||||
<UButton size="xs" color="cyan" variant="ghost" icon="i-heroicons-paper-airplane" @click="handleSubmit(row)" :loading="loading" />
|
||||
<UTooltip text="Einreichen" v-if="(row.original.state === 'draft' || row.original.state === 'factual') && row.original.stopped_at">
|
||||
<UButton size="xs" color="cyan" variant="ghost" icon="i-heroicons-paper-airplane" @click="handleSubmit(row.original)" :loading="loading" />
|
||||
</UTooltip>
|
||||
<UTooltip text="Genehmigen" v-if="row.state === 'submitted' && canViewAll">
|
||||
<UButton size="xs" color="green" variant="ghost" icon="i-heroicons-check" @click="handleApprove(row)" :loading="loading" />
|
||||
<UTooltip text="Genehmigen" v-if="row.original.state === 'submitted' && canViewAll">
|
||||
<UButton size="xs" color="green" variant="ghost" icon="i-heroicons-check" @click="handleApprove(row.original)" :loading="loading" />
|
||||
</UTooltip>
|
||||
<UTooltip text="Ablehnen" v-if="(row.state === 'submitted' || row.state === 'approved') && canViewAll">
|
||||
<UButton size="xs" color="red" variant="ghost" icon="i-heroicons-x-mark" @click="openRejectModal(row)" :loading="loading" />
|
||||
<UTooltip text="Ablehnen" v-if="(row.original.state === 'submitted' || row.original.state === 'approved') && canViewAll">
|
||||
<UButton size="xs" color="error" variant="ghost" icon="i-heroicons-x-mark" @click="openRejectModal(row.original)" :loading="loading" />
|
||||
</UTooltip>
|
||||
<UTooltip text="Bearbeiten" v-if="['draft', 'factual', 'submitted'].includes(row.state)">
|
||||
<UButton size="xs" color="gray" variant="ghost" icon="i-heroicons-pencil-square" @click="handleEdit(row)" />
|
||||
<UTooltip text="Bearbeiten" v-if="['draft', 'factual', 'submitted'].includes(row.original.state)">
|
||||
<UButton size="xs" color="gray" variant="ghost" icon="i-heroicons-pencil-square" @click="handleEdit(row.original)" />
|
||||
</UTooltip>
|
||||
</div>
|
||||
</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 #description-cell="{ row }">
|
||||
<span v-if="row.original.type === 'vacation'">{{ row.original.vacation_reason }}</span>
|
||||
<span v-else-if="row.original.type === 'sick'">{{ row.original.sick_reason }}</span>
|
||||
<span v-else>{{ row.original.description }}</span>
|
||||
</template>
|
||||
</UTable>
|
||||
</UCard>
|
||||
@@ -334,7 +338,7 @@ onMounted(async () => {
|
||||
|
||||
<UBadge v-if="entry.state === 'approved'" color="green" size="xs" variant="solid">Genehmigt</UBadge>
|
||||
<UBadge v-else-if="entry.state === 'submitted'" color="cyan" size="xs" variant="solid">Eingereicht</UBadge>
|
||||
<UBadge v-else-if="entry.state === 'rejected'" color="red" size="xs" variant="solid">Abgelehnt</UBadge>
|
||||
<UBadge v-else-if="entry.state === 'rejected'" color="error" size="xs" variant="solid">Abgelehnt</UBadge>
|
||||
<UBadge v-else color="gray" size="xs" variant="subtle">Entwurf</UBadge>
|
||||
</div>
|
||||
</div>
|
||||
@@ -363,7 +367,7 @@ onMounted(async () => {
|
||||
|
||||
<UButton
|
||||
v-if="(entry.state === 'submitted' || entry.state === 'approved') && canViewAll"
|
||||
size="xs" color="red" variant="ghost" icon="i-heroicons-x-mark" label="Ablehnen"
|
||||
size="xs" color="error" variant="ghost" icon="i-heroicons-x-mark" label="Ablehnen"
|
||||
@click="openRejectModal(entry)" :loading="loading"
|
||||
/>
|
||||
|
||||
@@ -403,7 +407,7 @@ onMounted(async () => {
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<template v-if="isViewingSelf">
|
||||
<UButton v-if="active" color="red" icon="i-heroicons-stop" :loading="loading" @click="handleStop" />
|
||||
<UButton v-if="active" color="error" icon="i-heroicons-stop" :loading="loading" @click="handleStop" />
|
||||
<UButton v-else color="green" icon="i-heroicons-play" :loading="loading" @click="handleStart" />
|
||||
</template>
|
||||
</div>
|
||||
@@ -422,7 +426,7 @@ onMounted(async () => {
|
||||
</div>
|
||||
<UBadge v-if="row.state === 'approved'" color="green">Genehmigt</UBadge>
|
||||
<UBadge v-else-if="row.state === 'submitted'" color="cyan">Eingereicht</UBadge>
|
||||
<UBadge v-else-if="row.state === 'rejected'" color="red">Abgelehnt</UBadge>
|
||||
<UBadge v-else-if="row.state === 'rejected'" color="error">Abgelehnt</UBadge>
|
||||
<UBadge v-else color="gray">Entwurf</UBadge>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 mt-1">Start: {{ useNuxtApp().$dayjs(row.started_at).format('DD.MM.YY HH:mm') }}</p>
|
||||
@@ -444,30 +448,32 @@ onMounted(async () => {
|
||||
:default-user-id="selectedUser"
|
||||
/>
|
||||
|
||||
<UModal v-model="showRejectModal">
|
||||
<UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
||||
Zeiteintrag ablehnen
|
||||
</h3>
|
||||
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="showRejectModal = false" />
|
||||
<UModal v-model:open="showRejectModal">
|
||||
<template #content>
|
||||
<UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
||||
Zeiteintrag ablehnen
|
||||
</h3>
|
||||
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="showRejectModal = false" />
|
||||
</div>
|
||||
</template>
|
||||
<div class="space-y-4">
|
||||
<p class="text-sm text-gray-500">
|
||||
Der Eintrag wird als "Rejected" markiert und nicht mehr zur Arbeitszeit gezählt.
|
||||
</p>
|
||||
<UFormField label="Grund (optional)" name="reason">
|
||||
<UTextarea v-model="rejectReason" placeholder="Falsche Buchung, Doppelt, etc." autofocus />
|
||||
</UFormField>
|
||||
</div>
|
||||
</template>
|
||||
<div class="space-y-4">
|
||||
<p class="text-sm text-gray-500">
|
||||
Der Eintrag wird als "Rejected" markiert und nicht mehr zur Arbeitszeit gezählt.
|
||||
</p>
|
||||
<UFormGroup label="Grund (optional)" name="reason">
|
||||
<UTextarea v-model="rejectReason" placeholder="Falsche Buchung, Doppelt, etc." autofocus />
|
||||
</UFormGroup>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<UButton color="gray" variant="soft" @click="showRejectModal = false">Abbrechen</UButton>
|
||||
<UButton color="red" :loading="loading" @click="confirmReject">Bestätigen</UButton>
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<UButton color="gray" variant="soft" @click="showRejectModal = false">Abbrechen</UButton>
|
||||
<UButton color="error" :loading="loading" @click="confirmReject">Bestätigen</UButton>
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
</template>
|
||||
</UModal>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -73,6 +73,24 @@ const sort = ref({
|
||||
column: dataType.sortColumn || "created_at",
|
||||
direction: 'desc'
|
||||
})
|
||||
const sorting = computed({
|
||||
get: () => [{
|
||||
id: sort.value.column,
|
||||
desc: sort.value.direction === 'desc'
|
||||
}],
|
||||
set: (value) => {
|
||||
const nextSort = Array.isArray(value) ? value[0] : undefined
|
||||
if (!nextSort?.id) {
|
||||
return
|
||||
}
|
||||
|
||||
sort.value = {
|
||||
column: nextSort.id,
|
||||
direction: nextSort.desc ? 'desc' : 'asc'
|
||||
}
|
||||
setupPage()
|
||||
}
|
||||
})
|
||||
|
||||
const columnsToFilter = ref({})
|
||||
|
||||
@@ -282,6 +300,33 @@ const handleFilterChange = async (action,column) => {
|
||||
setupPage()
|
||||
}
|
||||
|
||||
const truncateValue = (value, maxLength) => {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return '\u00A0'
|
||||
}
|
||||
|
||||
const stringValue = String(value)
|
||||
if (!maxLength || stringValue.length <= maxLength) {
|
||||
return stringValue
|
||||
}
|
||||
|
||||
return `${stringValue.substring(0, maxLength)}...`
|
||||
}
|
||||
|
||||
const getDistinctFilterItems = (columnKey) => {
|
||||
return (itemsMeta.value?.distinctValues?.[columnKey] || []).map((value) => ({
|
||||
label: String(value),
|
||||
value
|
||||
}))
|
||||
}
|
||||
|
||||
const isDistinctFilterActive = (columnKey) => {
|
||||
const available = itemsMeta.value?.distinctValues?.[columnKey] || []
|
||||
const selected = columnsToFilter.value[columnKey] || []
|
||||
|
||||
return selected.length > 0 && selected.length !== available.length
|
||||
}
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
@@ -313,7 +358,7 @@ const handleFilterChange = async (action,column) => {
|
||||
<UButton
|
||||
icon="i-heroicons-x-mark"
|
||||
variant="outline"
|
||||
color="rose"
|
||||
color="error"
|
||||
@click="clearSearchString()"
|
||||
|
||||
/>
|
||||
@@ -336,11 +381,11 @@ const handleFilterChange = async (action,column) => {
|
||||
<template #left>
|
||||
<UTooltip :text="`${dataType.label} pro Seite`">
|
||||
<USelectMenu
|
||||
: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}]"
|
||||
:items="[{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"
|
||||
value-key="value"
|
||||
label-key="value"
|
||||
@update:model-value="setupPage"
|
||||
/>
|
||||
</UTooltip>
|
||||
<UPagination
|
||||
@@ -363,15 +408,15 @@ const handleFilterChange = async (action,column) => {
|
||||
<USelectMenu
|
||||
v-model="selectedColumns"
|
||||
icon="i-heroicons-adjustments-horizontal-solid"
|
||||
:options="dataType.templateColumns.filter(i => !i.disabledInTable)"
|
||||
:items="dataType.templateColumns.filter(i => !i.disabledInTable)"
|
||||
multiple
|
||||
class="hidden lg:block"
|
||||
by="key"
|
||||
:color="selectedColumns.length !== dataType.templateColumns.filter(i => !i.disabledInTable).length ? 'primary' : 'white'"
|
||||
:ui-menu="{ width: 'min-w-max' }"
|
||||
@change="tempStore.modifyColumns(type,selectedColumns)"
|
||||
:content="{ width: 'min-w-max' }"
|
||||
@update:model-value="tempStore.modifyColumns(type, selectedColumns)"
|
||||
>
|
||||
<template #label>
|
||||
<template #default>
|
||||
Spalten
|
||||
</template>
|
||||
</USelectMenu>
|
||||
@@ -382,18 +427,15 @@ const handleFilterChange = async (action,column) => {
|
||||
<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-model:sorting="sorting"
|
||||
v-if="dataType && columns && items.length > 0 && !loading"
|
||||
:rows="items"
|
||||
:columns="columns"
|
||||
:data="items"
|
||||
:columns="normalizeTableColumns(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` }"
|
||||
:on-select="(row) => router.push(`/standardEntity/${type}/show/${row.original.id}`)"
|
||||
:empty="`Keine ${dataType.label} anzuzeigen`"
|
||||
>
|
||||
<template
|
||||
v-for="column in dataType.templateColumns.filter(i => !i.disabledInTable)"
|
||||
@@ -414,33 +456,26 @@ const handleFilterChange = async (action,column) => {
|
||||
: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]"
|
||||
class="min-w-0"
|
||||
:items="getDistinctFilterItems(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
|
||||
@update:model-value="handleFilterChange('change', column.key)"
|
||||
:search-input="{ placeholder: 'Suche...' }"
|
||||
value-key="value"
|
||||
label-key="label"
|
||||
:content="{ width: 'min-w-max' }"
|
||||
:disabled="getDistinctFilterItems(column.key).length === 0"
|
||||
>
|
||||
|
||||
<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'"
|
||||
>
|
||||
<template #default="slotProps">
|
||||
<span class="inline-flex min-w-0 items-center">
|
||||
<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>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
|
||||
</USelectMenu>
|
||||
</UTooltip>
|
||||
<UButton
|
||||
@@ -458,7 +493,7 @@ const handleFilterChange = async (action,column) => {
|
||||
<UButton
|
||||
@click="handleFilterChange('reset',column.key)"
|
||||
variant="outline"
|
||||
color="rose"
|
||||
color="error"
|
||||
>
|
||||
X
|
||||
</UButton>
|
||||
@@ -468,48 +503,54 @@ const handleFilterChange = async (action,column) => {
|
||||
</InputGroup>
|
||||
|
||||
</template>
|
||||
<template #name-data="{row}">
|
||||
<template #name-cell="{row}">
|
||||
<span
|
||||
v-if="row.id === items[selectedItem].id"
|
||||
class="text-primary-500 font-bold">
|
||||
v-if="row.original.id === items[selectedItem]?.id"
|
||||
class="block truncate text-primary-500 font-bold">
|
||||
<UTooltip
|
||||
:text="row.name"
|
||||
:text="row.original.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}}
|
||||
<span class="block truncate">
|
||||
{{ truncateValue(row.original.name, dataType.templateColumns.find(i => i.key === "name")?.maxLength) }}
|
||||
</span>
|
||||
</UTooltip> </span>
|
||||
<span v-else>
|
||||
<span v-else class="block truncate">
|
||||
<UTooltip
|
||||
:text="row.name"
|
||||
:text="row.original.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}}
|
||||
<span class="block truncate">
|
||||
{{ truncateValue(row.original.name, dataType.templateColumns.find(i => i.key === "name")?.maxLength) }}
|
||||
</span>
|
||||
</UTooltip>
|
||||
</span>
|
||||
</template>
|
||||
<template #fullName-data="{row}">
|
||||
<template #fullName-cell="{row}">
|
||||
<span
|
||||
v-if="row.id === items[selectedItem].id"
|
||||
class="text-primary-500 font-bold">{{row.fullName}}
|
||||
v-if="row.original.id === items[selectedItem]?.id"
|
||||
class="block truncate text-primary-500 font-bold">{{ row.original.fullName }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{row.fullName}}
|
||||
<span v-else class="block truncate">
|
||||
{{ row.original.fullName }}
|
||||
</span>
|
||||
</template>
|
||||
<template #licensePlate-data="{row}">
|
||||
<template #licensePlate-cell="{row}">
|
||||
<span
|
||||
v-if="row.id === items[selectedItem].id"
|
||||
class="text-primary-500 font-bold">{{row.licensePlate}}
|
||||
v-if="row.original.id === items[selectedItem]?.id"
|
||||
class="block truncate text-primary-500 font-bold">{{ row.original.licensePlate }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{row.licensePlate}}
|
||||
<span v-else class="block truncate">
|
||||
{{ row.original.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]">
|
||||
<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 : ''}`: ''}}
|
||||
v-slot:[`${column.key}-cell`]="{row}">
|
||||
<component v-if="column.component" :is="column.component" :row="row.original"></component>
|
||||
<span v-else-if="row.original[column.key]" class="block truncate">
|
||||
<UTooltip :text="String(row.original[column.key])">
|
||||
<span class="block truncate">
|
||||
{{ `${truncateValue(row.original[column.key], column.maxLength)}${column.unit ? ` ${column.unit}` : ''}` }}
|
||||
</span>
|
||||
</UTooltip>
|
||||
</span>
|
||||
|
||||
@@ -550,7 +591,7 @@ const handleFilterChange = async (action,column) => {
|
||||
v-if="searchString.length > 0"
|
||||
icon="i-heroicons-x-mark"
|
||||
variant="ghost"
|
||||
color="rose"
|
||||
color="error"
|
||||
@click="clearSearchString()"
|
||||
|
||||
/>
|
||||
@@ -639,70 +680,75 @@ const handleFilterChange = async (action,column) => {
|
||||
</UDashboardPanelContent>
|
||||
<!-- Mobile Filter Slideover -->
|
||||
<USlideover
|
||||
v-model="showMobileFilter"
|
||||
v-model:open="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%' }"
|
||||
<template #body>
|
||||
<!-- 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>
|
||||
|
||||
</div>
|
||||
<!-- Scrollable content -->
|
||||
<div class="flex-1 overflow-y-auto p-4 space-y-6">
|
||||
|
||||
<!-- 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>
|
||||
<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>
|
||||
|
||||
<UButton
|
||||
color="primary"
|
||||
class="flex-1"
|
||||
@click="applyMobileFilters"
|
||||
<USelectMenu
|
||||
v-model="columnsToFilter[column.key]"
|
||||
:items="getDistinctFilterItems(column.key)"
|
||||
multiple
|
||||
value-key="value"
|
||||
label-key="label"
|
||||
:search-input="{ placeholder: `${column.label} filtern...` }"
|
||||
:filter-fields="['label']"
|
||||
placeholder="Auswählen…"
|
||||
:content="{ width: 'w-full' }"
|
||||
class="w-full"
|
||||
/>
|
||||
</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
|
||||
"
|
||||
>
|
||||
Anwenden
|
||||
</UButton>
|
||||
</div>
|
||||
<UButton
|
||||
color="error"
|
||||
variant="outline"
|
||||
class="flex-1"
|
||||
@click="resetMobileFilters"
|
||||
>
|
||||
Zurücksetzen
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
color="primary"
|
||||
class="flex-1"
|
||||
@click="applyMobileFilters"
|
||||
>
|
||||
Anwenden
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
</USlideover>
|
||||
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user